Je comparais deux dates qui semblent égales, mais qui contiennent un nom de zone différent: l’une est Etc/UTC
, l’autre est UTC
.
Selon cette question: Y a-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 a suggéré , je devrais probablement utiliser la méthode compareTo(...)
. C'est une bonne idée, mais zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)
renvoie -16
valeur, 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 soient égaux.
Ma question est la suivante: s'agit-il d'un problème de bibliothèque ou je fais quelque chose de mal?
METTRE À JOUR
Plusieurs personnes ont essayé de me convaincre qu'il s'agissait d'un comportement normal, et qu'il est normal que l'implémentation de méthodes de comparaison utilise la représentation String
id de la 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:
Zone.of(...)
est cassé et devrait traiter Etc/UTC
et UTC
comme les mêmes fuseaux horaires. Sinon, je ne vois pas pourquoi UTC+0
et UTC
fonctionnent correctement.
UPDATE-2 J'ai signalé un bogue, ID: 9052414. Voyera ce que l'équipe Oracle décidera.
UPDATE-3 Le rapport de bogue accepté (je ne sais pas, ils le fermeront car "ne résoudra pas" ou pas): https://bugs.openjdk.Java.net/browse/JDK-8196398
Vous pouvez convertir les objets 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 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 méthode equals()
- la comparaison des membres ZoneId
- internes. Nous voyons donc dans cette classe (code source en 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;
}
Les représentations lexicales "Etc/UTC" et "UTC" sont évidemment des chaînes différentes de sorte que la comparaison zoneId donne trivialement 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, une ZoneId
est comme 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 que ce à quoi on s'attend habituellement.
Une autre remarque importante sur la comparaison d'instances 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'instances ZonedDateTime
- avec le même horodatage instantané et local est basée sur la comparaison lexicale d'id de zone. Ce n'est 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 par dans l’API . C'est une autre raison pour laquelle je n'aime pas travailler avec le type ZonedDateTime
. C'est intrinsè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 instances ZonedDateTime
-, convertissez-les (généralement en Instant
), puis comparez.
Le JDK-issue qui a été ouvert pour cette question a été fermé par Oracle en tant que "Pas un problème".
ZoneDateTime
devrait être converti en OffsetDateTime
et ensuite comparé à compareTo(..)
si vous souhaitez comparer l'heure.
ZoneDateTime.equals
et ZoneDateTime.compareTo
comparent l’instant et l’identificateur 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) tandis que OffsetDateTime
est un instant et un décalage de zone. Si vous souhaitez comparer le temps entre les deux objets ZoneDateTime
, vous devez utiliser OffsetDateTime
.
La méthode ZoneId.of(..)
est l’analyse de la chaîne que vous donnez et la transformation 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.String-
Si l'ID de 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 identifiants 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'ensemble de données par défaut est fourni par la base de données 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.
alors UTC+0
est transformé en UTC
tandis que Etc/UTC est conservé sans modification
ZoneDateTime.compareTo
compare la chaîne de l'id ("Etc/UTC".compareTo("UTC") == 16
). C'est la raison pour laquelle vous devriez utiliser OffsetDateTime
.