File Coverage

blib/lib/Date/Holidays/NZ.pm
Criterion Covered Total %
statement 118 123 95.9
branch 51 64 79.6
condition 21 36 58.3
subroutine 17 17 100.0
pod 4 10 40.0
total 211 250 84.4


line stmt bran cond sub pod time code
1             package Date::Holidays::NZ;
2 1     1   22960 use strict;
  1         2  
  1         43  
3 1     1   6 use base qw(Exporter);
  1         1  
  1         193  
4              
5             our $SET;
6             BEGIN {
7 1     1   2 eval { require Set::Scalar };
  1         381  
8 1 50       17 if ($@) {
9 1         1239 require Set::Object;
10 1 50       13992 die "Need at least Set::Object 1.09"
11             if ($Set::Object::VERSION < 1.09);
12 1         33 $SET = "Set::Object";
13             } else {
14 0         0 $SET = "Set::Scalar";
15             }
16             }
17              
18 1     1   11 use vars qw($VERSION @EXPORT @EXPORT_OK);
  1         2  
  1         437  
19             $VERSION = '1.02';
20             @EXPORT = qw(is_nz_holiday nz_holidays nz_regional_day nz_holiday_date);
21             @EXPORT_OK = qw(%HOLIDAYS $NATIONAL_HOLIDAYS %regions
22             nz_region_code nz_region_name %holiday_cache);
23              
24             my $AD = "Anniversary Day";
25              
26             our %HOLIDAYS
27             = (
28             # holidays that everyone gets, unless they fall on a weekend,
29             # in which case you only get them "if you would normally have
30             # worked on that day (of the week)
31             "Waitangi Day" => "0206",
32             "ANZAC Day" => "0425",
33              
34             # holidays which are fixed, but unless you would "normally have
35             # worked on that day", then they are moved to the next working
36             # day
37             "New Years Day" => "0101+",
38             "Day after New Years Day" => '0102+',
39             "Christmas Day" => "1225+",
40             "Boxing Day" => '1226+',
41              
42             # other nationwide holidays
43             "Easter Monday" => "Easter + 1day",
44             "Good Friday" => "Easter - 2days",
45             "Queens Birthday" => '1st Monday in June',
46             "Labour Day" => "4th Monday in October",
47              
48             # Anniversary days - these are the official dates, but regional
49             # authorities and sometimes district councils pick the actual
50             # dates.
51             "Auckland $AD" => "Closest Monday to 0129",
52              
53             #"Taranaki $AD" => "Closest Monday to 0331",
54             # Moves to 2nd Monday in March to avoid Easter.
55             "Taranaki $AD" => "2nd Monday in March",
56              
57             #"Hawkes' Bay $AD" => "Closest Monday to 1101",
58             # Moved to Friday before Labour Day.
59             "Hawkes' Bay $AD" => "4th Monday in October - 3days",
60              
61             "Wellington $AD" => "Closest Monday to 0122",
62              
63             # "Marlborough $AD" => "Closest Monday to 1101",
64             # Observed 1st Monday after Labour Day.
65             "Marlborough $AD" => "4th Monday in October + 7days",
66              
67             "Nelson $AD" => "Closest Monday to 0201",
68              
69             #"Westland $AD" => "Closest Monday to 1201",
70             "Westland $AD" => "1st monday in december",
71              
72             # ``Varies throughout Westland, but Greymouth observes the
73             # official day.'' - that's the West Coast for you
74              
75             "Otago $AD" => "Closest Monday to 0323",
76             "Southland $AD" => "Closest Monday to 0117",
77             "Chatham Islands $AD" => "Closest Monday to 1130",
78              
79             # oddballs
80             #"Canterbury $AD" => "Closest Monday to 1216",
81             "Dominion Day" => "4th Monday in September",
82             "Christchurch Show Day" => "1st Tuesday in November + 10d",
83             );
84              
85             our $NATIONAL_HOLIDAYS =
86             $SET->new( "Waitangi Day",
87             "ANZAC Day",
88             "New Years Day",
89             "Day after New Years Day",
90             "Christmas Day",
91             "Boxing Day",
92             "Easter Monday",
93             "Good Friday",
94             "Queens Birthday",
95             "Labour Day",
96             );
97              
98             our %holiday_aliases =
99             ( "Birthday of the Reigning Sovereign" => "Queens Birthday",
100             );
101              
102             # These are Census 2001 region codes.
103             our %regions =
104             (
105             1 => "Northland",
106             2 => "Auckland",
107             3 => "Waikato",
108             4 => "Bay of Plenty",
109             5 => "Gisbourne",
110             6 => "Hawkes' Bay",
111             7 => "Taranaki",
112             8 => "Manuwatu-Wanganui",
113             9 => "Wellington",
114             12 => "West Coast",
115             13 => "Canterbury",
116             -13 => "Canterbury (South)", # tsk! naughty use of sign bit
117             14 => "Otago",
118             15 => "Southland",
119             16 => "Tasman",
120             17 => "Nelson",
121             18 => "Marlborough",
122             99 => "Outside Regional Authority (incl. Chatham Is)",
123             );
124             our %rev_regions;
125              
126             # which anniversary days are followed by each region is largely
127             # educated guesses, please e-mail samv@cpan.org if this list is
128             # incorrect.
129             our %FOLLOW = ( 1 => 2, 3 => 2, 4 => 5, 5 => 6,
130             8 => 9, 12 => "Westland",
131             13 => "Christchurch Show Day",
132             -13 => "Dominion Day",
133             16 => 17,
134             99 => "Chatham Islands",
135             );
136              
137 1     1   7 use Scalar::Util qw(looks_like_number);
  1         2  
  1         162  
