Last active
January 2, 2024 13:02
-
-
Save andrebreves/bdaeb326e6eea191a9138827b70aa040 to your computer and use it in GitHub Desktop.
Date calculations considering Brazilian Holidays
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.time.LocalDate; | |
import static java.time.Month.APRIL; | |
import static java.time.Month.DECEMBER; | |
import static java.time.Month.JANUARY; | |
import static java.time.Month.MAY; | |
import static java.time.Month.NOVEMBER; | |
import static java.time.Month.OCTOBER; | |
import static java.time.Month.SEPTEMBER; | |
import java.time.MonthDay; | |
import java.time.Year; | |
import java.time.YearMonth; | |
import java.time.temporal.ChronoField; | |
import java.time.temporal.ChronoUnit; | |
import java.time.temporal.TemporalAccessor; | |
import java.util.Arrays; | |
import java.util.EnumSet; | |
import java.util.function.IntFunction; | |
import static java.util.function.Predicate.not; | |
public enum BrazilianHoliday { | |
NEW_YEAR ("Confraternização Universal", MonthDay.of(JANUARY , 1)), // Since 1950 http://www.planalto.gov.br/ccivil_03/leis/L0662.htm | |
TIRADENTES_DAY ("Tiradentes" , MonthDay.of(APRIL , 21)), // Since 1951 http://www.planalto.gov.br/ccivil_03/leis/L1266.htm | |
LABOR_DAY ("Dia do Trabalho" , MonthDay.of(MAY , 1)), // Since 1949 http://www.planalto.gov.br/ccivil_03/leis/L0662.htm | |
INDEPENDENCE_DAY ("Independência do Brasil" , MonthDay.of(SEPTEMBER, 7)), // Since 1949 http://www.planalto.gov.br/ccivil_03/leis/L0662.htm | |
PATRON_SAINT_DAY ("Padroeira do Brasil" , MonthDay.of(OCTOBER , 12)), // Since 1980 http://www.planalto.gov.br/ccivil_03/leis/L6802.htm | |
ALL_SOULS_DAY ("Finados" , MonthDay.of(NOVEMBER , 2)), // Since 2003 http://www.planalto.gov.br/ccivil_03/leis/2002/L10607.htm | |
PROCLAMATION_OF_REPUBLIC("Proclamação da República" , MonthDay.of(NOVEMBER , 15)), // Since 1949 http://www.planalto.gov.br/ccivil_03/leis/L0662.htm | |
BLACK_AWARENESS_DAY ("Dia da Consciência Negra" , MonthDay.of(NOVEMBER , 20)), // Since 2024 https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/L14759.htm | |
CHRISTMAS_DAY ("Natal" , MonthDay.of(DECEMBER , 25)), // Since 1949 http://www.planalto.gov.br/ccivil_03/leis/L0662.htm | |
EASTER_SUNDAY ("Páscoa" , BrazilianHoliday::easterSundayAtYear), | |
GOOD_FRIDAY ("Sexta-feira Santa" , year -> EASTER_SUNDAY.atYear(year).minusDays( 2)), | |
CARNIVAL_MONDAY ("Carnaval" , year -> EASTER_SUNDAY.atYear(year).minusDays(48)), | |
CARNIVAL_TUESDAY ("Carnaval" , year -> EASTER_SUNDAY.atYear(year).minusDays(47)), | |
CORPUS_CHRISTI ("Corpus Cristi" , year -> EASTER_SUNDAY.atYear(year).plusDays (60)); | |
public static final int MIN_VALID_YEAR = 2003; | |
private static void checkRange(int year) { | |
if (year < MIN_VALID_YEAR) | |
throw new IllegalArgumentException("Year " + year + " is before minimal valid year " + MIN_VALID_YEAR); | |
} | |
private static void checkRange(TemporalAccessor temporal) { | |
if (temporal.isSupported(ChronoField.YEAR)) | |
checkRange(temporal.get(ChronoField.YEAR)); | |
} | |
private final String name; | |
private final MonthDay fixedMonthDay; | |
private final IntFunction<LocalDate> occurrenceAtYear; | |
private BrazilianHoliday(String name, MonthDay monthDay) { | |
this.name = name; | |
this.fixedMonthDay = monthDay; | |
this.occurrenceAtYear = monthDay::atYear; | |
} | |
private static final int OCCURRENCE_CACHE_SIZE = 256; | |
private BrazilianHoliday(String name, IntFunction<LocalDate> occurrenceAtYear) { | |
this.name = name; | |
this.fixedMonthDay = null; | |
final LocalDate[] occurrenceCache = new LocalDate[OCCURRENCE_CACHE_SIZE]; | |
this.occurrenceAtYear = year -> { | |
int index = year - MIN_VALID_YEAR; | |
if (index >= 0 && index < OCCURRENCE_CACHE_SIZE) { | |
if (occurrenceCache[index] == null) | |
occurrenceCache[index] = occurrenceAtYear.apply(year); | |
return occurrenceCache[index]; | |
} else | |
return occurrenceAtYear.apply(year); | |
}; | |
} | |
public boolean isFixed() { | |
return fixedMonthDay != null; | |
} | |
public LocalDate atYear(Year year) { | |
return occurrenceAtYear.apply(year.getValue()); | |
} | |
public LocalDate atYear(int year) { | |
checkRange(year); | |
return occurrenceAtYear.apply(year); | |
} | |
public boolean occursAt(LocalDate date) { | |
checkRange(date); | |
if (fixedMonthDay != null) | |
return date.getMonthValue() == fixedMonthDay.getMonthValue() && | |
date.getDayOfMonth() == fixedMonthDay.getDayOfMonth(); | |
else | |
return date.equals(occurrenceAtYear.apply(date.getYear())); | |
} | |
public boolean occursAt(YearMonth yearMonth) { | |
checkRange(yearMonth); | |
if (fixedMonthDay != null) | |
return yearMonth.getMonthValue() == fixedMonthDay.getMonthValue(); | |
else | |
return yearMonth.getMonthValue() == occurrenceAtYear.apply(yearMonth.getYear()).getMonthValue(); | |
} | |
public static EnumSet<BrazilianHoliday> atLocalDate(LocalDate date) { | |
checkRange(date); | |
EnumSet<BrazilianHoliday> occurrences = EnumSet.noneOf(BrazilianHoliday.class); | |
for (BrazilianHoliday holiday : BrazilianHoliday.values()) | |
if (holiday.occursAt(date)) occurrences.add(holiday); | |
return occurrences; | |
} | |
public static EnumSet<BrazilianHoliday> atYearMonth(YearMonth yearMonth) { | |
checkRange(yearMonth); | |
EnumSet<BrazilianHoliday> occurrences = EnumSet.noneOf(BrazilianHoliday.class); | |
for (BrazilianHoliday holiday : BrazilianHoliday.values()) | |
if (holiday.occursAt(yearMonth)) occurrences.add(holiday); | |
return occurrences; | |
} | |
private static final BrazilianHoliday[] MOVABLE_HOLIDAYS = Arrays | |
.stream(BrazilianHoliday.values()) | |
.filter(not(BrazilianHoliday::isFixed)) | |
.toArray(BrazilianHoliday[]::new); | |
private static final boolean[] FIXED_HOLIDAYS = new boolean[32 * 13]; | |
static { | |
for (BrazilianHoliday holiday : BrazilianHoliday.values()) | |
if (holiday.isFixed()) | |
FIXED_HOLIDAYS[32 * holiday.fixedMonthDay.getMonthValue() + holiday.fixedMonthDay.getDayOfMonth()] = true; | |
} | |
public static boolean isHoliday(LocalDate date) { | |
checkRange(date); | |
if (FIXED_HOLIDAYS[32 * date.getMonthValue() + date.getDayOfMonth()]) | |
return true; | |
for (BrazilianHoliday holiday : MOVABLE_HOLIDAYS) | |
if (date.equals(holiday.occurrenceAtYear.apply(date.getYear()))) | |
return true; | |
return false; | |
} | |
public static boolean isBusinessDay(LocalDate date) { | |
checkRange(date); | |
final int dayOfWeek = dayOfWeek(date); | |
return dayOfWeek != SAT && dayOfWeek != SUN && !isHoliday(date); | |
} | |
public static LocalDate nextBusinessDay(LocalDate date) { | |
checkRange(date); | |
do { | |
switch (dayOfWeek(date)) { | |
case FRI: | |
date = date.plusDays(3L); | |
break; | |
case SAT: | |
date = date.plusDays(2L); | |
break; | |
default: | |
date = date.plusDays(1L); | |
} | |
} while (isHoliday(date)); | |
return date; | |
} | |
public static LocalDate prevBusinessDay(LocalDate date) { | |
checkRange(date); | |
do { | |
switch (dayOfWeek(date)) { | |
case MON: | |
date = date.plusDays(-3L); | |
break; | |
case SUN: | |
date = date.plusDays(-2L); | |
break; | |
default: | |
date = date.plusDays(-1L); | |
} | |
} while (isHoliday(date)); | |
return date; | |
} | |
public static LocalDate plusBusinessDay(long businessDaysToAdd, LocalDate date) { | |
// TODO: Write a algorithm faster than O(n) on businessDaysToAdd | |
long businessDays = businessDaysToAdd; | |
LocalDate newDate = date; | |
while (businessDays > 0) { | |
newDate = nextBusinessDay(newDate); | |
businessDays--; | |
} | |
while (businessDays < 0) { | |
newDate = prevBusinessDay(newDate); | |
businessDays++; | |
} | |
assert businessDaysBetween(date, newDate) == businessDaysToAdd; | |
return newDate; | |
} | |
public static LocalDate minusBusinessDay(long businessDaysToSubtract, LocalDate date) { | |
return plusBusinessDay(-businessDaysToSubtract, date); | |
} | |
// Inspired by https://stackoverflow.com/a/44942039/6910609 | |
public static long businessDaysBetween(LocalDate date1Inclusive, LocalDate date2Exclusive) { | |
checkRange(date1Inclusive); | |
checkRange(date2Exclusive); | |
if (date1Inclusive.equals(date2Exclusive)) return 0L; | |
final long calendarDays = ChronoUnit.DAYS.between(date1Inclusive, date2Exclusive); | |
// Remove one weekend (2 days) for each full week (7 days) | |
long businessDays = calendarDays - 2L * (calendarDays / 7L); | |
if (calendarDays > 0L) { // date1Inclusive < date2Exclusive | |
// Remove any remaining weekend days | |
if (calendarDays % 7L != 0L) { | |
/* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
* | Remainder | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | | |
* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
* | % 7 = 1 | 0/SUN 1/MON -1 | 1/MON 2/TUE 0 | 2/TUE 3/WED 0 | 3/WED 4/THU 0 | 4/THU 5/FRI 0 | 5/FRI 6/SAT 0 | 6/SAT 0/SUN -1 | | |
* | % 7 = 2 | 0/SUN 2/TUE -1 | 1/MON 3/WED 0 | 2/TUE 4/THU 0 | 3/WED 5/FRI 0 | 4/THU 6/SAT 0 | 5/FRI 0/SUN -1 | 6/SAT 1/MON -2 | | |
* | % 7 = 3 | 0/SUN 3/WED -1 | 1/MON 4/THU 0 | 2/TUE 5/FRI 0 | 3/WED 6/SAT 0 | 4/THU 0/SUN -1 | 5/FRI 1/MON -2 | 6/SAT 2/TUE -2 | | |
* | % 7 = 4 | 0/SUN 4/THU -1 | 1/MON 5/FRI 0 | 2/TUE 6/SAT 0 | 3/WED 0/SUN -1 | 4/THU 1/MON -2 | 5/FRI 2/TUE -2 | 6/SAT 3/WED -2 | | |
* | % 7 = 5 | 0/SUN 5/FRI -1 | 1/MON 6/SAT 0 | 2/TUE 0/SUN -1 | 3/WED 1/MON -2 | 4/THU 2/TUE -2 | 5/FRI 3/WED -2 | 6/SAT 4/THU -2 | | |
* | % 7 = 6 | 0/SUN 6/SAT -1 | 1/MON 0/SUN -1 | 2/TUE 1/MON -2 | 3/WED 2/TUE -2 | 4/THU 3/WED -2 | 5/FRI 4/THU -2 | 6/SAT 5/FRI -2 | | |
* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
*/ | |
final int dayOfWeek1 = dayOfWeek(date1Inclusive); | |
final int dayOfWeek2 = dayOfWeek(date2Exclusive); | |
if (dayOfWeek1 == SUN || dayOfWeek2 == SUN) | |
businessDays += -1L; | |
else if (dayOfWeek1 > dayOfWeek2) | |
businessDays += -2L; | |
} | |
// Remove holidays occurring on weekdays | |
for (int year = date1Inclusive.getYear(); year <= date2Exclusive.getYear() && businessDays != 0L; year++) { | |
for (BrazilianHoliday holiday : BrazilianHoliday.values()) { | |
if (businessDays == 0L) break; | |
LocalDate date = holiday.atYear(year); | |
if (!date.isBefore(date1Inclusive) && date.isBefore(date2Exclusive)) { | |
final int dayOfWeek = dayOfWeek(date); | |
if (dayOfWeek != SAT && dayOfWeek != SUN) businessDays--; | |
} | |
} | |
} | |
assert businessDays >= 0L; | |
} else { // date1Inclusive > date2Exclusive | |
// Remove any remaining weekend days | |
if (calendarDays % 7L != 0L) { | |
/* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
* | Remainder | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | date1 date2 | | |
* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
* | % 7 = -1 | 0/SUN 6/SAT 1 | 1/MON 0/SUN 0 | 2/TUE 1/MON 0 | 3/WED 2/TUE 0 | 4/THU 3/WED 0 | 5/FRI 4/THU 0 | 6/SAT 5/FRI 1 | | |
* | % 7 = -2 | 0/SUN 5/FRI 2 | 1/MON 6/SAT 1 | 2/TUE 0/SUN 0 | 3/WED 1/MON 0 | 4/THU 2/TUE 0 | 5/FRI 3/WED 0 | 6/SAT 4/THU 1 | | |
* | % 7 = -3 | 0/SUN 4/THU 2 | 1/MON 5/FRI 2 | 2/TUE 6/SAT 1 | 3/WED 0/SUN 0 | 4/THU 1/MON 0 | 5/FRI 2/TUE 0 | 6/SAT 3/WED 1 | | |
* | % 7 = -4 | 0/SUN 3/WED 2 | 1/MON 4/THU 2 | 2/TUE 5/FRI 2 | 3/WED 6/SAT 1 | 4/THU 0/SUN 0 | 5/FRI 1/MON 0 | 6/SAT 2/TUE 1 | | |
* | % 7 = -5 | 0/SUN 2/TUE 2 | 1/MON 3/WED 2 | 2/TUE 4/THU 2 | 3/WED 5/FRI 2 | 4/THU 6/SAT 1 | 5/FRI 0/SUN 0 | 6/SAT 1/MON 1 | | |
* | % 7 = -6 | 0/SUN 1/MON 2 | 1/MON 2/TUE 2 | 2/TUE 3/WED 2 | 3/WED 4/THU 2 | 4/THU 5/FRI 2 | 5/FRI 6/SAT 1 | 6/SAT 0/SUN 1 | | |
* +-----------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+ | |
*/ | |
final int dayOfWeek1 = dayOfWeek(date1Inclusive); | |
final int dayOfWeek2 = dayOfWeek(date2Exclusive); | |
if (dayOfWeek1 == SAT || dayOfWeek2 == SAT) | |
businessDays += 1L; | |
else if (dayOfWeek1 < dayOfWeek2) | |
businessDays += 2L; | |
} | |
// Remove holidays occurring on weekdays | |
for (int year = date2Exclusive.getYear(); year <= date1Inclusive.getYear() && businessDays != 0L; year++) { | |
for (BrazilianHoliday holiday : BrazilianHoliday.values()) { | |
if (businessDays == 0L) break; | |
LocalDate date = holiday.atYear(year); | |
if (!date.isAfter(date1Inclusive) && date.isAfter(date2Exclusive)) { | |
final int dayOfWeek = dayOfWeek(date); | |
if (dayOfWeek != SAT && dayOfWeek != SUN) businessDays++; | |
} | |
} | |
} | |
assert businessDays <= 0L; | |
} | |
return businessDays; | |
} | |
// Find the Day of the Week using Sakamoto's Method | |
// Much faster than LocalDate::getDayOfWeek in my benchmarks | |
// https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Sakamoto%27s_methods | |
private static final int SUN = 0; | |
private static final int MON = 1; | |
private static final int TUE = 2; | |
private static final int WED = 3; | |
private static final int THU = 4; | |
private static final int FRI = 5; | |
private static final int SAT = 6; | |
private static final int[] TABLE = {-1, 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; | |
private static int dayOfWeek(LocalDate date) { | |
final int month = date.getMonthValue(); | |
final int year = (month < 3) | |
? date.getYear() - 1 | |
: date.getYear(); | |
return (year + year / 4 - year / 100 + year / 400 + TABLE[month] + date.getDayOfMonth()) % 7; | |
} | |
// Find the Easter Sunday at a specific year using the version of | |
// Meeus/Jones/Butcher algorithm published by New Scientist in 1961 | |
// https://en.wikipedia.org/wiki/Date_of_Easter#Anonymous_Gregorian_algorithm | |
private static LocalDate easterSundayAtYear(int y) { | |
int a = y % 19; | |
int b = y / 100; | |
int c = y % 100; | |
int d = b / 4; | |
int e = b % 4; | |
int g = (8 * b + 13) / 25; | |
int h = (19 * a + b - d - g + 15) % 30; | |
int i = c / 4; | |
int k = c % 4; | |
int l = (32 + 2 * e + 2 * i - h - k) % 7; | |
int m = (a + 11 * h + 19 * l) / 433; | |
int n = (h + l - 7 * m + 90) / 25; | |
int p = (h + l - 7 * m + 33 * n + 19) % 32; | |
return LocalDate.of(y, n, p); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment