File Coverage

blib/lib/Date/Baha/i.pm
Criterion Covered Total %
statement 144 172 83.7
branch 37 64 57.8
condition 33 51 64.7
subroutine 28 35 80.0
pod 10 10 100.0
total 252 332 75.9


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.2001';
7              
8              
9 3     3   2423 use strict;
  3         7  
  3         89  
10 3     3   16 use warnings;
  3         5  
  3         87  
11              
12 3     3   1186 use parent 'Exporter';
  3         868  
  3         15  
13 3     3   229 use vars qw(@EXPORT @EXPORT_OK);
  3         6  
  3         195  
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         241 use Date::Calc qw(
28             Add_Delta_Days
29             Date_to_Days
30             Day_of_Week
31             leap_year
32 3     3   841 );
  3         12139  
33 3     3   1519 use Lingua::EN::Numbers qw(num2en_ordinal);
  3         6162  
  3         182  
34 3     3   1517 use Lingua::EN::Numbers::Years;
  3         14850  
  3         232  
35              
36             # Set constants
37 3     3   25 use constant FACTOR => 19; # Groups of 19
  3         6  
  3         283  
38 3     3   20 use constant FEBRUARY => 2; # Handy
  3         6  
  3         130  
39 3     3   17 use constant MARCH => 3; # Handy
  3         6  
  3         136  
40 3     3   18 use constant SHARAF => 16; # Handy
  3         26  
  3         168  
41 3     3   18 use constant LAST_START_DAY => 2; # 1st day of fast
  3         12  
  3         148  
42 3     3   61 use constant YEAR_START_DAY => 21; # Vernal equinox
  3         35  
  3         150  
43 3     3   18 use constant LEAP_START_DAY => 26; # Intercalary days
  3         6  
  3         177  
44 3     3   20 use constant FIRST_YEAR => 1844; # History!
  3         6  
  3         157  
45 3     3   17 use constant ADJUST_YEAR => 1900; # Year factor
  3         5  
  3         209  
46              
47              
48 3         261 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         6  
69 3         528 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   20 );
  3         5  
91              
92              
93             # We quote floats to avoid mis-computation.
94             # Month => [Number, Start, End] # TODO ?, ?
95 3         307 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   21 };
  3         5  
117              
118              
119 3         331 use constant DOW_NAME => qw(
120             Jalal
121             Jamal
122             Kaml
123             Fidal
124             'Idal
125             Istijlal
126             Istiqlal
127 3     3   21 );
  3         5  