138 1     1   7 use Carp qw(croak);
  1         2  
  1         1104  
139              
140             sub rev_regions {
141 36     36 0 66 my $region_name = shift;
142 36 50       164 unless ( $rev_regions{Auckland} ) {
143 36         184 %rev_regions = map { lc($_) } reverse %regions;
  1296         2595  
144             }
145 36   66     743 return $rev_regions{lc($region_name)}
146             || croak "`$region_name' is not a valid NZ region name";
147             }
148              
149             sub nz_region_name {
150 10     10 0 648 my $region = shift;
151 10 50       20 return undef unless defined $region;
152 10 100       28 if ( looks_like_number($region) ) {
153 6   66     133 return $regions{$region}
154             || croak "no such NZ region code `$region'";
155             } else {
156 4         7 return $regions{rev_regions($region)};
157             }
158             }
159              
160             sub nz_region_code {
161 34     34 0 702 my $region = shift;
162 34 50       77 return undef unless defined $region;
163 34 100       94 if ( looks_like_number($region) ) {
164 5 100       109 exists $regions{$region}
165             or croak "no such NZ region code `$region'";
166 4         10 return $region;
167             } else {
168 29         67 return rev_regions($region);
169             }
170             }
171              
172             # try to guess the regional day observed from the region
173             sub nz_regional_day {
174 93 50   93 1 2732 my $label = shift or return undef;
175              
176 93 100 100     936 if ( !looks_like_number($label) and
177             exists $HOLIDAYS{$label." $AD"} ) {
178 65         255 return "$label $AD";
179             }
180 28         60 my $region = nz_region_code($label);
181 28         55 my $followed;
182 28 100       77 if ( $followed = $FOLLOW{$region} ) {
183 27 100       80 if ( looks_like_number($followed) ) {
184 3         5 $followed = nz_region_name($followed);
185             }
186             }
187             else {
188 1         3 $followed = nz_region_name($region);
189             }
190 28 100       114 if ( $followed !~ /Day$/ ) {
191 12         35 $followed .= " $AD";
192             }
193 28         69 return $followed;
194             }
195              
196             sub check_falling_on {
197 32     32 0 66 my ($h, $year, $date) = @_;
198              
199 32         179 my $falls_on = UnixDate($year.$date, "%w");
200 32 100       18164 if ( $falls_on >= 6 ) {
201 7         24 my $name = delete $h->{$date};
202             #print STDERR "Blast, $name falls on a ".UnixDate($year.$date, "%A"). " ($falls_on)\n";
203 7         19 my $add = ($falls_on + 2) % 7;
204             #print STDERR "Adding $add days to it.\n";
205 7         40 my $to_fall_on = UnixDate(DateCalc($year.$date, "+${add}d"), "%m%d");
206             #print STDERR "Trying $to_fall_on instead.\n";
207 7   100     16199 while ( exists $h->{$to_fall_on} or
208             UnixDate($year.$to_fall_on, "%w") >= 6
209             ) {
210 3         532 $to_fall_on = UnixDate(DateCalc($year.$to_fall_on, "+1d"), "%m%d");
211             #print STDERR "That's no bloody good, trying $to_fall_on instead.\n";
212             }
213             #print STDERR "Settling on $to_fall_on ($name Holiday)\n";
214 7         10649 $h->{$to_fall_on} = "$name Holiday";
215             }
216             }
217              
218 1     1   1192 use Date::Manip qw(DateCalc ParseDate UnixDate ParseRecur);
  1         216012  
  1         1735  
