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