Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ZonedDateTime comparison: expected: [Etc/UTC] but was: [UTC]

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:

  • compareTo/equals method shouldn't use ZoneId id, but should compare their rules
  • 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

like image 347
Andremoniy Avatar asked Jan 26 '18 13:01

Andremoniy


People also ask

How does ZonedDateTime compare to date?

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.

What is the ZoneId of UTC?

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.

What is offset in ZonedDateTime?

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 ).


2 Answers

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));
like image 65
javaguest Avatar answered Sep 19 '22 13:09

javaguest


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".

like image 45
Meno Hochschild Avatar answered Sep 22 '22 13:09

Meno Hochschild