finance_solution/
lib.rs

1//! `finance_solution` is a collection of financial functions related to time-value-of-money.
2//! In addition to being rigourously tested with symmetry tests as well as excel-matching tests,
3//! the library provides `solution` structs to give the user more detailed information about
4//! each transaction, which has record-keeping benefits for financial software and 
5//! learning benefits for students of finance.
6//!  
7//! ## Example
8//! ```
9//! use finance_solution::*;
10//! let (rate, periods, present_value, is_continuous) = (0.034,10,1000, false);
11//! let fv = future_value_solution(rate, periods, present_value, is_continuous);
12//! dbg!(fv);
13//! ```
14//! which prints to the terminal:
15//! ```text
16//! fv = TvmSolution {
17//!    calculated_field: FutureValue,
18//!    continuous_compounding: false,
19//!    rate: 0.034,
20//!    periods: 10,
21//!    fractional_periods: 10.0,
22//!    present_value: 1000.0,
23//!    future_value: -1397.0288910795477,
24//!    formula: "-1397.0289 = -1000.0000 * (1.034000 ^ 10)",
25//!    symbolic_formula: "fv = -pv * (1 + r)^n",
26//! }
27//! ``` 
28//! and if you run this line:
29//! ```
30//! # use finance_solution::*;
31//! # let (rate, periods, present_value, is_continuous) = (0.034,10,1000, false);
32//! # let fv = future_value_solution(rate, periods, present_value, is_continuous);
33//! fv.series().print_table();
34//! ```
35//! a pretty-printed table will be displayed in the terminal:
36//! ```text
37//! period      rate        value
38//! ------  --------  -----------
39//!      0  0.000000  -1_000.0000
40//!      1  0.034000  -1_034.0000
41//!      2  0.034000  -1_069.1560
42//!      3  0.034000  -1_105.5073
43//!      4  0.034000  -1_143.0946
44//!      5  0.034000  -1_181.9598
45//!      6  0.034000  -1_222.1464
46//!      7  0.034000  -1_263.6994
47//!      8  0.034000  -1_306.6652
48//!      9  0.034000  -1_351.0918
49//!     10  0.034000  -1_397.0289
50//! ```
51//! This can be very useful for functions in the `cashflow` family, such as a payment.
52//! ```
53//! # use finance_solution::*;
54//! let (rate, periods, present_value, future_value, due) = (0.034, 10, 1000, 0, false);
55//! let pmt = payment_solution(rate, periods, present_value, future_value, due);
56//! pmt.print_table();
57//! ```
58//! Which prints to the terminal:
59//! ```
60//! // period  payments_to_date  payments_remaining  principal  principal_to_date  principal_remaining  interest  interest_to_date  interest_remaining
61//! // ------  ----------------  ------------------  ---------  -----------------  -------------------  --------  ----------------  ------------------
62//! //      1         -119.6361         -1_076.7248   -85.6361           -85.6361            -914.3639  -34.0000          -34.0000           -162.3609
63//! //      2         -239.2722           -957.0887   -88.5477          -174.1838            -825.8162  -31.0884          -65.0884           -131.2725
64//! //      3         -358.9083           -837.4526   -91.5583          -265.7421            -734.2579  -28.0778          -93.1661           -103.1947
65//! //      4         -478.5443           -717.8165   -94.6713          -360.4134            -639.5866  -24.9648         -118.1309            -78.2300
66//! //      5         -598.1804           -598.1804   -97.8901          -458.3036            -541.6964  -21.7459         -139.8768            -56.4840
67//! //      6         -717.8165           -478.5443  -101.2184          -559.5220            -440.4780  -18.4177         -158.2945            -38.0663
68//! //      7         -837.4526           -358.9083  -104.6598          -664.1818            -335.8182  -14.9763         -173.2708            -23.0901
69//! //      8         -957.0887           -239.2722  -108.2183          -772.4001            -227.5999  -11.4178         -184.6886            -11.6723
70//! //      9       -1_076.7248           -119.6361  -111.8977          -884.2978            -115.7022   -7.7384         -192.4270             -3.9339
71//! //     10       -1_196.3609             -0.0000  -115.7022          -999.0000              -0.0000   -3.9339         -196.3609              0.0000
72//! ```
73#![allow(dead_code)]
74
75use num_format::{Locale, ToFormattedString};
76use itertools::Itertools;
77
78extern crate float_cmp;
79pub extern crate num_format;
80
81pub mod convert_rate;
82#[doc(inline)]
83pub use convert_rate::*;
84
85pub mod round;
86#[doc(inline)]
87pub use round::*;
88
89pub mod cashflow;
90#[doc(inline)]
91pub use cashflow::*;
92
93pub mod tvm;
94#[doc(inline)]
95pub use tvm::*;
96
97pub mod tvm_convert_rate;
98#[doc(inline)]
99pub use tvm_convert_rate::*;
100use std::cmp::max;
101use std::fmt::{Debug, Formatter, Error};
102
103// use tvm_convert_rate::*;
104// use convert_rate::*;
105
106/*
107#[macro_export]
108macro_rules! assert_approx_equal {
109    ( $x1:expr, $x2:expr ) => {
110        if ($x1 * 10_000.0f64).round() / 10_000.0 != ($x2 * 10_000.0f64).round() / 10_000.0 {
111            let max_length = 6;
112            let mut str_1 = format!("{}", $x1);
113            let mut str_2 = format!("{}", $x2);
114            if str_1 == "-0.".to_string() {
115                str_1 = "0.0".to_string();
116            }
117            if str_2 == "-0.".to_string() {
118                str_2 = "0.0".to_string();
119            }
120            let mut length = std::cmp::min(str_1.len(), str_2.len());
121            length = std::cmp::min(length, max_length);
122            assert_eq!(str_1[..length], str_2[..length]);
123        }
124    };
125}
126*/
127
128#[macro_export]
129macro_rules! is_approx_equal {
130    ( $x1:expr, $x2:expr ) => {
131        float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.000001, ulps = 20)
132    };
133}
134
135#[macro_export]
136macro_rules! assert_approx_equal {
137    ( $x1:expr, $x2:expr ) => {
138        assert!(float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.000001, ulps = 20));
139    };
140}
141
142#[macro_export]
143macro_rules! assert_same_sign_or_zero {
144    ( $x1:expr, $x2:expr ) => {
145        assert!(
146            is_approx_equal!($x1, 0.0)
147            || is_approx_equal!($x2, 0.0)
148            || ($x1 > 0.0 && $x2 > 0.0)
149            || ($x1 < -0.0 && $x2 < -0.0)
150        );
151    };
152}
153
154#[macro_export]
155macro_rules! is_approx_equal_symmetry_test {
156    ( $x1:expr, $x2:expr ) => {
157        if (($x1 > 0.000001 && $x1 < 1_000_000.0) || ($x1 < -0.000001 && $x1 > -1_000_000.0)) && (($x2 > 0.000001 && $x2 < 1_000_000.0) || ($x2 < -0.000001 && $x2 > -1_000_000.0)) {
158            float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.00000001, ulps = 2)
159        } else {
160            true
161        }
162    };
163}
164
165#[macro_export]
166macro_rules! assert_approx_equal_symmetry_test {
167    ( $x1:expr, $x2:expr ) => {
168        if (($x1 > 0.000001 && $x1 < 1_000_000.0) || ($x1 < -0.000001 && $x1 > -1_000_000.0)) && (($x2 > 0.000001 && $x2 < 1_000_000.0) || ($x2 < -0.000001 && $x2 > -1_000_000.0)) {
169            assert!(float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.00000001, ulps = 2));
170        }
171    };
172}
173
174#[macro_export]
175macro_rules! assert_rounded_2 {
176    ( $x1:expr, $x2:expr ) => {
177        assert_eq!(($x1 * 100.0f64).round() / 100.0, ($x2 * 100.0f64).round() / 100.0);
178    };
179}
180
181#[macro_export]
182macro_rules! assert_rounded_4 {
183    ( $x1:expr, $x2:expr ) => {
184        assert_eq!(($x1 * 10_000.0f64).round() / 10_000.0, ($x2 * 10_000.0f64).round() / 10_000.0);
185    };
186}
187
188#[macro_export]
189macro_rules! assert_rounded_6 {
190    ( $x1:expr, $x2:expr ) => {
191        assert_eq!(($x1 * 1_000_000.0f64).round() / 1_000_000.0, ($x2 * 1_000_000f64).round() / 1_000_000.0);
192    };
193}
194
195#[macro_export]
196macro_rules! assert_rounded_8 {
197    ( $x1:expr, $x2:expr ) => {
198        assert_eq!(($x1 * 100_000_000.0f64).round() / 100_000_000.0, ($x2 * 100_000_000.0f64).round() / 100_000_000.0);
199    };
200}
201
202
203#[macro_export]
204macro_rules! repeating_vec {
205    ( $x1:expr, $x2:expr ) => {{
206        let mut repeats = vec![];
207        for _i in 0..$x2 {
208            repeats.push($x1);
209        }
210        repeats
211    }};
212}
213
214fn decimal_separator_locale_opt(locale: Option<&Locale>) -> String {
215    match locale {
216        Some(locale) => locale.decimal().to_string(),
217        None => ".".to_string(),
218    }
219}
220
221fn minus_sign_locale_opt(val: f64, locale: Option<&Locale>) -> String {
222    if val.is_sign_negative() {
223        match locale {
224            Some(locale) => locale.minus_sign().to_string(),
225            None => "-".to_string(),
226        }
227    } else {
228        "".to_string()
229    }
230}
231
232pub(crate) fn parse_and_format_int(val: &str) -> String {
233    parse_and_format_int_locale_opt(val, None)
234}
235
236pub(crate) fn parse_and_format_int_locale_opt(val: &str, locale: Option<&Locale>) -> String {
237    let float_val: f64 = val.parse().unwrap();
238    if float_val.is_finite() {
239        let int_val: i128 = val.parse().unwrap();
240        format_int_locale_opt(int_val, locale)
241    } else {
242        // This is a special case where the value was originally a floating point number that we
243        // normally wish to display as an integer, but it might be something like f64::INFINITY in
244        // which case we'd show something like "Inf" rather than try to convert it into an integer.
245        val.to_string()
246    }
247}
248
249pub(crate) fn format_int<T>(val: T) -> String
250    where T: ToFormattedString
251{
252    format_int_locale_opt(val, None)
253}
254
255pub(crate) fn format_int_locale_opt<T>(val: T, locale: Option<&Locale>) -> String
256    where T: ToFormattedString
257{
258    match locale {
259        Some(locale) => val.to_formatted_string(locale),
260        None => val.to_formatted_string(&Locale::en).replace(",", "_"),
261    }
262}
263
264pub(crate) fn format_float<T>(val: T) -> String
265    where T: Into<f64>
266{
267    format_float_locale_opt(val, None, None)
268}
269
270pub(crate) fn format_rate<T>(val: T) -> String
271    where T: Into<f64>
272{
273    format_float_locale_opt(val, None, Some(6))
274}
275
276pub(crate) fn format_float_locale_opt<T>(val: T, locale: Option<&Locale>, precision: Option<usize>) -> String
277    where T: Into<f64>
278{
279    let precision = precision.unwrap_or(4);
280    let val = val.into();
281    if val.is_finite() {
282        // let locale = SystemLocale::default().unwrap();
283        if precision == 0 {
284            format_int_locale_opt(val.round() as i128, locale)
285        } else {
286            let left = format_int_locale_opt(val.trunc().abs() as i128, locale);
287            let right = &format!("{:.*}", precision, val.fract().abs())[2..];
288            let minus_sign = minus_sign_locale_opt(val as f64, locale);
289            format!("{}{}{}{}", minus_sign, left, decimal_separator_locale_opt(locale), right)
290        }
291    } else {
292        format!("{:?}", val)
293    }
294}
295
296pub(crate) fn print_table_locale_opt(columns: &[(String, String, bool)], mut data: Vec<Vec<String>>, locale: Option<&num_format::Locale>, precision: Option<usize>) {
297    if columns.is_empty() || data.is_empty() {
298        return;
299    }
300
301    let column_separator = "  ";
302
303    let column_count = data[0].len();
304
305    for row_index in 0..data.len() {
306        for col_index in 0..column_count {
307            let visible = columns[col_index].2;
308            if visible {
309                // If the data in this cell is an empty string we're going to leave it with that
310                // value regardless of the type.
311                if !data[row_index][col_index].is_empty() {
312                    let col_type = columns[col_index].1.to_lowercase();
313                    //bg!(&col_type, &data[row_index][col_index]);
314                    if col_type != "s" {
315                        data[row_index][col_index] = if col_type == "f" || col_type == "r" {
316                            let precision = if col_type == "f" {
317                                precision
318                            } else {
319                                precision_opt_set_min(precision, 6)
320                            };
321                            format_float_locale_opt(data[row_index][col_index].parse::<f64>().unwrap(), locale, precision)
322                        } else if col_type == "i" {
323                            // format_int_locale_opt(data[row_index][col_index].parse::<i128>().unwrap(), locale)
324                            parse_and_format_int_locale_opt(&data[row_index][col_index], locale)
325                        } else {
326                            panic!("Unexpected column type = \"{}\"", col_type)
327                        }
328                    }
329                }
330            }
331        }
332    }
333
334    let mut column_widths = vec![];
335    for col_index in 0..column_count {
336        let visible = columns[col_index].2;
337        let width = if visible {
338            let mut width = columns[col_index].0.len();
339            for row in &data {
340                width = max(width, row[col_index].len());
341            }
342            width
343        } else {
344            0
345        };
346        column_widths.push(width);
347    }
348
349    let header_line = columns.iter()
350        .enumerate()
351        .map(|(col_index, (header, _type, visible))|
352            if *visible {
353                format!("{:>width$}{}", header, column_separator, width = column_widths[col_index])
354            } else {
355                "".to_string()
356            }
357        )
358        .join("");
359    println!("\n{}", header_line.trim_end());
360
361    let dash_line = columns.iter()
362        .enumerate()
363        .map(|(col_index, (_header, _type, visible))|
364            if *visible {
365                format!("{}{}", "-".repeat(column_widths[col_index]), column_separator)
366            } else {
367                "".to_string()
368            }
369        )
370        .join("");
371    println!("{}", dash_line.trim_end());
372
373    for row in data.iter() {
374        let value_line = row.iter()
375            .enumerate()
376            .map(|(col_index, value)| {
377                let visible = columns[col_index].2;
378                if visible {
379                    format!("{:>width$}{}", value, column_separator, width = column_widths[col_index])
380                } else {
381                    "".to_string()
382                }
383            }).join("");
384        println!("{}", value_line.trim_end());
385    }
386}
387
388pub(crate) fn print_ab_comparison_values_string(field_name: &str, value_a: &str, value_b: &str) {
389    print_ab_comparison_values_internal(field_name, value_a, value_b, false);
390}
391
392pub(crate) fn print_ab_comparison_values_int(field_name: &str, value_a: i128, value_b: i128, locale: Option<&num_format::Locale>) {
393    print_ab_comparison_values_internal(
394        field_name,
395        &format_int_locale_opt(value_a, locale),
396        &format_int_locale_opt(value_b, locale),
397        true
398    );
399}
400
401pub(crate) fn print_ab_comparison_values_float(field_name: &str, value_a: f64, value_b: f64, locale: Option<&num_format::Locale>, precision: Option<usize>) {
402    print_ab_comparison_values_internal(
403        field_name,
404        &format_float_locale_opt(value_a, locale, precision),
405        &format_float_locale_opt(value_b, locale, precision),
406        true
407    );
408}
409
410pub(crate) fn print_ab_comparison_values_rate(field_name: &str, value_a: f64, value_b: f64, locale: Option<&num_format::Locale>, precision: Option<usize>) {
411    let precision = precision_opt_set_min(precision, 6);
412    print_ab_comparison_values_float(field_name, value_a, value_b, locale, precision);
413}
414
415pub(crate) fn print_ab_comparison_values_bool(field_name: &str, value_a: bool, value_b: bool) {
416    print_ab_comparison_values_internal(
417        field_name,
418        &format!("{:?}", value_a),
419        &format!("{:?}", value_b),
420        false
421    );
422}
423
424fn print_ab_comparison_values_internal(field_name: &str, value_a: &str, value_b: &str, right_align: bool) {
425    if value_a == value_b {
426        println!("{}: {}", field_name, value_a);
427    } else if right_align {
428        let width = max(value_a.len(), value_b.len());
429        println!("{} a: {:>width$}", field_name, value_a, width = width);
430        println!("{} b: {:>width$}", field_name, value_b, width = width);
431    } else {
432        println!("{} a: {}", field_name, value_a);
433        println!("{} b: {}", field_name, value_b);
434    }
435}
436
437fn precision_opt_set_min(precision: Option<usize>, min: usize) -> Option<usize> {
438    Some(match precision {
439        Some(precision) => precision.max(min),
440        None => 6,
441    })
442}
443
444#[derive(Debug)]
445pub enum ValueType {
446    Payment,
447    Rate,
448}
449
450impl ValueType {
451    pub fn is_payment(&self) -> bool {
452        match self {
453            ValueType::Payment => true,
454            _ => false,
455        }
456    }
457
458    pub fn is_rate(&self) -> bool {
459        match self {
460            ValueType::Rate => true,
461            _ => false,
462        }
463    }
464}
465
466#[derive(Debug)]
467pub enum Schedule {
468    Repeating {
469        value_type: ValueType,
470        value: f64,
471        periods: u32,
472    },
473    Custom {
474        value_type: ValueType,
475        values: Vec<f64>
476    },
477}
478
479impl Schedule {
480
481    pub fn new_repeating(value_type: ValueType, value: f64, periods: u32) -> Self {
482        assert!(value.is_finite());
483        Schedule::Repeating {
484            value_type,
485            value,
486            periods,
487        }
488    }
489
490    pub fn new_custom(value_type: ValueType, values: &[f64]) -> Self {
491        for value in values {
492            assert!(value.is_finite());
493        }
494        Schedule::Custom {
495            value_type,
496            values: values.to_vec(),
497        }
498    }
499
500    pub fn is_payment(&self) -> bool {
501        self.value_type().is_payment()
502    }
503
504    pub fn is_rate(&self) -> bool {
505        self.value_type().is_rate()
506    }
507
508    pub fn value_type(&self) -> &ValueType {
509        match self {
510            Schedule::Repeating { value_type, .. } => value_type,
511            Schedule::Custom { value_type, .. } => value_type,
512        }
513    }
514
515    pub fn value(&self) -> Option<f64> {
516        match self {
517            Schedule::Repeating{ value_type: _, value, .. } => Some(*value),
518            Schedule::Custom { .. } => None,
519        }
520    }
521
522    pub fn get(&self, index: usize) -> f64 {
523        match self {
524            Schedule::Repeating { value, periods, .. } => {
525                assert!(index < *periods as usize);
526                *value
527            },
528            Schedule::Custom { values, .. } => {
529                *values.get(index).unwrap()
530            },
531        }
532    }
533
534    pub fn max(&self) -> Option<f64> {
535        match self {
536            Schedule::Repeating{ value, .. } => Some(*value),
537            Schedule::Custom { values, ..} => {
538                match values.len() {
539                    0 => None,
540                    1 => Some(values[0]),
541                    // https://www.reddit.com/r/rust/comments/3fg0xr/how_do_i_find_the_max_value_in_a_vecf64/ctoa7mp/
542                    _ => Some(values.iter().cloned().fold(std::f64::NAN, f64::max))
543                }
544            }
545        }
546    }
547}
548
549#[derive(Debug)]
550pub struct ScenarioList {
551    pub setup: String,
552    pub input_variable: TvmVariable,
553    pub output_variable: TvmVariable,
554    pub entries: Vec<ScenarioEntry>,
555}
556
557pub struct ScenarioEntry {
558    pub input: f64,
559    pub output: f64,
560    input_precision: usize,
561    output_precision: usize,
562}
563
564impl ScenarioList {
565
566    pub(crate) fn new(setup: String, input_variable: TvmVariable, output_variable: TvmVariable, entries: Vec<(f64, f64)>) -> Self {
567        let input_precision = match input_variable {
568            TvmVariable::Periods => 0,
569            TvmVariable::Rate => 6,
570            _ => 4,
571        };
572        let output_precision = match output_variable {
573            TvmVariable::Periods => 0,
574            TvmVariable::Rate => 6,
575            _ => 4,
576        };
577        let entries= entries.iter().map(|entry| ScenarioEntry::new(entry.0, entry.1, input_precision, output_precision)).collect();
578        Self {
579            setup,
580            input_variable,
581            output_variable,
582            entries,
583        }
584    }
585
586    pub fn print_table(&self) {
587        self.print_table_locale_opt(None, None);
588    }
589
590    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
591        self.print_table_locale_opt(Some(locale), Some(precision));
592    }
593
594    fn print_table_locale_opt(&self, locale: Option<&num_format::Locale>, precision: Option<usize>) {
595        let columns = vec![self.input_variable.table_column_spec(true), self.output_variable.table_column_spec(true)];
596        // let columns = columns_with_strings.iter().map(|x| &x.0[..], &x.1[..], x.2);
597        let data = self.entries.iter()
598            .map(|entry| vec![entry.input.to_string(), entry.output.to_string()])
599            .collect::<Vec<_>>();
600        print_table_locale_opt(&columns, data, locale, precision);
601    }
602
603}
604
605impl ScenarioEntry {
606    pub(crate) fn new(input: f64, output: f64, input_precision: usize, output_precision: usize) -> Self {
607        Self { input, output, input_precision, output_precision }
608    }
609}
610
611impl Debug for ScenarioEntry {
612    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
613        let input = format_float_locale_opt(self.input, None, Some(self.input_precision));
614        let output = format_float_locale_opt(self.output, None, Some(self.output_precision));
615        write!(f, "{{ input: {}, output: {} }}", input, output)
616    }
617}
618
619pub(crate) fn columns_with_strings(columns: &[(&str, &str, bool)]) -> Vec<(String, String, bool)> {
620    columns.iter().map(|(label, data_type, visible)| (label.to_string(), data_type.to_string(), *visible)).collect()
621}
622
623pub (crate) fn initialized_vector<L, V>(length: L, value: V) -> Vec<V>
624    where
625        L: Into<usize>,
626        V: Copy,
627{
628    let mut v = vec![];
629    for _ in 0..length.into() {
630        v.push(value);
631    }
632    v
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    #[test]
640    fn test_assert_same_sign_or_zero_nominal() {
641        assert_same_sign_or_zero!(0.0, 0.0);
642        assert_same_sign_or_zero!(0.0, -0.0);
643        assert_same_sign_or_zero!(-0.0, 0.0);
644        assert_same_sign_or_zero!(-0.0, -0.0);
645        assert_same_sign_or_zero!(0.023, 0.023);
646        assert_same_sign_or_zero!(10.0, 0.023);
647        assert_same_sign_or_zero!(-0.000045, -100.0);
648        assert_same_sign_or_zero!(0.023, 0.0);
649        assert_same_sign_or_zero!(0.0, 0.023);
650        assert_same_sign_or_zero!(0.023, -0.0);
651        assert_same_sign_or_zero!(-0.0, 0.023);
652        assert_same_sign_or_zero!(-0.000045, -100.0);
653        assert_same_sign_or_zero!(-0.000045, 0.0);
654        assert_same_sign_or_zero!(0.0, -100.0);
655        assert_same_sign_or_zero!(-0.000045, -0.0);
656        assert_same_sign_or_zero!(-0.0, -100.0);
657        assert_same_sign_or_zero!(100.0, -0.00000000001864464138634503);
658    }
659
660    #[should_panic]
661    #[test]
662    fn test_assert_same_sign_or_zero_fail_diff_sign() {
663        assert_same_sign_or_zero!(-0.000045, 100.0);
664    }
665}