How to normalise ZonedDateTime so that .equals() works? - java

I have code, similar to this:
import java.time._
object app {
def main (args :Array[String]) = {
println("app started")
// create two ZonedDateTime objects for 1st Jan 2018, 10am UTC
// using separate methods
val zdt1 = ZonedDateTime.of(2018, 1, 1, 10, 0, 0, 0, ZoneId.of("UTC"))
val zdt2 = ZonedDateTime.parse("2018-01-01T10:00:00Z")
println(s"COMPARING: $zdt1 and $zdt2")
println("== check: " + (zdt1 == zdt2))
println(".equals check: " + (zdt1.equals(zdt2)))
println(".isEqual check " + (zdt1.isEqual(zdt2)))
println("app finished")
}
}
Code available here: https://ideone.com/43zf8B
The issue:
these ARE both typed ZonedDateTime objects
they are equivalent according to the .isEqual() method..
they are not equivalent according to .equals() method
However my test suite uses deep matching using beEquals
operations against the classes these datetime instances are
in, therefore I need a way to normalise them so that
.equals() returns true.
how can I normalise them please?

If I create zdt1 with ZonedDateTime.of(2018, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC), the two objects are equal under equals() (still not under == in Java).
Apparently it’s not enough for the zones to be equivalent when their names are different. By using ZoneOffset.UTC for constructing the first ZonedDateTime, both will have the same time zone and will thus be equal. With my change, at least on my Mac, zdt1.getZone() == zdt2.getZone() now evaluates to true.
As a more direct answer to your question, you may normalize your ZonedDateTime objects this way (Java syntax with semicolon, please translate yourself):
zdt1 = zdt1.withZoneSameInstant(zdt1.getZone().normalized());
Similarly for zdt2, of course. ZoneId.normalized() promises to return a ZoneOffset where possible, which it is in your case. So in your case it does make two objects that are equal under equals(). I’m not sure it will in all other cases.
A safer way would be to have the comparison explicitly take care of different but equal time zones:
zdt1.toInstant().equals(zdt2.toInstant())
&& zdt1.getZone().getRules().equals(zdt2.getZone().getRules())
This evaluates to true with your two date-times from the question.
BTW isEqual() compares the instants in time only, not the zones at all, which is why it didn’t care.

ZoneOffset.UTC
What about the predefined constant ZoneOffset.UTC?
val zdt1 = ZonedDateTime.of(2018, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC)
val zdt2 = ZonedDateTime.parse("2018-01-01T10:00:00Z")
All three methods return true(==, equals and isEqual)

Related

java.time Comparison Is Not Consistent

