mardi 10 avril 2012

CALENDRIERS : Perles de Mongueurs (48)

Perles de Mongueurs (48)

Article publié dans Linux Magazine 126, avril 2010.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]
  • Index

La perle de ce mois-ci a été rédigée par Philippe "BooK" Bruhat (book@mongueurs.net), de Lyon.pm et Paris.pm.

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

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, 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.

Références

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]

Aucun commentaire:

Enregistrer un commentaire