struct Spec {
precision: usize,
conv: Conv,
upper: bool,
}
enum Conv {
G,
E,
}
fn parse(fmt: &str) -> Option<Spec> {
let b = fmt.as_bytes();
if b.first() != Some(&b'%') {
return None;
}
let mut i = 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
let mut precision = 6;
if i < b.len() && b[i] == b'.' {
i += 1;
let start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
precision = fmt[start..i].parse().unwrap_or(0);
}
let (conv, upper) = match b.get(i) {
Some(&b'g') => (Conv::G, false),
Some(&b'G') => (Conv::G, true),
Some(&b'e') => (Conv::E, false),
Some(&b'E') => (Conv::E, true),
_ => return None,
};
if i + 1 != b.len() {
return None;
}
Some(Spec {
precision,
conv,
upper,
})
}
#[must_use]
pub fn format(fmt: &str, x: f64) -> String {
let spec = parse(fmt).unwrap_or(Spec {
precision: 17,
conv: Conv::G,
upper: false,
});
if x.is_nan() {
return if spec.upper { "NAN" } else { "nan" }.to_string();
}
if x.is_infinite() {
let word = if spec.upper { "INF" } else { "inf" };
return if x > 0.0 {
word.to_string()
} else {
format!("-{word}")
};
}
let out = match spec.conv {
Conv::G => fmt_g(x, spec.precision),
Conv::E => fmt_e(x, spec.precision),
};
if spec.upper {
out.to_uppercase()
} else {
out
}
}
fn fmt_e(x: f64, precision: usize) -> String {
if x == 0.0 {
let mantissa = if precision == 0 {
"0".to_string()
} else {
format!("0.{}", "0".repeat(precision))
};
let sign = if x.is_sign_negative() { "-" } else { "" };
return format!("{sign}{mantissa}e+00");
}
let neg = x < 0.0;
let (digits, exp) = round_digits(x.abs(), precision + 1);
let mut out = String::new();
if neg {
out.push('-');
}
out.push(digits.as_bytes()[0] as char);
if precision > 0 {
out.push('.');
out.push_str(&digits[1..=precision]);
}
out.push('e');
if exp >= 0 {
out.push('+');
} else {
out.push('-');
}
let e = exp.abs();
if e < 10 {
out.push('0');
}
out.push_str(&e.to_string());
out
}
fn fmt_g(x: f64, precision: usize) -> String {
let p = if precision == 0 { 1 } else { precision };
if x == 0.0 {
return if x.is_sign_negative() {
"-0".to_string()
} else {
"0".to_string()
};
}
let neg = x < 0.0;
let (digits, exp) = round_digits(x.abs(), p);
let mut out = String::new();
if neg {
out.push('-');
}
if exp < -4 || exp >= p as i32 {
let mut mant = String::new();
mant.push(digits.as_bytes()[0] as char);
let frac = strip_zeros(&digits[1..]);
if !frac.is_empty() {
mant.push('.');
mant.push_str(&frac);
}
out.push_str(&mant);
out.push('e');
if exp >= 0 {
out.push('+');
} else {
out.push('-');
}
let e = exp.abs();
if e < 10 {
out.push('0');
}
out.push_str(&e.to_string());
} else if exp >= 0 {
let int_len = (exp + 1) as usize;
if digits.len() <= int_len {
let mut s = digits.clone();
s.push_str(&"0".repeat(int_len - digits.len()));
out.push_str(&s);
} else {
let (int_part, frac_part) = digits.split_at(int_len);
let frac = strip_zeros(frac_part);
out.push_str(int_part);
if !frac.is_empty() {
out.push('.');
out.push_str(&frac);
}
}
} else {
let zeros = (-exp - 1) as usize;
let frac = strip_zeros(&format!("{}{}", "0".repeat(zeros), digits));
out.push_str("0.");
out.push_str(&frac);
}
out
}
fn strip_zeros(s: &str) -> String {
s.trim_end_matches('0').to_string()
}
fn round_digits(x: f64, sig: usize) -> (String, i32) {
let s = format!("{x:.*e}", sig - 1);
let (mant, exp_str) = s.split_once('e').expect("scientific form has e");
let exp: i32 = exp_str.parse().expect("exponent parses");
let mut digits: Vec<u8> = mant.bytes().filter(|b| b.is_ascii_digit()).collect();
while digits.len() < sig {
digits.push(b'0');
}
digits.truncate(sig);
(String::from_utf8(digits).expect("ascii digits"), exp)
}