File Coverage

blib/lib/Date/Baha/i.pm
Criterion Covered Total %
statement 143 172 83.1
branch 34 64 53.1
condition 33 51 64.7
subroutine 28 35 80.0
pod 10 10 100.0
total 248 332 74.7


line stmt bran cond sub pod time code
1             package Date::Baha::i;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Convert to and from Baha'i dates
5              
6             our $VERSION = '0.2100';
7              
8              
9 3     3   2293 use strict;
  3         6  
  3         84  
10 3     3   16 use warnings;
  3         27  
  3         83  
11              
12 3     3   1260 use parent 'Exporter';
  3         886  
  3         25  
13 3     3   173 use vars qw(@EXPORT @EXPORT_OK);
  3         6  
  3         253  
14             @EXPORT = @EXPORT_OK = qw(
15             as_string
16             cycles
17             days
18             days_of_the_week
19             from_bahai
20             holy_days
21             months
22             next_holy_day
23             to_bahai
24             years
25             );
26              
27 3         253 use Date::Calc qw(
28             Add_Delta_Days
29             Date_to_Days
30             Day_of_Week
31             leap_year
32 3     3   934 );
  3         12919  
33 3     3   1542 use Lingua::EN::Numbers qw(num2en_ordinal);
  3         6153  
  3         172  
34 3     3   1381 use Lingua::EN::Numbers::Years;
  3         15102  
  3         201  
35              
36             # Set constants
37 3     3   23 use constant FACTOR => 19; # Groups of 19
  3         7  
  3         295  
38 3     3   28 use constant FEBRUARY => 2; # Handy
  3         4  
  3         154  
39 3     3   18 use constant MARCH => 3; # Handy
  3         5  
  3         146  
40 3     3   23 use constant SHARAF => 16; # Handy
  3         15  
  3         182  
41 3     3   19 use constant LAST_START_DAY => 2; # 1st day of fast
  3         13  
  3         155  
42 3     3   55 use constant YEAR_START_DAY => 21; # Vernal equinox
  3         31  
  3         153  
43 3     3   18 use constant LEAP_START_DAY => 26; # Intercalary days
  3         5  
  3         183  
44 3     3   64 use constant FIRST_YEAR => 1844; # History!
  3         6  
  3         171  
45 3     3   20 use constant ADJUST_YEAR => 1900; # Year factor
  3         4  
  3         207  
46              
47              
48 3         308 use constant CYCLE_YEAR => qw(
49             Alif
50             Ba
51             Ab
52             Dal
53             Bab
54             Vav
55             Abad
56             Jad
57             Baha
58             Hubb
59             Bahhaj
60             Javab
61             Ahad
62             Vahhab
63             Vidad
64             Badi
65             Bahi
66             Abha
67             Vahid
68 3     3   19 );
  3         5  
69 3         572 use constant MONTH_DAY => qw(
70             Baha
71             Jalal
72             Jamal
73             'Azamat
74             Nur
75             Rahmat
76             Kalimat
77             Kamal
78             Asma'
79             'Izzat
80             Mashiyyat
81             'Ilm
82             Qudrat
83             Qawl
84             Masa'il
85             Sharaf
86             Sultan
87             Mulk
88             'Ala
89             Ayyam-i-Ha
90 3     3   19 );
  3         5  
