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