improvise-711n: a trailing conversion letter no longer erases the precision
`parse_number_format` read the precision as `fmt[i+1..].parse::<u8>()` over
everything after the last `.`, so `,.2f` — the printf spelling the repo-map's
own `.improv` example carries — failed to parse and fell through to zero
decimals, silently, in the TUI, the browser projection and CSV export.
The precision is now the leading digit run after the last `.`, with any
conversion letter ignored. The spellings that are legal are stated once, in
`context/code/core.md`, along with why the trailing `f` is accepted rather than
refused (the grammar takes the format line as an opaque `rest_of_line`, so
refusing it rejects documents this repo already writes) and why the reading is
printf's rather than Excel's (`.00` is the spelling the two disagree on).
`export_csv_quotes_commas_per_rfc4180` asserted the value `starts_with("1,200")`
against a `,.2f` fixture, so it could not tell two decimals from none. It is now
an exact equality, and reverting the fix fails it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
@@ -10,14 +10,17 @@ pub fn format_value(v: Option<&CellValue>, comma: bool, decimals: u8) -> String
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a number format string like ",.0" into (use_commas, decimal_places).
|
||||
/// The digits a precision is named by: everything up to the first non-digit.
|
||||
fn leading_digits(s: &str) -> &str {
|
||||
&s[..s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len())]
|
||||
}
|
||||
|
||||
/// Parse a printf-style number format like ",.2f" into (use_commas, decimals).
|
||||
/// Which spellings are legal: `context/code/core.md`, "The number format".
|
||||
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)
|
||||
let precision = fmt.rsplit_once('.').map_or("", |(_, tail)| leading_digits(tail));
|
||||
(comma, precision.parse::<u8>().unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Round half away from zero (the "normal" rounding people expect).
|
||||
@@ -100,6 +103,38 @@ mod tests {
|
||||
assert_eq!(parse_number_format(",.1.3"), (true, 3));
|
||||
}
|
||||
|
||||
/// Bug improvise-711n: a trailing conversion letter made the whole suffix
|
||||
/// unparseable. Evidence: `context/test-evidence/core.md`.
|
||||
#[test]
|
||||
fn a_trailing_conversion_letter_does_not_erase_the_precision() {
|
||||
assert_eq!(parse_number_format(",.2f"), (true, 2));
|
||||
assert_eq!(parse_number_format(".2f"), (false, 2));
|
||||
assert_eq!(parse_number_format("%.2f"), (false, 2));
|
||||
// The precision is the LEADING digit run, not the tail with its
|
||||
// non-digits trimmed off — these two readings differ here.
|
||||
assert_eq!(parse_number_format(".2f3"), (false, 2));
|
||||
}
|
||||
|
||||
/// One convention, not two: `.00` is printf's "precision zero", never
|
||||
/// Excel's "two places". Evidence: `context/test-evidence/core.md`.
|
||||
#[test]
|
||||
fn a_zero_padded_picture_string_is_read_as_printf_not_as_excel() {
|
||||
assert_eq!(parse_number_format("0.00"), (false, 0));
|
||||
assert_eq!(parse_number_format(",.0f"), (true, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_precision_too_large_to_name_reads_as_none() {
|
||||
assert_eq!(parse_number_format(".300"), (false, 0));
|
||||
assert_eq!(parse_number_format(",.999f"), (true, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_format_naming_no_precision_at_all_reads_as_zero() {
|
||||
assert_eq!(parse_number_format(",f"), (true, 0));
|
||||
assert_eq!(parse_number_format(".f"), (false, 0));
|
||||
}
|
||||
|
||||
// ── format_f64 basic ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -139,11 +139,9 @@ mod tests {
|
||||
assert_eq!(r.len(), 2, "comma split a field: {r:?}");
|
||||
}
|
||||
assert_eq!(&records[1][0], "Smith, Jr.");
|
||||
assert!(
|
||||
records[1][1].starts_with("1,200"),
|
||||
"expected comma-grouped value, got: {:?}",
|
||||
&records[1][1]
|
||||
);
|
||||
// Exact, not `starts_with`: the fixture asks for `.2f`, and a prefix
|
||||
// check cannot tell two decimals from none (improvise-711n).
|
||||
assert_eq!(&records[1][1], "1,200.00");
|
||||
}
|
||||
|
||||
/// Bug improvise-iot6: `render_csv` built its layout with `GridLayout::new`,
|
||||
|
||||
Reference in New Issue
Block a user