Learning Objectives
- Distinguish between adding calendar units (months, years) and adding absolute units (days, seconds) and understand when each is appropriate
- Identify common boundary-crossing bugs in date arithmetic, including end-of-month clamping and year rollover
- Analyze the ambiguity in phrases like "30 days from February 1" and know how to test both interpretations
Duration vs Period: Two Different Things
Date arithmetic comes in two flavors that are often confused:
Duration (absolute): a fixed amount of time — 30 days = 30 × 86400 seconds. The result depends only on the starting instant and the duration.
Period (calendar-relative): "1 month" means advance the month counter by one, keeping the same day if possible, clamping if not. The result depends on the calendar structure.
In Java, Duration is for absolute time; Period is for calendar time. In JavaScript, there is no built-in distinction — adding months manually requires calendar awareness.
// These are NOT the same for February 1:
LocalDate start = LocalDate.of(2024, 2, 1);
LocalDate plusThirtyDays = start.plusDays(30); // March 2, 2024
LocalDate plusOneMonth = start.plusMonths(1); // March 1, 2024
The difference is one day. For a subscription "valid for 1 month from purchase," this distinction determines whether the user gets March 1 or March 2 as their expiry.
The "30 Days from February 1" Ambiguity
Consider a trial period described as "30 days." Starting February 1 in a non-leap year:
- Adding 30 days: March 2 (February has 28 days, so 28 days = March 1, +2 = March 2... wait: 28 days of Feb remains = 27 days remaining; day 28 is March 1; day 30 is March 3)
Let's be precise: Feb 1 + 30 days in 2023 (non-leap):
- Feb has 28 days; Feb 1 + 27 days = Feb 28; + 3 more = March 3
- So
2023-02-01 + 30 days = 2023-03-03
But 2023-02-01 + 1 month = 2023-03-01.
"30 days" and "1 month" differ by 2 days here. The correct interpretation depends on the business rule, and testers should verify which is implemented.
Year-Crossing Arithmetic
Adding days or months that cross a year boundary is straightforward in most libraries but can be tricky in custom implementations:
| Operation | Start | Result |
|---|---|---|
| +35 days | 2024-12-15 | 2025-01-19 |
| +3 months | 2024-11-30 | 2025-02-28 (Feb clamp) |
| +1 year | 2024-02-29 | 2025-02-28 (Feb clamp, non-leap) |
| -1 month | 2024-03-31 | 2024-02-29 (Feb clamp, leap) |
Off-by-One in Duration Calculations
A common bug: when calculating the duration between two dates, should the start day count, the end day count, or both?
- "A 7-day trial starts Monday" — does it expire Sunday night or Monday night?
- "Grace period of 30 days after cancellation" — is the 30th day included?
The off-by-one matters for expiry checks. A system that uses > instead of >= in its expiry comparison lets users through for one extra day; one using >= instead of > cuts them off one day early.
Testing Checklist
- Add 1 month to every month-end date (28th through 31st)
- Add months that cross a year boundary (October + 3 months = January)
- Subtract months from early-year dates (March - 3 months = December previous year)
- Verify "30 days" vs "1 month" if both terms appear in business requirements
- Test adding 1 year to February 29 of a leap year
- Check inclusive/exclusive boundary of duration end dates