La gestion du Temps sur une application client/serveur peut sembler simple de prime abord, mais se révélera beaucoup plus complexe au fur et à mesure des usages : timezones multiples, heures récurrentes, heure d'été/d'hiver, traitements cron, leap seconds sont des exemples parmi tant d'autres.
Saviez-vous même que la Commission Européenne envisage d'abolir le passage en heure d'hiver ? Quels impacts cela aurait sur nos applications ?
Mon objectif : qu'à partir de lundi prochain, vous vous posiez les bonnes questions à chaque fois que vous travaillerez sur une date ou une heure dans vos applications.
Pour cela, reprenons les bases du Temps en informatique : composantes d'une date, norme ISO 8601, Timezones et IANA.
Une fois ces bases posées, nous verrons, au travers d'un certain nombre de cas d'utilisation issus de la vraie vie, les bonnes questions qu'il convient de se poser pour mettre le doigt sur les complexités d'implémentation et éviter de tomber dans une faille spatio-temporelle lors du prochain changement d'heure.
Ce talk est accessible à tous et ne se focalisera pas sur un langage / une API en particulier : les concepts prévaudront sur le code.
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Back to Basics - Ne perdez plus votre Temps avec les Dates - BordeauxJUG Edition
1. Ne perdez plus votre
Temps avec les Dates
#RetourAuxSources
4SH France
@fcamblor
Frédéric CAMBLOR
Bordeaux JUG
2019-02-21T19:00:00+01:00
#DateTimeBasics
2. Avertissement
Cette présentation n’aborde pas vraiment des technologies à la mode.
Pire, vous risquez de ressortir de cette présentation avec moins de
certitudes que vous n’en aviez en arrivant.
Acceptez-vous de poursuivre la présentation ?
Si vous voulez savoir comment déployer dans la blockchain des
microservices Kotlin avec Kubernetes, il est encore temps de
rentrer chez vous…
3.
4. Ne perdez plus votre
Temps avec les Dates
#RetourAuxSources
4SH France
@fcamblor
Frédéric CAMBLOR
Workshop 4SH
2019-02-13T18:00:00+01:00
#DateTimeBasics
21. Coordinated Universal Time (UTC)
• Basé sur le TAI
• Agnostic de la géographie / géopolitique
• Stable dans le temps
Pas d'heure d'été/hiver
• EPOCH : 1970-01-01
26. ❓Ce qu'on ne connaît pasℹ Ce qu'on connaît
Timestamps
Unix milliseconds : 1548954000000
Unix : 1548954000
•Le positionnement absolu
dans le temps
•Comparaison triviale
•La date/heure
•Est-ce le jour ou la nuit ?
•En sec. ou en millisec. ?
•La timezone
⚠ à l'overflow le 2038-01-19T03:14:07
27. ❓Ce qu'on ne connaît pasℹ Ce qu'on connaît
Datetime ISO 8601 / RFC 3339
2019-01-31T18:00:00+01:00
2019-01-31T17:00:00Z
•Le positionnement absolu
dans le temps
•Comparaison
(⚠ au timezone offset)
•Est-ce le jour ou la nuit ?
•Le jour de la semaine
•La timezone
https://www.w3.org/TR/NOTE-datetime
28. ❓Ce qu'on ne connaît pasℹ Ce qu'on connaît
Date & Time"Local"
Jeudi 31 Janvier 2019, 06:00 PM
2019-01-31T18:00:00
2019-01-31
•C'est un jeudi
•C'est le soir
•On est en hiver
•Quel point UTC ?
•Non comparable à une
date provenant d'une
autre région
•Quelle heure sera-t-il si
on ajoute 24h ?
36. TIMEZONES: Points clef
• Région du globe observant une évolution
uniforme de son heure
• Heure définie par des offsets depuis UTC
Ces offsets peuvent changer
• Les offsets sont "généralement" d'1h
• Ils varient de -12:00 à +14:00
• Référence : IANA & Windows TimeZones
37. Internet Assigned Numbers Authority (IANA) for Europe
https://github.com/eggert/tz/blob/master/europe
39. En JAVA c'est packagé dans les JRE
http://bit.ly/JRE-IANA
40. Pour MySQL il faut le faire à la main
http://bit.ly/mysql-tzdata
41. Pour NodeJS il faut le gérer à la main
https://www.npmjs.com/package/geo-tz
42. tldr; posez-vous la question !
L'OS peut aussi mettre à disposition ces tables
43. TZ Data: Points clef
• Très lié à la géopolitique
• Les règles changent régulièrement
• Les règles changent subitement
• Une multitude de libs existent
• Ne rien considérer comme acquis
⚠ ⚠ ⚠ aux dates dans le futur
44. Daylight Saving Time (DST)
moment.tz("2018-03-25T01:10:00", "Europe/Paris")
.format()
=> 2018-03-25T01:10:00+01:00
Certaines heures locales n'existent pas
(02:00 -> 03:00 pour Europe/Paris)
moment.tz("2018-03-25T01:10:00", "Europe/Paris")
.add(1, 'hours').format()
=> 2018-03-25T03:10:00+02:00
moment.tz("2018-03-25T02:10:00", "Europe/Paris")
.format()
=> 2018-03-25T03:10:00+02:00
45. Daylight Saving Time (DST)
moment.tz("2018-10-28T02:10:00", "Europe/Paris")
.format()
=> 2018-10-28T02:10:00+02:00
Certaines heures locales existent 2 fois
(02:00 -> 03:00 pour Europe/Paris)
moment.tz("2018-10-28T03:10:00", "Europe/Paris")
.format()
=> 2018-10-28T03:10:00+01:00
moment.tz("2018-10-28T02:10:00", "Europe/Paris")
.add(1, 'hour').format()
=> 2018-10-28T02:10:00+01:00
46. La date/heure DST varie en fonction de la timezone
http://bit.ly/dst-diffs
47. Consultation Européenne pour/contre DST
• Sondage sur 40j : 2018-07-04 --> 2018-08-16
• 4.6M de réponses
• 84% en faveur d'une abolition de DST
Cependant considérée comme un sondage non représentatif
https://ec.europa.eu/info/consultations/2018-summertime-
arrangements_en
49. "A one-hour shift is customary."
"Twenty-minute and two-hour shifts
have been used in the past."
https://en.wikipedia.org/wiki/Daylight_saving_time
50. Fausse bonne idée
"Notre batch de facturation passe tous les jours à
2h30 du matin (heure locale),car il s'agit du
moment où le système est le moins sollicité"
🤦 )
51. "Notre batch de facturation passe tous les
jours à 2h30 1h30 du matin (heure locale)
UTC, car il s'agit du moment où le système est
le moins sollicité"
Le batch s'exécutera (heure locale) à 2h30
(en hiver) ou 3h30 (en été) mais au moins il
passera tous les jours 1 et 1 seule fois.
57. 🤔
Qui a raison ?
public ZonedDateTime calculateNextThingAfter(ZonedDateTime d) {
return d.plusDays(1);
}
public ZonedDateTime calculateNextThingAfter(ZonedDateTime d) {
return d.plusHours(24);
}
#scaleMatters
58. Fun fact : aucun 2011-12-30 (Friday) aux Samoa
https://www.timeanddate.com/news/time/samoa-dateline.html
61. 🤮 #NIH
Comparator<String> strDatesComparator =
(strD1, strD2) -> strD1.compareTo(strD2);
✅ Il existe généralement une API...
Comparator<String> strDatesComparator =
Comparator.comparing(ZonedDateTime::parse);
✅ À défaut,passer par des timestamps
Comparator<String> strDatesComparator = ????
strDatesComparator.compare(
"2019-01-31T18:00:00-02:00[America/Sao_Paulo]"
"2019-01-31T20:00:00+01:00[Europe/Paris]")
62. 🤔
Mais où est le problème ?
boolean datesAreEqual(ZonedDateTime d1, ZonedDateTime d2) {
return Objects.equals(d1, d2);
}
65. Format standard => date UTC | Format non standard => date Locale
https://twitter.com/seldo/status/1091861205260500992
66. D'autres blagues
• Les mois commencent à 0
On a trouvé d'où vient le "Java" de "Javascript" !
• Aucune gestion des timezones
• Aucune gestion de l'internationalisation
• La gestion des "anciens" timezone offsets est
(complètement) broken
... chez tout le monde (Chrome, Firefox, IE, Safari, Node)
https://codeofmatt.com/javascript-date-type-is-horribly-broken/
68. Les perles en Java
• Années basées sur 1900
• Structures mutables (Date / Calendar)
• SimpleDateFormat non thread-safe
• Pas de notion de "Local" / "Zoned" Dates
• Indigestion de Constantes
• Des defaults basés sur "now" => difficilement testable
• Date.toString() qui se base sur la Locale système
new Date(2017, Calendar.FEBRUARY, 13).toString()
=> Tue Feb 13 00:00:00 GMT 3917 😱
69. Utilisez une librairie
• En JS :
• momentjs + moment-timezone
• dayjs / Luxon
• date-fns
• En Java : Joda time ou JSR 310 (Java 8+)
71. "Les dates dans le futur ?
Tranquille !"
Vous connaissez votre propre avenir,ce qui veut dire que vous
pouvez le changer si vous le voulez.Vous avez encore le choix.
Minority Report
72. Problème des dates dans le futur
Je suis actuellement à Bordeaux,j'aimerais programmer une
campagne d'envoi de mails toutes les semaines le lundi à 10h
sur les 52 semaines à venir.
• Solution Naïve : Générer tous les timestamps à l'avance
• Problème : les Timezones offset peuvent changer en
fonction de la géopolitique
• Que se passe-t-il si dans 6 mois l'Europe abolit l'heure
d'hiver ?
73. Lorsque vous stockez une Date+Heure
dans le futur,stockez en plus l'heure
et la timezone de l'utilisateur à
l'origine de la manipulation
Vous pourrez détecter & corriger le
problème s'il survient
(et que vous en êtes informé)
74. Bon,on devrait avoir fait
le tour de toutes les règles
bizarres maintenant...
L’ignorance c’est comme des chaussures trop petites !
Dr Who
75. Le premier jour de la semaine peut varier...
https://en.wikipedia.org/wiki/Week
76. La notion de Chronology
• ISO 8601 = Calendrier Grégorien de facto
• Il en existe plein d'autres : Julien, Ethiopien,
Bouddhiste, Islamiste, Copte...
• Règles différentes sur l'année de départ
• Règles différentes sur les années bissextiles
• Répartition différente des jours dans les mois
79. Les serveurs doivent être en UTC
•Timezone de l'OS
•Timezone de la DB
Il n'est pas tout le temps possible de la changer
•Timezone de votre serveur d'App
sudo timedatectl set-timezone UTC
# Or : sudo dpkg-reconfigure tzdata
-Duser.timezone=UTC
80. L'intervalle de temps implicite
👍 GET /messages?start=2019-02-09T00:00:00+01:00
&end=2019-02-09T23:59:59+01:00
👎 GET /messages?date=2019-02-09
👋 GET /messages?date=2019-02-09&tz=Europe/Paris
81. L'intervalle de temps implicite
// Assumes client and server timezones are the same
SELECT * FROM messages WHERE published_on = '2019-02-09';
👍 Expected👎 Result
👎 GET /messages?date=2019-02-09
82. L'intervalle de temps implicite
1/ Convert date to date midnight using Europe/Paris Timezone
2019-02-09 => 2019-02-09T00:00:00+01:00
👍 Expected
👋 GET /messages?date=2019-02-09&tz=Europe/Paris
👎 Result
# Swap 2 & 3 for expected result !
2/ Convert to UTC
=> 2019-02-08T23:00:00Z
3/ Transform to Range using startOf/endOf day
=> [ 2019-02-08T00:00:00Z, 2019-02-08T23:59:59Z ]
83. L'intervalle de temps implicite
1/ Convert date to date midnight using Europe/Paris Timezone
2019-02-09 => 2019-02-09T00:00:00+01:00
2/ Transform to Range using startOf/endOf day
=> [ 2019-02-09T00:00:00+01:00, 2019-02-09T23:59:59+01:00 ]
3/ Convert to UTC
=> [ 2019-02-08T23:00:00Z, 2019-02-09T22:59:59Z ]
👍 Expected
👋 GET /messages?date=2019-02-09&tz=Europe/Paris
👍 Result
84. L'intervalle de temps implicite
// Nothing fancy happening serverside since we're
// working with timestamps
SELECT * FROM messages WHERE published_on
BETWEEN '2019-02-08T23:00:00Z' AND '2019-02-09T23:00:00Z' ;
👍 Expected👍 Result
👍 GET /messages?start=2019-02-09T00:00:00+01:00
&end=2019-02-09T23:59:59+01:00
85. Date search Patterns
• Dans 99% des cas vous allez faire des
recherches sur des intervalles de temps
• Dans la mesure du possible, faites les
conversions coté appelant (client)
• Stockez vos datetime en UTC / Timestamp
86. Attention aux"Heures locales"
[On est le 21/02 à Bordeaux]
"Le magasin ouvre à 9h du matin"
"J'aimerais planifier une réunion à 15h
(Europe/Paris) tous les lundi"
Heure sans Date attachée
87. Attention aux"Heures locales"
👎 POST /recurrentMeetings
{ "frequency": "WEEKLY","weekDays": ["MONDAY"],
"utcTime": "14:00" }
[On est le 21/02 à Bordeaux]
"J'aimerais planifier une réunion à 15h (Europe/Paris) tous les lundi"
ℹ GET /meetings?date=2019-02-25&tz=Europe/Paris
[{"date": "2019-02-25T15:00:00+01:00"}]
ℹ GET /meetings?date=2019-06-24&tz=Europe/Paris
[{"date": "2019-06-24T16:00:00+02:00"}]
88. Attention aux"Heures locales"
[On est le 21/02 à Bordeaux]
"J'aimerais planifier une réunion à 15h (Europe/Paris) tous les lundi"
ℹ GET /meetings?date=2019-02-25&tz=Europe/Paris
[{"date": "2019-02-25T15:00:00+01:00"}]
ℹ GET /meetings?date=2019-06-24&tz=Europe/Paris
[{"date": "2019-06-24T15:00:00+02:00"}]
👍 POST /recurrentMeetings
{ "frequency": "WEEKLY","weekDays": ["MONDAY"],
"localTime": "15:00 [Europe/Paris]"}
ℹ GET /meetings?date=2019-06-24&tz=America/New_York
[{"date": "2019-06-24T09:00:00-04:00"}]
89. Time-only Patterns
• Évitez de stocker votre Time dans un
DateTime (1970-01-01T15:35:00Z)
• Stockez tout le temps la Timezone de
l'utilisateur ayant saisi l'heure
• Mettez à disposition la Timezone cible de
l'utilisateur faisant une recherche
90. Attention aux"Dates locales"
"Je suis né
le 29 juin 1983 près de Bordeaux [sous-entendu en France]"
"Georges Washington est né
le 22 février 1732 [lieu non précisé]"
Date sans Heure attachée,
et potentiellement sans Timezone
91. Attention aux"Dates locales"
• Pas de "Silver bullet" ici
• Dépend énormément :
• des informations disponibles (Timezone notamment)
• de ce qu'on veut faire de cette Date locale
• En Java, il n'existe qu'une LocalDate
• Date sans Heure ni Timezone
92. Date-only Patterns
• Évitez de stocker votre Date dans un
DateTime (1732-02-22T00:00:00Z)
• Si vous êtes sûr de la Timezone,
stockez-la
• Utilisez une structure de données adhoc
93. Les"Local Datetime"
Aucun intérêt d'avoir un DateTime
sans Timezone
À éviter,à tout prix !
http://blog.schauderhaft.de/2018/03/14/dont-use-
localdatetime/