Calculating U.S Federal holidays with Time::Moment


Time::Moment 0.27 introduces the concept of $tm->with($adjuster). The adjuster is a CODE reference which is invoked with an instance of Time::Moment and is expected to return the same.

Time::Moment comes with Time::Moment::Adjusters which currently only provides routines for navigating/finding the day of week.

The following is also available on CPAN, as us_federal_holidays.pl or on github.


#!/usr/bin/perl
use strict;
use warnings;

use Time::Moment;
use Time::Moment::Adjusters qw[ NthDayOfWeekInMonth ];

use enum qw[ Monday=1 Tuesday Wednesday Thursday Friday Saturday Sunday ];
use enum qw[ First=1 Second Third Fourth Last=-1 ];

use constant FirstMondayInMonth => NthDayOfWeekInMonth(First, Monday);
use constant SecondMondayInMonth => NthDayOfWeekInMonth(Second, Monday);
use constant ThirdMondayInMonth => NthDayOfWeekInMonth(Third, Monday);
use constant LastMondayInMonth => NthDayOfWeekInMonth(Last, Monday);
use constant FourthThursdayInMonth => NthDayOfWeekInMonth(Fourth, Thursday);

# Adjusts the date to the nearest workday
use constant NearestWorkday => sub {
my ($tm) = @_;
return $tm unless $tm->day_of_week > Friday;
return $tm->plus_days($tm->day_of_week == Saturday ? -1 : +1);
};

# Federal law 5 USC § 6103 - HOLIDAYS
# http://www.law.cornell.edu/uscode/text/5/6103
sub compute_us_federal_holidays {
@_ == 1 or @_ == 2 or die q;
my ($year, $inauguration) = @_;

my @dates;
my $tm = Time::Moment->new(year => $year);

# New Year's Day, January 1.
push @dates, $tm->with_month(1)
->with_day_of_month(1)
->with(NearestWorkday);

# Birthday of Martin Luther King, Jr., the third Monday in January.
push @dates, $tm->with_month(1)
->with(ThirdMondayInMonth);

# Inauguration Day, January 20 of each fourth year after 1965.
if ($inauguration && $year % 4 == 1) {
my $date = $tm->with_month(1)
->with_day_of_month(20);

# When January 20 falls on Sunday, the next succeeding day is selected.
$date = $date->plus_days(1)
if $date->day_of_week == Sunday;

push @dates, $date
unless $date->day_of_week == Saturday
or $date->is_equal($dates[-1]); # 1997, 2013, 2025 ...
}

# Washington's Birthday, the third Monday in February.
push @dates, $tm->with_month(2)
->with(ThirdMondayInMonth);

# Memorial Day, the last Monday in May.
push @dates, $tm->with_month(5)
->with(LastMondayInMonth);

# Independence Day, July 4.
push @dates, $tm->with_month(7)
->with_day_of_month(4)
->with(NearestWorkday);

# Labor Day, the first Monday in September.
push @dates, $tm->with_month(9)
->with(FirstMondayInMonth);

# Columbus Day, the second Monday in October.
push @dates, $tm->with_month(10)
->with(SecondMondayInMonth);

# Veterans Day, November 11.
push @dates, $tm->with_month(11)
->with_day_of_month(11)
->with(NearestWorkday);

# Thanksgiving Day, the fourth Thursday in November.
push @dates, $tm->with_month(11)
->with(FourthThursdayInMonth);

# Christmas Day, December 25.
push @dates, $tm->with_month(12)
->with_day_of_month(25)
->with(NearestWorkday);

return @dates;
}

