Skip to content

CSV import: From Text/CSV, sep=, apostrophes, quotes in Text to Columns and exponents match Excel - #965

Closed
iluebbe wants to merge 7 commits into
storytold:mainfrom
iluebbe:fix/csv-import-excel
Closed

iluebbe wants to merge 7 commits into
storytold:mainfrom
iluebbe:fix/csv-import-excel

Conversation

@iluebbe

@iluebbe iluebbe commented Oct 13, 2026

Copy link
Copy Markdown

Problem

I tested CSV and text import against Excel 365 on Windows (de-DE UI). The test covered File › Open, Data › From Text/CSV (data.fromTextCsv), paste with Text to Columns, and NUMBERVALUE / TEXTSPLIT. It used about 60 variant files: delimiters , ; TAB |, quoting, line ends, BOMs, en-US / de-DE / fr / de-CH / en-IN / zh-CN numbers and dates, and Windows-1252 / UTF-16 / GB18030. Nine bugs came up. Main 84acf62 is the baseline; I found no existing issue or PR for any of them.

# Path Input Excel 365 GridCraft main
1 From Text/CSV ;-separated file (r2;1.234.567,89, 12,5 %) splits on ; (File › Open in GridCraft does too) detects only tab or comma: one column per line, and the decimal commas split numbers (1.234.567,89 → "r2;1.234.567" and 89, 12,5 % → 0.05)
2 From Text/CSV =1+1, =HYPERLINK("http://example.com","click") text (Power Query; GridCraft's File › Open keeps text too) live formulas: 2 and a working hyperlink; an unquoted comma inside the formula splits it
3 From Text/CSV 2024-03-04, 12.5%, $1,234.50 date, percent and currency formats 45355, 0.125, 1234.5 (General)
4 From Text/CSV a CSV starting with [ID],Name CSV refused: "invalid parameters for data.getData: expected value at line 1 column 2" (read as JSON)
5 all import paths 's-Hertogenbosch, 't Hooft, '007 the apostrophe is kept stripped (s-Hertogenbosch, 007)
6 Text to Columns Pipe 12" long,5,EUR; 5'10" tall,180,cm Pipe 12" long | 5 | EUR; 5'10" tall | 180 | cm one cell Pipe 12 long,5,EUR; one cell 5'10 tall,180,cm (a quote anywhere toggles quoting)
7 File › Open, From Text/CSV first line sep=| (or sep=;, sep=,) splits on that character and hides the line shows a sep= row; a pipe file stays one column
8 typing and all import paths 1,5E3, -1,5E3, 12,3e4, 1,2e3 text 15000, -15000, 1230000, 12000 (a German 1,5E3, meaning 1500, becomes 15000)
8 1,000E3, 1,234E2 1000000, 123400 text
9 From Text/CSV delimiter: ";" (U+FF1B) – cast as u8, so it never splits; a character whose low byte is 0x2C would split at commas

Cause

  • From Text/CSV (crates/engine/src/cmd/extra.rs):
    • It picked tab or comma from the first line instead of File › Open's sniff_delimiter.
    • It truncated the delimiter as u8.
    • It treated anything starting with [ as JSON.
    • It wrote every value back through range.setValues, which parses each field as typed input, so formulas went live and number formats were lost.
  • Apostrophe: CSV import and Text to Columns used parse_input (crates/core/src/parse.rs), where a leading ' is the typed-text prefix.
  • Text to Columns quoting: split_delimited (crates/engine/src/cmd/data.rs) was a third tokenizer, next to csv.rs (File › Open) and parse_tsv (paste). It toggled quoting on any " and dropped the quote.
  • Exponent: parse_plain checked digit groups on the whole number including the exponent, so 5E3 counted as a group of three.

Fix

  • One tokenizer: CSV open, paste (parse_tsv) and Text to Columns now share one tokenizer in crates/xlsx/src/csv.rs (split_records / split_fields with a Delimited options struct). The copies in data.rs and edit.rs are gone.
  • From Text/CSV:
    • Without a delimiter it detects the delimiter as File › Open does.
    • Both honour a sep=X first line (when the delimiter is detected) and hide it.
    • CsvOptions.delimiter is a char ('\0' means detect), so any character works; a delimiter param longer than one character is refused.
  • From Text/CSV writes the cells read_csv produced, values with their number formats, never through typing. JSON strings are read like CSV fields. Both use the workbook's date system.
  • JSON only when it is JSON: text is treated as JSON only when it parses as a JSON array of objects, or when the file is .json.
  • Apostrophe: a new parse_imported in core keeps a leading apostrophe as text. Typing 'abc still forces text.
  • Exponent: parse_plain checks thousands groups on the mantissa only.
  • docs/file-format-parity.md: CSV rows and a revision row.

Not changed here:

Testing

  • New tests:

    • core: thousands_separators_belong_to_the_mantissa, plus apostrophe cases for parse_imported.
    • xlsx: a_quote_inside_a_field_is_a_character, a_sep_line_names_the_delimiter, any_character_can_be_the_delimiter, plus apostrophe cases.
    • engine: text_to_columns_keeps_a_leading_apostrophe, text_to_columns_reads_a_quote_inside_a_field_as_a_character, from_text_csv_detects_the_delimiter_like_file_open, from_text_csv_keeps_field_text_and_number_formats, from_text_csv_reads_a_bracketed_header_as_csv.
  • cargo xtask ci: fmt, clippy, test, assets, layers and wasm pass. parity and keytips --check differ only in line endings on this Windows checkout (core.autocrlf); main does the same here.

  • Re-run of the Excel comparison: cells that differ from Excel 365, main → this branch. No count went up anywhere.

    Variant Path Before → after
    sep=, / sep=; / sep=| File › Open 8 / 8 / 7 → 0 / 0 / 0
    sep=, / sep=; / sep=| From Text/CSV 8 / 7 / 7 → 0 / 0 / 0
    apostrophes File › Open / From Text/CSV 4 / 8 → 0 / 0
    exponents with separators all paths 6 → 0
    quotes inside fields Text to Columns 10 → 0
    ;, de-DE, cp1252, de-CH, fr files From Text/CSV (against Excel de-DE) 38 / 48 / 12 / 10 / 12 → 2 / 20 / 2 / 1 / 4

    The remaining differences are the locale options above.

  • Overlaps:

🤖 Generated with Claude Code

https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj

iluebbe and others added 7 commits October 13, 2026 09:44
…e exponent

The group check split the whole text at commas, so the exponent counted as a group:
`1,5E3` (a decimal comma) was read as 15000 and `12,3e4` as 1230000, while `1,000E3`
stayed text. Excel 365 reads `1,000E3` and `1,234E2` as numbers and keeps `1,5E3`,
`-1,5E3`, `12,3e4` and `1,2e3` as text. The exponent is now split off before the
thousands groups are checked. This affects typing, CSV import, Text to Columns and
arithmetic on numeric text alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
Fields went through the typing parser, where a leading apostrophe forces text and is
dropped, so `'s-Hertogenbosch`, `'t Hooft`, `'93` and `'007` lost it on CSV open, From
Text/CSV and Text to Columns. Excel 365 keeps the apostrophe in all three. Files and
Text to Columns now use `parse_imported`, which keeps it; typing still forces text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
… for CSV, paste and Text to Columns

Text to Columns toggled quoting on every text qualifier, so `Pipe 12" long,5,EUR` stayed
in one cell as `Pipe 12 long,5,EUR`, and `x "y, z" w` lost its quotes and its split.
Excel 365 opens a quoted field only at the start of a field: `Pipe 12" long` | 5 | EUR,
`x "y` | ` z" w`.

CSV open, paste and Text to Columns had three copies of the same tokenizer that had
drifted apart. They now share `split_records` / `split_fields` in the CSV module, with
the delimiter set, text qualifier, "treat consecutive delimiters as one" and whether line
breaks end a record as options. Doubled qualifiers inside a quoted field still read as
one (storytold#119).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…p= and takes any delimiter character

From Text/CSV only looked for a tab in the first line and otherwise split at commas, so a
semicolon file (as a German Excel saves it) came in as one column per line, split again
at its decimal commas (`1.234.567,89` → `1.234.567` and 89, `12,5 %` → 0.05). Without a
`delimiter` it now uses the same detection as File › Open (`sniff_delimiter`, one copy).

Both now honour Excel's `sep=X` first line: they split on X and don't import the line,
so `sep=|` files open in columns. A given delimiter wins, and the line stays data.

The delimiter option is a character instead of a byte: `;` (U+FF1B) splits, and a
character whose low byte is a comma no longer splits at commas. A `delimiter` param that
isn't one character is refused instead of using its first letter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…as, number formats kept

From Text/CSV turned every value into JSON and wrote it back with `range.setValues`,
which treats strings as typed input. A field `=1+1` became a formula and
`=HYPERLINK("http://example.com","click")` a working link (a CSV injection path that
File › Open and Excel's From Text/CSV don't have). Numbers lost their formats on the
way, so `2024-03-04` showed as 45355, `12.5%` as 0.125 and `$1,234.50` as 1234.5.

It now writes the cells `read_csv` produced, with their number formats, so it types
fields exactly as File › Open does. JSON strings from Get Data are read the same way.
Both use the workbook's date system instead of always 1900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
Any text starting with `[` was parsed as JSON, so a CSV with a header like `[ID],Name`
failed with "invalid parameters for `data.getData`: expected value at line 1 column 2"
(File › Open reads it). The text is now JSON only when it parses as a JSON array of
objects, or when the file is a `.json`; otherwise it's read as CSV.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
@iluebbe

iluebbe commented Oct 15, 2026

Copy link
Copy Markdown
Author

Closing this pull request. The problem, Excel's measurements and a fix approach are now in #1446.

@iluebbe iluebbe closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant