Pourquoi soustraire ces deux fois (en 1927) donne-t-il un résultat étrange?

6828

Si j'exécute le programme suivant, qui analyse deux chaînes de date faisant référence à des intervalles de 1 seconde et les compare:

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    String str3 = "1927-12-31 23:54:07";  
    String str4 = "1927-12-31 23:54:08";  
    Date sDt3 = sf.parse(str3);  
    Date sDt4 = sf.parse(str4);  
    long ld3 = sDt3.getTime() /1000;  
    long ld4 = sDt4.getTime() /1000;
    System.out.println(ld4-ld3);
}

La sortie est:

353

Pourquoi ld4-ld3pas 1(comme je m'attendais à la différence d'une seconde dans le temps), mais 353?

Si je change les dates en fois 1 seconde plus tard:

String str3 = "1927-12-31 23:54:08";  
String str4 = "1927-12-31 23:54:09";  

Ce ld4-ld3sera le cas 1.


Version Java:

java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Dynamic Code Evolution Client VM (build 0.2-b02-internal, 19.0-b04-internal, mixed mode)

Timezone(`TimeZone.getDefault()`):

sun.util.calendar.ZoneInfo[id="Asia/Shanghai",
offset=28800000,dstSavings=0,
useDaylight=false,
transitions=19,
lastRule=null]

Locale(Locale.getDefault()): zh_CN
Freewind
la source
23
Cela pourrait être un problème de localisation.
Thorbjørn Ravn Andersen
72
La vraie réponse est de toujours, toujours utiliser les secondes depuis une époque pour la journalisation, comme l'époque Unix, avec une représentation entière de 64 bits (signée, si vous voulez autoriser les tampons avant l'époque). Tout système en temps réel a un comportement non linéaire et non monotone comme les heures bissextiles ou l'heure d'été.
Phil H
22
Lol. Ils l'ont corrigé pour jdk6 en 2011. Puis, deux ans après cela, ils ont découvert qu'il devrait aussi être corrigé dans jdk7 .... corrigé à partir de 7u25, bien sûr, je n'ai trouvé aucun indice dans la note de publication. Parfois, je me demande combien de bogues Oracle corrige et n'en parle à personne pour des raisons de relations publiques.
user1050755
8
Une excellente vidéo sur ce genre de choses: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen
4
@PhilH Ce qui est bien, c'est qu'il y aura encore des secondes intercalaires. Donc, même cela ne fonctionne pas.
12431234123412341234123

Réponses:

10876

C'est un changement de fuseau horaire le 31 décembre à Shanghai.

Voir cette page pour les détails de 1927 à Shanghai. Fondamentalement, à minuit fin 1927, les horloges sont remontées de 5 minutes et 52 secondes. Donc, "1927-12-31 23:54:08" s'est en fait produit deux fois, et il semble que Java l'ait analysé comme l' instant le plus tard possible pour cette date / heure locale - d'où la différence.

Juste un autre épisode dans le monde souvent étrange et merveilleux des fuseaux horaires.

EDIT: Arrêtez d'appuyer! L'histoire change ...

La question d'origine ne démontrerait plus le même comportement si elle était reconstruite avec la version 2013a de TZDB . En 2013a, le résultat serait de 358 secondes, avec un temps de transition de 23:54:03 au lieu de 23:54:08.

Je ne l'ai remarqué que parce que je collecte des questions comme celle-ci dans Noda Time, sous la forme de tests unitaires ... Le test a maintenant été modifié, mais cela ne fait que montrer - même les données historiques ne sont pas sûres.

EDIT: L' histoire a encore changé ...

Dans TZDB 2014f, l'heure du changement est passée au 1900-12-31, et il ne s'agit désormais que d'un changement de 343 secondes (donc le temps entre tet t+1est de 344 secondes, si vous voyez ce que je veux dire).

EDIT: Pour répondre à une question concernant une transition à 1900 ... il semble que l'implémentation du fuseau horaire Java traite tous les fuseaux horaires comme étant simplement à leur heure standard pour tout instant avant le début de 1900 UTC:

import java.util.TimeZone;

public class Test {
    public static void main(String[] args) throws Exception {
        long startOf1900Utc = -2208988800000L;
        for (String id : TimeZone.getAvailableIDs()) {
            TimeZone zone = TimeZone.getTimeZone(id);
            if (zone.getRawOffset() != zone.getOffset(startOf1900Utc - 1)) {
                System.out.println(id);
            }
        }
    }
}

Le code ci-dessus ne produit aucune sortie sur ma machine Windows. Ainsi, tout fuseau horaire qui a un décalage autre que son standard au début de 1900 comptera cela comme une transition. TZDB lui-même a des données remontant plus tôt que cela, et ne s'appuie sur aucune idée d'un temps standard "fixe" (ce qui getRawOffsetsuppose être un concept valide), de sorte que les autres bibliothèques n'ont pas besoin d'introduire cette transition artificielle.

Jon Skeet
la source
25
@ Jon: Par curiosité, pourquoi ont-ils remis leurs horloges à un tel intervalle "bizarre"? Quelque chose comme une heure aurait semblé logique, mais comment se fait-il qu'il était 5 h 52 minutes?
Johannes Rudolph
63
@Johannes: Pour en faire un fuseau horaire plus globalement normal, je pense - le décalage résultant est UTC + 8. Paris a fait le même genre de chose en 1911 par exemple: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet
34
@Jon Savez-vous si Java / .NET résiste à septembre 1752? J'aimais toujours montrer aux gens le cal 9 1752 sur les systèmes Unix
Mr Moose
30
Alors pourquoi diable était Shanghai à 5 minutes de wack en premier lieu?
Igby Largeman
25
@Charles: Beaucoup d'endroits avaient des décalages moins conventionnels à l'époque. Dans certains pays, différentes villes avaient chacune leur propre compensation pour être aussi proche de la géographie que possible.
Jon Skeet
1602

Vous avez rencontré une discontinuité d'heure locale :

Lorsque l'heure normale locale était sur le point d'atteindre le dimanche 1er janvier 1928, 00:00:00 les horloges ont été reculées de 0:05:52 heure au samedi 31 décembre. 1927, 23:54:08 heure locale au lieu

Ce n'est pas particulièrement étrange et cela s'est produit à peu près partout à un moment ou à un autre, car les fuseaux horaires ont été changés ou modifiés en raison d'actions politiques ou administratives.

Michael Borgwardt
la source
661

La morale de cette étrangeté est:

  • Utilisez les dates et les heures en UTC dans la mesure du possible.
  • Si vous ne pouvez pas afficher une date ou une heure en UTC, indiquez toujours le fuseau horaire.
  • Si vous ne pouvez pas exiger une date / heure d'entrée en UTC, exigez un fuseau horaire explicitement indiqué.
Raedwald
la source
75
La conversion / stockage en UTC n'aiderait vraiment pas le problème décrit car vous rencontriez la discontinuité dans la conversion en UTC.
impythonique
23
@Mark Mann: si votre programme utilise UTC en interne partout, en convertissant vers / à partir d'un fuseau horaire local uniquement dans l'interface utilisateur, vous ne vous souciez pas de ces discontinuités.
Raedwald
66
@Raedwald: Bien sûr que vous le feriez - Quelle est l'heure UTC pour 1927-12-31 23:54:08? (Ignorant, pour le moment, que l'UTC n'existait même pas en 1927). À un moment donné, cette heure et cette date arrivent dans votre système et vous devez décider quoi en faire. Dire à l'utilisateur qu'il doit saisir l'heure en UTC déplace simplement le problème vers l'utilisateur, il ne l'élimine pas.
Nick Bastin
72
Je me sens justifié de la quantité d'activité sur ce fil, après avoir travaillé sur le refactoring date / heure d'une grande application depuis près d'un an maintenant. Si vous faites quelque chose comme l'agenda, vous ne pouvez pas "simplement" stocker UTC, car les définitions des fuseaux horaires dans lesquels il peut être rendu changeront avec le temps. Nous stockons «l'heure de l'intention de l'utilisateur» - l'heure locale de l'utilisateur et son fuseau horaire - et l'UTC pour la recherche et le tri, et chaque fois que la base de données IANA est mise à jour, nous recalculons toutes les heures UTC.
taiganaut
366

Lors de l'incrémentation du temps, vous devez reconvertir en UTC, puis ajouter ou soustraire. Utilisez l'heure locale uniquement pour l'affichage.

De cette façon, vous pourrez parcourir toutes les périodes où les heures ou les minutes se produisent deux fois.

Si vous avez converti en UTC, ajoutez chaque seconde et convertissez en heure locale pour l'affichage. Vous passeriez par 23 h 54 min 08 s LMT - 23 h 59 min 59 s LMT puis 23 h 54 min 08 s CST - 23 h 59 min 59 s CST.

PatrickO
la source
309

Au lieu de convertir chaque date, vous pouvez utiliser le code suivant:

long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);

Et puis voyez que le résultat est:

1
Rajshri
la source
72
J'ai bien peur que ce ne soit pas le cas. Vous pouvez essayer mon code dans votre système, il sortira 1, car nous avons différents paramètres régionaux.
Freewind
14
Cela est vrai uniquement parce que vous n'avez pas spécifié les paramètres régionaux dans l'entrée de l'analyseur. C'est un mauvais style de codage et un énorme défaut de conception en Java - sa localisation inhérente. Personnellement, je mets "TZ = UTC LC_ALL = C" partout où j'utilise Java pour éviter cela. De plus, vous devez éviter chaque version localisée d'une implémentation, sauf si vous interagissez directement avec un utilisateur et que vous le souhaitez explicitement. Ne faites AUCUN calcul, y compris les localisations, utilisez toujours les fuseaux horaires Locale.ROOT et UTC, sauf si cela est absolument nécessaire.
user1050755
226

Je suis désolé de le dire, mais la discontinuité temporelle a un peu bougé

JDK 6 il y a deux ans, et dans JDK 7 tout récemment dans la mise à jour 25 .

Leçon à tirer: évitez à tout prix les heures non UTC, sauf peut-être pour l'affichage.

user1050755
la source
27
Ceci est une erreur. La discontinuité n'est pas un bug - c'est juste qu'une version plus récente de TZDB a des données légèrement différentes. Par exemple, sur ma machine avec Java 8, si vous modifiez très légèrement le code pour utiliser "1927-12-31 23:54:02" et "1927-12-31 23:54:03", vous verrez toujours un discontinuité - mais de 358 secondes maintenant, au lieu de 353. Même les versions plus récentes de TZDB ont encore une autre différence - voir ma réponse pour plus de détails. Il n'y a pas de vrai bug ici, juste une décision de conception sur la façon dont les valeurs de texte date / heure ambiguë sont analysées.
Jon Skeet
6
Le vrai problème est que les programmeurs ne comprennent pas que la conversion entre l'heure locale et l'heure universelle (dans les deux sens) n'est pas et ne peut pas être fiable à 100%. Pour les anciens horodatages, les données dont nous disposons sur l'heure locale sont au mieux instables. Pour les horodatages futurs, les actions politiques peuvent changer le temps universel auquel une heure locale donnée correspond. Pour les horodatages passés actuels et récents, vous pouvez avoir le problème que le processus de mise à jour de la base de données tz et de déploiement des modifications peut être plus lent que le calendrier de mise en œuvre des lois.
plugwash
200

Comme expliqué par d'autres, il y a là une discontinuité temporelle. Il existe deux décalages de fuseau horaire possibles pour 1927-12-31 23:54:08à Asia/Shanghai, mais un seul décalage pour 1927-12-31 23:54:07. Ainsi, selon le décalage utilisé, il y a soit une différence d'une seconde, soit une différence de 5 minutes et 53 secondes.

Ce léger décalage des décalages, au lieu de l'heure d'été habituelle (heure d'été) à laquelle nous sommes habitués, obscurcit un peu le problème.

Notez que la mise à jour 2013a de la base de données de fuseau horaire a déplacé cette discontinuité quelques secondes plus tôt, mais l'effet serait toujours observable.

Le nouveau java.timepackage sur Java 8 permet de voir cela plus clairement et fournit des outils pour le gérer. Donné:

DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.append(DateTimeFormatter.ISO_LOCAL_DATE);
dtfb.appendLiteral(' ');
dtfb.append(DateTimeFormatter.ISO_LOCAL_TIME);
DateTimeFormatter dtf = dtfb.toFormatter();
ZoneId shanghai = ZoneId.of("Asia/Shanghai");

String str3 = "1927-12-31 23:54:07";  
String str4 = "1927-12-31 23:54:08";  

ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);

Duration durationAtEarlierOffset = Duration.between(zdt3.withEarlierOffsetAtOverlap(), zdt4.withEarlierOffsetAtOverlap());

Duration durationAtLaterOffset = Duration.between(zdt3.withLaterOffsetAtOverlap(), zdt4.withLaterOffsetAtOverlap());

Il y durationAtEarlierOffsetaura ensuite une seconde, tandis que durationAtLaterOffsetsera de cinq minutes et 53 secondes.

De plus, ces deux décalages sont identiques:

// Both have offsets +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();

Mais ces deux sont différents:

// +08:05:52
ZoneOffset zo4Earlier = zdt4.withEarlierOffsetAtOverlap().getOffset();

