Learning Objectives
- Understand how rule-based recurrence engines generate dates and where they fail
- Identify the most common edge cases in monthly, weekly, and yearly recurrence
- Build a systematic test matrix for recurring event features
The Recurrence Problem
Recurring events seem simple: "every Monday," "every month on the 15th," "every year on January 1st." But when the calendar doesn't cooperate — specifically when a date doesn't exist in a given month — recurrence engines must make a decision, and different systems make different decisions.
The classic example: "every month on the 31st."
| Month | What happens? |
|---|---|
| January 31 | Exists — OK |
| February 31 | Does not exist. Skip? Use Feb 28/29? Use March 3? |
| March 31 | Exists — OK |
| April 31 | Does not exist. Use April 30? Skip? |
There is no universally correct answer — it depends on the business rule. But there is always a wrong answer: crashing with an exception or silently skipping months without telling the user.
Language and Library Behavior
// Java: January 31 + 1 month
LocalDate.of(2024, 1, 31).plusMonths(1);
// Result: February 29, 2024 (leap year) — Java clamps to last valid day
// Java in a non-leap year
LocalDate.of(2023, 1, 31).plusMonths(1);
// Result: February 28, 2023
// Adding another month from Feb 28 (not Feb 31!):
LocalDate.of(2023, 2, 28).plusMonths(1);
// Result: March 28 — NOT March 31!
// This is "month drift" — the 31st became the 28th and never recovers
This "month drift" bug is one of the most insidious in date handling. A subscription created on January 31 should renew on February 28/29, March 31, April 30... but a naive +1 month chain starting from Feb 28 will drift to the 28th permanently.
Testing Recurrence Edge Cases
Monthly recurrence test matrix:
| Start date | +1 month | +2 months | Notes |
|---|---|---|---|
| Jan 28 | Feb 28 | Mar 28 | No clamping needed |
| Jan 29 | Feb 28/29 | Mar 29 | Leap year matters |
| Jan 30 | Feb 28/29 | Mar 30 | Check clamping behavior |
| Jan 31 | Feb 28/29 | Mar 31 | Month drift risk |
| Mar 31 | Apr 30 | May 31 | April clamp, then recovery? |
Weekly recurrence:
- Test DST transition weeks — does "every Monday at 9 AM" shift by an hour?
- Test year boundary — does "every week" generate December 31 and January 1 correctly?
Yearly recurrence:
- Test February 29 — does "every year on Feb 29" generate Feb 28 or Mar 1 in non-leap years?
What to Verify in Your Bug Report
When a recurrence bug is found, document: the start date, the recurrence rule, the expected sequence, the actual sequence, and the first occurrence where they diverge. Include the timezone, as DST gaps can cause "every day at midnight" to skip or repeat a day.