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}