Skip to content

Commit

Permalink
Rewrite NorwegianDateUtil to use java.time
Browse files Browse the repository at this point in the history
java.util.Date and java.util.Calendar is old now and we don't want to
use that any more.

Breaking changes:

We use ZonedDateTime in stead of Date to represent a date to check
for working days.

The list of holidays are now a NavigableSet<LocalDate> and not a Date[].

Co-authored-by: tobiasgwaaler
Co-authored-by: runeflobakk
Co-authored-by: martin-jackson
Co-authored-by: hhaga
Co-authored-by: fredrbus
  • Loading branch information
Eivind Bergstøl committed Sep 20, 2024
1 parent 0f16b47 commit 3d621d9
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 273 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.co.probablyfine</groupId>
<artifactId>java-8-matchers</artifactId>
<version>1.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
Expand Down
256 changes: 71 additions & 185 deletions src/main/java/no/bekk/bekkopen/date/NorwegianDateUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,24 @@
* #L%
*/

import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;

/**
* Utility class for Norwegian dates.
*/
public class NorwegianDateUtil {
private static Map<Integer, Set<Date>> holidays;

private static Map<Integer, NavigableSet<LocalDate>> holidays;

/**
* Adds the given number of working days to the given date. A working day is
Expand All @@ -60,19 +65,14 @@ public class NorwegianDateUtil {
* The number of working days to add.
* @return The new date.
*/
public static Date addWorkingDaysToDate(Date date, int days) {
Calendar cal = dateToCalendar(date);

for (int i = 0; i < days; i++) {
cal.add(Calendar.DATE, 1);
while (!isWorkingDay(cal)) {
cal.add(Calendar.DATE, 1);
}
}

return cal.getTime();
public static ZonedDateTime addWorkingDaysToDate(ZonedDateTime date, int days) {
return Stream.iterate(date, (d) -> d.plusDays(1))
.filter(NorwegianDateUtil::isWorkingDay)
.limit(days + 1)
.max(Comparator.naturalOrder())
.orElse(date);
}

/**
* Will check if the given date is a working day. That is check if the given
* date is a weekend day or a national holiday.
Expand All @@ -81,8 +81,8 @@ public static Date addWorkingDaysToDate(Date date, int days) {
* The date to check.
* @return true if the given date is a working day, false otherwise.
*/
public static boolean isWorkingDay(Date date) {
return isWorkingDay(dateToCalendar(date));
public static boolean isWorkingDay(ZonedDateTime date) {
return date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY && !isHoliday(date);
}

/**
Expand All @@ -92,214 +92,100 @@ public static boolean isWorkingDay(Date date) {
* The Date to check.
* @return true if holiday, false otherwise.
*/
public static boolean isHoliday(Date date) {
return isHoliday(dateToCalendar(date));
public static boolean isHoliday(ZonedDateTime date) {
final Set<LocalDate> holidaySet = getHolidaySet(date.getYear());
return holidaySet.contains(date.toLocalDate());
}

/**
* Return a sorted array of holidays for a given year.
* Return a sorted set of holidays for a given year.
*
* @param year
* The year to get holidays for.
* @return The array of holidays, sorted by date.
* @return The set of holidays, naturally sorted by date.
*/
public static Date[] getHolidays(int year) {
Set<Date> days = getHolidaySet(year);
Date[] dates = days.toArray(new Date[days.size()]);
Arrays.sort(dates);
return dates;
public static NavigableSet<LocalDate> getHolidays(int year) {
return getHolidaySet(year);
}

/**
* Calculates easter day (sunday) by using Spencer Jones formula found here:
* <a href="http://no.wikipedia.org/wiki/P%C3%A5skeformelen">Wikipedia -
* Påskeformelen</a>
*
* @param year
* The year to calculate from.
* @return The LocalDate representing easter day for the given year.
*/
private static LocalDate getEasterDay(int year) {
int a = year % 19;
int b = year / 100;
int c = year % 100;
int d = b / 4;
int e = b % 4;
int f = (b + 8) / 25;
int g = (b - f + 1) / 3;
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) + (22 * l)) / 451;
int n = (h + l - (7 * m) + 114) / 31; // This is the month number.
int p = (h + l - (7 * m) + 114) % 31; // This is the date minus one.

return LocalDate.of(year, n, p + 1);
}

/**
* Get a set of holidays for a given year.
*
* @param year
* The year to get holidays for.
* @return The set of dates.
*/
private static Set<Date> getHolidaySet(int year) {
private static NavigableSet<LocalDate> getHolidaySet(int year) {
if (holidays == null) {
holidays = new HashMap<>();
}
if (!holidays.containsKey(year)) {
Set<Date> yearSet = new HashSet<>();
NavigableSet<LocalDate> yearSet = new TreeSet<>();

// Add set holidays.
yearSet.add(getDate(1, Calendar.JANUARY, year));
yearSet.add(getDate(1, Calendar.MAY, year));
yearSet.add(getDate(17, Calendar.MAY, year));
yearSet.add(getDate(25, Calendar.DECEMBER, year));
yearSet.add(getDate(26, Calendar.DECEMBER, year));
yearSet.add(LocalDate.of(year, Month.JANUARY, 1));
yearSet.add(LocalDate.of(year, Month.MAY, 1));
yearSet.add(LocalDate.of(year, Month.MAY, 17));
yearSet.add(LocalDate.of(year, Month.DECEMBER, 25));
yearSet.add(LocalDate.of(year, Month.DECEMBER, 26));

// Add movable holidays - based on easter day.
Calendar easterDay = dateToCalendar(getEasterDay(year));
final LocalDate easterDay = getEasterDay(year);

// Sunday before easter.
yearSet.add(rollGetDate(easterDay, -7));
yearSet.add(easterDay.minusDays(7));

// Thursday before easter.
yearSet.add(rollGetDate(easterDay, -3));
yearSet.add(easterDay.minusDays(3));

// Friday before easter.
yearSet.add(rollGetDate(easterDay, -2));
yearSet.add(easterDay.minusDays(2));

// Easter day.
yearSet.add(easterDay.getTime());
yearSet.add(easterDay);

// Second easter day.
yearSet.add(rollGetDate(easterDay, 1));
yearSet.add(easterDay.plusDays(1));

// "Kristi himmelfart" day.
yearSet.add(rollGetDate(easterDay, 39));
yearSet.add(easterDay.plusDays(39));

// "Pinse" day.
yearSet.add(rollGetDate(easterDay, 49));
yearSet.add(easterDay.plusDays(49));

// Second "Pinse" day.
yearSet.add(rollGetDate(easterDay, 50));
yearSet.add(easterDay.plusDays(50));

holidays.put(year, yearSet);
}
return holidays.get(year);
}

/**
* Will check if the given date is a working day. That is check if the given
* date is a weekend day or a national holiday.
*
* @param cal
* The Calendar object representing the date.
* @return true if the given date is a working day, false otherwise.
*/
private static boolean isWorkingDay(Calendar cal) {
return cal.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY && cal.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY
&& !isHoliday(cal);
}

/**
* Check if given Calendar object represents a holiday.
*
* @param cal
* The Calendar to check.
* @return true if holiday, false otherwise.
*/
private static boolean isHoliday(Calendar cal) {
int year = cal.get(Calendar.YEAR);
Set<?> yearSet = getHolidaySet(year);
for (Object aYearSet : yearSet) {
Date date = (Date) aYearSet;
if (checkDate(cal, dateToCalendar(date))) {
return true;
}
}
return false;
}

/**
* Calculates easter day (sunday) by using Spencer Jones formula found here:
* <a href="http://no.wikipedia.org/wiki/P%C3%A5skeformelen">Wikipedia -
* Påskeformelen</a>
*
* @param year
* The year to calculate from.
* @return The Calendar object representing easter day for the given year.
*/
private static Date getEasterDay(int year) {
int a = year % 19;
int b = year / 100;
int c = year % 100;
int d = b / 4;
int e = b % 4;
int f = (b + 8) / 25;
int g = (b - f + 1) / 3;
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) + (22 * l)) / 451;
int n = (h + l - (7 * m) + 114) / 31; // This is the month number.
int p = (h + l - (7 * m) + 114) % 31; // This is the date minus one.

Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, n - 1);
cal.set(Calendar.DATE, p + 1);

return cal.getTime();
}

/**
* Check if the given dates match on day and month.
*
* @param cal
* The Calendar representing the first date.
* @param other
* The Calendar representing the second date.
* @return true if they match, false otherwise.
*/
private static boolean checkDate(Calendar cal, Calendar other) {
return checkDate(cal, other.get(Calendar.DATE), other.get(Calendar.MONTH));
}

/**
* Check if the given date represents the given date and month.
*
* @param cal
* The Calendar object representing date to check.
* @param date
* The date.
* @param month
* The month.
* @return true if they match, false otherwise.
*/
private static boolean checkDate(Calendar cal, int date, int month) {
return cal.get(Calendar.DATE) == date && cal.get(Calendar.MONTH) == month;
}

/**
* Convert the given Date object to a Calendar instance.
*
* @param date
* The Date object.
* @return The Calendar instance.
*/
private static Calendar dateToCalendar(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
}

/**
* Add the given number of days to the calendar and convert to Date.
*
* @param calendar
* The calendar to add to.
* @param days
* The number of days to add.
* @return The date object given by the modified calendar.
*/
private static Date rollGetDate(Calendar calendar, int days) {
Calendar easterSunday = (Calendar) calendar.clone();
easterSunday.add(Calendar.DATE, days);
return easterSunday.getTime();
}

/**
* Get the date for the given values.
*
* @param day
* The day.
* @param month
* The month.
* @param year
* The year.
* @return The date represented by the given values.
*/
private static Date getDate(int day, int month, int year) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.DATE, day);
return cal.getTime();
}
}
Loading

0 comments on commit 3d621d9

Please sign in to comment.