Skip to content

Commit

Permalink
Allow holiday data to override weekends (#1958)
Browse files Browse the repository at this point in the history
* adding exclude section in holiday data ini file to handle shifted weekends

* Update holiday calendar change to be working days

Fix combined() to retain new information
  • Loading branch information
jodastephen authored and andreiruse committed Apr 26, 2019
1 parent db32975 commit a0c762a
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 22 deletions.
Expand Up @@ -19,6 +19,7 @@
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -58,6 +59,10 @@ final class HolidayCalendarIniLookup
* The Weekend key name.
*/
private static final String WEEKEND_KEY = "Weekend";
/**
* The WorkingDays key name.
*/
private static final String WORKING_DAYS_KEY = "WorkingDays";
/**
* The lenient day-of-week parser.
*/
Expand Down Expand Up @@ -130,10 +135,12 @@ static ImmutableMap<String, HolidayCalendar> loadFromIni(String filename) {
return ImmutableMap.copyOf(map);
}

// parses the holiday calendar
private static HolidayCalendar parseHolidayCalendar(String calendarName, PropertySet section) {
String weekendStr = section.value(WEEKEND_KEY);
Set<DayOfWeek> weekends = parseWeekends(weekendStr);
List<LocalDate> holidays = new ArrayList<>();
Set<LocalDate> workingDays = new HashSet<>();
for (String key : section.keys()) {
if (key.equals(WEEKEND_KEY)) {
continue;
Expand All @@ -142,12 +149,14 @@ private static HolidayCalendar parseHolidayCalendar(String calendarName, Propert
if (key.length() == 4) {
int year = Integer.parseInt(key);
holidays.addAll(parseYearDates(year, value));
} else if (WORKING_DAYS_KEY.equals(key)) {
workingDays.addAll(parseDates(value));
} else {
holidays.add(LocalDate.parse(key));
}
}
// build result
return ImmutableHolidayCalendar.of(HolidayCalendarId.of(calendarName), holidays, weekends);
return ImmutableHolidayCalendar.of(HolidayCalendarId.of(calendarName), holidays, weekends, workingDays);
}

// parse weekend format, such as 'Sat,Sun'
Expand All @@ -166,6 +175,14 @@ private static List<LocalDate> parseYearDates(int year, String str) {
.collect(toImmutableList());
}

// parse comma separated date format such as "2015-01-01,2015-03-12"
private static List<LocalDate> parseDates(String str) {
List<String> split = Splitter.on(',').splitToList(str);
return split.stream()
.map(LocalDate::parse)
.collect(toImmutableList());
}

private static LocalDate parseDate(int year, String str) {
try {
return MonthDay.parse(str, DAY_MONTH_PARSER).atYear(year);
Expand Down
Expand Up @@ -34,12 +34,14 @@
import org.joda.beans.impl.direct.DirectPrivateBeanBuilder;
import org.joda.beans.ser.SerDeserializer;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.tuple.Pair;

/**
* An immutable holiday calendar implementation.
Expand Down Expand Up @@ -126,7 +128,7 @@ public static ImmutableHolidayCalendar of(
DayOfWeek secondWeekendDay) {

ImmutableSet<DayOfWeek> weekendDays = Sets.immutableEnumSet(firstWeekendDay, secondWeekendDay);
return of(id, ImmutableSortedSet.copyOf(holidays), weekendDays);
return of(id, ImmutableSortedSet.copyOf(holidays), weekendDays, ImmutableSet.of());
}

/**
Expand All @@ -148,7 +150,37 @@ public static ImmutableHolidayCalendar of(
Iterable<LocalDate> holidays,
Iterable<DayOfWeek> weekendDays) {

return of(id, ImmutableSortedSet.copyOf(holidays), Sets.immutableEnumSet(weekendDays));
return of(id, ImmutableSortedSet.copyOf(holidays), Sets.immutableEnumSet(weekendDays), ImmutableSet.of());
}

/**
* Obtains an instance from a set of holiday dates and weekend days.
* <p>
* The holiday dates will be extracted into a set with duplicates ignored.
* The minimum supported date for query is the start of the year of the earliest holiday.
* The maximum supported date for query is the end of the year of the latest holiday.
* <p>
* The weekend days may be empty, in which case the holiday dates should contain any weekends.
* The working days are processed last, changing holidays and weekends back to business days,
* but only within the range of supported years.
*
* @param id the identifier
* @param holidays the set of holiday dates
* @param weekendDays the days that define the weekend, if empty then weekends are treated as business days
* @param workingDays the working days that override holidays and weekends
* @return the holiday calendar
*/
public static ImmutableHolidayCalendar of(
HolidayCalendarId id,
Iterable<LocalDate> holidays,
Iterable<DayOfWeek> weekendDays,
Iterable<LocalDate> workingDays) {

return of(
id,
ImmutableSortedSet.copyOf(holidays),
Sets.immutableEnumSet(weekendDays),
ArgChecker.notNull(workingDays, "workingDays"));
}

/**
Expand All @@ -172,11 +204,15 @@ public static ImmutableHolidayCalendar combined(ImmutableHolidayCalendar cal1, I
int endYear1 = cal1.startYear + cal1.lookup.length / 12;
int endYear2 = cal2.startYear + cal2.lookup.length / 12;
if (endYear1 < cal2.startYear || endYear2 < cal1.startYear) {
Pair<ImmutableSortedSet<LocalDate>, ImmutableSortedSet<LocalDate>> holsWork1 = cal1.getHolidaysAndWorkingDays();
Pair<ImmutableSortedSet<LocalDate>, ImmutableSortedSet<LocalDate>> holsWork2 = cal2.getHolidaysAndWorkingDays();
ImmutableSortedSet<LocalDate> newHolidays =
ImmutableSortedSet.copyOf(Iterables.concat(cal1.getHolidays(), cal2.getHolidays()));
ImmutableSortedSet.copyOf(Iterables.concat(holsWork1.getFirst(), holsWork2.getFirst()));
ImmutableSet<DayOfWeek> newWeekends =
ImmutableSet.copyOf(Iterables.concat(cal1.getWeekendDays(), cal2.getWeekendDays()));
return of(newId, newHolidays, newWeekends);
ImmutableSet<LocalDate> newWorkingDays =
ImmutableSet.copyOf(Iterables.concat(holsWork1.getSecond(), holsWork2.getSecond()));
return of(newId, newHolidays, newWeekends, newWorkingDays);
}

// merge calendars using bitwise operations
Expand All @@ -198,10 +234,13 @@ public static ImmutableHolidayCalendar combined(ImmutableHolidayCalendar cal1, I
}

// creates an instance calculating the supported range
static ImmutableHolidayCalendar of(HolidayCalendarId id, SortedSet<LocalDate> holidays, Set<DayOfWeek> weekendDays) {
static ImmutableHolidayCalendar of(
HolidayCalendarId id,
SortedSet<LocalDate> holidays,
Set<DayOfWeek> weekendDays,
Iterable<LocalDate> workingDays) {

ArgChecker.notNull(id, "id");
ArgChecker.notNull(holidays, "holidays");
ArgChecker.notNull(weekendDays, "weekendDays");
int weekends = weekendDays.stream().mapToInt(dow -> 1 << (dow.getValue() - 1)).sum();
int startYear = 0;
int[] lookup = new int[0];
Expand All @@ -213,7 +252,7 @@ static ImmutableHolidayCalendar of(HolidayCalendarId id, SortedSet<LocalDate> ho
// normal case where holidays are specified
startYear = holidays.first().getYear();
int endYearExclusive = holidays.last().getYear() + 1;
lookup = buildLookupArray(holidays, weekendDays, startYear, endYearExclusive);
lookup = buildLookupArray(holidays, weekendDays, startYear, endYearExclusive, workingDays);
}
return new ImmutableHolidayCalendar(id, weekends, startYear, lookup);
}
Expand All @@ -224,7 +263,8 @@ private static int[] buildLookupArray(
Iterable<LocalDate> holidays,
Iterable<DayOfWeek> weekendDays,
int startYear,
int endYearExclusive) {
int endYearExclusive,
Iterable<LocalDate> workingDays) {

// array that has one entry for each month
int[] array = new int[(endYearExclusive - startYear) * 12];
Expand All @@ -251,6 +291,14 @@ private static int[] buildLookupArray(
int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1;
array[index] &= ~(1 << (date.getDayOfMonth() - 1));
}
// set the bit associated with each overriding working day
for (LocalDate date : workingDays) {
if (date.getYear() < startYear || date.getYear() >= endYearExclusive) {
continue;
}
int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1;
array[index] |= (1 << (date.getDayOfMonth() - 1));
}
return array;
}

Expand Down Expand Up @@ -296,11 +344,32 @@ static ImmutableHolidayCalendar readExternal(DataInput in) throws IOException {

//-------------------------------------------------------------------------
// returns the holidays as a set
@VisibleForTesting
ImmutableSortedSet<LocalDate> getHolidays() {
return getHolidaysAndWorkingDays().getFirst();
}

// returns the weekend days as a set
@VisibleForTesting
ImmutableSet<DayOfWeek> getWeekendDays() {
return Stream.of(DayOfWeek.values())
.filter(dow -> (weekends & (1 << dow.ordinal())) != 0)
.collect(toImmutableSet());
}

// returns the working day overrides as a set
@VisibleForTesting
ImmutableSortedSet<LocalDate> getWorkingDays() {
return getHolidaysAndWorkingDays().getSecond();
}

// returns the working day overrides as a set
private Pair<ImmutableSortedSet<LocalDate>, ImmutableSortedSet<LocalDate>> getHolidaysAndWorkingDays() {
if (startYear == 0) {
return ImmutableSortedSet.of();
return Pair.of(ImmutableSortedSet.of(), ImmutableSortedSet.of());
}
ImmutableSortedSet.Builder<LocalDate> builder = ImmutableSortedSet.naturalOrder();
ImmutableSortedSet.Builder<LocalDate> holidays = ImmutableSortedSet.naturalOrder();
ImmutableSortedSet.Builder<LocalDate> workingDays = ImmutableSortedSet.naturalOrder();
LocalDate firstOfMonth = LocalDate.of(startYear, 1, 1);
for (int i = 0; i < lookup.length; i++) {
int monthData = lookup[i];
Expand All @@ -310,21 +379,18 @@ ImmutableSortedSet<LocalDate> getHolidays() {
for (int j = 0; j < monthLen; j++) {
// if it is a holiday and not a weekend, then add the date
if ((monthData & bit) == 0 && (weekends & (1 << dow0)) == 0) {
builder.add(firstOfMonth.withDayOfMonth(j + 1));
holidays.add(firstOfMonth.withDayOfMonth(j + 1));
}
// if it is a working day and a weekend, then add the date
if ((monthData & bit) != 0 && (weekends & (1 << dow0)) != 0) {
workingDays.add(firstOfMonth.withDayOfMonth(j + 1));
}
dow0 = (dow0 + 1) % 7;
bit <<= 1;
}
firstOfMonth = firstOfMonth.plusMonths(1);
}
return builder.build();
}

// returns the weekend days as a set
ImmutableSet<DayOfWeek> getWeekendDays() {
return Stream.of(DayOfWeek.values())
.filter(dow -> (weekends & (1 << dow.ordinal())) != 0)
.collect(toImmutableSet());
return Pair.of(holidays.build(), workingDays.build());
}

//-------------------------------------------------------------------------
Expand Down
Expand Up @@ -4,7 +4,7 @@
#
# The file contains sections, each of which defined a holiday calendar, such as 'GBLO'
# Each section consists of a number of entries
# Each entry consists of a key and value, with three different types of key
# Each entry consists of a key and value, with four different types of key
# The entries must fully define the holidays for the calendar
#
# The first key type is an ISO-8601 format date
Expand All @@ -20,6 +20,10 @@
# The third key type is 'Weekend', which requires the value to be a comma separated list of weekend days
# For example, 'Weekend = Sat,Sun' defines the weekend as Saturday and Sunday
#
# The fourth key type is 'WorkingDays', which requires the value to be a comma separated list of ISO-8601 format dates
# The dates listed here are actually business days even though they would normally be weekends
# For example, 'WorkingDays = 2016-01-02,2016-01-03'
#
# A valid holiday calendar section includes one 'Weekend' key and at least one holiday date

# NOTE that all Strata holiday calendars are defined in the 'GlobalHolidayCalendars' class
Expand Up @@ -8,6 +8,7 @@
import static com.opengamma.strata.collect.TestHelper.caputureLog;
import static com.opengamma.strata.collect.TestHelper.date;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import java.time.format.DateTimeParseException;
Expand Down Expand Up @@ -40,6 +41,23 @@ public void test_valid1() {
assertEquals(test.toString(), "HolidayCalendar[TEST-VALID]");
}

public void test_workingDays() {
ImmutableMap<String, HolidayCalendar> lookup =
HolidayCalendarIniLookup.loadFromIni("HolidayCalendarDataWorkingDays.ini");
assertEquals(lookup.size(), 1);

HolidayCalendar test = lookup.get("TEST-WORKINGDAYS");
assertTrue(test.isHoliday(date(2015, 1, 1)));
assertTrue(test.isHoliday(date(2015, 1, 6)));
assertTrue(test.isHoliday(date(2015, 4, 5)));
assertTrue(test.isHoliday(date(2015, 12, 25)));
assertTrue(test.isHoliday(date(2016, 1, 1)));
assertFalse(test.isHoliday(date(2016, 1, 2)));
assertFalse(test.isHoliday(date(2016, 1, 3)));
assertEquals(test.getName(), "TEST-WORKINGDAYS");
assertEquals(test.toString(), "HolidayCalendar[TEST-WORKINGDAYS]");
}

public void test_valid2() {
ImmutableMap<String, HolidayCalendar> lookup = HolidayCalendarIniLookup.loadFromIni("HolidayCalendarDataValid2.ini");
assertEquals(lookup.size(), 1);
Expand Down
Expand Up @@ -44,6 +44,7 @@ public class ImmutableHolidayCalendarTest {
private static final HolidayCalendarId TEST_ID2 = HolidayCalendarId.of("Test2");

private static final LocalDate MON_2014_06_30 = LocalDate.of(2014, 6, 30);
private static final LocalDate TUE_2014_07_08 = LocalDate.of(2014, 7, 8);
private static final LocalDate WED_2014_07_09 = LocalDate.of(2014, 7, 9);
private static final LocalDate THU_2014_07_10 = LocalDate.of(2014, 7, 10);
private static final LocalDate FRI_2014_07_11 = LocalDate.of(2014, 7, 11);
Expand Down Expand Up @@ -323,6 +324,50 @@ public void test_of_IterableIterable_noHolidays(LocalDate date, boolean isBusine
assertEquals(test.toString(), "HolidayCalendar[" + TEST_ID.getName() + "]");
}

//-------------------------------------------------------------------------
@DataProvider(name = "createWorkingDayOverrides")
public static Object[][] data_createWorkingDayOverrides() {
return new Object[][] {
{TUE_2014_07_08, true},
{WED_2014_07_09, false},
{THU_2014_07_10, false},
{FRI_2014_07_11, false},
{SAT_2014_07_12, true},
{SUN_2014_07_13, true},
};
}

@Test(dataProvider = "createWorkingDayOverrides")
public void test_of_IterableIterableIterable(LocalDate date, boolean isBusinessDay) {
Iterable<LocalDate> holidays = Arrays.asList(WED_2014_07_09, THU_2014_07_10);
Iterable<DayOfWeek> weekendDays = Arrays.asList(FRIDAY, SATURDAY);
Iterable<LocalDate> workingDays = Arrays.asList(SAT_2014_07_12);
ImmutableHolidayCalendar test = ImmutableHolidayCalendar.of(TEST_ID, holidays, weekendDays, workingDays);
assertEquals(test.isBusinessDay(date), isBusinessDay);
assertEquals(test.isHoliday(date), !isBusinessDay);
assertEquals(test.getHolidays(), ImmutableSortedSet.copyOf(holidays));
assertEquals(test.getWeekendDays(), ImmutableSet.of(FRIDAY, SATURDAY));
assertEquals(test.getWorkingDays(), ImmutableSortedSet.of(SAT_2014_07_12));
assertEquals(test.toString(), "HolidayCalendar[" + TEST_ID.getName() + "]");
}

@Test(dataProvider = "createWorkingDayOverrides")
public void test_of_IterableIterableIterable_combined(LocalDate date, boolean isBusinessDay) {
Iterable<LocalDate> holidays = Arrays.asList(WED_2014_07_09, THU_2014_07_10);
Iterable<DayOfWeek> weekendDays = Arrays.asList(FRIDAY, SATURDAY);
Iterable<LocalDate> workingDays = Arrays.asList(SAT_2014_07_12);
ImmutableHolidayCalendar base1 = ImmutableHolidayCalendar.of(TEST_ID, holidays, weekendDays, workingDays);
Iterable<LocalDate> holidays2 = Arrays.asList(date(2010, 6, 1));
ImmutableHolidayCalendar base2 = ImmutableHolidayCalendar.of(TEST_ID, holidays2, weekendDays);
ImmutableHolidayCalendar test = ImmutableHolidayCalendar.combined(base1, base2);
assertEquals(test.isBusinessDay(date), isBusinessDay);
assertEquals(test.isHoliday(date), !isBusinessDay);
assertEquals(test.getHolidays(), ImmutableSortedSet.of(date(2010, 6, 1), WED_2014_07_09, THU_2014_07_10));
assertEquals(test.getWeekendDays(), ImmutableSet.of(FRIDAY, SATURDAY));
assertEquals(test.getWorkingDays(), ImmutableSortedSet.of(SAT_2014_07_12));
assertEquals(test.toString(), "HolidayCalendar[" + TEST_ID.getName() + "]");
}

//-------------------------------------------------------------------------
public void test_combined() {
ImmutableHolidayCalendar base1 =
Expand Down
@@ -0,0 +1,29 @@
[TEST-WORKINGDAYS]
Weekend = Sat,Sun
2015-01-01
2015-01-06
2015-04-05
2015-04-06
2015-05-01
2015-05-03
2015-05-24
2015-06-04
2015-08-15
2015-11-01
2015-11-11
2015-12-25
2015-12-26
2016-01-01
2016-01-06
2016-03-27
2016-03-28
2016-05-01
2016-05-03
2016-05-15
2016-05-26
2016-08-15
2016-11-01
2016-11-11
2016-12-25
2016-12-26
WorkingDays = 2016-01-02,2016-01-03

0 comments on commit a0c762a

Please sign in to comment.