P3: odd cents split the same running way everywhere (defect 17, D-38)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Members' shares of earnings are split cumulatively by largest remainder in the closes, the members' equity statement, the K-1 worksheet and the viewer, so a member's capital on a given day never moves by a cent when its month is later closed. Old closes replay as they were; the first P3 close carries any true-up. Feature members_cumulative. Kani and Lean proofs of the bounded drift; bookspec examples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
44 files changed
+6636
-195
No files matched your search
@@ -171,4 +171,78 @@ proptest! {
|
||||
fn line_range_agrees(cents in arb_cents()) {
|
||||
prop_assert_eq!(cents.in_line_range(), dec(cents).in_line_range());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apportion_cumulative_agrees(
|
||||
parts in prop::collection::vec(arb_cents(), 0..5),
|
||||
rems in prop::collection::vec(0i64..100, 5),
|
||||
given in prop::collection::vec(arb_cents(), 5),
|
||||
short in 0i64..500,
|
||||
unpaired in any::<bool>(),
|
||||
) {
|
||||
let n = parts.len();
|
||||
let whole: i64 = parts.iter().sum::<i64>() + short;
|
||||
// Past both line ranges the two differ by design (i64's is 2^40
|
||||
// units, Decimal's a quadrillion).
|
||||
prop_assume!(whole.unsigned_abs() <= MAX_CENTS as u64);
|
||||
let mut pi = parts.clone();
|
||||
let mut pd: Vec<Decimal> = parts.iter().copied().map(dec).collect();
|
||||
let ri = &rems[..n];
|
||||
let rd: Vec<Decimal> = ri.iter().copied().map(dec).collect();
|
||||
// Sometimes one `given` too many: both refuse.
|
||||
let gi = &given[..(n + usize::from(unpaired)).min(5)];
|
||||
let gd: Vec<Decimal> = gi.iter().copied().map(dec).collect();
|
||||
let oi = kernel::apportion_cumulative(whole, &mut pi, ri, 100, gi);
|
||||
let od = kernel::apportion_cumulative(dec(whole), &mut pd, &rd, dec(100), &gd);
|
||||
prop_assert_eq!(oi.is_some(), od.is_some());
|
||||
prop_assert_eq!(pi.into_iter().map(dec).collect::<Vec<_>>(), pd);
|
||||
}
|
||||
|
||||
/// periods::allocate_cumulative, on Decimal shares w_i / W (exact:
|
||||
/// W is 100 or 10,000), against its integer model at cents: floors
|
||||
/// |T|·w_i div W, remainders |T|·w_i mod W, kernel::apportion_cumulative
|
||||
/// at i64, a loss as the negated gain (the model Kani's
|
||||
/// cumulative_close_never_drifts_a_unit_at_* and Lean's allocW prove).
|
||||
#[test]
|
||||
fn allocate_cumulative_agrees_with_the_integer_model(
|
||||
four_places in any::<bool>(),
|
||||
cuts in prop::collection::vec(0i64..=10_000, 0..3),
|
||||
closed in -10_000_000i64..10_000_000,
|
||||
net in -10_000_000i64..10_000_000,
|
||||
spread in prop::collection::vec(-5_000i64..5_000, 3),
|
||||
) {
|
||||
// Weights summing to W: the gaps between sorted cut points.
|
||||
let big_w: i64 = if four_places { 10_000 } else { 100 };
|
||||
let mut points: Vec<i64> = cuts.iter().map(|c| c % (big_w + 1)).collect();
|
||||
points.push(0);
|
||||
points.push(big_w);
|
||||
points.sort_unstable();
|
||||
let weights: Vec<i64> = points.windows(2).map(|w| w[1] - w[0]).filter(|&w| w > 0).collect();
|
||||
let n = weights.len();
|
||||
let scale = if four_places { 4 } else { 2 };
|
||||
let shares: Vec<Decimal> = weights.iter().map(|&w| Decimal::new(w, scale)).collect();
|
||||
// What earlier closes gave: anything summing to what they closed.
|
||||
let mut given: Vec<i64> = spread[..n - 1].to_vec();
|
||||
given.push(closed - given.iter().sum::<i64>());
|
||||
let total = closed + net;
|
||||
let got = ledger_core::periods::allocate_cumulative(
|
||||
dec(total),
|
||||
&shares,
|
||||
&given.iter().copied().map(dec).collect::<Vec<_>>(),
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
let magnitude = total.abs();
|
||||
let mut parts: Vec<i64> = weights.iter().map(|w| magnitude * w / big_w).collect();
|
||||
let rems: Vec<i64> = weights.iter().map(|w| magnitude * w % big_w).collect();
|
||||
let against: Vec<i64> = given.iter().map(|&g| if total < 0 { -g } else { g }).collect();
|
||||
kernel::apportion_cumulative(magnitude, &mut parts, &rems, 1, &against).unwrap();
|
||||
let want: Vec<Decimal> = parts.iter().map(|&p| dec(if total < 0 { -p } else { p })).collect();
|
||||
prop_assert_eq!(&got, &want);
|
||||
prop_assert_eq!(got.iter().sum::<Decimal>(), dec(net));
|
||||
for ((g, p), w) in given.iter().zip(&got).zip(&weights) {
|
||||
let drift = (dec(*g) + p) * Decimal::from(big_w) - dec(total) * Decimal::from(*w);
|
||||
prop_assert!(drift.abs() < Decimal::from(big_w) / Decimal::from(100), "{drift}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user