@@ -31,6 +31,8 @@ pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
3131 return -best_in_range_f64 ( -max, -min) ;
3232 }
3333
34+ debug_assert ! ( 0.0 < min && min < max, "Logic bug" ) ;
35+
3436 // Prefer finite numbers:
3537 if !max. is_finite ( ) {
3638 return min;
@@ -44,7 +46,8 @@ pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
4446 let max_exponent = max. log10 ( ) ;
4547
4648 if min_exponent. floor ( ) != max_exponent. floor ( ) {
47- // pick the geometric center of the two:
49+ // Different orders of magnitude.
50+ // Pick the geometric center of the two:
4851 let exponent = fast_midpoint ( min_exponent, max_exponent) ;
4952 return 10.0_f64 . powi ( exponent. round ( ) as i32 ) ;
5053 }
@@ -56,65 +59,85 @@ pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
5659 return 10.0_f64 . powf ( max_exponent) ;
5760 }
5861
59- let exp_factor = 10.0_f64 . powi ( max_exponent . floor ( ) as i32 ) ;
62+ // Find the proper scale, and then convert to integers:
6063
61- let min_str = to_decimal_string ( min / exp_factor ) ;
62- let max_str = to_decimal_string ( max / exp_factor ) ;
64+ let scale = NUM_DECIMALS as i32 - max_exponent . floor ( ) as i32 - 1 ;
65+ let scale_factor = 10.0_f64 . powi ( scale ) ;
6366
64- let mut ret_str = [ 0 ; NUM_DECIMALS ] ;
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 ) ;
6569
66- // Select the common prefix:
67- let mut i = 0 ;
68- while i < NUM_DECIMALS && max_str[ i] == min_str[ i] {
69- ret_str[ i] = max_str[ i] ;
70- i += 1 ;
71- }
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
7279
73- if i < NUM_DECIMALS {
74- // Pick the deciding digit.
75- // Note that "to_decimal_string" rounds down, so we that's why we add 1 here
76- ret_str[ i] = simplest_digit_closed_range ( min_str[ i] + 1 , max_str[ i] ) ;
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+ }
77115 }
78116
79- from_decimal_string ( & ret_str ) * exp_factor
117+ min // All digits are the same. Already handled earlier, but better safe than sorry
80118}
81119
82120fn is_integer ( f : f64 ) -> bool {
83121 f. round ( ) == f
84122}
85123
86- fn to_decimal_string ( v : f64 ) -> [ i32 ; NUM_DECIMALS ] {
87- debug_assert ! ( v < 10.0 , "{v:?}" ) ;
88- let mut digits = [ 0 ; NUM_DECIMALS ] ;
89- let mut v = v. abs ( ) ;
90- for r in & mut digits {
91- let digit = v. floor ( ) ;
92- * r = digit as i32 ;
93- v -= digit;
94- v *= 10.0 ;
95- }
96- digits
97- }
98-
99- fn from_decimal_string ( s : & [ i32 ] ) -> f64 {
100- let mut ret: f64 = 0.0 ;
101- for ( i, & digit) in s. iter ( ) . enumerate ( ) {
102- ret += ( digit as f64 ) * 10.0_f64 . powi ( -( i as i32 ) ) ;
124+ fn 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 ;
103130 }
104131 ret
105132}
106133
107- /// Find the simplest integer in the range [min, max]
108- fn simplest_digit_closed_range ( min : i32 , max : i32 ) -> i32 {
109- debug_assert ! (
110- 1 <= min && min <= max && max <= 9 ,
111- "min should be in [1, 9], but was {min:?} and max should be in [min, 9], but was {max:?}"
112- ) ;
113- if min <= 5 && 5 <= max {
114- 5
115- } else {
116- min. midpoint ( max)
134+ fn 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 ;
117139 }
140+ value
118141}
119142
120143#[ expect( clippy:: approx_constant) ]
@@ -161,4 +184,53 @@ fn test_aim() {
161184 assert_eq ! ( best_in_range_f64( NEG_INFINITY , NEG_INFINITY ) , NEG_INFINITY ) ;
162185 assert_eq ! ( best_in_range_f64( NEG_INFINITY , INFINITY ) , 0.0 ) ;
163186 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
164236}
0 commit comments