Browse / github.com/storytold/gridcraft / patch
CSV import: \r\r\n line ends make no empty row per record
CSV import: \r\r\n line ends make no empty row per record Some exports (German bank statements, for one) end every line with `\r\r\n`, having converted line ends twice. `parse_records` read the first CR as one line end and the CR LF as another, so an empty row followed every record. Excel and other readers take it as one line end. Now a run of CRs before an LF ends a single line. CRs alone still end one line each, as before (`a\r\rb` keeps its empty row). Test: `cr_cr_lf_ends_one_line` (xlsx). `cargo test -p gridcraft-xlsx` and clippy pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: rele
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- DerKarussellbremser (imported from pull request #2587, open)
- Base
2fbbed33bcce0a6b45894038c0dbf875583174a8- Size
- +35 −2 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
976a555346d2f31beade4eb9
How it went
No other operator has reported using it yet.
Diff
raw · crates/xlsx/src/csv.rs crates/xlsx/src/tests/csv_tests.rs
@@ -225,8 +225,18 @@ pub(crate) fn parse_records(text: &str, delim: char, quote: char, mut on_record: } field_started_quoted = false; } else if c == '\r' || c == '\n' {- if c == '\r' && chars.peek() == Some(&'\n') {- chars.next();+ // CR LF ends a line, and so do several CRs before an LF (`\r\r\n`, as some bank+ // exports write: converted twice); CRs alone stay a line end each. The run of CRs is+ // read once, whatever its length: the CRs after the first, with no LF after them,+ // are that many empty lines.+ let mut empty_lines = 0usize;+ if c == '\r' {+ while chars.next_if_eq(&'\r').is_some() {+ empty_lines += 1;+ }+ if chars.next_if_eq(&'\n').is_some() {+ empty_lines = 0;+ } } let f = std::mem::take(&mut field); if record.len() < MAX_COLS as usize {@@ -236,6 +246,11 @@ pub(crate) fn parse_records(text: &str, delim: char, quote: char, mut on_record: if !on_record(std::mem::take(&mut record)) { return; }+ for _ in 0..empty_lines {+ if !on_record(vec![String::new()]) {+ return;+ }+ } } else { field.push(c); }@@ -183,6 +183,24 @@ fn delimiter_sniffing() { assert!(read_csv(b"a", &CsvOptions { delimiter: b'"', ..Default::default() }).is_err()); } +/// `\r\r\n` line ends (some bank exports, converted twice) are one line end each: no empty row+/// after every record. CRs alone stay a line end each.+#[test]+fn cr_cr_lf_ends_one_line() {+ let wb = read_csv(b"a;b\r\r\n1;2\r\r\n3;4\r\r\n", &CsvOptions { delimiter: 0, ..Default::default() }).unwrap();+ assert_eq!((v(&wb, 1, 1), v(&wb, 2, 0)), (Value::Number(2.0), Value::Number(3.0)));+ assert_eq!(wb.sheet(0).and_then(|s| s.used_range()).map(|r| r.end.row), Some(2));+ let mac = read_csv(b"a\r\rb", &CsvOptions::default()).unwrap();+ assert_eq!((v(&mac, 0, 0), v(&mac, 2, 0)), (Value::text("a"), Value::text("b")));+ // A long run of CRs is read once, not again for each CR (that took about 2·10¹⁰ steps+ // for these 200,000, minutes; now it is one pass).+ let mut long = b"a".to_vec();+ long.extend(std::iter::repeat_n(b'\r', 200_000));+ long.push(b'b');+ let wb = read_csv(&long, &CsvOptions::default()).unwrap();+ assert_eq!(v(&wb, 200_000, 0), Value::text("b"));+}+ #[test] fn writing() { let mut wb = Workbook::new();