Learning Objectives
- Understand the distribution of month lengths in the Gregorian calendar and which months create boundary risks
- Identify the specific date arithmetic bugs that occur when adding months crosses a month-end boundary
- Write test cases for systems that perform month-based date calculations
Month Lengths Reference
The Gregorian calendar has months of three different fixed lengths plus one variable:
| Days | Months |
|---|---|
| 31 | January, March, May, July, August, October, December |
| 30 | April, June, September, November |
| 28/29 | February |
A simple mnemonic: odd months (except September and November) have 31 days. The "knuckle rule" — counting months on fists — encodes the same pattern. What matters for testing is not the mnemonic but the consequence: adding "1 month" to a date does not always produce a date in the next month with the same day number.
The March 31 Problem
Consider a subscription that renews every month. If the user subscribes on January 31, when does it renew?
- January 31 + 1 month = February 31 → doesn't exist
Different systems handle this differently:
- Clamp to last day: February 28 (or 29 in leap year)
- Overflow to next month: March 2 (or March 3 in leap year)
- Error: throws an exception or returns null
None of these is universally "correct" — it depends on business rules. But if the system clamps, then the user subscribed on January 31 renews on February 28, then (adding 1 month) on March 28 — not March 31. They permanently lost three days from their subscription cycle.
Another example: 2024-03-31 + 1 month. March has 31 days; April has 30. April 31 doesn't exist. What does the system do?
SQL DATE_ADD Pitfalls
SQL's DATE_ADD function behavior varies by database:
-- MySQL: clamps to last valid day
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH);
-- Result: 2024-02-29 (2024 is a leap year)
-- PostgreSQL: same clamping behavior
SELECT '2024-01-31'::date + INTERVAL '1 month';
-- Result: 2024-02-29
This is usually the least surprising behavior, but it means the day-of-month is not preserved across all months. Systems that assert "renews on the same day each month" will fail in months shorter than the subscription start day.
Critical Test Cases for Month-End Boundaries
| Input date | Operation | Expected | Risk |
|---|---|---|---|
2024-01-31 |
+1 month | Feb 29 (clamp) | What does the system do with Feb 31? |
2024-03-31 |
+1 month | Apr 30 (clamp) | April has only 30 days |
2024-08-31 |
+1 month | Sep 30 (clamp) | September has only 30 days |
2024-02-29 |
+1 year | Feb 28 2025 | Leap day + 1 year |
2024-01-30 |
+1 month | Feb 29 | Edge: within Feb capacity in leap year |
2023-01-30 |
+1 month | Feb 28 | Edge: Feb in non-leap year |
Testing Strategy
When testing month-addition logic, always test:
- Start dates on the 28th, 29th, 30th, and 31st of a month
- Operations that land in February (both leap and non-leap years)
- Operations that land in 30-day months when starting from a 31-day month
- Verify that the system's clamping behavior matches the documented business rule