FAILURE MAP
← Case archive

FA-60991 / Bond day-count conventions / Open access

Business day adjustment conventions: month change is detected with a greater-than comparison · case 01

A December date rolling into January is not recognized as leaving the month.

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

ROOT CAUSE

The modified-following test compares month numbers with > instead of !=.

VERIFIED REPAIR

Detect any change of month, including the December to January wrap.

Unsuccessful approach: Special-casing a January result also rolls back dates that started in January.

Case contract

Inputs a date [y,m,d], a convention code and a list of holiday dates. Saturdays, Sundays and holidays are non-business days. F rolls forward, P rolls back, MF rolls forward unless that changes the month in which case it rolls back, MP rolls back unless that changes the month in which case it rolls forward, U leaves the date unadjusted; other codes return "unknown convention". Return the adjusted [y,m,d].

Why this case matters

Bond accrual and pricing systems depend on exact day-count arithmetic; a single-day error changes settlement cash.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(d, conv, holidays):
    D = datetime.date(*d)
    H = {datetime.date(*h) for h in holidays}
    def bad(x):
        return x.weekday() >= 5 or x in H
    def roll(x, step):
        while bad(x):
            x += datetime.timedelta(days=step)
        return x
    if conv == 'F':
        r = roll(D, 1)
    elif conv == 'MF':
        r = roll(D, 1)
        if r.month > D.month:
            r = roll(D, -1)
    elif conv == 'P':
        r = roll(D, -1)
    elif conv == 'MP':
        r = roll(D, -1)
        if r.month != D.month:
            r = roll(D, 1)
    elif conv == 'U':
        r = D
    else:
        return 'unknown convention'
    return [r.year, r.month, r.day]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression month crossing test 1', [[2029, 12, 29], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2024, 12, 31], 'MF', [[2024, 12, 31]]], [2024, 12, 30]], ['partial repair probe 1', [[2023, 1, 1], 'MF', [[2023, 1, 4], [2023, 1, 1]]], [2023, 1, 2]], ['partial repair probe 2', [[2028, 1, 2], 'MF', []], [2028, 1, 3]], ['boundary control 1', [[2024, 12, 28], 'F', []], [2024, 12, 30]], ['boundary control 2', [[2024, 5, 15], 'X', []], 'unknown convention'], ['normal control 1', [[2028, 4, 30], 'MP', [[2028, 5, 3], [2028, 5, 1]]], [2028, 4, 28]], ['normal control 2', [[2022, 10, 28], 'U', [[2022, 10, 25], [2022, 10, 27], [2022, 10, 29]]], [2022, 10, 28]]], [['regression month crossing test 1', [[2023, 12, 31], 'MF', []], [2023, 12, 29]], ['regression month crossing test 2', [[2023, 12, 31], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2027, 1, 2], 'MF', [[2027, 1, 2], [2027, 1, 5], [2027, 1, 5]]], [2027, 1, 4]], ['partial repair probe 2', [[2028, 1, 1], 'MF', [[2028, 1, 4], [2028, 1, 4]]], [2028, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2029, 5, 31], 'MF', [[2029, 6, 1], [2029, 5, 28], [2029, 5, 29]]], [2029, 5, 31]], ['normal control 2', [[2020, 10, 31], 'MP', [[2020, 10, 28], [2020, 10, 31], [2020, 10, 30]]], [2020, 10, 29]]], [['regression month crossing test 1', [[2021, 12, 31], 'MF', [[2021, 12, 31]]], [2021, 12, 30]], ['regression month crossing test 2', [[2023, 12, 30], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2028, 1, 1], 'MF', []], [2028, 1, 3]], ['partial repair probe 2', [[2030, 1, 6], 'MF', []], [2030, 1, 7]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 6, 1], 'MP', []], [2024, 6, 3]], ['normal control 1', [[2029, 11, 2], 'MP', [[2029, 11, 2]]], [2029, 11, 1]], ['normal control 2', [[2021, 9, 30], 'MF', [[2021, 10, 1], [2021, 10, 1]]], [2021, 9, 30]]], [['regression month crossing test 1', [[2026, 12, 31], 'MF', [[2026, 12, 31]]], [2026, 12, 30]], ['regression month crossing test 2', [[2030, 12, 31], 'MF', [[2030, 12, 31]]], [2030, 12, 30]], ['partial repair probe 1', [[2020, 1, 26], 'MF', [[2020, 1, 28]]], [2020, 1, 27]], ['partial repair probe 2', [[2026, 1, 3], 'MF', [[2026, 1, 7]]], [2026, 1, 5]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2027, 7, 22], 'P', [[2027, 7, 25], [2027, 7, 20], [2027, 7, 23]]], [2027, 7, 22]], ['normal control 2', [[2026, 12, 31], 'U', [[2026, 12, 31], [2027, 1, 3]]], [2026, 12, 31]]], [['regression month crossing test 1', [[2029, 12, 31], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2020, 12, 31], 'MF', [[2020, 12, 31]]], [2020, 12, 30]], ['partial repair probe 1', [[2028, 1, 1], 'MF', [[2027, 12, 30]]], [2028, 1, 3]], ['partial repair probe 2', [[2023, 1, 2], 'MF', [[2023, 1, 2], [2022, 12, 31]]], [2023, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['normal control 1', [[2026, 1, 30], 'U', [[2026, 2, 1], [2026, 1, 29], [2026, 2, 2]]], [2026, 1, 30]], ['normal control 2', [[2023, 6, 30], 'MP', [[2023, 7, 4], [2023, 7, 2]]], [2023, 6, 30]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
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
regression month crossing test 1[2030, 1, 1][2029, 12, 28]Failed
regression month crossing test 2[2025, 1, 1][2024, 12, 30]Failed
partial repair probe 1[2023, 1, 2][2023, 1, 2]Passed
partial repair probe 2[2028, 1, 3][2028, 1, 3]Passed
boundary control 1[2024, 12, 30][2024, 12, 30]Passed
boundary control 2unknown conventionunknown conventionPassed
normal control 1[2028, 4, 28][2028, 4, 28]Passed
normal control 2[2022, 10, 28][2022, 10, 28]Passed

