Files
settled-reach/server/src/bps.rs
T
jpmschweitzerandClaude Opus 4.8 f1d52944af fix(simulation): correct crate name in log10_floor doctest
The doctest imported `server::bps::log10_floor`, but the library crate is
`settled_reach_server` — so `cargo test --doc` failed to compile the
example (unresolved module `server`). Broken since the helper landed in
#145; it only surfaces under `--doc`, not `--lib`, so the pre-push gate
(which runs `--lib`) never caught it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:44:06 +02:00

123 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Fixed-point / basis-point helpers (D-010 integer arithmetic).
//!
//! The codebase represents fractional values (0.01.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 (010_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);
}
}