feat: number formatting, real search navigation, CSV cross-product fix
- grid.rs: honour view.number_format (",.0" default, ",.2", ".4", etc.)
via parse_number_format/format_f64(n,comma,decimals); format_f64 now
pub so callers can reuse the same formatting logic.
- app.rs: n/N actually navigate to next/prev search match (cross-product
aware); fix dead unreachable N arm; add :set-format / :fmt command to
change the active view's number_format at runtime.
- persistence/mod.rs: CSV export now uses full cross-product of all
row/col-axis categories, matching grid rendering behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -61,6 +61,7 @@ impl<'a> GridWidget<'a> {
|
||||
let (sel_row, sel_col) = view.selected;
|
||||
let row_offset = view.row_offset;
|
||||
let col_offset = view.col_offset;
|
||||
let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format);
|
||||
|
||||
// Available cols
|
||||
let available_cols = ((area.width.saturating_sub(ROW_HEADER_WIDTH)) / COL_WIDTH) as usize;
|
||||
@ -133,7 +134,7 @@ impl<'a> GridWidget<'a> {
|
||||
let key = CellKey::new(coords);
|
||||
let value = self.model.evaluate(&key);
|
||||
|
||||
let cell_str = format_value(&value);
|
||||
let cell_str = format_value(&value, fmt_comma, fmt_decimals);
|
||||
let is_selected = abs_ri == sel_row && abs_ci == sel_col;
|
||||
let is_search_match = !self.search_query.is_empty()
|
||||
&& cell_str.to_lowercase().contains(&self.search_query.to_lowercase());
|
||||
@ -195,7 +196,7 @@ impl<'a> GridWidget<'a> {
|
||||
self.model.evaluate(&key).as_f64().unwrap_or(0.0)
|
||||
}).sum();
|
||||
|
||||
let total_str = format_f64(total);
|
||||
let total_str = format_f64(total, fmt_comma, fmt_decimals);
|
||||
buf.set_string(x, y,
|
||||
format!("{:>width$}", truncate(&total_str, COL_WIDTH as usize), width = COL_WIDTH as usize),
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
|
||||
@ -281,34 +282,46 @@ impl<'a> Widget for GridWidget<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_value(v: &CellValue) -> String {
|
||||
fn format_value(v: &CellValue, comma: bool, decimals: u8) -> String {
|
||||
match v {
|
||||
CellValue::Number(n) => format_f64(*n),
|
||||
CellValue::Number(n) => format_f64(*n, comma, decimals),
|
||||
CellValue::Text(s) => s.clone(),
|
||||
CellValue::Empty => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_f64(n: f64) -> String {
|
||||
if n == 0.0 {
|
||||
return "0".to_string();
|
||||
pub fn parse_number_format(fmt: &str) -> (bool, u8) {
|
||||
let comma = fmt.contains(',');
|
||||
let decimals = fmt.rfind('.')
|
||||
.and_then(|i| fmt[i + 1..].parse::<u8>().ok())
|
||||
.unwrap_or(0);
|
||||
(comma, decimals)
|
||||
}
|
||||
|
||||
pub fn format_f64(n: f64, comma: bool, decimals: u8) -> String {
|
||||
let formatted = format!("{:.prec$}", n, prec = decimals as usize);
|
||||
if !comma {
|
||||
return formatted;
|
||||
}
|
||||
if n.fract() == 0.0 && n.abs() < 1e12 {
|
||||
// Integer with comma formatting
|
||||
let i = n as i64;
|
||||
let s = i.to_string();
|
||||
let is_neg = s.starts_with('-');
|
||||
let digits = if is_neg { &s[1..] } else { &s[..] };
|
||||
let mut result = String::new();
|
||||
for (idx, c) in digits.chars().rev().enumerate() {
|
||||
if idx > 0 && idx % 3 == 0 { result.push(','); }
|
||||
result.push(c);
|
||||
}
|
||||
if is_neg { result.push('-'); }
|
||||
result.chars().rev().collect()
|
||||
// Split integer and decimal parts
|
||||
let (int_part, dec_part) = if let Some(dot) = formatted.find('.') {
|
||||
(&formatted[..dot], Some(&formatted[dot..]))
|
||||
} else {
|
||||
format!("{n:.2}")
|
||||
(&formatted[..], None)
|
||||
};
|
||||
let is_neg = int_part.starts_with('-');
|
||||
let digits = if is_neg { &int_part[1..] } else { int_part };
|
||||
let mut result = String::new();
|
||||
for (idx, c) in digits.chars().rev().enumerate() {
|
||||
if idx > 0 && idx % 3 == 0 { result.push(','); }
|
||||
result.push(c);
|
||||
}
|
||||
if is_neg { result.push('-'); }
|
||||
let mut out: String = result.chars().rev().collect();
|
||||
if let Some(dec) = dec_part {
|
||||
out.push_str(dec);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn truncate(s: &str, max_width: usize) -> String {
|
||||
|
||||
Reference in New Issue
Block a user