Skip to content

Instantly share code, notes, and snippets.

@andrebreves
Last active January 2, 2024 13:02
Show Gist options
  • Save andrebreves/bdaeb326e6eea191a9138827b70aa040 to your computer and use it in GitHub Desktop.
Save andrebreves/bdaeb326e6eea191a9138827b70aa040 to your computer and use it in GitHub Desktop.
Date calculations considering Brazilian Holidays
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