File Coverage

blib/lib/DateTime/Event/Easter.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #
2             # Perl DateTime extension for computing the dates for Easter and related feasts
3             # Copyright (C) 2003-2004, 2015 Rick Measham and Jean Forget
4             #
5             # See the license in the embedded documentation below.
6             #
7             package DateTime::Event::Easter;
8 9     9   150841 use DateTime;
  0            
  0            
9             use DateTime::Set;
10             use Carp;
11             use Params::Validate qw( validate SCALAR BOOLEAN OBJECT );
12              
13             use strict;
14             use warnings;
15             use vars qw(
16             $VERSION @ISA @EXPORT @EXPORT_OK
17             );
18              
19             require Exporter;
20              
21             @ISA = qw(Exporter);
22              
23             @EXPORT_OK = qw(easter);
24             $VERSION = '1.05';
25              
26             sub new {
27             my $class = shift;
28             my %args = validate( @_,
29             { easter => { type => SCALAR, default=>'western', optional=>1, regex => qr/^(western|eastern)$/i },
30             day => { type => SCALAR, default=>'sunday', optional=>1 },
31             as => { type => SCALAR, default=>'point', optional=>1 },
32             }
33             );
34            
35             my %self;
36             my $offset;
37             if ($args{day} =~/^fat/i) {
38             $offset = -47;
39             }
40             elsif ($args{day} =~/^ash/i) {
41             # First day of lent. Lent lasts for 40 days, excluding sundays.
42             # This translates to a 46-day duration, including sundays.
43             $offset = -46;
44             }
45             elsif ($args{day} =~/^ascension/i) {
46             $offset = 39;
47             }
48             elsif ($args{day} =~/^pentecost/i) {
49             $offset = 49;
50             }
51             elsif ($args{day} =~/^trinity/i) {
52             $offset = 56;
53             }
54             elsif ($args{day} =~/^palm/i) {
55             $offset = -7;
56             } elsif ($args{day} =~/saturday/i) {
57             $offset = -1;
58             } elsif ($args{day} =~/friday/i) {
59             $offset = -2;
60             } elsif ($args{day} =~/thursday/i) {
61             $offset = -3;
62             } elsif ($args{day} =~/^\-?\d+$/i) {
63             $offset = $args{day};
64             } else {
65             $offset = 0;
66             }
67             $self{offset} = DateTime::Duration->new(days=>$offset);
68             $self{easter} = lc $args{easter};
69            
70             if ($self{easter} eq 'eastern') {
71             require DateTime::Calendar::Julian;
72             }
73              
74             # Set to return points or spans
75             die("Argument 'as' must be 'point' or 'span'.") unless $args{as}=~/^(point|span)s?$/i;
76             $self{as} = lc $1;
77              
78             return bless \%self, $class;
79            
80             }
81              
82              
83             sub following {
84             my $self = shift;
85             my $dt = shift;
86              
87             my $class = ref($dt);
88             if ($self->{easter} eq 'eastern' && $class ne 'DateTime::Calendar::Julian') {
89             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
90             $dt = DateTime::Calendar::Julian->from_object(object=>$dt);
91             } elsif ($class ne 'DateTime') {
92             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
93             $dt = DateTime->from_object(object=>$dt);
94             }
95              
96             my $easter_this_year = $self->_easter($dt->year)+$self->{offset};
97              
98             my $easter = ($easter_this_year > $dt)
99             ? $easter_this_year
100             : $self->_easter($dt->year+1)+$self->{offset};
101              
102             $easter = $class->from_object(object=>$easter) if (ref($easter) ne $class);
103             return ($self->{as} eq 'span')
104             ? _tospan($easter)
105             : $easter;
106             }
107              
108             sub previous {
109             my $self = shift;
110             my $dt = shift;
111            
112             my $class = ref($dt);
113             if ($self->{easter} eq 'eastern' && $class ne 'DateTime::Calendar::Julian') {
114             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
115             $dt = DateTime::Calendar::Julian->from_object(object=>$dt);
116             } elsif ($class ne 'DateTime') {
117             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
118             $dt = DateTime->from_object(object=>$dt);
119             }
120              
121             my $easter_this_year = $self->_easter($dt->year)+$self->{offset};
122              
123             my $easter = ($easter_this_year->ymd lt $dt->ymd)
124             ? $easter_this_year
125             : $self->_easter($dt->year-1)+$self->{offset};
126              
127              
128             $easter = $class->from_object(object=>$easter) if (ref($easter) ne $class);
129             return ($self->{as} eq 'span')
130             ? _tospan($easter)
131             : $easter;
132             }
133              
134             sub closest {
135             my $self = shift;
136             my $dt = shift;
137              
138             my $class = ref($dt);
139             if ($class ne 'DateTime') {
140             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
141             $dt = DateTime->from_object(object=>$dt);
142             }
143              
144             if ($self->is($dt)) {
145             my $easter = $dt->clone->truncate(to=>'day');
146             $easter = $class->from_object(object=>$easter) if (ref($easter) ne $class);
147             return ($self->{as} eq 'span')
148             ? _tospan($easter)
149             : $easter;
150             }
151             my $following_easter = $self->following($dt);
152             my $following_delta = $following_easter - $dt;
153             my $previous_easter = $self->previous($dt);
154            
155             my $easter = ($previous_easter + $following_delta < $dt)
156             ? $following_easter
157             : $previous_easter;
158             $easter = $class->from_object(object=>$easter) if (ref($easter) ne $class);
159             return ($self->{as} eq 'span')
160             ? _tospan($easter)
161             : $easter;
162             }
163              
164             sub is {
165             my $self = shift;
166             my $dt = shift;
167              
168             my $class = ref($dt);
169             if ($class ne 'DateTime') {
170             croak ("Dates need to be datetime objects") unless ($dt->can('utc_rd_values'));
171             $dt = DateTime->from_object(object=>$dt);
172             }
173              
174             if ($self->{easter} eq 'eastern') {
175             $dt = DateTime::Calendar::Julian->from_object(object=>$dt)
176             }
177              
178             my $easter_this_year = $self->_easter($dt->year)+$self->{offset};
179              
180             return ($easter_this_year->ymd eq $dt->ymd) ? 1 : 0;
181             }
182              
183             sub as_list {
184             my $self = shift;
185             my %args = validate( @_,
186             { from => { type => OBJECT },
187             to => { type => OBJECT },
188             inclusive => { type => SCALAR, default=>0 },
189             }
190             );
191            
192             # Make sure our args are in the right order
193             ($args{from}, $args{to}) = sort ($args{from}, $args{to});
194            
195             my @set = ();
196            
197             if ($args{inclusive}) {
198             if ($self->is($args{from})) {
199             push(@set,$args{from});
200             }
201             if ($self->is($args{to})) {
202             push(@set,$args{to});
203             }
204             }
205            
206             my $checkdate = $args{from};
207              
208             while ($checkdate < $args{to}) {
209             $checkdate = $self->following($checkdate);
210             push(@set,$checkdate) if ($checkdate < $args{to});
211             }
212            
213             return sort @set;
214             }
215              
216             sub as_old_set {
217             my $self = shift;
218             return DateTime::Set->from_datetimes( dates => [ $self->as_list(@_) ] );
219             }
220             sub as_set {
221             my $self = shift;
222             my %args = @_;
223             if (exists $args{inclusive}) {
224             croak("You must specify both a 'from' and a 'to' datetime") unless
225             ref($args{to})=~/DateTime/ and
226             ref($args{from})=~/DateTime/;
227             if ($args{inclusive}) {
228             $args{start} = delete $args{from};
229             $args{end} = delete $args{to};
230             } else {
231             $args{after} = delete $args{from};
232             $args{before} = delete $args{to};
233             }
234             delete $args{inclusive};
235             } elsif (exists $args{from} or exists $args{to}) {
236             croak("You must specify both a 'from' and a 'to' datetime") unless
237             ref($args{to})=~/DateTime/ and
238             ref($args{from})=~/DateTime/;
239             $args{after} = delete $args{from};
240             $args{before} = delete $args{to};
241             }
242             return DateTime::Set->from_recurrence(
243             next => sub { return $_[0] if $_[0]->is_infinite; $self->following( $_[0] ) },
244             previous => sub { return $_[0] if $_[0]->is_infinite; $self->previous( $_[0] ) },
245             %args
246             );
247             }
248              
249             sub as_span {
250             my $self = shift;
251             $self->{as} = 'span';
252             return $self;
253             }
254              
255             sub as_point {
256             my $self = shift;
257             $self->{as} = 'point';
258             return $self;
259             }
260              
261             sub _tospan {
262             return DateTime::Span->from_datetime_and_duration(
263             start => $_[0],
264             hours => 24,
265             );
266             }
267              
268             sub _easter {
269             my $self = shift;
270             my $year = shift;
271             return ($self->{easter} eq 'eastern')
272             ? eastern_easter($year)
273             : western_easter($year);
274             }
275              
276             sub western_easter {
277             my $year = shift;
278             croak "Year value '$year' should be numeric." if $year!~/^\-?\d+$/;
279            
280             my $golden_number = $year % 19;
281             #quasicentury is so named because its a century, only its
282             # the number of full centuries rather than the current century
283             my $quasicentury = int($year / 100);
284             my $epact = ($quasicentury - int($quasicentury/4) - int(($quasicentury * 8 + 13)/25) + ($golden_number*19) + 15) % 30;
285             my $interval = $epact - int($epact/28)*(1 - int(29/($epact+1)) * int((21 - $golden_number)/11) );
286             my $weekday = ($year + int($year/4) + $interval + 2 - $quasicentury + int($quasicentury/4)) % 7;
287            
288             my $offset = $interval - $weekday;
289             my $month = 3 + int(($offset+40)/44);
290             my $day = $offset + 28 - 31* int($month/4);
291            
292             return DateTime->new(year=>$year, month=>$month, day=>$day);
293             }
294             *easter = \&western_easter; #alias so people can call 'easter($year)' externally
295              
296             sub eastern_easter {
297             my $year = shift;
298             croak "Year value '$year' should be numeric." if $year!~/^\-?\d+$/;
299            
300             my $golden_number = $year % 19;
301              
302             my $interval = ($golden_number * 19 + 15) % 30;
303             my $weekday = ($year + int($year/4) + $interval) % 7;
304            
305             my $offset = $interval - $weekday;
306             my $month = 3 + int(($offset+40)/44);
307             my $day = $offset + 28 - 31* int($month/4);
308              
309             return DateTime::Calendar::Julian->new(year=>$year, month=>$month, day=>$day);
310             }
311              
312             # Ending a module with an unspecified number, which could be zero, is wrong.
313             # Therefore the custom of ending a module with a boring "1".
314             # Instead of that, end it with some verse.
315             q{
316             Il reviendra à Pâques, mironton mironton mirontaine,
317             Il reviendra à Pâques
318             Ou à la Trinité.
319             Ou à la Trinité...
320             };
321             __END__