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