I'm using LocalData objects in my code, and I've noticed that using
compareTo method is not consistent.
I use the compareTo method in order to get the difference (in days)
between the two dates.
However, it seems like it only works for the dates in the same months.
I'm attaching a code snippet to demonstrate this issue:
import java.text.MessageFormat;
import java.time.LocalDate;
public class timeComparison {
public static void main(String[] args) {
LocalDate valentinesDay = LocalDate.of(2020, 2, 14);
LocalDate darwinDay = LocalDate.of(2020, 2, 12);
LocalDate leapDay = LocalDate.of(2020, 2, 29);
LocalDate superTuesday = LocalDate.of(2020, 3, 3);
String valentinesVsDarwin = MessageFormat.format(
"Valentine day is {0} days after Darwin day",
valentinesDay.compareTo(darwinDay)
);
System.out.println(valentinesVsDarwin);
String darwinVsLeap = MessageFormat.format(
"Leap day is {0} days after Darwin day",
leapDay.compareTo(darwinDay)
);
System.out.println(darwinVsLeap);
String superTuesdayVsLeap = MessageFormat.format(
"Super Tuesday is {0} days after leap day",
superTuesday.compareTo(leapDay)
);
System.out.println(superTuesdayVsLeap);
}
}
The output I get is:
Valentine day is 2 days after Darwin day
Leap day is 17 days after Darwin day
Super Tuesday is 1 days after leap day
I was expecting to get Super Tuesday is 3 days after leap day.
I would like to know what causes the problem
and how can I get the difference between two separate dates.
TL;DR
The compareTo method is not meant for this use.
It is meant to be an indicator of order, not show time differences.
Solving the Mystery
In order to understand this better one can look at the source code of LocalDate.
On any IDE you can choose "Go To => Implementation" to view the source code.
public int compareTo(ChronoLocalDate other) {
return other instanceof LocalDate ? this.compareTo0((LocalDate)other) : super.compareTo(other);
}
int compareTo0(LocalDate otherDate) {
int cmp = this.year - otherDate.year;
if (cmp == 0) {
cmp = this.month - otherDate.month;
if (cmp == 0) {
cmp = this.day - otherDate.day;
}
}
return cmp;
}
From the above source code, you can learn that the compareTo method
does not return the difference between the date in days, but rather the
difference between dates in the first parameter that has a difference
(it could be years, could be months, or could be days).
What Is the Purpose of compareTo Methods
In order the understand the above code and its purpose,
one would need to understand a little bit how comparison in Java works.
Java's LocalDate class implements ChronoLocalDate,
which is a comparable object.
This means that this object is capable of comparing itself with another object.
In order to do so, the class itself must implement the java.lang.Comparable
interface to compare its instances.
As described in Java's API documentation:
This interface imposes a total ordering on the objects of each class that
implements it. This ordering is referred to as the class's natural ordering,
and the class's compareTo method is referred to as its natural
comparison method.
Moreover, the method detail of the compareTo method is the following:
Compares this object with the specified object for order.
Returns a negative integer, zero, or a positive integer as this object
is less than, equal to, or greater than the specified object.
Meaning, that in order for the comparison to work we need to return an integer that corresponds with the above ruling.
Therefore, the number itself doesn't have any meaning,
rather than being an indication of the "order" of the objects.
That is why the method, as shown above, does not calculate the difference in days but rather seeks the immediate indication for the natural order of the objects: first in years, then in months, and in the ends in days.
When you were comparing dates that had a different mount - the method showed just that.
The same would apply if you were to compare between a date in
2020 and a date in 2018 - you would get 2 / -2 as a result.
How You Should Measure Data Difference
We found out that compareTo isn't meant to be used as a measurement tool of date difference.
You can use either Period or ChronoUnit, depending on your needs.
From Java's Period and Duration tutorial:
ChronoUnit
The ChronoUnit enum, discussed in the The Temporal Package, defines the
units used to measure time.
The ChronoUnit.between method is useful when
you want to measure an amount of time in a single unit of time only,
such as days or seconds.
The between method works with all temporal-based
objects, but it returns the amount in a single unit only.
Period
To define an amount of time with date-based values (years, months, days),
use the Period class.
The Period class provides various get methods,
such as getMonths, getDays, and getYears, so that you can extract the
amount of time from the period.
The total period of time is represented by all three units together:
months, days, and years.
To present the amount of time measured in a single
unit of time, such as days, you can use the ChronoUnit.between method.
Here's a code snippet demonstrating the usage of the mentioned above methods:
jshell> import java.time.LocalDate;
jshell> LocalDate darwinDay = LocalDate.of(2020, 2, 12);
darwinDay ==> 2020-02-12
jshell> LocalDate leapDay = LocalDate.of(2020, 2, 29);
leapDay ==> 2020-02-29
jshell> leapDay.compareTo(darwinDay);
==> 17
jshell> import java.time.Period;
jshell> Period.between(darwinDay, leapDay).getDays();
==> 17
jshell> import java.time.temporal.ChronoUnit;
jshell> ChronoUnit.DAYS.between(darwinDay, leapDay);
==> 17
jshell> LocalDate now = LocalDate.of(2020, 3, 28);
now ==> 2020-03-28
jshell> LocalDate yearAgo = LocalDate.of(2019, 3, 28);
yearAgo ==> 2019-03-28
jshell> yearAgo.compareTo(now);
==> -1
jshell> Period.between(yearAgo, now).getDays();
==> 0
jshell> ChronoUnit.DAYS.between(yearAgo, now);
==> 366
I was expecting to get Super Tuesday is 3 days after leap day.
This was a wrong expectation. See how Java compared two instances of LocalDate:
int compareTo0(LocalDate otherDate) {
int cmp = (year - otherDate.year);
if (cmp == 0) {
cmp = (month - otherDate.month);
if (cmp == 0) {
cmp = (day - otherDate.day);
}
}
return cmp;
}
Since the year was same (2020), it gave you the difference between months which is 3 - 2 = 1.
You should use java.time.temporal.ChronoUnit::between to find the number of days between two instances of LocalDate.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
System.out.println(ChronoUnit.DAYS.between(LocalDate.of(2020, 2, 29), LocalDate.of(2020, 4, 3)));
}
}
Output:
34

Java Vs C# Long to DateTime Conversion

