//! Fixed-point / basis-point helpers (D-010 integer arithmetic). //! //! The codebase represents fractional values (0.0–1.0) as **basis points** //! (`bps`), where `10_000 bps = 1.0`. This avoids f32/f64 in determinism- //! sensitive paths: given the same inputs, pure integer arithmetic produces //! identical results on every platform and compiler version. //! //! Convention: field names carry the `_bps` suffix; raw bps values are `u32`. //! //! # Integer log₁₀ //! //! [`log10_floor`] is the single reference implementation of `floor(log10(n))` //! for positive integers. Use this everywhere a "how many orders of magnitude" //! calculation would otherwise reach for `f64::log10`. District-mix population //! tiers and prosperity pop-bonuses both need it. /// Integer floor of log₁₀ for `n ≥ 1`. /// /// Returns 0 for n = 1..9, 1 for n = 10..99, 2 for n = 100..999, and so on. /// Panics in debug if `n == 0` (log10(0) is undefined); returns 0 in release. /// /// No floats, no platform-dependent rounding — deterministic (D-010). /// /// # Examples /// /// ``` /// use settled_reach_server::bps::log10_floor; /// assert_eq!(log10_floor(1), 0); /// assert_eq!(log10_floor(9), 0); /// assert_eq!(log10_floor(10), 1); /// assert_eq!(log10_floor(100), 2); /// assert_eq!(log10_floor(999), 2); /// assert_eq!(log10_floor(1_000_000), 6); /// ``` pub fn log10_floor(n: u64) -> u32 { debug_assert!(n >= 1, "log10_floor: n must be ≥ 1 (got {n})"); if n == 0 { return 0; } let mut v = n; let mut result = 0u32; while v >= 10 { v /= 10; result += 1; } result } /// Convert a bps value (0–10_000) to a clamped f32 in [0.0, 1.0]. /// /// Only call this at the edge of a system that genuinely needs f32 — the stored /// representation stays integer. Document why f32 is needed at the call site. #[inline] pub fn bps_to_f32(bps: u32) -> f32 { bps as f32 / 10_000.0 } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn log10_floor_single_digits() { // 1..9 all return 0. for n in 1u64..10 { assert_eq!(log10_floor(n), 0, "expected 0 for n={n}"); } } #[test] fn log10_floor_boundary_10() { assert_eq!(log10_floor(9), 0); assert_eq!(log10_floor(10), 1); } #[test] fn log10_floor_boundary_100() { assert_eq!(log10_floor(99), 1); assert_eq!(log10_floor(100), 2); } #[test] fn log10_floor_boundary_1000() { assert_eq!(log10_floor(999), 2); assert_eq!(log10_floor(1_000), 3); } #[test] fn log10_floor_large_values() { assert_eq!(log10_floor(999_999), 5); assert_eq!(log10_floor(1_000_000), 6); assert_eq!(log10_floor(9_999_999), 6); assert_eq!(log10_floor(10_000_000), 7); } #[test] fn log10_floor_very_large() { // u64::MAX = 18_446_744_073_709_551_615 → 19 digits → floor = 19. assert_eq!(log10_floor(u64::MAX), 19); } #[test] fn bps_to_f32_midpoint() { let f = bps_to_f32(5_000); assert!((f - 0.5).abs() < 1e-6, "5000 bps should be 0.5, got {f}"); } #[test] fn bps_to_f32_full_scale() { let f = bps_to_f32(10_000); assert!((f - 1.0).abs() < 1e-6, "10000 bps should be 1.0, got {f}"); } #[test] fn bps_to_f32_zero() { assert_eq!(bps_to_f32(0), 0.0); } }