split() loses or invents cents

Done

/home/devdi/devswarm/evals/tasks/hard_bill_split/repo · 2026-11-09 17:03:10 UTC · took 50.4s

  1. Plan (done)
  2. Code (done)
  3. Test (done)
  4. Review (done)
  5. Finish (done)
Cost
$0.0071
Tests
Passed
Iterations
1
Re-plans
0
LLM calls
11
Prompt read from cache
57%

Usage by agent

Cost per agent
planner: $0.0013 planner planner, Cost: $0.0013 $0.0013 coder: $0.0022 coder coder, Cost: $0.0022 $0.0022 tester: $0.0022 tester tester, Cost: $0.0022 $0.0022 reviewer: $0.0014 reviewer reviewer, Cost: $0.0014 $0.0014
Tokens per agent
  • Cache read
  • Cache write
  • Input
  • Output
planner: 8,769 planner planner, Cache read: 4,299 planner, Cache write: 2,531 planner, Output: 1,931 8,769 coder: 22.5K coder coder, Cache read: 13.5K coder, Cache write: 6,482 coder, Output: 2,525 22.5K tester: 21.5K tester tester, Cache read: 11.4K tester, Cache write: 8,021 tester, Output: 2,101 21.5K reviewer: 6,622 reviewer reviewer, Cache write: 5,183 reviewer, Output: 1,435 6,622
View as table
AgentCostCache readCache writeInputOutput
planner$0.00134,2992,53181,931
coder$0.002213,4656,482102,525
tester$0.002211,3628,02182,101
reviewer$0.001405,18341,435

Timeline

  1. planner iteration 1 Finished 3 calls · 8,769 tokens · $0.0013 · 11.6s

    Loading…

  2. coder iteration 1 Finished 4 calls · 22.5K tokens · $0.0022 · 14.5s

    Loading…

  3. tester iteration 1 Finished 3 calls · 21.5K tokens · $0.0022 · 15.1s

    Loading…

  4. verify iteration 1 Passed 0.5s
    ................                                                         [100%]
    16 passed in 0.03s
    
  5. reviewer iteration 1 Finished 1 calls · 6,622 tokens · $0.0014 · 8.0s

    Loading…

Final diff

diff --git a/money.py b/money.py
index 6c4f693..0347da4 100644
--- a/money.py
+++ b/money.py
@@ -1,3 +1,30 @@
 def split(amount_cents: int, weights: list[int]) -> list[int]:
+    """Split an integer number of cents by integer weights.
+
+    Uses the largest-remainder method so the shares always sum to
+    ``amount_cents``. Leftover cents go to the largest fractional remainders,
+    with ties going to the earlier position. Zero-weight shares are always 0.
+    """
+    if any(weight < 0 for weight in weights):
+        raise ValueError("weights must be non-negative")
     total = sum(weights)
-    return [round(amount_cents * weight / total) for weight in weights]
+    if total <= 0:
+        raise ValueError("weights must sum to more than 0")
+
+    sign = -1 if amount_cents < 0 else 1
+    n = abs(amount_cents)
+
+    shares = []
+    remainders = []
+    for weight in weights:
+        floor, rem = divmod(n * weight, total)
+        shares.append(floor)
+        remainders.append(rem)
+
+    leftover = n - sum(shares)
+    order = sorted(range(len(weights)), key=lambda i: (-remainders[i], i))
+    for i in order[:leftover]:
+        if weights[i] > 0:
+            shares[i] += 1
+
+    return [sign * share if share else 0 for share in shares]
diff --git a/test_money.py b/test_money.py
index e667503..1e42712 100644
--- a/test_money.py
+++ b/test_money.py
@@ -7,3 +7,88 @@ def test_even():
 
 def test_weighted():
     assert split(90, [1, 2]) == [30, 60]
+
+
+def test_leftover_cent_goes_to_earlier_position():
+    assert split(100, [1, 1, 1]) == [34, 33, 33]
+    assert sum(split(100, [1, 1, 1])) == 100
+
+
+def test_negative_amount():
+    assert split(-100, [1, 1, 1]) == [-34, -33, -33]
+
+
+def test_zero_weight_gets_zero():
+    assert split(10, [0, 1, 1]) == [0, 5, 5]
+
+
+def test_tie_ordering():
+    assert split(1, [1, 1]) == [1, 0]
+
+
+def test_sum_equals_amount():
+    for amount in range(-50, 51):
+        for weights in ([1], [1, 1], [1, 2, 3], [0, 1, 1], [7, 0, 3, 3], [1, 1, 1]):
+            assert sum(split(amount, weights)) == amount
+
+
+def test_large_amount_exact():
+    amount = 10**18 + 1
+    shares = split(amount, [1, 1, 1])
+    assert sum(shares) == amount
+
+
+def test_zero_total_weight_raises():
+    try:
+        split(5, [0, 0])
+    except ValueError:
+        pass
+    else:
+        raise AssertionError("expected ValueError")
+
+
+def test_empty_weights_raises():
+    try:
+        split(5, [])
+    except ValueError:
+        pass
+    else:
+        raise AssertionError("expected ValueError")
+
+
+def test_largest_remainder_beats_earlier_position():
+    # 1*1/3 -> floor 0 rem 1; 1*2/3 -> floor 0 rem 2; the larger remainder wins.
+    assert split(1, [1, 2]) == [0, 1]
+
+
+def test_zero_weight_earlier_does_not_take_leftover():
+    assert split(1, [0, 1, 1]) == [0, 1, 0]
+
+
+def test_zero_amount():
+    assert split(0, [1, 2]) == [0, 0]
+    assert split(0, [1, 1, 1]) == [0, 0, 0]
+
+
+def test_negative_large_amount_sum():
+    amount = -(10**18 + 1)
+    shares = split(amount, [1, 1, 1])
+    assert sum(shares) == amount
+    assert shares == [-333333333333333334, -333333333333333334, -333333333333333333]
+
+
+def test_exact_large_amount_no_float_error():
+    # 2**53 + 1 cannot be represented as a float; integer arithmetic must keep it exact.
+    amount = 2**53 + 1
+    shares = split(amount, [1, 1])
+    assert sum(shares) == amount
+    assert shares == [2**52 + 1, 2**52]
+
+
+def test_mixed_sign_weights_rejected():
+    try:
+        split(10, [1, -1, 2])
+    except ValueError:
+        pass
+    else:
+        raise AssertionError("expected ValueError")