128              
129              
130 3         6246 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   20 };
  3         14  
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 1351 my %args = @_;
159              
160             # Grab the ymd from the arguments if they have been passed in.
161 2         8 my ($year, $month, $day) = @args{qw(year month day)};
162             # Make sure we have a proper ymd before proceeding.
163 2         9 ($year, $month, $day) = _ymd(
164             %args,
165             year => $year,
166             month => $month,
167             day => $day,
168             );
169              
170 2         4 my ($bahai_month, $bahai_day);
171              
172 2         3 for (values %{ MONTHS() }) {
  2         8  
173 39         81 my ($days, $lower, $upper) = _setup_date_comparison(
174             $year, $month, $day, @$_[1,2]
175             );
176              
177 39 100 100     116 if ($days >= $lower && $days <= $upper) {
178 2         4 $bahai_month = $_->[0];
179 2         4 $bahai_day = $days - $lower;
180 2         5 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 197064 my %args = @_;
193              
194             # Figure out the year.
195 366         769 my $year = $args{year} + FIRST_YEAR;
196 366 100 100     1517 $year-- unless $args{month} > SHARAF || $args{month} == -1;
197              
198             # Reset the month number if we are given Ayyam-i-Ha.
199 366 100       865 $args{month} = 0 if $args{month} == -1;
200              
201             # This ugliness actually finds the month and day number.
202 366         1061 my $day = (MONTHS->{ (MONTH_DAY)[$args{month} - 1] })->[1];
203 366         1252 (my $month, $day) = split /\./, $day;
204             ($year, $month, $day) = Add_Delta_Days(
205 366         1306 $year, $month, $day, $args{day} - 1
206             );
207              
208             return wantarray
209 366 50       1421 ? ($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 5918 my ($date_hash, %args) = @_;
216              
217 8 50       25 $args{size} = 1 unless defined $args{size};
218 8 50       25 $args{numeric} = 0 unless defined $args{numeric};
219 8 50       15 $args{alpha} = 1 unless defined $args{alpha};
220              
221 8         12 my $date;
222              
223 8 50       22 my $is_ayyam_i_ha = $date_hash->{month} == -1 ? 1 : 0;
224              
225 8 100 100     85 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         10 @$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       5 my $month_string = $is_ayyam_i_ha ? '%s%s' : 'the %s month %s';
237 1         5 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       149 num2en_ordinal($date_hash->{kull_i_shay});
254             }
255             elsif (!$args{size} && $args{numeric} && !$args{alpha}) {
256             # short numeric
257 1         5 $date .= sprintf '%s/%s/%s', @$date_hash{qw(month day year)};
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         9 @$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         10 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         217 num2en_ordinal($date_hash->{kull_i_shay});
289             }
290              
291 8 0 33     426 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         28 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 39     39   61 my ($y, $m, $d, $s, $e) = @_;
339              
340             # Dates are encoded as decimals.
341 39         88 my ($start_month, $start_day) = split /\./, $s;
342 39         91 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 39         68 my ($start_year, $end_year) = ($y, $y);
347 39 100       79 if ($end_month < $start_month) {
348 2 50       12 if ($m == $start_month) {
    50          
349 0         0 $end_year++;
350             }
351             elsif ($m == $end_month) {
352 0         0 $start_year--;
353             }
354             }
355              
356             return
357 39         148 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         3 my %date;
366 2         6 @date{qw(month day)} = ($new_month, $new_day);
367              
368             # Set the day of the week (rotated by 2).
369 2         9 $date{dow} = Day_of_Week($year, $month, $day);
370 2         4 $date{dow} += 2;
371 2 50       15 $date{dow} = $date{dow} - 7 if $date{dow} > 7;
372 2         7 $date{dow_name} = (DOW_NAME)[$date{dow} - 1];
373              
374             # Set the day.
375 2         5 $date{day_name} = (MONTH_DAY)[$date{day}];
376 2         4 $date{day}++;
377              
378             # Set the the month.
379 2         3 $date{month_name} = (MONTH_DAY)[$date{month}];
380             # Fix the month number, unless we are in Ayyam-i-Ha.
381 2 50       12 $date{month}++ unless $date{month} == -1;
382              
383             # Set the year.
384             # Algorithm lifted from Danesh's "bahaidate".
385 2 50 33     23 $date{year} = ($month < MARCH) ||
386             ($month == MARCH && $day < YEAR_START_DAY)
387             ? $year - FIRST_YEAR
388             : $year - (FIRST_YEAR - 1);
389              
390 2         16 $date{year_name} = (CYCLE_YEAR)[($date{year} - 1) % FACTOR];
391 2         5 $date{cycle_year} = $date{year} % FACTOR;
392              
393             # Set the cycle.
394 2         9 $date{cycle} = int($date{year} / FACTOR) + 1;
395 2         4 $date{cycle_name} = (CYCLE_YEAR)[($date{cycle} - 1) % FACTOR];
396              
397             # Set the Kull-i-Shay.
398 2         5 $date{kull_i_shay} = int($date{cycle} / FACTOR) + 1;
399              
400             # $date{timezone} = tz_local_offset();
401              
402             # Get the holy day.
403 2         6 my %inverted = _invert_holy_days($year);
404 2         12 my $m_d = sprintf '%d.%d', $month, $day;
405 2 50       7 $date{holy_day} = $inverted{$m_d} if exists $inverted{$m_d};
406              
407 2 50       33 return wantarray ? %date : as_string(\%date, %args);
408             }
409              
410             sub _invert_holy_days {
411 2   33 2   6 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         74  
416 28         68 $inverted{$date->[0]} = $name;
417              
418             # Does this date contain a day span?
419 28 100       59 if (@$date > 1) {
420             # Increment the Ayyam-i-Ha day if we are in a leap year.
421 6 50 66     22 $date->[1]++ if $name eq 'Ayyam-i-Ha' && leap_year($year);
422              
423 6         16 for (1 .. $date->[1] - 1) {
424 64         197 (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         240 $inverted{ sprintf '%d.%d', $month, $day } = $name;
430             }
431             }
432             }
433              
434 2         54 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   6 my %args = @_;
440              
441             # Use the system time, if a ymd is not provided.
442 2 50 33     22 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         7 return $args{year}, $args{month}, $args{day};
453             }
454              
455             1;
456              
457             __END__