In Java I have the following test that passes fine
// 42 bits of time is good enough for the next 100 years.
// An IEEE double has 52 bits of mantissa, so our dates can be easily fit.
#Test
public void testMaxBits() throws ParseException {
// Maximum 42 bit integer
long millis = (1L << 42) - 1;
Date date = new Date(millis);
//DateTime maxDate = new DateTime(2109, 5, 15, 8, 35, 11, 103);
Date maxDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse("2109-05-15T08:35:11.103");
Assert.assertEquals(maxDate, date);
}
Now, I want to do the same sort of thing in C#, so I have a test in LinqPAD that test the C# implementation for correctness
DateTime maxDate = new DateTime(2109, 5, 15, 8, 35, 11, 103);
long beginTicks = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
long l = (1L << 42) - 1;
DateTime date = new DateTime(beginTicks + l, DateTimeKind.Utc);
maxDate.Dump();
date.Dump();
The output don't match, the values outputted ToString() values are
maxDate = 15/05/2109 08:35:11
date = 06/01/1970 02:10:04
What am I missing here?
Edit. I have see a great answer below from #zmitrok, I have changed
DateTime date = new DateTime(beginTicks + l, DateTimeKind.Utc);
to
DateTime date = new DateTime(beginTicks +
l * TimeSpan.TicksPerMillisecond, DateTimeKind.Utc);
but now get
date = 15/05/2109 07:35:11
Where has the hour gone?
Your test is basically confusing ticks with milliseconds. If you only need to store a number of milliseconds since the unix epoch, then do so - but I'd recommend using something like this to perform the conversion:
public static readonly DateTime UnixEpoch
= new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public DateTime FromMillisecondsSinceUnixEpoch(long milliseconds)
{
return UnixEpoch.AddMilliseconds(milliseconds);
}
(As a side-note, that method already exists in my Noda Time project... hint hint :)
Your test would then be:
[TestMethod]
public void TestMaxBits()
{
long maxMillis = (1L << 42) - 1;
DateTime maxDate = DateTimeHelper.FromMillisecondsSinceUnixEpoch(maxMillis);
Assert.Greater(maxDate, new DateTime(2100, 1, 1, 0, 0, 0));
}
Note that:
This code doesn't mention ticks at all, because you're not interested in ticks
This code doesn't assert that the maximum date is some very specific value, because that's not what you care about; you care that 42 bits of time will carry you until the end of the century. (The "next 100 years" comment is somewhat specious, as 2109 is less than 100 years away from now, so I'll assume it really means "until the end of the 21st century.")
That of course make your question of "Where has the hour gone?" irrelevant - but the answer to that is simply that SimpleDateFormat defaults to using the system time zone, so you're actually relying on the time zone of the system you're running the test on, which is a really bad idea. If you set the time zone of the SimpleDateFormat to UTC, you'll find that it's 07:35:11 in Java as well.
The constructor you are using takes ticks as the first argument, however you are passing a value that was added to milliseconds.
Ticks: A date and time expressed in the number of 100-nanosecond intervals that have elapsed since January 1, 0001 at 00:00:00.000 in the Gregorian calendar.
I think you need to multiply ticks by this constant: https://msdn.microsoft.com/en-us/library/system.timespan.tickspermillisecond%28v=vs.110%29.aspx

java 8 - ZonedDateTime not equal another ZonedDateTime

I created two ZonedDateTime objects and I think they are should be equal:
public static void main(String[] args) {
ZoneId zid = ZoneId.of("America/New_York");
ZoneOffset offset = ZoneOffset.from(LocalDateTime.now().atZone(zid));
ZonedDateTime zdt0 = ZonedDateTime.of(2014, 8, 24, 21, 10, 1, 777000002, offset);
ZonedDateTime zdt1 = ZonedDateTime.of(2014, 8, 24, 21, 10, 1, 777000002, zid);
boolean equals = Objects.equals(zdt0, zdt1);
System.out.println("equals: " + equals);
}
In debugger I see that class of member of ZonedDateTime zone in first case is java.time.ZoneOffset and in second java.time.ZoneRegion and this is makes ZonedDateTime objects not equal. This is confusing...
Any ideas?
You are checking for object equality which evaluates to false as these objects are not equivalent. One is bound to a ZoneId, the other to a ZoneOffset. If you want to check whether they represent the same time, you can use the not very intuitively named method isEqual.
E.g.:
ZoneId zid = ZoneId.of("America/New_York");
ZoneOffset offset = ZoneOffset.from(LocalDateTime.now().atZone(zid));
ZonedDateTime zdt0 = ZonedDateTime.of(2014, 8, 24, 21, 10, 1, 777000002, offset);
ZonedDateTime zdt1 = ZonedDateTime.of(2014, 8, 24, 21, 10, 1, 777000002, zid);
System.out.println("isEqual:" + zdt0.isEqual(zdt1));
System.out.println("equals: " + zdt0.equals(zdt1));
prints:
isEqual:true
equals: false
Btw, note that you don’t need to use Objects.equals(a,b) for two objects you already know to be non-null. You can invoke a.equals(b) directly.
This played hell on me for hours too when using Jackson to serialize / deserialize instances of ZonedDateTime and then compare them against each other for equality to verify that my code was working correctly. I don't fully understand the implications but all I've learned is to use isEqual instead of equals. But this throws a big wrench in testing plans as most assertion utilities will just call the standard .equals().
Here's what I finally came up with after struggling for quite some time:
#Test
public void zonedDateTimeCorrectlyRestoresItself() {
// construct a new instance of ZonedDateTime
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Z"));
// offset = {ZoneOffset#3820} "Z"
// zone = {ZoneOffset#3820} "Z"
String starting = now.toString();
// restore an instance of ZonedDateTime from String
ZonedDateTime restored = ZonedDateTime.parse(starting);
// offset = {ZoneOffset#3820} "Z"
// zone = {ZoneOffset#3820} "Z"
assertThat(now).isEqualTo(restored); // ALWAYS succeeds
System.out.println("test");
}
#Test
public void jacksonIncorrectlyRestoresZonedDateTime() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
// construct a new instance of ZonedDateTime
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Z"));
// offset = {ZoneOffset#3820} "Z"
// zone = {ZoneOffset#3820} "Z"
String converted = objectMapper.writeValueAsString(now);
// restore an instance of ZonedDateTime from String
ZonedDateTime restored = objectMapper.readValue(converted, ZonedDateTime.class);
// offset = {ZoneOffset#3820} "Z"
// zone = {ZoneOffset#3821} "UTC"
assertThat(now).isEqualTo(restored); // NEVER succeeds
System.out.println("break point me");
}
The equals() method on ZonedDateTime requires that all component parts of the object are equal. Since a ZoneOffset is not equal to a ZoneRegion (even though both are subclasses of ZoneId), the method returns false. Read about VALJOs to understand more as to why value types are compared in this way.
The isEqual method only compares the instant on the time-line, which may or may not be what you want. You can also use the timeLineOrder() method to compare two ZoneDateTime only using the time-line.
This caused us pain for a while too. The ZonedDateTime values actually represent identical times, however their zone "types" are different, which is why they are not equal.
The above answers are correct, however I thought I'd add a visual that might further help:
In the ZonedDateTime code, we find, which shows the zone comparison:

