The final month takes what the months before leave, spread over its days; a disposed asset stops on its disposal day. A skipped month is caught up in the month the run is made; a reversed run un-posts its month. New schedules spread by day; equal-months schedules must start on the 1st. Amounts round to the currency's own minor unit. Kani and Lean proofs; an exhaustive search over runs, reversals and disposals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
325 lines
15 KiB
Lean4
325 lines
15 KiB
Lean4
/-!
|
||
# Proration by days: the parts sum to the total, and none is negative,
|
||
for any number of parts
|
||
|
||
Mirrors `crates/ledger-core/src/kernel.rs` `prorated_through` and
|
||
`prorate_by_days` (depreciation, rules.md B-15; daily schedules, B-16).
|
||
The kernel computes, through any day of a span, the elapsed share of the
|
||
span as a fraction (the months held before, each as days held over days in
|
||
the month, plus the days of this one; in the last month, what the others
|
||
leave spread over its days). Here that share is `w / V`: `V` the whole
|
||
span over any common denominator, `w` the elapsed part of it, which the
|
||
walk only ever raises. The charge through it is `cum`: the total times the
|
||
share, rounded half up to a multiple of `unit`, capped at the total, and
|
||
the total itself once the whole span has elapsed (the kernel's last day,
|
||
exact even for a total that isn't a whole number of units).
|
||
|
||
Kani proves the kernel's laws for up to three parts at bounded sizes
|
||
(`prorate_parts_sum_to_total`, `prorate_last_part_absorbs_the_remainder`),
|
||
and each charge within half a unit of its exact share
|
||
(`a_months_charge_is_within_half_a_cent` and its siblings). Here they
|
||
hold for any number of parts and any sizes: `cum` never falls as the
|
||
share grows, never exceeds the total, and ends at it, so the parts
|
||
(differences of `cum` at each part's end) telescope to the total and are
|
||
each at least zero; each charge is within half a unit of its exact share,
|
||
so each part is within one unit of its days times the rate (`cum_near`,
|
||
`part_near`). The last section writes out the kernel's two formulas and
|
||
proves them `cum` (`chargeMid_eq_cum`, `chargeLast_eq_cum`), so these are
|
||
laws of the kernel's own arithmetic, not only of the model.
|
||
-/
|
||
|
||
namespace Wanderbooks.Prorate
|
||
|
||
/-- `num / den` rounded half up to a multiple of `unit`: the kernel's
|
||
`round_to_unit` (its `r >= step - r` is `2r >= step`, which is the floor of
|
||
`num / step + 1/2`). -/
|
||
def roundUnit (num den unit : Nat) : Nat :=
|
||
unit * ((2 * num + den * unit) / (2 * den * unit))
|
||
|
||
/-- The charge of total `t` once `w` of the span's `V` has elapsed. -/
|
||
def cum (t unit V w : Nat) : Nat :=
|
||
if w = V then t else min t (roundUnit (t * w) V unit)
|
||
|
||
/-- Rounding never falls as the amount rounded grows. -/
|
||
theorem roundUnit_mono (den unit : Nat) {a b : Nat} (h : a ≤ b) :
|
||
roundUnit a den unit ≤ roundUnit b den unit := by
|
||
unfold roundUnit
|
||
apply Nat.mul_le_mul_left
|
||
apply Nat.div_le_div_right
|
||
omega
|
||
|
||
/-- The charge never exceeds the total. -/
|
||
theorem cum_le (t unit V w : Nat) : cum t unit V w ≤ t := by
|
||
unfold cum
|
||
split
|
||
· exact Nat.le_refl t
|
||
· exact Nat.min_le_left _ _
|
||
|
||
/-- Once the whole span has elapsed, the charge is the total exactly. -/
|
||
theorem cum_full (t unit V : Nat) : cum t unit V V = t := by
|
||
simp [cum]
|
||
|
||
/-- Before anything has elapsed, nothing is charged. -/
|
||
theorem cum_zero (t unit V : Nat) (hV : 0 < V) (hu : 0 < unit) : cum t unit V 0 = 0 := by
|
||
have hne : (0 : Nat) ≠ V := by omega
|
||
have hlt : V * unit < 2 * V * unit := by
|
||
have : 0 < V * unit := Nat.mul_pos hV hu
|
||
rw [Nat.mul_assoc]
|
||
omega
|
||
simp [cum, hne, roundUnit, Nat.div_eq_of_lt hlt]
|
||
|
||
/-- **The charge never falls** as the elapsed share grows, up to the
|
||
whole span. -/
|
||
theorem cum_mono (t unit V : Nat) {w w' : Nat} (h : w ≤ w') (hw' : w' ≤ V) :
|
||
cum t unit V w ≤ cum t unit V w' := by
|
||
by_cases e' : w' = V
|
||
· subst e'
|
||
rw [cum_full]
|
||
exact cum_le t unit w' w
|
||
· have e : w ≠ V := by omega
|
||
simp only [cum, e, e', ite_false]
|
||
have r := roundUnit_mono V unit (Nat.mul_le_mul_left t h)
|
||
omega
|
||
|
||
/-- The parts of a split: at each part's end (`w` its elapsed share), the
|
||
charge through it less the charge before it (the kernel's `out[i]`,
|
||
`prorate_by_days`). -/
|
||
def parts (t unit V : Nat) : Nat → List Nat → List Int
|
||
| _, [] => []
|
||
| prev, w :: ws => ((cum t unit V w : Int) - cum t unit V prev) :: parts t unit V w ws
|
||
|
||
/-- **The parts telescope**: they sum to the charge at the last part's end
|
||
less the charge where they start (the shape of `runPart_telescopes`). -/
|
||
theorem parts_telescope (t unit V : Nat) (prev : Nat) (ws : List Nat) :
|
||
(parts t unit V prev ws).sum = (cum t unit V (ws.getLastD prev) : Int) - cum t unit V prev := by
|
||
induction ws generalizing prev with
|
||
| nil => simp [parts]
|
||
| cons w rest ih =>
|
||
simp only [parts, List.sum_cons, ih]
|
||
cases rest with
|
||
| nil => simp
|
||
| cons _ _ => simp only [List.getLastD_cons]; omega
|
||
|
||
/-- **The parts sum to the total**, for any number of parts: a span
|
||
walked from nothing elapsed to all of it. -/
|
||
theorem prorate_total (t unit V : Nat) (hV : 0 < V) (hu : 0 < unit) (ws : List Nat)
|
||
(hlast : ws.getLastD 0 = V) : (parts t unit V 0 ws).sum = t := by
|
||
rw [parts_telescope, hlast, cum_full, cum_zero t unit V hV hu]
|
||
simp
|
||
|
||
/-- The parts' ends rise: each at least the one before, none past the
|
||
span. -/
|
||
def Rising (V : Nat) : Nat → List Nat → Prop
|
||
| _, [] => True
|
||
| prev, w :: ws => prev ≤ w ∧ w ≤ V ∧ Rising V w ws
|
||
|
||
/-- **No part is negative** when the parts' ends rise, for any number of
|
||
parts. -/
|
||
theorem prorate_nonneg (t unit V : Nat) (prev : Nat) (ws : List Nat) (h : Rising V prev ws) :
|
||
∀ x ∈ parts t unit V prev ws, 0 ≤ x := by
|
||
induction ws generalizing prev with
|
||
| nil => simp [parts]
|
||
| cons w rest ih =>
|
||
obtain ⟨hpw, hwV, hrest⟩ := h
|
||
intro x hx
|
||
simp only [parts, List.mem_cons] at hx
|
||
rcases hx with hx | hx
|
||
· subst hx
|
||
have := cum_mono t unit V hpw hwV
|
||
omega
|
||
· exact ih w hrest x hx
|
||
|
||
/-- **Each part is within one unit of its exact share** when each charge
|
||
is within half a unit of its exact one (Kani's
|
||
`a_months_charge_is_within_half_a_cent` and its three siblings prove
|
||
that of the kernel): a part is the difference of two charges over the same
|
||
denominator `D` (W, the span in MONTH_L-ths of a month), so its error is
|
||
at most the two halves. With `x - y` = total × days × MONTH_L / days in
|
||
the month and `D` = months × MONTH_L, dividing through by MONTH_L / days in
|
||
the month gives the plan's form: |part × months × days in month − total ×
|
||
days| <= unit × months × days in month. -/
|
||
theorem part_within_a_unit (a b D x y u : Int)
|
||
(ha : -(u * D) ≤ 2 * (a * D - x) ∧ 2 * (a * D - x) ≤ u * D)
|
||
(hb : -(u * D) ≤ 2 * (b * D - y) ∧ 2 * (b * D - y) ≤ u * D) :
|
||
-(u * D) ≤ (a - b) * D - (x - y) ∧ (a - b) * D - (x - y) ≤ u * D := by
|
||
rw [Int.sub_mul]
|
||
omega
|
||
|
||
/-! ## The kernel's own formula
|
||
|
||
`prorated_through` charges in two ways. Through a part but the span's
|
||
last, the elapsed share is `e / W` (`W` = months × MONTH_L, `e` = before
|
||
+ k × MONTH_L / days in the month): `chargeMid`. Through `k` of the `d`
|
||
days of the last part, after `b` of `W`, it is `(b·d + (W − b)·k) / (W·d)`,
|
||
and the total itself on its last day: `chargeLast`. Both are `cum`, over
|
||
`W` and over `W·d` (`chargeMid_eq_cum`, `chargeLast_eq_cum`), and the two
|
||
meet where the last month starts (`chargeLast_start`, rounding being
|
||
unchanged when the amount and its denominator are scaled together). So
|
||
`cum`'s laws are the kernel's, at any size: through the last month the
|
||
charge never falls (`chargeLast_mono`), the split of any number of parts
|
||
sums to the total with none negative (`kernel_parts_total`,
|
||
`kernel_parts_nonneg`), and each part but the last is within one unit of
|
||
its days times the rate (`part_near`). A calendar walk keeps every part's
|
||
end short of the span's (rules.md B-15: the first month is at most one,
|
||
then whole months), which is the hypothesis `e < W`. -/
|
||
|
||
/-- **Rounding is within half a unit**, at any size (a tie goes up):
|
||
`2·den·r ≤ 2·num + den·unit` and `2·num < 2·den·r + den·unit`. -/
|
||
theorem roundUnit_near (num den unit : Nat) (hd : 0 < den) (hu : 0 < unit) :
|
||
2 * den * roundUnit num den unit ≤ 2 * num + den * unit ∧
|
||
2 * num < 2 * den * roundUnit num den unit + den * unit := by
|
||
unfold roundUnit
|
||
have hs : 0 < 2 * den * unit := Nat.mul_pos (Nat.mul_pos (by omega) hd) hu
|
||
have hdm := Nat.div_add_mod (2 * num + den * unit) (2 * den * unit)
|
||
have hml := Nat.mod_lt (2 * num + den * unit) hs
|
||
generalize (2 * num + den * unit) / (2 * den * unit) = q at *
|
||
generalize (2 * num + den * unit) % (2 * den * unit) = m at *
|
||
have e : 2 * den * (unit * q) = 2 * den * unit * q := by
|
||
rw [Nat.mul_assoc (2 * den) unit q]
|
||
have hS : 2 * den * unit = 2 * (den * unit) := Nat.mul_assoc 2 den unit
|
||
rw [e]
|
||
generalize 2 * den * unit * q = P at *
|
||
generalize 2 * den * unit = S at *
|
||
generalize den * unit = DU at *
|
||
omega
|
||
|
||
/-- Rounding is unchanged when the amount and its denominator are scaled
|
||
together. -/
|
||
theorem roundUnit_scale (n d u c : Nat) (hc : 0 < c) :
|
||
roundUnit (c * n) (c * d) u = roundUnit n d u := by
|
||
unfold roundUnit
|
||
have h1 : 2 * (c * n) + c * d * u = c * (2 * n + d * u) := by
|
||
rw [Nat.mul_add, Nat.mul_left_comm 2 c n, Nat.mul_assoc c d u]
|
||
have h2 : 2 * (c * d) * u = c * (2 * d * u) := by
|
||
rw [Nat.mul_left_comm 2 c d, Nat.mul_assoc c (2 * d) u]
|
||
rw [h1, h2, Nat.mul_div_mul_left _ _ hc]
|
||
|
||
/-- **The charge is within half a unit of its exact share** `t·w / V`, at
|
||
any size, short of the span's end (where it is the total): the cap at the
|
||
total included. -/
|
||
theorem cum_near (t u V w : Nat) (hV : 0 < V) (hu : 0 < u) (hw : w < V) :
|
||
2 * V * cum t u V w ≤ 2 * (t * w) + V * u ∧
|
||
2 * (t * w) < 2 * V * cum t u V w + V * u := by
|
||
have hne : w ≠ V := by omega
|
||
simp only [cum, hne, ite_false]
|
||
obtain ⟨h1, h2⟩ := roundUnit_near (t * w) V u hV hu
|
||
have htw : t * w ≤ t * V := Nat.mul_le_mul_left t (Nat.le_of_lt hw)
|
||
have hVu : 0 < V * u := Nat.mul_pos hV hu
|
||
by_cases hc : t ≤ roundUnit (t * w) V u
|
||
· rw [Nat.min_eq_left hc]
|
||
have hm : 2 * V * t ≤ 2 * V * roundUnit (t * w) V u := Nat.mul_le_mul_left _ hc
|
||
have e : 2 * V * t = 2 * (t * V) := by rw [Nat.mul_assoc, Nat.mul_comm V t]
|
||
constructor <;> omega
|
||
· rw [Nat.min_eq_right (by omega)]
|
||
exact ⟨h1, h2⟩
|
||
|
||
/-- **Each part is within one unit of its exact share**, at any size: a
|
||
part `c' − c` of the charge (its ends `w ≤ w'` short of the span's end),
|
||
against its exact share `t·(w' − w) / V`:
|
||
`|(c' − c)·V − t·(w' − w)| ≤ u·V`, written without subtraction. Over
|
||
the span `V` = months × MONTH_L and `w' − w` = days × MONTH_L / days in
|
||
the month, this is the plan's "each part equals days × rate within one
|
||
unit" (plan/served.md P3 defect 9). -/
|
||
theorem part_near (t u V w w' : Nat) (hV : 0 < V) (hu : 0 < u) (hw' : w' < V)
|
||
(h : w ≤ w') :
|
||
cum t u V w' * V + t * w ≤ cum t u V w * V + t * w' + u * V ∧
|
||
cum t u V w * V + t * w' ≤ cum t u V w' * V + t * w + u * V := by
|
||
obtain ⟨a1, a2⟩ := cum_near t u V w' hV hu hw'
|
||
obtain ⟨b1, b2⟩ := cum_near t u V w hV hu (by omega)
|
||
have e1 : 2 * V * cum t u V w' = 2 * (cum t u V w' * V) := by
|
||
rw [Nat.mul_assoc, Nat.mul_comm V]
|
||
have e2 : 2 * V * cum t u V w = 2 * (cum t u V w * V) := by
|
||
rw [Nat.mul_assoc, Nat.mul_comm V]
|
||
have e3 : V * u = u * V := Nat.mul_comm V u
|
||
constructor <;> omega
|
||
|
||
/-- **A total in whole units is charged in whole units**: every charge is
|
||
then a multiple of the unit, and so is every part (a difference of two). -/
|
||
theorem cum_dvd (t u V w : Nat) (h : u ∣ t) : u ∣ cum t u V w := by
|
||
unfold cum roundUnit
|
||
split
|
||
· exact h
|
||
· rcases Nat.le_total t (u * ((2 * (t * w) + V * u) / (2 * V * u))) with hc | hc
|
||
· rw [Nat.min_eq_left hc]; exact h
|
||
· rw [Nat.min_eq_right hc]; exact Nat.dvd_mul_right u _
|
||
|
||
/-- The kernel's charge through a part but the span's last
|
||
(`prorated_through`, `last = false`): `e` of the span's `W`. -/
|
||
def chargeMid (t u W e : Nat) : Nat := min t (roundUnit (t * e) W u)
|
||
|
||
/-- The kernel's charge through `k` of the `d` days of the span's last
|
||
part, after `b` of `W` (`prorated_through`, `last = true`). -/
|
||
def chargeLast (t u W b d k : Nat) : Nat :=
|
||
if k = d then t else min t (roundUnit (t * (b * d + (W - b) * k)) (W * d) u)
|
||
|
||
/-- A part but the last, short of the span's end, is charged `cum`. -/
|
||
theorem chargeMid_eq_cum (t u W e : Nat) (h : e < W) : chargeMid t u W e = cum t u W e := by
|
||
have : e ≠ W := by omega
|
||
simp [chargeMid, cum, this]
|
||
|
||
/-- The span's last part is charged `cum` over `W·d`. -/
|
||
theorem chargeLast_eq_cum (t u W b d k : Nat) (hb : b < W) (hk : k ≤ d) :
|
||
chargeLast t u W b d k = cum t u (W * d) (b * d + (W - b) * k) := by
|
||
by_cases hkd : k = d
|
||
· subst hkd
|
||
have e : b * k + (W - b) * k = W * k := by
|
||
rw [← Nat.add_mul, Nat.add_sub_of_le (Nat.le_of_lt hb)]
|
||
simp [chargeLast, cum, e]
|
||
· have hlt : (W - b) * k < (W - b) * d :=
|
||
(Nat.mul_lt_mul_left (by omega : 0 < W - b)).mpr (by omega)
|
||
have e : b * d + (W - b) * d = W * d := by
|
||
rw [← Nat.add_mul, Nat.add_sub_of_le (Nat.le_of_lt hb)]
|
||
have hne : b * d + (W - b) * k ≠ W * d := by omega
|
||
simp [chargeLast, cum, hkd, hne]
|
||
|
||
/-- **The last month starts where the month before it ends**: its charge
|
||
through none of its days is the charge through the end of the part before
|
||
it. -/
|
||
theorem chargeLast_start (t u W b d : Nat) (hd : 0 < d) :
|
||
chargeLast t u W b d 0 = chargeMid t u W b := by
|
||
have hne : (0 : Nat) ≠ d := by omega
|
||
have e1 : t * (b * d + (W - b) * 0) = d * (t * b) := by
|
||
rw [Nat.mul_zero, Nat.add_zero, ← Nat.mul_assoc, Nat.mul_comm (t * b) d]
|
||
have e2 : W * d = d * W := Nat.mul_comm W d
|
||
simp only [chargeLast, chargeMid, hne, ite_false, e1, e2, roundUnit_scale _ _ _ _ hd]
|
||
|
||
/-- **The charge never falls through the span's last month**, at any size:
|
||
for days `k ≤ k'` of it. -/
|
||
theorem chargeLast_mono (t u W b d k k' : Nat) (hb : b < W) (hk : k ≤ k') (hk' : k' ≤ d) :
|
||
chargeLast t u W b d k ≤ chargeLast t u W b d k' := by
|
||
rw [chargeLast_eq_cum t u W b d k hb (by omega), chargeLast_eq_cum t u W b d k' hb hk']
|
||
apply cum_mono
|
||
· exact Nat.add_le_add_left (Nat.mul_le_mul_left _ hk) _
|
||
· have : (W - b) * k' ≤ (W - b) * d := Nat.mul_le_mul_left _ hk'
|
||
have e : b * d + (W - b) * d = W * d := by
|
||
rw [← Nat.add_mul, Nat.add_sub_of_le (Nat.le_of_lt hb)]
|
||
omega
|
||
|
||
/-- The kernel's split (`prorate_by_days`): each part but the last the
|
||
charge at its end (`es`, each short of the span's end, `chargeMid_eq_cum`)
|
||
less the charge before it; the last, the total less the charge at the end
|
||
of the part before it (its last day is charged the total). -/
|
||
def kernelParts (t u W : Nat) (es : List Nat) : List Int :=
|
||
parts t u W 0 es ++ [(t : Int) - cum t u W (es.getLastD 0)]
|
||
|
||
/-- **The kernel's parts sum to the total**, for any number of parts. -/
|
||
theorem kernel_parts_total (t u W : Nat) (hW : 0 < W) (hu : 0 < u) (es : List Nat) :
|
||
(kernelParts t u W es).sum = t := by
|
||
simp only [kernelParts, List.sum_append, parts_telescope, cum_zero t u W hW hu,
|
||
List.sum_cons, List.sum_nil]
|
||
omega
|
||
|
||
/-- **No part of the kernel's split is negative**, for any number of
|
||
parts, when their ends rise within the span. -/
|
||
theorem kernel_parts_nonneg (t u W : Nat) (es : List Nat) (h : Rising W 0 es) :
|
||
∀ x ∈ kernelParts t u W es, 0 ≤ x := by
|
||
intro x hx
|
||
simp only [kernelParts, List.mem_append, List.mem_singleton] at hx
|
||
rcases hx with hx | hx
|
||
· exact prorate_nonneg t u W 0 es h x hx
|
||
· subst hx
|
||
have := cum_le t u W (es.getLastD 0)
|
||
omega
|
||
|
||
end Wanderbooks.Prorate
|