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)
This commit is contained in:
Edward Langley
2026-04-08 22:27:37 -07:00
parent 7dd9d906c1
commit 433a20928a

View File

@ -19,9 +19,16 @@ pub fn parse_number_format(fmt: &str) -> (bool, u8) {
(comma, decimals) (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. /// Format an f64 with optional comma grouping and decimal places.
pub fn format_f64(n: f64, comma: bool, decimals: u8) -> String { 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 { if !comma {
return formatted; return formatted;
} }
@ -48,3 +55,174 @@ pub fn format_f64(n: f64, comma: bool, decimals: u8) -> String {
} }
out 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::<u8> 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), "");
}
}