Pourquoi soustraire ces deux fois (en 1927) donner un résultat étrange?

Si j’exécute le programme suivant, qui parsing deux chaînes de dates faisant référence à des intervalles d’une seconde et les compare:

public static void main(Ssortingng[] args) throws ParseException { SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Ssortingng str3 = "1927-12-31 23:54:07"; Ssortingng 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 la suivante:

353

Pourquoi ld4-ld3 n’est ld4-ld3 pas 1 (comme on peut s’attendre à une seconde de différence), mais 353 ?

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

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

Alors ld4-ld3 sera 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 

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 ont reculé de 5 minutes et 52 secondes. Donc “1927-12-31 23:54:08” s’est réellement passé deux fois, et il semblerait 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 de presser! L’histoire change …

La question initiale 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 rassemble des questions comme celle-ci dans Noda Time, sous la forme de tests unitaires … Le test a maintenant été modifié, mais il montre que 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 à 1900-12-31, et ce n’est plus que 343 secondes (donc le temps entre t et t+1 est de 344 secondes, si vous voyez ce que je veux dire).

EDIT: Pour répondre à une question autour d’une transition à 1900 … il semble que l’implémentation du fuseau horaire Java traite tous les fuseaux horaires comme étant dans leur heure standard pour n’importe quel moment avant le début de 1900 UTC:

 import java.util.TimeZone; public class Test { public static void main(Ssortingng[] args) throws Exception { long startOf1900Utc = -2208988800000L; for (Ssortingng 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. Donc, tout fuseau horaire qui a un décalage autre que le standard au début de 1900 comptera comme une transition. TZDB lui-même a des données remontant plus tôt que cela et ne repose sur aucune idée d’un temps standard “fixe” (ce que getRawOffset comme un concept valide), donc les autres bibliothèques n’ont pas besoin d’introduire cette transition artificielle.

Vous avez rencontré une discontinuité locale :

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

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

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 de saisie en UTC, demandez un fuseau horaire explicitement indiqué.

Lors de l’incrémentation du temps, vous devez reconvertir en UTC, puis append 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-la en heure locale pour l’affichage. Vous iriez à 23h54: 23h LMT – 23h59: 59h puis à 23h54h28 CST – 23h59: 59h CST.

Au lieu de convertir chaque date, vous utilisez le code suivant

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

Et voir le résultat est:

 1 

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

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

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

Comme l’expliquent les autres, il y a une discontinuité dans le temps. Il y a 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 compensations, au lieu de l’heure d’été habituelle d’une heure (heure d’été), masque un peu le problème.

Notez que la mise à jour 2013a de la firebase database des fuseaux horaires a déplacé cette discontinuité quelques secondes plus tôt, mais que l’effet serait toujours observable.

Le nouveau package java.time sur Java 8 permet de voir 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"); Ssortingng str3 = "1927-12-31 23:54:07"; Ssortingng 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()); 

Alors durationAtEarlierOffset sera une seconde, tandis que durationAtLaterOffset sera cinq minutes et 53 secondes.

En outre, 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 en comparant 1927-12-31 23:59:59 avec 1928-01-01 00:00:00 , bien que, dans ce cas, c’est le décalage précédent qui produise la plus longue divergence et c’est le date antérieure qui comporte deux compensations possibles.

Une autre façon de procéder consiste à vérifier si une transition est 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 – dans ce cas, il existe plusieurs décalages valides pour cette date / heure – ou un intervalle – auquel cas cette date / heure n’est pas valide pour cet identifiant de zone – en utilisant le isOverlap() et méthodes isGap() sur zot4 .

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

IMHO la localisation implicite et omniprésente en Java est son défaut de conception le plus important. Il peut être destiné aux interfaces utilisateur, mais franchement, qui utilise réellement Java pour les interfaces utilisateur aujourd’hui, à l’exception de certains IDE où vous pouvez pratiquement ignorer la localisation car les programmeurs ne sont pas exactement le public cible. Vous pouvez le réparer (notamment 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 à moins que cela ne soit absolument nécessaire (par exemple pour l’affichage uniquement)

Je recommande aux membres du processus de communauté Java :

  • rendre les 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 FIXED car c’est simplement la valeur par défaut aujourd’hui. Il n’y a aucune raison de faire autre chose, sauf si vous voulez produire des threads comme celui-ci.

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