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}