use super::integer::{MAX_DIGITS, digits_of};
const POSITIONAL_FROM: i32 = -4;
const EXPONENT_FROM: i32 = 16;
const MAX_TIE_PLACES: u32 = 26;
const SCRATCH: usize = 40;
pub(super) fn push_float(f: f64, out: &mut Vec<u8>) {
if f.is_finite() {
push_finite(f, out);
} else {
out.extend_from_slice(b"null");
}
}
pub(crate) fn float_string(f: f64) -> String {
let mut out = Vec::new();
push_finite(f, &mut out);
String::from_utf8(out).unwrap_or_default()
}
fn push_finite(f: f64, out: &mut Vec<u8>) {
if f == 0.0 {
out.extend_from_slice(if f.is_sign_negative() {
b"-0.0"
} else {
b"0.0"
});
return;
}
let mut buffer = zmij::Buffer::new();
let shortest = buffer.format_finite(f);
let tie = tie_digits(f, shortest);
if tie.is_none() && f.abs() >= 1e-4 && !shortest.as_bytes().contains(&b'e') {
out.extend_from_slice(shortest.as_bytes());
return;
}
respell(shortest, tie, out);
}
fn tie_digits(f: f64, shortest: &str) -> Option<u64> {
let bits = f.to_bits();
let biased = i32::try_from((bits >> 52) & 0x7ff).unwrap_or(0);
let fraction = bits & ((1 << 52) - 1);
let (mantissa, exponent) = if biased == 0 {
(fraction, -1074)
} else {
(fraction | (1 << 52), biased - 1075)
};
let places = mantissa.trailing_zeros();
let binary_places = u32::try_from(-(exponent + i32::try_from(places).unwrap_or(0))).ok()?;
if binary_places == 0 || binary_places > MAX_TIE_PLACES {
return None;
}
let exact = u128::from(mantissa >> places) * u128::from(5_u64.pow(binary_places));
if usize::try_from(exact.ilog10()).ok()? != significant_digits(shortest) {
return None;
}
u64::try_from(exact / 10 + 1).ok()
}
fn significant_digits(shortest: &str) -> usize {
let mut first = None;
let mut last = 0;
let digits = shortest
.bytes()
.take_while(|&b| b != b'e')
.filter(u8::is_ascii_digit);
for (at, digit) in digits.enumerate() {
if digit != b'0' {
first.get_or_insert(at);
last = at;
}
}
first.map_or(0, |first| last - first + 1)
}
fn respell(shortest: &str, tie: Option<u64>, out: &mut Vec<u8>) {
let (negative, unsigned) = match shortest.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, shortest),
};
let (mantissa, exponent) = unsigned.split_once('e').unwrap_or((unsigned, "0"));
let exponent: i32 = exponent.trim_start_matches('+').parse().unwrap_or(0);
let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let mut scratch = [0_u8; SCRATCH];
let all = whole.bytes().chain(fraction.bytes());
let mut len = 0;
for (slot, digit) in scratch.iter_mut().zip(all) {
*slot = digit;
len += 1;
}
let leading = scratch[..len].iter().take_while(|&&d| d == b'0').count();
let digits = &scratch[leading..len];
let digits = trim_zeros(digits);
let mut point =
i32::try_from(whole.len()).unwrap_or(0) + exponent - i32::try_from(leading).unwrap_or(0);
if negative {
out.push(b'-');
}
let mut upper = [0_u8; MAX_DIGITS];
let Some(tie) = tie else {
return place(digits, point, out);
};
let upper = digits_of(tie, &mut upper);
point += i32::try_from(upper.len() - digits.len()).unwrap_or(0);
place(trim_zeros(upper), point, out);
}
fn trim_zeros(digits: &[u8]) -> &[u8] {
&digits[..digits.iter().rposition(|&d| d != b'0').map_or(0, |p| p + 1)]
}
fn place(digits: &[u8], point: i32, out: &mut Vec<u8>) {
let exponent = point - 1;
if !(POSITIONAL_FROM..EXPONENT_FROM).contains(&exponent) {
let (first, rest) = digits.split_at(1);
out.extend_from_slice(first);
if !rest.is_empty() {
out.push(b'.');
out.extend_from_slice(rest);
}
out.push(b'e');
out.extend_from_slice(exponent.to_string().as_bytes());
return;
}
let count = i32::try_from(digits.len()).unwrap_or(0);
if point <= 0 {
out.extend_from_slice(b"0.");
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(-point).unwrap_or(0),
));
out.extend_from_slice(digits);
} else if point >= count {
out.extend_from_slice(digits);
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(point - count).unwrap_or(0),
));
out.extend_from_slice(b".0");
} else {
let (whole, fraction) = digits.split_at(usize::try_from(point).unwrap_or(0));
out.extend_from_slice(whole);
out.push(b'.');
out.extend_from_slice(fraction);
}
}
#[cfg(test)]
mod tests {
use super::float_string;
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "a narrowing is what is wanted"
)]
fn single_of(f: f64) -> f32 {
f as f32
}
fn same_as_debug(f: f64) {
assert_eq!(
float_string(f),
format!("{f:?}"),
"bits {:#018x}",
f.to_bits()
);
}
#[test]
fn the_spellings_the_format_names() {
for (f, text) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(1.0, "1.0"),
(0.1, "0.1"),
(0.0001, "0.0001"),
(0.000_01, "1e-5"),
(1e-7, "1e-7"),
(123_456_789_012_345.0, "123456789012345.0"),
(1e16, "1e16"),
(5e-324, "5e-324"),
(f64::MAX, "1.7976931348623157e308"),
(-2.5e-5, "-2.5e-5"),
(1.5e300, "1.5e300"),
] {
assert_eq!(float_string(f), text);
same_as_debug(f);
}
}
#[test]
fn edges_of_the_thresholds_and_neighbours_of_every_power_of_ten() {
for exp in -330..=308 {
let power: f64 = format!("1e{exp}").parse().unwrap_or(1.0);
for bits in [
power.to_bits().wrapping_sub(1),
power.to_bits(),
power.to_bits() + 1,
] {
let f = f64::from_bits(bits);
if f.is_finite() {
same_as_debug(f);
same_as_debug(-f);
}
}
}
for exp in 0..1023 {
let f = 2.0_f64.powi(exp);
same_as_debug(f);
same_as_debug(f64::from_bits(f.to_bits() + 1));
}
}
#[test]
fn any_bit_pattern_spells_as_debug_does() {
let mut rng = Lcg(5);
for _ in 0..300_000 {
let f = f64::from_bits(rng.next());
if f.is_finite() {
same_as_debug(f);
}
}
}
#[test]
fn short_decimals_and_whole_numbers_spell_as_debug_does() {
let mut rng = Lcg(9);
for _ in 0..100_000 {
let n = f64::from(u32::try_from(rng.next() >> 33).unwrap_or(0));
for scale in [1.0, 10.0, 100.0, 1e3, 1e6, 1e10, 1e-3, 1e-6, 1e-9] {
same_as_debug(n / scale);
same_as_debug(n * scale);
}
}
}
#[test]
fn widened_singles_spell_as_debug_does() {
let mut rng = Lcg(3);
for _ in 0..200_000 {
let single = f32::from_bits(u32::try_from(rng.next() >> 32).unwrap_or(0));
if single.is_finite() {
same_as_debug(f64::from(single));
}
let unit = f64::from(u32::try_from(rng.next() >> 40).unwrap_or(0)) / 16_777_216.0;
same_as_debug(f64::from(single_of(unit * 100.0)));
same_as_debug(unit * 4096.0);
}
}
}