emath/
smart_aim.rs

1//! Find "simple" numbers is some range. Used by sliders.
2
3use crate::fast_midpoint;
4
5const NUM_DECIMALS: usize = 15;
6
7/// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits.
8///
9/// So in the range `[0.83, 1.354]` you will get `1.0`, and for `[0.37, 0.48]` you will get `0.4`.
10/// This is used when dragging sliders etc to get the values that users are most likely to desire.
11/// This assumes a decimal centric user.
12pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
13    // Avoid NaN if we can:
14    if min.is_nan() {
15        return max;
16    }
17    if max.is_nan() {
18        return min;
19    }
20
21    if max < min {
22        return best_in_range_f64(max, min);
23    }
24    if min == max {
25        return min;
26    }
27    if min <= 0.0 && 0.0 <= max {
28        return 0.0; // always prefer zero
29    }
30    if min < 0.0 {
31        return -best_in_range_f64(-max, -min);
32    }
33
34    debug_assert!(0.0 < min && min < max, "Logic bug");
35
36    // Prefer finite numbers:
37    if !max.is_finite() {
38        return min;
39    }
40    debug_assert!(
41        min.is_finite() && max.is_finite(),
42        "min: {min:?}, max: {max:?}"
43    );
44
45    let min_exponent = min.log10();
46    let max_exponent = max.log10();
47
48    if min_exponent.floor() != max_exponent.floor() {
49        // Different orders of magnitude.
50        // Pick the geometric center of the two:
51        let exponent = fast_midpoint(min_exponent, max_exponent);
52        return 10.0_f64.powi(exponent.round() as i32);
53    }
54
55    if is_integer(min_exponent) {
56        return 10.0_f64.powf(min_exponent);
57    }
58    if is_integer(max_exponent) {
59        return 10.0_f64.powf(max_exponent);
60    }
61
62    // Find the proper scale, and then convert to integers:
63
64    let scale = NUM_DECIMALS as i32 - max_exponent.floor() as i32 - 1;
65    let scale_factor = 10.0_f64.powi(scale);
66
67    let min_str = to_decimal_string((min * scale_factor).round() as u64);
68    let max_str = to_decimal_string((max * scale_factor).round() as u64);
69
70    // We now have two positive integers of the same length.
71    // We want to find the first non-matching digit,
72    // which we will call the "deciding digit".
73    // Everything before it will be the same,
74    // everything after will be zero,
75    // and the deciding digit itself will be picked as a "smart average"
76    // min:    12345
77    // max:    12780
78    // output: 12500
79
80    let mut ret_str = [0; NUM_DECIMALS];
81
82    for i in 0..NUM_DECIMALS {
83        if min_str[i] == max_str[i] {
84            ret_str[i] = min_str[i];
85        } else {
86            // Found the deciding digit at index `i`
87            let mut deciding_digit_min = min_str[i];
88            let deciding_digit_max = max_str[i];
89
90            debug_assert!(
91                deciding_digit_min < deciding_digit_max,
92                "Bug in smart aim code"
93            );
94
95            let rest_of_min_is_zeroes = min_str[i + 1..].iter().all(|&c| c == 0);
96
97            if !rest_of_min_is_zeroes {
98                // There are more digits coming after `deciding_digit_min`, so we cannot pick it.
99                // So the true min of what we can pick is one greater:
100                deciding_digit_min += 1;
101            }
102
103            let deciding_digit = if deciding_digit_min == 0 {
104                0
105            } else if deciding_digit_min <= 5 && 5 <= deciding_digit_max {
106                5 // 5 is the roundest number in the range
107            } else {
108                deciding_digit_min.midpoint(deciding_digit_max)
109            };
110
111            ret_str[i] = deciding_digit;
112
113            return from_decimal_string(ret_str) as f64 / scale_factor;
114        }
115    }
116
117    min // All digits are the same. Already handled earlier, but better safe than sorry
118}
119
120fn is_integer(f: f64) -> bool {
121    f.round() == f
122}
123
124fn to_decimal_string(v: u64) -> [u8; NUM_DECIMALS] {
125    let mut ret = [0; NUM_DECIMALS];
126    let mut value = v;
127    for i in (0..NUM_DECIMALS).rev() {
128        ret[i] = (value % 10) as u8;
129        value /= 10;
130    }
131    ret
132}
133
134fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 {
135    let mut value = 0;
136    for &c in &s {
137        debug_assert!(c <= 9, "Bad number");
138        value = value * 10 + c as u64;
139    }
140    value
141}
142
143#[expect(clippy::approx_constant)]
144#[test]
145fn test_aim() {
146    assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero");
147    assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero");
148    assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero");
149    assert_eq!(best_in_range_f64(0.2, 0.0), 0.0, "Prefer zero");
150    assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
151    assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
152    assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
153    assert_eq!(best_in_range_f64(0.4, 0.9), 0.5, "Prefer ending on 5");
154    assert_eq!(best_in_range_f64(14.1, 19.99), 15.0, "Prefer ending on 5");
155    assert_eq!(best_in_range_f64(12.3, 65.9), 50.0, "Prefer leading 5");
156    assert_eq!(best_in_range_f64(493.0, 879.0), 500.0, "Prefer leading 5");
157    assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
158    // assert_eq!(best_in_range_f64(123.71, 123.76), 123.75); // TODO(emilk): we get 123.74999999999999 here
159    // assert_eq!(best_in_range_f32(123.71, 123.76), 123.75);
160    assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
161    assert_eq!(best_in_range_f64(7.5, 76.3), 10.0);
162    assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
163    assert_eq!(best_in_range_f64(7.5, 1_345.0), 100.0);
164    assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0, "Geometric mean");
165    assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
166    assert_eq!(best_in_range_f64(10.000, 99.999), 10.0);
167    assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
168    assert_eq!(best_in_range_f64(10.001, 100.000), 100.0);
169    assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
170    assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
171
172    const NAN: f64 = f64::NAN;
173    const INFINITY: f64 = f64::INFINITY;
174    const NEG_INFINITY: f64 = f64::NEG_INFINITY;
175    assert!(best_in_range_f64(NAN, NAN).is_nan());
176    assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
177    assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);
178    assert_eq!(best_in_range_f64(1.2, NAN), 1.2);
179    assert_eq!(best_in_range_f64(1.2, INFINITY), 1.2);
180    assert_eq!(best_in_range_f64(INFINITY, 1.2), 1.2);
181    assert_eq!(best_in_range_f64(NEG_INFINITY, 1.2), 0.0);
182    assert_eq!(best_in_range_f64(NEG_INFINITY, -2.7), -2.7);
183    assert_eq!(best_in_range_f64(INFINITY, INFINITY), INFINITY);
184    assert_eq!(best_in_range_f64(NEG_INFINITY, NEG_INFINITY), NEG_INFINITY);
185    assert_eq!(best_in_range_f64(NEG_INFINITY, INFINITY), 0.0);
186    assert_eq!(best_in_range_f64(INFINITY, NEG_INFINITY), 0.0);
187
188    #[track_caller]
189    fn test_f64((min, max): (f64, f64), expected: f64) {
190        let aimed = best_in_range_f64(min, max);
191        assert!(
192            aimed == expected,
193            "smart_aim({min} – {max}) => {aimed}, but expected {expected}"
194        );
195    }
196    #[track_caller]
197    fn test_i64((min, max): (i64, i64), expected: i64) {
198        let aimed = best_in_range_f64(min as _, max as _);
199        assert!(
200            aimed == expected as f64,
201            "smart_aim({min} – {max}) => {aimed}, but expected {expected}"
202        );
203    }
204
205    test_i64((99, 300), 100);
206    test_i64((300, 99), 100);
207    test_i64((-99, -300), -100);
208    test_i64((-99, 123), 0); // Prefer zero
209    test_i64((4, 9), 5); // Prefer ending on 5
210    test_i64((14, 19), 15); // Prefer ending on 5
211    test_i64((12, 65), 50); // Prefer leading 5
212    test_i64((493, 879), 500); // Prefer leading 5
213    test_i64((37, 48), 40);
214    test_i64((100, 123), 100);
215    test_i64((101, 1000), 1000);
216    test_i64((999, 1000), 1000);
217    test_i64((123, 500), 500);
218    test_i64((500, 777), 500);
219    test_i64((500, 999), 500);
220    test_i64((12345, 12780), 12500);
221    test_i64((12371, 12376), 12375);
222    test_i64((12371, 12376), 12375);
223
224    test_f64((7.5, 16.3), 10.0);
225    test_f64((7.5, 76.3), 10.0);
226    test_f64((7.5, 763.3), 100.0);
227    test_f64((7.5, 1_345.0), 100.0); // Geometric mean
228    test_f64((7.5, 123_456.0), 1_000.0); // Geometric mean
229    test_f64((-0.2, 0.0), 0.0); // Prefer zero
230    test_f64((-10_004.23, 4.14), 0.0); // Prefer zero
231    test_f64((-0.2, 100.0), 0.0); // Prefer zero
232    test_f64((0.2, 0.0), 0.0); // Prefer zero
233    test_f64((7.8, 17.8), 10.0);
234    test_f64((14.1, 19.1), 15.0); // Prefer ending on 5
235    test_f64((12.3, 65.9), 50.0); // Prefer leading 5
236}