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