emath/
smart_aim.rs

1//! Find "simple" numbers is some range. Used by sliders.
2
3const NUM_DECIMALS: usize = 15;
4
5/// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits.
6///
7/// So in the range `[0.83, 1.354]` you will get `1.0`, and for `[0.37, 0.48]` you will get `0.4`.
8/// This is used when dragging sliders etc to get the values that users are most likely to desire.
9/// This assumes a decimal centric user.
10pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
11    // Avoid NaN if we can:
12    if min.is_nan() {
13        return max;
14    }
15    if max.is_nan() {
16        return min;
17    }
18
19    if max < min {
20        return best_in_range_f64(max, min);
21    }
22    if min == max {
23        return min;
24    }
25    if min <= 0.0 && 0.0 <= max {
26        return 0.0; // always prefer zero
27    }
28    if min < 0.0 {
29        return -best_in_range_f64(-max, -min);
30    }
31
32    // Prefer finite numbers:
33    if !max.is_finite() {
34        return min;
35    }
36    debug_assert!(
37        min.is_finite() && max.is_finite(),
38        "min: {min:?}, max: {max:?}"
39    );
40
41    let min_exponent = min.log10();
42    let max_exponent = max.log10();
43
44    if min_exponent.floor() != max_exponent.floor() {
45        // pick the geometric center of the two:
46        let exponent = (min_exponent + max_exponent) / 2.0;
47        return 10.0_f64.powi(exponent.round() as i32);
48    }
49
50    if is_integer(min_exponent) {
51        return 10.0_f64.powf(min_exponent);
52    }
53    if is_integer(max_exponent) {
54        return 10.0_f64.powf(max_exponent);
55    }
56
57    let exp_factor = 10.0_f64.powi(max_exponent.floor() as i32);
58
59    let min_str = to_decimal_string(min / exp_factor);
60    let max_str = to_decimal_string(max / exp_factor);
61
62    let mut ret_str = [0; NUM_DECIMALS];
63
64    // Select the common prefix:
65    let mut i = 0;
66    while i < NUM_DECIMALS && max_str[i] == min_str[i] {
67        ret_str[i] = max_str[i];
68        i += 1;
69    }
70
71    if i < NUM_DECIMALS {
72        // Pick the deciding digit.
73        // Note that "to_decimal_string" rounds down, so we that's why we add 1 here
74        ret_str[i] = simplest_digit_closed_range(min_str[i] + 1, max_str[i]);
75    }
76
77    from_decimal_string(&ret_str) * exp_factor
78}
79
80fn is_integer(f: f64) -> bool {
81    f.round() == f
82}
83
84fn to_decimal_string(v: f64) -> [i32; NUM_DECIMALS] {
85    debug_assert!(v < 10.0, "{v:?}");
86    let mut digits = [0; NUM_DECIMALS];
87    let mut v = v.abs();
88    for r in &mut digits {
89        let digit = v.floor();
90        *r = digit as i32;
91        v -= digit;
92        v *= 10.0;
93    }
94    digits
95}
96
97fn from_decimal_string(s: &[i32]) -> f64 {
98    let mut ret: f64 = 0.0;
99    for (i, &digit) in s.iter().enumerate() {
100        ret += (digit as f64) * 10.0_f64.powi(-(i as i32));
101    }
102    ret
103}
104
105/// Find the simplest integer in the range [min, max]
106fn simplest_digit_closed_range(min: i32, max: i32) -> i32 {
107    debug_assert!(
108        1 <= min && min <= max && max <= 9,
109        "min should be in [1, 9], but was {min:?} and max should be in [min, 9], but was {max:?}"
110    );
111    if min <= 5 && 5 <= max {
112        5
113    } else {
114        (min + max) / 2
115    }
116}
117
118#[expect(clippy::approx_constant)]
119#[test]
120fn test_aim() {
121    assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero");
122    assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero");
123    assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero");
124    assert_eq!(best_in_range_f64(0.2, 0.0), 0.0, "Prefer zero");
125    assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
126    assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
127    assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
128    assert_eq!(best_in_range_f64(0.4, 0.9), 0.5, "Prefer ending on 5");
129    assert_eq!(best_in_range_f64(14.1, 19.99), 15.0, "Prefer ending on 5");
130    assert_eq!(best_in_range_f64(12.3, 65.9), 50.0, "Prefer leading 5");
131    assert_eq!(best_in_range_f64(493.0, 879.0), 500.0, "Prefer leading 5");
132    assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
133    // assert_eq!(best_in_range_f64(123.71, 123.76), 123.75); // TODO(emilk): we get 123.74999999999999 here
134    // assert_eq!(best_in_range_f32(123.71, 123.76), 123.75);
135    assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
136    assert_eq!(best_in_range_f64(7.5, 76.3), 10.0);
137    assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
138    assert_eq!(best_in_range_f64(7.5, 1_345.0), 100.0);
139    assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0, "Geometric mean");
140    assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
141    assert_eq!(best_in_range_f64(10.000, 99.999), 10.0);
142    assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
143    assert_eq!(best_in_range_f64(10.001, 100.000), 100.0);
144    assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
145    assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
146
147    const NAN: f64 = f64::NAN;
148    const INFINITY: f64 = f64::INFINITY;
149    const NEG_INFINITY: f64 = f64::NEG_INFINITY;
150    assert!(best_in_range_f64(NAN, NAN).is_nan());
151    assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
152    assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);
153    assert_eq!(best_in_range_f64(1.2, NAN), 1.2);
154    assert_eq!(best_in_range_f64(1.2, INFINITY), 1.2);
155    assert_eq!(best_in_range_f64(INFINITY, 1.2), 1.2);
156    assert_eq!(best_in_range_f64(NEG_INFINITY, 1.2), 0.0);
157    assert_eq!(best_in_range_f64(NEG_INFINITY, -2.7), -2.7);
158    assert_eq!(best_in_range_f64(INFINITY, INFINITY), INFINITY);
159    assert_eq!(best_in_range_f64(NEG_INFINITY, NEG_INFINITY), NEG_INFINITY);
160    assert_eq!(best_in_range_f64(NEG_INFINITY, INFINITY), 0.0);
161    assert_eq!(best_in_range_f64(INFINITY, NEG_INFINITY), 0.0);
162}