Perles de Mongueurs (48)

Article publié dans Linux Magazine 126, avril 2010.

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

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 :

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 :

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 :

    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 :

mais il faut également :

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

À vous !

Envoyez vos perles à perles@mongueurs.net, elles seront peut-être publiées dans un prochain numéro de Linux Magazine.

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