File Coverage

blib/lib/Date/Convert/French_Rev.pm
Criterion Covered Total %
statement 196 196 100.0
branch 94 94 100.0
condition 44 44 100.0
subroutine 23 23 100.0
pod 8 9 88.8
total 365 366 99.7


line stmt bran cond sub pod time code
1             # -*- encoding: utf-8; indent-tabs-mode: nil -*-
2             #
3             # Perl Date::Convert extension to convert dates from/to the French Revolutionary calendar
4             # Copyright (C) 2001-2003, 2013, 2015, 2020 Jean Forget
5             #
6             # See the license in the embedded documentation below.
7             #
8             package Date::Convert::French_Rev;
9              
10 13     13   713511 use utf8;
  13         228  
  13         70  
11 13     13   385 use strict;
  13         31  
  13         242  
12 13     13   57 use warnings;
  13         23  
  13         394  
13 13     13   64 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
  13         23  
  13         1061  
14 13     13   6098 use Date::Convert;
  13         41769  
  13         347  
15 13     13   109 use Carp;
  13         25  
  13         604  
16 13     13   5338 use Roman;
  13         9524  
  13         1138  
17              
18             require Exporter;
19              
20             @ISA = qw(Date::Convert Exporter);
21             # Do not export methods, therefore export nothing
22             @EXPORT = qw(
23              
24             );
25             $VERSION = '0.09';
26              
27 13     13   120 use constant REV_BEGINNING => 2375840; # 1 Vendémiaire I in the Revolutionary calendar
  13         25  
  13         5866  