why toMillis() function returning -1 in java

In android, while running the below code snippet, date3 returns -1
booking_year2 = 2038;
booking_month2 = 1;
booking_day2 = 17;
Time t = new Time();
t.set(booking_day2, booking_month2 - 1, booking_year2);
long date3 = t.toMillis(false);
//date3 returns 2147451300000 as expected
//But if we run with values:
booking_year2 = 2038;
booking_month2 = 1;
booking_day2 = 18;
//date3 returns -1
While, the Time object 't' have expected value in all conditions, the long value returned by toMillis() function is only -1.
And also, for all the upper values of date after jan 19, 2038, the toMillis() function returns only -1 and not the expected value.
I didn't understood and didn't found any suitable reason or solution for this.
Please let me know, if I am doing anything wrong or is there any other way to find the millisecond values after this date.
That is the year-2038-problem which affects the class android.text.format.Time (android class). The problem is typical for UNIX. And the documentation of the class says:
"It is modelled after struct tm, and in fact, uses struct tm to implement most of the functionality."
So it uses in the background a C++-solution which is also widespread on UNIX-systems. This datatype has only 32 bits, so in year 2038 integer overflow will happen. Finally you can hope on a future bugfix, see following bug report of Android:
Issue 37653: android.text.format.Time not year 2038 safe
A workaround is to use Joda-Time, the open-source third-party library.
DateTime dateTime = new DateTime( 2099, 1, 2, 3, 4, 5, DateTimeZone.UTC );
long millis = dateTime.getMillis();
Dump to console…
System.out.println( "dateTime: " + dateTime );
System.out.println( "millis: " + millis );
When run…
dateTime: 2099-01-02T03:04:05.000Z
millis: 4071006245000

How to easily convert Joda YearMonth to LocalDateTime

As per the question title, I was wondering if there was something a little less verbose than this:
new YearMonth(2014, 1).toLocalDate(1).toLocalDateTime(new LocalTime())
maybe a utility method or instance method?
Your example uses the current time. If that was arbitrary, and midnight will do, try:
new LocalDateTime(1980, 1, 1, 0, 0).withFields(new YearMonth(2014, 1))
You can then pull the LocalDateTime into a constant for more brevity:
FIRST_OF_MONTH.withFields(new LocalDateTime(2014, 1))
Or, equivalently to your proposed "toLocalDateTime":
LocalDateTime.now().withFields(new LocalDateTime(2014, 1))

Categories