smarter_csv 1.18.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -0
- data/CONTRIBUTORS.md +2 -1
- data/README.md +3 -1
- data/docs/bad_row_quarantine.md +3 -1
- data/docs/basic_read_api.md +1 -1
- data/docs/data_transformations.md +6 -2
- data/docs/header_transformations.md +3 -1
- data/docs/migrating_from_csv.md +1 -1
- data/docs/options.md +3 -3
- data/docs/real_world_csv.md +1 -0
- data/docs/upgrade_path.json +24 -2
- data/docs/upgrade_wizard.html +6 -4
- data/ext/smarter_csv/smarter_csv.c +153 -57
- data/lib/smarter_csv/hash_transformations.rb +16 -15
- data/lib/smarter_csv/header_transformations.rb +14 -1
- data/lib/smarter_csv/headers.rb +16 -1
- data/lib/smarter_csv/parser.rb +59 -11
- data/lib/smarter_csv/reader.rb +126 -62
- data/lib/smarter_csv/reader_options.rb +44 -6
- data/lib/smarter_csv/version.rb +1 -1
- data/lib/smarter_csv/writer.rb +3 -3
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dec0abc82b39809fd4822789f0e3f5bc80437eccf484435a540ea4c8ac83d500
|
|
4
|
+
data.tar.gz: 210f7a67b2a5e0470a97f7d7c05d8fb2ec644a945d96e3580a951fdf72ff2d2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a31c7b13246f6abcb14fa882ad3cb962e74ad2d4b150dd034eb437dfb6a9c0eccb53c266d361b2b8d9bff1e461e4638b452d2c32868bd5e2e208dbaffc7bbd75
|
|
7
|
+
data.tar.gz: fe8d6066b2f25e6bb598ff0e93188ed77b0a409e412ebf1f03636d760abae8111eba63e291b943eacf7e2cac1504ebe818cb51e1ecd8eef2adf6396794748acf
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,68 @@
|
|
|
4
4
|
> [!TIP]
|
|
5
5
|
> **Upgrading?** The [SmarterCSV Upgrade Wizard](https://tilo.github.io/smarter_csv/upgrade_wizard.html) walks you through what (if anything) you need to change for your specific version. Most steps do not require any changes.
|
|
6
6
|
|
|
7
|
+
## 1.19.0 (2026-08-10)
|
|
8
|
+
|
|
9
|
+
RSpec tests: **2,595 → 3,164** (+569 tests)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Reverted Behavior Changes
|
|
13
|
+
|
|
14
|
+
- **Exponent forms are no longer auto-converted to numbers ([#345](https://github.com/tilo/smarter_csv/issues/345)).**
|
|
15
|
+
|
|
16
|
+
Version 1.18.0 started converting scientific notation (`"1e3"`, `"12E5"`, `"1.5e3"`) to Floats. In real-world CSV data, digits-E-digits values are far more often identifiers (short codes, hex IDs) than scientific notation, and the auto-conversion corrupted them irreversibly — an ID like `"0047583311587E590003"` came back as `Infinity`.
|
|
17
|
+
|
|
18
|
+
As of 1.19.0, exponent forms always stay Strings — as they did in every version before 1.18.0. If a column really does contain scientific notation, convert it per-column with `value_converters`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
SmarterCSV.process(file, value_converters: { measurement: ->(v) { v.to_f } })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Thanks to [Denis Sadomowski](https://github.com/sonicdes) for the report.
|
|
25
|
+
|
|
26
|
+
### Behavior Changes
|
|
27
|
+
|
|
28
|
+
- **`field_size_limit` values below `4096` now raise a `ValidationError`** — the option is overrun protection (a hard upper bound against runaway fields), not per-field validation.
|
|
29
|
+
|
|
30
|
+
- **The Hash form of `convert_values_to_numeric` is now validated and normalized.** It requires exactly one of `only:`/`except:` with field name(s) (String/Symbol or an Array of them); an empty hash, unknown keys, both keys together, empty lists, or `nil`/boolean values raise a `ValidationError` (the two parser paths previously disagreed on these shapes). The listed names are normalized to the row-key type, so `only:`/`except:` now also works with `strings_as_keys` / `keep_original_headers`.
|
|
31
|
+
|
|
32
|
+
- **All empty field values are ONE shared, frozen, UTF-8 empty-string object — on both paths** (no String allocation per empty field). Mutating an empty value now raises `FrozenError` instead of silently changing every other empty value in the result. Relevant with `remove_empty_values: false`; the default `true` removes empties anyway.
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **Writer**: fields needing quoting are wrapped in the configured `quote_char`, not a hard-coded `"` — output with a custom `quote_char` round-trips again.
|
|
37
|
+
- **`Reader#each` without a block** no longer clears the configured `chunk_size` for a later `each_chunk` on the same Reader.
|
|
38
|
+
- **Duplicate-header disambiguation** no longer steals a real column's name — `name,name,name2` no longer raises `DuplicateHeaders` (the second `name` becomes `name3`).
|
|
39
|
+
- **The caller's `user_provided_headers` array** (and a reused options hash) is no longer mutated when rows have more columns than headers.
|
|
40
|
+
- **`headers: { only: }` / `{ except: }`** now works with `strings_as_keys` / `keep_original_headers` — selectors are normalized to the row-key type (with `only:`, nothing matched and every row came back empty: silent total data loss).
|
|
41
|
+
- **A quoted header containing an embedded newline** is stitched across physical lines like data rows (`"first\nname"` → `:first_name`); previously the first fragment was silently lost. An unclosed header quote at end-of-file raises `MalformedCSV`.
|
|
42
|
+
- **`quote_char: :auto`** raises a `ValidationError` instead of crashing with `NoMethodError` — quote_char has no auto-detection.
|
|
43
|
+
|
|
44
|
+
### Bug Fixes — C/Ruby parity
|
|
45
|
+
|
|
46
|
+
The C-accelerated and pure-Ruby parsers now behave identically in all of the following cases (same input, same output — verified by differential fuzzing and by running every parsing spec on both paths):
|
|
47
|
+
|
|
48
|
+
- a partial multi-char separator at end-of-line is field content, not a separator — with `col_sep: '||'` the C path silently dropped the lone `|` from `"y|"` (also fixes an out-of-bounds read near end-of-line)
|
|
49
|
+
- a trailing `\r` before an LF row separator is part of the line terminator — a CRLF line with a quoted last field raised `MalformedCSV` on the C path, and `strip_whitespace: false` kept `"x\r"` / `"1\r"` as values
|
|
50
|
+
- with `strip_whitespace: true`, values are stripped of Ruby's full `String#strip` character set on the C path too (a stray `\r` from mixed LF/CRLF files survived before)
|
|
51
|
+
- an empty line yields `nil` for ALL columns with `remove_empty_values: false` (the C path gave the first column `""`)
|
|
52
|
+
- a row consisting only of NUL bytes counts as blank on the C path too (`String#strip` semantics; the NUL byte itself remains data with `remove_empty_hashes: false`)
|
|
53
|
+
- a `nil` entry in `user_provided_headers` drops that column on the C path too
|
|
54
|
+
- an empty-string header key (`strings_as_keys` + `duplicate_header_suffix: nil`) is dropped on the C path too
|
|
55
|
+
- non-ASCII `missing_header_prefix` (e.g. `"spalte_ä_"`) no longer raises `EncodingError` on the C path — extra-column keys are interned as UTF-8 symbols
|
|
56
|
+
- `col_sep` / `row_sep` / `missing_header_prefix` values longer than the C parser's internal buffers fall back to the pure-Ruby parser instead of being silently truncated
|
|
57
|
+
- `nil_values_matching` matches the RAW string value on the C path (a pattern like `/\A007\z/` only ever saw the converted `7`) — and no longer switches off numeric conversion and zero-removal for the non-matching values
|
|
58
|
+
- `field_size_limit` is checked against the raw field size BEFORE numeric conversion, so an oversized digit-only field raises on the C path too instead of being converted to a huge Integer (the exact overrun the option exists to prevent)
|
|
59
|
+
- `headers: { only: }` short-cuts on the pure-Ruby path too — parsing stops right after the last wanted column, matching the C path (no `:column_N` discovery behind it, and faster)
|
|
60
|
+
- a one-character multi-byte `col_sep` (e.g. `'é'`) no longer crashes the pure-Ruby parser
|
|
61
|
+
- a multi-byte character directly before a literal quote (`é"x`) no longer crashes the pure-Ruby parser (`IndexError` from a mid-character byte offset)
|
|
62
|
+
- invalid bytes in the input (typically Latin-1 data mislabeled as UTF-8) no longer crash the pure-Ruby parser — fields keep their raw bytes and encoding tag exactly (never transcoded, so the data stays recoverable via `force_encoding`); cleanup remains opt-in via `force_utf8` / `invalid_byte_sequence`
|
|
63
|
+
- the multiline stitch gate models the parser's rules exactly (doubled-quote precedence, backslash escapes, end-of-line chomp) — no more fabricated `MalformedCSV` on the pure-Ruby path for rows the parser can close
|
|
64
|
+
|
|
65
|
+
### Tests
|
|
66
|
+
|
|
67
|
+
- **Every parsing spec now runs on BOTH the C-accelerated and the pure-Ruby path** via `[true, false]` acceleration loops (~420 additional examples), a seeded differential parity-fuzz spec (2,000 randomized inputs per run, including combined option sets) guards C/Ruby parity permanently, and line coverage is at **100%**.
|
|
68
|
+
|
|
7
69
|
## 1.18.1 (2026-06-30)
|
|
8
70
|
|
|
9
71
|
### Bug Fixes
|
data/CONTRIBUTORS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A Big Thank You to all
|
|
1
|
+
# A Big Thank You to all 66 Contributors!!
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
A Big Thank you to everyone who filed issues, sent comments, and who contributed with pull requests:
|
|
@@ -68,3 +68,4 @@ A Big Thank you to everyone who filed issues, sent comments, and who contributed
|
|
|
68
68
|
* [Jonas Staškevičius](https://github.com/pirminis)
|
|
69
69
|
* [conorg](https://github.com/conorg)
|
|
70
70
|
* [Alex Shenia](https://github.com/alexshenia)
|
|
71
|
+
* [Denis Sadomowski](https://github.com/sonicdes)
|
data/README.md
CHANGED
|
@@ -297,6 +297,8 @@ Or install it yourself as:
|
|
|
297
297
|
$ gem install smarter_csv
|
|
298
298
|
```
|
|
299
299
|
|
|
300
|
+
The C extension is built on install and used automatically. On platforms where it can't build, the pure-Ruby implementation runs instead and produces identical results.
|
|
301
|
+
|
|
300
302
|
### CPU Optimization (`SMARTER_CSV_PERFORMANCE`)
|
|
301
303
|
|
|
302
304
|
The C extension is compiled when the gem is installed. By default it is built **portable**: it uses no CPU-specific instructions, so a binary built on one machine runs on any other CPU of the same architecture. Set `SMARTER_CSV_PERFORMANCE` at install time to trade portability for speed:
|
|
@@ -362,7 +364,7 @@ For reporting issues, please:
|
|
|
362
364
|
* open a pull-request adding a test that demonstrates the issue
|
|
363
365
|
* mention your version of SmarterCSV, Ruby, Rails
|
|
364
366
|
|
|
365
|
-
# [A Special Thanks to all
|
|
367
|
+
# [A Special Thanks to all Contributors!](CONTRIBUTORS.md) 🎉🎉🎉
|
|
366
368
|
|
|
367
369
|
|
|
368
370
|
## Contributing
|
data/docs/bad_row_quarantine.md
CHANGED
|
@@ -266,7 +266,9 @@ until it either finds the closing quote or reaches end-of-file, potentially cons
|
|
|
266
266
|
of megabytes.
|
|
267
267
|
|
|
268
268
|
`field_size_limit` sets a hard cap (in bytes) on the size of any individual extracted field.
|
|
269
|
-
The default is `nil` (no limit)
|
|
269
|
+
The default is `nil` (no limit); the minimum allowed value is `4096` — this option is overrun
|
|
270
|
+
protection against runaway or crafted fields, not a per-field validation tool, so small values
|
|
271
|
+
are rejected with a `ValidationError`. When a field exceeds the limit a
|
|
270
272
|
`SmarterCSV::FieldSizeLimitExceeded` exception is raised — and because it inherits from
|
|
271
273
|
`SmarterCSV::Error`, the `on_bad_row` option handles it exactly like any other parse error.
|
|
272
274
|
|
data/docs/basic_read_api.md
CHANGED
|
@@ -223,7 +223,7 @@ comment_regexp → strip_chars_from_headers → split on col_sep → strip quote
|
|
|
223
223
|
→ disambiguate_headers → symbolize → key_mapping
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
`user_provided_headers` bypasses the file header and all transformation steps — your array is used as-is.
|
|
226
|
+
`user_provided_headers` bypasses the file header and all transformation steps — your array is used as-is; a `nil` entry drops that column.
|
|
227
227
|
|
|
228
228
|
See [Header Transformations](./header_transformations.md) for the full step-by-step table and options.
|
|
229
229
|
|
|
@@ -114,6 +114,8 @@ data = SmarterCSV.process(file, remove_empty_values: false)
|
|
|
114
114
|
# => [{name: "Alice", score: 42, notes: nil}, {name: nil, score: nil, notes: "great player"}]
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
With `remove_empty_values: false`, all kept empty-string values are ONE shared frozen String (an allocation optimization) — `dup` before mutating one in place *(1.19.0+)*.
|
|
118
|
+
|
|
117
119
|
---
|
|
118
120
|
|
|
119
121
|
## `remove_zero_values`
|
|
@@ -156,7 +158,9 @@ data = SmarterCSV.process(file,
|
|
|
156
158
|
convert_values_to_numeric: { only: [:quantity, :price] })
|
|
157
159
|
```
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
The Hash form requires exactly one of `only:`/`except:` with field name(s) — anything else (empty hash, unknown keys, both keys, empty lists, `nil`/boolean values) raises a `ValidationError` *(1.19.0+)*.
|
|
162
|
+
|
|
163
|
+
Exponent forms (e.g. `"1e3"`, `"12E5"`, `"1.5e3"`) are NOT converted — they stay Strings *(changed in 1.19.0; only 1.18.x converted them)*. In real-world CSV data such values are far more often identifiers (short codes, hex IDs) than scientific notation, and auto-converting them corrupts data — e.g. an ID like `"0047583311587E590003"` became `Infinity`. If a column really does contain scientific notation, convert it per-column with [`value_converters`](./value_converters.md). Bare-dot forms like `".5"` and `"3."` are left as Strings (they are not valid numbers here). Integers and floats convert identically on the C-accelerated and pure-Ruby paths.
|
|
160
164
|
|
|
161
165
|
---
|
|
162
166
|
|
|
@@ -164,7 +168,7 @@ Scientific notation (e.g. `"1.5e3"`, `"6.022e23"`) is recognized and converted t
|
|
|
164
168
|
|
|
165
169
|
**Default: `:auto`**
|
|
166
170
|
|
|
167
|
-
Controls how decimal values (those with a `.`
|
|
171
|
+
Controls how decimal values (those with a `.`) are converted. Integers are unaffected — they are always returned as `Integer`.
|
|
168
172
|
|
|
169
173
|
| Value | Result |
|
|
170
174
|
|---------------|-----------------------------------------------------------------------------------------|
|
|
@@ -56,7 +56,9 @@ comment_regexp ──► strip_chars_from_headers ──► split on col_sep
|
|
|
56
56
|
| 9 | `strings_as_keys` | `false` | Converts headers to symbols (skipped if `true` or `keep_original_headers`) |
|
|
57
57
|
| 10 | `key_mapping` | `nil` | Renames or drops headers; use post-transformation key names as input |
|
|
58
58
|
|
|
59
|
-
> `user_provided_headers` bypasses all file header reading and transformation entirely — your array is used as-is. Versions >1.13 automatically set `headers_in_file: false` when `user_provided_headers` is given; if the file has a header row you want to skip, set `headers_in_file: true` explicitly.
|
|
59
|
+
> `user_provided_headers` bypasses all file header reading and transformation entirely — your array is used as-is; a `nil` entry drops that column. Versions >1.13 automatically set `headers_in_file: false` when `user_provided_headers` is given; if the file has a header row you want to skip, set `headers_in_file: true` explicitly.
|
|
60
|
+
|
|
61
|
+
A quoted header containing an embedded newline is stitched across physical lines like a data row *(1.19.0+)* — the newline becomes `_` via the standard transformations (`"first\nname"` → `:first_name`).
|
|
60
62
|
|
|
61
63
|
See [Configuration Options](./options.md) for full option reference.
|
|
62
64
|
|
data/docs/migrating_from_csv.md
CHANGED
|
@@ -223,7 +223,7 @@ rows = SmarterCSV.process('sample.csv',
|
|
|
223
223
|
convert_values_to_numeric: { except: [:zip_code, :phone, :account_number] })
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
**High-precision decimals — scientific data and geo coordinates.** GPS/geo coordinates, scientific measurements, and financial figures routinely carry 16+ significant digits, where Ruby's `Float()`-based conversion (`converters: :numeric` / `:float`) silently rounds the value. SmarterCSV's default `decimal_precision: :auto` returns a `BigDecimal` once a value exceeds 16 significant digits (and a `Float` otherwise), so the full value is preserved
|
|
226
|
+
**High-precision decimals — scientific data and geo coordinates.** GPS/geo coordinates, scientific measurements, and financial figures routinely carry 16+ significant digits, where Ruby's `Float()`-based conversion (`converters: :numeric` / `:float`) silently rounds the value. SmarterCSV's default `decimal_precision: :auto` returns a `BigDecimal` once a value exceeds 16 significant digits (and a `Float` otherwise), so the full value is preserved. (Exponent forms like `6.022e23` are not auto-converted — in CSV data they are usually identifiers, not numbers; use `value_converters` for columns that really contain scientific notation.)
|
|
227
227
|
|
|
228
228
|
**With Ruby CSV (precision lost):**
|
|
229
229
|
```ruby
|
data/docs/options.md
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|--------|---------|-------------|
|
|
34
34
|
| `:row_sep` | `$/` | Separates rows. Defaults to your OS row separator: `\n` on UNIX, `\r\n` on Windows. |
|
|
35
35
|
| `:col_sep` | `","` | Separates each value in a row. |
|
|
36
|
-
| `:quote_char` | `'"'` | Character used to quote CSV fields. |
|
|
36
|
+
| `:quote_char` | `'"'` | Character used to quote CSV fields. Must be a single byte. |
|
|
37
37
|
| `:force_quotes` | `false` | Forces each individual value to be quoted. |
|
|
38
38
|
| `:headers` | `[]` | List of keys from the input to use as headers in the CSV file. ⚠️ Disables automatic header detection! |
|
|
39
39
|
| `:map_headers` | `{}` | Like `:headers`, but also maps each key to a user-specified header value. ⚠️ Disables automatic header detection! |
|
|
@@ -121,7 +121,7 @@ See [Parsing Strategy](./parsing_strategy.md) for full details on quote handling
|
|
|
121
121
|
| Option | Default | Explanation |
|
|
122
122
|
|--------|---------|-------------|
|
|
123
123
|
| `:strip_whitespace` | `true` | Remove whitespace before/after values and headers. |
|
|
124
|
-
| `:convert_values_to_numeric` | `true` | Convert strings containing integers or floats
|
|
124
|
+
| `:convert_values_to_numeric` | `true` | Convert strings containing integers or floats to the appropriate numeric type. Exponent forms like `1.5e3` or `12E5` stay Strings (1.19.0+) — they are usually identifiers, not numbers. Accepts `{except: [:key1, :key2]}` or `{only: :key3}` to limit which columns. |
|
|
125
125
|
| `:decimal_precision` | `:auto` | How decimals are converted: `:auto` returns `Float` but `BigDecimal` above 16 significant digits (no precision loss); `:float` always returns `Float`; `:bigdecimal` always returns `BigDecimal`. Integers are unaffected. |
|
|
126
126
|
| `:value_converters` | `nil` | Hash of `:header => converter`; converter can be a lambda/Proc or a class implementing `self.convert(value)`. See [Value Converters](./value_converters.md). |
|
|
127
127
|
| `:remove_empty_values` | `true` | Remove key/value pairs where the value is `nil`, empty, or whitespace-only — any Unicode whitespace, same as Ruby's `String#blank?`. |
|
|
@@ -138,7 +138,7 @@ See [Bad Row Quarantine](./bad_row_quarantine.md) for full details.
|
|
|
138
138
|
| `:on_bad_row` | `:raise` | Behavior when a row raises a parse error. `:raise` (default): re-raise, stopping processing. `:skip`: skip the bad row and continue. `:collect`: skip and append an error record to `reader.errors[:bad_rows]`. callable: called with the error record per bad row; processing continues. |
|
|
139
139
|
| `:collect_raw_lines` | `true` | When collecting bad rows, include the raw stitched line in the error record. |
|
|
140
140
|
| `:bad_row_limit` | `nil` | If set, raises `SmarterCSV::TooManyBadRows` after this many bad rows. |
|
|
141
|
-
| `:field_size_limit` | `nil` | Maximum size of any extracted field in bytes. `nil` means no limit. Raises `SmarterCSV::FieldSizeLimitExceeded` (handled by `on_bad_row`) if a field or accumulating multiline buffer exceeds this size. Prevents DoS from runaway quoted fields or huge inline payloads. See [Bad Row Quarantine](./bad_row_quarantine.md#limiting-field-size-field_size_limit). |
|
|
141
|
+
| `:field_size_limit` | `nil` | Maximum size of any extracted field in bytes. `nil` means no limit; the minimum allowed value is `4096` (it is overrun protection, not per-field validation). Raises `SmarterCSV::FieldSizeLimitExceeded` (handled by `on_bad_row`) if a field or accumulating multiline buffer exceeds this size. Prevents DoS from runaway quoted fields or huge inline payloads. See [Bad Row Quarantine](./bad_row_quarantine.md#limiting-field-size-field_size_limit). |
|
|
142
142
|
|
|
143
143
|
### Output & Diagnostics
|
|
144
144
|
|
data/docs/real_world_csv.md
CHANGED
|
@@ -51,6 +51,7 @@ Real-world files come from dozens of different systems, each with their own defa
|
|
|
51
51
|
| Windows-1252 / Latin-1 | 🔘 | Specify `file_encoding: 'windows-1252'`. Common in European financial exports, older SAP systems, QuickBooks. |
|
|
52
52
|
| UTF-16 LE with BOM | 🔘 | Specify `file_encoding: 'utf-16le'`. Some Microsoft SQL Server and Access exports default to this. |
|
|
53
53
|
| Shift-JIS / EUC-JP | 🔘 | Specify `file_encoding: 'shift_jis'` or `'euc-jp'`. Japanese ERP and POS systems. |
|
|
54
|
+
| Invalid bytes / mislabeled encoding | ✅ | Never crashes — the affected field keeps its raw bytes exactly, recoverable via `force_encoding`. Opt into cleanup with `force_utf8` / `invalid_byte_sequence`. |
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
data/docs/upgrade_path.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "1.
|
|
3
|
-
"latest_release": "1.
|
|
2
|
+
"latest": "1.19",
|
|
3
|
+
"latest_release": "1.19.0",
|
|
4
4
|
"path": {
|
|
5
5
|
"1.0": {
|
|
6
6
|
"to": "1.1",
|
|
@@ -181,6 +181,28 @@
|
|
|
181
181
|
}
|
|
182
182
|
],
|
|
183
183
|
"note": "<strong>Bonus:</strong> 1.18 automatically converts scientific notation (e.g. <code>1e3</code>, <code>6.022e23</code>) to numbers, and preserves full precision on long decimals by returning a <code>BigDecimal</code> when a value carries more than 16 significant digits (<code>decimal_precision: :auto</code>, the default). Nothing to change — this just works."
|
|
184
|
+
},
|
|
185
|
+
"1.18": {
|
|
186
|
+
"to": "1.19",
|
|
187
|
+
"latest_release": "1.18.1",
|
|
188
|
+
"actions": [
|
|
189
|
+
{
|
|
190
|
+
"if": "you relied on 1.18.x auto-converting scientific notation (<code>\"1e3\"</code>, <code>\"1.5e3\"</code>) to numbers",
|
|
191
|
+
"then": "convert those columns explicitly with <code>value_converters: { column: ->(v) { v.to_f } }</code> — 1.19.0 keeps all exponent forms as Strings again (as every version before 1.18.0 did), because in real-world data they are usually identifiers, and IDs like <code>\"0047583311587E590003\"</code> were corrupted to <code>Infinity</code> (<a href=\"https://github.com/tilo/smarter_csv/issues/345\">#345</a>)."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"if": "you set <code>field_size_limit:</code> to a value below 4096",
|
|
195
|
+
"then": "raise it to at least <code>4096</code> — the option is overrun protection (a hard upper bound against runaway fields), not per-field validation, and 1.19.0 rejects smaller values with a <code>ValidationError</code>."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"if": "you pass <code>convert_values_to_numeric:</code> as a Hash with anything other than exactly one of <code>only:</code>/<code>except:</code> and field name(s)",
|
|
199
|
+
"then": "fix the option — an empty hash, unknown keys, both keys together, empty lists, or <code>nil</code>/boolean values now raise a <code>ValidationError</code> (the two parser paths previously disagreed silently on these shapes)."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"if": "you mutate empty-string values in the results (only possible with <code>remove_empty_values: false</code>)",
|
|
203
|
+
"then": "<code>dup</code> them first — all empty field values are now one shared, frozen empty string (an allocation optimization), so in-place mutation raises <code>FrozenError</code>."
|
|
204
|
+
}
|
|
205
|
+
]
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
}
|
data/docs/upgrade_wizard.html
CHANGED
|
@@ -238,7 +238,7 @@ function renderHop(series, originalVersion) {
|
|
|
238
238
|
if (hop.actions.length === 0) {
|
|
239
239
|
body = `<div class="hop dropin">
|
|
240
240
|
You can upgrade directly to version ${targetRelease}. No changes needed.
|
|
241
|
-
</div
|
|
241
|
+
</div>`;
|
|
242
242
|
} else {
|
|
243
243
|
body = `<div class="hop">
|
|
244
244
|
${hop.actions.map((a, i) => `
|
|
@@ -253,6 +253,8 @@ function renderHop(series, originalVersion) {
|
|
|
253
253
|
`).join("")}
|
|
254
254
|
</div>`;
|
|
255
255
|
}
|
|
256
|
+
// The bonus note renders below the actions (or below the drop-in box) regardless of whether this hop has actions.
|
|
257
|
+
if (hop.note) body += `<div class="benefit">${hop.note}</div>`;
|
|
256
258
|
|
|
257
259
|
const nextLabel = hop.to === LATEST ? `Finish at ${targetRelease} →` : `Continue to ${targetRelease} →`;
|
|
258
260
|
const reminder = hop.actions.length === 0 ? `<p class="reminder">You can upgrade directly to version ${targetRelease}. No changes needed.</p>` :
|
|
@@ -386,15 +388,15 @@ function renderSummary() {
|
|
|
386
388
|
const list = decisions.map(d => {
|
|
387
389
|
const targetRelease = latestReleaseFor(d.to);
|
|
388
390
|
const heading = `<p class="summary-hop-heading"><strong>${d.from}.x → ${targetRelease}</strong></p>`;
|
|
391
|
+
const noteHTML = d.note ? `<p>${d.note}</p>` : "";
|
|
389
392
|
if (d.dropIn) {
|
|
390
|
-
const noteHTML = d.note ? `<p>${d.note}</p>` : "";
|
|
391
393
|
return `<div class="summary-hop">${heading}<p class="muted">No code changes needed for this step.</p>${noteHTML}</div>`;
|
|
392
394
|
}
|
|
393
395
|
if (d.matched.length === 0) {
|
|
394
|
-
return `<div class="summary-hop">${heading}<p class="muted">None of the conditions in this step applied to your code.</p
|
|
396
|
+
return `<div class="summary-hop">${heading}<p class="muted">None of the conditions in this step applied to your code.</p>${noteHTML}</div>`;
|
|
395
397
|
}
|
|
396
398
|
const items = d.matched.map(a => `<li><strong>If</strong> ${a["if"]}<br>→ ${a.then}</li>`).join("");
|
|
397
|
-
return `<div class="summary-hop">${heading}<ul>${items}</ul
|
|
399
|
+
return `<div class="summary-hop">${heading}<ul>${items}</ul>${noteHTML}</div>`;
|
|
398
400
|
}).join("");
|
|
399
401
|
|
|
400
402
|
return `
|