28             my @MONTHS_SHORT = qw ( Vnd Bru Fri Niv Plu Vnt Ger Flo Pra Mes The Fru S-C);
29             my @MONTHS = qw(Vendémiaire Brumaire Frimaire
30             Nivôse Pluviôse Ventôse
31             Germinal Floréal Prairial
32             Messidor Thermidor Fructidor);
33             push @MONTHS, "jour complémentaire"; # Incompatible with qw(), because of embedded space
34              
35             # The day numer 10 is counterintuitively placed in the 0-th element
36             # because the modulus operator and the Perl arrays are 0-based.
37             # It works. Do not report a bug.
38             my @DECADE_DAYS = qw ( Décadi Primidi Duodi Tridi Quartidi Quintidi Sextidi Septidi Octidi Nonidi);
39             my @DECADE_DAYS_SHORT = qw ( Déc Pri Duo Tri Qua Qui Sex Sep Oct Non);
40              
41             # When initializing an array with lists within lists, it means one of two things:
42             # Either it is a newbie who does not know how to make multi-dimensional arrays,
43             # Or it is a (at least mildly) experienced Perl-coder who, for some reason,
44             # wants to initialize a flat array with the concatenation of lists.
45             # I am a (at least mildly) experienced programmer who wants to use qw() and yet insert
46             # comments in some places.
47             my @DAYS = (
48             # Vendémiaire
49             qw(
50             0raisin 0safran 1châtaigne 1colchique 0cheval
51             1balsamine 1carotte 2amaranthe 0panais 1cuve
52             1pomme_de_terre 2immortelle 0potiron 0réséda 2âne
53             1belle-de-nuit 1citrouille 0sarrasin 0tournesol 0pressoir
54             0chanvre 1pêche 0navet 2amarillis 0bœuf
55             2aubergine 0piment 1tomate 2orge 0tonneau
56             ),
57             # Brumaire
58             qw(
59             1pomme 0céleri 1poire 1betterave 2oie
60             2héliotrope 1figue 1scorsonère 2alisier 1charrue
61             0salsifis 1macre 0topinambour 2endive 0dindon
62             0chervis 0cresson 1dentelaire 1grenade 1herse
63             1bacchante 2azerole 1garance 2orange 0faisan
64             1pistache 0macjonc 0coing 0cormier 0rouleau
65             ),
66             # Frimaire
67             qw(
68             1raiponce 0turneps 1chicorée 1nèfle 0cochon
69             1mâche 0chou-fleur 0miel 0genièvre 1pioche
70             1cire 0raifort 0cèdre 0sapin 0chevreuil
71             2ajonc 0cyprès 0lierre 1sabine 0hoyau
72             2érable-sucre 1bruyère 0roseau 2oseille 0grillon
73             0pignon 0liège 1truffe 2olive 1pelle
74             ),
75             # Nivôse
76             qw(
77             1tourbe 1houille 0bitume 0soufre 0chien
78             1lave 1terre_végétale 0fumier 0salpêtre 0fléau
79             0granit 2argile 2ardoise 0grès 0lapin
80             0silex 1marne 1pierre_à_chaux 0marbre 0van
81             1pierre_à_plâtre 0sel 0fer 0cuivre 0chat
82             2étain 0plomb 0zinc 0mercure 0crible
83             ),
84             # Pluviôse
85             qw(
86             1lauréole 1mousse 0fragon 0perce-neige 0taureau
87             0laurier-thym 2amadouvier 0mézéréon 0peuplier 1coignée
88             2ellébore 0brocoli 0laurier 2avelinier 1vache
89             0buis 0lichen 2if 1pulmonaire 1serpette
90             0thlaspi 0thymelé 0chiendent 1traînasse 0lièvre
91             1guède 0noisetier 0cyclamen 1chélidoine 0traîneau
92             ),
93             # Ventôse
94             qw(
95             0tussilage 0cornouiller 0violier 0troène 0bouc
96             2asaret 2alaterne 1violette 0marceau 1bêche
97             0narcisse 2orme 1fumeterre 0vélar 1chèvre
98             3épinards 0doronic 0mouron 0cerfeuil 0cordeau
99             1mandragore 0persil 0cochléaria 1pâquerette 0thon
100             0pissenlit 1sylvie 0capillaire 0frêne 0plantoir
101             ),
102             # Germinal
103             qw(
104             1primevère 0platane 2asperge 1tulipe 1poule
105             1blette 0bouleau 1jonquille 2aulne 0couvoir
106             1pervenche 0charme 1morille 0hêtre 2abeille
107             1laitue 0mélèze 1ciguë 0radis 1ruche
108             0gainier 1romaine 0marronnier 1roquette 0pigeon
109             0lilas 2anémone 1pensée 1myrtille 0greffoir
110             ),
111             # Floréal
112             qw(
113             1rose 0chêne 1fougère 2aubépine 0rossignol
114             2ancolie 0muguet 0champignon 1hyacinthe 0râteau
115             1rhubarbe 0sainfoin 0bâton-d'or 0chamérisier 0ver_à_soie
116             1consoude 1pimprenelle 1corbeille-d'or 2arroche 0sarcloir
117             0staticé 1fritillaire 1bourrache 1valériane 1carpe
118             0fusain 1civette 1buglosse 0sénevé 1houlette
119             ),
120             # Prairial
121             qw(
122             1luzerne 2hémérocale 0trèfle 2angélique 0canard
123             1mélisse 0fromental 0martagon 0serpolet 1faulx
124             1fraise 1bétoine 0pois 2acacia 1caille
125             2œillet 0sureau 0pavot 0tilleul 1fourche
126             0barbeau 1camomille 0chèvre-feuille 0caille-lait 1tanche
127             0jasmin 1verveine 0thym 1pivoine 0chariot
128             ),
129             # Messidor
130             qw(
131             0seigle 2avoine 2oignon 1véronique 0mulet
132             0romarin 0concombre 2échalotte 2absinthe 1faucille
133             1coriandre 2artichaut 1giroflée 1lavande 0chamois
134             0tabac 1groseille 1gesse 1cerise 0parc
135             1menthe 0cumin 3haricots 2orcanète 1pintade
136             1sauge 2ail 1vesce 0blé 1chalémie
137             ),
138             # Thermidor
139             qw(
140             2épeautre 0bouillon-blanc 0melon 2ivraie 0bélier
141             1prêle 2armoise 0carthame 1mûre 2arrosoir
142             0panis 0salicor 2abricot 0basilic 1brebis
143             1guimauve 0lin 2amande 1gentiane 2écluse
144             1carline 0caprier 1lentille 2aunée 1loutre
145             1myrte 0colza 0lupin 0coton 0moulin
146             ),
147             # Fructidor
148             qw(
149             1prune 0millet 0lycoperde 2escourgeon 0saumon
150             1tubéreuse 0sucrion 2apocyn 1réglisse 2échelle
151             1pastèque 0fenouil 2épine-vinette 1noix 1truite
152             0citron 1cardère 0nerprun 0tagette 1hotte
153             2églantier 1noisette 0houblon 0sorgho 2écrevisse
154             1bigarade 1verge-d'or 0maïs 0marron 0panier
155             ),
156             # Jours complémentaires
157             qw(
158             1vertu 0génie 0travail 2opinion 3récompenses
159             1révolution
160             ));
161              
162             my @PREFIXES = ('jour du ', 'jour de la ', "jour de l'", 'jour des ');
163              
164 13     13   98 use constant NORMAL_YEAR => 365;
  13         32  
  13         687  