91              
92              
93             # We quote floats to avoid mis-computation.
94             # Month => [Number, Start, End] # TODO ?, ?
95 3         298 use constant MONTHS => {
96             "Baha" => [ 0, '3.21', '4.08'], # 80, 98
97             "Jalal" => [ 1, '4.09', '4.27'], # 99, 117
98             "Jamal" => [ 2, '4.28', '5.16'], #118, 136
99             "'Azamat" => [ 3, '5.17', '6.04'], #137, 155
100             "Nur" => [ 4, '6.05', '6.23'], #156, 174
101             "Rahmat" => [ 5, '6.24', '7.12'], #175, 193
102             "Kalimat" => [ 6, '7.13', '7.31'], #194, 212
103             "Kamal" => [ 7, '8.01', '8.19'], #213, 231
104             "Asma'" => [ 8, '8.20', '9.07'], #232, 250
105             "'Izzat" => [ 9, '9.08', '9.26'], #251, 269
106             "Mashiyyat" => [10, '9.27', '10.15'], #270, 288
107             "'Ilm" => [11, '10.16', '11.03'], #289, 307
108             "Qudrat" => [12, '11.04', '11.22'], #308, 326
109             "Qawl" => [13, '11.23', '12.11'], #327, 345
110             "Masa'il" => [14, '12.12', '12.30'], #346, 364
111             "Sharaf" => [15, '12.31', '1.18'], #365, 18
112             "Sultan" => [16, '1.19', '2.06'], # 19, 37
113             "Mulk" => [17, '2.07', '2.25'], # 38, 56
114             "Ayyam-i-Ha" => [-1, '2.26', '3.01'], # 57, 60
115             "'Ala" => [18, '3.02', '3.20'], # 61, 79
116 3     3   20 };
  3         10  
117              
118              
119 3         768 use constant DOW_NAME => qw(
120             Jalal
121             Jamal
122             Kaml
123             Fidal
124             'Idal
125             Istijlal
126             Istiqlal
127 3     3   20 );
  3         6  
128              
129              
130 3         6325 use constant HOLY_DAYS => {
131             # Work suspended':
132             "Naw Ruz" => [ '3.21' ],
133             "First Day of Ridvan" => [ '4.21' ],
134             "Ninth Day of Ridvan" => [ '4.29' ],
135             "Twelfth Day of Ridvan" => [ '5.02' ],
136             "Declaration of the Bab" => [ '5.23' ],
137             "Ascension of Baha'u'llah" => [ '5.29' ],
138             "Martyrdom of the Bab" => [ '7.09' ],
139             "Birth of the Bab" => [ '10.20' ],
140             "Birth of Baha'u'llah" => [ '11.12' ],
141             # Work not suspended:
142             "Ayyam-i-Ha" => [ '2.26', 4 ], # 5 days are calculated in leap years
143             "The Fast" => [ '3.02', 19 ],
144             "Days of Ridvan" => [ '4.21', 12 ],
145             "Day of the Covenant" => [ '11.26' ],
146             "Ascension of 'Abdu'l-Baha" => [ '11.28' ],
147 3     3   19 };
  3         5  
