Skip to content

Commit 736975d

Browse files
author
redglyph
committed
add initial commit
0 parents  commit 736975d

File tree

6 files changed

+473
-0
lines changed

6 files changed

+473
-0
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
/.idea
3+
/*.iml

Diff for: Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "rounding"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]

Diff for: README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Display::fmt rounding discrepancies
2+
3+
This is a basic program that looks for discrepancies in floating-point values rounded by `format!("{f:.prec$}")`,
4+
for f64 `f` values.
5+
6+
The rounded values are compared to a naive string-based rounding. This benchmark is not infaillible since it uses
7+
the full Display representation of the floating-point values, though a visual inspection on a number of tests hasn't
8+
revealed any issue. At worst there might be a few false positives or negatives, but far fewer than `Display::fmt`
9+
errors.
10+
11+
Usage:
12+
13+
Usage: `rounding [-v][-n] [depth]`
14+
15+
* `depth` : max number of digits in the fractional part in the test (default = 6)
16+
* `-v` : verbose output
17+
* `-n` : negative values (by default, the test is performed on positive values)
18+
19+
Observed results:
20+
21+
=> 5555555 / 22222222 error(s) for depth 0-8, so 25.0 %
22+
23+
The ratio is 5/22 for all tested depths.
24+
25+
# LICENSE
26+
27+
Copyright (c) 2022 Redglyph, All rights reserved.
28+
29+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
30+
following conditions are met:
31+
32+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
33+
disclaimer.
34+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
35+
disclaimer in the documentation and/or other materials provided with the distribution.
36+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
37+
derived from this software without specific prior written permission.
38+
39+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
40+
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
41+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
42+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
43+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
44+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
45+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Diff for: src/main.rs

+298
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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

Comments
 (0)