use crate::Cell;
pub trait CellFormatter: Send + Sync {
fn format(&self, cell: &Cell) -> String;
}
pub struct DefaultFormatter;
impl CellFormatter for DefaultFormatter {
fn format(&self, cell: &Cell) -> String {
cell.to_string()
}
}
pub struct NumberFormatter {
pub decimal_places: usize,
pub thousands_separator: bool,
}
impl NumberFormatter {
fn format_integer(n: i64, thousands: bool) -> String {
let s = n.unsigned_abs().to_string();
let with_sep = if thousands {
insert_thousands(s)
} else {
n.unsigned_abs().to_string()
};
if n < 0 {
format!("-{with_sep}")
} else {
with_sep
}
}
fn format_float(&self, v: f64) -> String {
let formatted = format!("{v:.prec$}", prec = self.decimal_places);
if !self.thousands_separator {
return formatted;
}
match formatted.split_once('.') {
Some((int_part, dec_part)) => {
let neg = int_part.starts_with('-');
let digits = if neg { &int_part[1..] } else { int_part };
let int_sep = insert_thousands(digits.to_owned());
let sign = if neg { "-" } else { "" };
format!("{sign}{int_sep}.{dec_part}")
}
None => insert_thousands(formatted),
}
}
}
fn insert_thousands(s: String) -> String {
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let mut out = String::with_capacity(len + len / 3);
for (i, c) in chars.iter().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
out.push(',');
}
out.push(*c);
}
out
}
impl CellFormatter for NumberFormatter {
fn format(&self, cell: &Cell) -> String {
match cell {
Cell::Int(n) => {
if self.decimal_places == 0 {
Self::format_integer(*n, self.thousands_separator)
} else {
self.format_float(*n as f64)
}
}
Cell::Float(v) => self.format_float(*v),
other => DefaultFormatter.format(other),
}
}
}
pub struct DateFormatter {
pub fmt: String,
}
impl CellFormatter for DateFormatter {
fn format(&self, cell: &Cell) -> String {
cell.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_formatter_strings() {
let f = DefaultFormatter;
assert_eq!(f.format(&Cell::Text("hi".into())), "hi");
assert_eq!(f.format(&Cell::Int(42)), "42");
assert_eq!(f.format(&Cell::Empty), "");
}
#[test]
fn number_formatter_decimal() {
let f = NumberFormatter {
decimal_places: 2,
thousands_separator: false,
};
assert_eq!(f.format(&Cell::Float(1.5)), "1.50");
assert_eq!(f.format(&Cell::Float(9.876_54)), "9.88");
}
#[test]
fn number_formatter_thousands() {
let f = NumberFormatter {
decimal_places: 0,
thousands_separator: true,
};
assert_eq!(f.format(&Cell::Int(1_000_000)), "1,000,000");
assert_eq!(f.format(&Cell::Int(1_234)), "1,234");
assert_eq!(f.format(&Cell::Int(999)), "999");
}
#[test]
fn number_formatter_negative() {
let f = NumberFormatter {
decimal_places: 0,
thousands_separator: true,
};
assert_eq!(f.format(&Cell::Int(-1_000)), "-1,000");
}
#[test]
fn number_formatter_float_thousands() {
let f = NumberFormatter {
decimal_places: 2,
thousands_separator: true,
};
assert_eq!(f.format(&Cell::Float(1234567.891)), "1,234,567.89");
}
#[test]
fn date_formatter_passthrough() {
let f = DateFormatter {
fmt: "%Y-%m-%d".into(),
};
assert_eq!(f.format(&Cell::Text("2026-05-29".into())), "2026-05-29");
}
}