fix(common): add serde derives, preserve Ratio finite invariant
- Add Serialize/Deserialize to all four financial types - Ratio::ln returns 0.0 for non-positive inputs (prevents NaN leak) - Ratio::exp clamps to f64::MAX on overflow (prevents Inf leak) - Ratio::powi clamps to f64::MAX/MIN on overflow - Add 4 invariant-preservation tests (80 total) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
use crate::types::CommonTypeError;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
|
||||
use std::str::FromStr;
|
||||
@@ -31,7 +32,7 @@ pub const SCALE: i64 = 1_000_000;
|
||||
///
|
||||
/// Internally stored as `i64` with a scale of 1,000,000.
|
||||
/// Rejects negative, NaN, and Inf on construction from floats.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Price(i64);
|
||||
|
||||
impl Price {
|
||||
@@ -323,7 +324,7 @@ impl Default for Price {
|
||||
/// Signed fixed-point quantity with 6 decimal places.
|
||||
///
|
||||
/// Negative values represent short positions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Quantity(i64);
|
||||
|
||||
impl Quantity {
|
||||
@@ -504,7 +505,7 @@ impl Default for Quantity {
|
||||
/// Signed fixed-point monetary amount with 6 decimal places.
|
||||
///
|
||||
/// Negative values represent losses. Rejects NaN/Inf.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Money(i64);
|
||||
|
||||
impl Money {
|
||||
@@ -662,7 +663,7 @@ impl Default for Money {
|
||||
/// Validated `f64` newtype. Rejects NaN and Inf on construction.
|
||||
///
|
||||
/// Division by zero returns `Ratio(0.0)` instead of panicking.
|
||||
#[derive(Debug, Clone, Copy, PartialOrd)]
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Ratio(f64);
|
||||
|
||||
impl Ratio {
|
||||
@@ -721,25 +722,41 @@ impl Ratio {
|
||||
Self(self.0.sqrt())
|
||||
}
|
||||
|
||||
/// Natural logarithm.
|
||||
/// Natural logarithm. Returns `Ratio(0.0)` for non-positive inputs
|
||||
/// (preserves the finite invariant — `ln(-1)` would be NaN).
|
||||
#[must_use]
|
||||
#[allow(clippy::float_arithmetic)]
|
||||
pub fn ln(self) -> Self {
|
||||
if self.0 <= 0.0 {
|
||||
return Self(0.0);
|
||||
}
|
||||
Self(self.0.ln())
|
||||
}
|
||||
|
||||
/// Exponential (`e^self`).
|
||||
/// Exponential (`e^self`). Clamps to `f64::MAX` to preserve the finite invariant.
|
||||
#[must_use]
|
||||
#[allow(clippy::float_arithmetic)]
|
||||
pub fn exp(self) -> Self {
|
||||
Self(self.0.exp())
|
||||
let result = self.0.exp();
|
||||
if result.is_finite() {
|
||||
Self(result)
|
||||
} else {
|
||||
Self(f64::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raise to an integer power.
|
||||
/// Raise to an integer power. Clamps to `f64::MAX`/`f64::MIN` to preserve finite invariant.
|
||||
#[must_use]
|
||||
#[allow(clippy::float_arithmetic)]
|
||||
pub fn powi(self, n: i32) -> Self {
|
||||
Self(self.0.powi(n))
|
||||
let result = self.0.powi(n);
|
||||
if result.is_finite() {
|
||||
Self(result)
|
||||
} else if result.is_sign_positive() {
|
||||
Self(f64::MAX)
|
||||
} else {
|
||||
Self(f64::MIN)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamp value to `[min, max]`.
|
||||
@@ -1416,6 +1433,34 @@ mod tests {
|
||||
assert!((result.to_f64() - (-10.0)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// ===== Ratio invariant preservation =====
|
||||
|
||||
#[test]
|
||||
fn ratio_ln_negative_returns_zero() {
|
||||
let r = Ratio::new(-1.0).unwrap();
|
||||
assert_eq!(r.ln().to_f64(), 0.0); // ln(-1) would be NaN; we return 0.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_ln_zero_returns_zero() {
|
||||
assert_eq!(Ratio::ZERO.ln().to_f64(), 0.0); // ln(0) would be -Inf; we return 0.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_exp_overflow_clamps() {
|
||||
let r = Ratio::new(1000.0).unwrap();
|
||||
let result = r.exp();
|
||||
assert!(result.to_f64().is_finite()); // would be Inf; clamped to f64::MAX
|
||||
assert_eq!(result.to_f64(), f64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_powi_overflow_clamps() {
|
||||
let r = Ratio::new(1e200).unwrap();
|
||||
let result = r.powi(2);
|
||||
assert!(result.to_f64().is_finite()); // would be Inf; clamped
|
||||
}
|
||||
|
||||
// ===== Module-level SCALE =====
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user