1#!/usr/bin/perl
2use strict;
3use warnings;
4
5use Time::Moment;
6use Time::Moment::Adjusters qw[ NthDayOfWeekInMonth ];
7
8use enum qw[ Monday=1 Tuesday Wednesday Thursday Friday Saturday Sunday ];
9use enum qw[ First=1 Second Third Fourth Last=-1 ];
10
11use constant FirstMondayInMonth    => NthDayOfWeekInMonth(First, Monday);
12use constant SecondMondayInMonth   => NthDayOfWeekInMonth(Second, Monday);
13use constant ThirdMondayInMonth    => NthDayOfWeekInMonth(Third, Monday);
14use constant LastMondayInMonth     => NthDayOfWeekInMonth(Last, Monday);
15use constant FourthThursdayInMonth => NthDayOfWeekInMonth(Fourth, Thursday);
16
17# Adjusts the date to the nearest workday
18use constant NearestWorkday => sub {
19    my ($tm) = @_;
20    return $tm unless $tm->day_of_week > Friday;
21    return $tm->plus_days($tm->day_of_week == Saturday ? -1 : +1);
22};
23
24# Federal law 5 USC § 6103 - HOLIDAYS
25# http://www.law.cornell.edu/uscode/text/5/6103
26sub compute_us_federal_holidays {
27    @_ == 1 or @_ == 2 or die q<Usage: compute_us_federal_holidays(year [, inauguration = false])>;
28    my ($year, $inauguration) = @_;
29
30    my @dates;
31    my $tm = Time::Moment->new(year => $year);
32
33    # New Year’s Day, January 1.
34    push @dates, $tm->with_month(1)
35                    ->with_day_of_month(1)
36                    ->with(NearestWorkday);
37
38    # Birthday of Martin Luther King, Jr., the third Monday in January.
39    push @dates, $tm->with_month(1)
40                    ->with(ThirdMondayInMonth);
41
42    # Inauguration Day, January 20 of each fourth year after 1965.
43    if ($inauguration && $year % 4 == 1) {
44        my $date = $tm->with_month(1)
45                      ->with_day_of_month(20);
46
47        # When January 20 falls on Sunday, the next succeeding day is selected.
48        $date = $date->plus_days(1)
49          if $date->day_of_week == Sunday;
50
51        push @dates, $date
52          unless $date->day_of_week == Saturday
53              or $date->is_equal($dates[-1]);    # 1997, 2013, 2025 ...
54    }
55
56    # Washington’s Birthday, the third Monday in February.
57    push @dates, $tm->with_month(2)
58                    ->with(ThirdMondayInMonth);
59
60    # Memorial Day, the last Monday in May.
61    push @dates, $tm->with_month(5)
62                    ->with(LastMondayInMonth);
63
64    # Independence Day, July 4.
65    push @dates, $tm->with_month(7)
66                    ->with_day_of_month(4)
67                    ->with(NearestWorkday);
68
69    # Labor Day, the first Monday in September.
70    push @dates, $tm->with_month(9)
71                    ->with(FirstMondayInMonth);
72
73    # Columbus Day, the second Monday in October.
74    push @dates, $tm->with_month(10)
75                    ->with(SecondMondayInMonth);
76
77    # Veterans Day, November 11.
78    push @dates, $tm->with_month(11)
79                    ->with_day_of_month(11)
80                    ->with(NearestWorkday);
81
82    # Thanksgiving Day, the fourth Thursday in November.
83    push @dates, $tm->with_month(11)
84                    ->with(FourthThursdayInMonth);
85
86    # Christmas Day, December 25.
87    push @dates, $tm->with_month(12)
88                    ->with_day_of_month(25)
89                    ->with(NearestWorkday);
90
91    return @dates;
92}
93
94# Test cases extracted from <http://www.opm.gov/Operating_Status_Schedules/fedhol/Index.asp>
95my @tests = (
96    [ 1997, '1997-01-01', '1997-01-20', '1997-02-17', '1997-05-26', '1997-07-04',
97            '1997-09-01', '1997-10-13', '1997-11-11', '1997-11-27', '1997-12-25' ],
98    [ 1998, '1998-01-01', '1998-01-19', '1998-02-16', '1998-05-25', '1998-07-03',
99            '1998-09-07', '1998-10-12', '1998-11-11', '1998-11-26', '1998-12-25' ],
100    [ 1999, '1999-01-01', '1999-01-18', '1999-02-15', '1999-05-31', '1999-07-05',
101            '1999-09-06', '1999-10-11', '1999-11-11', '1999-11-25', '1999-12-24' ],
102    [ 2000, '1999-12-31', '2000-01-17', '2000-02-21', '2000-05-29', '2000-07-04',
103            '2000-09-04', '2000-10-09', '2000-11-10', '2000-11-23', '2000-12-25' ],
104    [ 2001, '2001-01-01', '2001-01-15', '2001-02-19', '2001-05-28', '2001-07-04',
105            '2001-09-03', '2001-10-08', '2001-11-12', '2001-11-22', '2001-12-25' ],
106    [ 2002, '2002-01-01', '2002-01-21', '2002-02-18', '2002-05-27', '2002-07-04',
107            '2002-09-02', '2002-10-14', '2002-11-11', '2002-11-28', '2002-12-25' ],
108    [ 2003, '2003-01-01', '2003-01-20', '2003-02-17', '2003-05-26', '2003-07-04',
109            '2003-09-01', '2003-10-13', '2003-11-11', '2003-11-27', '2003-12-25' ],
110    [ 2004, '2004-01-01', '2004-01-19', '2004-02-16', '2004-05-31', '2004-07-05',
111            '2004-09-06', '2004-10-11', '2004-11-11', '2004-11-25', '2004-12-24' ],
112    [ 2005, '2004-12-31', '2005-01-17', '2005-02-21', '2005-05-30', '2005-07-04',
113            '2005-09-05', '2005-10-10', '2005-11-11', '2005-11-24', '2005-12-26' ],
114    [ 2006, '2006-01-02', '2006-01-16', '2006-02-20', '2006-05-29', '2006-07-04',
115            '2006-09-04', '2006-10-09', '2006-11-10', '2006-11-23', '2006-12-25' ],
116    [ 2007, '2007-01-01', '2007-01-15', '2007-02-19', '2007-05-28', '2007-07-04',
117            '2007-09-03', '2007-10-08', '2007-11-12', '2007-11-22', '2007-12-25' ],
118    [ 2008, '2008-01-01', '2008-01-21', '2008-02-18', '2008-05-26', '2008-07-04',
119            '2008-09-01', '2008-10-13', '2008-11-11', '2008-11-27', '2008-12-25' ],
120    [ 2009, '2009-01-01', '2009-01-19', '2009-02-16', '2009-05-25', '2009-07-03',
121            '2009-09-07', '2009-10-12', '2009-11-11', '2009-11-26', '2009-12-25' ],
122    [ 2010, '2010-01-01', '2010-01-18', '2010-02-15', '2010-05-31', '2010-07-05',
123            '2010-09-06', '2010-10-11', '2010-11-11', '2010-11-25', '2010-12-24' ],
124    [ 2011, '2010-12-31', '2011-01-17', '2011-02-21', '2011-05-30', '2011-07-04',
125            '2011-09-05', '2011-10-10', '2011-11-11', '2011-11-24', '2011-12-26' ],
126    [ 2012, '2012-01-02', '2012-01-16', '2012-02-20', '2012-05-28', '2012-07-04',
127            '2012-09-03', '2012-10-08', '2012-11-12', '2012-11-22', '2012-12-25' ],
128    [ 2013, '2013-01-01', '2013-01-21', '2013-02-18', '2013-05-27', '2013-07-04',
129            '2013-09-02', '2013-10-14', '2013-11-11', '2013-11-28', '2013-12-25' ],
130    [ 2014, '2014-01-01', '2014-01-20', '2014-02-17', '2014-05-26', '2014-07-04',
131            '2014-09-01', '2014-10-13', '2014-11-11', '2014-11-27', '2014-12-25' ],
132    [ 2015, '2015-01-01', '2015-01-19', '2015-02-16', '2015-05-25', '2015-07-03',
133            '2015-09-07', '2015-10-12', '2015-11-11', '2015-11-26', '2015-12-25' ],
134    [ 2016, '2016-01-01', '2016-01-18', '2016-02-15', '2016-05-30', '2016-07-04',
135            '2016-09-05', '2016-10-10', '2016-11-11', '2016-11-24', '2016-12-26' ],
136    [ 2017, '2017-01-02', '2017-01-16', '2017-02-20', '2017-05-29', '2017-07-04',
137            '2017-09-04', '2017-10-09', '2017-11-10', '2017-11-23', '2017-12-25' ],
138    [ 2018, '2018-01-01', '2018-01-15', '2018-02-19', '2018-05-28', '2018-07-04',
139            '2018-09-03', '2018-10-08', '2018-11-12', '2018-11-22', '2018-12-25' ],
140    [ 2019, '2019-01-01', '2019-01-21', '2019-02-18', '2019-05-27', '2019-07-04',
141            '2019-09-02', '2019-10-14', '2019-11-11', '2019-11-28', '2019-12-25' ],
142    [ 2020, '2020-01-01', '2020-01-20', '2020-02-17', '2020-05-25', '2020-07-03',
143            '2020-09-07', '2020-10-12', '2020-11-11', '2020-11-26', '2020-12-25' ],
144);
145
146use Test::More 0.88;
147
148foreach my $test (@tests) {
149    my ($year, @exp) = @$test;
150    my @got = map {
151        $_->strftime('%Y-%m-%d')
152    } compute_us_federal_holidays($year);
153    is_deeply([@got], [@exp], "U.S. federal holidays for year $year");
154}
155
156done_testing();
157