165 13     13   89 use constant LEAP_YEAR => 366;
  13         39  
  13         721  
166 13     13   75 use constant FOUR_YEARS => 4 * NORMAL_YEAR + 1; # one leap year every four years
  13         24  
  13         847  
167 13     13   91 use constant CENTURY => 25 * FOUR_YEARS - 1; # centuries aren't leap years...
  13         25  
  13         667  
168 13     13   69 use constant FOUR_CENTURIES => 4 * CENTURY + 1; # ...except every four centuries that are.
  13         25  
  13         769  
169 13     13   94 use constant FOUR_MILLENIA => 10 * FOUR_CENTURIES - 1; # ...except every four millenia that are not.
  13         32  
  13         21792  
170              
171             # number of days between the start of the revolutionary calendar, and the
172             # beginning of year n - 1
173             my @YEARS_BEGINS= (0, 365, 730, 1096, 1461, 1826, 2191, 2557, 2922, 3287, 3652,
174             4018, 4383, 4748, 5113, 5479, 5844);
175              
176             # This method shoudl be in the master class, but for the moment, it is only available here
177             sub change_to {
178 35 100   35 1 245 croak "Need to specify the new calendar"
179             if @_ <= 1;
180 34         61 my ($self, $new_cal) = @_;
181 34         103 $new_cal->convert($self);
182             }
183              
184             sub initialize {
185 174     174 1 150487 my $self = shift;
186 174         362 my ($year, $month, $day) = @_;
187 174 100 100     930 unless (defined($year) and defined($month) and defined($day))
      100        
188 4         28 { croak "Date::Convert::French_Rev::initialize needs more args" }
189 170         269 my $absol = REV_BEGINNING;
190 170         414 $$self{'year'} = $year;
191 170         265 $$self{'month'} = $month;
192 170         312 $$self{'day'} = $day;
193              
194 170         377 my $is_leap = Date::Convert::French_Rev->is_leap($year);
195 170 100       375 croak "year $year out of range" if $year <= 0;
196 168 100 100     565 croak "month $month out of range" if $month > 13 or $month <= 0;
197 166 100 100     379 croak "standard day number $day out of range" if $day <= 0 and $month <= 12;
198 165 100 100     363 croak "standard day number $day out of range" if $day > 30 and $month <= 12;
199 164 100 100     353 croak "additional day $day out of range" if ($month == 13) and ($day <= 0);
200 162 100 100     377 croak "additional day $day out of range" if ($month == 13) and ($day > 5) and !$is_leap;
      100        
201 161 100 100     365 croak "additional day $day out of range" if ($month == 13) and ($day > 6); # implying "and $is_leap" other cases already discarded
202              
203 159         234 $year --; #get years *before* this year. Makes math easier. :)
204             # first, convert year into days. . .
205 159 100       376 if ($year >= 16) # Romme rule in effect, or nearly so
206             {
207 98         265 $absol += int($year/4000) * FOUR_MILLENIA;
208 98         145 $year %= 4000;
209 98         160 $absol += int($year/400) * FOUR_CENTURIES;
210 98         126 $year %= 400;
211 98         171 $absol += int($year/100) * CENTURY;
212 98         128 $year %= 100;
213 98         151 $absol += int($year/4)* FOUR_YEARS;
214 98         132 $year %= 4;
215 98         135 $absol += $year * NORMAL_YEAR;
216             }
217             else # table look-up for the programmer-hostile equinox rule
218 61         109 { $absol += $YEARS_BEGINS[$year] }
219              
220             # now, month into days.
221 159         284 $absol += 30 * ($month - 1) + $day - 1;
222              
223 159         672 $$self{absol} = $absol;
224             }
225              
226             sub year {
227 573     573 1 788 my $self = shift;
228 573 100       1511 return $$self{year} if exists $$self{year}; # no point recalculating.
229 153         208 my $days;
230             my $year;
231             # note: years and days are initially days *before* today, rather than
232             # today's date. This is because of fenceposts. :)
233 153         202 $days = $$self{absol} - REV_BEGINNING;
234 153 100       288 if ($days < $YEARS_BEGINS[16]) {
235 64         116 $year = scalar grep { $_ <= $days } @YEARS_BEGINS;
  1088         1451  
236 64         96 $days -= $YEARS_BEGINS[$year - 1];
237 64         76 $days++;
238             }
239             else {
240 89         119 my $x;
241 89         138 $x = int ($days / FOUR_MILLENIA);
242 89         133 $year += $x * 4000;
243 89         114 $days -= $x * FOUR_MILLENIA;
244              
245 89         118 $x = int ($days / FOUR_CENTURIES);
246 89         111 $year += $x * 400;
247 89         106 $days -= $x * FOUR_CENTURIES;
248              
249 89         118 $x = int ($days / CENTURY);
250 89 100       153 $x = 3 if $x == 4; # last day of the 400-year period
251 89         126 $year += $x * 100;
252 89         129 $days -= $x * CENTURY;
253              
254 89         115 $x = int ($days / FOUR_YEARS);
255 89         104 $year += $x * 4;
256 89         99 $days -= $x * FOUR_YEARS;
257              
258 89         109 $x = int ($days / NORMAL_YEAR);
259 89 100       143 $x = 3 if $x == 4; # last day of the 4-year period
260 89         97 $year += $x;
261 89         100 $days -= $x * NORMAL_YEAR;
262              
263 89         104 ++$year; # because of 0-based mathematics vs 1-based chronology
264 89         114 ++$days;
265             }
266 153         240 $$self{year} = $year;
267 153         218 $$self{days_into_year} = $days;
268 153         227 return $year;
269             }
270              
271             sub month {
272 330     330 1 433 my $self = shift;
273 330 100       1254 return $$self{month} if exists $$self{month};
274 153         216 my $year = $self -> year;
275 153         236 my $days = $$self{days_into_year} - 1;
276 153         201 my $day = $days % 30;
277 153         184 $days -= $day;
278 153         240 my $month = $days / 30 + 1;
279 153         191 $$self{month} = $month;
280 153         224 $$self{day} = $day + 1;
281 153         216 return $month;
282             }
283              
284             sub day {
285 558     558 1 7862 my $self = shift;
286 558 100       1984 return $$self{day} if exists $$self{day};
287 25         55 $self->month; # calculates day as a side-effect
288 25         40 return $$self{day};
289             }
290              
291             sub date {
292 25     25 1 7436 my $self = shift;
293 25         48 return ($self->year, $self->month, $self->day);
294             }
295              
296             sub is_leap {
297 262     262 1 24694 my ($self, $year) = @_;
298 262 100       566 if (@_ == 1) {
299 46         102 $year = $self->year; # so is_leap can be static or method
300             }
301              
302             # Autumn equinox from I to XIX
303 262 100 100     1351 return 1 if ($year == 3) or ($year == 7) or ($year == 11) or ($year == 15);
      100        
      100        
304 241 100       559 return 0 if ($year < 20);
305              
306             # Romme rule from XX on
307 156 100       397 return 0 if $year % 4; # not a multiple of 4 -> normal year
308 90 100       180 return 1 if $year % 100; # a multiple of 4 but not of 100 -> leap year
309 71 100       187 return 0 if $year % 400; # a multiple of 100 but not of 400 -> normal year
310 32 100       84 return 1 if $year % 4000; # a multiple of 400 but not of 4000 -> leap
311 8         22 return 0; # multiple of 4000 -> normal year
312             }
313              
314             sub field {
315 378     378 0 1021 my ($self, $spec) = @_;
316 378         713 my $decade_day = $self->day % 10;
317             # below, a switch statement, more or less, as described in perlfaq7
318              
319 378 100       697 $spec eq '%d' && do { return sprintf "%02d", $self->day };
  13         21  
320 365 100       617 $spec eq '%j' && do { return sprintf "%03d", 30 * $self->month + $self->day - 30 };
  2         5  
321 363 100       571 $spec eq '%e' && do { return sprintf "%2d", $self->day };
  66         112  
322 297 100       440 $spec eq '%m' && do { return sprintf "%02d", $self->month };
  2         5  
323 295 100       470 $spec eq '%f' && do { return sprintf "%2d", $self->month };
  2         6  
324 293 100       748 $spec =~ /\%[YGL]/ && do { return sprintf "%04d", $self->year };
  9         27  
325 284 100       532 $spec =~ /\%B/ && do { return $MONTHS[$self->month - 1] };
  75         151  
326 209 100       381 $spec =~ /\%[bh]/ && do { return $MONTHS_SHORT[$self->month - 1] };
  6         13  
327 203 100       313 $spec eq '%y' && do { return sprintf "%02d", $self->year % 100 };
  2         6  
328 201 100       294 $spec eq '%n' && do { return "\n" };
  1         6  
329 200 100       304 $spec eq '%t' && do { return "\t" };
  1         9  
330 199 100       300 $spec eq '%+' && do { return '+' };
  1         7  
331 198 100       305 $spec eq '%%' && do { return '%' };
  4         29  
332 194 100       277 $spec eq '%a' && do { return $DECADE_DAYS_SHORT[$decade_day] };
  4         35  
333 190 100       291 $spec eq '%A' && do { return $DECADE_DAYS[$decade_day] };
  11         136  
334 179 100 100     267 $spec eq '%w' && do { return sprintf("%2d", $decade_day || 10) };
  3         30  
335 176 100       286 $spec eq '%EY' && do { return $self->year < 4000 ? Roman($self->year) : $self->year };
  91 100       155  
336 85 100       132 $spec eq '%Ey' && do { return $self->year < 4000 ? roman($self->year) : $self->year };
  4 100       5  
337             ($spec eq '%Ej' || $spec eq '%*')
338             && do
339 81 100 100     234 {
340 11         23 my $jj = 30 * $self->month + $self->day - 31; # %j is 1..366, but $jj is 0..365
341 11         24 my $lb = $DAYS[$jj];
342 11         22 $lb =~ s/_/ /g;
343 11         76 $lb =~ s/^(\d)/$PREFIXES[$1]/;
344 11         86 return $lb;
345             };
346             $spec eq '%EJ' && do
347 70 100       127 {
348 30         66 my $jj = 30 * $self->month + $self->day - 31; # %j is 1..366, but $jj is 0..365
349 30         55 my $lb = $DAYS[$jj];
350 30         63 $lb =~ s/_/ /g;
351             # Using a capitalized prefix, and capitalizing the first letter
352 30         203 $lb =~ s/^(\d)(.)/\u$PREFIXES[$1]\u$2/;
353 30         297 return $lb;
354             };
355             $spec eq '%Oj' && do
356 40 100       61 {
357 4         14 my $jj = 30 * $self->month + $self->day - 31; # %j is 1..366, but $jj is 0..365
358 4         19 my $lb = substr $DAYS[$jj], 1;
359 4         10 $lb =~ s/_/ /g;
360 4         31 return $lb;
361             };
362 36         280 return $spec;
363             }
364              
365             sub date_string {
366 119     119 1 52679 my ($self, $format) = @_;
367              
368             # Default value when not provided. I do not test true / false, because
369             # some adventurous mind could think that "0" is a valid format, even if false.
370 119 100 100     463 $format = "%e %B %EY" if (! defined $format or $format eq '');
371              
372 119         244 my $year = $self->year; # possibly to trigger the side effect
373 119         234 my $month = $self->month;
374 119         722 $format =~ s/( # start of $1
375             \% # percent sign
376             (?: # start of alternative
377             (?:O.) # extended field specifier: O with a second char
378             | # or
379             (?:E.) # other extended field specifier: E with a second char
380             | # or
381             . # basic field specifier: single char
382             )) # end of alternative and end of $1
383 378         17022 /'$self->field($1)'/eegx; # is there a simpler way to do it?
384              
385 119         4013 return $format;
386             }
387              
388             # A module must return a true value. Traditionally, a module returns 1.
389             # But this module is a revolutionary one, so it discards all old traditions.
390             "Liberté, égalité, fraternité
391             ou la mort !";
392              
393             __END__