148              
149             # List return functions
150 0     0 1 0 sub cycles { return CYCLE_YEAR }
151 0     0 1 0 sub years { return CYCLE_YEAR }
152 0     0 1 0 sub months { return MONTH_DAY }
153 0     0 1 0 sub days { return (MONTH_DAY)[0 .. 18] }
154 0     0 1 0 sub days_of_the_week { return DOW_NAME }
155 0     0 1 0 sub holy_days { return HOLY_DAYS }
156              
157             sub to_bahai {
158 2     2 1 1183 my %args = @_;
159              
160             # Grab the ymd from the arguments if they have been passed in.
161 2         7 my ($year, $month, $day) = @args{qw(year month day)};
162             # Make sure we have a proper ymd before proceeding.
163 2         7 ($year, $month, $day) = _ymd(
164             %args,
165             year => $year,
166             month => $month,
167             day => $day,
168             );
169              
170 2         5 my ($bahai_month, $bahai_day);
171              
172 2         3 for (values %{ MONTHS() }) {
  2         7  
173 16         35 my ($days, $lower, $upper) = _setup_date_comparison(
174             $year, $month, $day, @$_[1,2]
175             );
176              
177 16 100 100     59 if ($days >= $lower && $days <= $upper) {
178 2         4 $bahai_month = $_->[0];
179 2         5 $bahai_day = $days - $lower;
180 2         4 last;
181             }
182             }
183              
184             # Build the date hash to return.
185 2         8 return _build_date(
186             $year, $month, $day, $bahai_month, $bahai_day,
187             %args
188             );
189             }
190              
191             sub from_bahai {
192 366     366 1 197816 my %args = @_;
193              
194             # Figure out the year.
195 366         801 my $year = $args{year} + FIRST_YEAR;
196 366 100 100     2114 $year-- unless $args{month} > SHARAF || $args{month} == -1;
197              
198             # Reset the month number if we are given Ayyam-i-Ha.
199 366 100       764 $args{month} = 0 if $args{month} == -1;
200              
201             # This ugliness actually finds the month and day number.
202 366         1109 my $day = (MONTHS->{ (MONTH_DAY)[$args{month} - 1] })->[1];
203 366         1216 (my $month, $day) = split /\./, $day;
204             ($year, $month, $day) = Add_Delta_Days(
205 366         1337 $year, $month, $day, $args{day} - 1
206             );
207              
208             return wantarray
209 366 50       1524 ? ($year, $month, $day)
210             : join '/', $year, $month, $day;
211             }
212              
213             sub as_string {
214             # XXX With Lingua::EN::Numbers, naively assume that we only care about English.
215 8     8 1 3879 my ($date_hash, %args) = @_;
216              
217 8 50       23 $args{size} = 1 unless defined $args{size};
218 8 50       17 $args{numeric} = 0 unless defined $args{numeric};
219 8 50       15 $args{alpha} = 1 unless defined $args{alpha};
220              
221 8         10 my $date;
222              
223 8 50       28 my $is_ayyam_i_ha = $date_hash->{month} == -1 ? 1 : 0;
224              
225 8 100 100     75 if (!$args{size} && $args{numeric} && $args{alpha}) {
    100 100        
    100 100        
    100 66        
    100 100        
      66        
      66        
      66        
226             # short alpha-numeric
227             $date .= sprintf '%s (%d), %s (%d) of %s (%d), year %d, %s (%d) of %s (%d)',
228 1         8 @$date_hash{qw(
229             dow_name dow day_name day month_name month
230             year year_name cycle_year cycle_name cycle
231             )};
232             }
233             elsif ($args{size} && $args{numeric} && $args{alpha}) {
234             # long alpha-numeric
235             # XXX Fugly hacking begins.
236 1 50       4 my $month_string = $is_ayyam_i_ha ? '%s%s' : 'the %s month %s';
237 1         4 my $n = year2en($date_hash->{year});
238              
239             $date .= sprintf
240             "%s week day %s, %s day %s of $month_string, year %s (%d), %s year %s of the %s vahid %s of the %s kull-i-shay",
241             num2en_ordinal($date_hash->{dow}),
242             $date_hash->{dow_name},
243             num2en_ordinal($date_hash->{day}),
244             $date_hash->{day_name},
245             ($is_ayyam_i_ha ? '' : num2en_ordinal($date_hash->{month})),
246             $date_hash->{month_name},
247             $n,
248             $date_hash->{year},
249             num2en_ordinal($date_hash->{cycle_year}),
250             $date_hash->{year_name},
251             num2en_ordinal($date_hash->{cycle}),
252             $date_hash->{cycle_name},
253 1 50       41 num2en_ordinal($date_hash->{kull_i_shay});
254             }
255             elsif (!$args{size} && $args{numeric} && !$args{alpha}) {
256             # short numeric
257 1         6 $date .= sprintf '%s/%s/%s', @$date_hash{qw(year month day)};
258             }
259             elsif ($args{size} && $args{numeric}) {
260             # long numeric
261             $date .= sprintf
262             '%s day of the week, %s day of the %s month, year %s, %s year of the %s vahid of the %s kull-i-shay',
263             num2en_ordinal($date_hash->{dow}),
264             num2en_ordinal($date_hash->{day}),
265             num2en_ordinal($date_hash->{month}),
266             $date_hash->{year},
267             num2en_ordinal($date_hash->{cycle_year}),
268             num2en_ordinal($date_hash->{cycle}),
269 1         4 num2en_ordinal($date_hash->{kull_i_shay});
270             }
271             elsif (!$args{size} && $args{alpha}) {
272             # short alpha
273             $date .= sprintf '%s, %s of %s, %s of %s',
274 1         6 @$date_hash{qw(
275             dow_name day_name month_name year_name cycle_name
276             )};
277             }
278             else {
279             # long alpha
280 3 50       7 my $month_string = $is_ayyam_i_ha ? '%s' : 'month %s';
281 3         11 my $n = year2en($date_hash->{year});
282              
283             $date .= sprintf
284             "week day %s, day %s of $month_string, year %s, %s of the vahid %s of the %s kull-i-shay",
285             @$date_hash{qw(dow_name day_name month_name)},
286             $n,
287             @$date_hash{qw(year_name cycle_name)},
288 3         128 num2en_ordinal($date_hash->{kull_i_shay});
289             }
290              
291 8 0 33     403 if ($date_hash->{holy_day} && $args{size}) {
292 0         0 $date .= ', holy day: ' . join '', keys %{ $date_hash->{holy_day} };
  0         0  
293             }
294              
295 8         32 return $date;
296             }
297              
298             sub next_holy_day {
299 0     0 1 0 my ($year, $month, $day) = @_;
300              
301             # Use today if we are not provided with a date.
302 0         0 ($year, $month, $day) = _ymd(
303             year => $year,
304             month => $month,
305             day => $day,
306             );
307              
308             # Construct our lists of pseudo real number dates.
309 0         0 my %inverted = _invert_holy_days($year);
310 0         0 my @sorted = sort { $a <=> $b } keys %inverted;
  0         0  
311              
312             # Make the month and day a pseudo real number.
313 0         0 my $m_d = "$month.$day";
314 0         0 my $holy_date;
315              
316             # Find the first date greater than the one provided.
317 0         0 for (@sorted) {
318 0 0       0 if ($m_d < $_) {
319 0         0 $holy_date = $_;
320 0         0 last;
321             }
322             }
323              
324             # If one was not found, grab the last date in the list.
325 0 0       0 $holy_date = $sorted[-1] unless $holy_date;
326              
327             # Make this look like a date again.
328 0         0 (my $date = $holy_date) =~ s/\./\//;
329              
330             return wantarray
331 0 0       0 ? ($inverted{$holy_date}, $date)
332             : "$inverted{$holy_date} $date";
333             }
334              
335             # Helper functions
336             # Date comparison gymnastics.
337             sub _setup_date_comparison {
338 16     16   27 my ($y, $m, $d, $s, $e) = @_;
339              
340             # Dates are encoded as decimals.
341 16         51 my ($start_month, $start_day) = split /\./, $s;
342 16         121 my ($end_month, $end_day) = split /\./, $e;
343              
344             # Slide either the start or end year, given the month we're
345             # looking at.
346 16         32 my ($start_year, $end_year) = ($y, $y);
347 16 50       36 if ($end_month < $start_month) {
348 0 0       0 if ($m == $start_month) {
    0          
349 0         0 $end_year++;
350             }
351             elsif ($m == $end_month) {
352 0         0 $start_year--;
353             }
354             }
355              
356             return
357 16         61 Date_to_Days($y, $m, $d),
358             Date_to_Days($start_year, $start_month, $start_day),
359             Date_to_Days($end_year, $end_month, $end_day);
360             }
361              
362             sub _build_date {
363 2     2   7 my ($year, $month, $day, $new_month, $new_day, %args) = @_;
364              
365 2         4 my %date;
366 2         5 @date{qw(month day)} = ($new_month, $new_day);
367              
368             # Set the day of the week (rotated by 2).
369 2         7 $date{dow} = Day_of_Week($year, $month, $day);
370 2         4 $date{dow} += 2;
371 2 50       9 $date{dow} = $date{dow} - 7 if $date{dow} > 7;
372 2         5 $date{dow_name} = (DOW_NAME)[$date{dow} - 1];
373              
374             # Set the day.
375 2         4 $date{day_name} = (MONTH_DAY)[$date{day}];
376 2         3 $date{day}++;
377              
378             # Set the the month.
379 2         4 $date{month_name} = (MONTH_DAY)[$date{month}];
380             # Fix the month number, unless we are in Ayyam-i-Ha.
381 2 50       6 $date{month}++ unless $date{month} == -1;
382              
383             # Set the year.
384             # Algorithm lifted from Danesh's "bahaidate".
385 2 50 33     72 $date{year} = ($month < MARCH) ||
386             ($month == MARCH && $day < YEAR_START_DAY)
387             ? $year - FIRST_YEAR
388             : $year - (FIRST_YEAR - 1);
389              
390 2         12 $date{year_name} = (CYCLE_YEAR)[($date{year} - 1) % FACTOR];
391 2         4 $date{cycle_year} = $date{year} % FACTOR;
392              
393             # Set the cycle.
394 2         8 $date{cycle} = int($date{year} / FACTOR) + 1;
395 2         11 $date{cycle_name} = (CYCLE_YEAR)[($date{cycle} - 1) % FACTOR];
396              
397             # Set the Kull-i-Shay.
398 2         7 $date{kull_i_shay} = int($date{cycle} / FACTOR) + 1;
399              
400             # $date{timezone} = tz_local_offset();
401              
402             # Get the holy day.
403 2         11 my %inverted = _invert_holy_days($year);
404 2         13 my $m_d = sprintf '%d.%d', $month, $day;
405 2 50       12 $date{holy_day} = $inverted{$m_d} if exists $inverted{$m_d};
406              
407 2 50       107 return wantarray ? %date : as_string(\%date, %args);
408             }
409              
410             sub _invert_holy_days {
411 2   33 2   7 my $year = shift || (localtime)[5] + ADJUST_YEAR;
412              
413 2         4 my %inverted;
414              
415 2         3 while (my ($name, $date) = each %{ HOLY_DAYS() }) {
  30         109  
416 28         110 $inverted{$date->[0]} = $name;
417              
418             # Does this date contain a day span?
419 28 100       61 if (@$date > 1) {
420             # Increment the Ayyam-i-Ha day if we are in a leap year.
421 6 50 66     62 $date->[1]++ if $name eq 'Ayyam-i-Ha' && leap_year($year);
422              
423 6         18 for (1 .. $date->[1] - 1) {
424 64         233 (undef, my $month, my $day) = Add_Delta_Days(
425             $year, split(/\./, $date->[0]), $_
426             );
427              
428             # Pre-pad the day number with a zero.
429 64         255 $inverted{ sprintf '%d.%d', $month, $day } = $name;
430             }
431             }
432             }
433              
434 2         66 return %inverted;
435             }
436              
437             # Return a ymd date array but try to honor the epoch and use_gmtime settings.
438             sub _ymd {
439 2     2   8 my %args = @_;
440              
441             # Use the system time, if a ymd is not provided.
442 2 50 33     20 unless($args{year} && $args{month} && $args{day}) {
      33        
443 0   0     0 $args{epoch} ||= time;
444             ($args{year}, $args{month}, $args{day}) = $args{use_gmtime}
445             ? (gmtime $args{epoch})[5,4,3]
446 0 0       0 : (localtime $args{epoch})[5,4,3];
447             # Fix the year and the month.
448 0         0 $args{year} += ADJUST_YEAR;
449 0         0 $args{month}++;
450             }
451              
452 2         6 return $args{year}, $args{month}, $args{day};
453             }
454              
455             1;
456              
457             __END__