Quel est le mois le plus long en 2009 ?

Commençons par une devinette. Quel est le mois le plus long en 2009 ?

  use DateTime;
  use DateTime::Span;
  my $annee  = $ARGV[0] || 2009;
  my $fuseau = $ARGV[1] || 'Europe/Paris';
  my %mois_par_duree; # hachage de listes
  foreach (1..12) {
    my $debut    = DateTime->new(year => $annee, month => $_, day => 1,
                              time_zone => $fuseau, locale => 'fr');
    my $fin      = $debut->clone->add(months => 1);
    my $mois     = DateTime::Span->from_datetimes(start => $debut, end => $fin);
    my $duree    = $mois->duration;
    my $secondes = $duree->in_units('seconds');
    push @{$mois_par_duree{$secondes}}, $debut->strftime("%B %Y");
  }
  foreach (sort keys %mois_par_duree) {
    print "$_ : ", join ' ', @{$mois_par_duree{$_}}, "\n";
  }

Ce qui nous donne :

  2419200 : février 2009 
  2592000 : avril 2009 juin 2009 septembre 2009 novembre 2009 
  2674800 : mars 2009 
  2678400 : mai 2009 juillet 2009 août 2009 décembre 2009 
  2678401 : janvier 2009 
  2682000 : octobre 2009

Explication :

Le mois le plus long est le mois d'octobre, car il comporte 31 jours, dont 30 jours de 24 heures et 1 jour de 25 heures (en France métropolitaine et dans un certain nombre d'autres pays européens). Ensuite, c'est le mois de janvier, car il y a eu une seconde intercalaire le 31 décembre 2008 à 23 h 59 mn 60 s UTC, c'est-à-dire le 1er janvier 2009 à 0 h 59 mn 59 s dans le fuseau horaire de Paris.

On voit donc que le module DateTime, ou plutôt des modules annexes chargés de façon implicite connaissent les changements d'heure et les secondes intercalaires.


Et pourtant, c'était mal parti

À l'origine, Perl devait reprendre les bonnes idées de C, de Awk et du shell en laissant tomber les mauvaises idées de ces trois langages. Hélas, quelques défauts de C sont passés à travers les mailles du filet, dont deux concernent les dates :

  perl -e 'printf "%02d/%02d/%d\n", (gmtime())[3, 4, 5]'

qui donne pour le 2 octobre 2009 :

  02/09/109

Pourquoi avoir numéroté les mois de 0 à 11 ? L'argument de l'indexation d'un tableau par $mm ne tient pas, il est facile d'écrire

  print "Le joli mois de $nom_du_mois[$mm-1]"

Quant au nombre magique 1900...

Note d'espoir : la RFC-48 de Perl 6 suggère de corriger cette bourde en renvoyant un numéro de mois dans l'intervalle 1..12 et une année numériquement correcte. Il s'agirait de la fonction utctime au lieu de gmtime.


Première phase : chacun développe son propre module dans son coin.

  Date::Calc
  Date::Manip
  Date::Simple
  Date::Handler
  Class::Date

etc, jusqu'à

  Date::MMDDYY
  DateTime::PerpetualCalendar
  Date::LeapYear

