pub mod json;
pub mod text;
pub use json::{CamelCase, JsonOptions, write_json};
pub use text::{GeoFormat, TextOptions, write_text};
use crate::engine::format::timestamp::days_to_ymd;
pub(crate) fn precision_for(resolution: f64) -> usize {
if !resolution.is_finite() || resolution <= 0.0 {
return 0;
}
let mut p = 0usize;
let mut r = resolution;
while r > 0.0 && r < 1.0 {
p += 1;
r *= 10.0;
}
if (r - r.round()).abs() <= 1e-9 * r {
p
} else {
p + 2
}
}
pub(crate) fn time_precision_for(resolution: Option<f64>) -> usize {
let resolution = resolution.unwrap_or(1.0);
if !resolution.is_finite() || resolution <= 0.0 {
return 0;
}
let mut p = 0usize;
let mut r = resolution;
while r > 0.0 && r < 1.0 {
p += 1;
r *= 10.0;
}
p
}
pub(crate) fn effective_precision(precision: u8, resolution: Option<f64>) -> usize {
if precision > 0 {
precision as usize
} else {
precision_for(resolution.unwrap_or(1.0))
}
}
pub(crate) fn write_fixed_float<W: std::fmt::Write>(
w: &mut W,
v: f64,
precision: usize,
min_width: usize,
) -> std::fmt::Result {
const EXACT: f64 = 9_007_199_254_740_992.0; if !v.is_finite() || v.abs() >= 1e12 || v.abs() * 10f64.powi(precision as i32) >= EXACT {
return write!(
w,
"{:>width$.prec$}",
v,
width = min_width,
prec = precision
);
}
let neg = v.is_sign_negative();
fn lands_on_a_tie(v: f64, precision: usize) -> bool {
let scaled = v * 10f64.powi(precision as i32);
scaled.fract().abs() == 0.5
}
if lands_on_a_tie(v, precision) {
return write!(
w,
"{:>width$.prec$}",
v,
width = min_width,
prec = precision
);
}
if precision == 0 {
let rounded = v.round_ties_even() as i64;
let mut buf = itoa::Buffer::new();
let s = buf.format(rounded.unsigned_abs());
let signed_len = (if neg && rounded != 0 { 1 } else { 0 }) + s.len();
for _ in 0..min_width.saturating_sub(signed_len) {
w.write_char(' ')?;
}
if neg && rounded != 0 {
w.write_char('-')?;
}
return w.write_str(s);
}
let scale = 10f64.powi(precision as i32);
let scaled = v * scale;
let rounded = scaled.round_ties_even() as i128;
let abs = rounded.unsigned_abs();
let pow = 10u128.pow(precision as u32);
let int_part = (abs / pow) as u64;
let frac_part = (abs % pow) as u64;
let mut int_buf = itoa::Buffer::new();
let int_str = int_buf.format(int_part);
let mut frac_buf = itoa::Buffer::new();
let frac_str = frac_buf.format(frac_part);
let total_len = (if neg { 1 } else { 0 }) + int_str.len() + 1 + precision;
for _ in 0..min_width.saturating_sub(total_len) {
w.write_char(' ')?;
}
if neg {
w.write_char('-')?;
}
w.write_str(int_str)?;
w.write_char('.')?;
for _ in 0..precision.saturating_sub(frac_str.len()) {
w.write_char('0')?;
}
w.write_str(frac_str)
}
pub(crate) fn write_c_g<W: std::fmt::Write>(w: &mut W, v: f64) -> std::fmt::Result {
const PRECISION: i32 = 6;
if v.is_nan() {
return w.write_str("nan");
}
if v.is_infinite() {
return w.write_str(if v > 0.0 { "inf" } else { "-inf" });
}
let sci = format!("{:.*e}", (PRECISION - 1) as usize, v);
let (mantissa, exp_str) = sci.split_once('e').expect("{:e} always emits an 'e'");
let exp: i32 = exp_str
.parse()
.expect("{:e} always emits an integer exponent");
fn trim(s: &str) -> &str {
match s.split_once('.') {
Some(_) => s.trim_end_matches('0').trim_end_matches('.'),
None => s,
}
}
#[allow(clippy::manual_range_contains)]
let scientific = exp < -4 || exp >= PRECISION;
if scientific {
w.write_str(trim(mantissa))?;
write!(w, "e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
} else {
let decimals = (PRECISION - 1 - exp).max(0) as usize;
w.write_str(trim(&format!("{v:.decimals$}")))
}
}
pub fn format_date(days: u16, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
let (y, m, d) = days_to_ymd(days as i64);
write!(w, "{:04}.{:02}.{:02}", y, m, d)
}
pub fn parse_time(s: &str) -> Option<f64> {
let s = s.trim();
if let Ok(seconds) = s.parse::<f64>() {
return seconds.is_finite().then_some(seconds);
}
let (sign, clock) = match s.strip_prefix('-') {
Some(rest) => (-1.0, rest),
None => (1.0, s),
};
let part = |x: &str| {
x.parse::<f64>()
.ok()
.filter(|v| v.is_finite() && !x.starts_with(['-', '+']))
};
let mut it = clock.split(':');
let h = part(it.next()?)?;
let m = part(it.next()?)?;
let sec = match it.next() {
Some(x) => part(x)?,
None => 0.0,
};
if it.next().is_some() {
return None;
}
let total = sign * (h * 3600.0 + m * 60.0 + sec);
total.is_finite().then_some(total)
}
pub fn format_time(
seconds: f64,
precision: usize,
trim_zero_fraction: bool,
w: &mut dyn std::fmt::Write,
) -> std::fmt::Result {
if !seconds.is_finite() {
return w.write_str("00:00:00");
}
let neg = seconds < 0.0;
let abs = seconds.abs();
let sign = if neg { "-" } else { "" };
let whole = abs.trunc() as u64;
let h = whole / 3600;
let m = (whole / 60) % 60;
let s = whole % 60;
if precision == 0 {
return write!(w, "{sign}{:02}:{:02}:{:02}", h, m, s);
}
let frac = (abs - whole as f64) * 10f64.powi(precision as i32);
let frac_rounded = frac.round() as u64;
if trim_zero_fraction && frac_rounded == 0 {
write!(w, "{sign}{:02}:{:02}:{:02}", h, m, s)
} else {
write!(
w,
"{sign}{:02}:{:02}:{:02}.{:0width$}",
h,
m,
s,
frac_rounded,
width = precision
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn precision_matches_canboat() {
assert_eq!(precision_for(1.0), 0);
assert_eq!(precision_for(0.1), 1);
assert_eq!(precision_for(0.01), 2);
assert_eq!(precision_for(0.001), 3);
assert_eq!(precision_for(0.0001), 4);
assert_eq!(precision_for(10.0), 0);
assert_eq!(precision_for(0.0), 0);
assert_eq!(precision_for(-1.0), 0);
assert_eq!(precision_for(0.004), 3);
assert_eq!(precision_for(0.05), 2);
assert_eq!(precision_for(180.0), 0);
assert_eq!(precision_for(0.03125), 4);
assert_eq!(precision_for(0.25 / 60.0), 5);
assert_eq!(precision_for(0.1 / 3.6e6), 10);
assert_eq!(precision_for(2f64.powi(-38)), 14);
assert_eq!(precision_for(2f64.powi(-38) * std::f64::consts::PI), 13);
}
#[test]
fn date_round_trips_known_dates() {
assert_eq!(days_to_ymd(0), (1970, 1, 1));
assert_eq!(days_to_ymd(1), (1970, 1, 2));
assert_eq!(days_to_ymd(19245), (2022, 9, 10));
assert_eq!(days_to_ymd(19782), (2024, 2, 29));
}
#[test]
fn time_formats() {
let mut out = String::new();
format_time(3661.0, 0, false, &mut out).unwrap();
assert_eq!(out, "01:01:01");
let mut out = String::new();
format_time(3661.5, 3, false, &mut out).unwrap();
assert_eq!(out, "01:01:01.500");
}
#[test]
fn many_decimals_match_printf() {
let fixed = |v: f64, p: usize| {
let mut out = String::new();
write_fixed_float(&mut out, v, p, 0).unwrap();
out
};
assert_eq!(fixed(-3.1415926535e-7, 11), "-0.00000031416");
assert_eq!(fixed(-1.0000076e-7, 12), "-0.000000100001");
assert_eq!(fixed(123.456789012345, 12), "123.456789012345");
}
}
#[cfg(test)]
mod c_g_tests {
use super::write_c_g;
fn g(v: f32) -> String {
let mut s = String::new();
write_c_g(&mut s, v as f64).unwrap();
s
}
#[test]
fn matches_c_printf_g() {
let cases: &[(f32, &str)] = &[
(2.727_398_9, "2.7274"), (0.0, "0"),
(-0.0, "-0"),
(1.0, "1"),
(10.0, "10"),
(100_000.0, "100000"), (1_000_000.0, "1e+06"), (1_234_567.0, "1.23457e+06"),
(0.0001, "0.0001"), (0.00001, "1e-05"), (9.999_999, "10"), (-core::f32::consts::PI, "-3.14159"),
(123_456.0, "123456"),
(0.5, "0.5"),
(1e-10, "1e-10"),
(1e20, "1e+20"),
(65535.0, "65535"),
(2.5e-5, "2.5e-05"),
(999_999.5, "1e+06"),
(1e6, "1e+06"),
(0.000_123_456, "0.000123456"),
];
for (v, want) in cases {
assert_eq!(&g(*v), want, "%g of {v}");
}
}
#[test]
fn rounds_a_near_tie_the_way_printf_does() {
use super::write_fixed_float;
fn f(v: f64, p: usize) -> String {
let mut s = String::new();
write_fixed_float(&mut s, v, p, 0).unwrap();
s
}
assert_eq!(f(5.0 * 0.0001, 3), "0.001");
assert_eq!(f(30976.0 / 16384.0, 5), "1.89062");
assert_eq!(f(0.125, 2), "0.12");
assert_eq!(f(0.375, 2), "0.38");
}
#[test]
fn handles_non_finite() {
let mut s = String::new();
write_c_g(&mut s, f64::NAN).unwrap();
assert_eq!(s, "nan");
s.clear();
write_c_g(&mut s, f64::INFINITY).unwrap();
assert_eq!(s, "inf");
s.clear();
write_c_g(&mut s, f64::NEG_INFINITY).unwrap();
assert_eq!(s, "-inf");
}
}