diff --git a/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerApi.java b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerApi.java new file mode 100644 index 000000000..1662d007c --- /dev/null +++ b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerApi.java @@ -0,0 +1,30 @@ +package de.focus_shift.jollyday.tests; + +import de.focus_shift.jollyday.core.HolidayCalendar; +import de.focus_shift.jollyday.core.HolidayType; + +import java.time.Month; +import java.time.Year; + +public interface HolidayCheckerApi { + + interface Holiday { + Between fixed(final String propertyKey, final Month month, final int day); + Between fixed(final String propertyKey, final Month month, final int day, final HolidayType type); + + Between christian(final String propertyKey); + Between christian(final String propertyKey, final HolidayType type); + } + + interface Between { + Between valid(Year from, Year to); + + Holiday and(); + + void check(); + } + + static HolidayCheckerFluent assertHolidays(final HolidayCalendar calendar) { + return new HolidayCheckerFluent(calendar); + } +} diff --git a/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerFluent.java b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerFluent.java new file mode 100644 index 000000000..56991985b --- /dev/null +++ b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/HolidayCheckerFluent.java @@ -0,0 +1,273 @@ +package de.focus_shift.jollyday.tests; + +import de.focus_shift.jollyday.core.Holiday; +import de.focus_shift.jollyday.core.HolidayCalendar; +import de.focus_shift.jollyday.core.HolidayManager; +import de.focus_shift.jollyday.core.HolidayType; +import net.jqwik.api.Arbitraries; +import net.jqwik.time.api.arbitraries.YearArbitrary; +import org.junit.jupiter.api.Assertions; + +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +import static de.focus_shift.jollyday.core.ManagerParameters.create; +import static de.focus_shift.jollyday.tests.HolidayCheckerFluent.Category.BY_DAY; +import static de.focus_shift.jollyday.tests.HolidayCheckerFluent.Category.BY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +public class HolidayCheckerFluent implements HolidayCheckerApi.Holiday, HolidayCheckerApi.Between { + + enum Category { + BY_DAY, + BY_KEY + } + + private final HolidayCalendar calendar; + private String propertyKey; + private Month month; + private int day; + private HolidayType type; + private Category category; + private List validRanges = new ArrayList<>(); + + private final List checks = new ArrayList<>(); + + public HolidayCheckerFluent(HolidayCalendar calendar) { + this.calendar = calendar; + } + + + @Override + public HolidayCheckerApi.Between christian(final String propertyKey) { + return christian(propertyKey, HolidayType.PUBLIC_HOLIDAY); + } + + @Override + public HolidayCheckerApi.Between christian(final String propertyKey, final HolidayType type) { + Objects.requireNonNull(propertyKey, "propertyKey is required"); + Objects.requireNonNull(type, "holiday type is required"); + + this.category = BY_KEY; + this.propertyKey = "christian." + propertyKey; + this.type = type; + + return this; + } + + @Override + public HolidayCheckerApi.Between fixed(final String propertyKey, final Month month, final int day) { + return fixed(propertyKey, month, day, HolidayType.PUBLIC_HOLIDAY); + } + + @Override + public HolidayCheckerApi.Between fixed(final String propertyKey, final Month month, final int day, final HolidayType type) { + + Objects.requireNonNull(propertyKey, "propertyKey is required"); + Objects.requireNonNull(month, "month is required"); + if (day >= 32 || day <= 0) { + throw new IllegalArgumentException("day must be between 1 and 31"); + } + Objects.requireNonNull(type, "holiday type is required"); + + this.category = BY_DAY; + this.propertyKey = propertyKey; + this.month = month; + this.day = day; + this.type = type; + + return this; + } + + @Override + public HolidayCheckerApi.Between valid(final Year from, Year to) { + validRanges.add(new YearRange(from, to)); + return this; + } + + @Override + public HolidayCheckerApi.Holiday and() { + checks.add(new HolidayCheck(calendar, propertyKey, month, day, type, validRanges, category)); + + this.propertyKey = null; + this.month = null; + this.day = 0; + this.type = null; + this.category = null; + this.validRanges = new ArrayList<>(); + + return this; + } + + @Override + public void check() { + checks.add(new HolidayCheck(calendar, propertyKey, month, day, type, validRanges, category)); + + this.propertyKey = null; + this.month = null; + this.day = 0; + this.type = null; + this.category = null; + this.validRanges = new ArrayList<>(); + + for (HolidayCheck check : checks) { + switch (check.category) { + case BY_DAY: + checkByDate(check); + break; + case BY_KEY: + checkByKey(check); + break; + default: + throw new IllegalStateException("Unexpected value: " + check.category); + } + } + + this.checks.clear(); + } + + private void checkByDate(HolidayCheck check) { + for (final YearRange validRange : check.getValidRanges()) { + ((YearArbitrary) Arbitraries.defaultFor(Year.class)) + .between(validRange.getFrom().getValue(), validRange.getTo().getValue()) + .forEachValue(year -> { + final Set holidays = HolidayManager.getInstance(create(check.calendar)).getHolidays(year); + assertThat(holidays) + .isNotEmpty() + .contains(new Holiday(LocalDate.of(year.getValue(), check.getMonth(), check.getDay()), check.getPropertiesKey(), check.getHolidayType())); + } + ); + } + } + + private void checkByKey(HolidayCheck check) { + for (final YearRange validRange : check.getValidRanges()) { + ((YearArbitrary) Arbitraries.defaultFor(Year.class)) + .between(validRange.getFrom().getValue(), validRange.getTo().getValue()) + .forEachValue(year -> { + final Set holidays = HolidayManager.getInstance(create(check.calendar)).getHolidays(year); + assertThat(holidays) + .isNotEmpty() + .filteredOn(holiday -> holiday.getPropertiesKey().equals(check.getPropertiesKey())) + .extracting(Holiday::getType) + .contains(check.getHolidayType()); + } + ); + } + } + + private static final class HolidayCheck { + + private final HolidayCalendar calendar; + private final List validRanges; + private final Month month; + private final int day; + private final String propertiesKey; + private final HolidayType holidayType; + private final Category category; + + HolidayCheck(HolidayCalendar calendar, + String propertiesKey, Month month, int day, HolidayType holidayType, + List validRanges, Category category + ) { + this.calendar = calendar; + this.propertiesKey = propertiesKey; + this.month = month; + this.day = day; + this.holidayType = holidayType; + this.validRanges = validRanges.isEmpty() ? List.of(new YearRange(Year.of(1900), Year.of(2500))) : Collections.unmodifiableList(validRanges); + this.category = category; + } + + public HolidayCalendar getCalendar() { + return calendar; + } + + public List getValidRanges() { + return validRanges; + } + + public Month getMonth() { + return month; + } + + public int getDay() { + return day; + } + + public String getPropertiesKey() { + return propertiesKey; + } + + public HolidayType getHolidayType() { + return holidayType; + } + + public Category getCategory() { + return category; + } + } + + private static class YearRange implements Iterable { + + private final Year from; + private final Year to; + + YearRange(final Year from, final Year to) { + if (from != null && to != null) { + Assertions.assertFalse(from.isAfter(to), "To must be greater than or equal to the from year."); + } + this.from = from; + this.to = to; + } + + public Year getFrom() { + return from; + } + + public Year getTo() { + return to; + } + + @Override + public Iterator iterator() { + return new YearRangeIterator(from, to); + } + + private static final class YearRangeIterator implements Iterator { + + private final Year endYear; + private Year cursor; + + YearRangeIterator(final Year startYear, final Year endYear) { + this.cursor = startYear; + this.endYear = endYear; + } + + @Override + public boolean hasNext() { + return cursor.isBefore(endYear) || cursor.equals(endYear); + } + + @Override + public Year next() { + + if (cursor.isAfter(endYear)) { + throw new NoSuchElementException("next year is after endYear which is not in range anymore."); + } + + final Year current = cursor; + cursor = cursor.plusYears(1); + return current; + } + } + } +} diff --git a/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/country/HolidayATTest.java b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/country/HolidayATTest.java index 1077b507f..e52e5c3af 100644 --- a/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/country/HolidayATTest.java +++ b/jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/country/HolidayATTest.java @@ -4,8 +4,8 @@ import static de.focus_shift.jollyday.core.HolidayCalendar.AUSTRIA; import static de.focus_shift.jollyday.core.HolidayType.OBSERVANCE; -import static de.focus_shift.jollyday.tests.HolidayChecker.assertChristian; import static de.focus_shift.jollyday.tests.HolidayChecker.assertFixed; +import static de.focus_shift.jollyday.tests.HolidayCheckerApi.assertHolidays; import static java.time.Month.AUGUST; import static java.time.Month.DECEMBER; import static java.time.Month.JANUARY; @@ -19,22 +19,25 @@ class HolidayATTest { @Test void ensuresHolidays() { - assertFixed(AUSTRIA, JANUARY, 1, "NEW_YEAR"); - assertFixed(AUSTRIA, JANUARY, 6, "EPIPHANY"); - assertFixed(AUSTRIA, MAY, 1, "LABOUR_DAY"); - assertFixed(AUSTRIA, AUGUST, 15, "ASSUMPTION_DAY"); - assertFixed(AUSTRIA, OCTOBER, 26, "NATIONAL_DAY"); - assertFixed(AUSTRIA, NOVEMBER, 1, "ALL_SAINTS"); - assertFixed(AUSTRIA, DECEMBER, 8, "IMMACULATE_CONCEPTION"); - assertFixed(AUSTRIA, DECEMBER, 24, "CHRISTMAS_EVE", OBSERVANCE); - assertFixed(AUSTRIA, DECEMBER, 25, "CHRISTMAS"); - assertFixed(AUSTRIA, DECEMBER, 26, "STEPHENS"); - assertFixed(AUSTRIA, DECEMBER, 31, "NEW_YEARS_EVE", OBSERVANCE); - assertChristian(AUSTRIA, "EASTER"); - assertChristian(AUSTRIA, "EASTER_MONDAY"); - assertChristian(AUSTRIA, "ASCENSION_DAY"); - assertChristian(AUSTRIA, "WHIT_MONDAY"); - assertChristian(AUSTRIA, "CORPUS_CHRISTI"); + + assertHolidays(AUSTRIA) + .fixed("NEW_YEAR", JANUARY, 1).and() + .fixed("EPIPHANY", JANUARY, 6).and() + .fixed("LABOUR_DAY", MAY, 1).and() + .fixed("ASSUMPTION_DAY", AUGUST, 15).and() + .fixed("NATIONAL_DAY", OCTOBER, 26).and() + .fixed("ALL_SAINTS", NOVEMBER, 1).and() + .fixed("IMMACULATE_CONCEPTION", DECEMBER, 8).and() + .fixed("CHRISTMAS_EVE", DECEMBER, 24, OBSERVANCE).and() + .fixed("CHRISTMAS", DECEMBER, 25).and() + .fixed("STEPHENS", DECEMBER, 26).and() + .fixed("NEW_YEARS_EVE", DECEMBER, 31, OBSERVANCE).and() + .christian("EASTER").and() + .christian("EASTER_MONDAY").and() + .christian("ASCENSION_DAY").and() + .christian("WHIT_MONDAY").and() + .christian("CORPUS_CHRISTI") + .check(); assertFixed(AUSTRIA, "1", NOVEMBER, 11, "MARTINS_DAY"); assertFixed(AUSTRIA, "2", MARCH, 19, "JOSEFS_DAY");