FAILURE MAP
← Case archive

FA-68446 / Toll and parking fee computation / Open access

Time-band rate with calendar day cap: days are counted as 24 hour periods from entry · case 01

Time-band rate with calendar day cap returns a wrong result when days are counted as 24 hour periods from entry.

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

ROOT CAUSE

All days are merged, so band minutes of different days are pooled and capped together.

VERIFIED REPAIR

Restore the calendar day key rule so that the step reads `day, tod = divmod(t, 1440)`.

Unsuccessful approach: Periods measured from entry are not the calendar days the cap is defined on.

Case contract

Input {bands: [[start minute of day, cents per quarter hour]] sorted with first start 0, entry, exit (absolute minutes), daily_max}. Minutes of the stay are totalled per calendar day and band; each total is rounded up to whole quarter hours and priced at its band rate. Each calendar day's charge is capped at daily_max.

Why this case matters

Fee engines bill customers in integer cents; a wrong boundary, rounding stage or cap scope silently over- or under-charges.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    bands = x['bands']
    per = {}
    t = x['entry']
    while t < x['exit']:
        day, tod = 0, t % 1440
        i = max(k for k in range(len(bands)) if bands[k][0] <= tod)
        nxt = bands[i + 1][0] if i + 1 < len(bands) else 1440
        step = min(x['exit'] - t, nxt - tod)
        key = (day, bands[i][1])
        per[key] = per.get(key, 0) + step
        t += step
    days = {}
    for (day, rate), m in per.items():
        days[day] = days.get(day, 0) + -(-m // 15) * rate
    return sum(min(v, x['daily_max']) for v in days.values())
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [660, 50]], 'entry': 1959, 'exit': 3563, 'daily_max': 4000}, 4050), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [60, 100], [90, 25], [750, 100]], 'entry': 1555, 'exit': 3142, 'daily_max': 2500}, 3200), ({'bands': [[0, 50], [1380, 25]], 'entry': 1598, 'exit': 1707, 'daily_max': 2500}, 400), ({'bands': [[0, 25], [840, 100]], 'entry': 1257, 'exit': 1288, 'daily_max': 4000}, 300), ({'bands': [[0, 25], [930, 25], [1200, 25]], 'entry': 1229, 'exit': 2026, 'daily_max': 1500}, 1375)], [({'bands': [[0, 25], [240, 100], [1260, 25]], 'entry': 1155, 'exit': 1231, 'daily_max': 1500}, 600), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [930, 100], [1080, 50]], 'entry': 8, 'exit': 2696, 'daily_max': 2500}, 5000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [90, 100]], 'entry': 282, 'exit': 3269, 'daily_max': 4000}, 10300), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 100], [930, 50]], 'entry': 1990, 'exit': 4309, 'daily_max': 1500}, 3000), ({'bands': [[0, 25], [330, 25], [960, 50], [1050, 100]], 'entry': 1919, 'exit': 4180, 'daily_max': 4000}, 7325)], [({'bands': [[0, 25], [90, 25], [1170, 25], [1290, 50]], 'entry': 959, 'exit': 1724, 'daily_max': 1500}, 1550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [870, 50], [1410, 25]], 'entry': 363, 'exit': 3077, 'daily_max': 1500}, 3700), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [120, 50], [690, 50], [870, 100]], 'entry': 1709, 'exit': 2157, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [150, 100]], 'entry': 129, 'exit': 1475, 'daily_max': 4000}, 4075), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [780, 100], [1350, 50], [1380, 50]], 'entry': 1088, 'exit': 1522, 'daily_max': 4000}, 2400)], [({'bands': [[0, 50], [720, 100]], 'entry': 521, 'exit': 2910, 'daily_max': 1500}, 3100), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [690, 50]], 'entry': 1801, 'exit': 3960, 'daily_max': 2500}, 4950), ({'bands': [[0, 50], [1050, 25]], 'entry': 575, 'exit': 807, 'daily_max': 2500}, 800), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [510, 25], [1320, 25]], 'entry': 1282, 'exit': 1287, 'daily_max': 2500}, 25), ({'bands': [[0, 50], [690, 25], [810, 25], [1170, 50]], 'entry': 1301, 'exit': 1683, 'daily_max': 2500}, 1350), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [510, 100], [660, 100], [1140, 25]], 'entry': 154, 'exit': 385, 'daily_max': 4000}, 800), ({'bands': [[0, 25], [1290, 25]], 'entry': 370, 'exit': 2375, 'daily_max': 4000}, 3375), ({'bands': [[0, 25], [90, 100], [330, 25], [840, 25]], 'entry': 382, 'exit': 2522, 'daily_max': 1500}, 3000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [870, 100], [1080, 100], [1350, 100]], 'entry': 575, 'exit': 1314, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [270, 100]], 'entry': 1358, 'exit': 2853, 'daily_max': 1500}, 2100), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check('fee oracle' + ' %d' % i, 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
fee oracle 040004000Passed
fee oracle 140004050Failed
fee oracle 245504550Passed
fee oracle 3500500Passed
fee oracle 425003200Failed
fee oracle 5400400Passed
fee oracle 6300300Passed
fee oracle 713501375Failed

SHA-256 / 5d489e2261156a018111a40cd58d24552c7468e284bd17a85ae0bde3e8ae1698

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    bands = x['bands']
    per = {}
    t = x['entry']
    while t < x['exit']:
        day, tod = (t - x['entry']) // 1440, t % 1440
        i = max(k for k in range(len(bands)) if bands[k][0] <= tod)
        nxt = bands[i + 1][0] if i + 1 < len(bands) else 1440
        step = min(x['exit'] - t, nxt - tod)
        key = (day, bands[i][1])
        per[key] = per.get(key, 0) + step
        t += step
    days = {}
    for (day, rate), m in per.items():
        days[day] = days.get(day, 0) + -(-m // 15) * rate
    return sum(min(v, x['daily_max']) for v in days.values())
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [660, 50]], 'entry': 1959, 'exit': 3563, 'daily_max': 4000}, 4050), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [60, 100], [90, 25], [750, 100]], 'entry': 1555, 'exit': 3142, 'daily_max': 2500}, 3200), ({'bands': [[0, 50], [1380, 25]], 'entry': 1598, 'exit': 1707, 'daily_max': 2500}, 400), ({'bands': [[0, 25], [840, 100]], 'entry': 1257, 'exit': 1288, 'daily_max': 4000}, 300), ({'bands': [[0, 25], [930, 25], [1200, 25]], 'entry': 1229, 'exit': 2026, 'daily_max': 1500}, 1375)], [({'bands': [[0, 25], [240, 100], [1260, 25]], 'entry': 1155, 'exit': 1231, 'daily_max': 1500}, 600), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [930, 100], [1080, 50]], 'entry': 8, 'exit': 2696, 'daily_max': 2500}, 5000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [90, 100]], 'entry': 282, 'exit': 3269, 'daily_max': 4000}, 10300), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 100], [930, 50]], 'entry': 1990, 'exit': 4309, 'daily_max': 1500}, 3000), ({'bands': [[0, 25], [330, 25], [960, 50], [1050, 100]], 'entry': 1919, 'exit': 4180, 'daily_max': 4000}, 7325)], [({'bands': [[0, 25], [90, 25], [1170, 25], [1290, 50]], 'entry': 959, 'exit': 1724, 'daily_max': 1500}, 1550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [870, 50], [1410, 25]], 'entry': 363, 'exit': 3077, 'daily_max': 1500}, 3700), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [120, 50], [690, 50], [870, 100]], 'entry': 1709, 'exit': 2157, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [150, 100]], 'entry': 129, 'exit': 1475, 'daily_max': 4000}, 4075), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [780, 100], [1350, 50], [1380, 50]], 'entry': 1088, 'exit': 1522, 'daily_max': 4000}, 2400)], [({'bands': [[0, 50], [720, 100]], 'entry': 521, 'exit': 2910, 'daily_max': 1500}, 3100), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [690, 50]], 'entry': 1801, 'exit': 3960, 'daily_max': 2500}, 4950), ({'bands': [[0, 50], [1050, 25]], 'entry': 575, 'exit': 807, 'daily_max': 2500}, 800), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [510, 25], [1320, 25]], 'entry': 1282, 'exit': 1287, 'daily_max': 2500}, 25), ({'bands': [[0, 50], [690, 25], [810, 25], [1170, 50]], 'entry': 1301, 'exit': 1683, 'daily_max': 2500}, 1350), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [510, 100], [660, 100], [1140, 25]], 'entry': 154, 'exit': 385, 'daily_max': 4000}, 800), ({'bands': [[0, 25], [1290, 25]], 'entry': 370, 'exit': 2375, 'daily_max': 4000}, 3375), ({'bands': [[0, 25], [90, 100], [330, 25], [840, 25]], 'entry': 382, 'exit': 2522, 'daily_max': 1500}, 3000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [870, 100], [1080, 100], [1350, 100]], 'entry': 575, 'exit': 1314, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [270, 100]], 'entry': 1358, 'exit': 2853, 'daily_max': 1500}, 2100), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check('fee oracle' + ' %d' % i, 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
fee oracle 040004000Passed
fee oracle 140504050Passed
fee oracle 245504550Passed
fee oracle 3500500Passed
fee oracle 425003200Failed
fee oracle 5400400Passed
fee oracle 6300300Passed
fee oracle 713501375Failed

SHA-256 / 81b6b5b164800a9aa2afacca24f758aaf0b95a716880a2c3a73aba6c75592bc2

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    bands = x['bands']
    per = {}
    t = x['entry']
    while t < x['exit']:
        day, tod = divmod(t, 1440)
        i = max(k for k in range(len(bands)) if bands[k][0] <= tod)
        nxt = bands[i + 1][0] if i + 1 < len(bands) else 1440
        step = min(x['exit'] - t, nxt - tod)
        key = (day, bands[i][1])
        per[key] = per.get(key, 0) + step
        t += step
    days = {}
    for (day, rate), m in per.items():
        days[day] = days.get(day, 0) + -(-m // 15) * rate
    return sum(min(v, x['daily_max']) for v in days.values())
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [660, 50]], 'entry': 1959, 'exit': 3563, 'daily_max': 4000}, 4050), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [60, 100], [90, 25], [750, 100]], 'entry': 1555, 'exit': 3142, 'daily_max': 2500}, 3200), ({'bands': [[0, 50], [1380, 25]], 'entry': 1598, 'exit': 1707, 'daily_max': 2500}, 400), ({'bands': [[0, 25], [840, 100]], 'entry': 1257, 'exit': 1288, 'daily_max': 4000}, 300), ({'bands': [[0, 25], [930, 25], [1200, 25]], 'entry': 1229, 'exit': 2026, 'daily_max': 1500}, 1375)], [({'bands': [[0, 25], [240, 100], [1260, 25]], 'entry': 1155, 'exit': 1231, 'daily_max': 1500}, 600), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [120, 25], [930, 100], [1080, 50]], 'entry': 8, 'exit': 2696, 'daily_max': 2500}, 5000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 50], [90, 100]], 'entry': 282, 'exit': 3269, 'daily_max': 4000}, 10300), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 100], [930, 50]], 'entry': 1990, 'exit': 4309, 'daily_max': 1500}, 3000), ({'bands': [[0, 25], [330, 25], [960, 50], [1050, 100]], 'entry': 1919, 'exit': 4180, 'daily_max': 4000}, 7325)], [({'bands': [[0, 25], [90, 25], [1170, 25], [1290, 50]], 'entry': 959, 'exit': 1724, 'daily_max': 1500}, 1550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [870, 50], [1410, 25]], 'entry': 363, 'exit': 3077, 'daily_max': 1500}, 3700), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [120, 50], [690, 50], [870, 100]], 'entry': 1709, 'exit': 2157, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [150, 100]], 'entry': 129, 'exit': 1475, 'daily_max': 4000}, 4075), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 50], [780, 100], [1350, 50], [1380, 50]], 'entry': 1088, 'exit': 1522, 'daily_max': 4000}, 2400)], [({'bands': [[0, 50], [720, 100]], 'entry': 521, 'exit': 2910, 'daily_max': 1500}, 3100), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [690, 50]], 'entry': 1801, 'exit': 3960, 'daily_max': 2500}, 4950), ({'bands': [[0, 50], [1050, 25]], 'entry': 575, 'exit': 807, 'daily_max': 2500}, 800), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [510, 25], [1320, 25]], 'entry': 1282, 'exit': 1287, 'daily_max': 2500}, 25), ({'bands': [[0, 50], [690, 25], [810, 25], [1170, 50]], 'entry': 1301, 'exit': 1683, 'daily_max': 2500}, 1350), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 50], [510, 100], [660, 100], [1140, 25]], 'entry': 154, 'exit': 385, 'daily_max': 4000}, 800), ({'bands': [[0, 25], [1290, 25]], 'entry': 370, 'exit': 2375, 'daily_max': 4000}, 3375), ({'bands': [[0, 25], [90, 100], [330, 25], [840, 25]], 'entry': 382, 'exit': 2522, 'daily_max': 1500}, 3000), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [870, 100], [1080, 100], [1350, 100]], 'entry': 575, 'exit': 1314, 'daily_max': 1500}, 1500), ({'bands': [[0, 25], [270, 100]], 'entry': 1358, 'exit': 2853, 'daily_max': 1500}, 2100), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check('fee oracle' + ' %d' % i, 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
fee oracle 040004000Passed
fee oracle 140504050Passed
fee oracle 245504550Passed
fee oracle 3500500Passed
fee oracle 432003200Passed
fee oracle 5400400Passed
fee oracle 6300300Passed
fee oracle 713751375Passed

SHA-256 / c8bdd1e8ac7f1bfaddf86d099d1353826cfb289b58cf8354e5a77e0338db88ab

Verification & scope

A deterministic, bounded toy model with a stipulated contract; it makes no claim of conformance to any agency manual or standard. 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:48:02.235916+00:00.

Case digest / c26e5e08565dcb4433563f69c1646d9d4bdf8b16bfa7b393ea8839a429202b68