import: spec-blind suite (40 tests); POSIX two-digit years; exact sub-cent text
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
tests/spec_import.rs, written from the import crate's specs and sources
alone: every statement foots as printed on random books (P&L, balance
sheet with and without members, cash flow, trial balance, members'
equity), format_amount round-trips, bank CSV round-trips in both layouts
with date-order scenarios, IIF transactions sum to zero with every
account declared and ASCII fields, XLSX cells read back through a small
zip/DEFLATE reader, PDF labels with Typst markup render as text and the
bytes are deterministic, chart CSV and templates.
Its two findings, both fixed:
- "1/15/69" parsed as 2069: chrono's %y cuts over at 70, POSIX strptime
at 69 (69-99 are 1900s). Two-digit years are computed by POSIX now.
- An amount finer than a cent went into an XLSX as rounded text ("1.01"
for 1.005); it is written as its exact text now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
9f81fc0817
commit
11e33edc60
3 files changed
+2391
-9
No files matched your search
@@ -195,15 +195,21 @@ fn parse_date(s: &str, order: DateOrder) -> Result<NaiveDate, BankCsvError> {
|
|||||||
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
||||||
return Ok(d);
|
return Ok(d);
|
||||||
}
|
}
|
||||||
let (four, two) = match order {
|
let four = match order {
|
||||||
DateOrder::MonthFirst => ("%m/%d/%Y", "%m/%d/%y"),
|
DateOrder::MonthFirst => "%m/%d/%Y",
|
||||||
DateOrder::DayFirst => ("%d/%m/%Y", "%d/%m/%y"),
|
DateOrder::DayFirst => "%d/%m/%Y",
|
||||||
};
|
};
|
||||||
// A four-digit year first; a two-digit year follows POSIX (69-99 are
|
let bad = || BankCsvError::BadDate(s.to_string());
|
||||||
// 1900s), as chrono's %y does.
|
let (rest, year) = s.rsplit_once('/').ok_or_else(bad)?;
|
||||||
let year_digits = s.rsplit('/').next().map_or(0, str::len);
|
if year.len() != 2 {
|
||||||
let format = if year_digits == 2 { two } else { four };
|
return NaiveDate::parse_from_str(s, four).map_err(|_| bad());
|
||||||
NaiveDate::parse_from_str(s, format).map_err(|_| BankCsvError::BadDate(s.to_string()))
|
}
|
||||||
|
// A two-digit year follows POSIX strptime %y: 69-99 are 1900s, 00-68
|
||||||
|
// 2000s. (chrono's %y puts the cutoff at 70; found by the spec-blind
|
||||||
|
// suite, 2026-10-13.)
|
||||||
|
let yy: i32 = year.parse().map_err(|_| bad())?;
|
||||||
|
let full = if yy >= 69 { 1900 + yy } else { 2000 + yy };
|
||||||
|
NaiveDate::parse_from_str(&format!("{rest}/{full}"), four).map_err(|_| bad())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_amount(s: &str) -> Result<Decimal, BankCsvError> {
|
fn parse_amount(s: &str) -> Result<Decimal, BankCsvError> {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ fn number(
|
|||||||
None => sheet.write_string_with_format(
|
None => sheet.write_string_with_format(
|
||||||
row,
|
row,
|
||||||
col,
|
col,
|
||||||
crate::report::format_amount(d),
|
exact_text(d),
|
||||||
&fmt.clone().set_align(rust_xlsxwriter::FormatAlign::Right),
|
&fmt.clone().set_align(rust_xlsxwriter::FormatAlign::Right),
|
||||||
)?,
|
)?,
|
||||||
};
|
};
|
||||||
@@ -395,6 +395,26 @@ fn general_journal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decimal to f64 at the presentation boundary. Rounded to cents first.
|
/// Decimal to f64 at the presentation boundary. Rounded to cents first.
|
||||||
|
/// An amount as exact text: thousands grouped, every decimal it has (at
|
||||||
|
/// least two), a negative in parentheses. Never rounded.
|
||||||
|
fn exact_text(d: Decimal) -> String {
|
||||||
|
let places = d.normalize().scale().max(2) as usize;
|
||||||
|
let fixed = format!("{:.*}", places, d.abs());
|
||||||
|
let (int, frac) = fixed.split_once('.').expect("decimals");
|
||||||
|
let mut grouped = String::new();
|
||||||
|
for (i, c) in int.chars().enumerate() {
|
||||||
|
if i > 0 && (int.len() - i) % 3 == 0 {
|
||||||
|
grouped.push(',');
|
||||||
|
}
|
||||||
|
grouped.push(c);
|
||||||
|
}
|
||||||
|
if d.is_sign_negative() && !d.is_zero() {
|
||||||
|
format!("({grouped}.{frac})")
|
||||||
|
} else {
|
||||||
|
format!("{grouped}.{frac}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The double a cell holds for `d`, when Excel will show it exactly: whole
|
/// The double a cell holds for `d`, when Excel will show it exactly: whole
|
||||||
/// cents, at most 15 significant digits. None otherwise.
|
/// cents, at most 15 significant digits. None otherwise.
|
||||||
fn exact_f64(d: Decimal) -> Option<f64> {
|
fn exact_f64(d: Decimal) -> Option<f64> {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user