ZonedDateTime comparison: expected: [Etc/UTC] but was: [UTC]
Solution 1
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));
Solution 2
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.
Update from 2018-02-01:
The JDK-issue which was opened for this question has been closed by Oracle as "Not an issue".
Solution 3
ZoneDateTime
should be converted to OffsetDateTime
and then compared with compareTo(..)
if you want to compare the time.
ZoneDateTime.equals
and ZoneDateTime.compareTo
compare if the instant and the zone identifier, i.e. tth string identifying the timezone.
ZoneDateTime
is an instant and a zone (with its id and not only an offset) while OffsetDateTime
is an instant and a zone offset. If you want to compare the time between the two ZoneDateTime
objects, you should use OffsetDateTime
.
The method ZoneId.of(..)
is parsing the string you give and transform it if needed. ZoneId
represents the timezone and not the offset, i.e. the time shift with the GMT timezone. While ZoneOffset
reprensents the offset.
https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#of-java.lang.String-
If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a ZoneId with the same ID and rules equivalent to ZoneOffset.UTC.
If the zone ID starts with 'UTC+', 'UTC-', 'GMT+', 'GMT-', 'UT+' or 'UT-' then the ID is a prefixed offset-based ID. The ID is split in two, with a two or three letter prefix and a suffix starting with the sign. The suffix is parsed as a ZoneOffset. The result will be a ZoneId with the specified UTC/GMT/UT prefix and the normalized offset ID as per ZoneOffset.getId(). The rules of the returned ZoneId will be equivalent to the parsed ZoneOffset.
All other IDs are parsed as region-based zone IDs. Region IDs must match the regular expression [A-Za-z][A-Za-z0-9~/._+-]+ otherwise a DateTimeException is thrown. If the zone ID is not in the configured set of IDs, ZoneRulesException is thrown. The detailed format of the region ID depends on the group supplying the data. The default set of data is supplied by the IANA Time Zone Database (TZDB). This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'. This is compatible with most IDs from TimeZone.
so UTC+0
is transformed to UTC
while Etc/UTC is kept without modification
ZoneDateTime.compareTo
compares the string of the id ("Etc/UTC".compareTo("UTC") == 16
). That's the reason you should use OffsetDateTime
.
Andremoniy
For contacts: andremoniy at sign gmail dot com. I am open for team-lead/tech-lead positions.
Updated on June 07, 2022Comments
-
Andremoniy about 2 years
I was comparing two dates which seem to be equal, but they contain a different name of zones: one is
Etc/UTC
, another isUTC
.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 toZoneId.of("Etc/UTC")
, but they aren't!As @NicolasHenneaux suggested, I should probably use
compareTo(...)
method. That's good idea, butzoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)
returns-16
value, because of this implementation insideZoneDateTime
: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 theZoneId
. 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 asUTC
, then I see two options:- compareTo/equals method shouldn't use ZoneId id, but should compare their rules
-
Zone.of(...)
is broken and should treatEtc/UTC
andUTC
as the same time zones.
Otherwise I don't see why
UTC+0
andUTC
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
-
Andremoniy over 6 years
zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)
returns-16
. -
Eugene over 6 yearsnow this is what I meant in the comments, there has to be a way to actually compare them... 1+
-
Andremoniy over 6 yearsThank you for this analysis. But we all can read source codes and read javadocs. :-) Seriously, don't be offended :-) . This doesn't explain why
zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)
is not equal to0
. If zones are equal, so date-time representations with these zones should be equal as well. That is why I consider this as a bug in the impementation -
Andremoniy over 6 years@Eugene comparing rules of Zones doesn't solve the initial problem with comparing two ZonedDateTime constructed with them...
-
Anton Malyshev over 6 yearsObvious bug in java design/implementation I think. Why to use all that fancy objects if they even don't work in terms of comparison
-
Andremoniy over 6 yearsTo Meno, an additional comment: in fact I didn't use
Etc/UTC
explicitly (this is just from test). The problem came from some other code where this zone was assigned automatically without my desire... -
Eugene over 6 years@Andremoniy I know, this was just to say that at least there's a way to compare them I guess
-
Andremoniy over 6 years@Eugene to compare Zones - looks like, yes, there exists a way. But it is useless, as we can not compare ZonedDateTimes...
-
Andremoniy over 6 yearsAnd probably kind of this comparison
z1.getRules().equals(z2.getRules())
should be done insideZonedDateTime.compareTo
method instead of comparing ZoneId's.ID's. That is a bug in my opinion -
Bubletan over 6 yearsYou can do something like
zdt1.toInstant().equals(zdt2.toInstant()) && zdt1.getZone().getRules().equals(zdt2.getZone().getRules())
orzdt1.withFixedOffsetZone().equals(zdt2.withFixedOffsetZone())
. It's not very elegant but works. -
Meno Hochschild over 6 years@Andremoniy Good observation. The comparison finally delegates to
ZoneId.getId()
-comparison, see my answer. -
Andremoniy over 6 yearsBTW, @Nicolas, it is said in the javadoc also: ` It is "consistent with equals", as defined by Comparable`. So equals is also proper way to compare these objects
-
Meno Hochschild over 6 years@Andremoniy The statement "consistent with equals" does not necessarily means that it is a pure temporal comparison. In fact, other non-temporal state like the lexical representation of
ZoneId
is taken into account here. As said in my answer: AZoneId
is semantically only a pointer with name referencing rules/data but does not represent the ruels itself. Oracle will never accept that this is a bug because they have even described in the official API in all details this strange behaviour. No chance to get it changed in Java. IMHO a design error to makeZonedDateTime
comparable at all -
Andremoniy over 6 years@MenoHochschild IMHO a design error to make ZonedDateTime comparable at all - I agree!!!
-
Andremoniy over 6 years@MenoHochschild But take a note please and see my update: nevertheless
UTC+0
andUTC
are treated as equal. -
Meno Hochschild over 6 years@Andremoniy The fact that
ZonedDateTime
implements theComparable
-interface calls for problems. Your wish that the comparison does a rule-based comparison ofZoneId
is understandable but neglects a) that there is another non-temporal state like theChronology
and b) the comparison can break in context of deserialization (hard to understand on first glance). My personal view how usefulZonedDateTime
is can be found here The typeZonedDateTime
is simply too complex in many subtile ways. Just havingInstant
and local types should be preferred. -
Meno Hochschild over 6 years@Andremoniy Good observation about
UTC+0
andUTC
. Here Java does an inner normalization inside the classZoneId
but not with "Etc/...". Apparently inconsistent! But even this "feature" is described by Oracle. -
Nicolas Henneaux over 6 years@MenoHochschild it is exactly specified.
-
Meno Hochschild over 6 yearsExactly true, so Oracle will refuse to accept this post as a bug.
-
Basil Bourque over 6 yearsRegarding your first sentence, if you want to compare instants convert to
Instant
, notOffsetDateTime
.ZonedDateTime::toInstant()
-
Nicolas Henneaux over 6 years@BasilBourque right my explanation was not clear, I have updated it.
-
Andremoniy over 6 years
isEqual
is really a good observation! Your answer doesn't provide an exact answer to my question, but I am upvoting it for this good idea.