From 433a20928a44b167d17ec09df006dc246d49cef7 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Wed, 8 Apr 2026 22:27:37 -0700 Subject: [PATCH] refactor(format): improve number formatting and rounding Improve number formatting and add comprehensive tests: - Implemented `round_half_away` to provide more intuitive rounding (e.g., 2.5 -> 3, -2.5 -> -3). - Updated `format_f64` to use this rounding logic. - Added extensive unit tests for `parse_number_format` and `format_f64` , covering various edge cases and rounding behaviors. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL) --- src/format.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/src/format.rs b/src/format.rs index ed31509..b667df6 100644 --- a/src/format.rs +++ b/src/format.rs @@ -19,9 +19,16 @@ pub fn parse_number_format(fmt: &str) -> (bool, u8) { (comma, decimals) } +/// Round half away from zero (the "normal" rounding people expect). +fn round_half_away(n: f64, decimals: u8) -> f64 { + let factor = 10_f64.powi(decimals as i32); + (n * factor + n.signum() * 0.5).trunc() / factor +} + /// Format an f64 with optional comma grouping and decimal places. pub fn format_f64(n: f64, comma: bool, decimals: u8) -> String { - let formatted = format!("{:.prec$}", n, prec = decimals as usize); + let rounded = round_half_away(n, decimals); + let formatted = format!("{:.prec$}", rounded, prec = decimals as usize); if !comma { return formatted; } @@ -48,3 +55,174 @@ pub fn format_f64(n: f64, comma: bool, decimals: u8) -> String { } out } + +#[cfg(test)] +mod tests { + use super::*; + + // ── parse_number_format ──────────────────────────────────────────── + + #[test] + fn parse_comma_and_zero_decimals() { + assert_eq!(parse_number_format(",.0"), (true, 0)); + } + + #[test] + fn parse_comma_and_two_decimals() { + assert_eq!(parse_number_format(",.2"), (true, 2)); + } + + #[test] + fn parse_no_comma_two_decimals() { + assert_eq!(parse_number_format(".2"), (false, 2)); + } + + #[test] + fn parse_comma_only() { + assert_eq!(parse_number_format(","), (true, 0)); + } + + #[test] + fn parse_empty_string() { + assert_eq!(parse_number_format(""), (false, 0)); + } + + #[test] + fn parse_dot_no_digits_after() { + // "." has nothing after the dot — parse:: fails → default 0 + assert_eq!(parse_number_format("."), (false, 0)); + } + + #[test] + fn parse_multiple_dots_uses_last() { + // rfind picks the last dot + assert_eq!(parse_number_format(",.1.3"), (true, 3)); + } + + // ── format_f64 basic ─────────────────────────────────────────────── + + #[test] + fn format_no_comma_zero_decimals() { + assert_eq!(format_f64(1234.5, false, 0), "1235"); + } + + #[test] + fn format_no_comma_two_decimals() { + assert_eq!(format_f64(1234.5, false, 2), "1234.50"); + } + + #[test] + fn format_comma_zero_decimals() { + assert_eq!(format_f64(1234.0, true, 0), "1,234"); + } + + #[test] + fn format_comma_two_decimals() { + assert_eq!(format_f64(1234.56, true, 2), "1,234.56"); + } + + // ── comma placement boundaries ───────────────────────────────────── + + #[test] + fn format_comma_exactly_three_digits() { + assert_eq!(format_f64(999.0, true, 0), "999"); + } + + #[test] + fn format_comma_four_digits() { + assert_eq!(format_f64(1000.0, true, 0), "1,000"); + } + + #[test] + fn format_comma_seven_digits() { + assert_eq!(format_f64(1234567.0, true, 0), "1,234,567"); + } + + #[test] + fn format_comma_millions_with_decimals() { + assert_eq!(format_f64(1234567.89, true, 2), "1,234,567.89"); + } + + // ── negative numbers ─────────────────────────────────────────────── + + #[test] + fn format_negative_with_comma() { + assert_eq!(format_f64(-1234.0, true, 0), "-1,234"); + } + + #[test] + fn format_negative_with_comma_and_decimals() { + assert_eq!(format_f64(-1234567.89, true, 2), "-1,234,567.89"); + } + + #[test] + fn format_negative_no_comma() { + assert_eq!(format_f64(-42.5, false, 1), "-42.5"); + } + + // ── edge values ──────────────────────────────────────────────────── + + #[test] + fn format_zero() { + assert_eq!(format_f64(0.0, true, 2), "0.00"); + } + + #[test] + fn format_small_fraction() { + assert_eq!(format_f64(0.123, true, 2), "0.12"); + } + + #[test] + fn format_negative_small_fraction() { + assert_eq!(format_f64(-0.5, true, 1), "-0.5"); + } + + // ── rounding: half-away-from-zero ───────────────────────────────── + + #[test] + fn round_half_up_positive() { + // 2.5 → 3, not 2 (banker's would give 2) + assert_eq!(format_f64(2.5, false, 0), "3"); + } + + #[test] + fn round_half_down_negative() { + // -2.5 → -3, not -2 (away from zero) + assert_eq!(format_f64(-2.5, false, 0), "-3"); + } + + #[test] + fn round_half_at_one_decimal() { + // 1.25 → 1.3 + assert_eq!(format_f64(1.25, false, 1), "1.3"); + } + + #[test] + fn round_below_half_truncates() { + assert_eq!(format_f64(1.24, false, 1), "1.2"); + } + + #[test] + fn round_above_half_rounds_up() { + assert_eq!(format_f64(1.26, false, 1), "1.3"); + } + + // ── format_value dispatch ────────────────────────────────────────── + + #[test] + fn format_value_number() { + let v = CellValue::Number(1234.0); + assert_eq!(format_value(Some(&v), true, 0), "1,234"); + } + + #[test] + fn format_value_text() { + let v = CellValue::Text("hello".into()); + assert_eq!(format_value(Some(&v), true, 2), "hello"); + } + + #[test] + fn format_value_none() { + assert_eq!(format_value(None, true, 2), ""); + } +}