Skip to main content

optionstratlib_core/model/
decimal.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 25/12/24
5******************************************************************************/
6use crate::error::DecimalError;
7use num_traits::FromPrimitive;
8use positive::{Positive, PositiveError};
9use rand::distr::Distribution;
10use rand::{Rng, RngExt};
11use rand_distr::StandardNormal;
12use rust_decimal::{Decimal, MathematicalOps, RoundingStrategy};
13use rust_decimal_macros::dec;
14
15/// Represents the daily interest rate factor used for financial calculations,
16/// approximately equivalent to 1/252 (a standard value for the number of trading days in a year).
17///
18/// This constant converts annual interest rates to daily rates by providing a division factor.
19/// The value 0.00396825397 corresponds to 1/252, where 252 is the typical number of trading
20/// days in a financial year.
21///
22/// # Usage
23///
24/// This constant is commonly used in financial calculations such as:
25/// - Converting annual interest rates to daily rates
26/// - Time value calculations for options pricing
27/// - Discounting cash flows on a daily basis
28/// - Interest accrual calculations
29pub const ONE_DAY: Decimal = dec!(0.00396825397);
30
31/// Asserts that two Decimal values are approximately equal within a given epsilon
32#[macro_export]
33macro_rules! assert_decimal_eq {
34    ($left:expr, $right:expr, $epsilon:expr) => {
35        let diff = ($left - $right).abs();
36        assert!( // scan-banned: allow -- test assertion macro; it expands at the caller and no library code uses it
37            diff <= $epsilon,
38            "assertion failed: `(left == right)`\n  left: `{}`\n right: `{}`\n  diff: `{}`\n epsilon: `{}`",
39            $left,
40            $right,
41            diff,
42            $epsilon
43        );
44    };
45}
46
47/// Defines statistical operations for collections of decimal values.
48///
49/// This trait provides methods to calculate common statistical measures
50/// for sequences or collections of `Decimal` values. It allows implementing
51/// types to offer standardized statistical analysis capabilities.
52///
53/// ## Key Features
54///
55/// * Basic statistical calculations for `Decimal` collections
56/// * Consistent interface for various collection types
57/// * Precision-preserving operations using the `Decimal` type
58///
59/// ## Available Statistics
60///
61/// * `mean`: Calculates the arithmetic mean (average) of the values
62/// * `std_dev`: Calculates the standard deviation, measuring the dispersion from the mean
63///
64/// ## Example
65///
66/// ```rust
67/// use rust_decimal::Decimal;
68/// use rust_decimal_macros::dec;
69/// use optionstratlib_core::error::DecimalError;
70/// use optionstratlib_core::model::decimal::DecimalStats;
71///
72/// struct DecimalSeries(Vec<Decimal>);
73///
74/// impl DecimalStats for DecimalSeries {
75///     fn mean(&self) -> Result<Decimal, DecimalError> {
76///         if self.0.is_empty() {
77///             return Ok(dec!(0));
78///         }
79///         let sum: Decimal = self.0.iter().sum();
80///         Ok(sum / Decimal::from(self.0.len()))
81///     }
82///
83///     fn std_dev(&self) -> Result<Decimal, DecimalError> {
84///         // Implementation of standard deviation calculation
85///         // ...
86///         Ok(dec!(0)) // Placeholder return
87///     }
88/// }
89/// ```
90pub trait DecimalStats {
91    /// Calculates the arithmetic mean (average) of the collection.
92    ///
93    /// The mean is the sum of all values divided by the count of values.
94    /// This method should handle empty collections appropriately.
95    ///
96    /// # Errors
97    ///
98    /// Implementations return [`DecimalError`] when the running sum or the
99    /// division by the count leaves the representable `Decimal` range.
100    fn mean(&self) -> Result<Decimal, DecimalError>;
101
102    /// Calculates the standard deviation of the collection.
103    ///
104    /// The standard deviation measures the amount of variation or dispersion
105    /// from the mean. A low standard deviation indicates that values tend to be
106    /// close to the mean, while a high standard deviation indicates values are
107    /// spread out over a wider range.
108    ///
109    /// # Errors
110    ///
111    /// Implementations return [`DecimalError`] when a centred deviation, its
112    /// square, the sum of squares, or the final division leaves the
113    /// representable `Decimal` range.
114    fn std_dev(&self) -> Result<Decimal, DecimalError>;
115}
116
117impl DecimalStats for Vec<Decimal> {
118    /// # Errors
119    ///
120    /// Returns [`DecimalError::Overflow`] when the sum of the values leaves
121    /// the representable range — `vec![Decimal::MAX; 2]` is enough. The
122    /// previous signature had nowhere to put that, and `iter().sum()` aborts
123    /// with `Addition overflowed`.
124    fn mean(&self) -> Result<Decimal, DecimalError> {
125        if self.is_empty() {
126            return Ok(Decimal::ZERO);
127        }
128        let sum = d_sum(self, "decimal::stats::mean::sum")?;
129        // `Decimal::from(usize)` is total, and the slice is non-empty here,
130        // so the divisor is neither an overflow nor a zero.
131        d_div(sum, Decimal::from(self.len()), "decimal::stats::mean")
132    }
133
134    /// # Errors
135    ///
136    /// Returns [`DecimalError::Overflow`] when the mean, a centred deviation,
137    /// its square or the sum of squares leaves the representable range, and
138    /// [`DecimalError::ArithmeticError`] if the sample variance were negative.
139    /// The squaring was `.powd(Decimal::TWO)`, which aborts with
140    /// `Pow overflowed`.
141    fn std_dev(&self) -> Result<Decimal, DecimalError> {
142        // Population variance of a single value is zero
143        if self.len() < 2usize {
144            return Ok(Decimal::ZERO);
145        }
146        let mean = self.mean()?;
147        let mut sq_total = Decimal::ZERO;
148        for value in self {
149            let centred = d_sub(*value, mean, "decimal::stats::std_dev::centred")?;
150            let centred_sq = d_mul(centred, centred, "decimal::stats::std_dev::centred_sq")?;
151            sq_total = d_add(sq_total, centred_sq, "decimal::stats::std_dev::sq_total")?;
152        }
153        // `len() >= 2` above, so `len() - 1` neither underflows nor is zero;
154        // the checked form keeps that proof local.
155        let degrees_of_freedom = self.len().checked_sub(1).ok_or_else(|| {
156            DecimalError::arithmetic_error("decimal::stats::std_dev", "sample is empty")
157        })?;
158        let variance = d_div(
159            sq_total,
160            Decimal::from(degrees_of_freedom),
161            "decimal::stats::std_dev::variance",
162        )?;
163        d_sqrt(variance, "decimal::stats::std_dev")
164    }
165}
166
167/// `5^0` to `5^28`. `10^scale = 2^scale * 5^scale`, so a `Decimal` is
168/// `mantissa / 5^scale * 2^-scale`: only the division by `5^scale` rounds,
169/// and `5^28 < 2^66` keeps it in `u128`.
170const POWERS_OF_FIVE: [u128; 29] = [
171    1,
172    5,
173    25,
174    125,
175    625,
176    3125,
177    15625,
178    78125,
179    390625,
180    1953125,
181    9765625,
182    48828125,
183    244140625,
184    1220703125,
185    6103515625,
186    30517578125,
187    152587890625,
188    762939453125,
189    3814697265625,
190    19073486328125,
191    95367431640625,
192    476837158203125,
193    2384185791015625,
194    11920928955078125,
195    59604644775390625,
196    298023223876953125,
197    1490116119384765625,
198    7450580596923828125,
199    37252902984619140625,
200];
201
202/// Bits in an `f64` significand, the implicit leading one included.
203const F64_SIGNIFICAND_BITS: u32 = 53;
204
205/// Bias of the `f64` exponent field.
206const F64_EXPONENT_BIAS: i32 = 1023;
207
208/// Bits below the `f64` exponent field.
209const F64_EXPONENT_SHIFT: u32 = 52;
210
211#[cold]
212#[inline(never)]
213fn decimal_to_f64_error(value: Decimal, reason: &str) -> DecimalError {
214    DecimalError::ConversionError {
215        from_type: format!("Decimal: {value}"),
216        to_type: "f64".to_string(),
217        reason: reason.to_string(),
218    }
219}
220
221/// Converts a `Decimal` to the `f64` nearest to it (ties to even).
222///
223/// The result is the `f64` the literal written with the same digits parses
224/// to: `decimal_to_f64(dec!(2.999789999999902))` is `2.999789999999902_f64`
225/// bit for bit. `Decimal::to_f64` divides the mantissa by a power of ten in
226/// floating point and can land a few ULPs away from it once the value has 15
227/// or more decimal places (`2.999789999999903` for that input), so this
228/// function does not use it (#670). Zero, of either sign, converts to `+0.0`.
229///
230/// The value is `mantissa / 5^scale * 2^-scale`. The mantissa, shifted to
231/// the top of a `u128`, is divided by `5^scale` in integers, which leaves a
232/// quotient of at least 62 bits and an exact remainder; the quotient is
233/// rounded to 53 bits from its dropped bits and the remainder, and scaled by
234/// an exact power of two. It does no floating-point rounding before that one
235/// step and does not allocate on success.
236///
237/// # Parameters
238///
239/// * `value` - The `Decimal` value to convert
240///
241/// # Errors
242///
243/// Returns [`DecimalError::ConversionError`] if the scale exceeds the 28
244/// places a `Decimal` holds or an intermediate leaves its range. Every valid
245/// `Decimal` lies within `f64` range and converts, so neither happens; they
246/// are reported rather than assumed.
247///
248/// # Example
249///
250/// ```rust
251/// use rust_decimal_macros::dec;
252/// use optionstratlib_core::model::decimal::decimal_to_f64;
253///
254/// let value = decimal_to_f64(dec!(0.95));
255/// assert!(matches!(value, Ok(v) if v.to_bits() == 0.95_f64.to_bits()));
256///
257/// // 15 decimal places: the nearest `f64`, which `Decimal::to_f64` misses.
258/// let value = decimal_to_f64(dec!(2.999789999999902));
259/// assert!(matches!(value, Ok(v) if v.to_bits() == 2.999_789_999_999_902_f64.to_bits()));
260/// ```
261pub fn decimal_to_f64(value: Decimal) -> Result<f64, DecimalError> {
262    let mantissa = value.mantissa().unsigned_abs();
263    if mantissa == 0 {
264        return Ok(0.0);
265    }
266    let scale = value.scale();
267    let out_of_range = || decimal_to_f64_error(value, "intermediate outside its range");
268    let divisor = usize::try_from(scale)
269        .ok()
270        .and_then(|index| POWERS_OF_FIVE.get(index))
271        .copied()
272        .ok_or_else(|| decimal_to_f64_error(value, "scale exceeds the 28 places of a Decimal"))?;
273
274    // Left-aligned, the numerator is at least `2^127`, so after the division
275    // by `5^28 < 2^66` the quotient is above `2^61`: at least 62 bits.
276    let shift = mantissa.leading_zeros();
277    let numerator = mantissa << shift;
278    let quotient = numerator.checked_div(divisor).ok_or_else(out_of_range)?;
279    let remainder = numerator.checked_rem(divisor).ok_or_else(out_of_range)?;
280
281    // Keep the top 53 bits and round on the rest: above half rounds up, a
282    // tie (dropped bits exactly half and nothing in the remainder) rounds to
283    // an even significand.
284    let excess = u128::BITS
285        .checked_sub(quotient.leading_zeros())
286        .and_then(|bits| bits.checked_sub(F64_SIGNIFICAND_BITS))
287        .ok_or_else(out_of_range)?;
288    let half = excess
289        .checked_sub(1)
290        .and_then(|bits| 1_u128.checked_shl(bits))
291        .ok_or_else(out_of_range)?;
292    let dropped_mask = half
293        .checked_shl(1)
294        .and_then(|bit| bit.checked_sub(1))
295        .ok_or_else(out_of_range)?;
296    let dropped = quotient & dropped_mask;
297    let mut significand = quotient >> excess;
298    if dropped > half || (dropped == half && (remainder != 0 || significand & 1 == 1)) {
299        significand = significand.checked_add(1).ok_or_else(out_of_range)?;
300    }
301
302    // `significand <= 2^53` converts exactly and the power of two is exact,
303    // so the product is the rounded quotient scaled by `2^-shift * 2^-scale`.
304    let exponent = i32::try_from(excess)
305        .ok()
306        .zip(i32::try_from(shift).ok())
307        .zip(i32::try_from(scale).ok())
308        .and_then(|((excess, shift), scale)| {
309            F64_EXPONENT_BIAS
310                .checked_add(excess)?
311                .checked_sub(shift)?
312                .checked_sub(scale)
313        })
314        .and_then(|biased| u64::try_from(biased).ok())
315        .ok_or_else(out_of_range)?;
316    let power_of_two = f64::from_bits(exponent << F64_EXPONENT_SHIFT);
317    let magnitude = significand as f64 * power_of_two;
318    Ok(if value.is_sign_negative() {
319        -magnitude
320    } else {
321        magnitude
322    })
323}
324
325/// Converts an f64 floating-point number to a Decimal.
326///
327/// This function attempts to convert an f64 floating-point number to a Decimal value.
328/// If the conversion fails (for example, if the f64 represents NaN, infinity, or is otherwise
329/// not representable as a Decimal), it returns a DecimalError with detailed information about
330/// the failure.
331///
332/// # Parameters
333///
334/// * `value` - The f64 value to convert
335///
336/// # Returns
337///
338/// * `Result<Decimal, DecimalError>` - The converted Decimal value if successful, or a DecimalError
339///   if the conversion fails
340///
341/// # Errors
342///
343/// Returns [`DecimalError::ConversionError`] when the `f64` operand is not
344/// representable as a `Decimal`, for example `NaN`, `±Infinity`, or a value
345/// whose magnitude exceeds the `Decimal` range.
346///
347/// # Example
348///
349/// ```rust
350/// use rust_decimal::Decimal;
351/// use tracing::info;
352/// use optionstratlib_core::model::decimal::f64_to_decimal;
353///
354/// let float = std::f64::consts::PI;
355/// match f64_to_decimal(float) {
356///     Ok(decimal) => info!("Converted to Decimal: {}", decimal),
357///     Err(e) => info!("Conversion error: {:?}", e)
358/// }
359/// ```
360pub fn f64_to_decimal(value: f64) -> Result<Decimal, DecimalError> {
361    // `ok_or_else`: the error formats the value and allocates three
362    // `String`s, which `ok_or` paid on every successful call too (#857).
363    Decimal::from_f64(value).ok_or_else(|| DecimalError::ConversionError {
364        from_type: format!("f64: {value}"),
365        to_type: "Decimal".to_string(),
366        reason: "Failed to convert f64 to Decimal".to_string(),
367    })
368}
369
370/// Attempts to convert a finite `f64` into a `Decimal`.
371///
372/// Returns `None` when `value` is not finite (`NaN`, `+∞`, `-∞`) or
373/// when `Decimal::from_f64` rejects the conversion (the latter is
374/// vanishingly rare for representable `f64`). Checked helper
375/// that standardises the `is_finite()` check paired with
376/// `Decimal::from_f64` at every `f64` → `Decimal` boundary inside
377/// pricing, Greeks, volatility, and simulation kernels.
378///
379/// Callers wrap the `None` case with a domain-specific
380/// `*Error::NonFinite { context, value }` via `ok_or_else`:
381///
382/// ```ignore
383/// let v = finite_decimal(v_f64)
384///     .ok_or_else(|| PricingError::non_finite("pricing::bs::call::d1", v_f64))?;
385/// ```
386///
387/// The guard is enforced at the public boundary of every `f64`
388/// numerical kernel per the rules (`rules/global_rules.md`
389/// §Arithmetic).
390#[must_use]
391#[inline]
392pub fn finite_decimal(value: f64) -> Option<Decimal> {
393    if value.is_finite() {
394        Decimal::from_f64(value)
395    } else {
396        None
397    }
398}
399
400/// Draws a standard normal sample from the thread-local RNG, as a `Decimal`.
401///
402/// This is the unseeded path: every call reads the thread RNG, so two runs
403/// never see the same stream. Reproducible callers use
404/// [`decimal_normal_sample_with`] with a seeded generator such as
405/// [`crate::utils::deterministic_rng`]; this function is exactly that helper
406/// applied to [`rand::rng()`].
407///
408/// # Returns
409///
410/// A `Decimal` sampled from a standard normal distribution (it can be
411/// negative).
412///
413/// # Examples
414///
415/// ```rust
416/// use optionstratlib_core::model::decimal::decimal_normal_sample;
417/// let normal = decimal_normal_sample();
418/// ```
419///
420/// The sample is drawn from [`rand_distr::StandardNormal`], which is a unit
421/// struct with no constructor and therefore nothing to reject. It replaced
422/// `Normal::new(0.0, 1.0)`, whose `Err` arm had no value to return and so
423/// aborted. The two are the same distribution and the same value: `Normal`
424/// samples `StandardNormal` and applies `mean + std_dev * z`, which is the
425/// identity at `(0.0, 1.0)`.
426#[must_use]
427pub fn decimal_normal_sample() -> Decimal {
428    decimal_normal_sample_with(&mut rand::rng())
429}
430
431/// Draws a standard normal sample from `rng`, as a `Decimal`.
432///
433/// One `f64` is drawn from [`rand_distr::StandardNormal`] and converted once
434/// with `Decimal::from_f64`; a sample the conversion rejects (none is
435/// finite and out of range in practice) becomes zero, as in
436/// [`decimal_normal_sample`]. The generator advances by exactly the draws
437/// `StandardNormal` makes, so a seeded `rng` yields the same sequence of
438/// samples on every run.
439///
440/// # Examples
441///
442/// ```rust
443/// use optionstratlib_core::model::decimal::decimal_normal_sample_with;
444/// use optionstratlib_core::utils::deterministic_rng;
445///
446/// let mut first = deterministic_rng(42);
447/// let mut second = deterministic_rng(42);
448/// assert_eq!(
449///     decimal_normal_sample_with(&mut first),
450///     decimal_normal_sample_with(&mut second)
451/// );
452/// ```
453#[must_use]
454#[inline]
455pub fn decimal_normal_sample_with<R: Rng + ?Sized>(rng: &mut R) -> Decimal {
456    let sample: f64 = StandardNormal.sample(rng);
457    Decimal::from_f64(sample).unwrap_or(Decimal::ZERO)
458}
459
460/// Decimal places of a [`decimal_uniform_sample_with`] draw.
461const UNIFORM_SAMPLE_SCALE: u32 = 18;
462
463/// Number of grid points of a [`decimal_uniform_sample_with`] draw,
464/// `10^UNIFORM_SAMPLE_SCALE`; below `i64::MAX`, so every point is an exact
465/// `i64` mantissa.
466const UNIFORM_SAMPLE_GRID: i64 = 1_000_000_000_000_000_000;
467
468/// Draws a uniform sample on `[0, 1)` from `rng`, as a `Decimal`.
469///
470/// The sample is `k / 10^18`, with the integer `k` drawn uniformly from
471/// `0..10^18` by [`rand::RngExt::random_range`]. Its range is therefore
472/// `[0, 1 - 10^-18]`: zero is reachable, one never is. No `f64` is
473/// involved, so the value is exact and identical on every platform.
474///
475/// For any threshold `p` in `[0, 1]` with at most 18 decimal places,
476/// `P(sample < p) = p` exactly, up to the bias of `rand`'s single-sample
477/// range method (below `2^-64` per grid point). A threshold with more
478/// places is rounded up to the grid, an error below `10^-18`. This makes
479/// `sample < p` the Bernoulli(`p`) trial of jump and regime-switch
480/// decisions; `p >= 1` always fires and `p <= 0` never does.
481///
482/// The generator advances by the one or two `u64` draws the range method
483/// makes, so a seeded `rng` yields the same sequence of samples on every
484/// run.
485///
486/// # Examples
487///
488/// ```rust
489/// use optionstratlib_core::model::decimal::decimal_uniform_sample_with;
490/// use optionstratlib_core::utils::deterministic_rng;
491/// use rust_decimal::Decimal;
492///
493/// let mut rng = deterministic_rng(42);
494/// let u = decimal_uniform_sample_with(&mut rng);
495/// assert!(u >= Decimal::ZERO && u < Decimal::ONE);
496/// ```
497#[must_use]
498#[inline]
499pub fn decimal_uniform_sample_with<R: Rng + ?Sized>(rng: &mut R) -> Decimal {
500    let k = rng.random_range(0..UNIFORM_SAMPLE_GRID);
501    Decimal::new(k, UNIFORM_SAMPLE_SCALE)
502}
503
504/// Scale applied to banker's-rounding divisions in [`d_div`].
505///
506/// `28` matches `Decimal::MAX_SCALE` so a rounded division preserves every
507/// digit of precision the backing 96-bit mantissa can represent without
508/// triggering a later rescale overflow. Divisions that need a different
509/// scale (for example a P&L that rounds to cents) should apply a subsequent
510/// explicit `.round_dp_with_strategy(dp, RoundingStrategy::MidpointNearestEven)`.
511pub const DIV_DEFAULT_SCALE: u32 = 28;
512
513/// Checked `Decimal` addition with operand-preserving overflow reporting.
514///
515/// Checked helper used by every monetary-flow kernel in place of the
516/// raw `+` operator. Wraps [`Decimal::checked_add`] and converts `None`
517/// into a [`DecimalError::Overflow`] tagged with the static `op` string
518/// passed in by the call-site.
519///
520/// # Errors
521///
522/// Returns [`DecimalError::Overflow`] when the result is outside the
523/// representable `Decimal` range.
524#[inline]
525pub fn d_add(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
526    lhs.checked_add(rhs)
527        .ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
528}
529
530/// Checked sum over a slice of `Decimal` values.
531///
532/// Checked helper used by multi-leg strategy P&L aggregations
533/// (spreads, condors, butterflies) where each leg already returns a
534/// `Result<Decimal, _>` and the sum has to preserve the checked
535/// semantics of the individual legs. Returns `Decimal::ZERO` on an
536/// empty slice.
537///
538/// Delegates to [`d_sum_iter`] so the overflow-tagging semantics stay
539/// in lock-step between the slice and iterator entry points.
540///
541/// # Errors
542///
543/// Returns [`DecimalError::Overflow`] on the first accumulation that
544/// exceeds the representable `Decimal` range, tagged with the
545/// supplied `op` string so the caller can be identified without a
546/// stack trace.
547#[inline]
548pub fn d_sum(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
549    d_sum_iter(values.iter().copied(), op)
550}
551
552/// Checked sum over any `IntoIterator` of `Decimal` values.
553///
554/// Zero-allocation counterpart to [`d_sum`]. Use at aggregation sites
555/// that already have a natural iterator (e.g. `self.positions.iter()
556/// .map(..)`) to avoid the intermediate `Vec<Decimal>` that `d_sum`
557/// forces. Returns `Decimal::ZERO` on an empty iterator.
558///
559/// # Errors
560///
561/// Returns [`DecimalError::Overflow`] on the first accumulation that
562/// exceeds the representable `Decimal` range, tagged with the
563/// supplied `op` string so the caller can be identified without a
564/// stack trace.
565#[inline]
566pub fn d_sum_iter<I>(iter: I, op: &'static str) -> Result<Decimal, DecimalError>
567where
568    I: IntoIterator<Item = Decimal>,
569{
570    let mut acc = Decimal::ZERO;
571    for v in iter {
572        acc = acc
573            .checked_add(v)
574            .ok_or_else(|| DecimalError::overflow(op, acc, v))?;
575    }
576    Ok(acc)
577}
578
579/// Checked `Decimal` subtraction with operand-preserving overflow reporting.
580///
581/// Checked helper used by every monetary-flow kernel in place of the
582/// raw `-` operator. Wraps [`Decimal::checked_sub`] and converts `None`
583/// into a [`DecimalError::Overflow`] tagged with the static `op` string
584/// passed in by the call-site.
585///
586/// # Errors
587///
588/// Returns [`DecimalError::Overflow`] when the result is outside the
589/// representable `Decimal` range.
590#[inline]
591pub fn d_sub(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
592    lhs.checked_sub(rhs)
593        .ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
594}
595
596/// Checked `Decimal` multiplication with operand-preserving overflow reporting.
597///
598/// Checked helper used by every monetary-flow kernel in place of the
599/// raw `*` operator. Wraps [`Decimal::checked_mul`] and converts `None`
600/// into a [`DecimalError::Overflow`] tagged with the static `op` string
601/// passed in by the call-site.
602///
603/// # Errors
604///
605/// Returns [`DecimalError::Overflow`] when the result is outside the
606/// representable `Decimal` range.
607#[inline]
608pub fn d_mul(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
609    lhs.checked_mul(rhs)
610        .ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
611}
612
613/// Checked product over any `IntoIterator` of `Decimal` values.
614///
615/// Multiplicative counterpart to [`d_sum_iter`], with the same
616/// overflow-tagging semantics and the same zero-allocation shape. Returns
617/// [`Decimal::ONE`] on an empty iterator, so an empty product is the
618/// multiplicative identity just as an empty sum is [`Decimal::ZERO`].
619///
620/// # Ordering
621///
622/// The fold runs strictly left to right, and that order is part of the
623/// contract rather than an implementation detail. `Decimal` multiplication
624/// rounds as soon as a product needs more decimal places than `Decimal`'s
625/// scale limit of 28, which makes it non-associative: `(a * b) *
626/// c` and `a * (b * c)` can differ in their last digit. A parallel reducer
627/// picks its bracketing from the chunking rayon happens to choose on the day,
628/// so the same binary on the same input can return different last digits from
629/// one run to the next. Callers that have to reconcile, cache or
630/// regression-test a result at full precision need the fixed order this
631/// gives them.
632///
633/// # Errors
634///
635/// Returns [`DecimalError::Overflow`] on the first accumulation that
636/// exceeds the representable `Decimal` range, tagged with the supplied `op`
637/// string so the call-site can be identified without a stack trace.
638#[inline]
639pub fn d_product_iter<I>(iter: I, op: &'static str) -> Result<Decimal, DecimalError>
640where
641    I: IntoIterator<Item = Decimal>,
642{
643    let mut acc = Decimal::ONE;
644    for v in iter {
645        acc = acc
646            .checked_mul(v)
647            .ok_or_else(|| DecimalError::overflow(op, acc, v))?;
648    }
649    Ok(acc)
650}
651
652/// Checked `Decimal` division with banker's rounding at scale 28.
653///
654/// Checked helper used by every monetary-flow kernel in place of the
655/// raw `/` operator. Performs [`Decimal::checked_div`] then re-rounds the
656/// quotient with [`RoundingStrategy::MidpointNearestEven`] to the default
657/// [`DIV_DEFAULT_SCALE`]. This policy is applied uniformly across the
658/// crate so long-chain divisions do not silently accumulate bias.
659///
660/// # Errors
661///
662/// - Returns [`DecimalError::Overflow`] when the quotient is outside the
663///   representable `Decimal` range.
664/// - Returns [`DecimalError::ArithmeticError`] when `rhs` is zero.
665#[inline]
666pub fn d_div(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
667    if rhs.is_zero() {
668        return Err(DecimalError::arithmetic_error(op, "division by zero"));
669    }
670    let raw = lhs
671        .checked_div(rhs)
672        .ok_or_else(|| DecimalError::overflow(op, lhs, rhs))?;
673    Ok(raw.round_dp_with_strategy(DIV_DEFAULT_SCALE, RoundingStrategy::MidpointNearestEven))
674}
675
676/// Checked `e^x` with underflow flushed to zero.
677///
678/// Checked helper used by every kernel in place of
679/// [`MathematicalOps::exp`], which panics with `Exp overflowed` /
680/// `Exp underflowed` instead of reporting the failure.
681///
682/// A negative argument whose exponential is smaller than the smallest
683/// representable `Decimal` (`1e-28`) returns `Decimal::ZERO`: that is the
684/// value the discount factor takes at the limit, and it is the only
685/// representable answer. A positive argument that overflows is a real
686/// failure and is reported as such — the caller asked for a number the type
687/// cannot hold.
688///
689/// # Errors
690///
691/// Returns [`DecimalError::Overflow`] when `x` is positive and `e^x` is
692/// outside the representable `Decimal` range.
693#[inline]
694pub fn d_exp(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
695    if let Some(value) = x.checked_exp() {
696        return Ok(value);
697    }
698    if x.is_sign_negative() {
699        // e^x < 1e-28 for x <= -65: zero is the representable limit.
700        return Ok(Decimal::ZERO);
701    }
702    Err(DecimalError::overflow(op, x, Decimal::ZERO))
703}
704
705/// Checked natural logarithm.
706///
707/// Checked helper used in place of [`MathematicalOps::ln`], which
708/// panics on zero and on negative inputs. Note that a ratio such as `S / K`
709/// can round down to exactly zero for extreme operands, so the zero case is
710/// reachable from ordinary-looking code.
711///
712/// # Errors
713///
714/// Returns [`DecimalError::ArithmeticError`] when `x` is zero or negative,
715/// or when the series evaluation fails to converge to a representable
716/// value.
717#[inline]
718pub fn d_ln(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
719    if x <= Decimal::ZERO {
720        return Err(DecimalError::arithmetic_error(
721            op,
722            "logarithm of a non-positive value",
723        ));
724    }
725    x.checked_ln()
726        .ok_or_else(|| DecimalError::arithmetic_error(op, "logarithm is not representable"))
727}
728
729/// Natural logarithm evaluated in `f64`, for the numeric kernels (#857).
730///
731/// [`d_ln`] runs `rust_decimal`'s series to 28 places and costs about 8 µs;
732/// a kernel whose other inputs are already `f64`-accurate gains nothing from
733/// those places. This takes the logarithm in `f64` and converts the result
734/// back once:
735///
736/// - for `|x - 1| < 1/2` it evaluates `ln_1p(x - 1)`, with `x - 1` exact in
737///   `Decimal`, so the result keeps its relative accuracy as `x` approaches
738///   one (where `ln(x)` would lose it to cancellation) and `ln(1)` is exactly
739///   zero;
740/// - elsewhere it evaluates `ln(x)`, whose magnitude is at least `ln(3/2)`.
741///
742/// In both cases the result is within a few `f64` ulps, about `1e-15`
743/// relative, of the exact logarithm. The public [`d_ln`] stays `Decimal`.
744///
745/// # Errors
746///
747/// Returns [`DecimalError::ArithmeticError`] when `x` is zero or negative, or
748/// when the `f64` result is not finite, and the conversion errors of
749/// [`decimal_to_f64`] and [`f64_to_decimal`].
750#[inline]
751pub fn d_ln_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
752    if x <= Decimal::ZERO {
753        return Err(DecimalError::arithmetic_error(
754            op,
755            "logarithm of a non-positive value",
756        ));
757    }
758    let shifted = x
759        .checked_sub(Decimal::ONE)
760        .ok_or_else(|| DecimalError::arithmetic_error(op, "x - 1 is not representable"))?;
761    let value = if shifted.abs() < LN_1P_BAND {
762        decimal_to_f64(shifted)?.ln_1p()
763    } else {
764        decimal_to_f64(x)?.ln() // scan-banned: allow -- f64 `ln`: x > 0 here, and `f64::ln` returns -inf/NaN rather than aborting; a non-finite result is rejected below
765    };
766    if !value.is_finite() {
767        return Err(DecimalError::arithmetic_error(
768            op,
769            "logarithm is not finite in f64",
770        ));
771    }
772    f64_to_decimal(value)
773}
774
775/// `|x - 1|` below which [`d_ln_f64`] evaluates `ln_1p(x - 1)`.
776const LN_1P_BAND: Decimal = Decimal::from_parts(5, 0, 0, false, 1);
777
778/// Exponential evaluated in `f64`, for the per-step walk kernels (#860).
779///
780/// [`d_exp`] runs `rust_decimal`'s series to 28 places, about 0.8 µs a call;
781/// a walk step whose shock is an `f64` normal draw gains nothing from those
782/// places. This takes `e^x` in `f64` (within an ulp, about `1e-16`
783/// relative) and converts the result back once, with [`d_exp`]'s contract:
784/// a negative argument whose exponential is below the representable range
785/// returns zero, a positive one that overflows is an error. The public
786/// [`d_exp`] stays the `Decimal` series.
787///
788/// # Errors
789///
790/// Returns [`DecimalError::Overflow`] when `x` is positive and `e^x` is
791/// outside the representable `Decimal` range, and the conversion error of
792/// [`decimal_to_f64`].
793#[inline]
794pub fn d_exp_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
795    let value = decimal_to_f64(x)?.exp(); // scan-banned: allow -- f64 `exp`: returns inf on overflow, it does not abort; a non-finite or unrepresentable value is reported below
796    match finite_decimal(value) {
797        Some(result) => Ok(result),
798        None if x.is_sign_negative() => Ok(Decimal::ZERO),
799        None => Err(DecimalError::overflow(op, x, Decimal::ZERO)),
800    }
801}
802
803/// Square root evaluated in `f64`, for the per-step walk kernels (#860).
804///
805/// IEEE-754 `sqrt` is correctly rounded, so on every platform this is the
806/// nearest `f64` to the square root of the nearest `f64` to `x`, converted
807/// back once: about `1e-16` relative of the 28-place [`d_sqrt`], at a
808/// fraction of its Newton iteration.
809///
810/// # Errors
811///
812/// Returns [`DecimalError::ArithmeticError`] when `x` is negative, and the
813/// conversion errors of [`decimal_to_f64`] and [`f64_to_decimal`].
814#[inline]
815pub fn d_sqrt_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
816    if x.is_sign_negative() && !x.is_zero() {
817        return Err(DecimalError::arithmetic_error(
818            op,
819            "square root of a negative value",
820        ));
821    }
822    f64_to_decimal(decimal_to_f64(x)?.sqrt()) // scan-banned: allow -- f64 `sqrt` of a non-negative finite value is finite and does not abort
823}
824
825/// Checked `base^exponent`.
826///
827/// Checked helper used in place of [`MathematicalOps::powd`], which
828/// panics with `Pow overflowed` both when the result is too large and when
829/// it underflows below the representable scale.
830///
831/// # Errors
832///
833/// Returns [`DecimalError::Overflow`] when the power is outside the
834/// representable `Decimal` range.
835#[inline]
836pub fn d_powd(base: Decimal, exponent: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
837    base.checked_powd(exponent)
838        .ok_or_else(|| DecimalError::overflow(op, base, exponent))
839}
840
841/// Upper bound on the Newton steps of [`d_sqrt`]; the upstream iteration
842/// converges in a few dozen steps for every representable input and only
843/// exceeds this bound when it oscillates between two adjacent values.
844const SQRT_MAX_ITERATIONS: u32 = 1000;
845
846/// Checked square root.
847///
848/// Checked replacement for [`MathematicalOps::sqrt`]. The upstream
849/// implementation (`rust_decimal` 1.43, `maths.rs`) runs the same Newton
850/// iteration but aborts the process with `geo mean circuit breaker` when the
851/// iteration oscillates between two values that differ in the 28th decimal
852/// instead of converging, which happens for inputs a few units above a
853/// perfect square such as `4.0000000000000000000000000003` (#588). This
854/// version keeps the upstream initial guess and update step, so every input
855/// on which upstream converges yields the bit-identical result, and resolves
856/// the period-2 oscillation the moment it appears by returning the candidate
857/// whose square is closest to `x` (a bounded iteration count is the backstop
858/// for any other non-convergence).
859///
860/// # Errors
861///
862/// Returns [`DecimalError::ArithmeticError`] when `x` is negative or when an
863/// intermediate quotient or sum leaves the representable `Decimal` range.
864pub fn d_sqrt(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
865    sqrt_with_iterations(x, op).map(|(value, _)| value)
866}
867
868/// The Newton iteration behind [`d_sqrt`], reporting how many steps it took.
869///
870/// The count is what proves the period-2 cycle is resolved as soon as it
871/// appears rather than at the backstop; a test that measured elapsed time
872/// instead failed under coverage instrumentation, which slows every call
873/// (#604).
874fn sqrt_with_iterations(x: Decimal, op: &'static str) -> Result<(Decimal, u32), DecimalError> {
875    if x.is_sign_negative() {
876        return Err(DecimalError::arithmetic_error(
877            op,
878            "square root of a negative value",
879        ));
880    }
881    if x.is_zero() {
882        return Ok((Decimal::ZERO, 0));
883    }
884    let overflow = || DecimalError::arithmetic_error(op, "square root iteration overflowed");
885    // Same seed as upstream: half the input, or the input itself when the
886    // half is not representable.
887    let mut result = x.checked_div(Decimal::TWO).ok_or_else(overflow)?;
888    if result.is_zero() {
889        result = x;
890    }
891    let mut last = result.checked_add(Decimal::ONE).ok_or_else(overflow)?;
892    let mut before_last = last;
893    let mut iterations = 0u32;
894    // Squared distance from `x`; a candidate whose square is not
895    // representable is treated as infinitely far so the other one wins.
896    let error_of = |candidate: Decimal| -> Decimal {
897        candidate
898            .checked_mul(candidate)
899            .and_then(|square| square.checked_sub(x))
900            .map(|d| d.abs())
901            .unwrap_or(Decimal::MAX)
902    };
903    while last != result {
904        iterations = iterations.checked_add(1).ok_or_else(overflow)?;
905        // A period-2 cycle (`result` back to the value two steps ago) is how
906        // the upstream iteration fails to converge; resolve it as soon as it
907        // appears. The iteration bound is the backstop for anything else.
908        if result == before_last || iterations > SQRT_MAX_ITERATIONS {
909            return if error_of(last) <= error_of(result) {
910                Ok((last, iterations))
911            } else {
912                Ok((result, iterations))
913            };
914        }
915        before_last = last;
916        last = result;
917        let quotient = x.checked_div(result).ok_or_else(overflow)?;
918        result = result
919            .checked_add(quotient)
920            .ok_or_else(overflow)?
921            .checked_div(Decimal::TWO)
922            .ok_or_else(overflow)?;
923    }
924    Ok((result, iterations))
925}
926
927/// Checked square root of a [`Positive`], routed through [`d_sqrt`] so that
928/// no production path reaches the panicking upstream `sqrt` that
929/// `Positive::checked_sqrt` still wraps (#588).
930///
931/// # Errors
932///
933/// Returns [`PositiveError::ArithmeticError`] when the iteration overflows
934/// or the root cannot be represented as a `Positive`; the error type matches
935/// `Positive::checked_sqrt` so call sites keep their conversions.
936#[inline]
937pub fn p_sqrt(x: &Positive, op: &'static str) -> Result<Positive, PositiveError> {
938    let root =
939        d_sqrt(x.to_dec(), op).map_err(|e| PositiveError::arithmetic_error(op, &e.to_string()))?;
940    Positive::new_decimal(root)
941}
942
943/// Converts a Decimal value to f64 without error checking.
944///
945/// This macro converts a Decimal type to an f64 floating-point value.
946/// It's an "unchecked" version that doesn't handle potential conversion errors.
947///
948/// # Parameters
949/// * `$val` - A Decimal value to be converted to f64
950///
951/// # Example
952/// ```rust
953/// use rust_decimal_macros::dec;
954/// use optionstratlib_core::d2fu;
955/// let decimal_value = dec!(10.5);
956/// let float_value = d2fu!(decimal_value);
957/// ```
958#[macro_export]
959macro_rules! d2fu {
960    ($val:expr) => {
961        $crate::model::decimal::decimal_to_f64($val)
962    };
963}
964
965/// Converts a Decimal value to f64 with error propagation.
966///
967/// This macro converts a Decimal type to an f64 floating-point value.
968/// It propagates any errors that might occur during conversion using the `?` operator.
969///
970/// # Parameters
971/// * `$val` - A Decimal value to be converted to f64
972///
973#[macro_export]
974macro_rules! d2f {
975    ($val:expr) => {
976        $crate::model::decimal::decimal_to_f64($val)?
977    };
978}
979
980/// Builds a `NonZeroUsize` from a literal or constant expression.
981///
982/// Ergonomic shorthand for the `NonZeroUsize::new(N).expect(..)` pattern
983/// at call sites that know the value is non-zero by construction (tests,
984/// examples, benchmarks). Use this whenever passing literal step or
985/// simulation counts to one of the public pricing kernels migrated
986/// in #337.
987///
988/// # Panics
989///
990/// Panics with a descriptive message if `$val` evaluates to zero. For
991/// runtime values coming from JSON, CLI, or external APIs prefer
992/// `NonZeroUsize::new(x).ok_or_else(..)` at the boundary instead.
993///
994/// # Examples
995///
996/// ```rust
997/// use optionstratlib_core::nz;
998/// use std::num::NonZeroUsize;
999///
1000/// let steps = nz!(100);
1001/// assert_eq!(steps.get(), 100);
1002/// ```
1003#[macro_export]
1004macro_rules! nz {
1005    ($val:expr) => {{
1006        // The panic expands at the caller, never inside the library: no
1007        // production code uses `nz!`, only tests, examples and doc examples,
1008        // all of which pass a non-zero literal. A runtime count must go
1009        // through `NonZeroUsize::new(x).ok_or_else(..)` at the boundary, as
1010        // the doc comment above says.
1011        ::std::num::NonZeroUsize::new($val)
1012            .unwrap_or_else(|| panic!("nz!({}) must be non-zero", stringify!($val))) // scan-banned: allow -- expands at the caller only; no library code uses `nz!`
1013    }};
1014}
1015
1016/// Converts an f64 value to Decimal without error checking.
1017///
1018/// This macro converts an f64 floating-point value to a Decimal type.
1019/// It's an "unchecked" version that doesn't handle potential conversion errors.
1020///
1021/// # Parameters
1022/// * `$val` - An f64 value to be converted to Decimal
1023///
1024/// # Example
1025/// ```rust
1026/// use optionstratlib_core::f2du;
1027/// let float_value = 10.5;
1028/// let decimal_value = f2du!(float_value);
1029/// ```
1030#[macro_export]
1031macro_rules! f2du {
1032    ($val:expr) => {
1033        $crate::model::decimal::f64_to_decimal($val)
1034    };
1035}
1036
1037/// Converts an f64 value to Decimal with error propagation.
1038///
1039/// This macro converts an f64 floating-point value to a Decimal type.
1040/// It propagates any errors that might occur during conversion using the `?` operator.
1041///
1042/// # Parameters
1043/// * `$val` - An f64 value to be converted to Decimal
1044///
1045#[macro_export]
1046macro_rules! f2d {
1047    ($val:expr) => {
1048        $crate::model::decimal::f64_to_decimal($val)?
1049    };
1050}
1051
1052/// Conversion helpers' sanity tests (public module to support doctest wiring).
1053#[cfg(test)]
1054pub mod tests {
1055    use super::*;
1056    use std::str::FromStr;
1057
1058    #[test]
1059    fn test_f64_to_decimal_valid() {
1060        let value = 42.42;
1061        let result = f64_to_decimal(value);
1062        assert!(result.is_ok());
1063        assert_eq!(result.unwrap(), Decimal::from_str("42.42").unwrap());
1064    }
1065
1066    #[test]
1067    fn test_f64_to_decimal_zero() {
1068        let value = 0.0;
1069        let result = f64_to_decimal(value);
1070        assert!(result.is_ok());
1071        assert_eq!(result.unwrap(), Decimal::from_str("0").unwrap());
1072    }
1073
1074    #[test]
1075    fn test_decimal_to_f64_valid() {
1076        let decimal = Decimal::from_str("42.42").unwrap();
1077        let result = decimal_to_f64(decimal);
1078        assert!(result.is_ok());
1079        assert_eq!(result.unwrap(), 42.42);
1080    }
1081
1082    #[test]
1083    fn test_decimal_to_f64_zero() {
1084        let decimal = Decimal::from_str("0").unwrap();
1085        let result = decimal_to_f64(decimal);
1086        assert!(result.is_ok());
1087        assert_eq!(result.unwrap(), 0.0);
1088    }
1089
1090    /// Reproducer from #588: upstream `Decimal::sqrt` aborts with
1091    /// `geo mean circuit breaker` on this input because the Newton
1092    /// iteration oscillates at the 28th decimal instead of converging.
1093    #[test]
1094    fn test_d_sqrt_resolves_upstream_circuit_breaker() {
1095        let x = dec!(4.0000000000000000000000000003);
1096        let root = d_sqrt(x, "test").unwrap();
1097        let residual = (root * root - x).abs();
1098        assert!(
1099            residual <= dec!(0.0000000000000000000000000001),
1100            "sqrt({x}) = {root}, residual {residual}"
1101        );
1102    }
1103
1104    /// Every input on which upstream converges must yield the bit-identical
1105    /// result: same seed, same update step.
1106    #[test]
1107    fn test_d_sqrt_matches_upstream_on_converging_inputs() {
1108        let inputs = [
1109            Decimal::ZERO,
1110            Decimal::ONE,
1111            Decimal::TWO,
1112            dec!(4),
1113            dec!(0.25),
1114            dec!(0.0000000000000000000000000001),
1115            dec!(123456.789),
1116            Decimal::MAX,
1117        ];
1118        for x in inputs {
1119            let expected = x.sqrt().unwrap_or_default();
1120            assert_eq!(d_sqrt(x, "test").unwrap(), expected, "sqrt({x})");
1121        }
1122    }
1123
1124    /// The bit-identical claim, checked byte for byte on a seeded sweep:
1125    /// random mantissa/scale pairs plus squares of short roots nudged by a
1126    /// few units in the 28th decimal (the region where upstream can
1127    /// oscillate). Inputs on which upstream itself aborts are skipped
1128    /// through `catch_unwind`; a converging upstream result must be
1129    /// reproduced exactly, `serialize()` bytes included, so a scale
1130    /// difference cannot hide behind numerical equality.
1131    #[test]
1132    fn test_d_sqrt_matches_upstream_on_a_seeded_sweep() {
1133        use rand::{RngExt, SeedableRng};
1134        let mut rng = rand::rngs::StdRng::seed_from_u64(0x5eed_5eed_0588);
1135        let mut inputs: Vec<Decimal> = Vec::with_capacity(6_000);
1136        for _ in 0..4_000 {
1137            let mantissa: i64 = rng.random_range(0..=i64::MAX);
1138            let scale: u32 = rng.random_range(0..=28);
1139            inputs.push(Decimal::new(mantissa, scale));
1140        }
1141        let ulp = dec!(0.0000000000000000000000000001);
1142        for root in [1u32, 2, 3, 7, 12, 100, 1_000, 65_536] {
1143            let square = Decimal::from(root) * Decimal::from(root);
1144            for units in -6i32..=6 {
1145                let nudge = ulp * Decimal::from(units);
1146                if let Some(x) = square.checked_add(nudge) {
1147                    inputs.push(x);
1148                }
1149            }
1150        }
1151        let mut compared = 0usize;
1152        let mut skipped = 0usize;
1153        for x in inputs {
1154            let upstream = std::panic::catch_unwind(|| x.sqrt());
1155            match upstream {
1156                Ok(Some(expected)) => {
1157                    let actual = d_sqrt(x, "sweep").unwrap();
1158                    assert_eq!(
1159                        actual.serialize(),
1160                        expected.serialize(),
1161                        "sqrt({x}): upstream {expected}, d_sqrt {actual}"
1162                    );
1163                    compared += 1;
1164                }
1165                Ok(None) => unreachable!("non-negative inputs only"),
1166                Err(_) => {
1167                    // Upstream aborted: the whole point of d_sqrt.
1168                    assert!(d_sqrt(x, "sweep").is_ok(), "d_sqrt({x}) must be total");
1169                    skipped += 1;
1170                }
1171            }
1172        }
1173        assert!(compared > 4_000, "compared {compared}, skipped {skipped}");
1174    }
1175
1176    /// The reproducer resolves at the third iteration, not at the bound:
1177    /// a period-2 cycle is detected as soon as it appears.
1178    #[test]
1179    fn test_d_sqrt_resolves_the_cycle_in_a_few_iterations() {
1180        // The property is a count, not a duration: the period-2 cycle is
1181        // resolved as soon as it appears instead of running to the
1182        // 1000-iteration backstop. A wall-clock assertion here failed under
1183        // coverage instrumentation, which slows every call (#604).
1184        let oscillating = dec!(4.0000000000000000000000000003);
1185        let (_, iterations) = sqrt_with_iterations(oscillating, "cycle").unwrap();
1186        assert!(
1187            iterations < 10,
1188            "cycle detection is not immediate: {iterations} iterations"
1189        );
1190        assert!(iterations < SQRT_MAX_ITERATIONS);
1191        // Control: an input upstream converges on takes a comparable number
1192        // of steps, so the bound above is not vacuous.
1193        let (_, converging) =
1194            sqrt_with_iterations(dec!(4.0000000000000000000000000004), "control").unwrap();
1195        assert!(converging < 60, "control took {converging} iterations");
1196    }
1197
1198    #[test]
1199    fn test_d_sqrt_rejects_negative_input() {
1200        assert!(d_sqrt(dec!(-1), "test").is_err());
1201        assert!(d_sqrt(dec!(-0.0000000000000000000000000001), "test").is_err());
1202    }
1203
1204    #[test]
1205    fn test_p_sqrt_matches_d_sqrt() {
1206        let x = Positive::new_decimal(dec!(4.0000000000000000000000000003)).unwrap();
1207        let expected = d_sqrt(x.to_dec(), "test").unwrap();
1208        assert_eq!(p_sqrt(&x, "test").unwrap().to_dec(), expected);
1209    }
1210}
1211
1212#[cfg(test)]
1213mod tests_decimal_stats {
1214    use super::*;
1215
1216    #[test]
1217    fn test_decimal_stats_empty_and_singleton_return_zero() {
1218        let empty: Vec<Decimal> = vec![];
1219        assert_eq!(empty.mean().unwrap(), Decimal::ZERO);
1220        assert_eq!(empty.std_dev().unwrap(), Decimal::ZERO);
1221        let one = vec![dec!(3)];
1222        assert_eq!(one.mean().unwrap(), dec!(3));
1223        assert_eq!(one.std_dev().unwrap(), Decimal::ZERO);
1224    }
1225
1226    #[test]
1227    fn test_decimal_stats_ordinary_sample_is_unchanged() {
1228        // The textbook series: mean 5, sample variance 32 / 7.
1229        let values = vec![
1230            dec!(2),
1231            dec!(4),
1232            dec!(4),
1233            dec!(4),
1234            dec!(5),
1235            dec!(5),
1236            dec!(7),
1237            dec!(9),
1238        ];
1239        assert_eq!(values.mean().unwrap(), dec!(5));
1240        let expected = d_div(dec!(32), dec!(7), "test")
1241            .and_then(|v| d_sqrt(v, "test"))
1242            .unwrap();
1243        assert_eq!(values.std_dev().unwrap(), expected);
1244    }
1245
1246    #[test]
1247    fn test_decimal_stats_saturated_sample_reports_overflow_instead_of_aborting() {
1248        // `iter().sum()` aborted here with `Addition overflowed`, and the
1249        // squaring in `std_dev` with `Pow overflowed`. Both are a
1250        // `DecimalError` the caller can read now.
1251        let values = vec![Decimal::MAX, Decimal::MAX];
1252        assert!(values.mean().is_err());
1253        assert!(values.std_dev().is_err());
1254    }
1255
1256    #[test]
1257    fn test_decimal_stats_std_dev_reports_a_deviation_that_overflows() {
1258        // The mean is finite but a centred deviation is not.
1259        let values = vec![Decimal::MAX, Decimal::MIN, Decimal::MAX, Decimal::MIN];
1260        assert!(values.std_dev().is_err());
1261    }
1262}
1263
1264#[cfg(test)]
1265mod tests_random_generation {
1266    use super::*;
1267    use approx::assert_relative_eq;
1268    use num_traits::ToPrimitive;
1269    use rand::distr::Distribution;
1270    use std::collections::HashMap;
1271
1272    /// Seed of the sampled streams, so every run draws the same values.
1273    const SEED: u64 = 685;
1274
1275    #[test]
1276    fn test_normal_sample_returns() {
1277        // Run the function multiple times to ensure it always returns a positive value
1278        let mut rng = crate::utils::deterministic_rng(SEED);
1279        for _ in 0..1000 {
1280            let sample = decimal_normal_sample_with(&mut rng);
1281            assert!(sample <= Decimal::TEN);
1282            assert!(sample >= -Decimal::TEN);
1283        }
1284    }
1285
1286    #[test]
1287    fn test_normal_sample_distribution() {
1288        // Generate a large number of samples to check distribution characteristics
1289        const NUM_SAMPLES: usize = 10000;
1290        let mut samples = Vec::with_capacity(NUM_SAMPLES);
1291        let mut rng = crate::utils::deterministic_rng(SEED);
1292
1293        for _ in 0..NUM_SAMPLES {
1294            samples.push(decimal_normal_sample_with(&mut rng).to_f64().unwrap());
1295        }
1296
1297        // Calculate mean and standard deviation
1298        let sum: f64 = samples.iter().sum();
1299        let mean = sum / NUM_SAMPLES as f64;
1300
1301        let variance_sum: f64 = samples.iter().map(|&x| (x - mean).powi(2)).sum();
1302        let std_dev = (variance_sum / NUM_SAMPLES as f64).sqrt();
1303
1304        // Check if the distribution approximately matches a standard normal
1305        // Note: These tests use wide tolerances since we're working with random samples
1306        assert_relative_eq!(mean, 0.0, epsilon = 0.04);
1307        assert_relative_eq!(std_dev, 1.0, epsilon = 0.03);
1308    }
1309
1310    #[test]
1311    fn test_normal_distribution_transformation() {
1312        let mut t_rng = crate::utils::deterministic_rng(SEED);
1313        // Deliberately a distribution with a negative mean.
1314        let normal = rand_distr::Normal::new(-1.0, 0.5).unwrap();
1315
1316        // Count occurrences of values after transformation
1317        let mut value_counts: HashMap<i32, usize> = HashMap::new();
1318        const SAMPLES: usize = 5000;
1319
1320        for _ in 0..SAMPLES {
1321            let raw_sample = normal.sample(&mut t_rng);
1322            let positive_sample = raw_sample.to_f64().unwrap();
1323
1324            // Bucket values to the nearest integer for counting
1325            let bucket = (positive_sample.round() as i32).max(0);
1326            *value_counts.entry(bucket).or_insert(0) += 1;
1327        }
1328
1329        // Verify that zero values appear frequently (due to negative values being transformed)
1330        assert!(value_counts.get(&0).unwrap_or(&0) > &(SAMPLES / 10));
1331
1332        // Verify that we have a range of positive values
1333        let max_bucket = value_counts.keys().max().unwrap_or(&0);
1334        assert!(*max_bucket > 0);
1335    }
1336
1337    #[test]
1338    fn test_normal_sample_consistency() {
1339        // This test ensures that multiple calls in sequence produce different values
1340        // Unseeded on purpose: the test is about the thread-RNG entry point.
1341        let sample1 = decimal_normal_sample();
1342        let sample2 = decimal_normal_sample();
1343        let sample3 = decimal_normal_sample();
1344
1345        // It's statistically extremely unlikely to get the same value three times in a row
1346        // This verifies that the RNG is properly producing different values
1347        assert!(sample1 != sample2 || sample2 != sample3);
1348    }
1349
1350    #[test]
1351    fn test_decimal_normal_sample_with_same_seed_is_identical() {
1352        let mut first = crate::utils::deterministic_rng(42);
1353        let mut second = crate::utils::deterministic_rng(42);
1354        for _ in 0..1000 {
1355            assert_eq!(
1356                decimal_normal_sample_with(&mut first),
1357                decimal_normal_sample_with(&mut second)
1358            );
1359        }
1360    }
1361
1362    #[test]
1363    fn test_decimal_normal_sample_with_is_the_converted_standard_normal_draw() {
1364        // The helper adds nothing to the stream but the `Decimal` conversion.
1365        let mut helper = crate::utils::deterministic_rng(7);
1366        let mut raw = crate::utils::deterministic_rng(7);
1367        for _ in 0..1000 {
1368            let draw: f64 = StandardNormal.sample(&mut raw);
1369            assert_eq!(
1370                decimal_normal_sample_with(&mut helper),
1371                Decimal::from_f64(draw).unwrap_or(Decimal::ZERO)
1372            );
1373        }
1374    }
1375
1376    #[test]
1377    fn test_decimal_normal_sample_with_seed_pins_first_draws() {
1378        let mut rng = crate::utils::deterministic_rng(42);
1379        let draws: Vec<Decimal> = (0..3)
1380            .map(|_| decimal_normal_sample_with(&mut rng))
1381            .collect();
1382        // Recorded from this implementation (`StdRng`, rand 0.10).
1383        let expected = vec![
1384            dec!(0.0694279183619634),
1385            dec!(0.1329381219941254),
1386            dec!(0.2625763573739537),
1387        ];
1388        assert_eq!(draws, expected);
1389    }
1390
1391    #[test]
1392    fn test_decimal_uniform_sample_with_range_is_half_open_unit_interval() {
1393        let mut rng = crate::utils::deterministic_rng(SEED);
1394        for _ in 0..100_000 {
1395            let u = decimal_uniform_sample_with(&mut rng);
1396            assert!(u >= Decimal::ZERO, "{u} below zero");
1397            assert!(u < Decimal::ONE, "{u} not below one");
1398            assert_eq!(u.scale(), UNIFORM_SAMPLE_SCALE, "{u} off the grid");
1399        }
1400    }
1401
1402    #[test]
1403    fn test_decimal_uniform_sample_with_same_seed_is_identical() {
1404        let mut first = crate::utils::deterministic_rng(42);
1405        let mut second = crate::utils::deterministic_rng(42);
1406        for _ in 0..1000 {
1407            assert_eq!(
1408                decimal_uniform_sample_with(&mut first),
1409                decimal_uniform_sample_with(&mut second)
1410            );
1411        }
1412    }
1413
1414    #[test]
1415    fn test_decimal_uniform_sample_with_seed_pins_first_draws() {
1416        let mut rng = crate::utils::deterministic_rng(42);
1417        let draws: Vec<Decimal> = (0..3)
1418            .map(|_| decimal_uniform_sample_with(&mut rng))
1419            .collect();
1420        // Recorded from this implementation (`StdRng`, rand 0.10).
1421        let expected = vec![
1422            dec!(0.526557409002773877),
1423            dec!(0.542725209903143899),
1424            dec!(0.636465099143894996),
1425        ];
1426        assert_eq!(draws, expected);
1427    }
1428
1429    #[test]
1430    fn test_decimal_uniform_sample_with_moments_match_uniform() {
1431        // U(0,1) has mean 1/2 and variance 1/12. Over N draws the sample
1432        // mean has standard error sqrt(1/12 / N) = 0.000913 at N = 100_000;
1433        // the bound is five standard errors. The empirical frequency of
1434        // `u < p` is Binomial(N, p) / N, standard error sqrt(p (1-p) / N),
1435        // and each is checked within five standard errors as well.
1436        const N: usize = 100_000;
1437        let mut rng = crate::utils::deterministic_rng(SEED);
1438        let thresholds = [dec!(0.004), dec!(0.1), dec!(0.5), dec!(0.9)];
1439        let mut below = [0_u64; 4];
1440        let mut sum = 0.0_f64;
1441        for _ in 0..N {
1442            let u = decimal_uniform_sample_with(&mut rng);
1443            sum += u.to_f64().unwrap();
1444            for (count, p) in below.iter_mut().zip(thresholds) {
1445                if u < p {
1446                    *count += 1;
1447                }
1448            }
1449        }
1450        let n = N as f64;
1451        let mean_se = (1.0 / 12.0 / n).sqrt();
1452        assert!((sum / n - 0.5).abs() < 5.0 * mean_se, "mean {}", sum / n);
1453        for (count, p) in below.iter().zip(thresholds) {
1454            let p = p.to_f64().unwrap();
1455            let freq = *count as f64 / n;
1456            let se = (p * (1.0 - p) / n).sqrt();
1457            assert!((freq - p).abs() < 5.0 * se, "P(u < {p}) = {freq}");
1458        }
1459    }
1460
1461    /// Generator that returns the same 64-bit word on every draw.
1462    struct ConstantWord(u64);
1463
1464    impl rand::TryRng for ConstantWord {
1465        type Error = std::convert::Infallible;
1466
1467        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
1468            Ok((self.0 >> 32) as u32)
1469        }
1470
1471        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
1472            Ok(self.0)
1473        }
1474
1475        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
1476            dst.fill(0);
1477            Ok(())
1478        }
1479    }
1480
1481    #[test]
1482    fn test_decimal_uniform_sample_with_extreme_words_hit_grid_ends() {
1483        // The all-zero word maps to the lowest grid point, the all-ones word
1484        // to the highest: the range is exactly `[0, 1 - 10^-18]`.
1485        assert_eq!(
1486            decimal_uniform_sample_with(&mut ConstantWord(0)),
1487            Decimal::ZERO
1488        );
1489        assert_eq!(
1490            decimal_uniform_sample_with(&mut ConstantWord(u64::MAX)),
1491            dec!(0.999999999999999999)
1492        );
1493    }
1494}
1495
1496#[cfg(test)]
1497#[allow(clippy::unwrap_used)]
1498mod checked_helpers_tests {
1499    use super::*;
1500
1501    #[test]
1502    fn d_add_happy_path() {
1503        let result = d_add(dec!(1.25), dec!(2.50), "test::add");
1504        assert_eq!(result.unwrap(), dec!(3.75));
1505    }
1506
1507    #[test]
1508    fn d_add_overflow_on_max_plus_max() {
1509        let err = d_add(Decimal::MAX, Decimal::MAX, "test::add").unwrap_err();
1510        match err {
1511            DecimalError::Overflow { operation, .. } => assert_eq!(operation, "test::add"),
1512            other => panic!("expected Overflow, got {other:?}"),
1513        }
1514    }
1515
1516    /// A checked add that reports `Ok` has not necessarily moved the value.
1517    ///
1518    /// `Decimal::MAX` has scale 0 and a mantissa filling all 96 bits. Adding a
1519    /// fractional addend needs a 29th significant digit, which does not exist,
1520    /// so `rust_decimal` rescales the addend down to scale 0 instead of failing:
1521    /// anything below half a unit rounds to zero, the addition succeeds, and the
1522    /// result is `Decimal::MAX` again. The overflow path is never reached. The
1523    /// silent window is `|x| < 0.5`, not `|x| < 1` — at `0.5` the addend rounds
1524    /// away from zero to `1` and the sum does overflow, which is why the two
1525    /// halves of this test sit three hundredths apart.
1526    ///
1527    /// This has already broken two call sites in this crate:
1528    ///
1529    /// - `chains::optiondata::unlock` lifted a locked quote by one tick and read
1530    ///   `Ok` as "lifted", so it re-emitted the very quote it existed to remove.
1531    /// - `greeks::equations::alpha_from` returns `Decimal::MAX` as its
1532    ///   "theta vanished" sentinel. Summing one such leg with an ordinary one
1533    ///   returned `Ok` with the running total standing still and the ordinary
1534    ///   leg's alpha silently dropped, so that aggregation needs an explicit
1535    ///   guard on the operand; no arithmetic check can catch it.
1536    ///
1537    /// The rule this implies: when you nudge a value and need to know it moved,
1538    /// compare the result against the input. `checked_*` returning `Ok` is not
1539    /// that check — it only says the result was representable.
1540    #[test]
1541    fn checked_add_near_max_reports_ok_without_moving_the_value() {
1542        // Below half a unit: rounds to zero, succeeds, and stands still.
1543        assert_eq!(
1544            Decimal::MAX.checked_add(dec!(0.01)),
1545            Some(Decimal::MAX),
1546            "a fractional addend rescales away instead of overflowing"
1547        );
1548        assert_eq!(
1549            d_add(Decimal::MAX, dec!(0.47), "test::add").unwrap(),
1550            Decimal::MAX,
1551            "the crate helper inherits the same behaviour"
1552        );
1553
1554        // At half a unit and above: rounds away from zero and does overflow, so
1555        // the boundary is visible rather than implied.
1556        assert_eq!(Decimal::MAX.checked_add(dec!(0.5)), None);
1557        assert!(d_add(Decimal::MAX, dec!(0.5), "test::add").is_err());
1558    }
1559
1560    #[test]
1561    fn d_sub_happy_path() {
1562        let result = d_sub(dec!(10), dec!(3.5), "test::sub");
1563        assert_eq!(result.unwrap(), dec!(6.5));
1564    }
1565
1566    #[test]
1567    fn d_sub_overflow_on_min_minus_max() {
1568        let err = d_sub(Decimal::MIN, Decimal::MAX, "test::sub").unwrap_err();
1569        assert!(
1570            matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sub")
1571        );
1572    }
1573
1574    #[test]
1575    fn d_mul_happy_path() {
1576        let result = d_mul(dec!(2.5), dec!(4), "test::mul");
1577        assert_eq!(result.unwrap(), dec!(10.0));
1578    }
1579
1580    #[test]
1581    fn d_mul_overflow_on_max_times_two() {
1582        let err = d_mul(Decimal::MAX, dec!(2), "test::mul").unwrap_err();
1583        assert!(
1584            matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::mul")
1585        );
1586    }
1587
1588    #[test]
1589    fn d_div_happy_path_exact() {
1590        let result = d_div(dec!(10), dec!(4), "test::div");
1591        assert_eq!(result.unwrap(), dec!(2.5));
1592    }
1593
1594    #[test]
1595    fn d_div_applies_banker_rounding() {
1596        // Divide by three is recurring and must round half-to-even at the default scale.
1597        let result = d_div(dec!(1), dec!(3), "test::div").unwrap();
1598        // `1 / 3` at scale 28 under banker's rounding produces the canonical
1599        // `0.3333333333333333333333333333` (scale 28). Any other trailing digit
1600        // would signal the rounding strategy drifted.
1601        assert_eq!(result, dec!(0.3333333333333333333333333333));
1602    }
1603
1604    #[test]
1605    fn d_div_zero_denominator_returns_arithmetic_error() {
1606        let err = d_div(dec!(1), Decimal::ZERO, "test::div").unwrap_err();
1607        assert!(matches!(err, DecimalError::ArithmeticError { .. }));
1608    }
1609
1610    #[test]
1611    fn d_div_tag_is_preserved_on_overflow() {
1612        // `Decimal::MIN / 0.5` overflows because the quotient is 2 * MIN.
1613        let err = d_div(Decimal::MIN, dec!(0.5), "test::div").unwrap_err();
1614        match err {
1615            DecimalError::Overflow { operation, .. } => assert_eq!(operation, "test::div"),
1616            other => panic!("expected Overflow, got {other:?}"),
1617        }
1618    }
1619
1620    #[test]
1621    fn d_sum_empty_returns_zero() {
1622        assert_eq!(d_sum(&[], "test::sum").unwrap(), Decimal::ZERO);
1623    }
1624
1625    #[test]
1626    fn d_sum_happy_path() {
1627        let result = d_sum(&[dec!(1.5), dec!(2.25), dec!(-0.75), dec!(10)], "test::sum");
1628        assert_eq!(result.unwrap(), dec!(13));
1629    }
1630
1631    #[test]
1632    fn d_sum_overflow_returns_tagged_error() {
1633        let err = d_sum(&[Decimal::MAX, Decimal::MAX], "test::sum").unwrap_err();
1634        assert!(
1635            matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sum")
1636        );
1637    }
1638
1639    #[test]
1640    fn d_sum_iter_empty_returns_zero() {
1641        let empty: std::iter::Empty<Decimal> = std::iter::empty();
1642        assert_eq!(d_sum_iter(empty, "test::sum_iter").unwrap(), Decimal::ZERO);
1643    }
1644
1645    #[test]
1646    fn d_sum_iter_happy_path_matches_d_sum() {
1647        let values = [dec!(1.5), dec!(2.25), dec!(-0.75), dec!(10)];
1648        let via_iter = d_sum_iter(values.iter().copied(), "test::sum_iter").unwrap();
1649        let via_slice = d_sum(&values, "test::sum_iter").unwrap();
1650        assert_eq!(via_iter, dec!(13));
1651        assert_eq!(via_iter, via_slice);
1652    }
1653
1654    #[test]
1655    fn d_sum_iter_accepts_lazy_map() {
1656        // Exercise the no-allocation pathway: a `map` adapter over a range
1657        // should aggregate without any intermediate `Vec`.
1658        let sum = d_sum_iter((1i64..=4).map(Decimal::from), "test::sum_iter_lazy").unwrap();
1659        assert_eq!(sum, dec!(10));
1660    }
1661
1662    #[test]
1663    fn d_sum_iter_overflow_returns_tagged_error() {
1664        let err = d_sum_iter([Decimal::MAX, Decimal::MAX], "test::sum_iter").unwrap_err();
1665        assert!(
1666            matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sum_iter")
1667        );
1668    }
1669}
1670
1671#[cfg(test)]
1672mod tests_decimal_to_f64_rounding {
1673    use super::*;
1674    use num_traits::ToPrimitive;
1675    use rust_decimal_macros::dec;
1676
1677    fn converted(value: Decimal) -> f64 {
1678        match decimal_to_f64(value) {
1679            Ok(v) => v,
1680            Err(e) => panic!("every Decimal converts: {value}: {e}"),
1681        }
1682    }
1683
1684    /// The digits of `mantissa * 10^-scale`, written without `Decimal`'s
1685    /// `Display`, so the expectation does not share code with the helper.
1686    fn digits(negative: bool, mantissa: u128, scale: u32) -> String {
1687        let raw = mantissa.to_string();
1688        let width = match usize::try_from(scale) {
1689            Ok(w) => w,
1690            Err(e) => panic!("scale {scale} fits usize: {e}"),
1691        };
1692        let padded = format!("{raw:0>w$}", w = width + 1);
1693        let (integer, fraction) = padded.split_at(padded.len() - width);
1694        let sign = if negative { "-" } else { "" };
1695        if fraction.is_empty() {
1696            format!("{sign}{integer}")
1697        } else {
1698            format!("{sign}{integer}.{fraction}")
1699        }
1700    }
1701
1702    /// A fixed-seed 64-bit linear congruential generator (Knuth's MMIX
1703    /// constants), so the sweep needs no RNG dependency and draws the same
1704    /// inputs on every run. The step is taken in `u128` with an explicit
1705    /// modulus; `draw` returns the high 32 bits, the well-mixed ones.
1706    struct Lcg(u64);
1707
1708    impl Lcg {
1709        fn draw(&mut self) -> u64 {
1710            let step = (u128::from(self.0) * 6_364_136_223_846_793_005 + 1_442_695_040_888_963_407)
1711                % (1_u128 << 64);
1712            self.0 = match u64::try_from(step) {
1713                Ok(state) => state,
1714                Err(e) => panic!("a value below 2^64 fits u64: {e}"),
1715            };
1716            self.0 >> 32
1717        }
1718    }
1719
1720    #[test]
1721    fn test_decimal_to_f64_seeded_15_to_28_places_matches_parse() {
1722        let mut rng = Lcg(0x0656_f64d_ec1a);
1723        // Draws on which `Decimal::to_f64` misses the nearest `f64`: the sweep
1724        // must reach them for the comparison to mean anything.
1725        let mut plain_misses = 0_u32;
1726        for _ in 0..20_000 {
1727            // A 96-bit mantissa (the full `Decimal` range), a scale of 15 to
1728            // 28 and a sign, all from the same draw stream.
1729            let mantissa = (u128::from(rng.draw()) << 64)
1730                | (u128::from(rng.draw()) << 32)
1731                | u128::from(rng.draw());
1732            let scale = match u32::try_from(rng.draw() % 14) {
1733                Ok(offset) => 15 + offset,
1734                Err(e) => panic!("a value below 14 fits u32: {e}"),
1735            };
1736            let negative = rng.draw() % 2 == 1;
1737            let signed = match i128::try_from(mantissa) {
1738                Ok(m) if negative => -m,
1739                Ok(m) => m,
1740                Err(e) => panic!("96-bit mantissa fits i128: {e}"),
1741            };
1742            let value = match Decimal::try_from_i128_with_scale(signed, scale) {
1743                Ok(v) => v,
1744                Err(e) => panic!("96-bit mantissa with scale {scale} is a Decimal: {e}"),
1745            };
1746            let text = digits(negative && mantissa != 0, mantissa, scale);
1747            let expected: f64 = match text.parse() {
1748                Ok(v) => v,
1749                Err(e) => panic!("{text} parses: {e}"),
1750            };
1751            assert_eq!(
1752                converted(value).to_bits(),
1753                expected.to_bits(),
1754                "{value} ({text})"
1755            );
1756            if value.to_f64().map(f64::to_bits) != Some(expected.to_bits()) {
1757                plain_misses += 1;
1758            }
1759        }
1760        assert!(plain_misses > 0, "the sweep never reached a to_f64 miss");
1761    }
1762
1763    #[test]
1764    fn test_decimal_to_f64_edges_match_parse() {
1765        let edges = [
1766            Decimal::ZERO,
1767            Decimal::ONE,
1768            Decimal::NEGATIVE_ONE,
1769            Decimal::MAX,
1770            Decimal::MIN,
1771            Decimal::new(1, 28),
1772            Decimal::new(-1, 28),
1773            Decimal::from_i128_with_scale(79_228_162_514_264_337_593_543_950_335, 28),
1774            Decimal::from_i128_with_scale(-79_228_162_514_264_337_593_543_950_335, 15),
1775            dec!(0.95),
1776            dec!(-0.37),
1777            dec!(0.10),
1778        ];
1779        for value in edges {
1780            let expected: f64 = match value.to_string().parse() {
1781                Ok(v) => v,
1782                Err(e) => panic!("{value} parses: {e}"),
1783            };
1784            assert_eq!(converted(value).to_bits(), expected.to_bits(), "{value}");
1785        }
1786    }
1787
1788    /// `Decimal::to_f64`, which `decimal_to_f64` went through before #670,
1789    /// lands away from the nearest `f64` on these; the conversion returns the
1790    /// `f64` literal written with the same digits.
1791    #[test]
1792    fn test_decimal_to_f64_where_to_f64_differs_matches_literal() {
1793        let cases = [
1794            (dec!(2.999789999999902), 2.999_789_999_999_902_f64),
1795            (
1796                dec!(0.1234567890123456789012345678),
1797                // The nearest `f64` to the 28 digits, written in its
1798                // shortest form (the full literal trips `excessive_precision`).
1799                0.123_456_789_012_345_68_f64,
1800            ),
1801        ];
1802        for (value, literal) in cases {
1803            assert_eq!(converted(value).to_bits(), literal.to_bits(), "{value}");
1804            let plain = match value.to_f64() {
1805                Some(v) => v,
1806                None => panic!("{value} has an f64 form"),
1807            };
1808            assert_ne!(plain.to_bits(), literal.to_bits(), "{value}");
1809        }
1810    }
1811
1812    /// The case #670 names: `2.999789999999902` has 15 decimal places, and
1813    /// `Decimal::to_f64` returns `2.999789999999903`, three ULPs above the
1814    /// nearest `f64`. The merged conversion returns the nearest one.
1815    #[test]
1816    fn test_decimal_to_f64_fifteen_places_returns_nearest_not_to_f64() {
1817        let value = dec!(2.999789999999902);
1818        let nearest = 2.999_789_999_999_902_f64;
1819        let former = 2.999_789_999_999_903_f64;
1820        assert_eq!(converted(value).to_bits(), nearest.to_bits());
1821        assert_eq!(value.to_f64().map(f64::to_bits), Some(former.to_bits()));
1822        assert_eq!(former.to_bits() - nearest.to_bits(), 3);
1823    }
1824
1825    /// `2^53`, the largest significand an `f64` holds.
1826    const TWO_POW_53: u128 = 1 << 53;
1827
1828    /// Draws a `Decimal` with a mantissa below `bound` and a scale of 0 to
1829    /// 28, and the digits it is written with.
1830    fn draw_decimal(rng: &mut Lcg, bound: u128) -> (Decimal, String) {
1831        let mantissa = ((u128::from(rng.draw()) << 64)
1832            | (u128::from(rng.draw()) << 32)
1833            | u128::from(rng.draw()))
1834            % bound;
1835        let scale = match u32::try_from(rng.draw() % 29) {
1836            Ok(s) => s,
1837            Err(e) => panic!("a value below 29 fits u32: {e}"),
1838        };
1839        let negative = rng.draw() % 2 == 1;
1840        let signed = match i128::try_from(mantissa) {
1841            Ok(m) if negative => -m,
1842            Ok(m) => m,
1843            Err(e) => panic!("96-bit mantissa fits i128: {e}"),
1844        };
1845        let value = match Decimal::try_from_i128_with_scale(signed, scale) {
1846            Ok(v) => v,
1847            Err(e) => panic!("mantissa with scale {scale} is a Decimal: {e}"),
1848        };
1849        (value, digits(negative && mantissa != 0, mantissa, scale))
1850    }
1851
1852    fn assert_matches_parse(value: Decimal, text: &str) {
1853        let expected: f64 = match text.parse() {
1854            Ok(v) => v,
1855            Err(e) => panic!("{text} parses: {e}"),
1856        };
1857        assert_eq!(
1858            converted(value).to_bits(),
1859            expected.to_bits(),
1860            "{value} ({text})"
1861        );
1862    }
1863
1864    /// Mantissas up to `2^53` (exact in `f64`) and over the full 96 bits,
1865    /// each at every scale from 0 to 28, against the parse of the written
1866    /// digits.
1867    #[test]
1868    fn test_decimal_to_f64_seeded_every_scale_matches_parse() {
1869        let mut rng = Lcg(0x0670_f64d_ec1a);
1870        for bound in [TWO_POW_53 + 1, 1_u128 << 96] {
1871            for _ in 0..20_000 {
1872                let (value, text) = draw_decimal(&mut rng, bound);
1873                assert_matches_parse(value, &text);
1874            }
1875        }
1876    }
1877
1878    /// Exact ties between two `f64`s round to the even significand: `2^53 +
1879    /// 1` and `2^53 + 3` sit halfway between neighbours, as integers and
1880    /// scaled by `5^s / 10^s = 2^-s`, which leaves the division by `5^s` with
1881    /// no remainder.
1882    #[test]
1883    fn test_decimal_to_f64_exact_ties_round_to_even() {
1884        for offset in [1_u128, 3] {
1885            for scale in [0_u32, 1, 5, 18] {
1886                let mantissa = (TWO_POW_53 + offset) * 5_u128.pow(scale);
1887                let signed = match i128::try_from(mantissa) {
1888                    Ok(m) => m,
1889                    Err(e) => panic!("{mantissa} fits i128: {e}"),
1890                };
1891                for signed in [signed, -signed] {
1892                    let value = Decimal::from_i128_with_scale(signed, scale);
1893                    assert_matches_parse(value, &value.to_string());
1894                }
1895            }
1896        }
1897        // `2^53 + 1` rounds down to `2^53`, `2^53 + 3` up to `2^53 + 4`.
1898        let down = Decimal::from_i128_with_scale(9_007_199_254_740_993, 0);
1899        let up = Decimal::from_i128_with_scale(9_007_199_254_740_995, 0);
1900        assert_eq!(
1901            converted(down).to_bits(),
1902            9_007_199_254_740_992_f64.to_bits()
1903        );
1904        assert_eq!(converted(up).to_bits(), 9_007_199_254_740_996_f64.to_bits());
1905    }
1906
1907    /// Around `2^53`, the largest exact significand, at the smallest and
1908    /// largest scales.
1909    #[test]
1910    fn test_decimal_to_f64_around_two_pow_53_matches_parse() {
1911        let limit = match i128::try_from(TWO_POW_53) {
1912            Ok(l) => l,
1913            Err(e) => panic!("2^53 fits i128: {e}"),
1914        };
1915        for mantissa in [limit - 1, limit, limit + 1, limit + 2, limit + 3] {
1916            for scale in [0, 1, 21, 22, 23, 28] {
1917                for signed in [mantissa, -mantissa] {
1918                    let value = Decimal::from_i128_with_scale(signed, scale);
1919                    assert_matches_parse(value, &value.to_string());
1920                }
1921            }
1922        }
1923    }
1924
1925    /// Zero of either sign and at any scale converts to `+0.0`.
1926    #[test]
1927    fn test_decimal_to_f64_zero_any_sign_or_scale_is_positive_zero() {
1928        let mut negative_zero = Decimal::new(0, 25);
1929        negative_zero.set_sign_negative(true);
1930        for value in [Decimal::ZERO, Decimal::new(0, 25), negative_zero] {
1931            assert_eq!(converted(value).to_bits(), 0.0_f64.to_bits(), "{value:?}");
1932        }
1933    }
1934}
1935
1936/// [`d_ln_f64`] against the 28-place [`d_ln`] (#857).
1937#[cfg(test)]
1938mod tests_d_ln_f64 {
1939    use super::*;
1940    use rust_decimal_macros::dec;
1941
1942    /// The owner's bound (2026-10-09): 1e-12 relative.
1943    const TOLERANCE: Decimal = dec!(0.000000000001);
1944
1945    fn relative_error(x: Decimal) -> Decimal {
1946        let fast = d_ln_f64(x, "test").expect("fast ln");
1947        let exact = d_ln(x, "test").expect("series ln");
1948        if exact.is_zero() {
1949            return fast.abs();
1950        }
1951        (fast - exact).abs() / exact.abs()
1952    }
1953
1954    #[test]
1955    fn test_d_ln_f64_one_is_exactly_zero() {
1956        assert_eq!(d_ln_f64(Decimal::ONE, "test").ok(), Some(Decimal::ZERO));
1957    }
1958
1959    #[test]
1960    fn test_d_ln_f64_rejects_non_positive() {
1961        assert!(d_ln_f64(Decimal::ZERO, "test").is_err());
1962        assert!(d_ln_f64(dec!(-1), "test").is_err());
1963    }
1964
1965    #[test]
1966    fn test_d_ln_f64_within_the_owner_bound() {
1967        let inputs = [
1968            dec!(0.0000000000000000000000000001),
1969            dec!(0.000001),
1970            dec!(0.3),
1971            dec!(0.5),
1972            dec!(0.5000000001),
1973            dec!(0.99),
1974            dec!(0.9999999),
1975            dec!(0.9999999999999),
1976            dec!(1.0000000000001),
1977            dec!(1.0000001),
1978            dec!(1.01),
1979            dec!(1.4999999999),
1980            dec!(1.5),
1981            dec!(2.718281828459045),
1982            dec!(57.81),
1983            dec!(1000000),
1984            dec!(79228162514264337593543950335),
1985        ];
1986        let mut worst = Decimal::ZERO;
1987        for x in inputs {
1988            let error = relative_error(x);
1989            assert!(error <= TOLERANCE, "ln({x}): relative error {error}");
1990            worst = worst.max(error);
1991        }
1992        // Measured 2026-10-09: below 1e-15; held at 1e-14 so a regression
1993        // shows.
1994        assert!(worst < dec!(0.00000000000001), "worst {worst}");
1995    }
1996}
1997
1998/// [`d_exp_f64`] and [`d_sqrt_f64`] against the 28-place [`d_exp`] and
1999/// [`d_sqrt`] (#860).
2000#[cfg(test)]
2001mod tests_f64_exp_sqrt {
2002    use super::*;
2003    use rust_decimal_macros::dec;
2004
2005    /// The owner's bound (2026-10-09): 1e-12 relative.
2006    const TOLERANCE: Decimal = dec!(0.000000000001);
2007
2008    fn relative(fast: Decimal, exact: Decimal) -> Decimal {
2009        if exact.is_zero() {
2010            fast.abs()
2011        } else {
2012            (fast - exact).abs() / exact.abs()
2013        }
2014    }
2015
2016    #[test]
2017    fn test_d_exp_f64_within_the_owner_bound() {
2018        for x in [
2019            dec!(-60),
2020            dec!(-5),
2021            dec!(-0.5),
2022            dec!(-0.0001234),
2023            dec!(0),
2024            dec!(0.000002),
2025            dec!(0.0179),
2026            dec!(0.5),
2027            dec!(1),
2028            dec!(7.25),
2029            dec!(40),
2030            dec!(60),
2031        ] {
2032            let error = relative(d_exp_f64(x, "test").unwrap(), d_exp(x, "test").unwrap());
2033            assert!(error <= TOLERANCE, "exp({x}): {error}");
2034        }
2035    }
2036
2037    #[test]
2038    fn test_d_exp_f64_keeps_the_d_exp_contract() {
2039        assert_eq!(d_exp_f64(dec!(-1000), "test").ok(), Some(Decimal::ZERO));
2040        assert!(d_exp_f64(dec!(1000), "test").is_err());
2041        assert_eq!(d_exp_f64(Decimal::ZERO, "test").ok(), Some(Decimal::ONE));
2042    }
2043
2044    #[test]
2045    fn test_d_sqrt_f64_within_the_owner_bound() {
2046        for x in [
2047            dec!(0.0000000004),
2048            dec!(0.000016),
2049            dec!(0.04),
2050            dec!(0.0625),
2051            dec!(1),
2052            dec!(2),
2053            dec!(3.999999999999),
2054            dec!(100.5),
2055            dec!(1e12),
2056        ] {
2057            let error = relative(d_sqrt_f64(x, "test").unwrap(), d_sqrt(x, "test").unwrap());
2058            assert!(error <= TOLERANCE, "sqrt({x}): {error}");
2059        }
2060        assert_eq!(d_sqrt_f64(Decimal::ZERO, "test").ok(), Some(Decimal::ZERO));
2061        assert!(d_sqrt_f64(dec!(-0.01), "test").is_err());
2062    }
2063}