split() loses or invents cents

Done

/home/devdi/devswarm/evals/tasks/hard_bill_split/repo · 2026-11-09 17:04:01 UTC · took 43.2s

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

Usage by agent

Cost per agent
planner: $0.0011 planner planner, Cost: $0.0011 $0.0011 coder: $0.0023 coder coder, Cost: $0.0023 $0.0023 tester: $0.0019 tester tester, Cost: $0.0019 $0.0019
Tokens per agent
  • Cache read
  • Cache write
  • Input
  • Output
planner: 8,755 planner planner, Cache read: 5,955 planner, Cache write: 869 planner, Output: 1,923 8,755 coder: 24.1K coder coder, Cache read: 16.3K coder, Cache write: 4,767 coder, Output: 3,026 24.1K tester: 21.7K tester tester, Cache read: 13.8K tester, Cache write: 5,843 tester, Output: 2,107 21.7K
View as table
AgentCostCache readCache writeInputOutput
planner$0.00115,95586981,923
coder$0.002316,3354,767103,026
tester$0.001913,7695,84382,107

Timeline

  1. planner iteration 1 Finished 3 calls · 8,755 tokens · $0.0011 · 12.4s

    Loading…

  2. coder iteration 1 Finished 4 calls · 24.1K tokens · $0.0023 · 15.3s

    Loading…

  3. tester iteration 1 Finished 3 calls · 21.7K tokens · $0.0019 · 14.3s

    Loading…

  4. verify iteration 1 Passed 0.6s
    ...............                                                          [100%]
    15 passed in 0.04s
    

Final diff

diff --git a/money.py b/money.py
index 6c4f693..45998b5 100644
--- a/money.py
+++ b/money.py
@@ -1,3 +1,35 @@
 def split(amount_cents: int, weights: list[int]) -> list[int]:
+    """Split an integer number of cents in proportion to integer weights.
+
+    The shares always sum to exactly ``amount_cents``. Each share starts as
+    the rounded-down proportional amount, and leftover cents go one at a time
+    to the largest fractional remainders (ties go to the earlier position).
+    Negative amounts are split as their absolute value and then negated.
+    """
+    if any(weight < 0 for weight in weights):
+        raise ValueError("weights must not be 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)
+
+    bases = []
+    fracs = []
+    for weight in weights:
+        numerator = n * weight
+        bases.append(numerator // total)
+        fracs.append(numerator % total)
+
+    leftover = n - sum(bases)
+    order = sorted(range(len(weights)), key=lambda i: (-fracs[i], i))
+    shares = list(bases)
+    for i in order[:leftover]:
+        shares[i] += 1
+
+    for i, weight in enumerate(weights):
+        if weight == 0:
+            shares[i] = 0
+
+    return [sign * share for share in shares]
diff --git a/test_money.py b/test_money.py
index e667503..f699f68 100644
--- a/test_money.py
+++ b/test_money.py
@@ -7,3 +7,92 @@ 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 split(1, [1, 1]) == [1, 0]
+
+
+def test_negative_amount():
+    assert split(-100, [1, 1, 1]) == [-34, -33, -33]
+
+
+def test_zero_amount():
+    assert split(0, [1, 2]) == [0, 0]
+
+
+def test_zero_weight_gets_zero():
+    assert split(10, [0, 1, 1]) == [0, 5, 5]
+
+
+def test_sum_always_equals_amount():
+    import random
+
+    rng = random.Random(1234)
+    for _ in range(500):
+        amount = rng.randint(0, 10**9)
+        weights = [rng.randint(0, 20) for _ in range(rng.randint(1, 8))]
+        if sum(weights) == 0:
+            continue
+        shares = split(amount, weights)
+        assert sum(shares) == amount
+        assert all(s == 0 for s, w in zip(shares, weights) if w == 0)
+
+
+def test_no_positive_total_raises():
+    import pytest
+
+    with pytest.raises(ValueError):
+        split(100, [])
+    with pytest.raises(ValueError):
+        split(100, [0, 0])
+
+
+def test_negative_weight_raises():
+    import pytest
+
+    with pytest.raises(ValueError):
+        split(100, [2, -1, 1])
+
+
+def test_largest_remainder_beats_earlier_position():
+    # numerators 2, 4, 4 over total 5 -> bases 0, 0, 0; leftover 2 goes to
+    # the two largest remainders (indices 1 and 2), not to index 0.
+    assert split(2, [1, 2, 2]) == [0, 1, 1]
+
+
+def test_three_way_leftover_order():
+    assert split(10, [1, 1, 1]) == [4, 3, 3]
+
+
+def test_large_amount_is_exact():
+    amount = 2**60 + 1
+    shares = split(amount, [1, 2, 3])
+    assert sum(shares) == amount
+    assert all(isinstance(s, int) for s in shares)
+
+
+def test_negative_amount_with_zero_weight():
+    assert split(-10, [0, 1, 1]) == [0, -5, -5]
+
+
+def test_negative_sum_always_equals_amount():
+    import random
+
+    rng = random.Random(99)
+    for _ in range(300):
+        amount = -rng.randint(1, 10**9)
+        weights = [rng.randint(0, 20) for _ in range(rng.randint(1, 8))]
+        if sum(weights) == 0:
+            continue
+        shares = split(amount, weights)
+        assert sum(shares) == amount
+        assert shares == [-s for s in split(-amount, weights)]
+
+
+def test_empty_weights_with_negative_amount_raises():
+    import pytest
+
+    with pytest.raises(ValueError):
+        split(-100, [])