File Coverage

blib/lib/DateTime/Calendar/Discordian.pm
Criterion Covered Total %
statement 135 135 100.0
branch 64 64 100.0
condition 25 27 92.5
subroutine 27 27 100.0
pod 13 13 100.0
total 264 266 99.2


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             DateTime::Calendar::Discordian - Perl extension for the Discordian Calendar
5              
6             =head1 SYNOPSIS
7              
8             use DateTime::Calendar::Discordian;
9              
10             =head1 ABSTRACT
11              
12             A module that implements the Discordian calendar made popular(?) in the
13             "Illuminatus!" trilogy by Robert Shea and Robert Anton Wilson and by the
14             Church of the SubGenius.
15              
16             =cut
17              
18             package DateTime::Calendar::Discordian;
19              
20 15     15   3039498 use strict;
  15         38  
  15         730  
21 15     15   92 use warnings;
  15         29  
  15         474  
22 15     15   580 use 5.010;
  15         54  
  15         730  
23 15     15   90 use Carp;
  15         28  
  15         1508  
24 15     15   12353 use DateTime::Locale;
  15         1017846  
  15         628  
25 15     15   134 use Params::Validate qw( validate SCALAR OBJECT UNDEF);
  15         37  
  15         46687  
26              
27             =head1 VERSION
28              
29             This document describes DateTime::Calendar::Discordian version 1.0
30              
31             =cut
32              
33             our $VERSION = '1.0';
34              
35             =head1 DESCRIPTION
36              
37             =head2 The Discordian Calendar
38              
39             =head3 Seasons
40              
41             Name Patron apostle
42             ---- --------------
43             Chaos Hung Mung
44             Discord Dr. Van Van Mojo
45             Confusion Sri Syadasti
46             Bureaucracy Zarathud
47             The Aftermath The Elder Malaclypse
48              
49             Each season contains 73 consecutively numbered days.
50              
51             =head3 Holydays
52              
53             Apostle Holydays Season Holydays
54             ---------------- ---------------
55             1) Mungday 1) Chaoflux
56             2) Mojoday 2) Discoflux
57             3) Syaday 3) Confuflux
58             4) Zaraday 4) Bureflux
59             5) Maladay 5) Afflux
60              
61             Apostle Holydays occur on the 5th day of the season.
62              
63             Season Holydays occur on the 50th day of the deason.
64              
65             St. Tib's Day occurs once every 4 years (1+4=5) and is inserted between
66             the 59th and 60th days of the Season of Chaos.
67              
68             The era of the Discordian Calendar is called Year Of Lady Discord
69             (YOLD.) Its' epoch (Confusion 1 of year 0) is equivalent to January 1,
70             -1167 B.C.
71              
72             X Day is when the Church of the SubGenius believes the alien X-ists will
73             destroy the world. The revised date is equivalent to Confusion 40, 9827
74             YOLD.
75              
76             =cut
77              
78             my %seasons = (
79             'Chaos' => {
80             abbrev => 'Chs',
81             offset => 0,
82             apostle_holyday => 'Mungday',
83             season_holyday => 'Chaoflux',
84             },
85             'Discord' => {
86             abbrev => 'Dsc',
87             offset => 73,
88             apostle_holyday => 'Mojoday',
89             season_holyday => 'Discoflux',
90             },
91             'Confusion' => {
92             abbrev => 'Cfn',
93             offset => 146,
94             apostle_holyday => 'Syaday',
95             season_holyday => 'Confuflux',
96             },
97             'Bureaucracy' => {
98             abbrev => 'Bcy',
99             offset => 219,
100             apostle_holyday => 'Zaraday',
101             season_holyday => 'Bureflux',
102             },
103             'The Aftermath' => {
104             abbrev => 'Afm',
105             offset => 292,
106             apostle_holyday => 'Maladay',
107             season_holyday => 'Afflux',
108             },
109             );
110              
111             my $tibsday = qr/s(?:ain)?t[.]?\s*tib'?s?\s*(?:day)?/imsx;
112              
113             =head3 Days Of The Week
114              
115             1. Sweetmorn
116             2. Boomtime
117             3. Pungenday
118             4. Prickle-Prickle
119             5. Setting Orange
120              
121             The days of the week are named from the five Basic Elements: sweet,
122             boom, pungent, prickle and orange.
123              
124             =cut
125              
126             my @days = (
127             { name => 'Sweetmorn', abbrev => 'SM', },
128             { name => 'Boomtime', abbrev => 'BT', },
129             { name => 'Pungenday', abbrev => 'PD', },
130             { name => 'Prickle-Prickle', abbrev => 'PP', },
131             { name => 'Setting Orange', abbrev => 'SO', },
132             );
133              
134             my @excl = (
135             'Hail Eris!',
136             'All Hail Discordia!',
137             'Kallisti!',
138             'Fnord.',
139             'Or not.',
140             'Wibble.',
141             'Pzat!',
142             q{P'tang!},
143             'Frink!',
144             'Slack!',
145             'Praise "Bob"!',
146             'Or kill me.',
147             'Grudnuk demand sustenance!',
148             'Keep the Lasagna flying!',
149             'Umlaut Zebra über alles!',
150             'You are what you see.',
151             'Or is it?',
152             'This statement is false.',
153             'Hail Eris, Hack Perl!',
154             );
155              
156             =head1 METHODS
157              
158             =head2 new
159              
160             Constructs a new I object. This class
161             method requires the parameters I, I, and I. If
162             I is given as "St. Tib's Day" (or reasonable facsimile thereof,)
163             then I is omitted. This function will C if invalid
164             parameters are given. For example:
165              
166             my $dtcd = DateTime::Calendar::Discordian->new(
167             day => 8, season => 'Discord', year => 3137, );
168              
169             The I, I, and I parameters are also accepted for
170             compatability with L but nothing is done with them.
171              
172             =cut
173              
174             sub new {
175 217     217 1 3400 my ( $class, @arguments ) = @_;
176              
177             my %args = validate(
178             @arguments,
179             {
180             day => {
181             callbacks => {
182             q{between 1 and 73 or St. Tib's Day} => sub {
183 217     217   450 my ( $day, $opts ) = @_;
184 217 100 100     2109 if ( $day =~ $tibsday ) {
    100          
185 10 100       40 if ( !defined $opts->{season} ) {
186 9         159 return 1;
187             }
188             }
189             elsif ( $day > 0 && $day < 74 ) {
190 205         3411 return 1;
191             }
192 3         763 return;
193             },
194             },
195             },
196             season => {
197             default => undef,
198             callbacks => {
199             'valid season name' => sub {
200 211     211   368 my ( $season, $opts ) = @_;
201 211 100       552 if ( defined $season ) {
202 206         743 return scalar grep { /((?-x)$season)/imsx }
  1030         8421  
203             keys %seasons;
204             }
205 5         65 return 1;
206             },
207             },
208             },
209 217         6966 year => { type => SCALAR, },
210             second => { default => 0, },
211             nanosecond => { default => 0, },
212             locale => {
213             type => SCALAR | OBJECT | UNDEF,
214             optional => 1,
215             },
216              
217             }
218             );
219              
220 213 100       3733 if ( defined $args{season} ) {
221 203         782 $args{season} = join q{ }, map { ucfirst lc $_ } split q{ },
  228         1536  
222             $args{season};
223             }
224             else {
225 10 100       73 if ( $args{day} !~ $tibsday ) {
226 1         147 confess 'missing season';
227             }
228             }
229 212 100       939 if ( $args{day} =~ $tibsday ) {
230 9         22 $args{day} = q{St. Tib's Day};
231             }
232 212 100 100     679 croak q{Not a leap year}
233             if $args{day} eq q{St. Tib's Day}
234             && !_is_leap_year( $args{year} - 1166 );
235 210         739 my $self = bless \%args, $class;
236 210         579 $self->{epoch} = -426_237;
237 210         447 $self->{fnord} = 5;
238 210 100       495 if ( defined $self->{locale} ) {
239 3 100       80 if ( !ref $self->{locale} ) {
240 1         6 $self->{locale} = DateTime::Locale->load( $args{locale} );
241             }
242             }
243 210         568 $self->{rd} = $self->_discordian2rd;
244              
245 210         1023 return bless $self, $class;
246             }
247              
248             =head2 clone
249              
250             Returns a copy of the object.
251              
252             =cut
253              
254             sub clone {
255 1     1 1 8 my ($object) = @_;
256 1         2 return bless { %{$object} }, ref $object;
  1         44  
257             }
258              
259             =head2 day
260              
261             Returns the day of the season as a number between 1 and 73 or the string
262             "St. Tib's Day".
263              
264             =cut
265              
266             sub day {
267 43     43 1 147 my ($self) = @_;
268              
269 43         182 return $self->{day};
270             }
271              
272             =head2 day_abbr
273              
274             Returns the name of the day of the week in abbreviated form or false if
275             it is "St. Tib's Day".
276              
277             =cut
278              
279             sub day_abbr {
280 35     35 1 65 my ($self) = @_;
281              
282 35 100       84 if ( $self->{day} eq q{St. Tib's Day} ) {
283 1         5 return;
284             }
285              
286 34         74 my $day_of_year = $seasons{ $self->{season} }->{offset} + $self->{day};
287 34         194 return $days[ ( $day_of_year - 1 ) % 5 ]->{abbrev};
288             }
289              
290             =head2 day_name
291              
292             Returns the full name of the day of the week or "St. Tib's Day" if it is
293             that day.
294              
295             =cut
296              
297             sub day_name {
298 36     36 1 62 my ($self) = @_;
299              
300 36 100       155 return $self->{day} if ( $self->{day} eq q{St. Tib's Day} );
301              
302 35         65 my $day_of_year = $seasons{ $self->{season} }->{offset} + $self->{day};
303 35         169 return $days[ ( $day_of_year - 1 ) % 5 ]->{name};
304             }
305              
306             =head2 days_till_x
307              
308             Returns the number of days until X Day.
309              
310             =cut
311              
312             sub days_till_x {
313 2     2 1 151 my ($self) = @_;
314 2         18 return 3_163_186 - $self->{rd};
315             }
316              
317             =head2 from_object
318              
319             Builds a I object from another
320             I object. This function takes an I parameter and
321             optionally I. For example:
322              
323             my $dtcd = DateTime::Calendar::Discordian->from_object(
324             object => DateTime->new(day => 22, month => 3, year => 1971,));
325              
326             =cut
327              
328             sub from_object {
329 74     74 1 39637 my ( $class, @arguments ) = @_;
330 74         1771 my %args = validate(
331             @arguments,
332             {
333             object => {
334             type => OBJECT,
335             can => 'utc_rd_values',
336             },
337             locale => {
338             type => SCALAR | OBJECT | UNDEF,
339             default => undef,
340             },
341             },
342             );
343              
344 74 100       3634 if ( $args{object}->can('set_time_zone') ) {
345 73         256 $args{object} = $args{object}->clone->set_time_zone('floating');
346             }
347 74         2160 my ( $rd_days, $rd_secs, $rd_nanosecs ) = $args{object}->utc_rd_values;
348              
349 74         635 my ( $day, $season, $year ) = $class->_rd2discordian($rd_days);
350              
351 74         327 my $newobj = $class->new(
352             day => $day,
353             season => $season,
354             year => $year,
355             second => $rd_secs,
356             nanosecond => $rd_nanosecs,
357             locale => $args{locale},
358             );
359              
360 74         566 return $newobj;
361             }
362              
363             =head2 holyday
364              
365             If the current day is a holy day, returns the name of that day otherwise
366             returns an empty string.
367              
368             =cut
369              
370             sub holyday {
371 4     4 1 7 my ($self) = @_;
372              
373 4 100       23 return $seasons{ $self->{season} }->{apostle_holyday}
374             if ( $self->{day} == 5 );
375 3 100       19 return $seasons{ $self->{season} }->{season_holyday}
376             if ( $self->{day} == 50 );
377 1         34 return q{};
378             }
379              
380             =head2 season_abbr
381              
382             Returns the abbreviated name of the current season.
383              
384             =cut
385              
386             sub season_abbr {
387 1     1 1 2 my ($self) = @_;
388              
389 1         6 return $seasons{ $self->{season} }->{abbrev};
390             }
391              
392             =head2 season_name
393              
394             Returns the full name of the current season.
395              
396             =cut
397              
398             sub season_name {
399 40     40 1 62 my ($self) = @_;
400              
401 40         257 return $self->{season};
402             }
403              
404             =head2 strftime
405              
406             This function takes one or more parameters consisting of strings
407             containing special specifiers. For each such string it will return a
408             string formatted according to the specifiers, er, specified. See the
409             L section for a list of the
410             available format specifiers. They have been chosen to be compatible
411             with the L program not necessarily the L C
412             function. If you give a format specifier that doesn't exist, then it is
413             simply treated as text.
414              
415             =head3 strftime Specifiers
416              
417             The following specifiers are allowed in the format string given to the
418             B method:
419              
420             =over 4
421              
422             =item * %a
423              
424             Abbreviated name of the day of the week (i.e., SM.) Internally uses the
425             I function.
426              
427             =item * %A
428              
429             Full name of the day of the week (i.e., Sweetmorn.) Internally uses the
430             I function.
431              
432             =item * %b
433              
434             Abbreviated name of the season (i.e., Chs.) Internally uses the
435             I function.
436              
437             =item * %B
438              
439             Full name of the season (i.e., Chaos.) Internally uses the
440             I function.
441              
442             =item * %d
443              
444             Ordinal number of day in season (i.e., 23.) Internally uses the I
445             function.
446              
447             =item * %e
448              
449             Cardinal number of day in season (i.e., 23rd.)
450              
451             =item * %H
452              
453             Name of current Holyday, if any. Internally uses the I
454             function.
455              
456             =item * %n
457              
458             A newline character.
459              
460             =item * %N
461              
462             Magic code to prevent rest of format from being printed unless today is
463             a Holyday.
464              
465             =item * %t
466              
467             A tab character.
468              
469             =item * %X
470              
471             Number of days remaining until X-Day. Internally uses the
472             I function.
473              
474             =item * %Y
475              
476             Number of Year Of Lady Discord (YOLD.) Internally uses the I
477             function.
478              
479             =item * %{
480              
481             =item * %}
482              
483             Used to enclose the part of the string which is to be replaced with the
484             words "St. Tib's Day" if the current day is St. Tib's Day.
485              
486             =item * %%
487              
488             A literal `%' character.
489              
490             =item * %.
491              
492             Try it and see.
493              
494             =back
495              
496             =cut
497              
498             my %formats = (
499             'a' => sub { $_[0]->day_abbr },
500             'A' => sub { $_[0]->day_name },
501             'b' => sub { $_[0]->season_abbr },
502             'B' => sub { $_[0]->season_name },
503             'd' => sub { $_[0]->day },
504             'e' => sub { _cardinal( $_[0]->{day} ) },
505             'H' => sub { $_[0]->holyday },
506             'n' => sub { "\n" },
507             't' => sub { "\t" },
508             'X' => sub { $_[0]->days_till_x },
509             'Y' => sub { $_[0]->year },
510             q{%} => sub { q{%} },
511             q{.} => sub { $_[0]->_randexcl },
512             );
513              
514             sub strftime {
515 47     47 1 112 my ( $self, @r ) = @_;
516              
517 47         100 foreach (@r) {
518 48 100 66     394 ( $self->{day} eq q{St. Tib's Day}
519             || ( $self->{day} != 5 && $self->{day} != 50 ) )
520             ? s/%N.+$//msx
521             : s/%N//gmsx;
522 48 100       308 ( $self->{day} eq q{St. Tib's Day} )
523             ? s/%[{].+?%[}]/%d/gmsx
524             : s/%[{}]//gmsx;
525              
526 48 100       215 s/%([%*[:alpha:]])/ $formats{$1} ? $formats{$1}->($self) : $1 /egmsx;
  135         6750  
527 48 100       145 if ( !wantarray ) {
528 46         316 return $_;
529             }
530             }
531 1         11 return @r;
532             }
533              
534             =head2 utc_rd_values
535              
536             Returns a three-element array containing the current UTC RD days,
537             seconds, and nanoseconds. See L for more details.
538              
539             =cut
540              
541             sub utc_rd_values {
542 37     37 1 77 my ($self) = @_;
543              
544 37         212 return ( $self->{rd}, $self->{rd_secs}, $self->{rd_nanosecs} );
545             }
546              
547             =head2 year
548              
549             Returns the current year according to the YOLD (Year Of Lady Discord)
550             era.
551              
552             =cut
553              
554             sub year {
555 41     41 1 64 my ($self) = @_;
556              
557 41         156 return $self->{year};
558             }
559              
560             sub _cardinal {
561 75     75   84779 my ($day) = @_;
562              
563 75         104 my $cardinal = $day;
564 75 100 100     301 return $cardinal . 'st' if ( $day % 10 == 1 && $day != 11 );
565 68 100 100     253 return $cardinal . 'nd' if ( $day % 10 == 2 && $day != 12 );
566 61 100 100     217 return $cardinal . 'rd' if ( $day % 10 == 3 && $day != 13 );
567 54         1236 return $cardinal . 'th';
568             }
569              
570             #
571             # calculate RD (Rata Dia) date
572             #
573             sub _discordian2rd {
574 210     210   279 my ($self) = @_;
575              
576             # Convert Discordian year to Gregorian - 1
577 210         366 my $yr = $self->{year} - 1167;
578              
579             # Start with the epoch + number of elapsed days in intervening years.
580             # Add number of intervening leap days.
581 210         635 my $rd = 0 #
582             + 365 * ($yr) #
583             + _floor( $yr / 4 ) #
584             - _floor( $yr / 100 ) + _floor( $yr / 400 );
585              
586             # add number of days elapsed this year.
587 210 100       792 my $day_of_year =
588             $self->{day} eq q{St. Tib's Day}
589             ? 60
590             : $seasons{ $self->{season} }->{offset} + $self->{day};
591 210         242 $rd += $day_of_year;
592              
593             # add 1 if this is a leap year and it is past St. Tibs' Day.
594 210 100       624 $rd += $day_of_year <= 60 ? 0 : _is_leap_year( $yr + 1 ) ? 1 : 0;
    100          
595              
596 210         6564 return $rd;
597             }
598              
599             sub _floor {
600 992     992   1551 my ($x) = @_;
601 992         7009 my $ix = int $x;
602 992 100       2982 return ( $ix <= $x ) ? $ix : $ix - 1;
603             }
604              
605             sub _is_leap_year {
606 246     246   312 my ($yr) = @_;
607 246         387 my $c = ($yr) % 400;
608              
609 246   66     2284 return ( $yr % 4 == 0 ) && $c != 100 && $c != 200 && $c != 300;
610             }
611              
612             sub _randexcl {
613 20     20   58 my ($self) = @_;
614              
615 20         173 return $excl[ int rand $#excl ];
616             }
617              
618             sub _rd2discordian {
619 74     74   157 my ( $self, $rd ) = @_;
620              
621 74         215 my $n400 = _floor( $rd / 146_097 );
622 74         145 my $d1 = $rd % 146_097;
623 74         151 my $n100 = _floor( $d1 / 36_524 );
624 74         149 my $d2 = $d1 % 36_524;
625 74         172 my $n4 = _floor( $d2 / 1461 );
626 74         138 my $d3 = $d2 % 1461;
627 74         167 my $n1 = _floor( $d3 / 365 );
628 74         181 my $d4 = $d3 % 365;
629              
630             # $d4 == 0 is the last day of the year so has to be special cased.
631 74 100       260 my $year =
632             ( 400 * $n400 ) +
633             ( 100 * $n100 ) +
634             ( 4 * $n4 ) +
635             $n1 + 1166 +
636             ( ( $d4 == 0 ) ? 0 : 1 );
637              
638 74         90 my ( $season, $day );
639 74 100 100     259 if ( $d4 == 60 && _is_leap_year( $year - 1166 ) ) {
640 3         5 $season = undef;
641 3         6 $day = q{St. Tib's Day};
642             }
643             else {
644 71         524 my @seas =
645             ( 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath', );
646 71 100       210 $season = ( $d4 == 0 ) ? $seas[4] : $seas[ _floor( $d4 / 73 ) ];
647              
648 71 100       268 $day = ( $d4 == 0 ) ? 73 : $d4 - $seasons{$season}->{offset};
649              
650 71 100 100     431 if ( $d4 > 60 && _is_leap_year( $year - 1166 ) ) {
651 24         36 $day--;
652             }
653              
654 71 100       317 if ( $day < 1 ) {
655 12         26 $day += 73;
656             }
657             }
658              
659 74         225 return ( $day, $season, $year );
660             }
661              
662             1;
663             __END__