use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Copy, Debug)]
pub struct Mag {
pub log10: f64,
}
impl Serialize for Mag {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if self.log10 == f64::NEG_INFINITY {
return 0.0_f64.serialize(s);
}
if self.log10 < 300.0 {
return self.to_f64().serialize(s);
}
#[derive(Serialize)]
struct Wide {
log10: f64,
}
Wide { log10: self.log10 }.serialize(s)
}
}
impl<'de> Deserialize<'de> for Mag {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Either {
Number(f64),
Struct { log10: f64 },
}
match Either::deserialize(d)? {
Either::Number(v) => Ok(Mag::from_f64(v)),
Either::Struct { log10 } => Ok(Mag { log10 }),
}
}
}
impl Mag {
pub const ZERO: Mag = Mag {
log10: f64::NEG_INFINITY,
};
pub const ONE: Mag = Mag { log10: 0.0 };
pub fn from_f64(v: f64) -> Mag {
if !v.is_finite() || v <= 0.0 {
Mag::ZERO
} else {
Mag { log10: v.log10() }
}
}
pub fn to_f64(self) -> f64 {
if self.log10.is_nan() {
return 0.0;
}
if self.log10 == f64::NEG_INFINITY {
return 0.0;
}
if self.log10 >= f64::MAX.log10() {
return f64::MAX;
}
10f64.powf(self.log10)
}
pub fn is_zero(self) -> bool {
self.log10 == f64::NEG_INFINITY
}
#[allow(clippy::should_implement_trait)]
pub fn mul(self, rhs: Mag) -> Mag {
if self.is_zero() || rhs.is_zero() {
return Mag::ZERO;
}
Mag {
log10: self.log10 + rhs.log10,
}
}
#[allow(clippy::should_implement_trait)]
pub fn div(self, rhs: Mag) -> Mag {
if self.is_zero() {
return Mag::ZERO;
}
if rhs.is_zero() {
return Mag::ZERO;
}
Mag {
log10: self.log10 - rhs.log10,
}
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, rhs: Mag) -> Mag {
if self.is_zero() {
return rhs;
}
if rhs.is_zero() {
return self;
}
let (hi, lo) = if self.log10 >= rhs.log10 {
(self.log10, rhs.log10)
} else {
(rhs.log10, self.log10)
};
let diff = lo - hi;
if diff < -16.0 {
return Mag { log10: hi };
}
Mag {
log10: hi + (1.0 + 10f64.powf(diff)).log10(),
}
}
pub fn try_sub(self, rhs: Mag) -> Option<Mag> {
if rhs.is_zero() {
return Some(self);
}
if self.log10 < rhs.log10 {
return None;
}
if self.is_zero() {
return Some(Mag::ZERO);
}
let diff = rhs.log10 - self.log10;
if diff < -16.0 {
return Some(self);
}
let factor = 1.0 - 10f64.powf(diff);
if factor <= 0.0 {
return Some(Mag::ZERO);
}
Some(Mag {
log10: self.log10 + factor.log10(),
})
}
pub fn saturating_sub(self, rhs: Mag) -> Mag {
self.try_sub(rhs).unwrap_or(Mag::ZERO)
}
pub fn pow_i(self, n: i32) -> Mag {
if n == 0 {
return Mag::ONE;
}
if self.is_zero() {
return Mag::ZERO;
}
Mag {
log10: self.log10 * (n as f64),
}
}
pub fn floor_u64(self) -> u64 {
let v = self.to_f64().floor();
if v <= 0.0 {
0
} else if v >= u64::MAX as f64 {
u64::MAX
} else {
v as u64
}
}
}
impl Default for Mag {
fn default() -> Self {
Mag::ZERO
}
}
impl PartialEq for Mag {
fn eq(&self, other: &Self) -> bool {
let a = if self.log10.is_nan() {
f64::NEG_INFINITY
} else {
self.log10
};
let b = if other.log10.is_nan() {
f64::NEG_INFINITY
} else {
other.log10
};
a == b
}
}
impl PartialOrd for Mag {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.log10.partial_cmp(&other.log10)
}
}
impl From<f64> for Mag {
fn from(v: f64) -> Self {
Mag::from_f64(v)
}
}
impl From<u32> for Mag {
fn from(v: u32) -> Self {
Mag::from_f64(v as f64)
}
}
impl From<u64> for Mag {
fn from(v: u64) -> Self {
Mag::from_f64(v as f64)
}
}
impl std::ops::Mul for Mag {
type Output = Mag;
fn mul(self, rhs: Mag) -> Mag {
Mag::mul(self, rhs)
}
}
impl std::ops::Div for Mag {
type Output = Mag;
fn div(self, rhs: Mag) -> Mag {
Mag::div(self, rhs)
}
}
impl std::ops::Add for Mag {
type Output = Mag;
fn add(self, rhs: Mag) -> Mag {
Mag::add(self, rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
if a == b {
return true;
}
(a - b).abs() / b.abs().max(1e-12) < 1e-10
}
#[test]
fn from_f64_roundtrip() {
for v in [1.0, 1000.0, 1e20, 1e100, 1e300] {
let m = Mag::from_f64(v);
assert!(approx_eq(m.to_f64(), v), "roundtrip {v} got {}", m.to_f64());
}
}
#[test]
fn zero_and_negative_collapse_to_zero() {
assert_eq!(Mag::from_f64(0.0), Mag::ZERO);
assert_eq!(Mag::from_f64(-5.0), Mag::ZERO);
assert_eq!(Mag::from_f64(f64::NAN), Mag::ZERO);
assert_eq!(Mag::from_f64(f64::INFINITY), Mag::ZERO);
}
#[test]
fn mul_compounds_huge() {
let a = Mag::from_f64(1e200);
let b = Mag::from_f64(1e200);
let c = a.mul(b);
assert!(approx_eq(c.log10, 400.0));
assert_eq!(c.to_f64(), f64::MAX);
}
#[test]
fn pow_i_handles_grindy_compounding() {
let m = Mag::from_f64(1.005);
let p = m.pow_i(100_000);
let expected_log = (1.005_f64).log10() * 100_000.0;
assert!(approx_eq(p.log10, expected_log));
}
#[test]
fn add_handles_disparate_magnitudes() {
let a = Mag::from_f64(1e100);
let b = Mag::from_f64(1e50);
let s = a.add(b);
assert!(approx_eq(s.log10, a.log10));
}
#[test]
fn add_handles_equal_magnitudes() {
let a = Mag::from_f64(1e100);
let s = a.add(a);
assert!(approx_eq(s.log10, 100.0 + 2.0_f64.log10()));
}
#[test]
fn try_sub_returns_none_on_underflow() {
let a = Mag::from_f64(100.0);
let b = Mag::from_f64(200.0);
assert!(a.try_sub(b).is_none());
}
#[test]
fn try_sub_handles_close_values() {
let a = Mag::from_f64(100.0);
let b = Mag::from_f64(99.0);
let s = a.try_sub(b).expect("should fit");
assert!(approx_eq(s.to_f64(), 1.0));
}
#[test]
fn try_sub_zero_is_identity() {
let a = Mag::from_f64(50.0);
assert_eq!(a.try_sub(Mag::ZERO).unwrap(), a);
}
#[test]
fn comparison_order_matches_value_order() {
assert!(Mag::from_f64(10.0) < Mag::from_f64(100.0));
assert!(Mag::ZERO < Mag::from_f64(1.0));
assert!(Mag::from_f64(1e200).mul(Mag::from_f64(1e200)) > Mag::from_f64(1e300));
}
#[test]
fn mul_by_zero_is_zero() {
let a = Mag::from_f64(1e100);
assert_eq!(a.mul(Mag::ZERO), Mag::ZERO);
}
#[test]
fn one_is_multiplicative_identity() {
let a = Mag::from_f64(42.0);
assert!(approx_eq(a.mul(Mag::ONE).to_f64(), a.to_f64()));
assert!(approx_eq(Mag::ONE.mul(a).to_f64(), a.to_f64()));
}
#[test]
fn floor_u64_clamps_at_u64_max() {
let huge = Mag::from_f64(1e25);
assert_eq!(huge.floor_u64(), u64::MAX);
let small = Mag::from_f64(42.7);
assert_eq!(small.floor_u64(), 42);
assert_eq!(Mag::ZERO.floor_u64(), 0);
}
}