# Test cases extracted from
my @tests = (
[ 1997, '1997-01-01', '1997-01-20', '1997-02-17', '1997-05-26', '1997-07-04',
'1997-09-01', '1997-10-13', '1997-11-11', '1997-11-27', '1997-12-25' ],
[ 1998, '1998-01-01', '1998-01-19', '1998-02-16', '1998-05-25', '1998-07-03',
'1998-09-07', '1998-10-12', '1998-11-11', '1998-11-26', '1998-12-25' ],
[ 1999, '1999-01-01', '1999-01-18', '1999-02-15', '1999-05-31', '1999-07-05',
'1999-09-06', '1999-10-11', '1999-11-11', '1999-11-25', '1999-12-24' ],
[ 2000, '1999-12-31', '2000-01-17', '2000-02-21', '2000-05-29', '2000-07-04',
'2000-09-04', '2000-10-09', '2000-11-10', '2000-11-23', '2000-12-25' ],
[ 2001, '2001-01-01', '2001-01-15', '2001-02-19', '2001-05-28', '2001-07-04',
'2001-09-03', '2001-10-08', '2001-11-12', '2001-11-22', '2001-12-25' ],
[ 2002, '2002-01-01', '2002-01-21', '2002-02-18', '2002-05-27', '2002-07-04',
'2002-09-02', '2002-10-14', '2002-11-11', '2002-11-28', '2002-12-25' ],
[ 2003, '2003-01-01', '2003-01-20', '2003-02-17', '2003-05-26', '2003-07-04',
'2003-09-01', '2003-10-13', '2003-11-11', '2003-11-27', '2003-12-25' ],
[ 2004, '2004-01-01', '2004-01-19', '2004-02-16', '2004-05-31', '2004-07-05',
'2004-09-06', '2004-10-11', '2004-11-11', '2004-11-25', '2004-12-24' ],
[ 2005, '2004-12-31', '2005-01-17', '2005-02-21', '2005-05-30', '2005-07-04',
'2005-09-05', '2005-10-10', '2005-11-11', '2005-11-24', '2005-12-26' ],
[ 2006, '2006-01-02', '2006-01-16', '2006-02-20', '2006-05-29', '2006-07-04',
'2006-09-04', '2006-10-09', '2006-11-10', '2006-11-23', '2006-12-25' ],
[ 2007, '2007-01-01', '2007-01-15', '2007-02-19', '2007-05-28', '2007-07-04',
'2007-09-03', '2007-10-08', '2007-11-12', '2007-11-22', '2007-12-25' ],
[ 2008, '2008-01-01', '2008-01-21', '2008-02-18', '2008-05-26', '2008-07-04',
'2008-09-01', '2008-10-13', '2008-11-11', '2008-11-27', '2008-12-25' ],
[ 2009, '2009-01-01', '2009-01-19', '2009-02-16', '2009-05-25', '2009-07-03',
'2009-09-07', '2009-10-12', '2009-11-11', '2009-11-26', '2009-12-25' ],
[ 2010, '2010-01-01', '2010-01-18', '2010-02-15', '2010-05-31', '2010-07-05',
'2010-09-06', '2010-10-11', '2010-11-11', '2010-11-25', '2010-12-24' ],
[ 2011, '2010-12-31', '2011-01-17', '2011-02-21', '2011-05-30', '2011-07-04',
'2011-09-05', '2011-10-10', '2011-11-11', '2011-11-24', '2011-12-26' ],
[ 2012, '2012-01-02', '2012-01-16', '2012-02-20', '2012-05-28', '2012-07-04',
'2012-09-03', '2012-10-08', '2012-11-12', '2012-11-22', '2012-12-25' ],
[ 2013, '2013-01-01', '2013-01-21', '2013-02-18', '2013-05-27', '2013-07-04',
'2013-09-02', '2013-10-14', '2013-11-11', '2013-11-28', '2013-12-25' ],
[ 2014, '2014-01-01', '2014-01-20', '2014-02-17', '2014-05-26', '2014-07-04',
'2014-09-01', '2014-10-13', '2014-11-11', '2014-11-27', '2014-12-25' ],
[ 2015, '2015-01-01', '2015-01-19', '2015-02-16', '2015-05-25', '2015-07-03',
'2015-09-07', '2015-10-12', '2015-11-11', '2015-11-26', '2015-12-25' ],
[ 2016, '2016-01-01', '2016-01-18', '2016-02-15', '2016-05-30', '2016-07-04',
'2016-09-05', '2016-10-10', '2016-11-11', '2016-11-24', '2016-12-26' ],
[ 2017, '2017-01-02', '2017-01-16', '2017-02-20', '2017-05-29', '2017-07-04',
'2017-09-04', '2017-10-09', '2017-11-10', '2017-11-23', '2017-12-25' ],
[ 2018, '2018-01-01', '2018-01-15', '2018-02-19', '2018-05-28', '2018-07-04',
'2018-09-03', '2018-10-08', '2018-11-12', '2018-11-22', '2018-12-25' ],
[ 2019, '2019-01-01', '2019-01-21', '2019-02-18', '2019-05-27', '2019-07-04',
'2019-09-02', '2019-10-14', '2019-11-11', '2019-11-28', '2019-12-25' ],
[ 2020, '2020-01-01', '2020-01-20', '2020-02-17', '2020-05-25', '2020-07-03',
'2020-09-07', '2020-10-12', '2020-11-11', '2020-11-26', '2020-12-25' ],
);

use Test::More 0.88;

foreach my $test (@tests) {
my ($year, @exp) = @$test;
my @got = map {
$_->strftime('%Y-%m-%d')
} compute_us_federal_holidays($year);
is_deeply([@got], [@exp], "U.S. federal holidays for year $year");
}

done_testing();


2 Comments

This is awesome! There is also Bank::Holidays on the CPAN, which is limited to the five years the Federal Reserve posts on their site. Full disclosure: I am a contributor to that module.

Leave a comment

About Christian Hansen

user-pic I blog about Perl.