I was comparing two dates which seem to be equal, but they contain a different name of zones: one is Etc/UTC, another is UTC.
According to this question: Is there a difference between the UTC and Etc/UTC time zones? - this two zones are the same. But my tests fail:
import org.junit.Test;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.junit.Assert.assertEquals;
public class TestZoneDateTime {
@Test
public void compareEtcUtcWithUtc() {
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zoneDateTimeEtcUtc = now.withZoneSameInstant(ZoneId.of("Etc/UTC"));
ZonedDateTime zoneDateTimeUtc = now.withZoneSameInstant(ZoneId.of("UTC"));
// This is okay
assertEquals(Timestamp.from(zoneDateTimeEtcUtc.toInstant()), Timestamp.from(zoneDateTimeUtc.toInstant()));
// This one fails
assertEquals(zoneDateTimeEtcUtc,zoneDateTimeUtc);
// This fails as well (of course previous line should be commented!)
assertEquals(0, zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc));
}
}
The result:
java.lang.AssertionError:
Expected :2018-01-26T13:55:57.087Z[Etc/UTC]
Actual :2018-01-26T13:55:57.087Z[UTC]
More specifically, I would expect, that ZoneId.of("UTC") would be equal to ZoneId.of("Etc/UTC"), but they aren't!
As @NicolasHenneaux suggested, I should probably use compareTo(...) method. That's good idea, but zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) returns -16 value, because of this implementation inside ZoneDateTime:
cmp = getZone().getId().compareTo(other.getZone().getId());
Assertion result:
java.lang.AssertionError:
Expected :0
Actual :-16
So the problem lies somewhere in ZoneId implementation. But I still would expect that if both zone ids are valid and both designate the same zone, then they should be equal.
My question is: is it a library bug, or I am doing something wrong?
UPDATE
Several people tried to convince me that it is a normal behaviour, and it is normal that the implementation of comparison methods uses String id representation of the ZoneId. In this case I should ask, why does the following test runs okay?
@Test
public void compareUtc0WithUtc() {
ZonedDateTime now = ZonedDateTime.now();
ZoneId utcZone = ZoneId.of("UTC");
ZonedDateTime zonedDateTimeUtc = now.withZoneSameInstant(utcZone);
ZoneId utc0Zone = ZoneId.of("UTC+0");
ZonedDateTime zonedDateTimeUtc0 = now.withZoneSameInstant(utc0Zone);
// This is okay
assertEquals(Timestamp.from(zonedDateTimeUtc.toInstant()), Timestamp.from(zonedDateTimeUtc0.toInstant()));
assertEquals(0, zonedDateTimeUtc.compareTo(zonedDateTimeUtc0));
assertEquals(zonedDateTimeUtc,zonedDateTimeUtc0);
}
If Etc/UTC is the same as UTC, then I see two options:
Zone.of(...) is broken and should treat Etc/UTC and UTC as the same time zones. Otherwise I don't see why UTC+0 and UTC work fine.
UPDATE-2 I have reported a bug, ID : 9052414. Will see what Oracle team will decide.
UPDATE-3 The bug report accepted (don't know will they close it as "won't fix" or not): https://bugs.openjdk.java.net/browse/JDK-8196398
ZonedDateTime equals() method in Java with ExamplesThe equals() method of ZonedDateTime class in Java is used to compare this ZonedDateTime to the another date-time object passed as parameter. The comparison is based on the offset date-time and the zone.
A ZoneId is used to identify the rules used to convert between an Instant and a LocalDateTime. There are two distinct types of ID: Fixed offsets - a fully resolved offset from UTC/Greenwich, that uses the same offset for all local date-times.
ZoneOffset describes a time-zone offset, which is the amount of time (typically in hours) by which a time zone differs from UTC/Greenwich. ZonedDateTime describes a date-time with a time zone in the ISO-8601 calendar system (such as 2007-12-03T10:15:30+01:00 Europe/Paris ).
You can convert the ZonedDateTime objects to Instant, as the other answers/comments already told.
ZonedDateTime::isEqualOr you can use the isEqual method, which compares if both ZonedDateTime instances correspond to the same Instant:
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zoneDateTimeEtcUtc = now.withZoneSameInstant(ZoneId.of("Etc/UTC"));
ZonedDateTime zoneDateTimeUtc = now.withZoneSameInstant(ZoneId.of("UTC"));
Assert.assertTrue(zoneDateTimeEtcUtc.isEqual(zoneDateTimeUtc));
The class ZonedDateTime uses in its equals()-method the comparison of inner ZoneId-members. So we see in that class (source code in Java-8):
/**
* Checks if this time-zone ID is equal to another time-zone ID.
* <p>
* The comparison is based on the ID.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other time-zone ID
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof ZoneId) {
ZoneId other = (ZoneId) obj;
return getId().equals(other.getId());
}
return false;
}
The lexical representations "Etc/UTC" and "UTC" are obviously different strings so the zoneId-comparison trivially yields false. This behaviour is described in the javadoc, so we have no bug. I stress the statement of the documentation:
The comparison is based on the ID.
That said, a ZoneId is like a named pointer to the zone data and does not represent the data themselves.
But I assume that you rather want to compare the rules of both different zone-ids and not the lexical representations. Then ask the rules:
ZoneId z1 = ZoneId.of("Etc/UTC");
ZoneId z2 = ZoneId.of("UTC");
System.out.println(z1.equals(z2)); // false
System.out.println(z1.getRules().equals(z2.getRules())); // true
So you could use the comparison of zone rules and the other non-zone-related members of ZonedDateTime (a little bit awkward).
By the way, I strongly recommend not to use "Etc/..."-identifiers because (with the exception of "Etc/GMT" or "Etc/UTC") their offset signs are in reverse than what is usually expected.
Another important remark about the comparison of ZonedDateTime-instances. Look here:
System.out.println(zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)); // -16
System.out.println(z1.getId().compareTo(z2.getId())); // -16
We see that the comparison of ZonedDateTime-instances with same instant and local timestamp is based on the lexical comparison of zone-ids. Usually not what most users would expect. But it is no bug, too, because this behaviour is described in the API. This is another reason why I don't like to work with the type ZonedDateTime. It is inherently too complex. You should only use it for intermediate type conversions between Instant and the local types IMHO. This concretely means: Before you compare ZonedDateTime-instances, please convert them (usually to Instant) and then compare.
The JDK-issue which was opened for this question has been closed by Oracle as "Not an issue".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With