polydat_core/numeric/round_numbers.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The round-number families: the powers, Fibonacci steps, and
5//! nearest-pick the `floor_*`, `ceiling_*`, and `closest_*` nodes and
6//! their native lowerings compute with.
7
8// ---------------------------------------------------------------------------
9// Private helpers (not nodes — plain module fns callable from node bodies).
10// ---------------------------------------------------------------------------
11
12/// True only for a strictly-positive, finite `x`. All family selectors gate
13/// on this and return `0.0` otherwise, so `x <= 0`, `NaN`, and `±inf` all
14/// fold to the zero magnitude without special-casing each node.
15#[inline]
16pub fn positive_finite(x: f64) -> bool {
17 x.is_finite() && x > 0.0
18}
19
20/// Largest power of ten `10^n <= x`. Assumes `positive_finite(x)`.
21///
22/// The exponent is taken from `log10(x).floor()` but reconstructed with
23/// `powi` (exact for `|result| < 2^53`) and corrected by at most one step,
24/// so a one-ulp `log10` error at an exact power-of-ten boundary can't leak.
25///
26/// `pub` so non-node callers can reuse the exact `floor_base10` formula
27/// without re-deriving it (DRY). `floor_base10` the node is just this
28/// function behind the `positive_finite` guard, so a caller that already
29/// guarantees a strictly-positive, finite `x` (e.g. the CQL batch-stride
30/// planner, which floors `budget / row_size`) can call this directly.
31#[inline]
32pub fn floor_pow10(x: f64) -> f64 {
33 let mut e = x.log10().floor() as i32;
34 let mut p = 10f64.powi(e);
35 if p > x {
36 e -= 1;
37 p = 10f64.powi(e);
38 } else if p * 10.0 <= x {
39 e += 1;
40 p = 10f64.powi(e);
41 }
42 p
43}
44
45/// Largest power of two `2^n <= x`. Assumes `positive_finite(x)`.
46///
47/// Same `powi` + one-step-correction scheme as [`floor_pow10`].
48#[inline]
49pub fn floor_pow2(x: f64) -> f64 {
50 let mut e = x.log2().floor() as i32;
51 let mut p = 2f64.powi(e);
52 if p > x {
53 e -= 1;
54 p = 2f64.powi(e);
55 } else if p * 2.0 <= x {
56 e += 1;
57 p = 2f64.powi(e);
58 }
59 p
60}
61
62/// Pick whichever of `lo` / `hi` is nearer to `x` by absolute distance.
63/// Ties resolve to `lo` (the floor), per the `closest_*` contract.
64#[inline]
65pub fn pick_closest(x: f64, lo: f64, hi: f64) -> f64 {
66 if (hi - x).abs() < (x - lo).abs() {
67 hi
68 } else {
69 lo
70 }
71}
72
73/// Largest Fibonacci number (`1, 2, 3, 5, 8, …`) that is `<= x`, or `0.0`
74/// when `x < 1` (nothing in the sequence is that small). Assumes finite `x`.
75pub fn floor_fibonacci_val(x: f64) -> f64 {
76 if x < 1.0 {
77 return 0.0;
78 }
79 let (mut a, mut b) = (1.0f64, 2.0f64);
80 while b <= x {
81 let next = a + b;
82 a = b;
83 b = next;
84 if !b.is_finite() {
85 return a;
86 }
87 }
88 a
89}
90
91/// Smallest Fibonacci number (`1, 2, 3, 5, 8, …`) that is `>= x`; `1.0` for
92/// `x <= 1`. Assumes `positive_finite(x)`.
93pub fn ceiling_fibonacci_val(x: f64) -> f64 {
94 let (mut a, mut b) = (1.0f64, 2.0f64);
95 if x <= a {
96 return a;
97 }
98 loop {
99 if b >= x {
100 return b;
101 }
102 let next = a + b;
103 a = b;
104 b = next;
105 if !b.is_finite() {
106 return b;
107 }
108 }
109}