File Coverage

blib/lib/DateTime/Calendar/Chinese.pm
Criterion Covered Total %
statement 17 19 89.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 24 26 92.3


line stmt bran cond sub pod time code
1             package DateTime::Calendar::Chinese;
2 3     3   3316 use 5.008;
  3         10  
  3         109  
3 3     3   18 use strict;
  3         5  
  3         118  
4 3     3   2121 use utf8;
  3         22  
  3         18  
5 3     3   328 use vars qw($VERSION);
  3         7  
  3         200  
6             BEGIN {
7 3     3   66 $VERSION = '1.00';
8             }
9              
10 3     3   4066 use DateTime;
  3         636879  
  3         143  
11 0           use DateTime::Astro qw(MEAN_TROPICAL_YEAR MEAN_SYNODIC_MONTH moment dt_from_moment new_moon_after new_moon_before
12             solar_longitude_from_moment
13 3     3   1456 );
  0            
14             use DateTime::Event::Chinese qw(chinese_new_year_before);
15             use DateTime::Event::SolarTerm qw(prev_term_at no_major_term_on);
16             use Params::Validate;
17             use Math::Round qw(round);
18             use constant GREGORIAN_CHINESE_EPOCH => DateTime->new(
19             year => -2636, month => 2, day => 15, time_zone => 'UTC');
20             use constant GREGORIAN_CHINESE_EPOCH_MOMENT => moment(GREGORIAN_CHINESE_EPOCH);
21             use constant DEBUG => $ENV{PERL_DATETIME_CALENDAR_CHINESE_DEBUG};
22              
23             my %BasicValidate = (
24             cycle => {
25             default => 1,
26             },
27             cycle_year => {
28             default => 1,
29             callbacks => {
30             'is between 1 and 60' => sub { $_[0] >= 1 && $_[0] <= 60 }
31             }
32             },
33             month => {
34             default => 1,
35             callbacks => {
36             'is between 1 and 12' => sub { $_[0] >= 1 && $_[0] <= 12 }
37             }
38             },
39             leap_month => {
40             default => 0,
41             type => Params::Validate::BOOLEAN()
42             },
43             day => {
44             default => 1,
45             type => Params::Validate::SCALAR()
46             },
47             hour => {
48             type => Params::Validate::SCALAR(), default => 0,
49             callbacks => {
50             'is between 0 and 23' => sub { $_[0] >= 0 && $_[0] <= 23 },
51             },
52             },
53             minute => {
54             type => Params::Validate::SCALAR(), default => 0,
55             callbacks => {
56             'is between 0 and 59' => sub { $_[0] >= 0 && $_[0] <= 59 },
57             },
58             },
59             second => {
60             type => Params::Validate::SCALAR(), default => 0,
61             callbacks => {
62             'is between 0 and 61' => sub { $_[0] >= 0 && $_[0] <= 61 },
63             },
64             },
65             nanosecond => {
66             type => Params::Validate::SCALAR(), default => 0,
67             callbacks => {
68             'cannot be negative' => sub { $_[0] >= 0 },
69             }
70             },
71             locale => { type => Params::Validate::SCALAR() | Params::Validate::OBJECT(), optional => 1 },
72             language => { type => Params::Validate::SCALAR() | Params::Validate::OBJECT(), optional => 1 },
73             );
74              
75             my %NewValidate = (
76             %BasicValidate,
77             time_zone => { type => Params::Validate::SCALAR() | Params::Validate::OBJECT(), default => 'Asia/Shanghai' },
78             );
79             sub new
80             {
81             my $class = shift;
82             my %args = Params::Validate::validate(@_, \%NewValidate);
83              
84             # XXX - currently _calc_gregorian_components() calculates the
85             # date component only, then we set the time
86             my %hash;
87             $hash{cycle} = delete $args{cycle};
88             $hash{cycle_year} = delete $args{cycle_year};
89             $hash{month} = delete $args{month};
90             $hash{leap_month} = delete $args{leap_month};
91             $hash{day} = delete $args{day};
92              
93             my $self = bless \%hash, $class;
94             $self->_calc_gregorian_components(time_zone => delete $args{time_zone});
95             $self->{gregorian}->set(%args);
96              
97             $self;
98             }
99              
100             # XXX - these values are proxied directly to the underlying DateTime
101             # (Gregorian) object.
102             sub utc_rd_values { $_[0]->{gregorian}->utc_rd_values }
103             sub hour { $_[0]->{gregorian}->hour }
104             sub minute { $_[0]->{gregorian}->minute }
105             sub second { $_[0]->{gregorian}->second }
106             sub nanosecond { $_[0]->{gregorian}->nanosecond }
107             sub day_of_week { $_[0]->{gregorian}->day_of_week }
108             sub time_zone { $_[0]->{gregorian}->time_zone }
109             sub set_time_zone { shift->{gregorian}->set_time_zone(@_) }
110              
111             # XXX - accessors for DT::C::C specific fields
112             sub cycle { $_[0]->{cycle} }
113             sub cycle_year { $_[0]->{cycle_year} }
114             sub month { $_[0]->{month} }
115             sub leap_month { $_[0]->{leap_month} }
116             sub day { $_[0]->{day} }
117              
118             my @celestial_stems =
119             ( "甲",
120             "乙",
121             "丙",
122             "丁",
123             "戊",
124             "å·±",
125             "庚",
126             "辛",
127             "壬",
128             "癸",
129             );
130              
131             my @celestial_stems_py =
132             qw( jia3
133             yi3
134             bing3
135             ding1
136             wu4
137             ji3
138             geng1
139             xin1
140             ren2
141             gui3
142             );
143              
144             my @terrestrial_branches =
145             ( "子",
146             "丑",
147             "寅",
148             "卯",
149             "è¾°",
150             "å·³",
151             "午",
152             "未",
153             "申",
154             "酉",
155             "戌",
156             "亥",
157             );
158              
159             my @terrestrial_branches_py =
160             qw( zi
161             chou3
162             yin2
163             mao3
164             chen2
165             si4
166             wu3
167             wei4
168             shen1
169             you3
170             xu1
171             hai4
172             );
173              
174             my @zodiac_animals =
175             qw( rat
176             ox
177             tiger
178             hare
179             dragon
180             snake
181             horse
182             sheep
183             monkey
184             fowl
185             dog
186             pig
187             );
188              
189             sub celestial_stem { $celestial_stems[ ( $_[0]->cycle_year % 10 || 10) - 1 ] }
190             sub terrestrial_branch { $terrestrial_branches[ ( $_[0]->cycle_year % 12 || 12 ) - 1 ] }
191             sub year_name { $_[0]->celestial_stem . $_[0]->terrestrial_branch }
192              
193             sub celestial_stem_py { $celestial_stems_py[ ( $_[0]->cycle_year % 10 || 10 ) - 1 ] }
194             sub terrestrial_branch_py { $terrestrial_branches_py[ ( $_[0]->cycle_year % 12 || 12 ) - 1 ] }
195             sub year_name_py { $_[0]->celestial_stem_py . $_[0]->terrestrial_branch_py }
196              
197             sub zodiac_animal { $zodiac_animals[ ( $_[0]->cycle_year % 12 || 12 ) - 1 ] }
198              
199             my %SetValidate;
200             foreach my $key (keys %BasicValidate) {
201             my %hash = %{$BasicValidate{$key}};
202             delete $hash{default};
203             $hash{optional} = 1;
204             $SetValidate{$key} = \%hash;
205             }
206              
207             sub set
208             {
209             my $self = shift;
210             my %args = Params::Validate::validate(@_, \%SetValidate);
211              
212             #print STDERR
213             # "BEFORE SET ",
214             # "grgorian: ", $self->{gregorian}->datetime,
215             # " RD: ", ($self->{gregorian}->utc_rd_values)[0],
216             # " time_zone: ", $self->{gregorian}->time_zone_short_name, "\n";
217             foreach my $ch_component (qw(cycle cycle_year month leap_month day)) {
218             if (exists $args{$ch_component}) {
219             $self->{$ch_component} = delete $args{$ch_component};
220             }
221             }
222            
223             my $clone = $self->{gregorian}->clone;
224              
225             $self->_calc_gregorian_components(time_zone =>
226             $args{time_zone} || $clone->time_zone || 'UTC');
227              
228             # get "defaults" from the cloned dt object. we will only use these
229             # values if the field wasn't specified in the argument to set()
230             foreach my $dt_component (qw(hour minute second locale)) {
231             if (! exists $args{$dt_component}) {
232             $args{$dt_component} = $clone->$dt_component;
233             }
234             }
235             $self->{gregorian}->set(%args);
236              
237             #print STDERR
238             # "AFTER SET ",
239             # "grgorian: ", $self->{gregorian}->datetime,
240             # " RD: ", ($self->{gregorian}->utc_rd_values)[0],
241             # " time_zone: ", $self->{gregorian}->time_zone_short_name, "\n";
242              
243             $self;
244             }
245              
246             sub from_epoch
247             {
248             my $class = shift;
249             my $self = bless {}, $class;
250             my $dt = DateTime->from_epoch(@_);
251             $self->{gregorian} = $dt;
252             $self->_calc_local_components();
253             return $self;
254            
255             }
256             sub now { shift->from_epoch(@_, epoch => time()) }
257              
258             sub from_object
259             {
260             my $class = shift;
261             my $self = bless {}, $class;
262             my $dt = DateTime->from_object(@_);
263              
264             $self->{gregorian} = $dt;
265             $self->_calc_local_components();
266             return $self;
267             }
268              
269             sub _calc_gregorian_components
270             {
271             my $self = shift;
272              
273             my $mid_year = POSIX::floor(
274             GREGORIAN_CHINESE_EPOCH_MOMENT +
275             (($self->cycle() - 1) * 60 + $self->cycle_year() - 1 + 0.5) *
276             MEAN_TROPICAL_YEAR);
277             my $new_year = chinese_new_year_before(dt_from_moment($mid_year) );
278              
279             # XXX - I don't know why I need to do $self->month() - 2 here
280             my $p_dt = $new_year + DateTime::Duration->new(days => ($self->month() - 2) * 29);
281             my $p = new_moon_after( $p_dt );
282             my $d = DateTime::Calendar::Chinese->from_object(object => $p);
283              
284             my $prior_new_moon;
285             if ($d->month == $self->month && $d->leap_month == $self->leap_month) {
286             $prior_new_moon = $p;
287             } else {
288             $prior_new_moon = new_moon_after( $p + DateTime::Duration->new(days => 1) );
289             }
290              
291             my $tmp = $prior_new_moon + DateTime::Duration->new(days => $self->day - 1);
292             my %args = @_;
293             my %new_args = ();
294             foreach my $component (qw(
295             year month day hour minute second nanosecond locale)) {
296              
297             $new_args{$component} = $tmp->$component;
298             }
299             if ($args{time_zone}) {
300             $new_args{time_zone} = $args{time_zone};
301             } else {
302             $new_args{time_zone} = $tmp->time_zone;
303             }
304              
305             $self->{gregorian} = DateTime->new(%new_args);
306              
307             #print STDERR
308             # ">>>>>>>\n",
309             # " cycle: ", $self->cycle, "\n",
310             # " c_year: ", $self->cycle_year, "\n",
311             # "mid_year: ", dt_from_moment($mid_year)->datetime, "\n",
312             # "new_year: ", $new_year->datetime, "\n",
313             # " p: ", $p->datetime, "\n",
314             # " p_dt: ", $p_dt->datetime, "\n",
315             # "prior_nm: ", $prior_new_moon->datetime, "\n",
316             # " self: cycle: ", $self->cycle, " cycle_year: ", $self->cycle_year,
317             # " month: ", $self->month, " leap_month: ", $self->leap_month,
318             # " day: ", $self->day, "\n",
319             # " d: cycle: ", $d->cycle, " cycle_year: ", $d->cycle_year,
320             # " month: ", $d->month, " leap_month: ", $d->leap_month,
321             # " day: ", $d->day, "\n";
322             # "grgorian: ", $self->{gregorian}->datetime, "\n",
323             # "<<<<<<<\n";
324              
325             }
326              
327             sub _calc_local_components
328             {
329             my $self = shift;
330             my $dt = $self->{gregorian}->clone->truncate(to => 'day');
331              
332             # XXX TODO: Change these calculations to use moment, not DateTime
333              
334             # last winter solstice
335             my $s1 = prev_term_at( $dt, 270 );
336             # next winter solstice
337             my $s2 = prev_term_at( $s1 + DateTime::Duration->new(days => 370), 270 );
338              
339             # new moon after the last winter solstice (12th month in the last sui)
340             my $m12 = new_moon_after($s1 + DateTime::Duration->new(days => 1) );
341              
342             # new moon before the next winter solstice (11th month in the current sui)
343             my $next_m11 = new_moon_before($s2 + DateTime::Duration->new(days => 1));
344              
345             # new moon before now.
346             my $m = new_moon_before($dt + DateTime::Duration->new(days => 1));
347              
348             # pre-compute and save a call to moment()
349             my $m12_moment = moment($m12);
350             my $m_moment = moment($m);
351             my $m11_moment = moment($next_m11);
352              
353              
354             # if there are 12 lunar months (29.5 days) between the last 12th month
355             # and the next 11th month, then there must be a leap month some where
356             my $leap_year =
357             round(($m11_moment - $m12_moment) / MEAN_SYNODIC_MONTH) == 12;
358              
359             # XXX - hey, there are a lot of paranthesis, but it's required or
360             # else you get into some real unhappy problems
361             my $month;
362             {
363             my $x = round(($m_moment - $m12_moment) / MEAN_SYNODIC_MONTH);
364             if ($leap_year && $self->_prior_leap_month($m12, $m)) {
365             if (DEBUG) {
366             print STDERR ">>>> leap_year && prior_leap_mont $m12 : $m\n";
367             }
368             $x--;
369             }
370             $month = $x % 12 || 12;
371             }
372              
373             # XXX - tricky... we need to set month before calling elapsed_years,
374             # because it will be used by that function
375             $self->{month} = $month;
376             my $elapsed_years = $self->elapsed_years;
377              
378             $self->{cycle} = POSIX::floor( ($elapsed_years - 1) / 60) + 1;
379             $self->{cycle_year} = $elapsed_years % 60 || 60;
380             $self->{day} = POSIX::ceil(moment($dt) - $m_moment + 1);
381              
382             if ($leap_year && no_major_term_on($m)) {
383             my $end = new_moon_before($m - DateTime::Duration->new(days => 1));
384             $self->{leap_month} = ! $self->_prior_leap_month($m12, $end);
385             } else {
386             $self->{leap_month} = 0;
387             }
388              
389             if (DEBUG) {
390             print STDERR
391             ">>>>>>\n",
392             " dt: ", $dt->datetime, "\n",
393             " s1: ", $s1->datetime, "\n",
394             " s2: ", $s2->datetime, "\n",
395             " m12: ", $m12->datetime, "\n",
396             " n_m11: ", $next_m11->datetime, "\n",
397             " 11-12: ", round( (moment($next_m11) - $m12_moment) / MEAN_SYNODIC_MONTH), "\n",
398             " m: ", $m->datetime, "\n",
399             " leap year: ", $leap_year ? "yes" : "no", "\n",
400             "leap month: ", $self->{leap_month} ? "yes" : "no", "\n",
401             " m-m12: ", round(abs($m_moment - $m12_moment) / MEAN_SYNODIC_MONTH), "\n",
402             " sl_m: ", solar_longitude_from_moment($m_moment), "\n",
403             " sl_m12: ", solar_longitude_from_moment($m12_moment), "\n",
404              
405             # "pleap: ", $self->_prior_leap_month($m12, $m) ? "yes" : "no", "\n",
406             " month: ", $month, "\n",
407             " elapsed: ", $elapsed_years, "\n",
408             " cycle: ", $self->{cycle}, "\n",
409             "cycle_year: ", $self->{cycle_year}, "\n",
410             "<<<<<<\n";
411             }
412              
413             }
414              
415             sub elapsed_years
416             {
417             my $self = shift;
418             if (DEBUG) {
419             print STDERR
420             ">>>> elapsed_years\n",
421             "moment: ", moment($self->{gregorian}), "\n",
422             "epoch: ", GREGORIAN_CHINESE_EPOCH_MOMENT, "\n",
423             "month: ", $self->month, "\n",
424             "<<<<\n"
425             }
426             return POSIX::floor(
427             1.5 - $self->month / 12 + (moment($self->{gregorian}) - GREGORIAN_CHINESE_EPOCH_MOMENT) / MEAN_TROPICAL_YEAR);
428             }
429              
430             # [1] p.250
431             sub _prior_leap_month
432             {
433             my($class, $start, $end) = @_;
434              
435             if (DEBUG) {
436             print STDERR
437             ">>>> prior_leap_month\n",
438             "caller: ", join(':', (caller)[1, 2]), "\n",
439             "start: ", $start, "\n",
440             "end: ", $end, "\n",
441             "<<<<\n";
442             }
443              
444             while ($start <= $end) {
445             if (no_major_term_on($end)) {
446             if (DEBUG) {
447             print STDERR " + prior_leap_month: there are no major terms on $end\n";
448             }
449             return 1;
450             }
451              
452             $end = new_moon_before($end - DateTime::Duration->new(minutes => 30));
453             }
454             if (DEBUG) {
455             print " + prior_leap_month: nothing found, returning false\n";
456             }
457              
458             return ();
459             }
460              
461             1;
462             __END__