use crate::errors::GraphDDBError;
pub fn py_repr(f: f64) -> Result<String, GraphDDBError> {
if f.is_nan() || f.is_infinite() {
return Err(GraphDDBError::new("Cannot format non-finite number"));
}
if f == 0.0 {
return Ok(if f.is_sign_negative() {
"-0.0".to_string()
} else {
"0.0".to_string()
});
}
let (neg, digits, decpt) = shortest_digits(f);
let n = digits.len() as i32;
let out = if decpt <= -4 || decpt > 16 {
let mut mant = String::new();
mant.push(digits.as_bytes()[0] as char);
if n > 1 {
mant.push('.');
mant.push_str(&digits[1..]);
}
let e = decpt - 1;
let esign = if e < 0 { '-' } else { '+' };
let mut eabs = e.abs().to_string();
if eabs.len() < 2 {
eabs = format!("0{eabs}");
}
format!("{mant}e{esign}{eabs}")
} else if decpt <= 0 {
format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
} else if decpt >= n {
format!("{}{}.0", digits, "0".repeat((decpt - n) as usize))
} else {
format!(
"{}.{}",
&digits[..decpt as usize],
&digits[decpt as usize..]
)
};
Ok(if neg { format!("-{out}") } else { out })
}
fn shortest_digits(f: f64) -> (bool, String, i32) {
let mut chosen: Option<String> = None;
for p in 0..=17usize {
let s = format!("{:.*e}", p, f);
if s.parse::<f64>() == Ok(f) {
chosen = Some(s);
break;
}
}
let s = chosen.unwrap_or_else(|| format!("{:.17e}", f));
let mut bytes = s.as_str();
let neg = bytes.starts_with('-');
if neg {
bytes = &bytes[1..];
}
let (mantissa, exp_part) = match bytes.split_once('e') {
Some((m, e)) => (m, e),
None => (bytes, "0"),
};
let exp: i32 = exp_part
.parse()
.expect("f64 {:e} exponent is always an integer");
let (int_part, frac_part) = match mantissa.split_once('.') {
Some((i, f)) => (i, f),
None => (mantissa, ""),
};
let mut digits: String = format!("{int_part}{frac_part}");
let mut decpt = int_part.len() as i32 + exp;
let lead = digits.len() - digits.trim_start_matches('0').len();
if lead == digits.len() {
return (neg, "0".to_string(), 1);
}
digits = digits[lead..].to_string();
decpt -= lead as i32;
let trimmed = digits.trim_end_matches('0');
let digits = if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
};
(neg, digits, decpt)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::excessive_precision)] fn matches_python_json_dumps_float() {
let cases: &[(f64, &str)] = &[
(0.0, "0.0"),
(-0.0, "-0.0"),
(1e20, "1e+20"),
(2.6755e-9, "2.6755e-09"),
(9999999999999999.5, "1e+16"),
(3.0, "3.0"),
(-5.0, "-5.0"),
(1.5, "1.5"),
(1e-5, "1e-05"),
(1e-4, "0.0001"),
(1e16, "1e+16"),
(1e15, "1000000000000000.0"),
(123456789012345.678, "123456789012345.67"),
(1e-9, "1e-09"),
(-1e-9, "-1e-09"),
(2.5e-10, "2.5e-10"),
];
for (f, expected) in cases {
assert_eq!(py_repr(*f).unwrap(), *expected, "py_repr({f})");
}
}
#[test]
fn signed_zero_distinguished_without_div() {
assert_eq!(py_repr(0.0).unwrap(), "0.0");
assert_eq!(py_repr(-0.0).unwrap(), "-0.0");
}
#[test]
fn rejects_non_finite() {
assert!(py_repr(f64::NAN).is_err());
assert!(py_repr(f64::INFINITY).is_err());
assert!(py_repr(f64::NEG_INFINITY).is_err());
}
#[test]
#[allow(clippy::excessive_precision)] fn fuzz_against_python3() {
use std::process::Command;
let mut inputs: Vec<f64> = vec![
0.0,
-0.0,
1.0,
-1.0,
f64::MIN_POSITIVE, f64::from_bits(1), f64::from_bits(0x000f_ffff_ffff_ffff), 1e-5,
1e-4,
9.999e-5,
1e15,
1e16,
9.999_999_999_999_999e15,
1e20,
2.6755e-9,
5e-324,
1.7976931348623157e308, ];
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
for _ in 0..5000 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = state ^ (state >> 29);
let f = f64::from_bits(bits);
if f.is_finite() {
inputs.push(f);
}
}
let script = r#"
import sys, json
for line in sys.stdin:
line = line.strip()
if not line:
continue
f = float.fromhex(line)
print(json.dumps(f))
"#;
let stdin_data: String = inputs
.iter()
.map(|f| format!("{}\n", hex_float(*f)))
.collect();
let out = Command::new("python3")
.arg("-c")
.arg(script)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(stdin_data.as_bytes())?;
child.wait_with_output()
});
let out = match out {
Ok(o) if o.status.success() => o,
_ => {
eprintln!("fuzz_against_python3: python3 unavailable — skipping");
return;
}
};
let expected: Vec<&str> = std::str::from_utf8(&out.stdout).unwrap().lines().collect();
assert_eq!(
expected.len(),
inputs.len(),
"python3 produced a different count"
);
for (f, exp) in inputs.iter().zip(expected) {
assert_eq!(
py_repr(*f).unwrap(),
exp,
"py_repr mismatch for {:#018x} ({})",
f.to_bits(),
hex_float(*f)
);
}
}
fn hex_float(f: f64) -> String {
if f == 0.0 {
return if f.is_sign_negative() {
"-0x0p+0".into()
} else {
"0x0p+0".into()
};
}
let bits = f.to_bits();
let sign = if bits >> 63 == 1 { "-" } else { "" };
let exp = ((bits >> 52) & 0x7ff) as i64;
let mantissa = bits & 0x000f_ffff_ffff_ffff;
if exp == 0 {
format!("{sign}0x0.{:013x}p-1022", mantissa)
} else {
format!("{sign}0x1.{:013x}p{:+}", mantissa, exp - 1023)
}
}
}