use std::fmt::Write as _;
const EXPONENT_LOW: i32 = -4;
const EXPONENT_HIGH: i32 = 15;
pub fn push_float(out: &mut String, value: f32) {
push(
out,
value as f64,
|text| text.parse::<f32>() == Ok(value),
{
let mut s = String::new();
let _ = write!(s, "{value}");
s
},
{
let mut s = String::new();
let _ = write!(s, "{value:e}");
s
},
);
}
pub fn push_double(out: &mut String, value: f64) {
push(
out,
value,
|text| text.parse::<f64>() == Ok(value),
{
let mut s = String::new();
let _ = write!(s, "{value}");
s
},
{
let mut s = String::new();
let _ = write!(s, "{value:e}");
s
},
);
}
fn push(
out: &mut String,
value: f64,
round_trips: impl Fn(&str) -> bool,
plain: String,
exponential: String,
) {
if value.is_nan() {
out.push_str("nan");
return;
}
if value.is_infinite() {
out.push_str(if value < 0.0 { "-inf" } else { "inf" });
return;
}
let exponent: i32 = exponential
.rsplit_once('e')
.and_then(|(_, e)| e.parse().ok())
.unwrap_or(0);
let chosen = if value == 0.0 || (EXPONENT_LOW..EXPONENT_HIGH).contains(&exponent) {
&plain
} else {
&exponential
};
debug_assert!(
round_trips(chosen),
"{chosen} does not read back as {value:e}"
);
out.push_str(chosen);
}
#[cfg(test)]
mod tests {
use super::*;
fn g(value: f32) -> String {
let mut s = String::new();
push_float(&mut s, value);
s
}
fn d(value: f64) -> String {
let mut s = String::new();
push_double(&mut s, value);
s
}
#[test]
fn ordinary_values_read_the_way_they_were_written() {
for (value, expected) in [
(1.0f32, "1"),
(0.0, "0"),
(-2.0, "-2"),
(12345.0, "12345"),
(0.1, "0.1"),
(0.5, "0.5"),
(12345.678, "12345.678"),
(999_999.0, "999999"),
] {
assert_eq!(g(value), expected, "for {value:e}");
}
}
#[test]
fn extreme_magnitudes_use_the_exponent() {
for (value, expected) in [
(3e-7f32, "3e-7"),
(1.5e-10, "1.5e-10"),
(-2.5e-8, "-2.5e-8"),
(1e20, "1e20"),
(1e-5, "1e-5"),
] {
assert_eq!(g(value), expected, "for {value:e}");
}
}
#[test]
fn the_form_follows_the_exponent() {
assert_eq!(g(10_000.0), "10000"); assert_eq!(g(1e7), "10000000"); assert_eq!(g(1e-4), "0.0001"); assert_eq!(g(0.001), "0.001"); assert_eq!(g(1e-5), "1e-5"); assert_eq!(g(1e15), "1e15"); assert_eq!(g(1e14), "100000000000000"); assert_eq!(g(1.0), "1");
assert_eq!(g(0.0), "0");
assert_eq!(g(-0.0), "-0");
}
#[test]
fn shortest_means_shortest_not_a_precision_that_happened_to_work() {
assert_eq!(g(0.3), "0.3");
assert_eq!(g(1.0 / 3.0), "0.33333334");
assert_eq!(d(1.0f64 / 3.0), "0.3333333333333333");
}
#[test]
fn every_value_round_trips() {
let mut state = 0x9E37_79B9u32;
for _ in 0..50_000 {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let value = f32::from_bits(state);
if !value.is_finite() {
continue;
}
let text = g(value);
assert_eq!(
text.parse::<f32>().unwrap().to_bits(),
value.to_bits(),
"{value:e} formatted as {text}"
);
}
}
#[test]
fn every_double_round_trips() {
let mut state = 0x9E37_79B9_7F4A_7C15u64;
for _ in 0..50_000 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let value = f64::from_bits(state);
if !value.is_finite() {
continue;
}
let text = d(value);
assert_eq!(
text.parse::<f64>().unwrap().to_bits(),
value.to_bits(),
"{value:e} formatted as {text}"
);
}
}
#[test]
fn non_finite_values_use_the_c_spellings() {
assert_eq!(g(f32::NAN), "nan");
assert_eq!(g(f32::INFINITY), "inf");
assert_eq!(g(f32::NEG_INFINITY), "-inf");
assert_eq!(d(f64::NAN), "nan");
}
}