SHA-256 / 3f7935177e4fd6f707753a8e33cd6375de33cb2f02ab67038e12ba9668d1e70f

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(d, conv, holidays):
    D = datetime.date(*d)
    H = {datetime.date(*h) for h in holidays}
    def bad(x):
        return x.weekday() >= 5 or x in H
    def roll(x, step):
        while bad(x):
            x += datetime.timedelta(days=step)
        return x
    if conv == 'F':
        r = roll(D, 1)
    elif conv == 'MF':
        r = roll(D, 1)
        if r.month > D.month or r.month == 1:
            r = roll(D, -1)
    elif conv == 'P':
        r = roll(D, -1)
    elif conv == 'MP':
        r = roll(D, -1)
        if r.month != D.month:
            r = roll(D, 1)
    elif conv == 'U':
        r = D
    else:
        return 'unknown convention'
    return [r.year, r.month, r.day]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression month crossing test 1', [[2029, 12, 29], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2024, 12, 31], 'MF', [[2024, 12, 31]]], [2024, 12, 30]], ['partial repair probe 1', [[2023, 1, 1], 'MF', [[2023, 1, 4], [2023, 1, 1]]], [2023, 1, 2]], ['partial repair probe 2', [[2028, 1, 2], 'MF', []], [2028, 1, 3]], ['boundary control 1', [[2024, 12, 28], 'F', []], [2024, 12, 30]], ['boundary control 2', [[2024, 5, 15], 'X', []], 'unknown convention'], ['normal control 1', [[2028, 4, 30], 'MP', [[2028, 5, 3], [2028, 5, 1]]], [2028, 4, 28]], ['normal control 2', [[2022, 10, 28], 'U', [[2022, 10, 25], [2022, 10, 27], [2022, 10, 29]]], [2022, 10, 28]]], [['regression month crossing test 1', [[2023, 12, 31], 'MF', []], [2023, 12, 29]], ['regression month crossing test 2', [[2023, 12, 31], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2027, 1, 2], 'MF', [[2027, 1, 2], [2027, 1, 5], [2027, 1, 5]]], [2027, 1, 4]], ['partial repair probe 2', [[2028, 1, 1], 'MF', [[2028, 1, 4], [2028, 1, 4]]], [2028, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2029, 5, 31], 'MF', [[2029, 6, 1], [2029, 5, 28], [2029, 5, 29]]], [2029, 5, 31]], ['normal control 2', [[2020, 10, 31], 'MP', [[2020, 10, 28], [2020, 10, 31], [2020, 10, 30]]], [2020, 10, 29]]], [['regression month crossing test 1', [[2021, 12, 31], 'MF', [[2021, 12, 31]]], [2021, 12, 30]], ['regression month crossing test 2', [[2023, 12, 30], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2028, 1, 1], 'MF', []], [2028, 1, 3]], ['partial repair probe 2', [[2030, 1, 6], 'MF', []], [2030, 1, 7]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 6, 1], 'MP', []], [2024, 6, 3]], ['normal control 1', [[2029, 11, 2], 'MP', [[2029, 11, 2]]], [2029, 11, 1]], ['normal control 2', [[2021, 9, 30], 'MF', [[2021, 10, 1], [2021, 10, 1]]], [2021, 9, 30]]], [['regression month crossing test 1', [[2026, 12, 31], 'MF', [[2026, 12, 31]]], [2026, 12, 30]], ['regression month crossing test 2', [[2030, 12, 31], 'MF', [[2030, 12, 31]]], [2030, 12, 30]], ['partial repair probe 1', [[2020, 1, 26], 'MF', [[2020, 1, 28]]], [2020, 1, 27]], ['partial repair probe 2', [[2026, 1, 3], 'MF', [[2026, 1, 7]]], [2026, 1, 5]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2027, 7, 22], 'P', [[2027, 7, 25], [2027, 7, 20], [2027, 7, 23]]], [2027, 7, 22]], ['normal control 2', [[2026, 12, 31], 'U', [[2026, 12, 31], [2027, 1, 3]]], [2026, 12, 31]]], [['regression month crossing test 1', [[2029, 12, 31], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2020, 12, 31], 'MF', [[2020, 12, 31]]], [2020, 12, 30]], ['partial repair probe 1', [[2028, 1, 1], 'MF', [[2027, 12, 30]]], [2028, 1, 3]], ['partial repair probe 2', [[2023, 1, 2], 'MF', [[2023, 1, 2], [2022, 12, 31]]], [2023, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['normal control 1', [[2026, 1, 30], 'U', [[2026, 2, 1], [2026, 1, 29], [2026, 2, 2]]], [2026, 1, 30]], ['normal control 2', [[2023, 6, 30], 'MP', [[2023, 7, 4], [2023, 7, 2]]], [2023, 6, 30]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
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
regression month crossing test 1[2029, 12, 28][2029, 12, 28]Passed
regression month crossing test 2[2024, 12, 30][2024, 12, 30]Passed
partial repair probe 1[2022, 12, 30][2023, 1, 2]Failed
partial repair probe 2[2027, 12, 31][2028, 1, 3]Failed
boundary control 1[2024, 12, 30][2024, 12, 30]Passed
boundary control 2unknown conventionunknown conventionPassed
normal control 1[2028, 4, 28][2028, 4, 28]Passed
normal control 2[2022, 10, 28][2022, 10, 28]Passed

