Comparaison de ZonedDateTime: attendu: mais était:

Je comparais deux dates qui semblent égales, mais elles contiennent un nom de zone différent: l’une est Etc/UTC , l’autre est UTC .

Selon cette question: existe-t-il une différence entre les fuseaux horaires UTC et Etc / UTC? – ces deux zones sont les mêmes. Mais mes tests échouent:

 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)); } } 

Le résultat:

 java.lang.AssertionError: Expected :2018-01-26T13:55:57.087Z[Etc/UTC] Actual :2018-01-26T13:55:57.087Z[UTC] 

Plus précisément, je suppose que ZoneId.of("UTC") serait égal à ZoneId.of("Etc/UTC") , mais ils ne le sont pas!

Comme @NicolasHenneaux l’a suggéré , je devrais probablement utiliser la compareTo(...) . C’est une bonne idée, mais zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) renvoie la valeur -16 , en raison de cette implémentation dans ZoneDateTime :

 cmp = getZone().getId().compareTo(other.getZone().getId()); 

Résultat de l’assertion:

 java.lang.AssertionError: Expected :0 Actual :-16 

Le problème se situe donc quelque part dans la mise en œuvre de ZoneId . Mais je m’attendrais toujours à ce que si les deux identifiants de zone soient valides et désignent la même zone, ils devraient être égaux.

Ma question est: est-ce un bug de la bibliothèque ou est-ce que je fais quelque chose de mal?

METTRE À JOUR

Plusieurs personnes ont tenté de me convaincre qu’il s’agissait d’un comportement normal et qu’il est normal que la mise en œuvre de méthodes de comparaison utilise la représentation Ssortingng id de ZoneId . Dans ce cas, je devrais demander pourquoi le test suivant fonctionne correctement.

  @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); } 

Si Etc/UTC est identique à UTC , je vois deux options:

  • La méthode compareTo / equals ne devrait pas utiliser l’ID de ZoneId, mais devrait comparer leurs règles
  • Zone.of(...) est cassé et doit traiter Etc/UTC et UTC comme les mêmes fuseaux horaires.

Sinon, je ne vois pas pourquoi UTC+0 et UTC fonctionnent bien.

UPDATE-2 J’ai signalé un bogue, ID: 9052414. Voyera ce que l’équipe Oracle décidera.

UPDATE-3 Le rapport de bogue accepté (ne sait pas s’ils le fermeront car “ne résoudra pas” ou non): https://bugs.openjdk.java.net/browse/JDK-8196398

Vous pouvez convertir les objects ZonedDateTime en Instant , comme indiqué dans les autres réponses / commentaires.

ZonedDateTime::isEqual

Ou vous pouvez utiliser la méthode isEqual , qui compare si les deux instances de ZonedDateTime correspondent au même 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)); 

