smarter_csv 1.17.4 → 1.18.1
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 +64 -0
- data/README.md +26 -0
- data/UPGRADING.md +22 -7
- data/docs/_introduction.md +22 -0
- data/docs/data_transformations.md +33 -0
- data/docs/migrating_from_csv.md +18 -0
- data/docs/options.md +2 -1
- data/docs/upgrade_path.json +13 -2
- data/docs/upgrade_wizard.html +13 -2
- data/ext/smarter_csv/cpu_flags.rb +55 -0
- data/ext/smarter_csv/extconf.rb +23 -2
- data/ext/smarter_csv/smarter_csv.c +204 -32
- data/ext/smarter_csv/vendor/LICENSE-fast_float-MIT +27 -0
- data/ext/smarter_csv/vendor/eisel_lemire.h +117 -0
- data/ext/smarter_csv/vendor/eisel_lemire.md +29 -0
- data/ext/smarter_csv/vendor/eisel_lemire_powers.h +663 -0
- data/lib/smarter_csv/hash_transformations.rb +51 -2
- data/lib/smarter_csv/reader_options.rb +24 -0
- data/lib/smarter_csv/version.rb +1 -1
- data/lib/smarter_csv.rb +1 -0
- data/smarter_csv.gemspec +3 -0
- metadata +23 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b820fa45fcde042d9a96230a53d4065b104db91e50d48629b9d43cb099a6dca
|
|
4
|
+
data.tar.gz: 8bff7d0845fe3b6c3eecc71c58065364809fafb87bc25f238b53c35cf5b49519
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8b4a0d5cb0620b542617c8426eb3004dbb7a89602b38174280636194c634d1ee2e9951eac727a1972761d5434b66bb1f1bf9c90ec2659790405cc8c34159062
|
|
7
|
+
data.tar.gz: f6e042464354f8714aa5504eb04c16f137806766057bc22a6db0677d65f49eb4d57f5d54d1378f106fa0899c61408c9cb890d5e20d20f1802a1225338a7b9ba8
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@
|
|
|
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.18.1 (2026-06-30)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **Portable builds by default — fixes the "Illegal instruction" crash on heterogeneous CPUs ([#343](https://github.com/tilo/smarter_csv/issues/343)).**
|
|
12
|
+
|
|
13
|
+
Since 1.14.3 the C extension was compiled with `-march=native` on every platform except Apple Silicon, baking-in the build host's CPU instructions (e.g. AVX-512).
|
|
14
|
+
A binary built on one machine then could encounter `Illegal instruction` when run on a CPU lacking those instructions — common when the build host differs from the run host (CI/build servers, Docker images, mixed-hardware fleets).
|
|
15
|
+
|
|
16
|
+
The C extension is now built **portable** by default (no host-specific instructions). Thanks to [@paholg](https://github.com/paholg) for the report.
|
|
17
|
+
|
|
18
|
+
### New Features
|
|
19
|
+
|
|
20
|
+
- **`SMARTER_CSV_PERFORMANCE` build option** (`portable` default, `tuned`, or `max`)
|
|
21
|
+
|
|
22
|
+
| Level | Flags added | Portable? | Use when |
|
|
23
|
+
|----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
|
|
24
|
+
| `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
|
|
25
|
+
| `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
|
|
26
|
+
| `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |
|
|
27
|
+
|
|
28
|
+
See the [Introduction](docs/_introduction.md#build-time-performance-tuning-smarter_csv_performance) for details.
|
|
29
|
+
|
|
30
|
+
## 1.18.0 (2026-06-17)
|
|
31
|
+
|
|
32
|
+
This release is focused on both performance and the introduction of automatic conversion of decimals to big_decimal or float, preserving the precision, and also supporting scientific notation.
|
|
33
|
+
|
|
34
|
+
⚠️ This version is particularly interesting if you have geolocation, scientific, or high-precision data.
|
|
35
|
+
|
|
36
|
+
### New Features
|
|
37
|
+
|
|
38
|
+
- **`decimal_precision` option** (`:auto` default, or `:float` / `:bigdecimal`) — controls how decimal values are converted. `:auto` returns a `Float` unless the value carries more than 16 significant digits, in which case it returns a `BigDecimal` so no precision is lost; `:float` always returns `Float`; `:bigdecimal` always returns `BigDecimal`. Integers are unaffected (always `Integer`). Works identically on the C and Ruby paths. (Ruby's standard-library CSV has no high-precision option — its `:numeric`/`:float` converters use `Float()` and lose precision.)
|
|
39
|
+
- **Float** conversion on the C path now uses the fast **Eisel-Lemire** algorithm (fast_float, vendored) for mantissas up to 19 significant digits — correctly rounded, bit-for-bit identical to `String#to_f` — with a `strtod` fallback beyond that (more than 19 digits / extreme exponents). High-precision values that become `BigDecimal` under `:auto`/`:bigdecimal` are parsed by Ruby's `BigDecimal`.
|
|
40
|
+
|
|
41
|
+
### Behavior Changes
|
|
42
|
+
|
|
43
|
+
- **Scientific notation now converts to a number** (e.g. `"1e3"`, `"1.5e-5"`, `"6.022e23"`). Previously the Ruby path left these as Strings and the C path was inconsistent.
|
|
44
|
+
- **The C and Ruby numeric-conversion paths are now aligned.** Bare-dot forms like `".5"` and `"3."` stay Strings on **both** paths (the shared grammar requires an integer part and, when a dot is present, a fraction digit). Previously the C path converted these and the Ruby path did not.
|
|
45
|
+
- With the default `decimal_precision: :auto`, decimal values carrying more than 16 significant digits are now returned as `BigDecimal` instead of `Float`. Pass `decimal_precision: :float` to keep the previous always-`Float` behavior.
|
|
46
|
+
- `bigdecimal` is now a runtime dependency (it is no longer a default gem on Ruby 3.4+).
|
|
47
|
+
|
|
48
|
+
### Performance
|
|
49
|
+
|
|
50
|
+
The C-accelerated path is faster across the board, **up to ~1.5× on the right shapes** — numeric-heavy data and backslash-escaped quoted fields — and ~1.04–1.08× on typical files.
|
|
51
|
+
|
|
52
|
+
- Eisel-Lemire (Mushtak-Lemire) algorithm on the C path to convert decimals to `Float` or `BigDecimal`. Numeric-heavy data (many float/decimal columns) parses significantly faster.
|
|
53
|
+
- SIMD scanner for backslash-escaped quoted fields (C-path), using NEON (arm64) and SSE2 (x86-64) with a scalar fallback. Speeds up `quote_escaping: :backslash` parsing of long quoted fields.
|
|
54
|
+
|
|
55
|
+
| File | C-path | driver |
|
|
56
|
+
|---------------------------------|----------------------------------|-----------------------|
|
|
57
|
+
| backslash_long_fields_60k | 1.48× faster (0.1880s → 0.1273s) | SIMD quote/backslash scanner |
|
|
58
|
+
| sensor_data_50krows_50cols | 1.40× faster (0.2763s → 0.1975s) | Eisel-Lemire numeric conversion |
|
|
59
|
+
|
|
60
|
+
### Improvements
|
|
61
|
+
|
|
62
|
+
- Improved robustness of symbol-valued enum option processing.
|
|
63
|
+
|
|
64
|
+
### Tests
|
|
65
|
+
|
|
66
|
+
- added parity tests for long quoted-field scanning across 16-byte boundaries, running on both the C and Ruby paths.
|
|
67
|
+
- added tests for string-to-symbol coercion of the enum options.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
7
71
|
## 1.17.4 (2026-06-03)
|
|
8
72
|
|
|
9
73
|
### Bug Fix
|
data/README.md
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
> See [**Ruby CSV Pitfalls**](docs/ruby_csv_pitfalls.md) for 10 ways `CSV.read` silently corrupts or loses data, and how SmarterCSV handles them.
|
|
17
17
|
|
|
18
|
+
> [!TIP]
|
|
19
|
+
> **No silent precision loss (new in 1.18.0).** For scientific data, GPS/geo coordinates, and financial figures — which routinely carry 16+ significant digits — Ruby's standard CSV converts with `Float()`, so a value like `1234567890.123456789` is silently rounded to `1234567890.1234567`. SmarterCSV's default `decimal_precision: :auto` returns a `BigDecimal` for values beyond 16 significant digits (and `Float` otherwise) — full precision, no data loss. Floats are decoded with the Eisel-Lemire algorithm: correctly rounded, bit-for-bit identical to `String#to_f`.
|
|
20
|
+
|
|
18
21
|
Beyond raw speed, SmarterCSV is designed to provide a significantly more convenient and developer-friendly interface than traditional CSV libraries. Instead of returning raw arrays that require substantial post-processing, SmarterCSV produces Rails-ready hashes for each row, making the data immediately usable with ActiveRecord, Sidekiq pipelines, parallel processing, and JSON-based workflows such as S3.
|
|
19
22
|
|
|
20
23
|
In a Rails app, warnings auto-route through `Rails.logger` and instrumentation hooks compose with `ActiveSupport::Notifications` — no setup required. Outside Rails, warnings fall back to `$stderr` and the same APIs work without any framework dependency.
|
|
@@ -89,6 +92,8 @@ rows = SmarterCSV.process('data.csv')
|
|
|
89
92
|
data = SmarterCSV.parse(csv_string)
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
Numeric conversion is also more accurate: where Ruby's `:numeric`/`:float` converters round high-precision decimals through `Float()`, SmarterCSV's default `decimal_precision: :auto` returns a `BigDecimal` past 16 significant digits, so no precision is lost (pass `decimal_precision: :float` for like-for-like `Float` output).
|
|
96
|
+
|
|
92
97
|
* See [**Migrating from Ruby CSV**](docs/migrating_from_csv.md) for a full comparison of options, behavior differences, and a quick-reference table.
|
|
93
98
|
|
|
94
99
|
## Examples
|
|
@@ -292,6 +297,27 @@ Or install it yourself as:
|
|
|
292
297
|
$ gem install smarter_csv
|
|
293
298
|
```
|
|
294
299
|
|
|
300
|
+
### CPU Optimization (`SMARTER_CSV_PERFORMANCE`)
|
|
301
|
+
|
|
302
|
+
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:
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
| Level | Flags added | Portable? | Use when |
|
|
306
|
+
|----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
|
|
307
|
+
| `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
|
|
308
|
+
| `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
|
|
309
|
+
| `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |
|
|
310
|
+
|
|
311
|
+
`max` enables host-specific instructions, so a binary built with it can crash with `Illegal instruction` if it later runs on a CPU that lacks them (for example, built on an AVX-512 machine and run on one without). `tuned` only changes instruction scheduling, never the instruction set, so it stays portable. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
SMARTER_CSV_PERFORMANCE=tuned gem install smarter_csv # portable, tuned for this machine's microarchitecture
|
|
315
|
+
SMARTER_CSV_PERFORMANCE=max gem install smarter_csv # fastest, NOT portable — only when you build on the machine you run on
|
|
316
|
+
SMARTER_CSV_PERFORMANCE=tuned bundle install # same, under Bundler
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_csv`.
|
|
320
|
+
|
|
295
321
|
## Documentation
|
|
296
322
|
|
|
297
323
|
* [Introduction](docs/_introduction.md)
|
data/UPGRADING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> [!TIP]
|
|
4
4
|
> Prefer the interactive [Upgrade Wizard](https://tilo.github.io/smarter_csv/upgrade_wizard.html) for a guided walk-through with Yes/No questions.
|
|
5
|
-
> This document is auto-generated from `CHANGELOG.md` and `docs/upgrade_path.json` by `bin/
|
|
5
|
+
> This document is auto-generated from `CHANGELOG.md` and `docs/upgrade_path.json` by `bin/generate-upgrading-md`.
|
|
6
6
|
|
|
7
7
|
## How to use this guide
|
|
8
8
|
|
|
@@ -12,25 +12,40 @@
|
|
|
12
12
|
|
|
13
13
|
Prefer an interactive walk-through? The [Upgrade Wizard](https://tilo.github.io/smarter_csv/upgrade_wizard.html) asks one question at a time and only shows the migration steps that apply to your code.
|
|
14
14
|
|
|
15
|
-
**Latest release:** `1.
|
|
15
|
+
**Latest release:** `1.18.1` (in the `1.18.x` series).
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## 1.
|
|
19
|
+
## 1.18.x — latest series
|
|
20
20
|
|
|
21
21
|
**Versions in this series:**
|
|
22
|
-
[1.
|
|
22
|
+
[1.18.0, 1.18.1]
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
> ⚠️ **In-series notes** worth checking:
|
|
25
|
+
> - **1.18.0:** This version is particularly interesting if you have geolocation, scientific, or high-precision data.
|
|
26
|
+
|
|
27
|
+
**Latest release:** `1.18.1`
|
|
25
28
|
|
|
26
29
|
Update your Gemfile to:
|
|
27
30
|
|
|
28
31
|
```ruby
|
|
29
|
-
gem 'smarter_csv', '~> 1.
|
|
32
|
+
gem 'smarter_csv', '~> 1.18.0'
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
Then run `bundle update smarter_csv`.
|
|
33
36
|
|
|
37
|
+
## Series 1.17 → 1.18
|
|
38
|
+
|
|
39
|
+
**Coming from any 1.17 version:**
|
|
40
|
+
[1.17.0, 1.17.1, 1.17.2, 1.17.3, 1.17.4]
|
|
41
|
+
|
|
42
|
+
**Upgrading to 1.18.x** (latest: `1.18.1`):
|
|
43
|
+
|
|
44
|
+
- **If** you build and run the gem on the same machine (or a fleet of identical CPUs) and want the previous native-optimized build for maximum speed:
|
|
45
|
+
→ set `SMARTER_CSV_PERFORMANCE=max` (or `tuned`) at install time — 1.18.1 builds <strong>portable</strong> by default (no host-specific CPU instructions) to fix an `Illegal instruction` crash when a binary built on one CPU is run on another (<a href="https://github.com/tilo/smarter_csv/issues/343">#343</a>). The default is safe everywhere; the env var opts back into host optimization.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
34
49
|
## Series 1.16 → 1.17
|
|
35
50
|
|
|
36
51
|
**Coming from any 1.16 version:**
|
|
@@ -40,7 +55,7 @@ Then run `bundle update smarter_csv`.
|
|
|
40
55
|
> - **1.16.1:** **Fibers:** `SmarterCSV.errors` uses `Thread.current` for storage, which is **shared across all fibers running in the same thread**. If you process CSV files concurrently in fibers (e.g. with `Async`, `Falcon`, or manual `Fiber` scheduling), `SmarterCSV.errors` may return stale or wrong results. **Use `SmarterCSV::Reader` directly** — errors are scoped to the reader instance and are always correct regardless of fiber context.
|
|
41
56
|
> - **1.16.2:** If your code references auto-generated keys for blank headers, update those to use the absolute column position.
|
|
42
57
|
|
|
43
|
-
**Upgrading to 1.17.x** (latest: `1.17.
|
|
58
|
+
**Upgrading to 1.17.x** (latest: `1.17.4`): you can upgrade all the way — no code changes needed.
|
|
44
59
|
|
|
45
60
|
---
|
|
46
61
|
|
data/docs/_introduction.md
CHANGED
|
@@ -75,6 +75,28 @@ SmarterCSV was created to solve exactly these problems: nightly imports of large
|
|
|
75
75
|
* **CSV writing:**
|
|
76
76
|
`SmarterCSV.generate` writes arrays of hashes to CSV, with support for header renaming and value converters on output. See [The Basic Write API](./basic_write_api.md).
|
|
77
77
|
|
|
78
|
+
## Build-Time Performance Tuning (`SMARTER_CSV_PERFORMANCE`)
|
|
79
|
+
|
|
80
|
+
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 compiled on one machine runs on any other CPU of the same architecture. This matters whenever the machine that builds the gem differs from the machine that runs it — a CI or build server, a Docker image moved between hosts, or a mixed-hardware fleet. A build that bakes in instructions the run host lacks (such as AVX-512) would otherwise crash with `Illegal instruction`.
|
|
81
|
+
|
|
82
|
+
Set `SMARTER_CSV_PERFORMANCE` at install time to trade portability for speed:
|
|
83
|
+
|
|
84
|
+
| Level | Flags added | Portable? | Use when |
|
|
85
|
+
|----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
|
|
86
|
+
| `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
|
|
87
|
+
| `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
|
|
88
|
+
| `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |
|
|
89
|
+
|
|
90
|
+
`tuned` only changes instruction scheduling, never the instruction set, so it stays portable — and it pays off when the build and run hosts share a microarchitecture (the same chip, or a fleet of identical instances). `max` enables host-specific instructions and is the fastest, but a binary built with it can crash on a different CPU. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
SMARTER_CSV_PERFORMANCE=tuned gem install smarter_csv # portable, tuned for this machine's microarchitecture
|
|
94
|
+
SMARTER_CSV_PERFORMANCE=max gem install smarter_csv # fastest, NOT portable — only when you build on the machine you run on
|
|
95
|
+
SMARTER_CSV_PERFORMANCE=tuned bundle install # same, under Bundler
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_csv`.
|
|
99
|
+
|
|
78
100
|
---------------
|
|
79
101
|
|
|
80
102
|
NEXT: [Migrating from Ruby CSV](./migrating_from_csv.md) | UP: [README](../README.md)
|
|
@@ -156,6 +156,39 @@ data = SmarterCSV.process(file,
|
|
|
156
156
|
convert_values_to_numeric: { only: [:quantity, :price] })
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
Scientific notation (e.g. `"1.5e3"`, `"6.022e23"`) is recognized and converted too. 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
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## `decimal_precision`
|
|
164
|
+
|
|
165
|
+
**Default: `:auto`**
|
|
166
|
+
|
|
167
|
+
Controls how decimal values (those with a `.` or an exponent) are converted. Integers are unaffected — they are always returned as `Integer`.
|
|
168
|
+
|
|
169
|
+
| Value | Result |
|
|
170
|
+
|---------------|-----------------------------------------------------------------------------------------|
|
|
171
|
+
| `:auto` | `Float`, unless the value carries more than 16 significant digits — then `BigDecimal`. |
|
|
172
|
+
| `:float` | Always `Float` (correctly rounded; matches `String#to_f`). |
|
|
173
|
+
| `:bigdecimal` | Always `BigDecimal` (full precision). |
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# :auto (default) — keeps full precision only when needed
|
|
177
|
+
SmarterCSV.process(file)
|
|
178
|
+
# "3.14" => 3.14 (Float)
|
|
179
|
+
# "1234567890.123456789" => 0.1234567890123456789e10 (BigDecimal — >16 sig digits)
|
|
180
|
+
|
|
181
|
+
# :float — always Float (faster, may lose precision on long decimals)
|
|
182
|
+
SmarterCSV.process(file, decimal_precision: :float)
|
|
183
|
+
# "1234567890.123456789" => 1234567890.1234567 (Float)
|
|
184
|
+
|
|
185
|
+
# :bigdecimal — always BigDecimal
|
|
186
|
+
SmarterCSV.process(file, decimal_precision: :bigdecimal)
|
|
187
|
+
# "3.14" => 0.314e1 (BigDecimal)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Unlike Ruby's standard-library CSV — whose `:numeric`/`:float` converters use `Float()` and silently lose precision — `:auto` preserves high-precision decimals as `BigDecimal`. Decimal values are decoded on the C path with the Eisel-Lemire algorithm (correctly rounded, identical to `String#to_f`).
|
|
191
|
+
|
|
159
192
|
---
|
|
160
193
|
|
|
161
194
|
## `remove_empty_hashes`
|
data/docs/migrating_from_csv.md
CHANGED
|
@@ -223,6 +223,24 @@ 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; scientific notation (`6.022e23`, `1.6e-19`) is recognized as numeric too.
|
|
227
|
+
|
|
228
|
+
**With Ruby CSV (precision lost):**
|
|
229
|
+
```ruby
|
|
230
|
+
CSV.read('locations.csv', headers: true, converters: :float).first['lat']
|
|
231
|
+
# => -122.42200352825247 ← Float() dropped the last digits of -122.422003528252475
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**With SmarterCSV (full precision kept):**
|
|
235
|
+
```ruby
|
|
236
|
+
SmarterCSV.process('locations.csv').first[:lat]
|
|
237
|
+
# => -0.122422003528252475e3 (BigDecimal — all 18 significant digits preserved)
|
|
238
|
+
|
|
239
|
+
# Force Float everywhere, like-for-like with Ruby CSV:
|
|
240
|
+
SmarterCSV.process('locations.csv', decimal_precision: :float).first[:lat]
|
|
241
|
+
# => -122.42200352825247 (Float)
|
|
242
|
+
```
|
|
243
|
+
|
|
226
244
|
### 3. Empty values are removed by default
|
|
227
245
|
|
|
228
246
|
SmarterCSV drops key/value pairs where the value is `nil` or blank
|
data/docs/options.md
CHANGED
|
@@ -121,7 +121,8 @@ 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 to the appropriate numeric type. Accepts `{except: [:key1, :key2]}` or `{only: :key3}` to limit which columns. |
|
|
124
|
+
| `:convert_values_to_numeric` | `true` | Convert strings containing integers or floats (including scientific notation like `1.5e3`) to the appropriate numeric type. Accepts `{except: [:key1, :key2]}` or `{only: :key3}` to limit which columns. |
|
|
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. |
|
|
125
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). |
|
|
126
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?`. |
|
|
127
128
|
| `:remove_zero_values` | `false` | Remove key/value pairs whose value is zero — numeric `0` / `0.0`, or any textual form of zero (`"0"`, `"0.0"`, `"00.00"`, `"+0"`, `"-0.0"`, …). |
|
data/docs/upgrade_path.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "1.
|
|
3
|
-
"latest_release": "1.
|
|
2
|
+
"latest": "1.18",
|
|
3
|
+
"latest_release": "1.18.0",
|
|
4
4
|
"path": {
|
|
5
5
|
"1.0": {
|
|
6
6
|
"to": "1.1",
|
|
@@ -170,6 +170,17 @@
|
|
|
170
170
|
"to": "1.17",
|
|
171
171
|
"latest_release": "1.16.6",
|
|
172
172
|
"actions": []
|
|
173
|
+
},
|
|
174
|
+
"1.17": {
|
|
175
|
+
"to": "1.18",
|
|
176
|
+
"latest_release": "1.17.4",
|
|
177
|
+
"actions": [
|
|
178
|
+
{
|
|
179
|
+
"if": "you build and run the gem on the same machine (or a fleet of identical CPUs) and want the previous native-optimized build for maximum speed",
|
|
180
|
+
"then": "set <code>SMARTER_CSV_PERFORMANCE=max</code> (or <code>tuned</code>) at install time — 1.18.1 builds <strong>portable</strong> by default (no host-specific CPU instructions) to fix an <code>Illegal instruction</code> crash when a binary built on one CPU is run on another (<a href=\"https://github.com/tilo/smarter_csv/issues/343\">#343</a>). The default is safe everywhere; the env var opts back into host optimization."
|
|
181
|
+
}
|
|
182
|
+
],
|
|
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."
|
|
173
184
|
}
|
|
174
185
|
}
|
|
175
186
|
}
|
data/docs/upgrade_wizard.html
CHANGED
|
@@ -94,6 +94,15 @@ input[type="text"] {
|
|
|
94
94
|
color: var(--green);
|
|
95
95
|
font-weight: 600;
|
|
96
96
|
}
|
|
97
|
+
.benefit {
|
|
98
|
+
background: var(--green-soft);
|
|
99
|
+
border: 1px solid #b6dab6;
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
padding: 1em 1.25em;
|
|
102
|
+
margin-bottom: 1em;
|
|
103
|
+
color: var(--fg);
|
|
104
|
+
}
|
|
105
|
+
.benefit strong { color: var(--green); }
|
|
97
106
|
.check {
|
|
98
107
|
padding: 0.85em 0;
|
|
99
108
|
border-top: 1px solid var(--border);
|
|
@@ -229,7 +238,7 @@ function renderHop(series, originalVersion) {
|
|
|
229
238
|
if (hop.actions.length === 0) {
|
|
230
239
|
body = `<div class="hop dropin">
|
|
231
240
|
You can upgrade directly to version ${targetRelease}. No changes needed.
|
|
232
|
-
</div
|
|
241
|
+
</div>${hop.note ? `<div class="benefit">${hop.note}</div>` : ""}`;
|
|
233
242
|
} else {
|
|
234
243
|
body = `<div class="hop">
|
|
235
244
|
${hop.actions.map((a, i) => `
|
|
@@ -324,6 +333,7 @@ function renderHop(series, originalVersion) {
|
|
|
324
333
|
from: series,
|
|
325
334
|
to: hop.to,
|
|
326
335
|
dropIn: hop.actions.length === 0,
|
|
336
|
+
note: hop.note || null,
|
|
327
337
|
matched
|
|
328
338
|
});
|
|
329
339
|
|
|
@@ -377,7 +387,8 @@ function renderSummary() {
|
|
|
377
387
|
const targetRelease = latestReleaseFor(d.to);
|
|
378
388
|
const heading = `<p class="summary-hop-heading"><strong>${d.from}.x → ${targetRelease}</strong></p>`;
|
|
379
389
|
if (d.dropIn) {
|
|
380
|
-
|
|
390
|
+
const noteHTML = d.note ? `<p>${d.note}</p>` : "";
|
|
391
|
+
return `<div class="summary-hop">${heading}<p class="muted">No code changes needed for this step.</p>${noteHTML}</div>`;
|
|
381
392
|
}
|
|
382
393
|
if (d.matched.length === 0) {
|
|
383
394
|
return `<div class="summary-hop">${heading}<p class="muted">None of the conditions in this step applied to your code.</p></div>`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmarterCSV
|
|
4
|
+
# Pure (mkmf-free) selection of CPU-optimization flags from the
|
|
5
|
+
# SMARTER_CSV_PERFORMANCE environment variable. Kept separate from extconf.rb
|
|
6
|
+
# so the logic can be unit-tested without invoking a compiler.
|
|
7
|
+
#
|
|
8
|
+
# Levels:
|
|
9
|
+
# portable - no host-specific flags. The binary runs on any CPU of the same
|
|
10
|
+
# architecture. The safe default: a binary built here will not
|
|
11
|
+
# crash with "Illegal instruction" on an older/different CPU.
|
|
12
|
+
# tuned - -mtune=native: tunes instruction scheduling for the build host's
|
|
13
|
+
# microarchitecture WITHOUT changing the instruction set, so the
|
|
14
|
+
# binary stays portable. A real win when build and run hosts share
|
|
15
|
+
# a microarchitecture (same chip or a homogeneous fleet).
|
|
16
|
+
# max - host-specific instructions: -march=native, or -mcpu=native on
|
|
17
|
+
# ARM/Clang where -march=native is rejected. Fastest, but NOT
|
|
18
|
+
# portable -- may crash on a CPU lacking the build host's
|
|
19
|
+
# instructions. Use only when build host and run host match.
|
|
20
|
+
#
|
|
21
|
+
# `accepts` is a predicate (in the real build, a wrapper over mkmf's
|
|
22
|
+
# try_compile) returning true when the compiler accepts a given flag; each
|
|
23
|
+
# candidate is probed so an unsupported flag is skipped rather than breaking
|
|
24
|
+
# the build.
|
|
25
|
+
module CpuFlags
|
|
26
|
+
LEVELS = %w[portable tuned max].freeze
|
|
27
|
+
|
|
28
|
+
# Candidate flags per level, in preference order. The first one the compiler
|
|
29
|
+
# accepts wins. `max` degrades march -> mcpu -> mtune; tuned only ever
|
|
30
|
+
# considers -mtune=native (never an instruction-set flag).
|
|
31
|
+
CANDIDATES = {
|
|
32
|
+
'portable' => [].freeze,
|
|
33
|
+
'tuned' => ['-mtune=native'].freeze,
|
|
34
|
+
'max' => ['-march=native', '-mcpu=native', '-mtune=native'].freeze,
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Returns a Hash: { level: String, flags: Array<String>, warning: String|nil }.
|
|
38
|
+
def self.select(raw_level, accepts:)
|
|
39
|
+
level, warning = normalize(raw_level)
|
|
40
|
+
chosen = CANDIDATES[level].find { |flag| accepts.call(flag) }
|
|
41
|
+
{ level: level, flags: chosen ? [chosen] : [], warning: warning }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Normalizes the env value to a known level. Unknown values fall back to
|
|
45
|
+
# 'portable' (a typo can then only ever be slower, never non-portable) and
|
|
46
|
+
# return a warning naming the bad value and the fallback.
|
|
47
|
+
def self.normalize(raw_level)
|
|
48
|
+
value = raw_level.to_s.strip.downcase
|
|
49
|
+
return ['portable', nil] if value.empty?
|
|
50
|
+
return [value, nil] if LEVELS.include?(value)
|
|
51
|
+
|
|
52
|
+
['portable', "SMARTER_CSV_PERFORMANCE=#{raw_level.inspect} is not one of #{LEVELS.join('|')}; using 'portable'."]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/ext/smarter_csv/extconf.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'mkmf'
|
|
4
4
|
require "rbconfig"
|
|
5
|
+
require_relative 'cpu_flags'
|
|
5
6
|
|
|
6
7
|
if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O3")
|
|
7
8
|
fixed_CFLAGS = RbConfig::MAKEFILE_CONFIG["CFLAGS"].sub("-g -O3", "-O3 $(cflags)")
|
|
@@ -9,11 +10,31 @@ if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O3")
|
|
|
9
10
|
RbConfig::MAKEFILE_CONFIG["CFLAGS"] = fixed_CFLAGS
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
# Probe whether the compiler accepts a flag by compiling a trivial program with
|
|
14
|
+
# it. Lets us skip flags the toolchain rejects (e.g. -march=native on Clang/ARM,
|
|
15
|
+
# or GCC-only flags on MSVC) instead of breaking the build. Replaces the old
|
|
16
|
+
# RUBY_PLATFORM string guesses: ask the actual compiler, don't infer from the OS.
|
|
17
|
+
def compiler_accepts?(flag)
|
|
18
|
+
try_compile("int main(void){return 0;}", flag)
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
optflags = "-O3 -flto -fomit-frame-pointer -DNDEBUG".dup
|
|
13
|
-
|
|
22
|
+
|
|
23
|
+
# CPU optimization level, set via SMARTER_CSV_PERFORMANCE (default: portable).
|
|
24
|
+
# See cpu_flags.rb for the full description of each level.
|
|
25
|
+
#
|
|
26
|
+
# portable (default) - no host-specific flags; runs on any CPU of the same arch.
|
|
27
|
+
# tuned - -mtune=native; host scheduling tuning, still portable.
|
|
28
|
+
# max - host instruction set (-march/-mcpu native); fastest, but
|
|
29
|
+
# NOT portable -- may crash on a CPU lacking those instructions.
|
|
30
|
+
cpu = SmarterCSV::CpuFlags.select(ENV["SMARTER_CSV_PERFORMANCE"], accepts: method(:compiler_accepts?))
|
|
31
|
+
warn(cpu[:warning]) if cpu[:warning]
|
|
32
|
+
cpu[:flags].each { |flag| optflags << " #{flag}" }
|
|
33
|
+
puts("SmarterCSV performance level: #{cpu[:level]} -- optflags: #{optflags}")
|
|
34
|
+
|
|
14
35
|
# -fno-semantic-interposition: GCC/Clang only (not MSVC). Allows intra-library
|
|
15
36
|
# calls to bypass the PLT on Linux and enables more aggressive LTO inlining.
|
|
16
|
-
optflags << " -fno-semantic-interposition"
|
|
37
|
+
optflags << " -fno-semantic-interposition" if compiler_accepts?("-fno-semantic-interposition")
|
|
17
38
|
|
|
18
39
|
append_cflags('-Wno-compound-token-split-by-macro')
|
|
19
40
|
|