quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

use super::{
    decimal_to_f64_checked, f64_to_decimal_checked, NonFiniteKind, PrecisionBoundaryError,
    PRECISION_SCALE,
};

#[test]
fn decimal_to_f64_preserves_typical_z_score() {
    let out = decimal_to_f64_checked(dec!(1.5)).expect("finite Decimal must convert");
    assert!((out - 1.5).abs() < 1e-10);
}

#[test]
fn decimal_to_f64_preserves_negative() {
    let out = decimal_to_f64_checked(dec!(-2.25)).expect("negative must convert");
    assert!((out - -2.25).abs() < 1e-10);
}

#[test]
fn decimal_to_f64_zero() {
    let out = decimal_to_f64_checked(Decimal::ZERO).expect("zero must convert");
    assert_eq!(out, 0.0);
}

#[test]
fn decimal_to_f64_max_decimal_still_finite() {
    let out = decimal_to_f64_checked(Decimal::MAX).expect("Decimal::MAX fits in f64");
    assert!(out.is_finite(), "Decimal::MAX ({}) maps to finite f64", out);
}

#[test]
fn f64_to_decimal_rejects_nan() {
    let err = f64_to_decimal_checked(f64::NAN).expect_err("NaN must be rejected");
    assert_eq!(
        err,
        PrecisionBoundaryError::NonFiniteInput {
            kind: NonFiniteKind::Nan
        }
    );
}

#[test]
fn f64_to_decimal_rejects_positive_infinity() {
    let err = f64_to_decimal_checked(f64::INFINITY).expect_err("+Inf must be rejected");
    assert_eq!(
        err,
        PrecisionBoundaryError::NonFiniteInput {
            kind: NonFiniteKind::PositiveInfinity
        }
    );
}

#[test]
fn f64_to_decimal_rejects_negative_infinity() {
    let err = f64_to_decimal_checked(f64::NEG_INFINITY).expect_err("-Inf must be rejected");
    assert_eq!(
        err,
        PrecisionBoundaryError::NonFiniteInput {
            kind: NonFiniteKind::NegativeInfinity
        }
    );
}

#[test]
fn f64_to_decimal_rounds_to_canonical_scale() {
    let out = f64_to_decimal_checked(1.234567891).expect("finite must convert");
    assert_eq!(out, dec!(1.234568));
    assert_eq!(out.scale(), PRECISION_SCALE);
}

#[test]
fn f64_to_decimal_preserves_sign() {
    let out = f64_to_decimal_checked(-0.123456).expect("negative must convert");
    assert_eq!(out, dec!(-0.123456));
}

#[test]
fn f64_to_decimal_zero_is_exact() {
    let out = f64_to_decimal_checked(0.0).expect("0.0 must convert");
    assert_eq!(out, Decimal::ZERO);
}

#[test]
fn f64_to_decimal_rejects_value_above_decimal_max() {
    // 1e30 is well outside Decimal's ~7.9e28 max.
    let err = f64_to_decimal_checked(1e30).expect_err("1e30 must overflow Decimal");
    assert_eq!(
        err,
        PrecisionBoundaryError::OutOfDomain {
            reason: "f64 magnitude exceeds Decimal 96-bit mantissa range"
        }
    );
}

#[test]
fn roundtrip_is_stable_within_scale() {
    // A finite, representable value roundtrips through both helpers at the scale.
    let original = dec!(0.987654);
    let as_f = decimal_to_f64_checked(original).expect("to f64");
    let back = f64_to_decimal_checked(as_f).expect("back to Decimal");
    assert_eq!(back, original);
}

#[test]
fn non_finite_kind_display_matches_variant() {
    assert_eq!(format!("{}", NonFiniteKind::Nan), "NaN");
    assert_eq!(format!("{}", NonFiniteKind::PositiveInfinity), "+Inf");
    assert_eq!(format!("{}", NonFiniteKind::NegativeInfinity), "-Inf");
}

#[test]
fn error_display_includes_kind() {
    let err = PrecisionBoundaryError::NonFiniteInput {
        kind: NonFiniteKind::Nan,
    };
    assert!(
        format!("{}", err).contains("NaN"),
        "error display must surface the non-finite kind, got: {}",
        err
    );
}