Skip to content

Instantly share code, notes, and snippets.

@bdkosher
Created May 1, 2020 18:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdkosher/330738040879664e6482ee5201dc9fd4 to your computer and use it in GitHub Desktop.
Save bdkosher/330738040879664e6482ee5201dc9fd4 to your computer and use it in GitHub Desktop.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.MonthDay;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.time.DayOfWeek.*;
import static java.time.Month.*;
import static java.util.Objects.requireNonNull;
/**
* Represents the public holidays as declared in Federal law (5 U.S.C. 6103). All dates listed in section (a) are
* enumerated. Inauguration day, which is described in section (c) is not enumerated, but is accounted for in the
* static {@link #observancesForYear} and {@link #isObserved(LocalDate)} methods.
* <p>
* Fixed holidays that fall on specific days each year (e.g. Christmas) are observed on the nearest work day. If the
* actual holiday is on a Saturday, the observance occurs on the Friday before; if the holiday is on a Sunday, the
* observance occurs on the Monday after. The dates used for such fixed holidays will be the observed dates and not
* the actual ones.
* <p>
* This method does not account for historical changes in Federal law regarding Holidays and should not be used to
* determine holidays in the past, such as Martin Luther King Jr.'s birthday for years prior to 1983, when the
* observance was formally adopted.
*
* @see <a href="https://www.opm.gov/policy-data-oversight/snow-dismissal-procedures/federal-holidays/#url=Overview">
* Federal Holidays - OPM Web site</a>
* @see <a href="https://www.law.cornell.edu/uscode/text/5/6103">5 U.S. Code § 6103 - Holidays</a>
*/
public enum FederalHoliday {
NEW_YEARS("New Year's Day", year -> LocalDate.of(year, JANUARY, 1)),
MLK_JR_BIRTHDAY("Birthday of Martin Luther King, Jr.", year -> nthDayOfWeek(3, MONDAY, JANUARY, year)),
WASHINGTON_BIRTHDAY("Washington's Birthday", year -> nthDayOfWeek(3, MONDAY, FEBRUARY, year)),
MEMORIAL_DAY("Memorial Day", year -> nthDayOfWeek(-1, MONDAY, MAY, year)),
INDEPENDENCE_DAY("Independence Day", year -> LocalDate.of(year, JULY, 4)),
LABOR_DAY("Labor Day", year -> nthDayOfWeek(1, MONDAY, SEPTEMBER, year)),
COLUMBUS_DAY("Columbus Day", year -> nthDayOfWeek(2, MONDAY, OCTOBER, year)),
VETERANS_DAY("Veterans Day", year -> LocalDate.of(year, NOVEMBER, 11)),
THANKSGIVING("Thanksgiving Day", year -> nthDayOfWeek(4, THURSDAY, NOVEMBER, year)),
CHRISTMAS("Christmas Day", year -> LocalDate.of(year, DECEMBER, 25));
private static final MonthDay INAUGURATION_DATE = MonthDay.of(JANUARY, 20);
private final String legalName;
private final Function<Integer, LocalDate> onDate;
FederalHoliday(String legalName, Function<Integer, LocalDate> onDate) {
this.legalName = legalName;
this.onDate = onDate;
}
/**
* Returns the official, legal name of the Holiday as designated by the Federal government.
*
* @return the Holiday's legal name
*/
public String getLegalName() {
return legalName;
}
/**
* Returns the observed date of this Holiday for the given year. Fixed date holidays are adjusted so that they fall
* on a working date. If the fixed date falls on Saturday, it is moved to the preceding Friday; if Sunday, it is
* moved to the following Monday.
* <p>
* Note: if New Year's Day falls on a Saturday on the year provided, the year of returned date will be December 31st
* of the previous year. This most recently happened in 2011.
*
* @param year the calendar year
* @return the date of this holiday observance for that year
*/
public LocalDate forYear(int year) {
return adjustForWeekends(onDate.apply(year));
}
/**
* Returns the date that falls on the nth day of the week for the given year. For example, the fourth Friday
* in the month of October, 2018 is October 26th, 2018.
*/
private static LocalDate nthDayOfWeek(int n, DayOfWeek dayOfWeek, Month month, int year) {
return LocalDate.of(year, month, 1).with(TemporalAdjusters.dayOfWeekInMonth(n, dayOfWeek));
}
/**
* Adjusts the date, if necessary, so that it does not fall on a weekend. If the date is on a Saturday, returns
* the previous Friday; if it's on a Sunday, returns the following Monday.
*/
private static LocalDate adjustForWeekends(LocalDate date) {
switch (date.getDayOfWeek()) {
case SATURDAY:
return date.minusDays(1);
case SUNDAY:
return date.plusDays(1);
default:
return date;
}
}
/**
* For the purposes of this class it is assumed 1965 itself is an inauguration year.
*/
private static boolean isInaugurationYear(int year) {
return (year - 1965) % 4 == 0;
}
/**
* According to Federal law (5 U.S.C. 6103):
* <p>
* "January 20 of each fourth year after 1965, Inauguration Day, is a legal public holiday for the purpose of
* statutes relating to pay and leave of employees...employed by the government of the District of Columbia employed
* in the District of Columbia, Montgomery and Prince Georges Counties in Maryland, Arlington and Fairfax Counties
* in Virginia, and the cities of Alexandria and Falls Church in Virginia. When January 20 of any fourth year after
* 1965 falls on Sunday, the next succeeding day selected for the public observance of the inauguration of the
* President is a legal public holiday for the purpose of this subsection."
* <p>
* If the given year is an inauguration year, January 20th of that year (adjusted for weekends) will be returned.
* Otherwise, the returned optional will be empty
*
* @param year some calendar year, e.g. 2018
* @return an empty optional if the given year did not have an inauguration day
*/
public static Optional<LocalDate> inaugurationDay(int year) {
return Optional.ofNullable(
isInaugurationYear(year) ? adjustForWeekends(INAUGURATION_DATE.atYear(year)) : null
);
}
/**
* Returns all Federal Holidays for the given calendar year. For years like 2011, where New Years Day was observed
* on December 31st, 2010, the returned list will include that 2010 date.
* <p>
* If the year happens to be the year of an inauguration, then the list will also include the date of the
* inauguration, provided the observed inauguration date that year does not coincide with Martin Luther King Jr.'s
* Birthday, as it did in 2013.
*
* @param year some calendar year, e.g. 2018
* @return a chronologically ordered set of the observed Federal Holidays that fall within that year.
*/
public static SortedSet<LocalDate> observancesForYear(int year) {
// supplies a TreeSet for collecting our holiday dates
Supplier<SortedSet<LocalDate>> setSupplier = () -> new TreeSet<>(inaugurationDay(year)
.map(Arrays::asList) // seed the TreeSet with a single-element collection containing inauguration day
.orElseGet(ArrayList::new)); // or seed the TreeSet with an empty list when there's no inauguration day
return Stream.of(values())
.map(holiday -> holiday.forYear(year))
.collect(Collectors.toCollection(setSupplier));
}
/**
* Indicates if the given date is an observed Federal holiday, including inauguration days.
*
* @param date cannot be null
* @return true only when the provided date is the observance of a Federal holiday, including inauguration days
*/
public static boolean isObserved(LocalDate date) {
requireNonNull(date);
return IntStream.of(0, 1) // sometimes a date is a holiday in the subsequent year
.map(i -> date.getYear() + i) // so look at holidays within both the current and subsequent year
.mapToObj(FederalHoliday::observancesForYear)
.flatMap(SortedSet::stream)
.anyMatch(date::equals);
}
/**
* Indicates if the given date falls on the weekend (i.e. is a Saturday or Sunday).
*
* @param date cannot be null
* @return true if the given date is a Saturday or Sunday
*/
public static boolean isWeekend(LocalDate date) {
requireNonNull(date);
return date.getDayOfWeek() == SUNDAY || date.getDayOfWeek() == SATURDAY;
}
/**
* Indicates if the given date is a Saturday, Sunday, or an observed Federal holiday (inauguration days included).
* <p>
* Generally, this method should not be used for determining historic non-workdays. The rules currently implemented
* within this class have not always been in effect (e.g. Columbus Day was not a holiday until 1968). Furthermore,
* this method does not take into account days where Federal offices closed due to weather or shutdown reasons.
* <p>
* Caution should also be used when using this method for dates in the far future as laws or circumstances may
* change between now and that date, causing the rules currently implemented to be inapplicable at that time.
*
* @param date cannot be null
* @return true when the date is a weekend or observed Federal holiday, inauguration days included
*/
public static boolean isNonWorkday(LocalDate date) {
requireNonNull(date);
return isWeekend(date) || isObserved(date);
}
}
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static FederalHoliday.*;
import static java.time.Month.*;
import static org.assertj.core.api.Assertions.assertThat;
class FederalHolidayTest {
@Test
void no_holidays_observed_on_weekends() {
assertThat(
IntStream.rangeClosed(1900, 2100)
.mapToObj(FederalHoliday::observancesForYear)
.flatMap(Collection::stream)
).noneMatch(FederalHoliday::isWeekend);
}
@Test
void holidays_observed_on_nonworkdays() {
assertThat(
IntStream.rangeClosed(1900, 2100)
.mapToObj(FederalHoliday::observancesForYear)
.flatMap(Collection::stream)
).allMatch(FederalHoliday::isNonWorkday);
}
@Test
void holidays_in_2011_include_new_years_on_dec_31_2010() {
final LocalDate expectedNewYears = LocalDate.of(2010, DECEMBER, 31);
Optional<LocalDate> date = observancesForYear(2011).stream()
.filter(d -> d.getYear() == 2010)
.findFirst();
assertThat(date).hasValue(expectedNewYears);
assertThat(FederalHoliday.isObserved(LocalDate.of(2010, DECEMBER, 30))).isFalse();
assertThat(FederalHoliday.isObserved(expectedNewYears)).isTrue();
assertThat(FederalHoliday.isObserved(LocalDate.of(2011, JANUARY, 1))).isFalse();
}
/*
* "Inauguration Day, January 20, 2017, falls on a Friday. An employee who works in the District of Columbia,
* Montgomery or Prince George's Counties in Maryland, Arlington or Fairfax Counties in Virginia, or the
* cities of Alexandria or Fairfax in Virginia, and who is regularly scheduled to perform non-overtime work on
* Inauguration Day, is entitled to a holiday. (See 5 U.S.C. 6103(c).) There is no in-lieu-of holiday for
* employees who are not regularly scheduled to work on Inauguration Day."
*/
@Test
void year_2017_is_an_inauguration_year() {
final int year = 2017;
final LocalDate expected = LocalDate.of(year, JANUARY, 20);
Optional<LocalDate> trump = inaugurationDay(year);
assertThat(trump).hasValue(expected);
SortedSet<LocalDate> holidays = observancesForYear(year);
assertThat(holidays).hasSize(FederalHoliday.values().length + 1); // all holidays plus inaug day
assertThat(holidays).contains(expected);
}
/*
* "This year, the Inauguration Day holiday falls on Monday, January 21, 2013, which is also the legal public
* holiday for the Birthday of Martin Luther King, Jr. (See 5 U.S.C. 6103(c).) For Federal employees who work
* in the District of Columbia, Montgomery or Prince George's Counties in Maryland, Arlington or Fairfax Counties
* in Virginia, or the cities of Alexandria or Fairfax in Virginia, Inauguration Day is observed concurrently with
* the Martin Luther King, Jr., holiday. Federal employees in these areas are not entitled to an in-lieu-of
* holiday for Inauguration Day."
*/
@Test
void year_2013_had_inauguration_day_fall_on_mlk_day() {
final int year = 2013;
Optional<LocalDate> date = inaugurationDay(year);
assertThat(date).hasValue(MLK_JR_BIRTHDAY.forYear(year));
SortedSet<LocalDate> holidays = observancesForYear(year);
assertThat(holidays).hasSize(FederalHoliday.values().length);
assertThat(FederalHoliday.isObserved(MLK_JR_BIRTHDAY.forYear(year))).isTrue();
}
@Test
void years_without_inaugurations() {
assertThat(inaugurationDay(2016)).isNotPresent();
assertThat(inaugurationDay(2018)).isNotPresent();
assertThat(inaugurationDay(2019)).isNotPresent();
assertThat(inaugurationDay(2020)).isNotPresent();
assertThat(inaugurationDay(2022)).isNotPresent();
}
@Test
void christmas_2011_falls_on_sunday_observed_on_monday() {
assertThat(CHRISTMAS.forYear(2011))
.isEqualTo(LocalDate.of(2011, DECEMBER, 26));
}
@Test
void independency_day_2015_falls_on_saturday_observed_on_friday() {
assertThat(INDEPENDENCE_DAY.forYear(2015))
.isEqualTo(LocalDate.of(2015, JULY, 3));
}
/*
* Only for years without an inauguration day (since those do not have a corresponding enum).
* Given a year, asserts that the mapping of month days to a federal holiday are in agreement with
* the dates generated by FederalHoliday for that year AND that each particular holiday's date for that year
* equals the mapped date.
*/
private void assertCalendarYear(int year, Map.Entry<FederalHoliday, MonthDay>... holidays) {
Map<FederalHoliday, LocalDate> expected = Stream.of(holidays)
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> Year.of(year).atMonthDay(entry.getValue()),
(a, b) -> a,
() -> new EnumMap<>(FederalHoliday.class))
);
assertThat(FederalHoliday.observancesForYear(year))
.containsExactlyElementsOf(expected.values());
assertThat(expected).allSatisfy((holiday, date) -> {
assertThat(holiday.forYear(year)).isEqualTo(date);
});
}
private Map.Entry<FederalHoliday, MonthDay> holiday(FederalHoliday holiday, Month month, int day) {
return new AbstractMap.SimpleEntry<>(holiday, MonthDay.of(month, day));
}
/*
* Monday, January 1 New Year's Day
* Monday, January 15 Birthday of Martin Luther King, Jr.
* Monday, February 19 Washington's Birthday
* Monday, May 28 Memorial Day
* Wednesday, July 4 Independence Day
* Monday, September 3 Labor Day
* Monday, October 8 Columbus Day
* Monday, November 12+ Veterans Day
* Thursday, November 22 Thanksgiving Day
* Tuesday, December 25 Christmas Day
*
* +November 11, 2018 (the legal public holiday for Veterans Day), falls on a Sunday. For most Federal employees,
* Monday, November 12, will be treated as a holiday for pay and leave purposes. (See section 3(a) of Executive
* order 11582, February 11, 1971.)
*/
@Test
void verify_2018_holidays() {
assertCalendarYear(2018,
holiday(NEW_YEARS, JANUARY, 1),
holiday(MLK_JR_BIRTHDAY, JANUARY, 15),
holiday(WASHINGTON_BIRTHDAY, FEBRUARY, 19),
holiday(MEMORIAL_DAY, MAY, 28),
holiday(INDEPENDENCE_DAY, JULY, 4),
holiday(LABOR_DAY, SEPTEMBER, 3),
holiday(COLUMBUS_DAY, OCTOBER, 8),
holiday(VETERANS_DAY, NOVEMBER, 12),
holiday(THANKSGIVING, NOVEMBER, 22),
holiday(CHRISTMAS, DECEMBER, 25));
}
/*
* Monday, January 1 New Year's Day
* Monday, January 21 Birthday of Martin Luther King, Jr.
* Monday, February 18 Washington's Birthday
* Monday, May 27 Memorial Day
* Wednesday, July 4 Independence Day
* Monday, September 2 Labor Day
* Monday, October 14 Columbus Day
* Monday, November 11 Veterans Day
* Thursday, November 28 Thanksgiving Day
* Tuesday, December 25 Christmas Day
*/
@Test
void verify_2019_holidays() {
assertCalendarYear(2019,
holiday(NEW_YEARS, JANUARY, 1),
holiday(MLK_JR_BIRTHDAY, JANUARY, 21),
holiday(WASHINGTON_BIRTHDAY, FEBRUARY, 18),
holiday(MEMORIAL_DAY, MAY, 27),
holiday(INDEPENDENCE_DAY, JULY, 4),
holiday(LABOR_DAY, SEPTEMBER, 2),
holiday(COLUMBUS_DAY, OCTOBER, 14),
holiday(VETERANS_DAY, NOVEMBER, 11),
holiday(THANKSGIVING, NOVEMBER, 28),
holiday(CHRISTMAS, DECEMBER, 25));
}
/*
* Monday, January 1 New Year's Day
* Monday, January 20 Birthday of Martin Luther King, Jr.
* Monday, February 17 Washington's Birthday
* Monday, May 25 Memorial Day
* Wednesday, July 3+ Independence Day
* Monday, September 7 Labor Day
* Monday, October 12 Columbus Day
* Monday, November 11 Veterans Day
* Thursday, November 26 Thanksgiving Day
* Tuesday, December 25 Christmas Day
*
* +July 4, 2020 (the legal public holiday for Independence Day), falls on a Saturday. For most Federal employees,
* Friday, July 3, will be treated as a holiday for pay and leave purposes. (See 5 U.S.C. 6103(b).)
*/
@Test
void verify_2020_holidays() {
assertCalendarYear(2020,
holiday(NEW_YEARS, JANUARY, 1),
holiday(MLK_JR_BIRTHDAY, JANUARY, 20),
holiday(WASHINGTON_BIRTHDAY, FEBRUARY, 17),
holiday(MEMORIAL_DAY, MAY, 25),
holiday(INDEPENDENCE_DAY, JULY, 3),
holiday(LABOR_DAY, SEPTEMBER, 7),
holiday(COLUMBUS_DAY, OCTOBER, 12),
holiday(VETERANS_DAY, NOVEMBER, 11),
holiday(THANKSGIVING, NOVEMBER, 26),
holiday(CHRISTMAS, DECEMBER, 25));
}
@Test
void isWeekend_works() {
assertThat(FederalHoliday.isWeekend(LocalDate.of(2018, OCTOBER, 26))).isFalse();
assertThat(FederalHoliday.isWeekend(LocalDate.of(2018, OCTOBER, 27))).isTrue();
assertThat(FederalHoliday.isWeekend(LocalDate.of(2018, OCTOBER, 28))).isTrue();
assertThat(FederalHoliday.isWeekend(LocalDate.of(2018, OCTOBER, 29))).isFalse();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment