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::isEqual
Or 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