|
| 1 | +// Detects rounding discrepancies in the f64 implementation of Display::fmt "{:.prec$}", |
| 2 | +// for a range of floating-point values. |
| 3 | +// |
| 4 | +// Usage: rounding [-v][-n] [depth] |
| 5 | +// |
| 6 | +// depth : max number of digits in the fractional part in the test |
| 7 | +// -v : verbose output |
| 8 | +// -n : negative values |
| 9 | + |
| 10 | +use std::env; |
| 11 | +use std::str::FromStr; |
| 12 | +use std::time::Instant; |
| 13 | + |
| 14 | +mod tests; |
| 15 | + |
| 16 | +fn main() { |
| 17 | + let mut depth = 6; |
| 18 | + let mut verbose = false; |
| 19 | + let mut negative = false; |
| 20 | + let mut args = env::args().skip(1); |
| 21 | + while let Some(arg) = args.next() { |
| 22 | + match arg { |
| 23 | + opt if opt.starts_with('-') => { |
| 24 | + match opt.as_ref() { |
| 25 | + "-v" => verbose = true, |
| 26 | + "-n" => negative = true, |
| 27 | + _ => println!("unknown -option '{opt}'") |
| 28 | + } |
| 29 | + } |
| 30 | + arg => { |
| 31 | + match usize::from_str(&arg) { |
| 32 | + Ok(num) if 0 < num && num < 15 => { |
| 33 | + depth = num; |
| 34 | + } |
| 35 | + _ => { |
| 36 | + println!("Usage: rounding [-v][-n][depth = 1..15]"); |
| 37 | + return; |
| 38 | + } |
| 39 | + } |
| 40 | + } |
| 41 | + } |
| 42 | + } |
| 43 | + let timer = Instant::now(); |
| 44 | + find_issues(depth, verbose, negative); |
| 45 | + let elapsed = timer.elapsed(); |
| 46 | + println!("elapsed time: {:.3} s", elapsed.as_secs_f64()); |
| 47 | +} |
| 48 | + |
| 49 | + |
| 50 | +/// Iterates through floating-point values and compares Display::fmt implementation for f64 |
| 51 | +/// and simple string-based rounding to detect discrepancies. |
| 52 | +/// |
| 53 | +/// * `depth`: maximum number of fractional digits to test |
| 54 | +/// * `verbose`: displays all values |
| 55 | +/// * `negative`: tests negative values instead of positive ones |
| 56 | +/// |
| 57 | +/// Note: we could also check [Round::round_digit] for comparison but it's not correct all |
| 58 | +/// the time anyway. |
| 59 | +fn find_issues(depth: usize, verbose: bool, negative: bool) { |
| 60 | + let it = RoundTestIter::new(depth, negative); |
| 61 | + let mut nbr_test = 0; |
| 62 | + let mut nbr_error = 0; |
| 63 | + if verbose { |
| 64 | + println!("'original value' :'precision': 'Display-rounded' <> 'expected'") |
| 65 | + } |
| 66 | + for (sval, pr) in it { |
| 67 | + let val = f64::from_str(&sval).expect(&format!("error converting {} to f64", sval)); |
| 68 | + let display_val = format!("{val:.pr$}"); |
| 69 | + let sround_val = str_sround(&sval, pr); |
| 70 | + let comp = if display_val == sround_val { |
| 71 | + "==" |
| 72 | + } else { |
| 73 | + nbr_error += 1; |
| 74 | + "<>" |
| 75 | + }; |
| 76 | + nbr_test += 1; |
| 77 | + if verbose { |
| 78 | + println!("{sval:<8}:{pr}: {display_val} {comp} {sround_val}"); |
| 79 | + } |
| 80 | + } |
| 81 | + println!("\n=> {nbr_error} / {nbr_test} error(s) for depth 0-{depth}, so {} %", f64_sround(100.0 * nbr_error as f64 / nbr_test as f64, 1)); |
| 82 | +} |
| 83 | + |
| 84 | +//============================================================================== |
| 85 | +// Iteration through floating-point values (string representation) |
| 86 | +//------------------------------------------------------------------------------ |
| 87 | + |
| 88 | +const INIT_STEP: u8 = b'a'; |
| 89 | +const LAST_STEP: u8 = b'9'; |
| 90 | + |
| 91 | +struct RoundTestIter { |
| 92 | + base: Vec<u8>, |
| 93 | + precision: usize, |
| 94 | + max: usize |
| 95 | +} |
| 96 | + |
| 97 | +impl RoundTestIter { |
| 98 | + pub fn new(max: usize, negative: bool) -> RoundTestIter { |
| 99 | + RoundTestIter { |
| 100 | + base: if negative { b"-0.a".to_vec() } else { b"0.a".to_vec() }, |
| 101 | + precision: 1, |
| 102 | + max, |
| 103 | + } |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +/// step[pr]: |
| 108 | +/// 'a' : checks base + 4*10^-pr, then jumps to 'b' |
| 109 | +/// 'b' : checks base + 5*10^-pr, then tries pr+1, otherwise increases base digits and jumps to 'a' |
| 110 | +/// '0'-'9': base digits |
| 111 | +impl Iterator for RoundTestIter { |
| 112 | + type Item = (String, usize); |
| 113 | + |
| 114 | + fn next(&mut self) -> Option<Self::Item> { |
| 115 | + match self.base.pop() { |
| 116 | + Some(step) if step >= b'a' => { |
| 117 | + let mut value = self.base.clone(); |
| 118 | + value.push(step as u8 - INIT_STEP + b'4'); |
| 119 | + // 'value' only contains ASCII characters: |
| 120 | + let result = Some((unsafe { String::from_utf8_unchecked(value) }, self.precision - 1)); |
| 121 | + if step == b'b' { |
| 122 | + if self.precision < self.max { |
| 123 | + self.base.push(b'0'); |
| 124 | + self.base.push(INIT_STEP); |
| 125 | + self.precision += 1; |
| 126 | + } else { |
| 127 | + self.precision -= 1; |
| 128 | + loop { |
| 129 | + match self.base.pop() { |
| 130 | + Some(digit) if digit == LAST_STEP => { |
| 131 | + self.precision -= 1; |
| 132 | + } |
| 133 | + Some(digit) if digit != b'.' => { |
| 134 | + self.base.push(1 + digit as u8); |
| 135 | + self.base.push(INIT_STEP); |
| 136 | + self.precision += 1; |
| 137 | + break; |
| 138 | + } |
| 139 | + _ => break |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + result |
| 144 | + } else { |
| 145 | + self.base.push(step + 1); |
| 146 | + result |
| 147 | + } |
| 148 | + } |
| 149 | + _ => None |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +//============================================================================== |
| 155 | +// Simple and naive rounding |
| 156 | +//------------------------------------------------------------------------------ |
| 157 | + |
| 158 | +pub trait Round { |
| 159 | + fn round_digit(self, pr: usize) -> Self; |
| 160 | + fn trunc_digit(self, pr: usize) -> Self; |
| 161 | +} |
| 162 | + |
| 163 | +impl Round for f64 { |
| 164 | + #[inline] |
| 165 | + fn round_digit(self, pr: usize) -> f64 { |
| 166 | + let n = pow10(pr as i32); |
| 167 | + (self * n).round() / n |
| 168 | + } |
| 169 | + |
| 170 | + #[inline] |
| 171 | + fn trunc_digit(self, pr: usize) -> f64 { |
| 172 | + let n = pow10(pr as i32); |
| 173 | + (self * n).trunc() / n |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +fn pow10(n: i32) -> f64 { |
| 178 | + match n { |
| 179 | + 0 => 1.0, |
| 180 | + 1 => 10.0, |
| 181 | + 2 => 100.0, |
| 182 | + 3 => 1000.0, |
| 183 | + 4 => 10000.0, |
| 184 | + 5 => 100000.0, |
| 185 | + 6 => 1000000.0, |
| 186 | + 7 => 10000000.0, |
| 187 | + 8 => 100000000.0, |
| 188 | + 9 => 1000000000.0, |
| 189 | + 10 => 10000000000.0, |
| 190 | + 11 => 100000000000.0, |
| 191 | + n => 10.0_f64.powi(n) |
| 192 | + } |
| 193 | +} |
| 194 | + |
| 195 | +//============================================================================== |
| 196 | +// String-based rounding (for comparison) |
| 197 | +//------------------------------------------------------------------------------ |
| 198 | + |
| 199 | +/// Rounds the fractional part of `n` to `pr` digits, using [str_sround] to perform |
| 200 | +/// a rounding to the nearest, away from zero. |
| 201 | +/// |
| 202 | +/// * `n`: floating-point value to round |
| 203 | +/// * `pr`: number of digits to keep in the fractional part |
| 204 | +/// |
| 205 | +/// ``` |
| 206 | +/// assert_eq!(f64_sround(2.95, 1), "3.0"); |
| 207 | +/// assert_eq!(f64_sround(-2.95, 1), "-3.0"); |
| 208 | +/// ``` |
| 209 | +pub fn f64_sround(n: f64, pr: usize) -> String { |
| 210 | + let s = n.to_string(); |
| 211 | + if !n.is_normal() { |
| 212 | + s |
| 213 | + } else { |
| 214 | + str_sround(&s, pr) |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +/// Rounds the fractional part of `n` to `pr` digits, using `str_sround()` to perform |
| 219 | +/// a rounding to the nearest (on the absolute value). The rounding is made by processing the |
| 220 | +/// string, using the "away from zero" method. |
| 221 | +/// |
| 222 | +/// * `n`: string representation of the floating-point value to round. It must contain more than |
| 223 | +/// `pr` digits in the fractional part and ideally the last non-null digit must be rounded properly |
| 224 | +/// (by default of anything better, a `format!("{:.}", f)` of the value - see [f64_sround]) |
| 225 | +/// * `pr`: number of digits to keep in the fractional part |
| 226 | +/// |
| 227 | +/// ``` |
| 228 | +/// assert_eq!(f64_sround("2.95", 1), "3.0"); |
| 229 | +/// assert_eq!(f64_sround("-2.95", 1), "-3.0"); |
| 230 | +/// ``` |
| 231 | +pub fn str_sround(n: &str, pr: usize) -> String { |
| 232 | + let mut s = n.to_string().into_bytes(); |
| 233 | + match s.iter().position(|&x| x == b'.') { |
| 234 | + None => { |
| 235 | + s.push(b'.'); |
| 236 | + for _ in 0..pr { |
| 237 | + s.push(b'0'); |
| 238 | + } |
| 239 | + unsafe { String::from_utf8_unchecked(s) } |
| 240 | + } |
| 241 | + Some(pos) => { |
| 242 | + let prec = s.len() - pos - 1; |
| 243 | + if prec < pr { |
| 244 | + for _ in prec..pr { |
| 245 | + s.push(b'0') |
| 246 | + } |
| 247 | + } else if prec > pr { |
| 248 | + let ch = *s.iter().nth(pos + pr + 1).unwrap(); |
| 249 | + s.truncate(pos + pr + 1); |
| 250 | + if ch >= b'5' { |
| 251 | + // increment s |
| 252 | + let mut frac = 0; |
| 253 | + let mut int = 0; |
| 254 | + let mut is_frac = true; |
| 255 | + loop { |
| 256 | + match s.pop() { |
| 257 | + Some(b'9') if is_frac => { |
| 258 | + frac += 1; |
| 259 | + } |
| 260 | + Some(b'.') => is_frac = false, |
| 261 | + Some(b'9') if !is_frac => { |
| 262 | + int += 1; |
| 263 | + } |
| 264 | + Some(b'-') => { |
| 265 | + s.push(b'-'); |
| 266 | + s.push(b'1'); |
| 267 | + break; |
| 268 | + } |
| 269 | + Some(ch) => { |
| 270 | + s.push(ch + 1); |
| 271 | + break; |
| 272 | + } |
| 273 | + None => { |
| 274 | + s.push(b'1'); |
| 275 | + break; |
| 276 | + }, |
| 277 | + } |
| 278 | + } |
| 279 | + if !is_frac { |
| 280 | + for _ in 0..int { |
| 281 | + s.push(b'0'); |
| 282 | + } |
| 283 | + s.push(b'.'); |
| 284 | + } |
| 285 | + for _ in 0..frac { |
| 286 | + s.push(b'0'); |
| 287 | + } |
| 288 | + } |
| 289 | + } |
| 290 | + // removes '.' if no digit after: |
| 291 | + if s.len() == pos + 1 { |
| 292 | + s.pop(); |
| 293 | + } |
| 294 | + // 's' only contains ASCII characters: |
| 295 | + unsafe { String::from_utf8_unchecked(s) } |
| 296 | + } |
| 297 | + } |
| 298 | +} |
0 commit comments