use super::funejson::Value; use super::money_add::subtract_money; use super::money_allocate::allocate; use super::money_amount::{money_from_value, money_to_value, Money}; /// A straight-line depreciation schedule, year by year. /// /// The depreciable amount is split with `money::allocate` instead of being /// divided by the number of years. Dividing leaves 1000.00 over three years as /// 333.33 three times - a penny short - and an asset that never quite finishes /// writing down to its residual value. `allocate` hands out every minor unit, so /// the last closing value equals the residual value exactly, by construction /// rather than by a fudge at the end. /// /// The odd units go to the earliest years, following `allocate`'s largest- /// remainder rule rather than a second rule invented here. /// /// # Panics /// Panics if `years` is below 1, either amount is negative, the residual value /// exceeds the cost, or the two amounts are in different currencies. pub fn straight_line_depreciation( cost: &Money, residual_value: &Money, years: i64, ) -> Vec { if years < 1 { panic!("years must be at least 1, received {}", years); } if cost.minor < 0 { panic!("cost must not be negative"); } if residual_value.minor < 0 { panic!("residual value must not be negative"); } // subtract_money also rejects a cost and a residual value in different // currencies, which would otherwise produce a plausible-looking schedule. let depreciable = subtract_money(cost, residual_value); if depreciable.minor < 0 { panic!("residual value must not exceed cost"); } let ratios = vec![1i64; years as usize]; let charges = allocate(&depreciable, &ratios); let mut schedule: Vec = Vec::with_capacity(years as usize); let mut opening_value = cost.clone(); for (index, charge) in charges.iter().enumerate() { let closing_value = subtract_money(&opening_value, charge); schedule.push(DepreciationPeriod { year: index as i64 + 1, opening_value: opening_value.clone(), charge: charge.clone(), closing_value: closing_value.clone(), }); opening_value = closing_value; } schedule } /// The written-down value after `elapsed_years` complete years. /// /// # Panics /// Panics if `elapsed_years` is outside `0..=years`. pub fn written_down_value( cost: &Money, residual_value: &Money, years: i64, elapsed_years: i64, ) -> Money { let schedule = straight_line_depreciation(cost, residual_value, years); if !(0..=years).contains(&elapsed_years) { panic!( "elapsed_years must be between 0 and {}, received {}", years, elapsed_years ); } if elapsed_years == 0 { cost.clone() } else { schedule[elapsed_years as usize - 1].closing_value.clone() } } pub fn depreciation_period_to_value(period: &DepreciationPeriod) -> Value { Value::obj(vec![ ("year", Value::Int(period.year)), ("openingValue", money_to_value(&period.opening_value)), ("charge", money_to_value(&period.charge)), ("closingValue", money_to_value(&period.closing_value)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. if let Value::Float(f) = args[2] { if f.fract() != 0.0 { panic!("years must be at least 1, received {}", f); } } Value::Arr( straight_line_depreciation( &money_from_value(&args[0]), &money_from_value(&args[1]), args[2].as_i64(), ) .iter() .map(depreciation_period_to_value) .collect(), ) }