1. Recycler ses calendriers
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
2. Perl et le bug de 2038
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, le 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ème 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 suivant 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 suffisamment 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 libres de le réutiliser librement.
Références
Page Wikipédia sur le bug de 2038 : http://fr.wikipedia.org/wiki/Bogue_de_l%27an_2038
Le module Time::y2038 : http://search.cpan.org/dist/Time-y2038/
Projet sur Google fournissant le code C : http://code.google.com/p/y2038/
Page collectant d'autres bugs similaires au bug de l'an 2000 et de 2038 : http://www.courtois.cc/humour/y2k.html
À vous !
Envoyez vos perles à perles@mongueurs.net, elles seront peut-être publiées dans un prochain numéro de Linux Magazine.