FA-68426 / Toll and parking fee computation / Open access
Time-band rate with calendar day cap: each band segment is rounded up separately · case 01
Time-band rate with calendar day cap returns a wrong result when each band segment is rounded up separately.
ROOT CAUSE
Rounding each band visit separately charges extra quarters when a day has several segments at the same rate.
VERIFIED REPAIR
Restore the quarter-hour rounding stage rule so that the step reads `per.get(key, 0) + step`.
Unsuccessful approach: Rounding each segment to the nearest quarter is still per-segment rounding and can also drop short segments.
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 = 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 // 15) * 15
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, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [810, 100]], 'entry': 779, 'exit': 924, 'daily_max': 2500}, 1000), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [150, 100], [720, 25], [1110, 25]], 'entry': 1780, 'exit': 3334, 'daily_max': 2500}, 4850), ({'bands': [[0, 25], [360, 50], [780, 100]], 'entry': 1138, 'exit': 1792, 'daily_max': 2500}, 2700), ({'bands': [[0, 50], [330, 100], [1410, 25]], 'entry': 1073, 'exit': 1206, 'daily_max': 1500}, 900), ({'bands': [[0, 50], [150, 100], [720, 100]], 'entry': 698, 'exit': 889, 'daily_max': 1500}, 1300)], [({'bands': [[0, 50], [150, 25], [180, 100], [240, 100]], 'entry': 1386, 'exit': 1508, 'daily_max': 2500}, 650), ({'bands': [[0, 50], [120, 25], [690, 25], [840, 50]], 'entry': 664, 'exit': 769, 'daily_max': 4000}, 175), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [270, 25], [690, 50], [930, 100]], 'entry': 1225, 'exit': 1760, 'daily_max': 1500}, 2050), ({'bands': [[0, 25], [1170, 100]], 'entry': 53, 'exit': 384, 'daily_max': 2500}, 575), ({'bands': [[0, 25], [1080, 25]], 'entry': 683, 'exit': 1355, 'daily_max': 1500}, 1125), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [60, 100], [270, 25]], 'entry': 1474, 'exit': 1937, 'daily_max': 2500}, 1825), ({'bands': [[0, 25], [120, 25], [1320, 100]], 'entry': 28, 'exit': 796, 'daily_max': 4000}, 1300), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 50], [840, 50], [900, 50]], 'entry': 582, 'exit': 608, 'daily_max': 1500}, 100), ({'bands': [[0, 25], [780, 50]], 'entry': 916, 'exit': 1921, 'daily_max': 4000}, 2575), ({'bands': [[0, 50], [540, 50], [1140, 100], [1380, 25]], 'entry': 800, 'exit': 1580, 'daily_max': 4000}, 3350), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 50], [360, 25], [750, 100]], 'entry': 1444, 'exit': 1450, 'daily_max': 1500}, 50), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 25]], 'entry': 586, 'exit': 1032, 'daily_max': 1500}, 750), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [390, 100], [1230, 100], [1260, 50]], 'entry': 1138, 'exit': 1253, 'daily_max': 4000}, 800), ({'bands': [[0, 50], [330, 25]], 'entry': 694, 'exit': 727, 'daily_max': 2500}, 75), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 25], [1110, 25]], 'entry': 1079, 'exit': 1217, 'daily_max': 1500}, 250)], [({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [930, 25]], 'entry': 763, 'exit': 1087, 'daily_max': 1500}, 550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 25], [840, 50], [1200, 100]], 'entry': 1615, 'exit': 1987, 'daily_max': 1500}, 625), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [510, 100], [540, 100], [870, 100]], 'entry': 1841, 'exit': 1956, 'daily_max': 2500}, 300), ({'bands': [[0, 50], [540, 25], [1110, 50]], 'entry': 42, 'exit': 1833, 'daily_max': 1500}, 2850), ({'bands': [[0, 50], [870, 25]], 'entry': 1775, 'exit': 2668, 'daily_max': 1500}, 1500)]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| fee oracle 0 | 500 | 500 | Passed |
| fee oracle 1 | 1100 | 1000 | Failed |
| fee oracle 2 | 4000 | 4000 | Passed |
| fee oracle 3 | 4575 | 4550 | Failed |
| fee oracle 4 | 4850 | 4850 | Passed |
| fee oracle 5 | 2700 | 2700 | Passed |
| fee oracle 6 | 900 | 900 | Passed |
| fee oracle 7 | 1400 | 1300 | Failed |
SHA-256 / b796d2f9f79e59692038fceb5d507fba0bd7753466357a5f04758ab0a7ecfe94
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 = 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 + 7) // 15 * 15
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, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [810, 100]], 'entry': 779, 'exit': 924, 'daily_max': 2500}, 1000), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [150, 100], [720, 25], [1110, 25]], 'entry': 1780, 'exit': 3334, 'daily_max': 2500}, 4850), ({'bands': [[0, 25], [360, 50], [780, 100]], 'entry': 1138, 'exit': 1792, 'daily_max': 2500}, 2700), ({'bands': [[0, 50], [330, 100], [1410, 25]], 'entry': 1073, 'exit': 1206, 'daily_max': 1500}, 900), ({'bands': [[0, 50], [150, 100], [720, 100]], 'entry': 698, 'exit': 889, 'daily_max': 1500}, 1300)], [({'bands': [[0, 50], [150, 25], [180, 100], [240, 100]], 'entry': 1386, 'exit': 1508, 'daily_max': 2500}, 650), ({'bands': [[0, 50], [120, 25], [690, 25], [840, 50]], 'entry': 664, 'exit': 769, 'daily_max': 4000}, 175), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [270, 25], [690, 50], [930, 100]], 'entry': 1225, 'exit': 1760, 'daily_max': 1500}, 2050), ({'bands': [[0, 25], [1170, 100]], 'entry': 53, 'exit': 384, 'daily_max': 2500}, 575), ({'bands': [[0, 25], [1080, 25]], 'entry': 683, 'exit': 1355, 'daily_max': 1500}, 1125), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [60, 100], [270, 25]], 'entry': 1474, 'exit': 1937, 'daily_max': 2500}, 1825), ({'bands': [[0, 25], [120, 25], [1320, 100]], 'entry': 28, 'exit': 796, 'daily_max': 4000}, 1300), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 50], [840, 50], [900, 50]], 'entry': 582, 'exit': 608, 'daily_max': 1500}, 100), ({'bands': [[0, 25], [780, 50]], 'entry': 916, 'exit': 1921, 'daily_max': 4000}, 2575), ({'bands': [[0, 50], [540, 50], [1140, 100], [1380, 25]], 'entry': 800, 'exit': 1580, 'daily_max': 4000}, 3350), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 50], [360, 25], [750, 100]], 'entry': 1444, 'exit': 1450, 'daily_max': 1500}, 50), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 25]], 'entry': 586, 'exit': 1032, 'daily_max': 1500}, 750), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [390, 100], [1230, 100], [1260, 50]], 'entry': 1138, 'exit': 1253, 'daily_max': 4000}, 800), ({'bands': [[0, 50], [330, 25]], 'entry': 694, 'exit': 727, 'daily_max': 2500}, 75), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 25], [1110, 25]], 'entry': 1079, 'exit': 1217, 'daily_max': 1500}, 250)], [({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [930, 25]], 'entry': 763, 'exit': 1087, 'daily_max': 1500}, 550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 25], [840, 50], [1200, 100]], 'entry': 1615, 'exit': 1987, 'daily_max': 1500}, 625), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [510, 100], [540, 100], [870, 100]], 'entry': 1841, 'exit': 1956, 'daily_max': 2500}, 300), ({'bands': [[0, 50], [540, 25], [1110, 50]], 'entry': 42, 'exit': 1833, 'daily_max': 1500}, 2850), ({'bands': [[0, 50], [870, 25]], 'entry': 1775, 'exit': 2668, 'daily_max': 1500}, 1500)]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| fee oracle 0 | 500 | 500 | Passed |
| fee oracle 1 | 1000 | 1000 | Passed |
| fee oracle 2 | 4000 | 4000 | Passed |
| fee oracle 3 | 4550 | 4550 | Passed |
| fee oracle 4 | 4750 | 4850 | Failed |
| fee oracle 5 | 2575 | 2700 | Failed |
| fee oracle 6 | 900 | 900 | Passed |
| fee oracle 7 | 1200 | 1300 | Failed |
SHA-256 / de0af27895a1379e06d32021013d9e814bd15cb395da6ecfe809c6ef4428babc
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, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [810, 100]], 'entry': 779, 'exit': 924, 'daily_max': 2500}, 1000), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [150, 100], [720, 25], [1110, 25]], 'entry': 1780, 'exit': 3334, 'daily_max': 2500}, 4850), ({'bands': [[0, 25], [360, 50], [780, 100]], 'entry': 1138, 'exit': 1792, 'daily_max': 2500}, 2700), ({'bands': [[0, 50], [330, 100], [1410, 25]], 'entry': 1073, 'exit': 1206, 'daily_max': 1500}, 900), ({'bands': [[0, 50], [150, 100], [720, 100]], 'entry': 698, 'exit': 889, 'daily_max': 1500}, 1300)], [({'bands': [[0, 50], [150, 25], [180, 100], [240, 100]], 'entry': 1386, 'exit': 1508, 'daily_max': 2500}, 650), ({'bands': [[0, 50], [120, 25], [690, 25], [840, 50]], 'entry': 664, 'exit': 769, 'daily_max': 4000}, 175), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [270, 25], [690, 50], [930, 100]], 'entry': 1225, 'exit': 1760, 'daily_max': 1500}, 2050), ({'bands': [[0, 25], [1170, 100]], 'entry': 53, 'exit': 384, 'daily_max': 2500}, 575), ({'bands': [[0, 25], [1080, 25]], 'entry': 683, 'exit': 1355, 'daily_max': 1500}, 1125), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 25], [60, 100], [270, 25]], 'entry': 1474, 'exit': 1937, 'daily_max': 2500}, 1825), ({'bands': [[0, 25], [120, 25], [1320, 100]], 'entry': 28, 'exit': 796, 'daily_max': 4000}, 1300), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 50], [840, 50], [900, 50]], 'entry': 582, 'exit': 608, 'daily_max': 1500}, 100), ({'bands': [[0, 25], [780, 50]], 'entry': 916, 'exit': 1921, 'daily_max': 4000}, 2575), ({'bands': [[0, 50], [540, 50], [1140, 100], [1380, 25]], 'entry': 800, 'exit': 1580, 'daily_max': 4000}, 3350), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550)], [({'bands': [[0, 50], [360, 25], [750, 100]], 'entry': 1444, 'exit': 1450, 'daily_max': 1500}, 50), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [300, 25]], 'entry': 586, 'exit': 1032, 'daily_max': 1500}, 750), ({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [390, 100], [1230, 100], [1260, 50]], 'entry': 1138, 'exit': 1253, 'daily_max': 4000}, 800), ({'bands': [[0, 50], [330, 25]], 'entry': 694, 'exit': 727, 'daily_max': 2500}, 75), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [570, 25], [1110, 25]], 'entry': 1079, 'exit': 1217, 'daily_max': 1500}, 250)], [({'bands': [[0, 50], [720, 100]], 'entry': 1400, 'exit': 1500, 'daily_max': 1500}, 500), ({'bands': [[0, 25], [930, 25]], 'entry': 763, 'exit': 1087, 'daily_max': 1500}, 550), ({'bands': [[0, 25], [420, 100], [1080, 50]], 'entry': 400, 'exit': 1100, 'daily_max': 4000}, 4000), ({'bands': [[0, 25], [420, 25], [840, 50], [1200, 100]], 'entry': 1615, 'exit': 1987, 'daily_max': 1500}, 625), ({'bands': [[0, 25], [420, 100], [1080, 25]], 'entry': 350, 'exit': 1100, 'daily_max': 9000}, 4550), ({'bands': [[0, 25], [510, 100], [540, 100], [870, 100]], 'entry': 1841, 'exit': 1956, 'daily_max': 2500}, 300), ({'bands': [[0, 50], [540, 25], [1110, 50]], 'entry': 42, 'exit': 1833, 'daily_max': 1500}, 2850), ({'bands': [[0, 50], [870, 25]], 'entry': 1775, 'exit': 2668, 'daily_max': 1500}, 1500)]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| fee oracle 0 | 500 | 500 | Passed |
| fee oracle 1 | 1000 | 1000 | Passed |
| fee oracle 2 | 4000 | 4000 | Passed |
| fee oracle 3 | 4550 | 4550 | Passed |
| fee oracle 4 | 4850 | 4850 | Passed |
| fee oracle 5 | 2700 | 2700 | Passed |
| fee oracle 6 | 900 | 900 | Passed |
| fee oracle 7 | 1300 | 1300 | Passed |
SHA-256 / 81d27591d92a9d12c22f403a2b984a961cb9f25d33e708bcebf2e24dcbbb0633
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.144773+00:00.
Case digest / cc7eaa9a797ba7d4c94742ebdd1d5d2d75fe29b4bceac6e82ff3adf617bb2c8c