Skip to main content

timevalue/
lib.rs

1#![deny(rust_2018_idioms)]
2#![deny(unused)]
3
4pub trait AnnuityRegular {
5    type Output;
6    type Error;
7
8    fn present_value(self) -> Result<Self::Output, Self::Error>;
9    fn future_value(self) -> Result<Self::Output, Self::Error>;
10}
11pub trait AnnuityDue {
12    type Output;
13    type Error;
14
15    fn present_value(self) -> Result<Self::Output, Self::Error>;
16    fn future_value(self) -> Result<Self::Output, Self::Error>;
17}
18
19#[cfg(not(feature = "rust_decimal"))]
20type F64 = f64;
21
22#[cfg(feature = "rust_decimal")]
23use rust_decimal::dec;
24#[cfg(feature = "rust_decimal")]
25type F64 = rust_decimal::Decimal;
26
27#[cfg(feature = "rust_decimal")]
28use rust_decimal::MathematicalOps;
29
30#[derive(PartialEq, Eq, Debug)]
31pub enum TimeValueError {
32    EmptyCashFlow,
33    NegativeDiscount,
34}
35
36#[derive(Clone, Copy)]
37pub struct Annuity<T, I>
38where
39    I: IntoIterator<Item = T>,
40    T: Into<F64> + Copy,
41{
42    cashflows: I,
43    rate: F64,
44}
45
46impl<T, I> Annuity<T, I>
47where
48    I: IntoIterator<Item = T>,
49    T: Into<F64> + Copy,
50{
51    pub fn new(cashflows: I, rate: impl Into<F64>) -> Self {
52        Self {
53            cashflows,
54            rate: rate.into(),
55        }
56    }
57}
58
59#[cfg(not(feature = "rust_decimal"))]
60const ZERO: F64 = 0.0;
61
62#[cfg(feature = "rust_decimal")]
63const ZERO: F64 = dec!(0.0);
64
65#[cfg(not(feature = "rust_decimal"))]
66const ONE: F64 = 1.0;
67
68#[cfg(feature = "rust_decimal")]
69const ONE: F64 = dec!(1.0);
70
71impl<T, I> AnnuityRegular for Annuity<T, I>
72where
73    I: IntoIterator<Item = T>,
74    T: Into<F64> + Copy,
75{
76    type Output = F64;
77
78    type Error = TimeValueError;
79
80    fn present_value(self) -> Result<Self::Output, Self::Error> {
81        if self.rate < ZERO {
82            return Err(TimeValueError::NegativeDiscount);
83        }
84
85        let mut is_empty = true;
86        let mut sum = ZERO;
87
88        for (n, cf) in self.cashflows.into_iter().enumerate() {
89            is_empty = false;
90            let val: F64 = cf.into();
91
92            #[cfg(not(feature = "rust_decimal"))]
93            let exp: i32 = n as i32 + 1;
94
95            #[cfg(feature = "rust_decimal")]
96            let exp: i64 = n as i64 + 1;
97
98            let disc = val / (ONE + self.rate).powi(exp);
99            sum += disc;
100        }
101
102        if is_empty {
103            return Err(TimeValueError::EmptyCashFlow);
104        }
105
106        Ok(sum)
107    }
108
109    fn future_value(self) -> Result<Self::Output, Self::Error> {
110        if self.rate < ZERO {
111            return Err(TimeValueError::NegativeDiscount);
112        }
113
114        let cash_flows: Vec<_> = self.cashflows.into_iter().map(|i| i.into()).collect();
115        if cash_flows.is_empty() {
116            return Err(TimeValueError::EmptyCashFlow);
117        }
118
119        let mut f: Vec<_> = cash_flows
120            .iter()
121            .enumerate()
122            .map(|(n, &cash_flow)| {
123                #[cfg(not(feature = "rust_decimal"))]
124                let exp: i32 = n as i32 + 1;
125
126                #[cfg(feature = "rust_decimal")]
127                let exp: i64 = n as i64 + 1;
128
129                cash_flow * (ONE + self.rate).powi(exp)
130            })
131            .collect();
132        f.pop();
133
134        if let Some(value) = cash_flows.last() {
135            f.push(*value);
136        }
137        let future_value = f.iter().sum::<F64>();
138
139        Ok(future_value)
140    }
141}
142
143impl<T, I> AnnuityDue for Annuity<T, I>
144where
145    I: IntoIterator<Item = T>,
146    T: Into<F64> + Copy,
147{
148    type Output = F64;
149
150    type Error = TimeValueError;
151
152    fn present_value(self) -> Result<Self::Output, Self::Error> {
153        let rate = self.rate;
154        let pv = <Self as AnnuityRegular>::present_value(self)?;
155
156        Ok(pv * (ONE + rate))
157    }
158
159    fn future_value(self) -> Result<Self::Output, Self::Error> {
160        if self.rate < ZERO {
161            return Err(TimeValueError::NegativeDiscount);
162        }
163        let cash_flows: Vec<_> = self.cashflows.into_iter().map(|i| i.into()).collect();
164        if cash_flows.is_empty() {
165            return Err(TimeValueError::EmptyCashFlow);
166        }
167
168        let future_value = cash_flows
169            .iter()
170            .enumerate()
171            .map(|(i, &cash_flow)| {
172                #[cfg(not(feature = "rust_decimal"))]
173                let exp: i32 = i as i32 + 1;
174
175                #[cfg(feature = "rust_decimal")]
176                let exp: i64 = i as i64 + 1;
177
178                cash_flow * (ONE + self.rate).powi(exp)
179            })
180            .sum();
181
182        Ok(future_value)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188
189    use super::*;
190
191    macro_rules! f64 {
192        ($val:literal) => {{
193            #[cfg(not(feature = "rust_decimal"))]
194            {
195                $val as f64
196            }
197            #[cfg(feature = "rust_decimal")]
198            {
199                dec!($val)
200            }
201        }};
202    }
203
204    #[test]
205    fn annuity_pv() {
206        let rate = f64!(0.12);
207
208        let f = Annuity::new(
209            [
210                5_000, 5_000, 5_000, 5_000, 5_000, 5_000, 5_000, 5_000, 5_000, 5_000,
211            ],
212            rate,
213        );
214
215        let pv = super::AnnuityRegular::present_value(f).unwrap();
216
217        #[cfg(not(feature = "rust_decimal"))]
218        let exp: F64 = 28251.115142054317;
219
220        #[cfg(feature = "rust_decimal")]
221        let exp: F64 = dec!(28251.115142054324467040310164);
222
223        #[cfg(feature = "rust_decimal")]
224        assert_eq!(pv, exp); // Decimal is precise, assert_eq is fine
225
226        #[cfg(not(feature = "rust_decimal"))]
227        assert!(
228            (pv - exp).abs() < f64::EPSILON,
229            "Left: {}, Right: {}",
230            pv,
231            exp
232        );
233    }
234    #[test]
235    fn annuity_pv_due() {
236        let f = Annuity::new([5_000].repeat(10), f64!(0.12));
237        let pv = super::AnnuityDue::present_value(f).unwrap();
238
239        #[cfg(not(feature = "rust_decimal"))]
240        assert_eq!(pv, f64!(31641.248959100838));
241
242        #[cfg(feature = "rust_decimal")]
243        assert_eq!(pv, f64!(31641.248959100843403085147384));
244    }
245    #[test]
246    fn annuity_fv_reg() {
247        #[cfg(not(feature = "rust_decimal"))]
248        let rate: F64 = 0.09;
249
250        #[cfg(feature = "rust_decimal")]
251        let rate: F64 = dec!(0.09);
252
253        let f = Annuity::new([50_000].repeat(7), rate);
254        let pv = super::AnnuityRegular::future_value(f).unwrap();
255
256        #[cfg(not(feature = "rust_decimal"))]
257        let exp: F64 = 460021.7337870501;
258
259        #[cfg(feature = "rust_decimal")]
260        let exp: F64 = dec!(460021.733787050000);
261
262        assert_eq!(pv, exp);
263    }
264
265    #[test]
266    fn annuity_fv_due() {
267        let f = Annuity::new(
268            [
269                200_000, 200_000, 200_000, 200_000, 200_000, 200_000, 200_000,
270            ],
271            f64!(0.12),
272        );
273        let pv = super::AnnuityDue::future_value(f).unwrap();
274
275        #[cfg(not(feature = "rust_decimal"))]
276        assert_eq!(pv, f64!(2259938.6271580164));
277
278        #[cfg(feature = "rust_decimal")]
279        assert_eq!(pv, f64!(2259938.62715801600000));
280    }
281}