219              
220             sub interpret_date {
221 178     178 0 12564 my $year = shift;
222 178         269 my $value = shift;
223              
224 178         254 my ($date, $add);
225 178 100       1525 if ( $value =~ m/^(\d\d)(\d\d)\+?$/ ) {
    100          
    100          
    50          
226 49         163 return $value;
227             }
228             elsif ( $value =~ m/^(\d+(?:st|nd|rd|th) \w+day in \w+)(.*)$/i) {
229 60         237 (my $spec, $add) = ($1, $2);
230 60         280 $date = ParseDate($spec. " $year");
231             }
232             elsif ( $value =~ m/^Closest (\w+day) to (\d\d)(\d\d)(.*)$/i) {
233             #print STDERR "**** $value($year):\n";
234 49         223 (my ($day, $month, $dom), $add) = ($1, $2, $3, $4);
235 49         279 $date = ParseDate("$year-$month-$dom");
236 49         29015 my $wanted = UnixDate($day, "%w");
237 49         64342 my $got = UnixDate($date, "%w");
238             #print STDERR " $year-$month-$dom is a ".UnixDate($date, "%A")
239             #." ($got)\n";
240 49 100       19505 if ( my $diff = ($wanted - $got + 7) % 7 ) {
241             #print STDERR " difference is $diff days\n";
242 43 100       117 if ( $diff < 4 ) {
243             #print STDERR "Adding $diff days\n";
244 20         113 $date = DateCalc($date, "+${diff}d");
245             } else {
246 23         56 $diff = 7 - $diff;
247             #print STDERR "Subtracting $diff days\n";
248 23         138 $date = DateCalc($date, "-${diff}d");
249             }
250             } else {
251             #print STDERR "Which is good\n";
252             }
253             }
254             elsif ( $value =~ m/^Easter(.*)$/i) {
255 20         114 ($date) = ParseRecur("*$year:0:0:0:0:0:0*EASTER");
256 20         40687 $add = $1;
257             }
258 129 100       155182 $date = DateCalc($date, "$add") if $add;
259 129         189038 return UnixDate($date, "%m%d");
260             }
261              
262             sub nz_stat_holidays {
263 8     8 0 15 my $year = shift;
264              
265             # build the relative dates
266 8         16 my (%h, @tentative);
267 8         95 foreach my $holiday ($NATIONAL_HOLIDAYS->members) {
268              
269 80 50 50     448 my $when = interpret_date($year, $HOLIDAYS{$holiday}||die)
270             or die "couldn't interpret $year, $HOLIDAYS{$holiday}";
271              
272 80 100       16216 if ( $when =~ s/\+// ) {
273 32         57 push @tentative, $when;
274             }
275 80         251 $h{$when} = $holiday;
276             }
277 8         64 for my $date ( sort { $a <=> $b } @tentative ) {
  32         74  
278 32         104 check_falling_on(\%h, $year, $date);
279             }
280              
281 8         72 return \%h;
282             }
283              
284             our %regional_holiday_cache
285             = # exceptions
286             ( "2004/Westland $AD" => "1129",
287             "2008/Hawkes' Bay $AD" => "1017",
288             "2008/Otago $AD" => "0325",
289             );
290              
291             our %holiday_cache;
292              
293             sub nz_holidays {
294 80     80 1 23442 my ($year, $region) = @_;
295              
296 80   66     313 my $hols = $holiday_cache{$year} ||= nz_stat_holidays($year);
297              
298 80 50       335 if ( $region ) {
299 0         0 my $rd = nz_regional_day($region);
300              
301 0   0     0 my $when = $regional_holiday_cache{"$year/$rd"} ||=
      0        
302             (interpret_date($year, $HOLIDAYS{$rd}||die)
303             or die "couldn't interpret $year, $HOLIDAYS{$rd}");
304              
305 0         0 $hols = { %$hols, $when => "$rd" };
306             }
307              
308 80         209 return $hols;
309             }
310              
311             sub is_nz_holiday {
312 168     168 1 135100 my ($year, $month, $day, $region) = @_;
313 168         763 my $mmdd = sprintf("%.2d%.2d", $month, $day);
314              
315 168   33     605 my $hols = $holiday_cache{$year} ||= nz_stat_holidays($year);
316              
317 168 50       492 if ( exists $hols->{$mmdd} ) {
318 0         0 return $hols->{$mmdd};
319             }
320              
321 168 100       386 if ( $region ) {
322 84         300 my $rd = nz_regional_day($region);
323              
324 84   50     731 my $when = $regional_holiday_cache{"$year/$rd"} ||=
      66        
325             (interpret_date($year, $HOLIDAYS{$rd}||die)
326             or die "couldn't interpret $year, $HOLIDAYS{$rd}");
327              
328 84 50       34107 if ( $when eq $mmdd ) {
329 84         370 return $rd;
330             }
331             }
332              
333 84         258 return undef;
334             }
335              
336             sub nz_holiday_date {
337 6     6 1 1097 my ($year, $holname) = @_;
338 6 100       27 $holname = $holiday_aliases{$holname} if $holiday_aliases{$holname};
339 6 50       20 exists $HOLIDAYS{$holname} or croak "no such holiday $holname";
340              
341 6 100       35 if ( $NATIONAL_HOLIDAYS->has($holname) ) {
342 3         31 my $date = interpret_date($year, $HOLIDAYS{$holname});
343 3         717 my $hols = nz_holidays($year);
344              
345 3 100       18 if ( $date =~ s/\+$// ) {
346 1   66     8 while ( not exists $hols->{$date} or
347             $hols->{$date} !~ m/^\Q$holname\E/i
348             ) {
349 2 50       2587 die "couldn't find it!" if $date eq "1231";
350 2         11 $date = UnixDate(DateCalc($year.$date, "+1d"), "%m%d");
351             }
352             }
353              
354 3         2401 return $date;
355             } else {
356 3   66     41 return $regional_holiday_cache{"$year/$holname"}
357             || interpret_date($year, $HOLIDAYS{$holname});
358             }
359             }
360              
361             1;
362              
363             __END__
364              
365             =encoding utf-8
366              
367             =head1 NAME
368              
369             Date::Holidays::NZ - Determine New Zealand public holidays
370              
371             =head1 SYNOPSIS
372              
373             use Date::Holidays::NZ;
374             my ($year, $month, $day) = (localtime)[ 5, 4, 3 ];
375             $year += 1900;
376             $month += 1;
377             print "Woohoo" if is_nz_holiday( $year, $month, $day );
378              
379             # supply Census 2001 region codes (1-19), or names like
380             # "Wellington", "Canterbury (South)", etc
381             print "Yes!" if is_nz_holiday( $year, $month, $day, $region );
382              
383             my $h = nz_holidays($year);
384             printf "Dec. 25th is named '%s'\n", $h->{'1225'};
385              
386             =head1 DESCRIPTION
387              
388             Determines whether a given date is a New Zealand public holiday or
389             not.
390              
391             As described at
392             L<http://www.ers.dol.govt.nz/holidays_act_2003/dates/>, the system of
393             determining holidays in New Zealand is a complicated matter. Not only
394             do you need to know what region the country is living in to figure out
395             the relevant Anniversary Day, but sometimes the district too (for the
396             West Coast and Canterbury). As regions are free to pick the actual
397             days observed for particular holidays, this module cannot guarantee
398             dates past the last time it was checked against the Employment
399             Relations Service web site (currently returns known good values for
400             statutory holidays 2003 - 2009 and regional holidays for 2005).
401              
402             This module hopes to return values that are Mostly Right(tm) when
403             passed in census 2001 region codes (widely used throughout government
404             and industry), and can also be passed in textual region labels, which
405             includes the appropriate Anniversary Day.
406              
407             Also, there is a difference between what is considered a holiday by
408             the Holidays Act 2003 - and hence entitling a person to time in lieu
409             and/or extra pay - and what is considered to be a Bank Holiday. This
410             module returns Bank Holiday dates, so if Christmas Day falls on a
411             Sunday, the 26th will be called "Boxing Day", and the 27th "Christmas
412             Day Holiday".
413              
414              
415             =head1 Functions
416              
417             =over 4
418              
419             =item is_nz_holiday($year, $month, $date, [$region])
420              
421             Returns the name of the Holiday that falls on the given day, or undef
422             if there is none.
423              
424             Optionally, a region may be specified, which also checks the
425             anniversary day applicable to that region.
426              
427             =item nz_holidays($year, [$region])
428              
429             Returns a hashref of all defined holidays in the year. Keys in the
430             hashref are in 'mmdd' format, the values are the names of the
431             holidays.
432              
433             As per the previous function, a region name may be specified. If you
434             do not specify a region, then no regional holidays are included in the
435             returned hash.
436              
437             =item nz_regional_day($region)
438              
439             Returns the name of the regional holiday for the specified region.
440              
441             This can be passed into the next function to find the actual date for
442             a given year.
443              
444             Valid regions are:
445              
446             Number Region Name
447             1 Northland
448             1 Northland
449             2 Auckland
450             3 Waikato
451             4 Bay of Plenty
452             5 Gisbourne
453             6 Hawkes' Bay
454             7 Taranaki
455             8 Manuwatu-Wanganui
456             9 Wellington
457             12 West Coast
458             13 Canterbury
459             -13 Canterbury (South)
460             14 Otago
461             15 Southland
462             16 Tasman
463             17 Nelson
464             18 Marlborough
465             99 Outside Regional Authority
466              
467             Note: for the purposes of calculating a holiday, 99 is considered to
468             be Chatham Islands, as there is no Regional Authority there.
469              
470             Sorry about the -13 for South Canterbury. That's a bit of a hack.
471             Better ideas welcome.
472              
473             =item nz_holiday_date($year, $holiday)
474              
475             Return the actual day that a given holiday falls on for a particular
476             year.
477              
478             Valid holiday names are:
479              
480             ANZAC Day
481             Boxing Day
482             Christmas Day
483             Day after New Years Day
484             Dominion Day
485             Easter Monday
486             Good Friday
487             Labour Day
488             New Years Day
489             Queens Birthday
490             Waitangi Day
491            
492             Auckland Anniversary Day
493             Chatham Islands Anniversary Day
494             Christchurch Show Day
495             Hawkes' Bay Anniversary Day
496             Marlborough Anniversary Day
497             Nelson Anniversary Day
498             Otago Anniversary Day
499             Southland Anniversary Day
500             Taranaki Anniversary Day
501             Wellington Anniversary Day
502             Westland Anniversary Day
503              
504             Somebody let me know if any of those are incorrect.
505              
506             C<Date::Holidays::NZ> version 1.00 and later also supports referring
507             to Queen's Birthday as "Birthday of the Reigning Sovereign". Not that
508             it has anything to do with the Queen's Birthday, really - it's just
509             another day off to stop us British subjects getting all restless,
510             revolutionary and whatnot.
511              
512             =back
513              
514             =head1 ERRATA
515              
516             Otago Anniversary Day is due to fall on Easter Monday in 2035 and 2046.
517             When this happened in 2008, the council made Easter Tuesday the
518             anniversary day; however this is not currently codified as a general
519             rule, as it is up to the council to declare this in advance. This was
520             only fixed in Date::Holidays::NZ 1.02, which was released very close
521             to the actual anniversary day - apologies for the delay in the update.
522             For those days (in 2035 and beyond), depending on which method you
523             call you might get a different answer as to why that day is a holiday
524             for that region.
525              
526             Also in 1.02 was a fix which affected functions which would return
527             the "normal" day for a holiday, rather than the day listed on the
528             official government site, for a couple of regional days which did not
529             match the general rule for when they were due.
530              
531             Note that district councils are free to alter the holidays schedule at
532             any time. Also, strictly speaking, it is the Pope that decides the
533             date of Easter, upon which Easter Friday and Monday are based.
534              
535             I'm not entirely sure on which Anniversary Day the following NZ
536             regions observe, so if it matters for you, please check that it is
537             correct and let me know if I need to fix anything:
538              
539             =over
540              
541             =item *
542              
543             Waikato (assumed Auckland)
544              
545             =item *
546              
547             BOP (assumed Auckland)
548              
549             =item *
550              
551             Gisbourne (assumed Hawkes' Bay)
552              
553             =item *
554              
555             Tasman (assumed Nelson)
556              
557             =item *
558              
559             Area 99 - that is, areas outside regional authority. The biggest one
560             of these is the Chatham Islands, so this module assumes Region 99 is
561             the Chatham Islands.
562              
563             =back
564              
565             Maybe someone can shed some light on the situation in the West Coast,
566             although this is confounded by the matter that the whole concept of
567             holidays or even time there is only a loosely observed phenomenon.
568              
569             =head1 EXPORTS
570              
571             Exports the four listed functions in this manual page by default.
572              
573             You may also import various internal variables and methods used by
574             this module if you like. Log a ticket if you want any of them to be
575             added to the documentation.
576              
577             =head1 BUGS
578              
579             This module does not support Te Reo Māori. If you would be interested
580             in translating the holiday names, region names or manual page to
581             Māori, please contact the author.
582              
583             Please report issues via CPAN RT:
584              
585             http://rt.cpan.org/NoAuth/Bugs.html?Dist=Date-Holidays-NZ
586              
587             or by sending mail to
588              
589             bug-Date-Holidays-NZ@rt.cpan.org
590              
591             =head1 AUTHORS
592              
593             Modified for NZ holidays by Sam Vilain <samv@cpan.org>, from
594             Date::Holidays::DK, by Lars Thegler <lars@thegler.dk>
595              
596             =head1 COPYRIGHT
597              
598             portions:
599              
600             Copyright (c) 2004 Lars Thegler. All rights reserved.
601              
602             some modifications
603              
604             Copyright (c) 2005, 2008, Sam Vilain. All rights reserved.
605              
606             This program is free software; you can redistribute it and/or modify
607             it under the same terms as Perl itself.
608              
609             =cut
610