File Coverage

blib/lib/Calendar/Any/Chinese.pm
Criterion Covered Total %
statement 63 166 37.9
branch 18 46 39.1
condition 5 18 27.7
subroutine 16 38 42.1
pod 13 25 52.0
total 115 293 39.2


line stmt bran cond sub pod time code
1             package Calendar::Any::Chinese;
2             {
3             $Calendar::Any::Chinese::VERSION = '0.5';
4             }
5 1     1   19778 use base 'Calendar::Any';
  1         2  
  1         555  
6 1     1   7 use Carp;
  1         2  
  1         62  
7 1     1   527 use Calendar::Any::Gregorian;
  1         2  
  1         32  
8 1     1   680 use Calendar::Any::Util::Lunar;
  1         3  
  1         2956  
9              
10             sub new {
11 1     1 1 12 my $_class = shift;
12 1   33     7 my $class = ref $_class || $_class;
13 1         2 my $self = {};
14 1         2 bless $self, $class;
15 1 50       4 if ( @_ ) {
16 1         2 my %arg;
17 1 50       6 if ( $_[0] =~ /-\D/ ) {
18 0         0 %arg = @_;
19             } else {
20 1 50       4 if ( $#_ > 0 ) {
21 1         9 $arg{$_} = shift for qw(-cycle -year -month -day);
22             } else {
23 0         0 return $self->from_absolute(@_);
24             }
25             }
26 1         2 foreach ( qw(-cycle -year -month -day) ) {
27 4 50       84 $self->{substr($_, 1)} = $arg{$_} if exists $arg{$_};
28             }
29 1         4 $self->absolute_date();
30             }
31 1         3 return $self;
32             }
33              
34             sub from_absolute {
35 0     0 0 0 my $self = shift;
36 0         0 my $absdate = shift;
37 0         0 $self->{absolute} = $absdate;
38 0         0 my $date = Calendar::Any::Gregorian->new($absdate);
39 0         0 $self->{gdate} = $date;
40 0         0 my $cyear = $date->year+2695;
41 0         0 my @list = (@{_year($date->year-1)},
  0         0  
42 0         0 @{_year($date->year)},
43 0         0 @{_year($date->year+1)});
44 0         0 foreach ( 0..$#list ) {
45 0 0       0 if ( $list[$_]->[0] == 1 ) {
46 0         0 $cyear++;
47             }
48 0 0       0 if ( $list[$_+1]->[1] > $absdate ) {
49 0         0 $date = $list[$_];
50 0         0 last;
51             }
52             }
53 0         0 $self->{cycle} = int(($cyear-1)/60);
54 0         0 $self->{year} = _mod($cyear, 60);
55 0         0 $self->{month} = $date->[0];
56 0         0 $self->{day} = $absdate - $date->[1] + 1;
57 0         0 return $self;
58             }
59              
60             sub absolute_date {
61 5     5 0 6 my $self = shift;
62 5 100       14 if (exists $self->{absolute} ) {
63 4         36 return $self->{absolute};
64             }
65 1         4 my ($cycle, $year, $month, $day) = ($self->{cycle}, $self->{year}, $self->{month}, $self->{day});
66 1         4 my $gyear = 60*($cycle-1)+$year-1-2636;
67 1         4 my $monthday = _assoc_month($month, [_memq_month(1, _year($gyear)), @{_year($gyear+1)}]);
  1         3  
68 1         6 $self->{absolute} = $day-1+$monthday->[1];
69 1         4 $self->assert_date();
70 1         4 return $self->{absolute};
71             }
72              
73 0     0 1 0 sub cycle { shift->{cycle}; }
74              
75             sub is_leap_year {
76 0     0 1 0 my $self = shift;
77 0         0 my $list = _year_month_list($self->cycle, $self->year);
78 0         0 return $#{$list} == 12;
  0         0  
79             }
80              
81 1     1 1 6 sub gyear { shift->gdate->year; }
82              
83 1     1 1 3 sub gmonth { shift->gdate->month; }
84              
85 1     1 1 4 sub gday { shift->gdate->day; }
86              
87             sub gdate {
88 3     3 1 4 my $self = shift;
89 3 100       7 if ( !exists $self->{gdate} ) {
90 1         3 $self->{gdate} = Calendar::Any::Gregorian->new($self->absolute_date);
91             }
92 3         17 return $self->{gdate};
93             }
94              
95             sub last_day_of_month {
96 1     1 1 3 my $self = shift;
97 1 50       4 my $date = Calendar::Any::Util::Lunar::new_moon_date
98             ( $self->day==1 ? $self->absolute_date+1 : $self,
99             timezone(Calendar::Any::Gregorian->new($self->absolute_date)->year));
100 1         4 return int($date-1-$self->absolute_date + $self->day);
101             }
102              
103             sub year_month_list {
104 0     0 1 0 my $self = shift;
105 0         0 return _year_month_list($self->cycle, $self->year);
106             }
107              
108             sub timezone {
109 1     1 1 2 my $year = shift;
110 1 50 33     11 return ((defined $year && $year >= 1928) ? 480 : 465 + 40.0/60.0 );
111             }
112              
113             sub next_jieqi_date {
114 0     0 1 0 Calendar::Any::Util::Solar::next_longitude_date($_[0], 15, $_[1]);
115             }
116              
117             sub assert_date {
118 1     1 0 2 my $self = shift;
119 1 50 33     11 if ( $self->year < 1 || $self->year > 60 ) {
120 0         0 confess('Not a valid year: should not from 1 to 60 for ' . ref $self);
121             }
122 1 50 33     8 if ( $self->month < 1 || $self->month > 12 ) {
123 0         0 confess(sprintf('Not a valid month %d: should from 1 to 12 for %s', $self->month, ref $self));
124             }
125 1 50 33     7 if ( $self->day < 1 || $self->day > $self->last_day_of_month() ) {
126 0         0 confess(sprintf('Not a valid day %d: should from 1 to %d in %d, %d for %s',
127             $self->day, $self->last_day_of_month, $self->month, $self->year, ref $self));
128             }
129             }
130              
131             #==========================================================
132             # Format calendar
133             #==========================================================
134             our @celestial_stem = qw(甲 乙 丙 丁 戊 已 庚 辛 壬 癸);
135             our @terrestrial_branch = qw(子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥);
136             our @weekday_name = qw(日 一 二 三 四 五 六);
137             our @month_name =
138             qw(正月 二月 三月 四月 五月 六月 七月 八月 九月 十月 十一月 腊月);
139             our @day_name = qw
140             (初一 初二 初三 初四 初五 初六 初七 初八 初九 初十
141             十一 十二 十三 十四 十五 十六 十七 十八 十九 二十
142             廿一 廿二 廿三 廿四 廿五 廿六 廿七 廿八 廿九 三十
143             卅一);
144             our @zodiac_name = qw(鼠 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪);
145             our @jieqi_name = qw
146             (小寒 大寒 立春 雨水 惊蛰 春分
147             清明 谷雨 立夏 小满 芒种 夏至
148             小暑 大暑 立秋 处暑 白露 秋分
149             寒露 霜降 立冬 小雪 大雪 冬至);
150              
151             sub day_name {
152 0     0 0 0 return $day_name[shift->day-1];
153             }
154              
155             sub month_name {
156 0     0 1 0 my $self = shift;
157 0         0 my $month = $self->month;
158 0 0       0 if ( _is_int($month) ) {
159 0         0 $month_name[$month-1];
160             } else {
161 0         0 return "闰".$month_name[$month-1];
162             }
163             }
164              
165             sub weekday_name {
166 0     0 1 0 return "星期".$weekday_name[shift->weekday];
167             }
168              
169             sub sexagesimal_name {
170 0     0 0 0 my $self = shift;
171 0         0 my $year = $self->year-1;
172 0         0 return $celestial_stem[$year%10] . $terrestrial_branch[$year%12];
173             }
174              
175             sub zodiac_name {
176 0     0 0 0 my $self = shift;
177 0         0 my $year = $self->year-1;
178 0         0 return $zodiac_name[$year%12];
179             }
180              
181 0     0 0 0 sub format_Y { shift->gyear }
182 0     0 0 0 sub format_S { shift->sexagesimal_name }
183 0     0 0 0 sub format_D { shift->day_name }
184 0     0 0 0 sub format_Z { shift->zodiac_name }
185 0     0 0 0 sub format_m { sprintf("%02d", shift->gmonth) }
186 0     0 0 0 sub format_d { sprintf("%02d", shift->gday) }
187             our $default_format = "%Y年%m月%d日 %W %S%Z年%M%D";
188              
189             #==========================================================
190             # Private functions
191             #==========================================================
192             #==========================================================
193             # Input : chinese year cycle, year
194             # Output : the array of month in the chinese year
195             # Desc :
196             #==========================================================
197             sub _year_month_list {
198 0     0   0 my ($cycle, $year) = @_;
199 0         0 my $date = __PACKAGE__->new($cycle, $year, 1, 1);
200 0         0 $year = $date->gyear;
201 0         0 my $list1 = _year($year);
202 0         0 my $list2 = _year($year+1);
203 0         0 my @list = _memq_month(1, $list1);
204 0         0 foreach ( @$list2 ) {
205 0 0       0 last if $_->[0]==1;
206 0         0 push @list, $_;
207             }
208 0         0 return \@list;
209             }
210              
211             #==========================================================
212             # Input : x, y
213             # Output : x modulo y, range from 1-y
214             # Desc : like operator %, but instead of 0, return the exclusive y
215             #==========================================================
216             sub _mod {
217 0 0   0   0 $_[0] % $_[1] || $_[1];
218             }
219              
220             sub _is_int {
221 0     0   0 $_[0]-int($_[0])==0;
222             }
223              
224             #==========================================================
225             # Input : month, an array of month list
226             # Output : the month list from month
227             # Desc : eg, _memq_month(2, [[12, 726464], [1, 726494], [2, 726523], [3, 726553], ...])
228             # return [[2, 726523], [3, 726553], ...]
229             #==========================================================
230             sub _memq_month {
231 1     1   2 my ($month, $list) = @_;
232 1         2 my $i = 0;
233 1         4 for ( ; $i<=$#$list; $i++ ) {
234 2 100       8 last if ($list->[$i][0] == $month);
235             }
236 1         4 return @{$list}[$i..$#$list];
  1         12  
237             }
238              
239             #==========================================================
240             # Input : month, an array of month list
241             # Output : the month in the list
242             # Desc : eg, _assoc_month(2, [[12, 726464], [1, 726494], [2, 726523], [3, 726553], ...])
243             # return [2, 726523]
244             #==========================================================
245             sub _assoc_month {
246 1     1   1 my ($month, $list) = @_;
247 1         3 foreach ( @$list ) {
248 12 100       22 return $_ if $_->[0] == $month;
249             }
250             }
251              
252             #==========================================================
253             # Input : Gregorian year
254             # Output : the chinese month list of the year
255             # Desc : The month list always range from winter solstice day in year-1
256             # to winter in solstice day. Usually, the month list is start
257             # chinese month 12 in last year, but possible start from 11.5.
258             # The month with .5 indicate that is a leap month.
259             #==========================================================
260             my %year_cache = (
261             '2000' => [
262             [12, 730126],[1, 730155],[2, 730185],[3, 730215],[4, 730244],[5, 730273],
263             [6, 730303],[7, 730332],[8, 730361],[9, 730391],[10, 730420],[11, 730450]
264             ],
265             '2001' => [
266             [12, 730480],[1, 730509],[2, 730539],[3, 730569],[4, 730598],[4.5, 730628],
267             [5, 730657],[6, 730687],[7, 730716],[8, 730745],[9, 730775],[10, 730804],
268             [11, 730834]
269             ],
270             '2002' => [
271             [12, 730863],[1, 730893],[2, 730923],[3, 730953],[4, 730982],[5, 731012],
272             [6, 731041],[7, 731071],[8, 731100],[9, 731129],[10, 731159],[11, 731188]
273             ],
274             '2003' => [
275             [12, 731218],[1, 731247],[2, 731277],[3, 731307],[4, 731336],[5, 731366],
276             [6, 731396],[7, 731425],[8, 731455],[9, 731484],[10, 731513],[11, 731543]
277             ],
278             '2004' => [
279             [12, 731572],[1, 731602],[2, 731631],[2.5, 731661],[3, 731690],[4, 731720],
280             [5, 731750],[6, 731779],[7, 731809],[8, 731838],[9, 731868],[10, 731897],
281             [11, 731927]
282             ],
283             '2005' => [
284             [12, 731956],[1, 731986],[2, 732015],[3, 732045],[4, 732074],[5, 732104],
285             [6, 732133],[7, 732163],[8, 732193],[9, 732222],[10, 732252],[11, 732281]
286             ],
287             '2006' => [
288             [12, 732311],[1, 732340],[2, 732370],[3, 732399],[4, 732429],[5, 732458],
289             [6, 732488],[7, 732517],[7.5, 732547],[8, 732576],[9, 732606],[10, 732636],
290             [11, 732665]
291             ],
292             '2007' => [
293             [12, 732695],[1, 732725],[2, 732754],[3, 732783],[4, 732813],[5, 732842],
294             [6, 732871],[7, 732901],[8, 732930],[9, 732960],[10, 732990],[11, 733020]
295             ],
296             '2008' => [
297             [12, 733049],[1, 733079],[2, 733109],[3, 733138],[4, 733167],[5, 733197],
298             [6, 733226],[7, 733255],[8, 733285],[9, 733314],[10, 733344],[11, 733374]
299             ],
300             '2009' => [
301             [12, 733403],[1, 733433],[2, 733463],[3, 733493],[4, 733522],[5, 733551],
302             [5.5, 733581],[6, 733610],[7, 733639],[8, 733669],[9, 733698],[10, 733728],
303             [11, 733757]
304             ],
305             '2010' => [
306             [12, 733787],[1, 733817],[2, 733847],[3, 733876],[4, 733906],[5, 733935],
307             [6, 733965],[7, 733994],[8, 734023],[9, 734053],[10, 734082],[11, 734112]
308             ],
309             '2011' => [
310             [12, 734141],[1, 734171],[2, 734201],[3, 734230],[4, 734260],[5, 734290],
311             [6, 734319],[7, 734349],[8, 734378],[9, 734407],[10, 734437],[11, 734466]
312             ],
313             '2012' => [
314             [12, 734496],[1, 734525],[2, 734555],[3, 734584],[4, 734614],[4.5, 734644],
315             [5, 734673],[6, 734703],[7, 734732],[8, 734762],[9, 734791],[10, 734821],
316             [11, 734850]
317             ],
318             '2013' => [
319             [12, 734880],[1, 734909],[2, 734939],[3, 734968],[4, 734998],[5, 735027],
320             [6, 735057],[7, 735087],[8, 735116],[9, 735146],[10, 735175],[11, 735205]
321             ],
322             '2014' => [
323             [12, 735234],[1, 735264],[2, 735293],[3, 735323],[4, 735352],[5, 735382],
324             [6, 735411],[7, 735441],[8, 735470],[9, 735500],[9.5, 735530],[10, 735559],
325             [11, 735589]
326             ],
327             '2015' => [
328             [12, 735618],[1, 735648],[2, 735677],[3, 735707],[4, 735736],[5, 735765],
329             [6, 735795],[7, 735824],[8, 735854],[9, 735884],[10, 735914],[11, 735943]
330             ],
331             '2016' => [
332             [12, 735973],[1, 736002],[2, 736032],[3, 736061],[4, 736091],[5, 736120],
333             [6, 736149],[7, 736179],[8, 736208],[9, 736238],[10, 736268],[11, 736297]
334             ],
335             '2017' => [
336             [12, 736327],[1, 736357],[2, 736386],[3, 736416],[4, 736445],[5, 736475],
337             [6, 736504],[6.5, 736533],[7, 736563],[8, 736592],[9, 736622],[10, 736651],
338             [11, 736681]
339             ],
340             '2018' => [
341             [12, 736711],[1, 736741],[2, 736770],[3, 736800],[4, 736829],[5, 736859],
342             [6, 736888],[7, 736917],[8, 736947],[9, 736976],[10, 737006],[11, 737035]
343             ],
344             '2019' => [
345             [12, 737065],[1, 737095],[2, 737125],[3, 737154],[4, 737184],[5, 737213],
346             [6, 737243],[7, 737272],[8, 737301],[9, 737331],[10, 737360],[11, 737389]
347             ],
348             '2020' => [
349             [12, 737419],[1, 737449],[2, 737478],[3, 737508],[4, 737538],[4.5, 737568],
350             [5, 737597],[6, 737627],[7, 737656],[8, 737685],[9, 737715],[10, 737744],
351             [11, 737774]
352             ],
353             );
354              
355             sub _year {
356 2     2   4 my $y = shift;
357 2 50       7 if ( !exists $year_cache{$y} ) {
358 0         0 $year_cache{$y} = _compute_chinese_year($y);
359             }
360 2         12 return $year_cache{$y};
361             }
362              
363             sub _compute_chinese_year {
364 0     0     my $y = shift;
365 0           my $oldtz = $Calendar::Any::Util::Solar::timezone;
366 0           $Calendar::Any::Util::Solar::timezone = timezone($y);
367 0           my $next_solstice = _zodiac_sign(Calendar::Any::Gregorian->new(12, 15, $y));
368 0           my $months = _month_list(_zodiac_sign(Calendar::Any::Gregorian->new(12, 15, $y-1))+1,
369             $next_solstice);
370 0           my $list;
371 0 0         if ( scalar(@$months) == 12 ) {
372 0           $list = [[12, $months->[0]], map { [ $_, $months->[$_] ]} 1..11];
  0            
373             } else {
374 0           my $next_sign = _zodiac_sign($months->[0]);
375 0 0 0       if ( $months->[0]>$next_sign || $next_sign >= $months->[1] ) {
376 0           $list = [[11.5, $months->[0]], [12, $months->[1]],
377 0           map { [ $_, $months->[$_+1] ] } 1..11];
378             } else {
379 0           my @list = ([12, $months->[0]]);
380 0 0         if ( _zodiac_sign($months->[1]) >= _zodiac_sign($months->[2]) ) {
381 0           push @list, [12.5, $months->[1]],
382 0           map { [ $_, $months->[$_+1] ] } 1..11;
383             } else {
384 0           push @list, [1, $months->[1]];
385 0           my $i = 2;
386 0           while ( $months->[$i+1] > _zodiac_sign($months->[$i]) ) {
387 0           push @list, [$i, $months->[$i]];
388 0           $i++;
389             }
390 0           push @list, [$i-0.5, $months->[$i]];
391 0           foreach ( $i..11 ) {
392 0           push @list, [$_, $months->[$_+1]];
393             }
394             }
395 0           $list = \@list;
396             }
397             }
398 0           $Calendar::Any::Util::Solar::timezone = $oldtz;
399 0           return $list;
400             }
401              
402             sub _zodiac_sign {
403 0     0     int(Calendar::Any::Util::Solar::next_longitude_date(shift, 30));
404             }
405              
406             #==========================================================
407             # Input : start, end, timezone
408             # Output : the array of new moon date between start and end
409             # Desc : start and end should be Calendar object or absolute date
410             #==========================================================
411             sub _month_list {
412 0     0     my ($start, $end) = @_;
413 0           my @list;
414 0           while ( $start <= $end ) {
415 0           $start = int(Calendar::Any::Util::Lunar::new_moon_date($start));
416 0           push @list, $start;
417 0           $start++;
418             }
419 0 0         pop @list if $list[-1]>$end;
420 0           return \@list;
421             }
422              
423             1;
424              
425             __END__