FAILURE MAP
← Case archive

FA-291 / Calendar arithmetic / Open access

Adding a calendar month drifts past the target month · case 01

A month offset expressed as thirty days lands on the wrong date, or a blanket day cap loses valid month-end days.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Calendar month movement is replaced with fixed-duration movement or a universal twenty-eight-day cap.

VERIFIED REPAIR

Move the year/month pair by the requested number of months, then clamp the original day to that destination month's actual length.

Unsuccessful approach: Capping every day at twenty-eight avoids invalid dates but truncates thirty-day, thirty-one-day, and leap-February destinations.

Case contract

Given a valid ISO calendar date and an integer month offset, return an ISO date in the destination month, keeping the original day when possible and otherwise clamping once to the last day. Supported results stay within years 1 through 9999.

Why this case matters

Billing dates and calendar schedules need an explicit month-end convention. Calendar movement is not equivalent to adding elapsed days or repeatedly applying a one-month clamp.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
from datetime import date, timedelta
import calendar
N = 1
observations = []
def solve(text, months):
    return (date.fromisoformat(text) + timedelta(days=30 * months)).isoformat()
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
year = 2019 + N
february_day = [29, 28, 28, 28, 29][N - 1]
check('January end clamps to actual February', solve(f'{year}-01-31', 1), f'{year}-02-{february_day}')
check('two-month move clamps only at destination', solve(f'{year}-01-31', 2), f'{year}-03-31')
check('reverse month movement', solve(f'{year}-03-31', -1), f'{year}-02-{february_day}')
check('year rollover with variable offset', solve('2023-12-15', N), f'2024-{N:02d}-15')
check('zero offset preserves month end', solve(f'{year}-07-31', 0), f'{year}-07-31')
check('century divisible by four hundred is leap', solve('2000-01-31', 1), '2000-02-29')
check('other century is not leap', solve('2100-01-31', 1), '2100-02-28')
check('thirty-day destination', solve(f'{year}-03-31', 1), f'{year}-04-30')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
January end clamps to actual February2020-03-012020-02-29Failed
two-month move clamps only at destination2020-03-312020-03-31Passed
reverse month movement2020-03-012020-02-29Failed
year rollover with variable offset2024-01-142024-01-15Failed
zero offset preserves month end2020-07-312020-07-31Passed
century divisible by four hundred is leap2000-03-012000-02-29Failed
other century is not leap2100-03-022100-02-28Failed
thirty-day destination2020-04-302020-04-30Passed

SHA-256 / 3cd3b42c0c60d12bd68836afd3dc504bb8b4c32e25515ef099f83fb0161bcf02

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
from datetime import date, timedelta
import calendar
N = 1
observations = []
def solve(text, months):
    original = date.fromisoformat(text)
    index = original.year * 12 + original.month - 1 + months
    year, month = divmod(index, 12)
    return date(year, month + 1, min(original.day, 28)).isoformat()
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
year = 2019 + N
february_day = [29, 28, 28, 28, 29][N - 1]
check('January end clamps to actual February', solve(f'{year}-01-31', 1), f'{year}-02-{february_day}')
check('two-month move clamps only at destination', solve(f'{year}-01-31', 2), f'{year}-03-31')
check('reverse month movement', solve(f'{year}-03-31', -1), f'{year}-02-{february_day}')
check('year rollover with variable offset', solve('2023-12-15', N), f'2024-{N:02d}-15')
check('zero offset preserves month end', solve(f'{year}-07-31', 0), f'{year}-07-31')
check('century divisible by four hundred is leap', solve('2000-01-31', 1), '2000-02-29')
check('other century is not leap', solve('2100-01-31', 1), '2100-02-28')
check('thirty-day destination', solve(f'{year}-03-31', 1), f'{year}-04-30')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
January end clamps to actual February2020-02-282020-02-29Failed
two-month move clamps only at destination2020-03-282020-03-31Failed
reverse month movement2020-02-282020-02-29Failed
year rollover with variable offset2024-01-152024-01-15Passed
zero offset preserves month end2020-07-282020-07-31Failed
century divisible by four hundred is leap2000-02-282000-02-29Failed
other century is not leap2100-02-282100-02-28Passed
thirty-day destination2020-04-282020-04-30Failed

SHA-256 / 682a36f70635b46063f9848be47ca87c42c4323add4b48c5bbf493361fe5c179

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
from datetime import date, timedelta
import calendar
N = 1
observations = []
def solve(text, months):
    original = date.fromisoformat(text)
    index = original.year * 12 + original.month - 1 + months
    year, month = divmod(index, 12)
    month += 1
    day = min(original.day, calendar.monthrange(year, month)[1])
    return date(year, month, day).isoformat()
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
year = 2019 + N
february_day = [29, 28, 28, 28, 29][N - 1]
check('January end clamps to actual February', solve(f'{year}-01-31', 1), f'{year}-02-{february_day}')
check('two-month move clamps only at destination', solve(f'{year}-01-31', 2), f'{year}-03-31')
check('reverse month movement', solve(f'{year}-03-31', -1), f'{year}-02-{february_day}')
check('year rollover with variable offset', solve('2023-12-15', N), f'2024-{N:02d}-15')
check('zero offset preserves month end', solve(f'{year}-07-31', 0), f'{year}-07-31')
check('century divisible by four hundred is leap', solve('2000-01-31', 1), '2000-02-29')
check('other century is not leap', solve('2100-01-31', 1), '2100-02-28')
check('thirty-day destination', solve(f'{year}-03-31', 1), f'{year}-04-30')
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
January end clamps to actual February2020-02-292020-02-29Passed
two-month move clamps only at destination2020-03-312020-03-31Passed
reverse month movement2020-02-292020-02-29Passed
year rollover with variable offset2024-01-152024-01-15Passed
zero offset preserves month end2020-07-312020-07-31Passed
century divisible by four hundred is leap2000-02-292000-02-29Passed
other century is not leap2100-02-282100-02-28Passed
thirty-day destination2020-04-302020-04-30Passed

SHA-256 / c8d0f3b82b455305b42700e8223ddcd45d3ab4560c5bdc29b86064ba05c25fb9

Verification & scope

This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:36:52.038828+00:00.

Case digest / 7bd6a63044f8d966259a0dd388753a70888ae4567aa48361313c96947d776962