// +08:00
ZoneOffset zo4Later = zdt4.withLaterOffsetAtOverlap().getOffset();

Vous pouvez voir le même problème par rapport 1927-12-31 23:59:59à 1928-01-01 00:00:00, cependant, dans ce cas, c'est le décalage antérieur qui produit la divergence la plus longue, et c'est la date antérieure qui a deux décalages possibles.

Une autre façon d'aborder cela est de vérifier s'il y a une transition en cours. Nous pouvons le faire comme ceci:

// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

Vous pouvez vérifier si la transition est un chevauchement où il y a plus d'un décalage valide pour cette date / heure ou un écart où cette date / heure n'est pas valide pour cet identifiant de zone - en utilisant les méthodes isOverlap()et .isGap()zot4

J'espère que cela aidera les gens à gérer ce type de problème une fois que Java 8 sera largement disponible, ou à ceux qui utilisent Java 7 qui adoptent le backport JSR 310.

Daniel C. Sobral
la source
1
Salut Daniel, j'ai exécuté votre morceau de code mais il ne donne pas la sortie comme prévu. comme durationAtEarlierOffset et durationAtLaterOffset sont tous les deux 1 seconde seulement et zot3 et zot4 sont tous deux nuls. Je viens de copier et d'exécuter ce code sur ma machine. y a-t-il quelque chose qui doit être fait ici. Faites-moi savoir si vous voulez voir un morceau de code. Voici le code tutorialspoint.com/… pouvez-vous me faire savoir ce qui se passe ici.
vineeshchauhan
2
@vineeshchauhan Cela dépend de la version de Java, car cela a changé dans tzdata, et différentes versions de JDK regroupent différentes versions de tzdata. Sur mon propre Java installé, les temps sont 1900-12-31 23:54:16et 1900-12-31 23:54:17, mais cela ne fonctionne pas sur le site que vous avez partagé, ils utilisent donc une version Java différente de la mienne.
Daniel C. Sobral
167

À mon humble avis, la localisation implicite omniprésente dans Java est sa plus grande faille de conception. Il peut être destiné aux interfaces utilisateur, mais franchement, qui utilise vraiment Java pour les interfaces utilisateur aujourd'hui, à l'exception de certains IDE où vous pouvez essentiellement ignorer la localisation car les programmeurs ne sont pas exactement le public cible pour cela. Vous pouvez le corriger (en particulier sur les serveurs Linux) en:

  • export LC_ALL = C TZ = UTC
  • réglez votre horloge système sur UTC
  • n'utilisez jamais d'implémentations localisées sauf en cas d'absolue nécessité (c'est-à-dire uniquement pour l'affichage)

Aux membres du Java Community Process , je recommande:

  • faire des méthodes localisées pas la valeur par défaut, mais obliger l'utilisateur à demander explicitement la localisation.
  • utilisez plutôt UTF-8 / UTC comme valeur par défaut FIXE car c'est tout simplement la valeur par défaut aujourd'hui. Il n'y a aucune raison de faire autre chose, sauf si vous souhaitez produire des threads comme celui-ci.

Je veux dire, allez, les variables statiques globales ne sont-elles pas un modèle anti-OO? Rien d'autre n'est ces défauts omniprésents donnés par certaines variables d'environnement rudimentaires .......

user1050755
la source
21

Comme d'autres l'ont dit, c'est un changement d'heure en 1927 à Shanghai.

Lorsqu'il était 23:54:07à Shanghai, l'heure locale, mais après 5 minutes et 52 secondes, il est passé au lendemain à 00:00:00, puis l'heure locale a changé 23:54:08. C'est pourquoi la différence entre les deux temps est de 343 secondes et non d'une seconde, comme vous vous en doutez.

Le temps peut également gâcher dans d'autres endroits comme les États-Unis. Les États-Unis ont l'heure d'été. Lorsque l'heure d'été commence, l'heure avance d'une heure. Mais après un certain temps, l'heure d'été se termine et revient en arrière d'une heure au fuseau horaire standard. Ainsi, parfois, lorsque l'on compare les heures aux États-Unis, la différence est d'environ 3600secondes, pas 1 seconde.

Mais il y a quelque chose de différent dans ces deux changements de temps. Ce dernier change continuellement et le premier n'était qu'un changement. Il n'a pas changé ou changé à nouveau du même montant.

Il est préférable d'utiliser UTC où l'heure ne change pas, sauf si nécessaire pour utiliser l'heure non UTC comme dans l'affichage.

zixuan
la source