La classe ZonedDateTime utilise dans sa ZonedDateTime equals() la comparaison des membres internes de ZoneId . Nous voyons donc dans cette classe (code source en Java-8):

 /** * Checks if this time-zone ID is equal to another time-zone ID. * 

* 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; }

Les représentations lexicales “Etc / UTC” et “UTC” sont évidemment des chaînes différentes, de sorte que la comparaison zoneId produit false . Ce comportement est décrit dans le javadoc, nous n’avons donc aucun bug. J’insiste sur l’énoncé de la documentation :

La comparaison est basée sur l’ID.

Cela dit, un ZoneId est semblable à un pointeur nommé sur les données de zone et ne représente pas les données elles-mêmes.

Mais je suppose que vous voulez plutôt comparer les règles des deux identificateurs de zone et non les représentations lexicales. Puis demandez les règles:

 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 

Vous pouvez donc utiliser la comparaison des règles de zone et des autres membres non liés à la zone de ZonedDateTime (un peu gênant).

En passant, je recommande fortement de ne pas utiliser les identifiants “Etc /…”- car (à l’exception de” Etc / GMT “ou” Etc / UTC “), leurs signes de décalage sont inversés par rapport à ce que l’on attend habituellement.

Autre remarque importante sur la comparaison d’ ZonedDateTime ZonedDateTime . Regardez ici:

 System.out.println(zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)); // -16 System.out.println(z1.getId().compareTo(z2.getId())); // -16 

Nous voyons que la comparaison d’ ZonedDateTime ZonedDateTime avec le même horodatage instantané et local est basée sur la comparaison lexicale d’ ZonedDateTime de zone. Généralement pas ce à quoi la plupart des utilisateurs s’attendent. Mais il n’ya pas non plus de bogue, car ce comportement est décrit dans l’API . C’est une autre raison pour laquelle je n’aime pas travailler avec le type ZonedDateTime . C’est insortingnsèquement trop complexe. Vous ne devriez l’utiliser que pour les conversions de types intermédiaires entre Instant et les types locaux IMHO. Cela signifie concrètement: Avant de comparer les ZonedDateTime ZonedDateTime, convertissez-les (généralement en Instant ), puis comparez.

Mise à jour du 2018-02-01:

Le problème JDK qui a été ouvert pour cette question a été fermé par Oracle en tant que “Pas un problème”.

ZoneDateTime doit être converti en OffsetDateTime , puis comparé à compareTo(..) si vous souhaitez comparer l’heure.

ZoneDateTime.equals et ZoneDateTime.compareTo comparer si l’identifiant instantané et l’identifiant de zone, c’est-à-dire la chaîne identifiant le fuseau horaire.

ZoneDateTime est un instant et une zone (avec son identifiant et pas seulement un offset), alors que OffsetDateTime est un instant et un décalage de zone. Si vous souhaitez comparer le temps entre les deux objects ZoneDateTime , vous devez utiliser OffsetDateTime .

La méthode ZoneId.of(..) parsing la chaîne que vous donnez et la transforme si nécessaire. ZoneId représente le fuseau horaire et non le décalage, c’est-à-dire le décalage temporel avec le fuseau horaire GMT. Alors que ZoneOffset représente le décalage. https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#of-java.lang.Ssortingng-

Si l’ID de la zone est égal à «GMT», «UTC» ou «UT», le résultat est un ZoneId avec le même ID et les mêmes règles équivalentes à ZoneOffset.UTC.

Si l’ID de zone commence par «UTC +», «UTC-», «GMT +», «GMT-», «UT +» ou «UT-», l’identifiant est un identifiant basé sur le décalage et préfixé. L’identifiant est divisé en deux, avec un préfixe de deux ou trois lettres et un suffixe commençant par le signe. Le suffixe est analysé comme un ZoneOffset. Le résultat sera un ZoneId avec le préfixe UTC / GMT / UT spécifié et l’ID de décalage normalisé, conformément à ZoneOffset.getId (). Les règles de la ZoneId retournée seront équivalentes à la ZoneOffset analysée.

Tous les autres ID sont analysés en tant qu’ID de zone basés sur une région. Les ID de région doivent correspondre à l’expression régulière [A-Za-z] [A-Za-z0-9 ~ /._+-unset +, sinon une exception DateTimeException est levée. Si l’ID de zone ne fait pas partie de l’ensemble d’ID configuré, une exception ZoneRulesException est levée. Le format détaillé de l’ID de région dépend du groupe fournissant les données. L’dataset par défaut est fourni par la firebase database IANA Time Zone (TZDB). Cela a des identifiants de région de la forme ‘{area} / {city}’, tels que ‘Europe / Paris’ ou ‘America / New_York’. Ceci est compatible avec la plupart des identifiants de TimeZone.

donc UTC+0 est transformé en UTC tandis que Etc / UTC est conservé sans modification

ZoneDateTime.compareTo compare la chaîne de l’identifiant ( "Etc/UTC".compareTo("UTC") == 16 ). C’est la raison pour laquelle vous devriez utiliser OffsetDateTime .