File Coverage

blib/lib/DateTime/Event/NameDay.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package DateTime::Event::NameDay;
2              
3 1     1   844 use strict;
  1         2  
  1         41  
4              
5 1     1   5 use vars qw ($VERSION);
  1         2  
  1         88  
6              
7             $VERSION = '0.02';
8              
9 1     1   15 use Carp;
  1         2  
  1         73  
10 1     1   898 use Params::Validate qw( validate SCALAR OBJECT HASHREF );
  1         11942  
  1         95  
11              
12 1     1   458 use DateTime;
  0            
  0            
13             use DateTime::Set;
14             use DateTime::Calendar::Christian;
15              
16              
17             my %namedays = ();
18              
19              
20             sub new {
21             my $class = shift;
22             my %p = validate( @_,
23             { country => { type => SCALAR,
24             default => undef,
25             # Leave the heavy validation to set
26             },
27             date_args => { type => HASHREF,
28             default => {},
29             },
30             },
31             );
32             my $self = { };
33             bless $self, $class;
34             $self->set( %p );
35              
36             return $self;
37             }
38              
39             sub set {
40             my $self = shift;
41              
42             my %p = validate( @_,
43             { country => { type => SCALAR,
44             optional => 1,
45             callbacks =>
46             {'known day mapping' => \&_check_country }
47             },
48             date_args => { type => HASHREF,
49             default => {},
50             },
51             }
52             );
53              
54             if (defined $p{country}) {
55             $self->{country} = lc $p{country};
56             }
57              
58             if (defined $p{date_args}) {
59             $self->{date_args} = $p{date_args};
60             }
61              
62             return $self;
63             }
64              
65             sub country {
66             my ($self) = @_;
67             return undef unless ref $self;
68             return $self->{country};
69             }
70              
71              
72             sub date_args {
73             my ($self) = @_;
74             return {} unless ref $self;
75             return $self->{date_args};
76             }
77              
78             sub get_daynames
79             {
80             my $self = shift;
81             my %p = validate( @_,
82             { country => { type => SCALAR,
83             optional => 1,
84             callbacks =>
85             {'known day mapping' => \&_check_country }
86             },
87             date => { type => OBJECT,
88             can => 'utc_rd_values',
89             },
90             }
91             );
92              
93             # Work out our country
94             my $country = lc $p{country};
95             if (not defined $country) {
96             $country = $self->country();
97              
98             croak "Unable to determine the correct country"
99             unless defined $country;
100             }
101              
102             # Get the namedays for the given date
103             # - Find our section
104             my $nameday_info =
105             $self->_init_nameday_country(namedays => \%namedays,
106             country => $country);
107              
108             # - Convert to the Julian calendar
109             my $adj_dt = DateTime::Calendar::Christian->from_object
110             (object => $p{date},
111             reform_date => $nameday_info->{reform_date},
112             %{ $self->date_args() },
113             );
114            
115             # - Get the appropriate nameday based on month number and day
116             my $names = $nameday_info->{names}{ $adj_dt->month() }{ $adj_dt->day() };
117             my @names = defined $names ? @$names : ();
118              
119             return @names;
120             }
121              
122             sub get_namedays {
123             my $self = shift;
124             my %p = validate( @_,
125             { country => { type => SCALAR,
126             optional => 1,
127             callbacks =>
128             {'known day mapping' => \&_check_country }
129             },
130             date_args => { type => HASHREF,
131             default => undef,
132             },
133             name => { type => SCALAR,
134             },
135             }
136             );
137              
138             # Work out our country
139             my $country = lc $p{country};
140             if (not defined $country) {
141             $country = $self->country();
142              
143             croak "Unable to determine the correct country"
144             unless defined $country;
145             }
146              
147             # Work out the date args
148             my $date_args = $p{date_args};
149             if (not defined $date_args) {
150             $date_args = ref $self ? $self->date_args() : {};
151             }
152              
153             # Get the canonical name
154             my $name = _clean_name( $p{name} );
155              
156             # Find the month and day for the given name
157             my $nameday_info =
158             $self->_init_nameday_country(namedays => \%namedays,
159             country => $country);
160             croak "Unknown name '$p{name}' for country '$p{country}'"
161             unless exists $nameday_info->{reverse_names}{$name};
162             my ($month, $day) = @{ $nameday_info->{reverse_names}{$name} };
163              
164             # Build a set of all of the days that the given name is for
165             my $set = DateTime::Set->from_recurrence
166             (next =>
167             sub { _make_recurrence($_[0], $nameday_info->{reform_date},
168             $month, $day, 1, $date_args);
169             },
170             previous =>
171             sub { _make_recurrence($_[0], $nameday_info->{reform_date},
172             $month, $day, -1, $date_args);
173             },
174             );
175              
176             return $set;
177             }
178              
179             sub _make_recurrence {
180             my ($last, $reform_date, $month, $day, $direction, $date_args) = @_;
181              
182             my $dt = DateTime::Calendar::Christian->from_object
183             (object => $last,
184             reform_date => $reform_date,
185             %$date_args,
186             );
187             $dt->truncate(to => 'day');
188             my $target = $dt->clone();
189             $target->set( month => $month,
190             day => $day,
191             );
192            
193             if ($direction == 1) {
194             if ($dt >= $target) {
195             $target->add( years => 1);
196             }
197             } else {
198             if ($dt <= $target) {
199             $target->subtract( years => 1);
200             }
201             }
202              
203             $target->set( month => $month,
204             day => $day,
205             );
206              
207             return DateTime->from_object(object => $target, %$date_args);
208             }
209              
210             sub _init_nameday_country {
211             my $self = shift;
212             my %args = @_;
213             my $country = $args{country};
214             my $namedays = $args{namedays};
215              
216              
217             return $namedays->{$country}
218             if exists $namedays->{$country};
219            
220             # Okay, load the nameday info from the sub item
221             my $package = "DateTime::Event::NameDay::$country";
222             eval "require $package;";
223            
224             my ($reform_date, $data) = $package->nameday_data();
225             my @data = split /\n/, $data;
226             undef $data;
227              
228             $namedays->{$country} =
229             {reform_date => $reform_date,
230             names => {},
231             reverse_names => {},
232             };
233             my $forward = $namedays->{$country}{names};
234             my $reverse = $namedays->{$country}{reverse_names};
235            
236              
237             # Format of files:
238             # - #s are comments, blank lines are ignored
239             # - leading whitespace and trailing whitespace is ignorred
240             # - months occur by number on their own line followed by a : (i.e. 1:)
241             # - name days are given by number followed by a space then a , separated list of names
242             # surrounding whitespace is trimmed
243             # - names starting with * are immovable holidays
244             #
245             # e.g.:
246             # # Nameday file
247             # # Source: www.whatever.com
248             #
249             # 1: # January
250             # 1 *New Year's Day
251             # 2 Svea
252             # 3 Alfred, Alfrida
253             # ...
254             #
255             # 2: # February
256             # ...
257              
258             my $month = undef;
259             foreach my $line (@data) {
260             $line =~ s/\s*\#.*//;
261             next if $line =~ /^\s*$/;
262            
263             if ($line =~ /^\s*(\d+)\s+(.*)/) {
264             my ($day, $names) = ($1, $2);
265              
266             # We have a day
267             croak "Malformed nameday file for '$package': Missing month before line '$line'"
268             unless defined $month;
269              
270             # Split the names apart and store the forward mapping
271             # For the moment remove the * indicating holidays
272             $names =~ s/\s+$//;
273             my @names = map { s/^*//; $_ } split /\s*,\s*/, $names;
274             $forward->{$month}{$day} = \@names;
275              
276             # Store the reverse mapping
277             foreach my $name (@names) {
278             my $n = _clean_name($name);
279             croak "Duplicate name '$name' (cleaned '$n')"
280             if exists $reverse->{$n};
281             $reverse->{$n} = [$month, $day];
282             }
283             }
284             elsif ($line =~ /^\s*(\d+):\s*$/) {
285             # Change the month
286             $month = $1;
287             }
288             }
289             @data = ();
290              
291             return $namedays->{$country};
292             }
293              
294             # Get the canonical sort string for the name... all lowercase
295             # I would like to make it insensitive to accents too, but there
296             # is no good way to do that yet
297             sub _clean_name {
298             my $name = shift;
299              
300             $name = lc $name;
301             # $name = NFD( lc($name) );
302             # $name =~ s/\pM//g;
303              
304             return $name;
305             }
306              
307             # See if the given country is one we support
308             sub _check_country {
309             my $country = lc shift;
310            
311             # See if we can load the module
312             my $package = "DateTime::Event::NameDay::$country";
313             eval "require $package;";
314              
315             return $@ ? 0 : 1;
316             }
317              
318             1;
319              
320             __END__