use crate::error::DecimalError;
use num_traits::FromPrimitive;
use positive::{Positive, PositiveError};
use rand::distr::Distribution;
use rand::{Rng, RngExt};
use rand_distr::StandardNormal;
use rust_decimal::{Decimal, MathematicalOps, RoundingStrategy};
use rust_decimal_macros::dec;
pub const ONE_DAY: Decimal = dec!(0.00396825397);
#[macro_export]
macro_rules! assert_decimal_eq {
($left:expr, $right:expr, $epsilon:expr) => {
let diff = ($left - $right).abs();
assert!( diff <= $epsilon,
"assertion failed: `(left == right)`\n left: `{}`\n right: `{}`\n diff: `{}`\n epsilon: `{}`",
$left,
$right,
diff,
$epsilon
);
};
}
pub trait DecimalStats {
fn mean(&self) -> Result<Decimal, DecimalError>;
fn std_dev(&self) -> Result<Decimal, DecimalError>;
}
impl DecimalStats for Vec<Decimal> {
fn mean(&self) -> Result<Decimal, DecimalError> {
if self.is_empty() {
return Ok(Decimal::ZERO);
}
let sum = d_sum(self, "decimal::stats::mean::sum")?;
d_div(sum, Decimal::from(self.len()), "decimal::stats::mean")
}
fn std_dev(&self) -> Result<Decimal, DecimalError> {
if self.len() < 2usize {
return Ok(Decimal::ZERO);
}
let mean = self.mean()?;
let mut sq_total = Decimal::ZERO;
for value in self {
let centred = d_sub(*value, mean, "decimal::stats::std_dev::centred")?;
let centred_sq = d_mul(centred, centred, "decimal::stats::std_dev::centred_sq")?;
sq_total = d_add(sq_total, centred_sq, "decimal::stats::std_dev::sq_total")?;
}
let degrees_of_freedom = self.len().checked_sub(1).ok_or_else(|| {
DecimalError::arithmetic_error("decimal::stats::std_dev", "sample is empty")
})?;
let variance = d_div(
sq_total,
Decimal::from(degrees_of_freedom),
"decimal::stats::std_dev::variance",
)?;
d_sqrt(variance, "decimal::stats::std_dev")
}
}
const POWERS_OF_FIVE: [u128; 29] = [
1,
5,
25,
125,
625,
3125,
15625,
78125,
390625,
1953125,
9765625,
48828125,
244140625,
1220703125,
6103515625,
30517578125,
152587890625,
762939453125,
3814697265625,
19073486328125,
95367431640625,
476837158203125,
2384185791015625,
11920928955078125,
59604644775390625,
298023223876953125,
1490116119384765625,
7450580596923828125,
37252902984619140625,
];
const F64_SIGNIFICAND_BITS: u32 = 53;
const F64_EXPONENT_BIAS: i32 = 1023;
const F64_EXPONENT_SHIFT: u32 = 52;
#[cold]
#[inline(never)]
fn decimal_to_f64_error(value: Decimal, reason: &str) -> DecimalError {
DecimalError::ConversionError {
from_type: format!("Decimal: {value}"),
to_type: "f64".to_string(),
reason: reason.to_string(),
}
}
pub fn decimal_to_f64(value: Decimal) -> Result<f64, DecimalError> {
let mantissa = value.mantissa().unsigned_abs();
if mantissa == 0 {
return Ok(0.0);
}
let scale = value.scale();
let out_of_range = || decimal_to_f64_error(value, "intermediate outside its range");
let divisor = usize::try_from(scale)
.ok()
.and_then(|index| POWERS_OF_FIVE.get(index))
.copied()
.ok_or_else(|| decimal_to_f64_error(value, "scale exceeds the 28 places of a Decimal"))?;
let shift = mantissa.leading_zeros();
let numerator = mantissa << shift;
let quotient = numerator.checked_div(divisor).ok_or_else(out_of_range)?;
let remainder = numerator.checked_rem(divisor).ok_or_else(out_of_range)?;
let excess = u128::BITS
.checked_sub(quotient.leading_zeros())
.and_then(|bits| bits.checked_sub(F64_SIGNIFICAND_BITS))
.ok_or_else(out_of_range)?;
let half = excess
.checked_sub(1)
.and_then(|bits| 1_u128.checked_shl(bits))
.ok_or_else(out_of_range)?;
let dropped_mask = half
.checked_shl(1)
.and_then(|bit| bit.checked_sub(1))
.ok_or_else(out_of_range)?;
let dropped = quotient & dropped_mask;
let mut significand = quotient >> excess;
if dropped > half || (dropped == half && (remainder != 0 || significand & 1 == 1)) {
significand = significand.checked_add(1).ok_or_else(out_of_range)?;
}
let exponent = i32::try_from(excess)
.ok()
.zip(i32::try_from(shift).ok())
.zip(i32::try_from(scale).ok())
.and_then(|((excess, shift), scale)| {
F64_EXPONENT_BIAS
.checked_add(excess)?
.checked_sub(shift)?
.checked_sub(scale)
})
.and_then(|biased| u64::try_from(biased).ok())
.ok_or_else(out_of_range)?;
let power_of_two = f64::from_bits(exponent << F64_EXPONENT_SHIFT);
let magnitude = significand as f64 * power_of_two;
Ok(if value.is_sign_negative() {
-magnitude
} else {
magnitude
})
}
pub fn f64_to_decimal(value: f64) -> Result<Decimal, DecimalError> {
Decimal::from_f64(value).ok_or_else(|| DecimalError::ConversionError {
from_type: format!("f64: {value}"),
to_type: "Decimal".to_string(),
reason: "Failed to convert f64 to Decimal".to_string(),
})
}
#[must_use]
#[inline]
pub fn finite_decimal(value: f64) -> Option<Decimal> {
if value.is_finite() {
Decimal::from_f64(value)
} else {
None
}
}
#[must_use]
pub fn decimal_normal_sample() -> Decimal {
decimal_normal_sample_with(&mut rand::rng())
}
#[must_use]
#[inline]
pub fn decimal_normal_sample_with<R: Rng + ?Sized>(rng: &mut R) -> Decimal {
let sample: f64 = StandardNormal.sample(rng);
Decimal::from_f64(sample).unwrap_or(Decimal::ZERO)
}
const UNIFORM_SAMPLE_SCALE: u32 = 18;
const UNIFORM_SAMPLE_GRID: i64 = 1_000_000_000_000_000_000;
#[must_use]
#[inline]
pub fn decimal_uniform_sample_with<R: Rng + ?Sized>(rng: &mut R) -> Decimal {
let k = rng.random_range(0..UNIFORM_SAMPLE_GRID);
Decimal::new(k, UNIFORM_SAMPLE_SCALE)
}
pub const DIV_DEFAULT_SCALE: u32 = 28;
#[inline]
pub fn d_add(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
lhs.checked_add(rhs)
.ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
}
#[inline]
pub fn d_sum(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
d_sum_iter(values.iter().copied(), op)
}
#[inline]
pub fn d_sum_iter<I>(iter: I, op: &'static str) -> Result<Decimal, DecimalError>
where
I: IntoIterator<Item = Decimal>,
{
let mut acc = Decimal::ZERO;
for v in iter {
acc = acc
.checked_add(v)
.ok_or_else(|| DecimalError::overflow(op, acc, v))?;
}
Ok(acc)
}
#[inline]
pub fn d_sub(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
lhs.checked_sub(rhs)
.ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
}
#[inline]
pub fn d_mul(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
lhs.checked_mul(rhs)
.ok_or_else(|| DecimalError::overflow(op, lhs, rhs))
}
#[inline]
pub fn d_product_iter<I>(iter: I, op: &'static str) -> Result<Decimal, DecimalError>
where
I: IntoIterator<Item = Decimal>,
{
let mut acc = Decimal::ONE;
for v in iter {
acc = acc
.checked_mul(v)
.ok_or_else(|| DecimalError::overflow(op, acc, v))?;
}
Ok(acc)
}
#[inline]
pub fn d_div(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
if rhs.is_zero() {
return Err(DecimalError::arithmetic_error(op, "division by zero"));
}
let raw = lhs
.checked_div(rhs)
.ok_or_else(|| DecimalError::overflow(op, lhs, rhs))?;
Ok(raw.round_dp_with_strategy(DIV_DEFAULT_SCALE, RoundingStrategy::MidpointNearestEven))
}
#[inline]
pub fn d_exp(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
if let Some(value) = x.checked_exp() {
return Ok(value);
}
if x.is_sign_negative() {
return Ok(Decimal::ZERO);
}
Err(DecimalError::overflow(op, x, Decimal::ZERO))
}
#[inline]
pub fn d_ln(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
if x <= Decimal::ZERO {
return Err(DecimalError::arithmetic_error(
op,
"logarithm of a non-positive value",
));
}
x.checked_ln()
.ok_or_else(|| DecimalError::arithmetic_error(op, "logarithm is not representable"))
}
#[inline]
pub fn d_ln_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
if x <= Decimal::ZERO {
return Err(DecimalError::arithmetic_error(
op,
"logarithm of a non-positive value",
));
}
let shifted = x
.checked_sub(Decimal::ONE)
.ok_or_else(|| DecimalError::arithmetic_error(op, "x - 1 is not representable"))?;
let value = if shifted.abs() < LN_1P_BAND {
decimal_to_f64(shifted)?.ln_1p()
} else {
decimal_to_f64(x)?.ln() };
if !value.is_finite() {
return Err(DecimalError::arithmetic_error(
op,
"logarithm is not finite in f64",
));
}
f64_to_decimal(value)
}
const LN_1P_BAND: Decimal = Decimal::from_parts(5, 0, 0, false, 1);
#[inline]
pub fn d_exp_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
let value = decimal_to_f64(x)?.exp(); match finite_decimal(value) {
Some(result) => Ok(result),
None if x.is_sign_negative() => Ok(Decimal::ZERO),
None => Err(DecimalError::overflow(op, x, Decimal::ZERO)),
}
}
#[inline]
pub fn d_sqrt_f64(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
if x.is_sign_negative() && !x.is_zero() {
return Err(DecimalError::arithmetic_error(
op,
"square root of a negative value",
));
}
f64_to_decimal(decimal_to_f64(x)?.sqrt()) }
#[inline]
pub fn d_powd(base: Decimal, exponent: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
base.checked_powd(exponent)
.ok_or_else(|| DecimalError::overflow(op, base, exponent))
}
const SQRT_MAX_ITERATIONS: u32 = 1000;
pub fn d_sqrt(x: Decimal, op: &'static str) -> Result<Decimal, DecimalError> {
sqrt_with_iterations(x, op).map(|(value, _)| value)
}
fn sqrt_with_iterations(x: Decimal, op: &'static str) -> Result<(Decimal, u32), DecimalError> {
if x.is_sign_negative() {
return Err(DecimalError::arithmetic_error(
op,
"square root of a negative value",
));
}
if x.is_zero() {
return Ok((Decimal::ZERO, 0));
}
let overflow = || DecimalError::arithmetic_error(op, "square root iteration overflowed");
let mut result = x.checked_div(Decimal::TWO).ok_or_else(overflow)?;
if result.is_zero() {
result = x;
}
let mut last = result.checked_add(Decimal::ONE).ok_or_else(overflow)?;
let mut before_last = last;
let mut iterations = 0u32;
let error_of = |candidate: Decimal| -> Decimal {
candidate
.checked_mul(candidate)
.and_then(|square| square.checked_sub(x))
.map(|d| d.abs())
.unwrap_or(Decimal::MAX)
};
while last != result {
iterations = iterations.checked_add(1).ok_or_else(overflow)?;
if result == before_last || iterations > SQRT_MAX_ITERATIONS {
return if error_of(last) <= error_of(result) {
Ok((last, iterations))
} else {
Ok((result, iterations))
};
}
before_last = last;
last = result;
let quotient = x.checked_div(result).ok_or_else(overflow)?;
result = result
.checked_add(quotient)
.ok_or_else(overflow)?
.checked_div(Decimal::TWO)
.ok_or_else(overflow)?;
}
Ok((result, iterations))
}
#[inline]
pub fn p_sqrt(x: &Positive, op: &'static str) -> Result<Positive, PositiveError> {
let root =
d_sqrt(x.to_dec(), op).map_err(|e| PositiveError::arithmetic_error(op, &e.to_string()))?;
Positive::new_decimal(root)
}
#[macro_export]
macro_rules! d2fu {
($val:expr) => {
$crate::model::decimal::decimal_to_f64($val)
};
}
#[macro_export]
macro_rules! d2f {
($val:expr) => {
$crate::model::decimal::decimal_to_f64($val)?
};
}
#[macro_export]
macro_rules! nz {
($val:expr) => {{
::std::num::NonZeroUsize::new($val)
.unwrap_or_else(|| panic!("nz!({}) must be non-zero", stringify!($val))) }};
}
#[macro_export]
macro_rules! f2du {
($val:expr) => {
$crate::model::decimal::f64_to_decimal($val)
};
}
#[macro_export]
macro_rules! f2d {
($val:expr) => {
$crate::model::decimal::f64_to_decimal($val)?
};
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_f64_to_decimal_valid() {
let value = 42.42;
let result = f64_to_decimal(value);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Decimal::from_str("42.42").unwrap());
}
#[test]
fn test_f64_to_decimal_zero() {
let value = 0.0;
let result = f64_to_decimal(value);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Decimal::from_str("0").unwrap());
}
#[test]
fn test_decimal_to_f64_valid() {
let decimal = Decimal::from_str("42.42").unwrap();
let result = decimal_to_f64(decimal);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42.42);
}
#[test]
fn test_decimal_to_f64_zero() {
let decimal = Decimal::from_str("0").unwrap();
let result = decimal_to_f64(decimal);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0.0);
}
#[test]
fn test_d_sqrt_resolves_upstream_circuit_breaker() {
let x = dec!(4.0000000000000000000000000003);
let root = d_sqrt(x, "test").unwrap();
let residual = (root * root - x).abs();
assert!(
residual <= dec!(0.0000000000000000000000000001),
"sqrt({x}) = {root}, residual {residual}"
);
}
#[test]
fn test_d_sqrt_matches_upstream_on_converging_inputs() {
let inputs = [
Decimal::ZERO,
Decimal::ONE,
Decimal::TWO,
dec!(4),
dec!(0.25),
dec!(0.0000000000000000000000000001),
dec!(123456.789),
Decimal::MAX,
];
for x in inputs {
let expected = x.sqrt().unwrap_or_default();
assert_eq!(d_sqrt(x, "test").unwrap(), expected, "sqrt({x})");
}
}
#[test]
fn test_d_sqrt_matches_upstream_on_a_seeded_sweep() {
use rand::{RngExt, SeedableRng};
let mut rng = rand::rngs::StdRng::seed_from_u64(0x5eed_5eed_0588);
let mut inputs: Vec<Decimal> = Vec::with_capacity(6_000);
for _ in 0..4_000 {
let mantissa: i64 = rng.random_range(0..=i64::MAX);
let scale: u32 = rng.random_range(0..=28);
inputs.push(Decimal::new(mantissa, scale));
}
let ulp = dec!(0.0000000000000000000000000001);
for root in [1u32, 2, 3, 7, 12, 100, 1_000, 65_536] {
let square = Decimal::from(root) * Decimal::from(root);
for units in -6i32..=6 {
let nudge = ulp * Decimal::from(units);
if let Some(x) = square.checked_add(nudge) {
inputs.push(x);
}
}
}
let mut compared = 0usize;
let mut skipped = 0usize;
for x in inputs {
let upstream = std::panic::catch_unwind(|| x.sqrt());
match upstream {
Ok(Some(expected)) => {
let actual = d_sqrt(x, "sweep").unwrap();
assert_eq!(
actual.serialize(),
expected.serialize(),
"sqrt({x}): upstream {expected}, d_sqrt {actual}"
);
compared += 1;
}
Ok(None) => unreachable!("non-negative inputs only"),
Err(_) => {
assert!(d_sqrt(x, "sweep").is_ok(), "d_sqrt({x}) must be total");
skipped += 1;
}
}
}
assert!(compared > 4_000, "compared {compared}, skipped {skipped}");
}
#[test]
fn test_d_sqrt_resolves_the_cycle_in_a_few_iterations() {
let oscillating = dec!(4.0000000000000000000000000003);
let (_, iterations) = sqrt_with_iterations(oscillating, "cycle").unwrap();
assert!(
iterations < 10,
"cycle detection is not immediate: {iterations} iterations"
);
assert!(iterations < SQRT_MAX_ITERATIONS);
let (_, converging) =
sqrt_with_iterations(dec!(4.0000000000000000000000000004), "control").unwrap();
assert!(converging < 60, "control took {converging} iterations");
}
#[test]
fn test_d_sqrt_rejects_negative_input() {
assert!(d_sqrt(dec!(-1), "test").is_err());
assert!(d_sqrt(dec!(-0.0000000000000000000000000001), "test").is_err());
}
#[test]
fn test_p_sqrt_matches_d_sqrt() {
let x = Positive::new_decimal(dec!(4.0000000000000000000000000003)).unwrap();
let expected = d_sqrt(x.to_dec(), "test").unwrap();
assert_eq!(p_sqrt(&x, "test").unwrap().to_dec(), expected);
}
}
#[cfg(test)]
mod tests_decimal_stats {
use super::*;
#[test]
fn test_decimal_stats_empty_and_singleton_return_zero() {
let empty: Vec<Decimal> = vec![];
assert_eq!(empty.mean().unwrap(), Decimal::ZERO);
assert_eq!(empty.std_dev().unwrap(), Decimal::ZERO);
let one = vec![dec!(3)];
assert_eq!(one.mean().unwrap(), dec!(3));
assert_eq!(one.std_dev().unwrap(), Decimal::ZERO);
}
#[test]
fn test_decimal_stats_ordinary_sample_is_unchanged() {
let values = vec![
dec!(2),
dec!(4),
dec!(4),
dec!(4),
dec!(5),
dec!(5),
dec!(7),
dec!(9),
];
assert_eq!(values.mean().unwrap(), dec!(5));
let expected = d_div(dec!(32), dec!(7), "test")
.and_then(|v| d_sqrt(v, "test"))
.unwrap();
assert_eq!(values.std_dev().unwrap(), expected);
}
#[test]
fn test_decimal_stats_saturated_sample_reports_overflow_instead_of_aborting() {
let values = vec![Decimal::MAX, Decimal::MAX];
assert!(values.mean().is_err());
assert!(values.std_dev().is_err());
}
#[test]
fn test_decimal_stats_std_dev_reports_a_deviation_that_overflows() {
let values = vec![Decimal::MAX, Decimal::MIN, Decimal::MAX, Decimal::MIN];
assert!(values.std_dev().is_err());
}
}
#[cfg(test)]
mod tests_random_generation {
use super::*;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use rand::distr::Distribution;
use std::collections::HashMap;
const SEED: u64 = 685;
#[test]
fn test_normal_sample_returns() {
let mut rng = crate::utils::deterministic_rng(SEED);
for _ in 0..1000 {
let sample = decimal_normal_sample_with(&mut rng);
assert!(sample <= Decimal::TEN);
assert!(sample >= -Decimal::TEN);
}
}
#[test]
fn test_normal_sample_distribution() {
const NUM_SAMPLES: usize = 10000;
let mut samples = Vec::with_capacity(NUM_SAMPLES);
let mut rng = crate::utils::deterministic_rng(SEED);
for _ in 0..NUM_SAMPLES {
samples.push(decimal_normal_sample_with(&mut rng).to_f64().unwrap());
}
let sum: f64 = samples.iter().sum();
let mean = sum / NUM_SAMPLES as f64;
let variance_sum: f64 = samples.iter().map(|&x| (x - mean).powi(2)).sum();
let std_dev = (variance_sum / NUM_SAMPLES as f64).sqrt();
assert_relative_eq!(mean, 0.0, epsilon = 0.04);
assert_relative_eq!(std_dev, 1.0, epsilon = 0.03);
}
#[test]
fn test_normal_distribution_transformation() {
let mut t_rng = crate::utils::deterministic_rng(SEED);
let normal = rand_distr::Normal::new(-1.0, 0.5).unwrap();
let mut value_counts: HashMap<i32, usize> = HashMap::new();
const SAMPLES: usize = 5000;
for _ in 0..SAMPLES {
let raw_sample = normal.sample(&mut t_rng);
let positive_sample = raw_sample.to_f64().unwrap();
let bucket = (positive_sample.round() as i32).max(0);
*value_counts.entry(bucket).or_insert(0) += 1;
}
assert!(value_counts.get(&0).unwrap_or(&0) > &(SAMPLES / 10));
let max_bucket = value_counts.keys().max().unwrap_or(&0);
assert!(*max_bucket > 0);
}
#[test]
fn test_normal_sample_consistency() {
let sample1 = decimal_normal_sample();
let sample2 = decimal_normal_sample();
let sample3 = decimal_normal_sample();
assert!(sample1 != sample2 || sample2 != sample3);
}
#[test]
fn test_decimal_normal_sample_with_same_seed_is_identical() {
let mut first = crate::utils::deterministic_rng(42);
let mut second = crate::utils::deterministic_rng(42);
for _ in 0..1000 {
assert_eq!(
decimal_normal_sample_with(&mut first),
decimal_normal_sample_with(&mut second)
);
}
}
#[test]
fn test_decimal_normal_sample_with_is_the_converted_standard_normal_draw() {
let mut helper = crate::utils::deterministic_rng(7);
let mut raw = crate::utils::deterministic_rng(7);
for _ in 0..1000 {
let draw: f64 = StandardNormal.sample(&mut raw);
assert_eq!(
decimal_normal_sample_with(&mut helper),
Decimal::from_f64(draw).unwrap_or(Decimal::ZERO)
);
}
}
#[test]
fn test_decimal_normal_sample_with_seed_pins_first_draws() {
let mut rng = crate::utils::deterministic_rng(42);
let draws: Vec<Decimal> = (0..3)
.map(|_| decimal_normal_sample_with(&mut rng))
.collect();
let expected = vec![
dec!(0.0694279183619634),
dec!(0.1329381219941254),
dec!(0.2625763573739537),
];
assert_eq!(draws, expected);
}
#[test]
fn test_decimal_uniform_sample_with_range_is_half_open_unit_interval() {
let mut rng = crate::utils::deterministic_rng(SEED);
for _ in 0..100_000 {
let u = decimal_uniform_sample_with(&mut rng);
assert!(u >= Decimal::ZERO, "{u} below zero");
assert!(u < Decimal::ONE, "{u} not below one");
assert_eq!(u.scale(), UNIFORM_SAMPLE_SCALE, "{u} off the grid");
}
}
#[test]
fn test_decimal_uniform_sample_with_same_seed_is_identical() {
let mut first = crate::utils::deterministic_rng(42);
let mut second = crate::utils::deterministic_rng(42);
for _ in 0..1000 {
assert_eq!(
decimal_uniform_sample_with(&mut first),
decimal_uniform_sample_with(&mut second)
);
}
}
#[test]
fn test_decimal_uniform_sample_with_seed_pins_first_draws() {
let mut rng = crate::utils::deterministic_rng(42);
let draws: Vec<Decimal> = (0..3)
.map(|_| decimal_uniform_sample_with(&mut rng))
.collect();
let expected = vec![
dec!(0.526557409002773877),
dec!(0.542725209903143899),
dec!(0.636465099143894996),
];
assert_eq!(draws, expected);
}
#[test]
fn test_decimal_uniform_sample_with_moments_match_uniform() {
const N: usize = 100_000;
let mut rng = crate::utils::deterministic_rng(SEED);
let thresholds = [dec!(0.004), dec!(0.1), dec!(0.5), dec!(0.9)];
let mut below = [0_u64; 4];
let mut sum = 0.0_f64;
for _ in 0..N {
let u = decimal_uniform_sample_with(&mut rng);
sum += u.to_f64().unwrap();
for (count, p) in below.iter_mut().zip(thresholds) {
if u < p {
*count += 1;
}
}
}
let n = N as f64;
let mean_se = (1.0 / 12.0 / n).sqrt();
assert!((sum / n - 0.5).abs() < 5.0 * mean_se, "mean {}", sum / n);
for (count, p) in below.iter().zip(thresholds) {
let p = p.to_f64().unwrap();
let freq = *count as f64 / n;
let se = (p * (1.0 - p) / n).sqrt();
assert!((freq - p).abs() < 5.0 * se, "P(u < {p}) = {freq}");
}
}
struct ConstantWord(u64);
impl rand::TryRng for ConstantWord {
type Error = std::convert::Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
Ok((self.0 >> 32) as u32)
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
Ok(self.0)
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
dst.fill(0);
Ok(())
}
}
#[test]
fn test_decimal_uniform_sample_with_extreme_words_hit_grid_ends() {
assert_eq!(
decimal_uniform_sample_with(&mut ConstantWord(0)),
Decimal::ZERO
);
assert_eq!(
decimal_uniform_sample_with(&mut ConstantWord(u64::MAX)),
dec!(0.999999999999999999)
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod checked_helpers_tests {
use super::*;
#[test]
fn d_add_happy_path() {
let result = d_add(dec!(1.25), dec!(2.50), "test::add");
assert_eq!(result.unwrap(), dec!(3.75));
}
#[test]
fn d_add_overflow_on_max_plus_max() {
let err = d_add(Decimal::MAX, Decimal::MAX, "test::add").unwrap_err();
match err {
DecimalError::Overflow { operation, .. } => assert_eq!(operation, "test::add"),
other => panic!("expected Overflow, got {other:?}"),
}
}
#[test]
fn checked_add_near_max_reports_ok_without_moving_the_value() {
assert_eq!(
Decimal::MAX.checked_add(dec!(0.01)),
Some(Decimal::MAX),
"a fractional addend rescales away instead of overflowing"
);
assert_eq!(
d_add(Decimal::MAX, dec!(0.47), "test::add").unwrap(),
Decimal::MAX,
"the crate helper inherits the same behaviour"
);
assert_eq!(Decimal::MAX.checked_add(dec!(0.5)), None);
assert!(d_add(Decimal::MAX, dec!(0.5), "test::add").is_err());
}
#[test]
fn d_sub_happy_path() {
let result = d_sub(dec!(10), dec!(3.5), "test::sub");
assert_eq!(result.unwrap(), dec!(6.5));
}
#[test]
fn d_sub_overflow_on_min_minus_max() {
let err = d_sub(Decimal::MIN, Decimal::MAX, "test::sub").unwrap_err();
assert!(
matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sub")
);
}
#[test]
fn d_mul_happy_path() {
let result = d_mul(dec!(2.5), dec!(4), "test::mul");
assert_eq!(result.unwrap(), dec!(10.0));
}
#[test]
fn d_mul_overflow_on_max_times_two() {
let err = d_mul(Decimal::MAX, dec!(2), "test::mul").unwrap_err();
assert!(
matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::mul")
);
}
#[test]
fn d_div_happy_path_exact() {
let result = d_div(dec!(10), dec!(4), "test::div");
assert_eq!(result.unwrap(), dec!(2.5));
}
#[test]
fn d_div_applies_banker_rounding() {
let result = d_div(dec!(1), dec!(3), "test::div").unwrap();
assert_eq!(result, dec!(0.3333333333333333333333333333));
}
#[test]
fn d_div_zero_denominator_returns_arithmetic_error() {
let err = d_div(dec!(1), Decimal::ZERO, "test::div").unwrap_err();
assert!(matches!(err, DecimalError::ArithmeticError { .. }));
}
#[test]
fn d_div_tag_is_preserved_on_overflow() {
let err = d_div(Decimal::MIN, dec!(0.5), "test::div").unwrap_err();
match err {
DecimalError::Overflow { operation, .. } => assert_eq!(operation, "test::div"),
other => panic!("expected Overflow, got {other:?}"),
}
}
#[test]
fn d_sum_empty_returns_zero() {
assert_eq!(d_sum(&[], "test::sum").unwrap(), Decimal::ZERO);
}
#[test]
fn d_sum_happy_path() {
let result = d_sum(&[dec!(1.5), dec!(2.25), dec!(-0.75), dec!(10)], "test::sum");
assert_eq!(result.unwrap(), dec!(13));
}
#[test]
fn d_sum_overflow_returns_tagged_error() {
let err = d_sum(&[Decimal::MAX, Decimal::MAX], "test::sum").unwrap_err();
assert!(
matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sum")
);
}
#[test]
fn d_sum_iter_empty_returns_zero() {
let empty: std::iter::Empty<Decimal> = std::iter::empty();
assert_eq!(d_sum_iter(empty, "test::sum_iter").unwrap(), Decimal::ZERO);
}
#[test]
fn d_sum_iter_happy_path_matches_d_sum() {
let values = [dec!(1.5), dec!(2.25), dec!(-0.75), dec!(10)];
let via_iter = d_sum_iter(values.iter().copied(), "test::sum_iter").unwrap();
let via_slice = d_sum(&values, "test::sum_iter").unwrap();
assert_eq!(via_iter, dec!(13));
assert_eq!(via_iter, via_slice);
}
#[test]
fn d_sum_iter_accepts_lazy_map() {
let sum = d_sum_iter((1i64..=4).map(Decimal::from), "test::sum_iter_lazy").unwrap();
assert_eq!(sum, dec!(10));
}
#[test]
fn d_sum_iter_overflow_returns_tagged_error() {
let err = d_sum_iter([Decimal::MAX, Decimal::MAX], "test::sum_iter").unwrap_err();
assert!(
matches!(err, DecimalError::Overflow { operation, .. } if operation == "test::sum_iter")
);
}
}
#[cfg(test)]
mod tests_decimal_to_f64_rounding {
use super::*;
use num_traits::ToPrimitive;
use rust_decimal_macros::dec;
fn converted(value: Decimal) -> f64 {
match decimal_to_f64(value) {
Ok(v) => v,
Err(e) => panic!("every Decimal converts: {value}: {e}"),
}
}
fn digits(negative: bool, mantissa: u128, scale: u32) -> String {
let raw = mantissa.to_string();
let width = match usize::try_from(scale) {
Ok(w) => w,
Err(e) => panic!("scale {scale} fits usize: {e}"),
};
let padded = format!("{raw:0>w$}", w = width + 1);
let (integer, fraction) = padded.split_at(padded.len() - width);
let sign = if negative { "-" } else { "" };
if fraction.is_empty() {
format!("{sign}{integer}")
} else {
format!("{sign}{integer}.{fraction}")
}
}
struct Lcg(u64);
impl Lcg {
fn draw(&mut self) -> u64 {
let step = (u128::from(self.0) * 6_364_136_223_846_793_005 + 1_442_695_040_888_963_407)
% (1_u128 << 64);
self.0 = match u64::try_from(step) {
Ok(state) => state,
Err(e) => panic!("a value below 2^64 fits u64: {e}"),
};
self.0 >> 32
}
}
#[test]
fn test_decimal_to_f64_seeded_15_to_28_places_matches_parse() {
let mut rng = Lcg(0x0656_f64d_ec1a);
let mut plain_misses = 0_u32;
for _ in 0..20_000 {
let mantissa = (u128::from(rng.draw()) << 64)
| (u128::from(rng.draw()) << 32)
| u128::from(rng.draw());
let scale = match u32::try_from(rng.draw() % 14) {
Ok(offset) => 15 + offset,
Err(e) => panic!("a value below 14 fits u32: {e}"),
};
let negative = rng.draw() % 2 == 1;
let signed = match i128::try_from(mantissa) {
Ok(m) if negative => -m,
Ok(m) => m,
Err(e) => panic!("96-bit mantissa fits i128: {e}"),
};
let value = match Decimal::try_from_i128_with_scale(signed, scale) {
Ok(v) => v,
Err(e) => panic!("96-bit mantissa with scale {scale} is a Decimal: {e}"),
};
let text = digits(negative && mantissa != 0, mantissa, scale);
let expected: f64 = match text.parse() {
Ok(v) => v,
Err(e) => panic!("{text} parses: {e}"),
};
assert_eq!(
converted(value).to_bits(),
expected.to_bits(),
"{value} ({text})"
);
if value.to_f64().map(f64::to_bits) != Some(expected.to_bits()) {
plain_misses += 1;
}
}
assert!(plain_misses > 0, "the sweep never reached a to_f64 miss");
}
#[test]
fn test_decimal_to_f64_edges_match_parse() {
let edges = [
Decimal::ZERO,
Decimal::ONE,
Decimal::NEGATIVE_ONE,
Decimal::MAX,
Decimal::MIN,
Decimal::new(1, 28),
Decimal::new(-1, 28),
Decimal::from_i128_with_scale(79_228_162_514_264_337_593_543_950_335, 28),
Decimal::from_i128_with_scale(-79_228_162_514_264_337_593_543_950_335, 15),
dec!(0.95),
dec!(-0.37),
dec!(0.10),
];
for value in edges {
let expected: f64 = match value.to_string().parse() {
Ok(v) => v,
Err(e) => panic!("{value} parses: {e}"),
};
assert_eq!(converted(value).to_bits(), expected.to_bits(), "{value}");
}
}
#[test]
fn test_decimal_to_f64_where_to_f64_differs_matches_literal() {
let cases = [
(dec!(2.999789999999902), 2.999_789_999_999_902_f64),
(
dec!(0.1234567890123456789012345678),
0.123_456_789_012_345_68_f64,
),
];
for (value, literal) in cases {
assert_eq!(converted(value).to_bits(), literal.to_bits(), "{value}");
let plain = match value.to_f64() {
Some(v) => v,
None => panic!("{value} has an f64 form"),
};
assert_ne!(plain.to_bits(), literal.to_bits(), "{value}");
}
}
#[test]
fn test_decimal_to_f64_fifteen_places_returns_nearest_not_to_f64() {
let value = dec!(2.999789999999902);
let nearest = 2.999_789_999_999_902_f64;
let former = 2.999_789_999_999_903_f64;
assert_eq!(converted(value).to_bits(), nearest.to_bits());
assert_eq!(value.to_f64().map(f64::to_bits), Some(former.to_bits()));
assert_eq!(former.to_bits() - nearest.to_bits(), 3);
}
const TWO_POW_53: u128 = 1 << 53;
fn draw_decimal(rng: &mut Lcg, bound: u128) -> (Decimal, String) {
let mantissa = ((u128::from(rng.draw()) << 64)
| (u128::from(rng.draw()) << 32)
| u128::from(rng.draw()))
% bound;
let scale = match u32::try_from(rng.draw() % 29) {
Ok(s) => s,
Err(e) => panic!("a value below 29 fits u32: {e}"),
};
let negative = rng.draw() % 2 == 1;
let signed = match i128::try_from(mantissa) {
Ok(m) if negative => -m,
Ok(m) => m,
Err(e) => panic!("96-bit mantissa fits i128: {e}"),
};
let value = match Decimal::try_from_i128_with_scale(signed, scale) {
Ok(v) => v,
Err(e) => panic!("mantissa with scale {scale} is a Decimal: {e}"),
};
(value, digits(negative && mantissa != 0, mantissa, scale))
}
fn assert_matches_parse(value: Decimal, text: &str) {
let expected: f64 = match text.parse() {
Ok(v) => v,
Err(e) => panic!("{text} parses: {e}"),
};
assert_eq!(
converted(value).to_bits(),
expected.to_bits(),
"{value} ({text})"
);
}
#[test]
fn test_decimal_to_f64_seeded_every_scale_matches_parse() {
let mut rng = Lcg(0x0670_f64d_ec1a);
for bound in [TWO_POW_53 + 1, 1_u128 << 96] {
for _ in 0..20_000 {
let (value, text) = draw_decimal(&mut rng, bound);
assert_matches_parse(value, &text);
}
}
}
#[test]
fn test_decimal_to_f64_exact_ties_round_to_even() {
for offset in [1_u128, 3] {
for scale in [0_u32, 1, 5, 18] {
let mantissa = (TWO_POW_53 + offset) * 5_u128.pow(scale);
let signed = match i128::try_from(mantissa) {
Ok(m) => m,
Err(e) => panic!("{mantissa} fits i128: {e}"),
};
for signed in [signed, -signed] {
let value = Decimal::from_i128_with_scale(signed, scale);
assert_matches_parse(value, &value.to_string());
}
}
}
let down = Decimal::from_i128_with_scale(9_007_199_254_740_993, 0);
let up = Decimal::from_i128_with_scale(9_007_199_254_740_995, 0);
assert_eq!(
converted(down).to_bits(),
9_007_199_254_740_992_f64.to_bits()
);
assert_eq!(converted(up).to_bits(), 9_007_199_254_740_996_f64.to_bits());
}
#[test]
fn test_decimal_to_f64_around_two_pow_53_matches_parse() {
let limit = match i128::try_from(TWO_POW_53) {
Ok(l) => l,
Err(e) => panic!("2^53 fits i128: {e}"),
};
for mantissa in [limit - 1, limit, limit + 1, limit + 2, limit + 3] {
for scale in [0, 1, 21, 22, 23, 28] {
for signed in [mantissa, -mantissa] {
let value = Decimal::from_i128_with_scale(signed, scale);
assert_matches_parse(value, &value.to_string());
}
}
}
}
#[test]
fn test_decimal_to_f64_zero_any_sign_or_scale_is_positive_zero() {
let mut negative_zero = Decimal::new(0, 25);
negative_zero.set_sign_negative(true);
for value in [Decimal::ZERO, Decimal::new(0, 25), negative_zero] {
assert_eq!(converted(value).to_bits(), 0.0_f64.to_bits(), "{value:?}");
}
}
}
#[cfg(test)]
mod tests_d_ln_f64 {
use super::*;
use rust_decimal_macros::dec;
const TOLERANCE: Decimal = dec!(0.000000000001);
fn relative_error(x: Decimal) -> Decimal {
let fast = d_ln_f64(x, "test").expect("fast ln");
let exact = d_ln(x, "test").expect("series ln");
if exact.is_zero() {
return fast.abs();
}
(fast - exact).abs() / exact.abs()
}
#[test]
fn test_d_ln_f64_one_is_exactly_zero() {
assert_eq!(d_ln_f64(Decimal::ONE, "test").ok(), Some(Decimal::ZERO));
}
#[test]
fn test_d_ln_f64_rejects_non_positive() {
assert!(d_ln_f64(Decimal::ZERO, "test").is_err());
assert!(d_ln_f64(dec!(-1), "test").is_err());
}
#[test]
fn test_d_ln_f64_within_the_owner_bound() {
let inputs = [
dec!(0.0000000000000000000000000001),
dec!(0.000001),
dec!(0.3),
dec!(0.5),
dec!(0.5000000001),
dec!(0.99),
dec!(0.9999999),
dec!(0.9999999999999),
dec!(1.0000000000001),
dec!(1.0000001),
dec!(1.01),
dec!(1.4999999999),
dec!(1.5),
dec!(2.718281828459045),
dec!(57.81),
dec!(1000000),
dec!(79228162514264337593543950335),
];
let mut worst = Decimal::ZERO;
for x in inputs {
let error = relative_error(x);
assert!(error <= TOLERANCE, "ln({x}): relative error {error}");
worst = worst.max(error);
}
assert!(worst < dec!(0.00000000000001), "worst {worst}");
}
}
#[cfg(test)]
mod tests_f64_exp_sqrt {
use super::*;
use rust_decimal_macros::dec;
const TOLERANCE: Decimal = dec!(0.000000000001);
fn relative(fast: Decimal, exact: Decimal) -> Decimal {
if exact.is_zero() {
fast.abs()
} else {
(fast - exact).abs() / exact.abs()
}
}
#[test]
fn test_d_exp_f64_within_the_owner_bound() {
for x in [
dec!(-60),
dec!(-5),
dec!(-0.5),
dec!(-0.0001234),
dec!(0),
dec!(0.000002),
dec!(0.0179),
dec!(0.5),
dec!(1),
dec!(7.25),
dec!(40),
dec!(60),
] {
let error = relative(d_exp_f64(x, "test").unwrap(), d_exp(x, "test").unwrap());
assert!(error <= TOLERANCE, "exp({x}): {error}");
}
}
#[test]
fn test_d_exp_f64_keeps_the_d_exp_contract() {
assert_eq!(d_exp_f64(dec!(-1000), "test").ok(), Some(Decimal::ZERO));
assert!(d_exp_f64(dec!(1000), "test").is_err());
assert_eq!(d_exp_f64(Decimal::ZERO, "test").ok(), Some(Decimal::ONE));
}
#[test]
fn test_d_sqrt_f64_within_the_owner_bound() {
for x in [
dec!(0.0000000004),
dec!(0.000016),
dec!(0.04),
dec!(0.0625),
dec!(1),
dec!(2),
dec!(3.999999999999),
dec!(100.5),
dec!(1e12),
] {
let error = relative(d_sqrt_f64(x, "test").unwrap(), d_sqrt(x, "test").unwrap());
assert!(error <= TOLERANCE, "sqrt({x}): {error}");
}
assert_eq!(d_sqrt_f64(Decimal::ZERO, "test").ok(), Some(Decimal::ZERO));
assert!(d_sqrt_f64(dec!(-0.01), "test").is_err());
}
}