Depreciation
P
//! # Depreciation
//!
//! In accounting, depreciation refers to the decreases in the value of a fixed
//! asset during the asset's useful life. When an organization purchases a fixed
//! asset, the purchase expenditure is not recognized as an expense immediately.
//! Instead, the decreases in the asset's value are recognized as expenses over
//! the years during which the asset is used.
//!
//! The following methods are implemented here:
//! - **Straight-line method** — cost spread evenly over the asset's useful life.
//! - **Diminishing balance method** — a fixed percentage is applied each year to
//! the asset's remaining book value.
//! - **Units-of-production method** — depreciation is tied to actual usage
//! (units produced / hours used) rather than time.
//! - **Sum-of-years' digits (SYD)** — an accelerated method that applies a
//! declining fraction to the depreciable cost each year.
//! - **Double-declining balance (DDB)** — the most common accelerated method;
//! uses `rate = 2 / useful_years` and automatically switches to straight-line
//! in the year that method yields a higher charge.
//!
//! Further information: <https://en.wikipedia.org/wiki/Depreciation>
// ─────────────────────────────────────────────────────────────
// Error type
// ─────────────────────────────────────────────────────────────
/// Errors that can occur during a depreciation calculation.
#[derive(Debug, PartialEq)]
pub enum DepreciationError {
/// `useful_years` was supplied as zero.
UsefulYearsZero,
/// `purchase_value` is negative.
NegativePurchaseValue,
/// `purchase_value` is less than `residual_value`.
PurchaseValueLessThanResidual,
/// A rate or percentage argument is out of its valid range.
InvalidRate(String),
/// The sum of units-of-production estimates does not match `total_units`.
UnitsMismatch { expected: f64, got: f64 },
}
impl std::fmt::Display for DepreciationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UsefulYearsZero => write!(f, "Useful years must be at least 1"),
Self::NegativePurchaseValue => {
write!(f, "Purchase value cannot be less than zero")
}
Self::PurchaseValueLessThanResidual => {
write!(f, "Purchase value cannot be less than residual value")
}
Self::InvalidRate(msg) => write!(f, "Invalid rate: {msg}"),
Self::UnitsMismatch { expected, got } => write!(
f,
"Units mismatch: expected {expected} total units, got {got}"
),
}
}
}
impl std::error::Error for DepreciationError {}
// ─────────────────────────────────────────────────────────────
// Shared validation helper
// ─────────────────────────────────────────────────────────────
fn validate_common(
useful_years: u32,
purchase_value: f64,
residual_value: f64,
) -> Result<(), DepreciationError> {
if useful_years == 0 {
return Err(DepreciationError::UsefulYearsZero);
}
if purchase_value < 0.0 {
return Err(DepreciationError::NegativePurchaseValue);
}
if purchase_value < residual_value {
return Err(DepreciationError::PurchaseValueLessThanResidual);
}
Ok(())
}
// ─────────────────────────────────────────────────────────────
// Strategy 1 — Straight-line
// ─────────────────────────────────────────────────────────────
/// Calculates depreciation using the **straight-line** method.
///
/// The depreciable cost is divided equally across every period.
///
/// **Formula:**
/// ```text
/// annual_expense = (purchase_value - residual_value) / useful_years
/// ```
///
/// The last year's expense is adjusted so that accumulated depreciation
/// sums to exactly `purchase_value - residual_value` (avoids floating-point
/// drift).
///
/// # Errors
/// Returns [`DepreciationError`] if any argument is invalid.
///
/// # Examples
/// ```
/// use the_algorithms_rust::financial::straight_line_depreciation;
///
/// let result = straight_line_depreciation(10, 1100.0, 100.0).unwrap();
/// assert!(result.iter().all(|&v| (v - 100.0).abs() < 1e-9));
/// ```
pub fn straight_line_depreciation(
useful_years: u32,
purchase_value: f64,
residual_value: f64,
) -> Result<Vec<f64>, DepreciationError> {
validate_common(useful_years, purchase_value, residual_value)?;
let depreciable_cost = purchase_value - residual_value;
let annual_expense = depreciable_cost / useful_years as f64;
let mut schedule: Vec<f64> = Vec::with_capacity(useful_years as usize);
let mut accumulated = 0.0_f64;
for period in 0..useful_years {
if period == useful_years - 1 {
// Last period absorbs any floating-point remainder
schedule.push(depreciable_cost - accumulated);
} else {
accumulated += annual_expense;
schedule.push(annual_expense);
}
}
Ok(schedule)
}
// ─────────────────────────────────────────────────────────────
// Strategy 2 — Diminishing balance (declining balance)
// ─────────────────────────────────────────────────────────────
/// Calculates depreciation using the **diminishing balance** (declining
/// balance) method.
///
/// A fixed `rate` (0 < rate ≤ 1) is applied each year to the *remaining book
/// value* of the asset. In the final year the entire remaining book value
/// above the residual is written off, ensuring the schedule sums exactly to
/// the depreciable cost.
///
/// **Formula per period:**
/// ```text
/// expense(t) = book_value(t) * rate
/// book_value(t+1) = book_value(t) - expense(t)
/// ```
///
/// A common variant is the **double-declining balance** method, which sets
/// `rate = 2 / useful_years`.
///
/// # Errors
/// Returns [`DepreciationError`] if any argument is invalid or `rate` is not
/// in `(0, 1]`.
///
/// # Examples
/// ```
/// use the_algorithms_rust::financial::diminishing_balance_depreciation;
///
/// // Double-declining balance on a 5-year, $10 000 asset with no salvage value
/// let rate = 2.0 / 5.0;
/// let schedule = diminishing_balance_depreciation(5, 10_000.0, 0.0, rate).unwrap();
/// let total: f64 = schedule.iter().sum();
/// assert!((total - 10_000.0).abs() < 1e-6);
/// ```
pub fn diminishing_balance_depreciation(
useful_years: u32,
purchase_value: f64,
residual_value: f64,
rate: f64,
) -> Result<Vec<f64>, DepreciationError> {
validate_common(useful_years, purchase_value, residual_value)?;
if rate <= 0.0 || rate > 1.0 {
return Err(DepreciationError::InvalidRate(format!(
"Diminishing balance rate must be in (0, 1], got {rate}"
)));
}
let mut schedule: Vec<f64> = Vec::with_capacity(useful_years as usize);
let mut book_value = purchase_value;
for period in 0..useful_years {
if period == useful_years - 1 {
// Final year: write off everything above residual value
schedule.push(book_value - residual_value);
} else {
let expense = book_value * rate;
// Never depreciate below the residual value mid-life
let expense = expense.min(book_value - residual_value);
schedule.push(expense);
book_value -= expense;
}
}
Ok(schedule)
}
// ─────────────────────────────────────────────────────────────
// Strategy 3 — Units-of-production
// ─────────────────────────────────────────────────────────────
/// Calculates depreciation using the **units-of-production** method.
///
/// Depreciation in each period is proportional to the number of units
/// produced (or hours used) in that period relative to the total expected
/// output over the asset's life.
///
/// **Formula per period:**
/// ```text
/// expense(t) = (units_in_period(t) / total_units) * depreciable_cost
/// ```
///
/// `units_per_period` must sum to `total_units` (within a small floating-point
/// tolerance).
///
/// # Errors
/// Returns [`DepreciationError`] if any argument is invalid or the unit
/// totals do not match.
///
/// # Examples
/// ```
/// use the_algorithms_rust::financial::units_of_production_depreciation;
///
/// // 4-year asset; total output = 100 000 units
/// let schedule = units_of_production_depreciation(
/// 1_000.0, 100.0, 100_000.0,
/// &[30_000.0, 25_000.0, 25_000.0, 20_000.0],
/// ).unwrap();
/// let total: f64 = schedule.iter().sum();
/// assert!((total - 900.0).abs() < 1e-6);
/// ```
pub fn units_of_production_depreciation(
purchase_value: f64,
residual_value: f64,
total_units: f64,
units_per_period: &[f64],
) -> Result<Vec<f64>, DepreciationError> {
let useful_years = units_per_period.len() as u32;
validate_common(useful_years, purchase_value, residual_value)?;
if total_units <= 0.0 {
return Err(DepreciationError::InvalidRate(
"total_units must be positive".into(),
));
}
let units_sum: f64 = units_per_period.iter().sum();
if (units_sum - total_units).abs() > 1e-6 * total_units {
return Err(DepreciationError::UnitsMismatch {
expected: total_units,
got: units_sum,
});
}
let depreciable_cost = purchase_value - residual_value;
let depreciation_per_unit = depreciable_cost / total_units;
let mut schedule: Vec<f64> = Vec::with_capacity(units_per_period.len());
let mut accumulated = 0.0_f64;
for (i, &units) in units_per_period.iter().enumerate() {
if i == units_per_period.len() - 1 {
// Final period absorbs floating-point remainder
schedule.push(depreciable_cost - accumulated);
} else {
let expense = units * depreciation_per_unit;
accumulated += expense;
schedule.push(expense);
}
}
Ok(schedule)
}
// ─────────────────────────────────────────────────────────────
// Strategy 4 — Sum-of-years' digits (SYD)
// ─────────────────────────────────────────────────────────────
/// Calculates depreciation using the **sum-of-years' digits (SYD)** method.
///
/// This is an *accelerated* method: higher depreciation is charged in earlier
/// years, tapering off as the asset ages. Each year's expense is computed by
/// multiplying the depreciable cost by a fraction whose numerator is the
/// remaining useful life at the start of the period and whose denominator is
/// the sum of all year numbers (the "sum of digits").
///
/// **Formula:**
/// ```text
/// SYD = n * (n + 1) / 2 where n = useful_years
/// fraction(t) = (n - t + 1) / SYD for period t = 1 … n
/// expense(t) = fraction(t) * depreciable_cost
/// ```
///
/// The schedule always sums exactly to `purchase_value - residual_value`.
///
/// # Errors
/// Returns [`DepreciationError`] if any argument is invalid.
///
/// # Examples
/// ```
/// use the_algorithms_rust::financial::sum_of_years_digits_depreciation;
///
/// // 5-year asset, $10 000 cost, no residual
/// // SYD = 15; fractions: 5/15, 4/15, 3/15, 2/15, 1/15
/// let s = sum_of_years_digits_depreciation(5, 10_000.0, 0.0).unwrap();
/// assert!((s[0] - 10_000.0 * 5.0 / 15.0).abs() < 1e-9);
/// assert!((s[4] - 10_000.0 * 1.0 / 15.0).abs() < 1e-9);
/// let total: f64 = s.iter().sum();
/// assert!((total - 10_000.0).abs() < 1e-9);
/// ```
pub fn sum_of_years_digits_depreciation(
useful_years: u32,
purchase_value: f64,
residual_value: f64,
) -> Result<Vec<f64>, DepreciationError> {
validate_common(useful_years, purchase_value, residual_value)?;
let n = useful_years as f64;
// Sum of digits: 1 + 2 + … + n = n(n+1)/2
let syd = n * (n + 1.0) / 2.0;
let depreciable_cost = purchase_value - residual_value;
let mut schedule: Vec<f64> = Vec::with_capacity(useful_years as usize);
let mut accumulated = 0.0_f64;
for period in 1..=useful_years {
let remaining_life = (useful_years - period + 1) as f64;
if period == useful_years {
// Last period absorbs any floating-point remainder
schedule.push(depreciable_cost - accumulated);
} else {
let expense = (remaining_life / syd) * depreciable_cost;
accumulated += expense;
schedule.push(expense);
}
}
Ok(schedule)
}
// ─────────────────────────────────────────────────────────────
// Strategy 5 — Double-declining balance (DDB)
// ─────────────────────────────────────────────────────────────
/// Calculates depreciation using the **double-declining balance (DDB)** method.
///
/// DDB is the most widely used form of the diminishing balance method. It
/// applies a rate of `2 / useful_years` to the current book value each period.
///
/// A key accounting rule is applied automatically: **in any year where the
/// straight-line charge on the remaining book value would exceed the DDB
/// charge, the method switches to straight-line** for that year and all
/// subsequent years. This ensures the book value reaches the residual value
/// by the end of the asset's life without requiring a large write-off in the
/// final year.
///
/// **Formula:**
/// ```text
/// rate = 2 / useful_years
/// ddb_expense(t) = book_value(t) * rate
/// sl_expense(t) = (book_value(t) - residual_value) / remaining_years(t)
/// expense(t) = max(ddb_expense(t), sl_expense(t))
/// ```
///
/// # Errors
/// Returns [`DepreciationError`] if any argument is invalid.
///
/// # Examples
/// ```
/// use the_algorithms_rust::financial::double_declining_balance_depreciation;
///
/// // Classic textbook example: 5-year, $10 000, $1 000 residual
/// let s = double_declining_balance_depreciation(5, 10_000.0, 1_000.0).unwrap();
/// let total: f64 = s.iter().sum();
/// assert!((total - 9_000.0).abs() < 1e-6);
/// // First year should be higher than straight-line ($1 800 vs $4 000)
/// assert!(s[0] > s[4]);
/// ```
pub fn double_declining_balance_depreciation(
useful_years: u32,
purchase_value: f64,
residual_value: f64,
) -> Result<Vec<f64>, DepreciationError> {
validate_common(useful_years, purchase_value, residual_value)?;
let rate = 2.0 / useful_years as f64;
let depreciable_cost = purchase_value - residual_value;
let mut schedule: Vec<f64> = Vec::with_capacity(useful_years as usize);
let mut book_value = purchase_value;
let mut accumulated = 0.0_f64;
for period in 1..=useful_years {
let remaining_years = (useful_years - period + 1) as f64;
// DDB charge for this period (never below zero)
let ddb_expense = (book_value * rate).max(0.0);
// Straight-line charge on remaining depreciable cost
let sl_expense = ((book_value - residual_value) / remaining_years).max(0.0);
// Switch to straight-line if it gives a larger charge
let expense = ddb_expense.max(sl_expense);
// Cap so we never depreciate below residual value
let expense = expense.min(book_value - residual_value);
if period == useful_years {
// Final period: write off exactly what remains above residual
schedule.push(depreciable_cost - accumulated);
} else {
accumulated += expense;
schedule.push(expense);
book_value -= expense;
}
}
Ok(schedule)
}
// ─────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// ── Straight-line ────────────────────────────────────────
#[test]
fn sl_basic() {
let s = straight_line_depreciation(10, 1100.0, 100.0).unwrap();
assert_eq!(s.len(), 10);
assert!(s.iter().all(|&v| (v - 100.0).abs() < 1e-9));
}
#[test]
fn sl_six_years() {
let s = straight_line_depreciation(6, 1250.0, 50.0).unwrap();
assert!(s.iter().all(|&v| (v - 200.0).abs() < 1e-9));
}
#[test]
fn sl_no_residual() {
let s = straight_line_depreciation(4, 1001.0, 0.0).unwrap();
assert!(s.iter().all(|&v| (v - 250.25).abs() < 1e-9));
}
#[test]
fn sl_sum_equals_depreciable_cost() {
let purchase = 7_500.0_f64;
let residual = 500.0_f64;
let s = straight_line_depreciation(7, purchase, residual).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-9);
}
#[test]
fn sl_single_year() {
let s = straight_line_depreciation(1, 4985.0, 100.0).unwrap();
assert_eq!(s.len(), 1);
assert!((s[0] - 4885.0).abs() < 1e-9);
}
#[test]
fn sl_err_zero_years() {
assert_eq!(
straight_line_depreciation(0, 1000.0, 0.0),
Err(DepreciationError::UsefulYearsZero)
);
}
#[test]
fn sl_err_negative_purchase() {
assert_eq!(
straight_line_depreciation(5, -500.0, 0.0),
Err(DepreciationError::NegativePurchaseValue)
);
}
#[test]
fn sl_err_residual_exceeds_purchase() {
assert_eq!(
straight_line_depreciation(5, 100.0, 200.0),
Err(DepreciationError::PurchaseValueLessThanResidual)
);
}
// ── Diminishing balance ──────────────────────────────────
#[test]
fn db_sum_equals_depreciable_cost() {
let purchase = 10_000.0_f64;
let residual = 0.0_f64;
let rate = 2.0 / 5.0; // double-declining, 5-year
let s = diminishing_balance_depreciation(5, purchase, residual, rate).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-6);
}
#[test]
fn db_never_below_residual() {
let purchase = 5_000.0_f64;
let residual = 500.0_f64;
let s = diminishing_balance_depreciation(6, purchase, residual, 0.5).unwrap();
// book value must never drop below residual
let mut book = purchase;
for expense in &s {
book -= expense;
}
assert!(book >= residual - 1e-9);
}
#[test]
fn db_err_rate_zero() {
assert!(matches!(
diminishing_balance_depreciation(5, 1000.0, 0.0, 0.0),
Err(DepreciationError::InvalidRate(_))
));
}
#[test]
fn db_err_rate_above_one() {
assert!(matches!(
diminishing_balance_depreciation(5, 1000.0, 0.0, 1.1),
Err(DepreciationError::InvalidRate(_))
));
}
// ── Units-of-production ──────────────────────────────────
#[test]
fn up_sum_equals_depreciable_cost() {
let purchase = 1_000.0_f64;
let residual = 100.0_f64;
let total_units = 100_000.0_f64;
let periods = vec![30_000.0, 25_000.0, 25_000.0, 20_000.0];
let s =
units_of_production_depreciation(purchase, residual, total_units, &periods).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-9);
}
#[test]
fn up_proportional_expenses() {
let purchase = 10_000.0_f64;
let residual = 0.0_f64;
let total = 1_000.0_f64;
let periods = vec![250.0, 250.0, 250.0, 250.0];
let s = units_of_production_depreciation(purchase, residual, total, &periods).unwrap();
assert!(s.iter().all(|&v| (v - 2_500.0).abs() < 1e-6));
}
#[test]
fn up_err_units_mismatch() {
assert!(matches!(
units_of_production_depreciation(1000.0, 0.0, 100.0, &[30.0, 40.0, 20.0]),
Err(DepreciationError::UnitsMismatch { .. })
));
}
#[test]
fn up_err_zero_total_units() {
assert!(matches!(
units_of_production_depreciation(1000.0, 0.0, 0.0, &[0.0]),
Err(DepreciationError::InvalidRate(_))
));
}
// ── Sum-of-years' digits ─────────────────────────────────
#[test]
fn syd_sum_equals_depreciable_cost() {
let purchase = 10_000.0_f64;
let residual = 0.0_f64;
let s = sum_of_years_digits_depreciation(5, purchase, residual).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-9);
}
#[test]
fn syd_first_year_fraction() {
// Year 1 fraction = n / SYD = 5/15 for a 5-year asset
let s = sum_of_years_digits_depreciation(5, 10_000.0, 0.0).unwrap();
let expected = 10_000.0 * 5.0 / 15.0;
assert!((s[0] - expected).abs() < 1e-9);
}
#[test]
fn syd_last_year_fraction() {
// Year 5 fraction = 1/15
let s = sum_of_years_digits_depreciation(5, 10_000.0, 0.0).unwrap();
let expected = 10_000.0 * 1.0 / 15.0;
assert!((s[4] - expected).abs() < 1e-9);
}
#[test]
fn syd_decreasing_charges() {
// Each year's charge must be strictly less than the previous
let s = sum_of_years_digits_depreciation(6, 12_000.0, 0.0).unwrap();
for window in s.windows(2) {
assert!(window[0] > window[1]);
}
}
#[test]
fn syd_with_residual() {
let purchase = 5_000.0_f64;
let residual = 500.0_f64;
let s = sum_of_years_digits_depreciation(4, purchase, residual).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-9);
}
#[test]
fn syd_single_year() {
let s = sum_of_years_digits_depreciation(1, 2_000.0, 200.0).unwrap();
assert_eq!(s.len(), 1);
assert!((s[0] - 1_800.0).abs() < 1e-9);
}
#[test]
fn syd_err_zero_years() {
assert_eq!(
sum_of_years_digits_depreciation(0, 1000.0, 0.0),
Err(DepreciationError::UsefulYearsZero)
);
}
// ── Double-declining balance ─────────────────────────────
#[test]
fn ddb_sum_equals_depreciable_cost() {
let purchase = 10_000.0_f64;
let residual = 1_000.0_f64;
let s = double_declining_balance_depreciation(5, purchase, residual).unwrap();
let total: f64 = s.iter().sum();
assert!((total - (purchase - residual)).abs() < 1e-6);
}
#[test]
fn ddb_first_year_is_double_sl() {
// Year-1 DDB charge = 2/n * purchase_value
let s = double_declining_balance_depreciation(5, 10_000.0, 0.0).unwrap();
let expected_first = 10_000.0 * 2.0 / 5.0;
assert!((s[0] - expected_first).abs() < 1e-9);
}
#[test]
fn ddb_never_below_residual() {
let purchase = 8_000.0_f64;
let residual = 800.0_f64;
let s = double_declining_balance_depreciation(6, purchase, residual).unwrap();
let mut book = purchase;
for expense in &s {
book -= expense;
}
assert!(book >= residual - 1e-9);
}
#[test]
fn ddb_switches_to_sl() {
// With residual value, DDB must switch to straight-line at some point;
// the last few charges should be equal (straight-line portion).
let s = double_declining_balance_depreciation(5, 10_000.0, 2_000.0).unwrap();
// At minimum the last two years should not decrease like pure DDB
assert!(s[3] <= s[2] || (s[3] - s[4]).abs() < 1e-6);
}
#[test]
fn ddb_err_zero_years() {
assert_eq!(
double_declining_balance_depreciation(0, 1000.0, 0.0),
Err(DepreciationError::UsefulYearsZero)
);
}
#[test]
fn ddb_err_residual_exceeds_purchase() {
assert_eq!(
double_declining_balance_depreciation(5, 100.0, 200.0),
Err(DepreciationError::PurchaseValueLessThanResidual)
);
}
}