use serde::{Deserialize, Serialize};
use std::ops::Add;
use thiserror::Error;
#[derive(Debug, Error, Clone, Copy)]
pub enum UsdValueError {
#[error("USD value cannot be negative: {0}")]
Negative(f64),
#[error("USD value cannot be NaN")]
NaN,
#[error("USD value cannot be infinite: {0}")]
Infinite(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct UsdValue(f64);
impl std::hash::Hash for UsdValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
impl UsdValue {
pub const ZERO: Self = Self(0.0);
pub fn new(value: f64) -> Self {
Self::try_new(value).unwrap_or_else(|e| panic!("Invalid USD value: {}", e))
}
pub fn try_new(value: f64) -> Result<Self, UsdValueError> {
if value.is_nan() {
return Err(UsdValueError::NaN);
}
if value.is_infinite() {
return Err(UsdValueError::Infinite(value));
}
const TOLERANCE: f64 = 1e-6;
if value < -TOLERANCE {
return Err(UsdValueError::Negative(value));
}
Ok(Self(value.max(0.0)))
}
pub const fn from_non_negative(value: f64) -> Self {
Self(value)
}
pub const fn as_f64(&self) -> f64 {
self.0
}
pub fn is_zero(&self) -> bool {
self.0.abs() < f64::EPSILON
}
pub fn format(&self, precision: usize) -> String {
format!("${:.precision$}", self.0, precision = precision)
}
pub fn abs(&self) -> Self {
Self(self.0.abs())
}
}
impl Add for UsdValue {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl std::ops::AddAssign for UsdValue {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl std::ops::Sub for UsdValue {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self((self.0 - rhs.0).max(0.0))
}
}
impl std::fmt::Display for UsdValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "${:.2}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_usd_value_format() {
let value = UsdValue::new(1234.567);
assert_eq!(value.format(2), "$1234.57");
assert_eq!(value.format(0), "$1235");
assert_eq!(value.format(3), "$1234.567");
}
#[test]
fn test_try_new_negative() {
let result = UsdValue::try_new(-100.0);
assert!(result.is_err());
match result {
Err(UsdValueError::Negative(v)) => assert_eq!(v, -100.0),
_ => panic!("Expected Negative error"),
}
}
#[test]
fn test_try_new_tiny_negative_clamped_to_zero() {
let tiny_negative = -0.0000000005433305882411751; let result = UsdValue::try_new(tiny_negative);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_f64(), 0.0);
let within_tolerance = -0.0000005; assert!(UsdValue::try_new(within_tolerance).unwrap().as_f64() == 0.0);
}
#[test]
fn test_try_new_negative_beyond_tolerance_rejected() {
let beyond_tolerance = -0.000002; let result = UsdValue::try_new(beyond_tolerance);
assert!(result.is_err());
assert!(matches!(result, Err(UsdValueError::Negative(_))));
assert!(UsdValue::try_new(-0.01).is_err());
}
#[test]
fn test_try_new_nan() {
let result = UsdValue::try_new(f64::NAN);
assert!(result.is_err());
assert!(matches!(result, Err(UsdValueError::NaN)));
}
#[test]
fn test_try_new_infinite() {
let result = UsdValue::try_new(f64::INFINITY);
assert!(result.is_err());
assert!(matches!(result, Err(UsdValueError::Infinite(_))));
}
#[test]
#[should_panic(expected = "Invalid USD value")]
fn test_new_panics_on_negative() {
let _value = UsdValue::new(-100.0);
}
#[test]
#[should_panic(expected = "Invalid USD value")]
fn test_new_panics_on_nan() {
let _value = UsdValue::new(f64::NAN);
}
#[test]
fn test_subtraction_saturates() {
let a = UsdValue::new(100.0);
let b = UsdValue::new(30.0);
assert_eq!((a - b).as_f64(), 70.0);
let c = UsdValue::new(10.0);
let d = UsdValue::new(50.0);
assert_eq!((c - d).as_f64(), 0.0);
}
#[test]
fn test_from_non_negative_const() {
const HUNDRED: UsdValue = UsdValue::from_non_negative(100.0);
assert_eq!(HUNDRED.as_f64(), 100.0);
}
}