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