rust_finprim/amort_dep_tax/
dep.rs

1use crate::amort_dep_tax::DepreciationPeriod;
2use crate::ZERO;
3use rust_decimal::prelude::*;
4
5#[cfg(feature = "std")]
6/// Straight Line Depreciation (SLN)
7///
8/// Calculates the depreciation schedule for an asset using the straight-line method.
9///
10/// # Feature
11/// This function requires the `std` feature to be enabled as it uses the `std::Vec`. `sln_into`
12/// can be used in a `no_std` environment as any allocation is done by the caller.
13///
14/// # Arguments
15/// * `cost` - The initial cost of the asset
16/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
17/// * `life` - The number of periods over which the asset will be depreciated
18///
19///
20/// # Returns
21/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
22///
23/// # Examples
24/// * $10,000 asset, $1,000 salvage value, 5 year life
25/// ```
26/// use rust_finprim::amort_dep_tax::sln;
27/// use rust_decimal_macros::*;
28///
29/// let cost = dec!(10_000);
30/// let salvage = dec!(1_000);
31/// let life = 5;
32/// let schedule = sln(cost, salvage, life);
33/// ```
34pub fn sln(cost: Decimal, salvage: Decimal, life: u32) -> Vec<DepreciationPeriod> {
35    // let depreciation_expense = (cost - salvage) / Decimal::from_u32(life).unwrap();
36    //
37    // let mut periods = Vec::with_capacity(life as usize);
38    // let mut remaining_book_value = cost;
39    // for period in 1..=life {
40    //     remaining_book_value -= depreciation_expense;
41    //     periods.insert(
42    //         period as usize - 1,
43    //         DepreciationPeriod::new(period, depreciation_expense, remaining_book_value),
44    //     );
45    // }
46    let mut periods = vec![DepreciationPeriod::default(); life as usize];
47    sln_into(periods.as_mut_slice(), cost, salvage);
48    periods
49}
50
51/// Straight Line Depreciation (SLN) Into
52///
53/// Calculates the depreciation schedule for an asset using the straight-line method, mutating a
54/// "slice" of `DepreciationPeriod`.
55///
56/// # Arguments
57/// * `slice` - A mutable slice of `DepreciationPeriod` instances to be filled with the depreciation schedule.
58///
59/// **Warning**: The length of the slice should be as long as the life as the asset or there will
60/// be unexpected behavior.
61/// * `cost` - The initial cost of the asset
62/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
63///
64/// # Examples
65/// * $10,000 asset, $1,000 salvage value, 5 year life
66/// ```
67/// use rust_finprim::amort_dep_tax::{DepreciationPeriod, sln_into};
68/// use rust_decimal_macros::*;
69///
70/// let life = 5;
71/// let cost = dec!(10_000);
72/// let salvage = dec!(1_000);
73///
74/// let mut schedule = vec![DepreciationPeriod::default(); life as usize];
75/// sln_into(&mut schedule, cost, salvage);
76/// ```
77pub fn sln_into(slice: &mut [DepreciationPeriod], cost: Decimal, salvage: Decimal) {
78    let life = slice.len() as u32;
79    let depreciation_expense = (cost - salvage) / Decimal::from_u32(life).unwrap();
80
81    let mut remaining_book_value = cost;
82    for (period, item) in slice.iter_mut().enumerate() {
83        remaining_book_value -= depreciation_expense;
84        item.period = period as u32 + 1;
85        item.depreciation_expense = depreciation_expense;
86        item.remaining_book_value = remaining_book_value;
87    }
88}
89
90#[cfg(feature = "std")]
91/// Declining Balance Depreciation (DB)
92///
93/// Calculates the depreciation schedule for an asset using the declining balance method given a
94/// declining balance factor (e.g. double-declining balance).
95///
96/// # Arguments
97/// * `cost` - The initial cost of the assert
98/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
99/// * `life` - The number of periods over which the asset will be depreciated
100/// * `factor` (optional) - The factor by which the straight-line depreciation rate is multiplied (default is 2 for double-declining balance)
101/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding strategy for the amounts `(dp, RoundingStrategy)`,
102/// default is no rounding of calculations. The final depreciation expense is adjusted to ensure the remaining book value is equal to the salvage value.
103/// `rust_decimal::RoundingStrategy::MidpointNearestEven` ("Bankers Rounding") is likely what you are looking for as the rounding strategy.
104///
105/// If rounding is enabled, the final period will be adjusted to "zero" out the remaining book
106/// value to the salvage value.
107///
108/// # Returns
109/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
110///
111/// # Examples
112/// * $10,000 asset, $1,000 salvage value, 5 year life
113/// ```
114/// use rust_finprim::amort_dep_tax::db;
115/// use rust_decimal_macros::*;
116///
117/// let cost = dec!(10_000);
118/// let salvage = dec!(1_000);
119/// let life = 5;
120/// let schedule = db(cost, salvage, life, None, None);
121/// ```
122pub fn db(
123    cost: Decimal,
124    salvage: Decimal,
125    life: u32,
126    factor: Option<Decimal>,
127    round: Option<(u32, RoundingStrategy)>,
128) -> Vec<DepreciationPeriod> {
129    let mut periods = vec![DepreciationPeriod::default(); life as usize];
130    db_into(periods.as_mut_slice(), cost, salvage, factor, round);
131    periods
132}
133
134/// Declining Balance Depreciation (DB) Into
135///
136/// Calculates the depreciation schedule for an asset using the declining balance method given a
137/// declining balance factor (e.g. double-declining balance), mutating a "slice" of DepreciationPeriod.
138///
139/// # Arguments
140/// * `slice` - A mutable slice of `DepreciationPeriod` instances to be filled with the depreciation schedule.
141///
142/// **Warning**: The length of the slice should be as long as the life as the asset or there will
143/// be unexpected behavior.
144/// * `cost` - The initial cost of the assert
145/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
146/// * `factor` (optional) - The factor by which the straight-line depreciation rate is multiplied (default is 2 for double-declining balance)
147/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding strategy for the amounts `(dp, RoundingStrategy)`,
148/// default is no rounding of calculations. The final depreciation expense is adjusted to ensure the remaining book value is equal to the salvage value.
149/// `rust_decimal::RoundingStrategy::MidpointNearestEven` ("Bankers Rounding") is likely what you are looking for as the rounding strategy.
150///
151/// If rounding is enabled, the final period will be adjusted to "zero" out the remaining book
152/// value to the salvage value.
153///
154/// # Examples
155/// * $10,000 asset, $1,000 salvage value, 5 year life
156/// ```
157/// use rust_finprim::amort_dep_tax::{DepreciationPeriod, db_into};
158/// use rust_decimal_macros::*;
159///
160/// let life = 5;
161/// let cost = dec!(10_000);
162/// let salvage = dec!(1_000);
163///
164/// let mut schedule = vec![DepreciationPeriod::default(); life as usize];
165/// db_into(&mut schedule, cost, salvage, None, None);
166/// ```
167pub fn db_into(
168    slice: &mut [DepreciationPeriod],
169    cost: Decimal,
170    salvage: Decimal,
171    factor: Option<Decimal>,
172    round: Option<(u32, RoundingStrategy)>,
173) {
174    let factor = factor.unwrap_or(Decimal::TWO);
175    let life = slice.len() as u32;
176
177    let mut remain_bv = cost;
178    let mut accum_dep = ZERO;
179    for (period, item) in slice.iter_mut().enumerate() {
180        let mut dep_exp = factor * (cost - accum_dep) / Decimal::from_u32(life).unwrap();
181        if let Some((dp, rounding)) = round {
182            dep_exp = dep_exp.round_dp_with_strategy(dp, rounding);
183        }
184
185        if dep_exp > remain_bv - salvage {
186            dep_exp = remain_bv - salvage;
187        }
188        accum_dep += dep_exp;
189        remain_bv -= dep_exp;
190
191        item.period = period as u32 + 1;
192        item.depreciation_expense = dep_exp;
193        item.remaining_book_value = remain_bv;
194    }
195
196    if round.is_some() {
197        let last = slice.last_mut().unwrap();
198        last.depreciation_expense += last.remaining_book_value - salvage;
199        last.remaining_book_value = salvage;
200    }
201}
202
203#[cfg(feature = "std")]
204/// Sum of the Years Digits (SYD)
205///
206/// # Feature
207/// This function requires the `std` feature to be enabled as it uses the `std::Vec`. `syd_into`
208/// can be used in a `no_std` environment as any allocation is done by the caller.
209///
210/// Calculates the depreciation schedule for an asset using the sum of the years' digits method.
211/// The sum of the years' digits method is an accelerated depreciation method that allocates
212/// more depreciation expense to the early years of an asset's life.
213///
214/// # Arguments
215/// * `cost` - The initial cost of the asset
216/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
217/// * `life` - The number of periods over which the asset will be depreciated
218/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding strategy for the amounts `(dp, RoundingStrategy)`,
219/// default is no rounding of calculations. The final depreciation expense is adjusted to ensure the remaining book value is equal to the salvage value.
220/// `rust_decimal::RoundingStrategy::MidpointNearestEven` ("Bankers Rounding") is likely what you are looking for as the rounding strategy.
221///
222/// If rounding is enabled, the final period will be adjusted to "zero" out the remaining book value to the salvage value.
223///
224/// # Returns
225/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
226///
227/// # Examples
228/// * $10,000 asset, $1,000 salvage value, 5 year life
229/// ```
230/// use rust_finprim::amort_dep_tax::syd;
231/// use rust_decimal_macros::*;
232///
233/// let cost = dec!(10_000);
234/// let salvage = dec!(1_000);
235/// let life = 5;
236/// let schedule = syd(cost, salvage, life, None);
237/// ```
238pub fn syd(
239    cost: Decimal,
240    salvage: Decimal,
241    life: u32,
242    round: Option<(u32, RoundingStrategy)>,
243) -> Vec<DepreciationPeriod> {
244    let mut periods = vec![DepreciationPeriod::default(); life as usize];
245    syd_into(periods.as_mut_slice(), cost, salvage, round);
246    periods
247}
248
249/// Sum of the Years Digits (SYD) Into
250///
251/// Calculates the depreciation schedule for an asset using the sum of the years' digits method.
252/// The sum of the years' digits method is an accelerated depreciation method that allocates
253/// more depreciation expense to the early years of an asset's life. Mutates a slice of
254/// `DepreciationPeriod`.
255///
256/// # Arguments
257/// * `slice` - A mutable slice of `DepreciationPeriod` instances to be filled with the depreciation schedule.
258///
259/// **Warning**: The length of the slice should be as long as the life as the asset or there will
260/// be unexpected behavior.
261/// * `salvage` - The estimated salvage value of the asset at the end of its useful life
262/// * `life` - The number of periods over which the asset will be depreciated
263/// * `round` (optional) - A tuple specifying the number of decimal places and a rounding strategy for the amounts `(dp, RoundingStrategy)`,
264/// default is no rounding of calculations. The final depreciation expense is adjusted to ensure the remaining book value is equal to the salvage value.
265/// `rust_decimal::RoundingStrategy::MidpointNearestEven` ("Bankers Rounding") is likely what you are looking for as the rounding strategy.
266///
267/// If rounding is enabled, the final period will be adjusted to "zero" out the remaining book value to the salvage value.
268///
269/// # Returns
270/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
271///
272/// # Examples
273/// * $10,000 asset, $1,000 salvage value, 5 year life
274/// ```
275/// use rust_finprim::amort_dep_tax::{DepreciationPeriod, syd_into};
276/// use rust_decimal_macros::*;
277///
278/// let life = 5;
279/// let cost = dec!(10_000);
280/// let salvage = dec!(1_000);
281///
282/// let mut schedule = vec![DepreciationPeriod::default(); life as usize];
283/// syd_into(&mut schedule, cost, salvage, None);
284/// ```
285pub fn syd_into(
286    slice: &mut [DepreciationPeriod],
287    cost: Decimal,
288    salvage: Decimal,
289    round: Option<(u32, RoundingStrategy)>,
290) {
291    let life = slice.len() as u32;
292    let mut remain_bv = cost;
293    let mut accum_dep = ZERO;
294    let sum_of_years = Decimal::from_u32(life * (life + 1)).unwrap() / Decimal::TWO;
295    for (period, item) in slice.iter_mut().enumerate() {
296        let mut dep_exp = (cost - salvage) * Decimal::from_u32(life - (period as u32)).unwrap() / sum_of_years;
297        if let Some((dp, rounding)) = round {
298            dep_exp = dep_exp.round_dp_with_strategy(dp, rounding)
299        };
300
301        accum_dep += dep_exp;
302        remain_bv -= dep_exp;
303
304        item.period = period as u32 + 1;
305        item.depreciation_expense = dep_exp;
306        item.remaining_book_value = remain_bv;
307    }
308
309    if round.is_some() {
310        let last = slice.last_mut().unwrap();
311        last.depreciation_expense += last.remaining_book_value - salvage;
312        last.remaining_book_value = salvage;
313    }
314}
315
316#[cfg(feature = "std")]
317/// MACRS Deprectiation
318///
319/// Calculates the depreciation schedule for an asset using the Modified Accelerated Cost Recovery
320/// System (MACRS method). MACRS is a depreciation method allowed by the IRS for tax purposes.
321///
322/// # Arguments
323/// * `cost` - The initial cost of the asset
324/// * `rates` - A slice representing the MACRS depreciation rates for all periods of the asset's
325/// life, starting with the first year (period 1) and ending with the last year (period 2). Rates
326/// for each period can be found in IRS Publication 946 or other tax resources. The rates should
327/// be in decimal form (e.g., 0.20 for 20%).
328///
329/// # Returns
330/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
331/// The length of the vector will be equal to the number of rates provided.
332///
333/// # Examples
334/// * $10,000 asset, MACRS rates for 5 year life
335/// ```
336/// use rust_finprim::amort_dep_tax::macrs;
337/// use rust_decimal_macros::*;
338/// use rust_decimal::Decimal;
339///
340/// let cost = dec!(10_000);
341/// let rates = vec![
342///    dec!(0.20),
343///    dec!(0.32),
344///    dec!(0.1920),
345///    dec!(0.1152),
346///    dec!(0.1152),
347///    dec!(0.0576)
348/// ];
349/// let schedule = macrs(cost, &rates);
350/// ```
351pub fn macrs(cost: Decimal, rates: &[Decimal]) -> Vec<DepreciationPeriod> {
352    let mut periods = vec![DepreciationPeriod::default(); rates.len()];
353    macrs_into(periods.as_mut_slice(), cost, rates);
354    periods
355}
356
357/// MACRS Deprectiation Into
358///
359/// Calculates the depreciation schedule for an asset using the Modified Accelerated Cost Recovery
360/// System (MACRS method). MACRS is a depreciation method allowed by the IRS for tax purposes.
361/// Mutates a slice of `DepreciationPeriod`.
362///
363/// # Arguments
364/// * `slice` - A mutable slice of `DepreciationPeriod` instances to be filled with the depreciation schedule.
365///
366/// **Warning**: The length of the slice should be as long as the life as the asset, in this case,
367/// that is as long as the number of rates provided. If the length of the slice is not equal to
368/// the number of rates, this will panic.
369/// * `cost` - The initial cost of the asset
370/// * `rates` - A slice representing the MACRS depreciation rates for all periods of the asset's
371/// life, starting with the first year (period 1) and ending with the last year (period 2). Rates
372/// for each period can be found in IRS Publication 946 or other tax resources. The rates should
373/// be in decimal form (e.g., 0.20 for 20%).
374///
375/// # Returns
376/// * A vector of `DepreciationPeriod` instances representing each period in the depreciation schedule.
377/// The length of the vector will be equal to the number of rates provided.
378///
379/// # Examples
380/// * $10,000 asset, MACRS rates for 5 year life
381/// ```
382/// use rust_finprim::amort_dep_tax::{DepreciationPeriod, macrs_into};
383/// use rust_decimal_macros::*;
384/// use rust_decimal::Decimal;
385///
386/// let cost = dec!(10_000);
387/// let rates = vec![
388///    dec!(0.20),
389///    dec!(0.32),
390///    dec!(0.1920),
391///    dec!(0.1152),
392///    dec!(0.1152),
393///    dec!(0.0576)
394/// ];
395/// let life = rates.len() as u32;
396/// let mut schedule = vec![DepreciationPeriod::default(); life as usize];
397/// macrs_into(&mut schedule, cost, &rates);
398/// ```
399pub fn macrs_into(slice: &mut [DepreciationPeriod], cost: Decimal, rates: &[Decimal]) {
400    if slice.len() != rates.len() {
401        panic!("Length of slice must be equal to the number of rates");
402    }
403    let mut remain_bv = cost;
404    for (period, &rate) in rates.iter().enumerate() {
405        let dep_exp = cost * rate;
406        remain_bv -= dep_exp;
407        let item = &mut slice[period];
408        item.period = period as u32 + 1;
409        item.depreciation_expense = dep_exp;
410        item.remaining_book_value = remain_bv;
411    }
412}
413
414// TODO: Add tests for no_std environments, but if they pass in std, they should pass in no_std
415// since the underlying logic is the same. Just the allocation is different.
416#[cfg(test)]
417#[cfg(feature = "std")]
418mod tests {
419    use super::*;
420    use rust_decimal_macros::dec;
421    #[cfg(not(feature = "std"))]
422    extern crate std;
423    #[cfg(not(feature = "std"))]
424    use std::prelude::v1::*;
425    #[cfg(not(feature = "std"))]
426    use std::{assert_eq, println, vec};
427
428    #[test]
429    fn test_macrs() {
430        let cost = dec!(10_000);
431        let rates = vec![
432            dec!(0.20),
433            dec!(0.32),
434            dec!(0.1920),
435            dec!(0.1152),
436            dec!(0.1152),
437            dec!(0.0576),
438        ];
439        const LIFE: u32 = 6;
440        let mut schedule: [DepreciationPeriod; LIFE as usize] = [DepreciationPeriod::default(); LIFE as usize];
441        macrs_into(&mut schedule, cost, &rates);
442        schedule.iter().for_each(|period| println!("{:?}", period));
443        assert_eq!(schedule.len(), rates.len());
444        assert_eq!(schedule[0].depreciation_expense, dec!(2000));
445        assert_eq!(schedule[0].remaining_book_value, dec!(8000));
446        assert_eq!(schedule[5].depreciation_expense, dec!(576));
447        assert_eq!(schedule[5].remaining_book_value, dec!(0));
448    }
449
450    #[test]
451    fn test_syd() {
452        struct TestCase {
453            cost: Decimal,
454            salvage: Decimal,
455            life: u32,
456            round: Option<(u32, RoundingStrategy)>,
457            expected: Decimal,
458        }
459
460        impl TestCase {
461            fn new(cost: f64, salvage: f64, life: u32, round: Option<(u32, RoundingStrategy)>, expected: f64) -> Self {
462                Self {
463                    cost: Decimal::from_f64(cost).unwrap(),
464                    salvage: Decimal::from_f64(salvage).unwrap(),
465                    life,
466                    round,
467                    expected: Decimal::from_f64(expected).unwrap(),
468                }
469            }
470        }
471
472        let cases = [
473            TestCase::new(10_000.00, 1_000.00, 5, None, 600.00),
474            TestCase::new(
475                9_000.00,
476                1_000.00,
477                5,
478                Some((2, RoundingStrategy::MidpointNearestEven)),
479                533.33,
480            ),
481            TestCase::new(
482                9_000.00,
483                1_500.00,
484                10,
485                Some((2, RoundingStrategy::MidpointNearestEven)),
486                136.36,
487            ),
488        ];
489        for case in &cases {
490            let schedule = syd(case.cost, case.salvage, case.life, case.round);
491            schedule.iter().for_each(|period| println!("{:?}", period));
492            assert_eq!(schedule.len(), case.life as usize);
493            assert_eq!(schedule.last().unwrap().depreciation_expense, case.expected);
494        }
495    }
496
497    #[test]
498    fn test_db() {
499        struct TestCase {
500            cost: Decimal,
501            salvage: Decimal,
502            life: u32,
503            factor: Option<Decimal>,
504            round: Option<(u32, RoundingStrategy)>,
505            expected: Decimal,
506        }
507        impl TestCase {
508            fn new(
509                cost: f64,
510                salvage: f64,
511                life: u32,
512                factor: Option<f64>,
513                round: Option<(u32, RoundingStrategy)>,
514                expected: f64,
515            ) -> Self {
516                Self {
517                    cost: Decimal::from_f64(cost).unwrap(),
518                    salvage: Decimal::from_f64(salvage).unwrap(),
519                    life,
520                    factor: factor.map(Decimal::from_f64).unwrap_or(None),
521                    round,
522                    expected: Decimal::from_f64(expected).unwrap(),
523                }
524            }
525        }
526
527        let cases = [
528            TestCase::new(4_000.00, 1_000.00, 5, None, None, 0.00),
529            TestCase::new(10_000.00, 1_000.00, 5, None, None, 296.00),
530            TestCase::new(10_000.00, 1_000.00, 10, None, None, 268.435456),
531            TestCase::new(
532                10_000.00,
533                1_000.00,
534                10,
535                None,
536                Some((2, RoundingStrategy::MidpointNearestEven)),
537                342.18,
538            ),
539        ];
540        for case in &cases {
541            let schedule = db(case.cost, case.salvage, case.life, case.factor, case.round);
542            schedule.iter().for_each(|period| println!("{:?}", period));
543            assert_eq!(schedule.len(), case.life as usize);
544            assert_eq!(schedule.last().unwrap().depreciation_expense, case.expected);
545        }
546    }
547
548    #[test]
549    fn test_sln() {
550        let cost = dec!(10_000);
551        let salvage = dec!(1_000);
552        let life = 5;
553        let schedule = sln(cost, salvage, life);
554        schedule.iter().for_each(|period| println!("{:?}", period));
555        assert_eq!(schedule.len(), 5);
556        assert_eq!(schedule[0].depreciation_expense, dec!(1800));
557        assert_eq!(schedule[0].remaining_book_value, dec!(8200));
558    }
559}