SHA-256 / 61ea36a3c8091fcf2a358437d64bb64ca911f8021a6e7cde312914967245476d

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
from fractions import Fraction
N = 1
observations = []
def solve(d, conv, holidays):
    D = datetime.date(*d)
    H = {datetime.date(*h) for h in holidays}
    def bad(x):
        return x.weekday() >= 5 or x in H
    def roll(x, step):
        while bad(x):
            x += datetime.timedelta(days=step)
        return x
    if conv == 'F':
        r = roll(D, 1)
    elif conv == 'MF':
        r = roll(D, 1)
        if r.month != D.month:
            r = roll(D, -1)
    elif conv == 'P':
        r = roll(D, -1)
    elif conv == 'MP':
        r = roll(D, -1)
        if r.month != D.month:
            r = roll(D, 1)
    elif conv == 'U':
        r = D
    else:
        return 'unknown convention'
    return [r.year, r.month, r.day]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression month crossing test 1', [[2029, 12, 29], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2024, 12, 31], 'MF', [[2024, 12, 31]]], [2024, 12, 30]], ['partial repair probe 1', [[2023, 1, 1], 'MF', [[2023, 1, 4], [2023, 1, 1]]], [2023, 1, 2]], ['partial repair probe 2', [[2028, 1, 2], 'MF', []], [2028, 1, 3]], ['boundary control 1', [[2024, 12, 28], 'F', []], [2024, 12, 30]], ['boundary control 2', [[2024, 5, 15], 'X', []], 'unknown convention'], ['normal control 1', [[2028, 4, 30], 'MP', [[2028, 5, 3], [2028, 5, 1]]], [2028, 4, 28]], ['normal control 2', [[2022, 10, 28], 'U', [[2022, 10, 25], [2022, 10, 27], [2022, 10, 29]]], [2022, 10, 28]]], [['regression month crossing test 1', [[2023, 12, 31], 'MF', []], [2023, 12, 29]], ['regression month crossing test 2', [[2023, 12, 31], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2027, 1, 2], 'MF', [[2027, 1, 2], [2027, 1, 5], [2027, 1, 5]]], [2027, 1, 4]], ['partial repair probe 2', [[2028, 1, 1], 'MF', [[2028, 1, 4], [2028, 1, 4]]], [2028, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2029, 5, 31], 'MF', [[2029, 6, 1], [2029, 5, 28], [2029, 5, 29]]], [2029, 5, 31]], ['normal control 2', [[2020, 10, 31], 'MP', [[2020, 10, 28], [2020, 10, 31], [2020, 10, 30]]], [2020, 10, 29]]], [['regression month crossing test 1', [[2021, 12, 31], 'MF', [[2021, 12, 31]]], [2021, 12, 30]], ['regression month crossing test 2', [[2023, 12, 30], 'MF', [[2023, 12, 31]]], [2023, 12, 29]], ['partial repair probe 1', [[2028, 1, 1], 'MF', []], [2028, 1, 3]], ['partial repair probe 2', [[2030, 1, 6], 'MF', []], [2030, 1, 7]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 6, 1], 'MP', []], [2024, 6, 3]], ['normal control 1', [[2029, 11, 2], 'MP', [[2029, 11, 2]]], [2029, 11, 1]], ['normal control 2', [[2021, 9, 30], 'MF', [[2021, 10, 1], [2021, 10, 1]]], [2021, 9, 30]]], [['regression month crossing test 1', [[2026, 12, 31], 'MF', [[2026, 12, 31]]], [2026, 12, 30]], ['regression month crossing test 2', [[2030, 12, 31], 'MF', [[2030, 12, 31]]], [2030, 12, 30]], ['partial repair probe 1', [[2020, 1, 26], 'MF', [[2020, 1, 28]]], [2020, 1, 27]], ['partial repair probe 2', [[2026, 1, 3], 'MF', [[2026, 1, 7]]], [2026, 1, 5]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 3, 30], 'MF', []], [2024, 3, 29]], ['normal control 1', [[2027, 7, 22], 'P', [[2027, 7, 25], [2027, 7, 20], [2027, 7, 23]]], [2027, 7, 22]], ['normal control 2', [[2026, 12, 31], 'U', [[2026, 12, 31], [2027, 1, 3]]], [2026, 12, 31]]], [['regression month crossing test 1', [[2029, 12, 31], 'MF', [[2029, 12, 31]]], [2029, 12, 28]], ['regression month crossing test 2', [[2020, 12, 31], 'MF', [[2020, 12, 31]]], [2020, 12, 30]], ['partial repair probe 1', [[2028, 1, 1], 'MF', [[2027, 12, 30]]], [2028, 1, 3]], ['partial repair probe 2', [[2023, 1, 2], 'MF', [[2023, 1, 2], [2022, 12, 31]]], [2023, 1, 3]], ['boundary control 1', [[2024, 5, 15], 'X', []], 'unknown convention'], ['boundary control 2', [[2024, 5, 15], 'F', [[2024, 5, 15]]], [2024, 5, 16]], ['normal control 1', [[2026, 1, 30], 'U', [[2026, 2, 1], [2026, 1, 29], [2026, 2, 2]]], [2026, 1, 30]], ['normal control 2', [[2023, 6, 30], 'MP', [[2023, 7, 4], [2023, 7, 2]]], [2023, 6, 30]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
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
regression month crossing test 1[2029, 12, 28][2029, 12, 28]Passed
regression month crossing test 2[2024, 12, 30][2024, 12, 30]Passed
partial repair probe 1[2023, 1, 2][2023, 1, 2]Passed
partial repair probe 2[2028, 1, 3][2028, 1, 3]Passed
boundary control 1[2024, 12, 30][2024, 12, 30]Passed
boundary control 2unknown conventionunknown conventionPassed
normal control 1[2028, 4, 28][2028, 4, 28]Passed
normal control 2[2022, 10, 28][2022, 10, 28]Passed

SHA-256 / d605bbba604c4ad1643684f419061f3e22b3b569ef1a834573eff075b986e06c

Verification & scope

A deterministic toy contract stated explicitly in the contract field; no claim of conformance to any published convention text. 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:46:50.819186+00:00.

Case digest / f222a20410b39fafbd3b3940f53d7a304aa22fd3bdc29918935c078ae51ae5a2