Skip to main content

finance_solution/
lib.rs

1//! `finance_solution` is a collection of financial functions for time-value-of-money, cashflows,
2//! amortization, returns, and basic equity metrics.
3//!
4//! In addition to being rigorously tested with symmetry tests and Excel-matching tests,
5//! the library provides `solution` structs with formulas and period-by-period series — useful for
6//! financial software audit trails and for students of finance.
7//!
8//! # Error handling (v0.1+)
9//!
10//! Public financial calculations return [`FinanceResult`] (`Result<T, `[`FinanceError`]`)`).
11//! Invalid rates, non-finite amounts, and unsolvable inputs are **errors**, not panics.
12//! Compose with `?` or match on [`FinanceError`] variants.
13//!
14//! There is **no dual panicking / `try_*` API** for public math: the ordinary name
15//! (e.g. [`future_value`], [`payment`], [`amortization_solution`]) is the fallible function.
16//!
17//! ## Example
18//! ```
19//! use finance_solution::*;
20//! let (rate, periods, present_value, is_continuous) = (0.034, 10, -1000.0, false);
21//! let fv = future_value_solution(rate, periods, present_value, is_continuous).unwrap();
22//! dbg!(&fv);
23//! ```
24//! which prints to the terminal:
25//! ```text
26//! fv = TvmSolution {
27//!    calculated_field: FutureValue,
28//!    continuous_compounding: false,
29//!    rate: 0.034,
30//!    periods: 10,
31//!    fractional_periods: 10.0,
32//!    present_value: -1000.0,
33//!    future_value: 1397.0288910795477,
34//!    formula: "1397.0289 = 1000.0000 * (1.034000 ^ 10)",
35//!    symbolic_formula: "fv = -pv * (1 + r)^n",
36//! }
37//! ```
38//! and if you run this line:
39//! ```
40//! # use finance_solution::*;
41//! # let (rate, periods, present_value, is_continuous) = (0.034, 10, -1000.0, false);
42//! # let fv = future_value_solution(rate, periods, present_value, is_continuous).unwrap();
43//! fv.series().print_table();
44//! ```
45//! a pretty-printed table will be displayed in the terminal:
46//! ```text
47//! period      rate        value
48//! ------  --------  -----------
49//!      0  0.000000  -1_000.0000
50//!      1  0.034000  -1_034.0000
51//!      2  0.034000  -1_069.1560
52//!      3  0.034000  -1_105.5073
53//!      4  0.034000  -1_143.0946
54//!      5  0.034000  -1_181.9598
55//!      6  0.034000  -1_222.1464
56//!      7  0.034000  -1_263.6994
57//!      8  0.034000  -1_306.6652
58//!      9  0.034000  -1_351.0918
59//!     10  0.034000  -1_397.0289
60//! ```
61//! This can be very useful for functions in the `cashflow` family, such as a payment.
62//! ```
63//! # use finance_solution::*;
64//! let (rate, periods, present_value, future_value, due) = (0.034, 10, 1000, 0, false);
65//! let pmt = payment_solution(rate, periods, present_value, future_value, due).unwrap();
66//! pmt.print_table();
67//! ```
68//! Which prints to the terminal:
69//! ```
70//! // period  payments_to_date  payments_remaining  principal  principal_to_date  principal_remaining  interest  interest_to_date  interest_remaining
71//! // ------  ----------------  ------------------  ---------  -----------------  -------------------  --------  ----------------  ------------------
72//! //      1         -119.6361         -1_076.7248   -85.6361           -85.6361            -914.3639  -34.0000          -34.0000           -162.3609
73//! //      2         -239.2722           -957.0887   -88.5477          -174.1838            -825.8162  -31.0884          -65.0884           -131.2725
74//! //      3         -358.9083           -837.4526   -91.5583          -265.7421            -734.2579  -28.0778          -93.1661           -103.1947
75//! //      4         -478.5443           -717.8165   -94.6713          -360.4134            -639.5866  -24.9648         -118.1309            -78.2300
76//! //      5         -598.1804           -598.1804   -97.8901          -458.3036            -541.6964  -21.7459         -139.8768            -56.4840
77//! //      6         -717.8165           -478.5443  -101.2184          -559.5220            -440.4780  -18.4177         -158.2945            -38.0663
78//! //      7         -837.4526           -358.9083  -104.6598          -664.1818            -335.8182  -14.9763         -173.2708            -23.0901
79//! //      8         -957.0887           -239.2722  -108.2183          -772.4001            -227.5999  -11.4178         -184.6886            -11.6723
80//! //      9       -1_076.7248           -119.6361  -111.8977          -884.2978            -115.7022   -7.7384         -192.4270             -3.9339
81//! //     10       -1_196.3609             -0.0000  -115.7022          -999.0000              -0.0000   -3.9339         -196.3609              0.0000
82//! ```
83#![allow(dead_code)]
84
85use itertools::Itertools;
86use num_format::{Locale, ToFormattedString};
87
88pub use float_cmp;
89pub use num_format;
90
91// ---------------------------------------------------------------------------
92// Core utilities
93// ---------------------------------------------------------------------------
94
95pub mod util;
96#[doc(inline)]
97pub use util::{FinanceError, FinanceResult};
98
99pub mod round;
100#[doc(inline)]
101pub use round::*;
102
103// ---------------------------------------------------------------------------
104// Time value of money & cashflows
105// ---------------------------------------------------------------------------
106
107pub mod tvm;
108#[doc(inline)]
109pub use tvm::*;
110
111pub mod cashflow;
112#[doc(inline)]
113pub use cashflow::*;
114
115/// Rate conversions (APR / EAR / EPR).
116/// Historical module path kept for API stability (cannot be named `rate` at the crate
117/// root because [`rate`] is a TVM function).
118pub mod convert_rate;
119#[doc(inline)]
120pub use convert_rate::*;
121
122pub mod tvm_convert_rate;
123#[doc(inline)]
124pub use tvm_convert_rate::*;
125
126// ---------------------------------------------------------------------------
127// Domain modules (only modules with real, tested APIs)
128// ---------------------------------------------------------------------------
129
130pub mod amortization;
131#[doc(inline)]
132pub use amortization::*;
133
134pub mod returns;
135#[doc(inline)]
136pub use returns::*;
137
138pub mod stocks;
139#[doc(inline)]
140pub use stocks::*;
141
142use std::cmp::max;
143use std::fmt::{Debug, Error, Formatter};
144
145// use tvm_convert_rate::*;
146// use convert_rate::*;
147
148/*
149#[macro_export]
150macro_rules! assert_approx_equal {
151    ( $x1:expr, $x2:expr ) => {
152        if ($x1 * 10_000.0f64).round() / 10_000.0 != ($x2 * 10_000.0f64).round() / 10_000.0 {
153            let max_length = 6;
154            let mut str_1 = format!("{}", $x1);
155            let mut str_2 = format!("{}", $x2);
156            if str_1 == "-0.".to_string() {
157                str_1 = "0.0".to_string();
158            }
159            if str_2 == "-0.".to_string() {
160                str_2 = "0.0".to_string();
161            }
162            let mut length = std::cmp::min(str_1.len(), str_2.len());
163            length = std::cmp::min(length, max_length);
164            assert_eq!(str_1[..length], str_2[..length]);
165        }
166    };
167}
168*/
169
170#[macro_export]
171macro_rules! is_approx_equal {
172    ( $x1:expr, $x2:expr ) => {
173        float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.000001, ulps = 20)
174    };
175}
176
177#[macro_export]
178macro_rules! assert_approx_equal {
179    ( $x1:expr, $x2:expr ) => {
180        assert!(float_cmp::approx_eq!(
181            f64,
182            $x1,
183            $x2,
184            epsilon = 0.000001,
185            ulps = 20
186        ));
187    };
188}
189
190#[macro_export]
191macro_rules! assert_same_sign_or_zero {
192    ( $x1:expr, $x2:expr ) => {
193        assert!(
194            is_approx_equal!($x1, 0.0)
195                || is_approx_equal!($x2, 0.0)
196                || ($x1 > 0.0 && $x2 > 0.0)
197                || ($x1 < -0.0 && $x2 < -0.0)
198        );
199    };
200}
201
202#[macro_export]
203macro_rules! is_approx_equal_symmetry_test {
204    ( $x1:expr, $x2:expr ) => {
205        if (($x1 > 0.000001 && $x1 < 1_000_000.0) || ($x1 < -0.000001 && $x1 > -1_000_000.0))
206            && (($x2 > 0.000001 && $x2 < 1_000_000.0) || ($x2 < -0.000001 && $x2 > -1_000_000.0))
207        {
208            float_cmp::approx_eq!(f64, $x1, $x2, epsilon = 0.00000001, ulps = 2)
209        } else {
210            true
211        }
212    };
213}
214
215#[macro_export]
216macro_rules! assert_approx_equal_symmetry_test {
217    ( $x1:expr, $x2:expr ) => {
218        if (($x1 > 0.000001 && $x1 < 1_000_000.0) || ($x1 < -0.000001 && $x1 > -1_000_000.0))
219            && (($x2 > 0.000001 && $x2 < 1_000_000.0) || ($x2 < -0.000001 && $x2 > -1_000_000.0))
220        {
221            assert!(float_cmp::approx_eq!(
222                f64,
223                $x1,
224                $x2,
225                epsilon = 0.00000001,
226                ulps = 2
227            ));
228        }
229    };
230}
231
232#[macro_export]
233macro_rules! assert_rounded_2 {
234    ( $x1:expr, $x2:expr ) => {
235        assert_eq!(
236            ($x1 * 100.0f64).round() / 100.0,
237            ($x2 * 100.0f64).round() / 100.0
238        );
239    };
240}
241
242#[macro_export]
243macro_rules! assert_rounded_4 {
244    ( $x1:expr, $x2:expr ) => {
245        assert_eq!(
246            ($x1 * 10_000.0f64).round() / 10_000.0,
247            ($x2 * 10_000.0f64).round() / 10_000.0
248        );
249    };
250}
251
252#[macro_export]
253macro_rules! assert_rounded_6 {
254    ( $x1:expr, $x2:expr ) => {
255        assert_eq!(
256            ($x1 * 1_000_000.0f64).round() / 1_000_000.0,
257            ($x2 * 1_000_000f64).round() / 1_000_000.0
258        );
259    };
260}
261
262#[macro_export]
263macro_rules! assert_rounded_8 {
264    ( $x1:expr, $x2:expr ) => {
265        assert_eq!(
266            ($x1 * 100_000_000.0f64).round() / 100_000_000.0,
267            ($x2 * 100_000_000.0f64).round() / 100_000_000.0
268        );
269    };
270}
271
272#[macro_export]
273macro_rules! repeating_vec {
274    ( $x1:expr, $x2:expr ) => {{
275        let mut repeats = vec![];
276        for _i in 0..$x2 {
277            repeats.push($x1);
278        }
279        repeats
280    }};
281}
282
283fn decimal_separator_locale_opt(locale: Option<&Locale>) -> String {
284    match locale {
285        Some(locale) => locale.decimal().to_string(),
286        None => ".".to_string(),
287    }
288}
289
290fn minus_sign_locale_opt(val: f64, locale: Option<&Locale>) -> String {
291    if val.is_sign_negative() {
292        match locale {
293            Some(locale) => locale.minus_sign().to_string(),
294            None => "-".to_string(),
295        }
296    } else {
297        "".to_string()
298    }
299}
300
301pub(crate) fn parse_and_format_int(val: &str) -> String {
302    parse_and_format_int_locale_opt(val, None)
303}
304
305pub(crate) fn parse_and_format_int_locale_opt(val: &str, locale: Option<&Locale>) -> String {
306    let float_val: f64 = val.parse().unwrap();
307    if float_val.is_finite() {
308        let int_val: i128 = val.parse().unwrap();
309        format_int_locale_opt(int_val, locale)
310    } else {
311        // This is a special case where the value was originally a floating point number that we
312        // normally wish to display as an integer, but it might be something like f64::INFINITY in
313        // which case we'd show something like "Inf" rather than try to convert it into an integer.
314        val.to_string()
315    }
316}
317
318pub(crate) fn format_int<T>(val: T) -> String
319where
320    T: ToFormattedString,
321{
322    format_int_locale_opt(val, None)
323}
324
325pub(crate) fn format_int_locale_opt<T>(val: T, locale: Option<&Locale>) -> String
326where
327    T: ToFormattedString,
328{
329    match locale {
330        Some(locale) => val.to_formatted_string(locale),
331        None => val.to_formatted_string(&Locale::en).replace(",", "_"),
332    }
333}
334
335pub(crate) fn format_float<T>(val: T) -> String
336where
337    T: Into<f64>,
338{
339    format_float_locale_opt(val, None, None)
340}
341
342pub(crate) fn format_rate<T>(val: T) -> String
343where
344    T: Into<f64>,
345{
346    format_float_locale_opt(val, None, Some(6))
347}
348
349pub(crate) fn format_float_locale_opt<T>(
350    val: T,
351    locale: Option<&Locale>,
352    precision: Option<usize>,
353) -> String
354where
355    T: Into<f64>,
356{
357    let precision = precision.unwrap_or(4);
358    let val = val.into();
359    if val.is_finite() {
360        // Round at the requested precision *before* splitting integer / fractional
361        // parts. Otherwise values like 9.999999999999998 at precision 4 become
362        // "9.0000" (trunc left=9, fract formats as "1.0000"[2..]="0000").
363        if precision == 0 {
364            format_int_locale_opt(val.round() as i128, locale)
365        } else {
366            // Round to `precision` decimal places first so fractional rounding can
367            // carry into the integer part (e.g. 9.99995 → 10.0000 at 4 places).
368            let scale = 10_f64.powi(precision as i32);
369            let rounded_abs = (val.abs() * scale).round() / scale;
370            let left = format_int_locale_opt(rounded_abs.trunc() as i128, locale);
371            let frac_digits = (rounded_abs.fract() * scale).round() as u64;
372            let right = format!("{:0>width$}", frac_digits, width = precision);
373            let minus_sign = minus_sign_locale_opt(val, locale);
374            format!(
375                "{}{}{}{}",
376                minus_sign,
377                left,
378                decimal_separator_locale_opt(locale),
379                right
380            )
381        }
382    } else {
383        format!("{:?}", val)
384    }
385}
386
387pub(crate) fn print_table_locale_opt(
388    columns: &[(String, String, bool)],
389    mut data: Vec<Vec<String>>,
390    locale: Option<&num_format::Locale>,
391    precision: Option<usize>,
392) {
393    if columns.is_empty() || data.is_empty() {
394        return;
395    }
396
397    let column_separator = "  ";
398
399    let column_count = data[0].len();
400
401    for row_index in 0..data.len() {
402        for col_index in 0..column_count {
403            let visible = columns[col_index].2;
404            if visible {
405                // If the data in this cell is an empty string we're going to leave it with that
406                // value regardless of the type.
407                if !data[row_index][col_index].is_empty() {
408                    let col_type = columns[col_index].1.to_lowercase();
409                    //bg!(&col_type, &data[row_index][col_index]);
410                    if col_type != "s" {
411                        if col_type == "f" || col_type == "r" {
412                            let precision = if col_type == "f" {
413                                precision
414                            } else {
415                                precision_opt_set_min(precision, 6)
416                            };
417                            // Non-numeric placeholders (e.g. "n/a") stay as plain strings.
418                            if let Ok(n) = data[row_index][col_index].parse::<f64>() {
419                                data[row_index][col_index] =
420                                    format_float_locale_opt(n, locale, precision);
421                            }
422                        } else if col_type == "i" {
423                            data[row_index][col_index] = parse_and_format_int_locale_opt(
424                                &data[row_index][col_index],
425                                locale,
426                            );
427                        }
428                        // Unknown types: leave the cell as a string.
429                    }
430                }
431            }
432        }
433    }
434
435    let mut column_widths = vec![];
436    for col_index in 0..column_count {
437        let visible = columns[col_index].2;
438        let width = if visible {
439            let mut width = columns[col_index].0.len();
440            for row in &data {
441                width = max(width, row[col_index].len());
442            }
443            width
444        } else {
445            0
446        };
447        column_widths.push(width);
448    }
449
450    let header_line = columns
451        .iter()
452        .enumerate()
453        .map(|(col_index, (header, _type, visible))| {
454            if *visible {
455                format!(
456                    "{:>width$}{}",
457                    header,
458                    column_separator,
459                    width = column_widths[col_index]
460                )
461            } else {
462                "".to_string()
463            }
464        })
465        .join("");
466    println!("\n{}", header_line.trim_end());
467
468    let dash_line = columns
469        .iter()
470        .enumerate()
471        .map(|(col_index, (_header, _type, visible))| {
472            if *visible {
473                format!(
474                    "{}{}",
475                    "-".repeat(column_widths[col_index]),
476                    column_separator
477                )
478            } else {
479                "".to_string()
480            }
481        })
482        .join("");
483    println!("{}", dash_line.trim_end());
484
485    for row in data.iter() {
486        let value_line = row
487            .iter()
488            .enumerate()
489            .map(|(col_index, value)| {
490                let visible = columns[col_index].2;
491                if visible {
492                    format!(
493                        "{:>width$}{}",
494                        value,
495                        column_separator,
496                        width = column_widths[col_index]
497                    )
498                } else {
499                    "".to_string()
500                }
501            })
502            .join("");
503        println!("{}", value_line.trim_end());
504    }
505}
506
507pub(crate) fn print_ab_comparison_values_string(field_name: &str, value_a: &str, value_b: &str) {
508    print_ab_comparison_values_internal(field_name, value_a, value_b, false);
509}
510
511pub(crate) fn print_ab_comparison_values_int(
512    field_name: &str,
513    value_a: i128,
514    value_b: i128,
515    locale: Option<&num_format::Locale>,
516) {
517    print_ab_comparison_values_internal(
518        field_name,
519        &format_int_locale_opt(value_a, locale),
520        &format_int_locale_opt(value_b, locale),
521        true,
522    );
523}
524
525pub(crate) fn print_ab_comparison_values_float(
526    field_name: &str,
527    value_a: f64,
528    value_b: f64,
529    locale: Option<&num_format::Locale>,
530    precision: Option<usize>,
531) {
532    print_ab_comparison_values_internal(
533        field_name,
534        &format_float_locale_opt(value_a, locale, precision),
535        &format_float_locale_opt(value_b, locale, precision),
536        true,
537    );
538}
539
540pub(crate) fn print_ab_comparison_values_rate(
541    field_name: &str,
542    value_a: f64,
543    value_b: f64,
544    locale: Option<&num_format::Locale>,
545    precision: Option<usize>,
546) {
547    let precision = precision_opt_set_min(precision, 6);
548    print_ab_comparison_values_float(field_name, value_a, value_b, locale, precision);
549}
550
551pub(crate) fn print_ab_comparison_values_bool(field_name: &str, value_a: bool, value_b: bool) {
552    print_ab_comparison_values_internal(
553        field_name,
554        &format!("{:?}", value_a),
555        &format!("{:?}", value_b),
556        false,
557    );
558}
559
560fn print_ab_comparison_values_internal(
561    field_name: &str,
562    value_a: &str,
563    value_b: &str,
564    right_align: bool,
565) {
566    if value_a == value_b {
567        println!("{}: {}", field_name, value_a);
568    } else if right_align {
569        let width = max(value_a.len(), value_b.len());
570        println!("{} a: {:>width$}", field_name, value_a, width = width);
571        println!("{} b: {:>width$}", field_name, value_b, width = width);
572    } else {
573        println!("{} a: {}", field_name, value_a);
574        println!("{} b: {}", field_name, value_b);
575    }
576}
577
578fn precision_opt_set_min(precision: Option<usize>, min: usize) -> Option<usize> {
579    Some(match precision {
580        Some(precision) => precision.max(min),
581        None => 6,
582    })
583}
584
585/// Discriminator for values stored in a [`Schedule`].
586#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
587pub enum ValueType {
588    Payment,
589    Rate,
590}
591
592impl ValueType {
593    pub fn is_payment(&self) -> bool {
594        matches!(self, ValueType::Payment)
595    }
596
597    pub fn is_rate(&self) -> bool {
598        matches!(self, ValueType::Rate)
599    }
600}
601
602impl std::fmt::Display for ValueType {
603    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604        match self {
605            ValueType::Payment => write!(f, "Payment"),
606            ValueType::Rate => write!(f, "Rate"),
607        }
608    }
609}
610
611/// Sparse or repeating schedule of rates or payments.
612///
613/// Construct with [`Schedule::new_repeating`] / [`Schedule::new_custom`]
614/// (fallible `FinanceResult`) so non-finite values are rejected without panicking.
615///
616/// # Examples
617/// ```
618/// use finance_solution::{Schedule, ValueType};
619///
620/// // Repeating 5% for three periods
621/// let rates = Schedule::new_repeating(ValueType::Rate, 0.05, 3)?;
622/// assert_eq!(rates.len(), 3);
623/// assert_eq!(rates.get(0), Some(0.05));
624/// assert_eq!(rates.get(3), None); // OOB → Option, not panic
625///
626/// // Custom payment ladder
627/// let pmts = Schedule::new_custom(ValueType::Payment, &[100.0, 110.0, 120.0])?;
628/// assert_eq!(pmts.get(1), Some(110.0));
629///
630/// // Invalid input is Err, not abort
631/// assert!(Schedule::new_repeating(ValueType::Rate, f64::NAN, 1).is_err());
632/// assert!(Schedule::new_custom(ValueType::Payment, &[]).is_err());
633/// # Ok::<(), finance_solution::FinanceError>(())
634/// ```
635#[derive(Clone, Debug)]
636pub enum Schedule {
637    Repeating {
638        value_type: ValueType,
639        value: f64,
640        periods: u32,
641    },
642    Custom {
643        value_type: ValueType,
644        values: Vec<f64>,
645    },
646}
647
648impl Schedule {
649    /// Repeating constant value for `periods` steps.
650    ///
651    /// # Errors
652    /// [`FinanceError::NonFinite`] if `value` is not finite.
653    pub fn new_repeating(value_type: ValueType, value: f64, periods: u32) -> FinanceResult<Self> {
654        crate::util::error::require_finite("value", value)?;
655        Ok(Schedule::Repeating {
656            value_type,
657            value,
658            periods,
659        })
660    }
661
662    /// Custom value series (one entry per period).
663    ///
664    /// # Errors
665    /// [`FinanceError::NonFinite`] if any value is not finite;
666    /// [`FinanceError::EmptyInput`] if `values` is empty.
667    pub fn new_custom(value_type: ValueType, values: &[f64]) -> FinanceResult<Self> {
668        if values.is_empty() {
669            return Err(FinanceError::EmptyInput { what: "values" });
670        }
671        for (i, &value) in values.iter().enumerate() {
672            if !value.is_finite() {
673                return Err(FinanceError::NonFinite {
674                    field: "values",
675                    value,
676                });
677            }
678            let _ = i;
679        }
680        Ok(Schedule::Custom {
681            value_type,
682            values: values.to_vec(),
683        })
684    }
685
686    pub fn is_payment(&self) -> bool {
687        self.value_type().is_payment()
688    }
689
690    pub fn is_rate(&self) -> bool {
691        self.value_type().is_rate()
692    }
693
694    pub fn value_type(&self) -> &ValueType {
695        match self {
696            Schedule::Repeating { value_type, .. } => value_type,
697            Schedule::Custom { value_type, .. } => value_type,
698        }
699    }
700
701    /// Constant value for a repeating schedule; `None` for custom series.
702    pub fn value(&self) -> Option<f64> {
703        match self {
704            Schedule::Repeating { value, .. } => Some(*value),
705            Schedule::Custom { .. } => None,
706        }
707    }
708
709    /// Value at a 0-based index, or `None` if out of range.
710    pub fn get(&self, index: usize) -> Option<f64> {
711        match self {
712            Schedule::Repeating { value, periods, .. } => {
713                if index < *periods as usize {
714                    Some(*value)
715                } else {
716                    None
717                }
718            }
719            Schedule::Custom { values, .. } => values.get(index).copied(),
720        }
721    }
722
723    /// Maximum value in the schedule, if any.
724    pub fn max(&self) -> Option<f64> {
725        match self {
726            Schedule::Repeating { value, .. } => Some(*value),
727            Schedule::Custom { values, .. } => {
728                if values.is_empty() {
729                    None
730                } else {
731                    Some(values.iter().cloned().fold(f64::NAN, f64::max))
732                }
733            }
734        }
735    }
736
737    /// Number of periods / entries.
738    pub fn len(&self) -> usize {
739        match self {
740            Schedule::Repeating { periods, .. } => *periods as usize,
741            Schedule::Custom { values, .. } => values.len(),
742        }
743    }
744
745    pub fn is_empty(&self) -> bool {
746        self.len() == 0
747    }
748}
749
750#[derive(Debug)]
751pub struct ScenarioList {
752    pub setup: String,
753    pub input_variable: TvmVariable,
754    pub output_variable: TvmVariable,
755    pub entries: Vec<ScenarioEntry>,
756}
757
758pub struct ScenarioEntry {
759    pub input: f64,
760    pub output: f64,
761    input_precision: usize,
762    output_precision: usize,
763}
764
765impl ScenarioList {
766    pub(crate) fn new(
767        setup: String,
768        input_variable: TvmVariable,
769        output_variable: TvmVariable,
770        entries: Vec<(f64, f64)>,
771    ) -> Self {
772        let input_precision = match input_variable {
773            TvmVariable::Periods => 0,
774            TvmVariable::Rate => 6,
775            _ => 4,
776        };
777        let output_precision = match output_variable {
778            TvmVariable::Periods => 0,
779            TvmVariable::Rate => 6,
780            _ => 4,
781        };
782        let entries = entries
783            .iter()
784            .map(|entry| ScenarioEntry::new(entry.0, entry.1, input_precision, output_precision))
785            .collect();
786        Self {
787            setup,
788            input_variable,
789            output_variable,
790            entries,
791        }
792    }
793
794    pub fn print_table(&self) {
795        self.print_table_locale_opt(None, None);
796    }
797
798    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
799        self.print_table_locale_opt(Some(locale), Some(precision));
800    }
801
802    fn print_table_locale_opt(
803        &self,
804        locale: Option<&num_format::Locale>,
805        precision: Option<usize>,
806    ) {
807        let columns = vec![
808            self.input_variable.table_column_spec(true),
809            self.output_variable.table_column_spec(true),
810        ];
811        // let columns = columns_with_strings.iter().map(|x| &x.0[..], &x.1[..], x.2);
812        let data = self
813            .entries
814            .iter()
815            .map(|entry| vec![entry.input.to_string(), entry.output.to_string()])
816            .collect::<Vec<_>>();
817        print_table_locale_opt(&columns, data, locale, precision);
818    }
819}
820
821impl ScenarioEntry {
822    pub(crate) fn new(
823        input: f64,
824        output: f64,
825        input_precision: usize,
826        output_precision: usize,
827    ) -> Self {
828        Self {
829            input,
830            output,
831            input_precision,
832            output_precision,
833        }
834    }
835}
836
837impl Debug for ScenarioEntry {
838    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
839        let input = format_float_locale_opt(self.input, None, Some(self.input_precision));
840        let output = format_float_locale_opt(self.output, None, Some(self.output_precision));
841        write!(f, "{{ input: {}, output: {} }}", input, output)
842    }
843}
844
845pub(crate) fn columns_with_strings(columns: &[(&str, &str, bool)]) -> Vec<(String, String, bool)> {
846    columns
847        .iter()
848        .map(|(label, data_type, visible)| (label.to_string(), data_type.to_string(), *visible))
849        .collect()
850}
851
852pub(crate) fn initialized_vector<L, V>(length: L, value: V) -> Vec<V>
853where
854    L: Into<usize>,
855    V: Copy,
856{
857    let mut v = vec![];
858    for _ in 0..length.into() {
859        v.push(value);
860    }
861    v
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867
868    #[test]
869    fn test_schedule_new_and_get() {
870        let s = Schedule::new_repeating(ValueType::Rate, 0.05, 3).unwrap();
871        assert_eq!(s.len(), 3);
872        assert_eq!(s.get(0), Some(0.05));
873        assert_eq!(s.get(3), None);
874        assert!(Schedule::new_repeating(ValueType::Rate, f64::NAN, 1).is_err());
875
876        let c = Schedule::new_custom(ValueType::Payment, &[1.0, 2.0]).unwrap();
877        assert_eq!(c.get(1), Some(2.0));
878        assert_eq!(c.get(2), None);
879        assert!(Schedule::new_custom(ValueType::Payment, &[]).is_err());
880        assert!(Schedule::new_custom(ValueType::Payment, &[1.0, f64::INFINITY]).is_err());
881    }
882
883    #[test]
884    fn test_assert_same_sign_or_zero_nominal() {
885        assert_same_sign_or_zero!(0.0, 0.0);
886        assert_same_sign_or_zero!(0.0, -0.0);
887        assert_same_sign_or_zero!(-0.0, 0.0);
888        assert_same_sign_or_zero!(-0.0, -0.0);
889        assert_same_sign_or_zero!(0.023, 0.023);
890        assert_same_sign_or_zero!(10.0, 0.023);
891        assert_same_sign_or_zero!(-0.000045, -100.0);
892        assert_same_sign_or_zero!(0.023, 0.0);
893        assert_same_sign_or_zero!(0.0, 0.023);
894        assert_same_sign_or_zero!(0.023, -0.0);
895        assert_same_sign_or_zero!(-0.0, 0.023);
896        assert_same_sign_or_zero!(-0.000045, -100.0);
897        assert_same_sign_or_zero!(-0.000045, 0.0);
898        assert_same_sign_or_zero!(0.0, -100.0);
899        assert_same_sign_or_zero!(-0.000045, -0.0);
900        assert_same_sign_or_zero!(-0.0, -100.0);
901        assert_same_sign_or_zero!(100.0, -0.00000000001864464138634503);
902    }
903
904    #[should_panic]
905    #[test]
906    fn test_assert_same_sign_or_zero_fail_diff_sign() {
907        assert_same_sign_or_zero!(-0.000045, 100.0);
908    }
909}