web-dev-qa-db-fra.com

Comparaison de ZonedDateTime: attendu: [Etc/UTC] mais était: [UTC]

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/UTCest 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 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

9
Andremoniy

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));
5
javaguest

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.

Mise à jour du 2018-02-01:

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

3
Meno Hochschild

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.

0
Nicolas Henneaux