Il existe aussi une liste (http://www.nntp.perl.org/group/perl.datetime/), mais son trafic est faible : 386 messages de la création en mai 2001 jusqu'en janvier 2003.


Le tournant : 9 janvier 2003, ``Picking up the ball''.

Message historique de Dave Rolsky, Picking up the ball (http://www.nntp.perl.org/group/perl.datetime/2003/01/msg388.html). Il décrit l'état désolant des modules de dates et d'heures et lance le développement d'un nouveau module (encore un).

Trafic de la liste : 320 messages la première semaine, le message de Dave a dû toucher une corde sensible chez plusieurs personnes. Il y a eu 466 messages en janvier 2003 et 4368 pour l'année 2003 dans son ensemble. Le trafic a considérablement diminué à partir de 2004 : 833 en 2004, 595 en 2005 pour arriver à 287 en 2008. Nous sommes donc revenus à un rythme assez calme, espérons que cela cessera de décroître.


Modules de base

  DateTime

pour le calendrier grégorien

  DateTime::Locale
  DateTime::Locale::fr
  DateTime::Locale::fr_BE
  DateTime::Locale::fr_CA
  DateTime::Locale::de
  DateTime::Locale::it

etc. : modules qui travaillent dans l'ombre de DateTime et permettent de traduire dans une langue autre que l'anglais

  DateTime::LeapSecond

Autre module travaillant dans l'ombre de DateTime pour tenir compte des secondes intercalaires comme celle de fin 2008 (début 2009 pour notre fuseau horaire).

  DateTime::TimeZone

Support des fuseaux horaires


Cas particulier : heure d'été

DateTime::TimeZone est basé sur la base de données Olson, peut-être la plus complète des bases existantes : elle donne la liste des plages de dates où l'heure d'été est en vigueur. Pour cette raison, entre autres, le nombre de fuseaux horaires dépasse largement 24.

On peut même voir à quelle époque l'heure légale française était basée sur le méridien de Paris : décalage de 9mn 21s par rapport à Greenwich.

  use DateTime;
  my ($aaaa, $mm, $jj, $tz) = @ARGV;
  my $auj = DateTime->now;
  $aaaa ||= $auj->year;
  $mm   ||= $auj->month;
  $jj   ||= $auj->day;
  $tz   ||= 'Europe/Paris';
  my $date = DateTime->new(year => $aaaa, month => $mm, day => $jj,
                           time_zone => $tz, locale => 'fr');
  my $fuseau = DateTime::TimeZone->new(name => $tz);
  my $decalage = $fuseau->offset_for_datetime($date);
  my $s =     $decalage         % 60;
  my $m = int($decalage /   60) % 60;
  my $h = int($decalage / 3600);
  print $date->strftime("%d/%m/%Y décalage $h h $m mn $s s%n");


Modules pour gérer les interactions entre dates

  DateTime::Span

Définit un intervalle de dates, avec une date de début et une date de fin.

  DateTime::Duration

Définit une durée. La solution évidente qui consiste à définir une durée comme un nombre de secondes et rien de plus n'est pas valide : 1 jour n'est pas 86400 secondes, car lorsque vous partez du 31/12/2008 à 12:00:00 et que vous ajoutez 86400 secondes, vous obtenez le 01/01/2009 à 11:59:59. De même, lorsque vous partez du 24/10/2009 à 12:00:00 et que vous ajoutez 86400 secondes, vous obtenez le 25/10/2009 à 11:00:00.

  DateTime::Set

Mieux que les tableaux @dates : plus efficace, permet de définir des ensembles infinis.

  DateTime::Event::Recurrence

Alternative au précédent lorsque les dates répondent à des critères de récurrence. Mais, me direz-vous, cela existe déjà avec ICal (RFC 2445). Effectivement, c'est pourquoi il y a :

  DateTime::Event::ICal

Par exemple, pour les réunions du groupe Paris.pm, le deuxième mercredi de chaque mois à 20 h, on pourrait définir une récurrence ICal par :

  'FREQ=MONTHLY;BYDAY=2WE;BYHOUR=20;BYMINUTE=0;BYSECOND=0'

et la donner à DateTime::Event::Ical. Mais c'est déjà fait avec :

  Acme::PM::Paris::Meetings


Modules de formattage

À part deux exceptions, ces modules ont été conçus pour une utilisation sérieuse, pour les bases de données et pour Internet.

  DateTime::Format::DBI
  DateTime::Format::MySQL
  DateTime::Format::Pg   (PostgreSQL)
  DateTime::Format::ICal
  DateTime::Format::HTTP
  DateTime::Format::Mail

Avec en plus des modules pour générer des modules de formattage :

  DateTime::Format::Builder
  DateTime::Format::Strptime


Les modules DateTime::Event::

Le lever et le coucher du soleil :

  DateTime::Event::Sunrise

Attention : contenait un bug jusqu'à la version 0.0501, cf http://rt.cpan.org/Public/Bug/Display.html. Heureusement, la version 0.0502 corrige ce bug.

Pour connaître la date de Pâques et des fêtes mobiles associées :

  DateTime::Event::Easter

Mais, même s'il en a été beaucoup question sur la liste, ne cherchez pas

  DateTime::Event::FourthOfJuly

Pour connaître le saint patron du jour :

  DateTime::Event::NameDay

Il existe également quelques modules un peu plus sérieux, comme DateTime::Event::Cron qui permet d'interfacer Perl avec cron ainsi que DateTime::Event::ICal et DateTime::Event::Recurrence déjà cités.

Anecdote : pendant la campagne d'Italie de 1943-1944, les troupes alliées sont restées bloquées plusieurs mois sur la ligne du Garigliano, devant le Mont Cassin. Les services de propagande de l'Axe se sont inspirés de la tradition selon laquelle les cloches vont à Rome à Pâques pour diffuser ce jour-là des tracts en forme de cloche et sur lesquels était écrit :

Je reviens de Rome... Les Anglais n'y étaient pas encore... Alors... à la Trinité ?

Avec le programme suivant :

  use strict;
  use warnings;
  use DateTime::Event::Easter;
  my $construc = DateTime::Event::Easter->new();
  my $paques = $construc->following(DateTime->new(year => 1944, month => 1, day => 1));
  print $paques->strftime("Pâques : %d/%m/%Y%n");
  print $paques->add(weeks => 8)->strftime("Trinité : %d/%m/%Y%n");

Vous pourrez constater qu'en 1944, le dimanche de Pâques était le 9 avril 1944 et que le dimanche de la Trinité était le 4 juin 1944... jour où les premiers éléments alliés sont entrés dans Rome.

Anecdote dans l'anecdote (même si cela nous éloigne de DateTime). L'association entre Pâques et la Trinité était déjà présente dans la chanson « Malbrouk s'en va-t-en guerre ». Dans cette chanson, Malbrouk est basé sur John Churchill, duc de Marlborough, général dans l'armée britannique et vainqueur de la bataille de Blenheim. Son descendant est Sir Winston Churchill de Marlborough, dont on peut dire qu'il était un peu impliqué dans la campagne d'Italie et la libération de Rome.


Modules de calendrier

Une autre devinette : Shakespeare et Cervantès sont tous les deux morts le 23 avril 1616. Lequel des deux est mort le premier ?

  use DateTime::Calendar::Christian;
  my $ws = DateTime::Calendar::Christian->new(
                       year  => 1616,
                       month => 4,
                       day   => 23,
                       reform_date => 'uk' );
  my $mc   = DateTime::Calendar::Christian->new(
                       year  => 1616,
                       month => 4,
                       day   => 23,
                       reform_date => 'italy' ); # même date que l'Espagne
  my $dt_ws = DateTime->from_object(object => $ws); # pour avoir un "overload"
  my $dt_mc = DateTime->from_object(object => $mc); # sur les comparaisons
  my $comp = 'le même jour que';
  $comp = 'avant' if $dt_mc < $dt_ws;
  $comp = 'après' if $dt_mc > $dt_ws;
  print "Cervantes est mort $comp Shakespeare\n";
  my $d = $dt_mc - $dt_ws;
  print abs($d->delta_days), " jour(s) d'écart\n";

Utilise

  DateTime                       (calendrier grégorien)
  DateTime::Calendar::Julian
  DateTime::Calendar::Christian  (bascule automatiquement de julien vers grégorien)

et nous apprenons que Cervantes est mort le premier, 10 jours avant Shakespeare.


Mais aussi

  DateTime::Calendar::Coptic
  DateTime::Calendar::Chinese
  DateTime::Calendar::Hebrew
  DateTime::Calendar::Hijri
  DateTime::Calendar::Japanese
  DateTime::Calendar::Maya
  DateTime::Calendar::FrenchRevolutionary
  DateTime::Calendar::Pataphysical
  DateTime::Format::Roman
  DateTime::Fiction::JRRTolkien::Shire

Ces modules sont un peu plus récréatifs que les modules DateTime::Event::xxx et surtout les modules DateTime::Format::xxx. Toutefois, certains sont très utilisés, notamment DateTime::Calendar::Japanese car les Japonais utilisent traditionnellement des dates basées sur les ères japonaises, c'est-à-dire l'accession au trône d'un nouvel empereur (merci à Sébastien pour ces précisions). D'autres modules sont peut-être utilisés pour des raisons analogues : les modules des calendriers julien, coptique, hébreu et chinois.

En revanche, les calendriers maya, révolutionnaire, hobbit et 'pataphysique sont là essentiellement pour la culture et la distraction. Par exemple, chaque fois que je me connecte sur ma machine, j'obtiens un message du genre :

  Grégorien : 2009-10-02, vendredi 02 octobre 2009
  JD 2455107.16666667 MJD 55106.6666666665
  Les saints du jour : Léger, Ruth
  La grande aiguille est sur le douze et la petite aiguille est sur le six
  Zee beeg hund is un zee tvelfe und zee little hund is un zee six. Bork, bork, bork!
  Lever du soleil à 07:50:47
  Coucher du soleil à 19:30:10
  Calendrier romain : VI Non oct. 2009
  Julien : 2009-09-19, Friday 19 September 2009
  Calendrier maya
  12.19.16.13.4 (baktun, katun, tun, uinal, kin) 2 Yax (Haab) 11 Kan (Tzolkin)
  Hébreu : 5770-07-14, Friday 14 Tishrei 5770
  Hijri : 1430-10-12 AH
  Hobbit : Monday 10 Winterfilth 7473
  Calendrier républicain
  0218-01-11T7:50:00, Primidi 11 Vendémiaire CCXVIII, jour de la pomme de terre
  11 Vendémiaire II Armée des Alpes. Capture du poste de Valmeyer, du poste de Beaufort, de Moutiers,
  du bourg Saint-Maurice et du Col de la Madeleine.
  11 Vendémiaire III Armée de Sambre-et-Meuse. Bataille d'Aldenhoven et déroute des troupes
  coalisées.
  11 Vendémiaire V. L'Armée du Rhin et Moselle attaque sur toute la ligne et met l'ennemi en déroute.
  Pataphysique
  137-01-25, mercredi 25 Absolu 137, Nativité de Sa Magnificence Opach

On remarque l'utilisation des deux modules DateTime::Format::xxx « récréatifs », DateTime::Format::Roman, qui utilise le système disparu des ides, des nones et des calendes et DateTime::Format::Baby, qui affiche l'heure comme le feraient un bébé et le Chef suédois du Muppet Show.

Autre remarque : de l'aveu même de son auteur, DateTime::Calendar::Hijri (calendrier de l'Hégire) donne des informations à titre indicatif, qui ne doivent pas être considérées comme faisant autorité. En effet, les irrégularités de ce calendrier font intervenir les autorités musulmanes, un peu comme les changements d'heure font intervenir les autorités nationales de chaque pays concerné et les secondes intercalaires font intervenir un organisme international. En fait, il faudrait que le module soit diffusé avec d'une façon analogue à la diffusion de DateTime::TimeZone et de DateTime::LeapSeconds, avec une nouvelle distribution chaque fois que les autorités concernées statuent sur les irrégularités du calendrier.


Et pour installer tout ça ?

Pas de problème, Eugene van der Pijll a créé :

  Bundle::DateTime::complete

Si, comme sur ma machine, CPAN.pm vous demande s'il faut installer les modules prérequis à chaque fois qu'il en manque, alors CPAN.pm vous demandera maintes fois s'il faut installer DateTime::Set et DateTime::Format::Builder. Donc, pour diminuer le nombre d'interactions, il peut se révéler judicieux d'installer ces deux modules en premier lieu, puis seulement après d'installer Bundle::DateTime::Complete.

Attention, certains modules sont bugués et ne peuvent pas s'installer.

  DateTime::Calendar::Hebrew
  DateTime::Fiction::JRRTolkien::Shire
  DateTime::Event::NameDay
  DateTime::TimeZone::Alias
  DateTime::TimeZone::LMT

DateTime::Calendar::Hebrew présente un problème avec le fuseau horaire « flottant ». Si vous spécifiez toujours un fuseau horaire précis, alors vous pouvez utiliser ce module après un force install. Post-scriptum de 2012 : une nouvelle version 0.05 écrite par un nouveau développeur est disponible depuis novembre 2011. Cette version corrige le problème du fuseau horaire « flottant ».

Pour DateTime::Fiction::JRRTolkien::Shire, un test échoue. Mais vous ne risquez pas grand chose à faire également un force install.

Pour DateTime::Event::NameDay, j'ai déclaré un bug 40834 avec un patch correctif. Donc, récupérez le patch, appliquez-le à la distribution et installez le module.

Pour DateTime::TimeZone::Alias, aucun ticket n'a été déclaré dans RT.

Pour DateTime::TimeZone::LMT, il y a un ticket 39340 mais pas de patch.

Un autre, DateTime::Event::Sunrise s'installe correctement mais donnait des résultats incorrects jusqu'à la version 0.0501, cf mon rapport de bug 34770. Donc, récupérez le patch, appliquez-le à la distribution et réinstallez le module.

Post-scriptum de 2013 : j'ai obtenu d'être co-mainteneur de DateTime::Event::Sunrise et la nouvelle version 0.0502 ne contient plus ce bug.

D'autres modules s'installent correctement, mais génèrent des avertissements à l'exécution : DateTime::Format::Baby, DateTime::Format::Roman et DateTime::Format::Pataphysical. J'ai déclaré des tickets sur CPAN pour chacun : 69841 pour DT::F::Baby, 69845 pour DT::F::Roman et 69846 pour DT::C::Pataphysical. Récupérez les patchs associés, appliquez-les et réinstallez les modules.

Post-scriptum de 2014 : BooK est maintenant co-mainteneur de DateTime::Calendar::Pataphysical et la nouvelle version 0.05 ne contient plus ce bug. Donc en fait, avant d'installer les patchs que je vous propose, vérifiez le statut du ticket sur RT.CPAN

Finalement, je n'ai pas installé DateTime::Calendar::Japanese ni DateTime::Calendar::Chinese à cause d'un prérequis, mais d'après les CPAN-Testers, si le prérequis est installé, ces deux modules n'ont pas de problème.

Remarque : je ne sais pas si les outils automatiques comme cpanm permettent d'installer un module en le patchant. Moi, je reviens à l'époque de make et j'applique la procédure suivante :

tar -zxvf DateTime-Event-NameDay-0.02.tar.gz
mv DateTime-Event-NameDay-0.02 DateTime-Event-NameDay-0.03
patch -p0 < dt-e-nameday.patch
cd DateTime-Event-NameDay-0.03
perl Makefile.PL
make
make test
sudo make install


Sur le web

Complet mais en anglais

  http://datetime.perl.org/

En français mais loin d'être complet ni à jour

  http://datetime.mongueurs.net/

La liste de diffusion (en anglais)

  http://lists.perl.org/showlist.cgi?name=datetime