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