Article publié dans Linux Magazine 126, avril 2010.
La perle de ce mois-ci a été rédigée par Philippe "BooK" Bruhat
(book@mongueurs.net
), de Lyon.pm et Paris.pm.
Nous achetons chaque année de grands calendriers avec photos pour décorer notre appartement. Certaines photos sont si jolies qu'on regrette de ne pouvoir les contempler qu'un mois.
Si on ne tient pas compte de l'année, il est possible de réutiliser un calendrier, pourvu que les jours se correspondent. Par exemple, en 2009, nous pouvions utiliser un calendrier de 1998, comme nous le montre la commande cal(1):
janvier 1998 janvier 2009 lu ma me je ve sa di lu ma me je ve sa di 1 2 3 4 1 2 3 4 5 6 7 8 9 10 11 5 6 7 8 9 10 11 12 13 14 15 16 17 18 12 13 14 15 16 17 18 19 20 21 22 23 24 25 19 20 21 22 23 24 25 26 27 28 29 30 31 26 27 28 29 30 31
Les critères de "recyclage" sont les suivants :
le 1er janvier doit commencer le même jour,
les deux années doivent être toutes deux bissextiles ou non.
La relation d'équivalence que nous venons de définir entre deux années « échangeables » nous permet de définir des classes d'équivalences dans lesquelles nous allons pouvoir classer les années.
Il est facile de calculer qu'il y a 14 classes d'équivalence, en multipliant le nombre de jours de la semaine (7) par le nombre de possibilités d'être bissextile ou non (2).
Pour déterminer l'appartenance d'une année à une classe d'équivalence, il suffit de calculer une simple « somme de contrôle ». Deux années ayant la même somme de contrôle sont échangeables.
Si on prend un objet DateTime
pour représenter le premier janvier
de l'année en question, deux formules sont utilisables :
7 * $dt->is_leap_year + $dt->day_of_week
2 * $dt->day_of_week + $dt->is_leap_year
Même si les deux calculs ne donnent pas le même résultat pour une année donnée, ils définissent les mêmes classes d'équivalence (le résultat sera identique pour toutes les années équivalentes).
Pour savoir si l'année est bissextile, le calcul sera différent selon la formule choisie. Avec la première formule, la comparaison du résultat avec 7 ou 8 nous permet de savoir si l'année est bissextile : de 1 à 7 c'est une année normale, de 8 à 15 c'est une année bissextile.
Avec la seconde formule, la parité du résultat nous permet de savoir si la classe d'équivalence correspond à une année bissextile ou non.
use strict; use warnings; use DateTime; my %classe; # range chaque année dans sa classe d'équivalence for my $year ( 1970 .. 2038 ) { # calcule la somme de contrôle de l'année courante my $dt = DateTime->new( year => $year, month => 1, day => 1 ); my $type = 2 * $dt->day_of_week + $dt->is_leap_year; # ajoute l'année à sa classe d'équivalence push @{ $classe{$type} }, $year; } # affiche les différentes classes d'équivalence # triées par la première année qu'elles contiennent # préfixe les années bissextiles d'une étoile print $_ % 2 ? '*' : ' ', " @{ $classe{$_} }\n" for sort { $classe{$a}[0] <=> $classe{$b}[0] } keys %classe;
Et voici le résultat pour une plage de dates « classique » :
1970 1981 1987 1998 2009 2015 2026 2037 1971 1982 1993 1999 2010 2021 2027 2038 * 1972 2000 2028 1973 1979 1990 2001 2007 2018 2029 2035 1974 1985 1991 2002 2013 2019 2030 1975 1986 1997 2003 2014 2025 2031 * 1976 2004 2032 1977 1983 1994 2005 2011 2022 2033 1978 1989 1995 2006 2017 2023 2034 * 1980 2008 2036 * 1984 2012 * 1988 2016 * 1992 2020 * 1996 2024
On peut facilement vérifier qu'on obtient le même résultat avec chacune des deux formules (sauf pour l'étoile qui marque les années bissextiles, bien sûr).
Évidemment, les années bissextiles ne se produisent pas très souvent. On peut donc essayer de tourner avec moins de calendriers en changeant de calendrier après le 29 février.
Les années bissextiles, le 29 février sera un jour imaginaire (au sens où il ne sera pas inscrit sur le calendrier) et on changera de calendrier le premier mars.
Nous aurons donc besoin de deux nouvelles relations d'équivalences, qui ne tiennent pas compte du caractère bissextile d'une année :
les années pour lesquelles le premier janvier est le même jour de la semaine
les années pour lesquelles le premier mars est le même jour de la semaine
use strict; use warnings; use DateTime; my %classe; my (%janvier, %mars); # range chaque année dans sa classe d'équivalence for my $year ( 1970 .. 2038 ) { # calcule la somme de contrôle de l'année courante my $dt = DateTime->new( year => $year, month => 1, day => 1 ); my $type = 7 * $dt->is_leap_year + $dt->day_of_week; # ajoute l'année à sa classe d'équivalence push @{ $classe{$type} }, $year; # classe de janvier push @{ $janvier{ $dt->day_of_week } }, $year; # classe de mars $dt->set_month(3); push @{ $mars{ $dt->day_of_week } }, $year; } # affiche les différente classes d'équivalence # triées par la première année qu'elles contiennent for ( sort { $classe{$a}[0] <=> $classe{$b}[0] } keys %classe ) { print "@{ $classe{$_} }\n"; my $dt = DateTime->new( year => $classe{$_}[0], month => 1, day => 1 ); if( $dt->is_leap_year) { print " Jan: @{ $janvier{ $dt->day_of_week } }\n"; $dt->set_month(3); print " Mar: @{ $mars{ $dt->day_of_week } }\n"; } }
On obtient les mêmes résultats pour les années normales, avec des solutions de substitutions pour les années bissextiles :
1970 1981 1987 1998 2009 2015 2026 2037 1971 1982 1993 1999 2010 2021 2027 2038 1972 2000 2028 Jan: 1972 1977 1983 1994 2000 2005 2011 2022 2028 2033 Mar: 1972 1978 1989 1995 2000 2006 2017 2023 2028 2034 1973 1979 1990 2001 2007 2018 2029 2035 1974 1985 1991 2002 2013 2019 2030 1975 1986 1997 2003 2014 2025 2031 1976 2004 2032 Jan: 1970 1976 1981 1987 1998 2004 2009 2015 2026 2032 2037 Mar: 1971 1976 1982 1993 1999 2004 2010 2021 2027 2032 2038 1977 1983 1994 2005 2011 2022 2033 1978 1989 1995 2006 2017 2023 2034 1980 2008 2036 Jan: 1974 1980 1985 1991 2002 2008 2013 2019 2030 2036 Mar: 1975 1980 1986 1997 2003 2008 2014 2025 2031 2036 1984 2012 Jan: 1978 1984 1989 1995 2006 2012 2017 2023 2034 Mar: 1973 1979 1984 1990 2001 2007 2012 2018 2029 2035 1988 2016 Jan: 1971 1982 1988 1993 1999 2010 2016 2021 2027 2038 Mar: 1977 1983 1988 1994 2005 2011 2016 2022 2033 1992 2020 Jan: 1975 1986 1992 1997 2003 2014 2020 2025 2031 Mar: 1970 1981 1987 1992 1998 2009 2015 2020 2026 2037 1996 2024 Jan: 1973 1979 1990 1996 2001 2007 2018 2024 2029 2035 Mar: 1974 1985 1991 1996 2002 2013 2019 2024 2030
Sous Unix, les dates sont stockées sous forme d'un nombre entier de
secondes, comptées depuis l'epoch (date arbitrairement fixée à
1970-01-01 00:00:00 GMT
). Sur les systèmes où cet entier est un
entier signé de 32 bits, cela limite le nombre de secondes à 2 ** 31 - 1
,
soit 2 147 483 647 secondes. Autrement dit, 19 janvier 2038 à 3 h 14
min 7 s temps universel, le système se croira le 13 décembre 1901.
Mais le problème ne se posera pas seulement dans un peu moins de trente ans. Beaucoup de monde utilise le système basé sur l'epoch pour représenter des dates dans le futur. Et il est des applications qui s'intéressent à des dates 30 ans dans le futur (au hasard, les prêts bancaires).
Les développeurs de Perl sont très attentifs à ce genre de problèmes
de compatibilité. C'est pourquoi Michael Schwern a écrit le module
Time::y2038
, qui fournit des versions des fonctions internes de Perl
(gmtime()
, localtime()
, timegm()
, timelocal()
) compatibles
avec des dates post-2038.
Cependant, pour Time::y2038
, les contraintes de réutilisation des
dates sont un peu plus fortes, car le module doit tenir compte des
fuseaux horaires, en particulier pour tenir compte des horaires d'été,
très variables suivants les régions du monde.
Pour une année donnée après 2038, le module va sélectionner la dernière année correspondante dans le calendrier de 28 ans prédéfini. Pour que deux années se correspondent, il faut non seulement :
qu'elles commencent le même jour de la semaine,
qu'elles aient toutes deux le même nombre de jour en février,
mais il faut également :
que les années précédentes se correspondent, afin que lorsqu'on fait des calculs de date le premier janvier avec un fuseau horaire pré-UTC, le 31 décembre corresponde également,
que l'année suivante commence le même jour de la semaine, quand on fait des calculs le 31 décembre avec un fuseau horaire post-UTC.
Dans le second cas, l'état bissextile ou non n'a pas d'importance, car on s'intéresse seulement au premier janvier. Ce mécanisme est suffisant efficace pour permettre de travailler avec des dates allant jusqu'après 2400 (modulo les bases de locales qui ne seront pas à jour).
À noter que le code C derrière ce module Perl a été repris d'un code déjà existant, amélioré et retouché pour être rendu portable, permettant ainsi aux auteurs d'autres projets projets libres de le réutiliser librement.
http://fr.wikipedia.org/wiki/Bogue_de_l%27an_2038 - Page Wikipédia sur le bug de 2038.
http://search.cpan.org/dist/Time-y2038/
- Le module Time::y2038
.
http://code.google.com/p/y2038/ - Projet sur Google fournissant le code C
http://www.courtois.cc/humour/y2k.html - Page collectant d'autres bugs similaires au bug de l'an 2000 et de 2038.
Envoyez vos perles à perles@mongueurs.net
, elles seront peut-être
publiées dans un prochain numéro de Linux Magazine.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.