File Coverage

blib/lib/Date/Roman.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Date::Roman;
2 9     9   11218 use Carp;
  9         18  
  9         982  
3 9     9   54459 use Roman qw(); #do not import roman and Roman, we want define our
  0            
  0            
4             #'roman' sub, which does something entierly different
5             #from Roman::roman
6              
7             use strict;
8              
9             use vars qw($VERSION);
10              
11             use constant LastJulian => 1582;
12              
13             # Symbolic constants to identify fixed days
14              
15             use constant Kalendae => 0;
16             use constant Nonae => 1;
17             use constant Idibus => 2;
18              
19             $VERSION = '1.06';
20              
21             # Array local variables containing months' names
22             my @MONS_SHORT = ('','Ian.','Feb.','Mar.','Apr.','Mai.','Iun.',
23             'Iul.','Aug.','Sep.','Oct.','Nov.','Dec.');
24              
25             my @MONS = ('','Ianuarias','Februarias','Martias','Apriles',
26             'Maias','Iunias','Iulias','Augustas','Septembres',
27             'Octobres','Novembres','Decembres');
28              
29             my @MONS_FD = ('','Ianuariis','Februariis','Martiis','Aprilibus',
30             'Maiis','Iuniis','Iuliis','Augustis','Septembribus',
31             'Octobribus','Novembribus','Decembribus');
32              
33              
34             # Array local variables containing fixed day names (Nota: the indexing
35             # is based on the constants above).
36              
37             my @FD_SHORT = ('Kal.','Non.','Id.');
38             my @FD = ('Kalendas','Nonas','Idus');
39             my @FD_FD = ('Kalendis','Nonis','Idibus');
40              
41              
42              
43              
44             sub new {
45             my $class = shift;
46             my ($ical,$roman);
47            
48             # default behavior: Date::Roman->new() is the same thing as
49             # Date::Roman->new(epoch => time())
50            
51             @_ = ('epoch',time()) unless @_;
52              
53             if (ref($_[0])) {
54             my $dateobject = shift;
55             if ($dateobject->can('roman')) {
56             $roman = _parse_roman($dateobject->roman());
57             }
58             elsif ($dateobject->can('ical')) {
59             $ical = _parse_ical($dateobject->ical());
60             }
61             else {
62             croak "Bad parameter to ",__PACKAGE__,"::new()";
63             }
64             }
65             elsif ($_[0] eq 'roman') {
66             $roman = _parse_roman($_[1]);
67             }
68             elsif ($_[0] eq 'ical') {
69             $ical = _parse_ical($_[1]);
70             }
71             elsif ($_[0] eq 'epoch') {
72             my ($day,$month,$year) = (localtime($_[1]))[3,4,5];
73             $ical->{day} = $day;
74             $ical->{month} = $month + 1;
75             $ical->{year} = $year + 1900;
76             }
77             else {
78             my %args = @_;
79             if (grep {exists $args{$_}} qw(base mons annus)) {
80             $args{ad} = 1 unless exists $args{ad};
81             $args{bis} = 0 unless exists $args{bis};
82             $args{base} = lc($args{base});
83             $args{base} = ($args{base} eq 'kal') ? Kalendae :
84             (($args{base} eq 'non') ? Nonae : Idibus);
85              
86             foreach (qw(base mons annus)) {
87             croak "Parameter $_ is mandatory" unless exists $args{$_};
88             }
89              
90             #I hate it...
91             foreach (qw(ad mons annus)) {
92             $args{$_} += 0;
93             }
94              
95             _check_roman(\%args);
96             $roman = \%args;
97             }
98             elsif (grep {exists $args{$_}} qw(day month year)) {
99             #I hate it...
100             foreach (keys %args) {
101             croak "Parameter $_ mandatory" unless exists $args{$_};
102             $args{$_} += 0;
103             }
104             _check_ical(\%args);
105             $ical = \%args;
106             }
107             else {
108             croak "Bad parameters to ",__PACKAGE__,"::new()";
109             }
110             }
111              
112             $roman = _ical_to_roman($ical) unless $roman;
113              
114            
115             bless $roman,$class;
116             }
117              
118             sub roman {
119             my $self = shift;
120            
121             if (@_) {
122             my $roman = shift;
123             my $rhash = _parse_roman($roman);
124             if ($rhash) {
125             %{$self} = %{$rhash};
126             }
127             else {
128             carp __PACKAGE__," Malformed roman date string $roman";
129             }
130             }
131            
132             my $result;
133             if ($self->{bis}) {
134             $result = "b6 ";
135             }
136             elsif ($self->{ad} == 2) {
137             $result = "pd ";
138             }
139             elsif ($self->{ad} > 2) {
140             $result = "$self->{ad} ";
141             }
142              
143             $result .= (qw(kal non id))[$self->{base}];
144              
145             $result .= " $self->{mons} $self->{annus}";
146              
147             return $result;
148             }
149              
150              
151             sub ical {
152             my $self = shift;
153             my $ihash;
154              
155             if (@_) {
156             my $ical = shift;
157             $ihash = _parse_ical($ical);
158             if ($ihash) {
159             my $rhash = _ical_to_roman($ihash);
160             %{$self} = %{$rhash};
161             }
162             else {
163             carp __PACKAGE__," Malformed ical string $ical";
164             }
165             }
166            
167             unless ($ihash) {
168             $ihash = _roman_to_ical($self);
169             }
170            
171             sprintf "%d%0.2d%0.2d",$ihash->{year},$ihash->{month},$ihash->{day};
172             }
173              
174              
175              
176             sub add {
177             my $self = shift;
178             my ($days) = @_;
179             my $class = ref($self);
180              
181             my %result = %{$self};
182             $result{ad} -= $days;
183            
184             _normalize_roman_date(\%result);
185              
186             bless \%result,$class;
187             }
188              
189             sub postridie {
190             $_[0]->add(1);
191             }
192              
193             sub heri {
194             $_[0]->add(-1);
195             }
196              
197              
198             sub as_string {
199             my $self = shift;
200             my %args = @_;
201             my %params = ();
202             my $string = "";
203              
204             $params{prefix} = $args{prefix} ||$args{words} || 'abbrev';
205             $params{die} = $args{die} || $args{num} || 'Roman';
206             $params{mons} = $args{mons} || $args{words} || 'abbrev';
207             $params{fday} = $args{fday} || $args{words} || 'abbrev';
208             $params{annus} = $args{annus} || $args{num} || 'Roman';
209             $params{auc} = $args{auc} || $args{words} || 'abbrev';
210              
211             if ($self->{ad} == 2) {
212             $string = ($params{prefix} eq 'abbrev') ? "p.d. " : "pridie ";
213             }
214             elsif ($self->{ad} > 2) {
215             $string = ($params{prefix} eq 'abbrev') ? "a.d. " : "ante diem ";
216             if ($params{die} eq 'Roman') {
217             $string .= Roman::Roman($self->{ad});
218             }
219             elsif ($params{die} eq 'roman') {
220             $string .= Roman::roman($self->{ad});
221             }
222             else {
223             $string .= $self->{ad};
224             }
225             $string .= " ";
226             }
227              
228            
229             if ($params{fday} eq 'abbrev') {
230             $string .= $FD_SHORT[$self->{base}]." ";
231             }
232             else {
233             $string .= ($self->{ad} == 0) ? "$FD_FD[$self->{base}] "
234             : "$FD[$self->{base}] ";
235             }
236              
237             if ($params{mons} eq 'abbrev') {
238             $string .= "$MONS_SHORT[$self->{mons}] ";
239             }
240             else {
241             $string .= ($self->{ad} == 0) ? $MONS_FD[$self->{mons}] : $MONS[$self->{mons}];
242             $string .= " ";
243             }
244              
245             if ($params{annus} eq 'Roman') {
246             $string .= Roman::Roman($self->{annus});
247             }
248             elsif ($params{annus} eq 'roman') {
249             $string .= Roman::roman($self->{annus});
250             }
251             else {
252             $string .= $self->{annus};
253             }
254              
255             if ($params{auc} eq 'abbrev') {
256             $string .= " AUC";
257             }
258             else {
259             $string .= " ab Urbe Condida";
260             }
261              
262             return $string;
263             }
264              
265              
266              
267              
268             #private subroutines
269              
270             # _parse_roman: takes a roman date string and returns a reference to
271             # an hash describing the corresponding roman date and ready to be
272             # blessed.
273              
274             my $roman_date_regexp = qr/^(?:(b6|pd|\d+)\s+)?(kal|non|id)\s+(\d+)\s+(\d+)/i;
275              
276             sub _parse_roman {
277             my $datestring = shift;
278              
279             my %result = (bis => 0);
280             unless ($datestring =~ /$roman_date_regexp/) {
281             croak "Malformed roman date string: $datestring";
282             }
283            
284             my ($prefix,$base,$mons,$annus) = map lc,($1,$2,$3,$4);
285              
286             if (!$prefix) {
287             $result{ad} = 1;
288             }
289             elsif ($prefix eq 'pd') {
290             $result{ad} = 2;
291             }
292             elsif ($prefix eq 'b6') {
293             $result{ad} = 6;
294             $result{bis} = 1;
295             }
296             else {
297             $result{ad} = $prefix;
298             }
299              
300             $result{base} = ($base eq 'kal') ? Kalendae :
301             (($base eq 'non') ? Nonae : Idibus);
302              
303             $result{mons} = $mons;
304             $result{annus} = $annus;
305              
306             #sanity checks
307             _check_roman(\%result) || carp "Malformed Roman date hash";
308              
309             return \%result;
310            
311             }
312              
313             # _check_roman: given a reference to an hash supposed to define a date
314             # in the Roman format (such the one returned by _parse_roman) it
315             # returns true if and only if the hash repesents a correct date.
316              
317             sub _check_roman {
318             my $rhr = shift;
319              
320             return undef unless $rhr->{base} >= Kalendae and $rhr->{base} <= Idibus;
321             return undef unless ($rhr->{mons} >= 1) and ($rhr->{mons} <= 12);
322             return undef if $rhr->{bis} and ($rhr->{base} != Kalendae or
323             $rhr->{mons} != 3 or
324             ! _leap($rhr->{annus},'roman'));
325             return undef unless $rhr->{ad} <= _days_before($rhr->{base},$rhr->{mons});
326             return 1;
327             }
328              
329              
330             # _parse_ical: takes a ical date string and returns a reference to
331             # an hash describing the corresponding date.
332              
333             my $ical_date_regexp = qr/^(-?\d+)(\d\d)(\d\d)(?:$|T)/;
334              
335             sub _parse_ical {
336             my $datestring = shift;
337             my %result = ();
338              
339             unless (@result{'year','month','day'} = map {$_+0} ($datestring =~ /$ical_date_regexp/)) {
340             croak "Malformed ical date string: $datestring";
341             }
342              
343             #sanity check
344             _check_ical(\%result) || carp "Malformed ical date hash";
345              
346             return \%result;
347             }
348              
349             # _check_ical: given a reference to an hash supposed to define a date
350             # in the ical format (such the one returned by _parse_ical) it
351             # returns true if and only if the hash repesents a correct date.
352             sub _check_ical {
353             my $ihr = shift;
354              
355             return undef if ($ihr->{month} < 1 or
356             $ihr->{month} > 12);
357              
358             return undef if ($ihr->{day} <= 0 or
359             $ihr->{day} > 31);
360              
361             return undef if ($ihr->{day} > 30 and
362             ($ihr->{month} == 4 or $ihr->{month} == 6 or
363             $ihr->{month} == 9 or $ihr->{month} == 11));
364              
365             return undef if ($ihr->{month} == 2 and
366             ($ihr->{day} > 29 or
367             ($ihr->{day} > 28 and !_leap($ihr->{year}))));
368             return 1;
369             }
370              
371              
372             # _normalize_month: Given a month number, 'normalize' it, i.e. replace
373             # it in the 1..12 interval.
374              
375             sub _normalize_month {
376             use integer;
377              
378             my ($mons) = @_;
379             my $result = (($_[0] - 1) % 12) + 1;
380              
381             unless (wantarray()) {
382             return $result;
383             }
384              
385             return ($result,$mons/12);
386             }
387              
388             sub _normalize_roman_date {
389             my $rhr = shift;
390              
391             # print "_normalize_roman_date({",
392             # join(",", map {"$_ => $rhr->{$_}"} keys %{$rhr}),
393             # "})\n";
394              
395             my ($mons,$deltay) = _normalize_month($rhr->{mons});
396              
397             $rhr->{mons} = $mons;
398             $rhr->{annus} += $deltay;
399             $rhr->{ad}++ if $rhr->{bis} or (($rhr->{mons} == 3) and
400             ($rhr->{base} == Kalendae) and
401             ($rhr->{ad} > 6) and
402             (_leap($rhr->{annus},'roman')));
403             $rhr->{bis} = 0;
404              
405             # print "After initialization:\n";
406             # print "{",join(",", map {"$_ => $rhr->{$_}"} keys %{$rhr}),"}\n";
407              
408             while (($rhr->{ad} > _days_before($rhr->{base},$rhr->{mons}) - 1) or
409             (($rhr->{base} == Kalendae) and
410             ($rhr->{mons} == 3) and
411             _leap($rhr->{annus},'roman') and
412             ($rhr->{ad} > 17))) {
413             #decrement $rhr->{ad} and set mons and annus accordingly
414             # print "decrementing\n";
415              
416             $rhr->{ad} -= _days_before($rhr->{base},$rhr->{mons});
417             $rhr->{ad}-- if (($rhr->{base} == Kalendae) and
418             ($rhr->{mons} == 3) and
419             _leap($rhr->{annus},'roman'));
420            
421              
422             $rhr->{base} = ($rhr->{base} - 1) % 3;
423             $rhr->{mons}-- if $rhr->{base} == Idibus;
424              
425             if ($rhr->{mons} == 0) {
426             $rhr->{mons} = 12;
427             $rhr->{annus}--;
428             }
429             }
430              
431             while ($rhr->{ad} < 1) {
432             # print "Incrementing\n";
433             #increment $rhr->{ad} and set mons and annus accordingly
434             $rhr->{base} = ($rhr->{base} + 1) % 3;
435              
436             $rhr->{mons}++ if $rhr->{base} == Kalendae;
437              
438             if ($rhr->{mons} == 13) {
439             $rhr->{mons} = 1;
440             $rhr->{annus}++;
441             }
442              
443             $rhr->{ad} += _days_before($rhr->{base},$rhr->{mons});
444            
445             $rhr->{ad}++ if (($rhr->{base} == Kalendae) and
446             ($rhr->{mons} == 3) and
447             _leap($rhr->{annus},'roman'));
448             }
449              
450             if (_leap($rhr->{annus},'roman') and
451             ($rhr->{mons} == 3) and
452             ($rhr->{base} == Kalendae) and
453             ($rhr->{ad} > 6)) {
454             $rhr->{bis} = 1 if $rhr->{ad} == 7;
455             $rhr->{ad}--;
456             }
457              
458             return $rhr;
459              
460             }
461              
462             # _days_before: Given a fixed day and a month returns the number of
463             # days existing in that month before that given fixed day. Reamrks
464             # that this is always the same, unregarding if the year is leap or
465             # not.
466              
467             sub _days_before {
468             my ($base,$month) = @_;
469             return 8 if $base == Idibus;
470            
471             return _fixed_day(Nonae,$month) -1 if $base == Nonae;
472              
473             #Kalendas
474             return _monthlength($month - 1) - _fixed_day(Idibus,$month - 1) + 1;
475             }
476              
477             # _fixed_day: Given a fixed day and a month returns the 'position' of
478             # the fixed day in the month. Month is normalized.
479              
480             sub _fixed_day {
481             my ($fd,$mons) = @_;
482              
483             return 1 if $fd == Kalendae;
484              
485             $mons = _normalize_month($mons);
486              
487             if ($fd == Idibus) {
488             return 15 if ($mons == 3) or ($mons == 5) or
489             ($mons == 7) or ($mons == 10);
490             return 13;
491             }
492             else {
493             return 7 if ($mons == 3) or ($mons == 5) or
494             ($mons == 7) or ($mons == 10);
495             return 5;
496             }
497             }
498              
499             # _monthlength: given a month number returns the length of the month.
500             #
501             # Notes:
502             #
503             # 1. for February we always returns 28.
504             #
505             # 2. month is normalized.
506              
507             sub _monthlength {
508             my $month = _normalize_month(shift);
509              
510             return 28 if $month == 2;
511             return 30 if ($month == 4 or $month == 6 or $month == 9 or $month == 11);
512             return 31;
513             }
514              
515             # _leap: is a given year leap?
516             sub _leap {
517             my $year = shift;
518             my $format = shift || 'christian';
519              
520             $year -= 753 if $format eq 'roman';
521            
522             return 0 if ($year % 4);
523             return 1 if ($year <= LastJulian) or ($year % 100);
524             return 0 if ($year % 400);
525             return 1;
526             }
527              
528              
529             # _ical_to_roman: Given a reference to an hash representing a date in
530             # the ical format (as returned by the _parse_ical sub) it returns a
531             # reference to an hash containing the corresponding Roman date, as
532             # returned by the _parse_roman sub.
533              
534             sub _ical_to_roman {
535             my $ihr = shift; #ical hash ref
536             my %result = (bis => 0);
537             my $fd;
538              
539             if ($ihr->{day} == 1) {
540             $result{ad} = 1;
541             $result{base} = Kalendae;
542             $result{mons} = $ihr->{month};
543             $result{annus} = $ihr->{year};
544             }
545             elsif ($ihr->{day} <= ($fd = _fixed_day(Nonae,$ihr->{month}))) {
546             $result{ad} = $fd - $ihr->{day} + 1;
547             $result{base} = Nonae;
548             $result{mons} = $ihr->{month};
549             $result{annus} = $ihr->{year};
550             }
551             elsif ($ihr->{day} <= ($fd = _fixed_day(Idibus,$ihr->{month}))) {
552             $result{ad} = $fd - $ihr->{day} + 1;
553             $result{base} = Idibus;
554             $result{mons} = $ihr->{month};
555             $result{annus} = $ihr->{year};
556             }
557             else {
558             $result{base} = Kalendae;
559             $result{mons} = ($ihr->{month} < 12) ? $ihr->{month} + 1 : 1;
560             $result{annus} = ($result{mons} == 1) ? $ihr->{year} + 1 : $ihr->{year};
561              
562             if ($result{mons} != 3 or !_leap($ihr->{year}) or ($ihr->{day} < 24)) {
563             $result{ad} = _monthlength($ihr->{month}) - $ihr->{day} + 2;
564             }
565             elsif ($ihr->{day} == 24) {
566             $result{bis} = 1;
567             $result{ad} = 6;
568             }
569             else {
570             $result{ad} = 31 - $ihr->{day};
571             }
572             }
573              
574             $result{annus} += 753;
575              
576             return \%result;
577             }
578              
579             # _roman_to_ical:
580             sub _roman_to_ical {
581             my $rhr = shift; # roman hash ref
582             my %result = ();
583              
584              
585             $result{year} = (($rhr->{mons} == 1) and
586             ($rhr->{base} == Kalendae) and
587             ($rhr->{ad} > 1)) ? $rhr->{annus} - 1 : $rhr->{annus};
588            
589             $result{year} -= 753;
590              
591             $result{month} = (($rhr->{base} != Kalendae) or ($rhr->{ad} == 1)) ?
592             $rhr->{mons} :
593             (($rhr->{mons} > 1) ? $rhr->{mons} - 1 : 12);
594              
595             if ($rhr->{base} == Kalendae) {
596             if ($rhr->{ad} == 1) {
597             $result{day} = 1;
598             }
599             else {
600             $result{day} = _monthlength($result{month}) - $rhr->{ad} + 2;
601             if ($result{month} == 2 and
602             ($rhr->{ad} < 6 or ($rhr->{ad} == 6 and !$rhr->{bis})) and
603             (_leap($result{year}))) {
604             $result{day}++;
605             }
606             }
607             }
608             else {
609             $result{day} = _fixed_day($rhr->{base},$rhr->{mons}) - $rhr->{ad} + 1;
610             }
611              
612             return \%result;
613             }
614             1;
615             __END__