Repository: focus-shift/jollyday
Pull request #803 (2025-07-08 09:54:57)

Improve relative to weekday in month parser code
<!--

Thanks for contributing.
Please review the following notes before submitting you pull request.

Please look for other issues or pull requests which already work on this topic. Is somebody already on it? Do you need to synchronize?

# Security Vulnerabilities

🛑 STOP! 🛑 If your contribution fixes a security vulnerability, please do not submit it.
Instead, please write an E-Mail to to one of the maintainer with all the information
to recreate the security vulnerability.

# Describing Your Changes

If, having reviewed the notes above, you're ready to submit your pull request, please
provide a brief description of the proposed changes.

If they:
 🐞 fix a bug, please describe the broken behaviour and how the changes fix it.
    Please label with 'type: bug' and 'status: new'
    
 🎁 make an enhancement, please describe the new functionality and why you believe it's useful.
    Please label with 'type: enhancement' and 'status: new'
 
If your pull request relates to any existing issues,
please reference them by using the issue number prefixed with #.

-->



Patch:
diff --git a/jollyday-core/src/main/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParser.java b/jollyday-core/src/main/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParser.java
index 1691a26b..2d98ed25 100644
--- a/jollyday-core/src/main/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParser.java
+++ b/jollyday-core/src/main/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParser.java
@@ -6,18 +6,26 @@ import de.focus_shift.jollyday.core.parser.functions.CreateHoliday;
 import de.focus_shift.jollyday.core.parser.functions.FindWeekDayInMonth;
 import de.focus_shift.jollyday.core.parser.predicates.ValidLimitation;
 import de.focus_shift.jollyday.core.spi.Holidays;
-import de.focus_shift.jollyday.core.spi.Relation;
+import de.focus_shift.jollyday.core.spi.RelativeToWeekdayInMonth;
 
 import java.time.LocalDate;
 import java.time.Year;
 import java.util.List;
 
+import static de.focus_shift.jollyday.core.spi.Relation.BEFORE;
 import static java.util.stream.Collectors.toList;
 
 /**
- * <p>
  * RelativeToWeekdayInMonthParser class.
- * </p>
+ * <p>
+ * Resolves the holiday date for a given relative-to-weekday-in-month configuration.
+ * <p>
+ * This method works as follows:
+ * <ul>
+ *   <li>Finds the base date in the month using the weekdayInMonth configuration.</li>
+ *   <li>Calculates the difference in days to the target weekday, considering the direction (BEFORE or AFTER).</li>
+ *   <li>Returns a Holiday for the resulting date.</li>
+ * </ul>
  */
 public class RelativeToWeekdayInMonthParser implements HolidayParser {
 
@@ -25,14 +33,25 @@ public class RelativeToWeekdayInMonthParser implements HolidayParser {
   public List<Holiday> parse(final Year year, final Holidays holidays) {
     return holidays.relativeToWeekdayInMonth().stream()
       .filter(new ValidLimitation(year))
-      .map(rwm -> {
-        LocalDate date = new FindWeekDayInMonth(year).apply(rwm.weekdayInMonth()).plusDays(1);
-        int direction = rwm.when() == Relation.BEFORE ? -1 : 1;
-        while (date.getDayOfWeek() != rwm.weekday()) {
-          date = date.plusDays(direction);
-        }
-        return new CreateHoliday(date).apply(rwm);
-      })
+      .map(relativeToWeekdayInMonth -> resolveHolidayForRelativeToWeekdayInMonth(year, relativeToWeekdayInMonth))
       .collect(toList());
   }
+
+  private Holiday resolveHolidayForRelativeToWeekdayInMonth(final Year year, final RelativeToWeekdayInMonth rwm) {
+    final LocalDate baseDate = new FindWeekDayInMonth(year).apply(rwm.weekdayInMonth());
+
+    final int currentDayValue = baseDate.getDayOfWeek().getValue();
+    final int targetDayValue = rwm.weekday().getValue();
+    final int direction = (rwm.when() == BEFORE ? -1 : 1);
+
+    int daysDifference = targetDayValue - currentDayValue;
+    if (direction < 0 && daysDifference >= 0) {
+      daysDifference -= 7;
+    } else if (direction > 0 && daysDifference <= 0) {
+      daysDifference += 7;
+    }
+
+    final LocalDate resultDate = baseDate.plusDays(daysDifference);
+    return new CreateHoliday(resultDate).apply(rwm);
+  }
 }


Test patch:
diff --git a/jollyday-core/src/test/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParserTest.java b/jollyday-core/src/test/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParserTest.java
index c001aa7b..58dbfbbb 100644
--- a/jollyday-core/src/test/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParserTest.java
+++ b/jollyday-core/src/test/java/de/focus_shift/jollyday/core/parser/impl/RelativeToWeekdayInMonthParserTest.java
@@ -24,6 +24,7 @@ import java.util.List;
 
 import static de.focus_shift.jollyday.core.HolidayType.PUBLIC_HOLIDAY;
 import static de.focus_shift.jollyday.core.spi.Limited.YearCycle.EVERY_YEAR;
+import static de.focus_shift.jollyday.core.spi.Occurrence.FIRST;
 import static de.focus_shift.jollyday.core.spi.Occurrence.LAST;
 import static java.time.DayOfWeek.MONDAY;
 import static java.time.DayOfWeek.TUESDAY;
@@ -53,6 +54,21 @@ class RelativeToWeekdayInMonthParserTest {
     assertThat(calculatedHoliday.get(0).getDate()).isEqualTo(expectedLocalDate);
   }
 
+  @ParameterizedTest
+  @CsvSource({"BEFORE,2025-03-31", "AFTER,2025-04-14"})
+  void ensureThatRelativeToWeekdayInMonthWithRelationBeforeIsValidAndIsOnSameWeekday(final Relation relation, final LocalDate expectedLocalDate) {
+
+    final Year year = Year.of(2025);
+    final FixedWeekdayInMonth fixedWeekdayInMonth = getFixedWeekdayInMonth(APRIL, MONDAY, FIRST);
+    final RelativeToWeekdayInMonth relativeToWeekdayInMonth = getRelativeToWeekdayInMonth(MONDAY, relation, fixedWeekdayInMonth, year, year);
+
+    final RelativeToWeekdayInMonthParser sut = new RelativeToWeekdayInMonthParser();
+    when(holidays.relativeToWeekdayInMonth()).thenReturn(List.of(relativeToWeekdayInMonth));
+
+    final List<Holiday> calculatedHoliday = sut.parse(year, holidays);
+    assertThat(calculatedHoliday.get(0).getDate()).isEqualTo(expectedLocalDate);
+  }
+
   @Nested
   class LimitedTests {