diff --git a/src/helpers.rs b/src/helpers.rs index c642af7..8731f15 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -55,11 +55,12 @@ pub(crate) fn uuid_to_hex_string(uuid: Uuid) -> String { pub fn truncate_float(float: f64, decimals: u32, round_up: bool) -> f64 { let pow10 = 10i64.pow(decimals) as f64; - let mut float = (float * pow10) as u64; + let sign = float.signum(); + let mut abs_truncated = (float.abs() * pow10) as u64; if round_up { - float += 1; + abs_truncated += 1; } - float as f64 / pow10 + sign * abs_truncated as f64 / pow10 } pub fn bps_diff(x: f64, y: f64) -> u16 { @@ -135,4 +136,64 @@ mod tests { "987654321".to_string() ); } + + #[test] + fn truncate_float_negative_values() { + // Regression: negatives previously cast to u64 and became 0.0 + assert_eq!(truncate_float(-123.456, 2, false), -123.45); + assert_eq!(truncate_float(-99.999, 2, false), -99.99); + assert_eq!(truncate_float(-0.001, 4, false), -0.001); + assert_eq!(truncate_float(-0.00000001, 8, false), -0.00000001); + // decimals = 0 + assert_eq!(truncate_float(-5.9, 0, false), -5.0); + } + + #[test] + fn truncate_float_positive_unchanged() { + // Positive path must be byte-identical to the original implementation + assert_eq!(truncate_float(123.456, 2, false), 123.45); + assert_eq!(truncate_float(99.999, 2, false), 99.99); + assert_eq!(truncate_float(0.00000001, 8, false), 0.00000001); + assert_eq!(truncate_float(0.0, 2, false), 0.0); + } + + #[test] + fn truncate_float_round_up_both_signs() { + // round_up grows magnitude (away from zero) for both signs + assert_eq!(truncate_float(123.456, 2, true), 123.46); + assert_eq!(truncate_float(-123.456, 2, true), -123.46); + assert_eq!(truncate_float(-5.9, 0, true), -6.0); + } + + #[test] + fn truncate_float_zero_edges() { + // 0.0 and -0.0 both compare equal to 0.0 + assert_eq!(truncate_float(0.0, 2, false), 0.0); + assert_eq!(truncate_float(-0.0, 2, false), 0.0); + } + + #[test] + fn truncate_float_nan_propagates() { + // NaN input yields NaN (previous impl silently returned 0.0) + assert!(truncate_float(f64::NAN, 2, false).is_nan()); + } + + #[test] + fn truncate_float_antisymmetry_property() { + // Invariant: truncate(-x) == -truncate(x) for finite x, across + // many magnitudes, decimals, and the round_up flag. + for xi in 1..=2000u32 { + let x = f64::from(xi) * 0.0137; + for d in 0..=8u32 { + for round_up in [false, true] { + let neg = truncate_float(-x, d, round_up); + let pos = -truncate_float(x, d, round_up); + assert!( + (neg - pos).abs() < 1e-12, + "antisymmetry broke: x={x} d={d} round_up={round_up} neg={neg} pos={pos}" + ); + } + } + } + } }