Tip
Upgrading? The SmarterCSV Upgrade Wizard walks you through what (if anything) you need to change for your specific version. Most steps do not require any changes.
- Portable builds by default — fixes the "Illegal instruction" crash on heterogeneous CPUs (#343).
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).
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).
The C extension is now built portable by default (no host-specific instructions). Thanks to @paholg for the report.
SMARTER_CSV_PERFORMANCEbuild option (portabledefault,tuned, ormax)
| Level | Flags added | Portable? | Use when |
|---|---|---|---|
portable (default) |
none | Yes, any CPU of the arch | Build host may differ from run host |
tuned |
-mtune=native |
Yes, instruction scheduling only | Build and run hosts share a microarch |
max |
-march=native, or -mcpu=native on ARM |
No, host instruction optimization | Build host and run host are the same |
See the Introduction for details.
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.
decimal_precisionoption (:autodefault, or:float/:bigdecimal) — controls how decimal values are converted.:autoreturns aFloatunless the value carries more than 16 significant digits, in which case it returns aBigDecimalso no precision is lost;:floatalways returnsFloat;:bigdecimalalways returnsBigDecimal. Integers are unaffected (alwaysInteger). Works identically on the C and Ruby paths. (Ruby's standard-library CSV has no high-precision option — its:numeric/:floatconverters useFloat()and lose precision.)- 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 astrtodfallback beyond that (more than 19 digits / extreme exponents). High-precision values that becomeBigDecimalunder:auto/:bigdecimalare parsed by Ruby'sBigDecimal.
- 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. - 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. - With the default
decimal_precision: :auto, decimal values carrying more than 16 significant digits are now returned asBigDecimalinstead ofFloat. Passdecimal_precision: :floatto keep the previous always-Floatbehavior. bigdecimalis now a runtime dependency (it is no longer a default gem on Ruby 3.4+).
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.
- Eisel-Lemire (Mushtak-Lemire) algorithm on the C path to convert decimals to
FloatorBigDecimal. Numeric-heavy data (many float/decimal columns) parses significantly faster. - SIMD scanner for backslash-escaped quoted fields (C-path), using NEON (arm64) and SSE2 (x86-64) with a scalar fallback. Speeds up
quote_escaping: :backslashparsing of long quoted fields.
| File | C-path | driver |
|---|---|---|
| backslash_long_fields_60k | 1.48× faster (0.1880s → 0.1273s) | SIMD quote/backslash scanner |
| sensor_data_50krows_50cols | 1.40× faster (0.2763s → 0.1975s) | Eisel-Lemire numeric conversion |
- Improved robustness of symbol-valued enum option processing.
- added parity tests for long quoted-field scanning across 16-byte boundaries, running on both the C and Ruby paths.
- added tests for string-to-symbol coercion of the enum options.
- fixed Issue #337:
Pathnameinput no longer worked (regression since 1.17.0); passing aPathnameraisedNoMethodError: private method 'gets' called.SmarterCSVnow opens any path-like input (StringorPathname) and reads directly from any already-open IO. Thanks to Alex Shenia
RSpec tests: 2,274→ 2,277 (+3 tests)
- No functional changes
- added 3 test cases
- DRY-up C-code
- no performance changes on the C-path
-
performance improvement on the Ruby-path
File RB-path PEOPLE_IMPORT_B / PEOPLE_IMPORT_C 13.5% faster tab_separated_60k 13.2% faster sample_100k 10.3% faster multi_char_separator 9.0% faster utf8_multibyte 7.1% faster many_empty_fields 6.7% faster PEOPLE_IMPORT_NC 5.2% faster sensor_data 4.5% faster
RSpec tests: 2,220→ 2,274 (+54 tests)
- fixed Issue #334 with escaped double quote followed by comma. Thanks to conorg
- fixed bug when using
headers: { except: } - added more tests
RSpec tests: 2,210→ 2,220 (+10 tests)
- fixing issue with
remove_empty_hashes: falsenot being honored in accelerated path (does not affect you when you use default settings)
RSpec tests: 1,434 → 2,210 (+776 tests)
-
Streaming IO support — SmarterCSV now works with non-seekable IO sources such as pipes, STDIN, and Zlib streams. A rewindable peek buffer transparently captures the first bytes of the stream so that
row_sepandcol_sepauto-detection can replay them without requiring the underlying source to supportrewindorseek. -
Structured warnings — auto-detection and configuration warnings are now collected on the Reader as a deduped histogram:
reader = SmarterCSV::Reader.new('data.csv') reader.process reader.warnings # => [{ type:, code:, severity:, message:, count: }, ...]
Repeated warnings of the same
(type, code)are deduped —counttracks occurrences. Available codes today::chunk_size_default,:header_a_method,:utf8_missing_binary_mode,:no_clear_row_sep,:no_row_sep_found. -
Class-level
SmarterCSV.warningsaccessor — mirrorsSmarterCSV.errors. Per-thread, cleared at the start of each.process/.parse/.each/.each_chunkcall. Safe under Puma/Sidekiq. -
Rails.logger routing — when
Rails.loggeris present, warnings are routed through it at the severity declared at the call site (:debug/:info/:warn/:error/:fatal); otherwiseKernel#warnis used as a fallback. Detection is cached at construct time, no per-call overhead.
-
Improved auto-detection of
row_sepandcol_sep— giving more accurate results on files with comment headers. -
Larger scan window for accurate row separator detection on files with wide headers or long first lines.
-
guess_line_endingnow scans the input in chunks up to a 64KB hard cap, returning as soon as one separator has a clear majority. Near-tie chunk-boundary artifacts no longer cause spurious warnings; only true ties at the hard cap fall back to"\n"and emit a:no_clear_row_sepwarning at:errorseverity (silent miss-parse risk).
-
buffer_sizeis now a public option — peek buffer chunk size for non-seekable inputs (pipes, gzip readers, HTTP/S3 bodies). Default16_384. Out-of-range values warn and clamp to the supported range rather than raising. -
auto_row_sep_charsdefault changed to4096(was500in 1.16.x). Sized to cover wide-header CSVs in a single read. Bump it higher if your files have very wide headers or long comment preambles.
-
Files ending in a lone
\rare now correctly detected as\r-terminated instead of falling through to a "no clear row separator" warning. -
remove_empty_valuesnow treats Unicode whitespace as empty — a field containing only whitespace, including characters like non-breaking space (U+00A0) or ideographic space (U+3000), is now dropped, the same way Ruby'sString#blank?behaves. Previously only ASCII whitespace counted (and only Rails apps got the Unicode behavior, viablank?— an inconsistency that's now gone). Behavior is identical with or without the C extension. -
remove_zero_valuesnow also removes signed zeros —+0,-0,-0.0,+0.00, etc. are recognized as zero and dropped, just like0and0.0. (Only applies whenremove_zero_values: true, which is off by default.)
Measured against 1.16.4 (Apple M4, Ruby 3.4.7):
- C-accelerated path (the default): quote-heavy, large-field, and wide CSVs parse meaningfully faster — roughly 7–22% faster (city/address-style files ~10–12%; long-field and wide files the most). CSVs with very short lines and many tiny fields are up to ~3% slower — a side effect of the larger default auto-detection scan window (see
auto_row_sep_chars); set it back to a smaller value if that matters for your workload. Net: solid wins where there's real per-row work, a small cost on the trivially-cheap cases. - Ruby fallback path (
acceleration: false): faster on nearly every file — typically 3–20% faster than 1.16.4, with the biggest gains on wide and many-small-field CSVs.
Per-file breakdown: docs/releases/1.17.0/performance_notes.md.
RSpec tests: 1,467 → 1,591 (+124 tests)
- fixed Issue #334 with escaped double quote followed by comma. Thanks to conorg
- fixed bug when using
headers: { except: } - added more tests
- fixing issue with
remove_empty_hashes: falsenot being honored in accelerated path (does not affect you when you use default settings)
RSpec tests: 1,434 → 1,467 (+33 tests)
-
Fixed bug in
SmarterCSV.errorsthat could lose collected records when processing raises mid-stream, e.g. whenbad_row_limit:was exceeded (TooManyBadRows), or when a user's block raised through.process/.each/.each_chunk. -
Fixed
enforce_utf8_encodingincorrectly replacing all non-ASCII bytes when the input string was tagged asASCII-8BIT(binary). The encoding is now relabeled to UTF-8 before transcoding, so only genuinely invalid byte sequences are replaced.
RSpec tests: 1,425 → 1,434 (+9 tests)
-
write_headers: false— newSmarterCSV::Writeroption to suppress the header line when appending rows to an existing CSV file opened in'a'mode. Defaults totrue(existing behavior, fully backwards-compatible).
- Refactor of internal options handling
RSpec tests: 1,410 → 1,425 (+15 tests)
-
Fixed
value_convertersto accept lambdas and Procs in addition to class-based converters. Thanks to Jonas Staškevičius for issue #329. -
Fixed blank header auto-naming to use absolute column position, consistent with extra data column naming.
name,,now producescolumn_2/column_3instead ofcolumn_1/column_2.⚠️ If your code references auto-generated keys for blank headers, update those to use the absolute column position. -
Fixed
Writer: when bothmap_headers:andheader_converter:were used together,map_headerswas silently ignored.map_headersis now applied first, thenheader_converteron top.
RSpec tests: 1,247 → 1,410 (+163 tests)
-
SmarterCSV.errors— class-level error access after anyprocess,parse,each, oreach_chunkcall. Exposes the samereader.errorshash without requiring access to theReaderinstance. Errors are cleared at the start of each call and stored per-thread (safe in Puma/Sidekiq).# Previously — required Reader instance to access errors reader = SmarterCSV::Reader.new('data.csv', on_bad_row: :skip) reader.process puts reader.errors[:bad_row_count] # Now — works with the class-level API too SmarterCSV.process('data.csv', on_bad_row: :skip) puts SmarterCSV.errors[:bad_row_count]
Note:
SmarterCSV.errorsonly surfaces errors from the most recent run on the current thread. In a multi-threaded environment (Puma, Sidekiq), each thread maintains its own error state independently. If you callSmarterCSV.processtwice in the same thread, the second call's errors replace the first's. For long-running or complex pipelines where you need to aggregate errors across multiple files, use the Reader API.
⚠️ Fibers:SmarterCSV.errorsusesThread.currentfor storage, which is shared across all fibers running in the same thread. If you process CSV files concurrently in fibers (e.g. withAsync,Falcon, or manualFiberscheduling),SmarterCSV.errorsmay return stale or wrong results. UseSmarterCSV::Readerdirectly — errors are scoped to the reader instance and are always correct regardless of fiber context.
- fixed #325:
col_sepin quoted headers was handled incorrectly; Thanks to Paho Lurie-Gregg. - fixed issue with quoted numeric fields that were not converted to numeric
- Added 163 tests covering new features and corner cases
Full details · Benchmarks · Performance notes
RSpec tests: 714 → 1,247 (+533 tests)
In short — most users will see incorrect output silently improve. If your CSV files don't contain stray " characters in the middle of unquoted fields, you are not affected. If they do, the new default produces correct output where the old default produced corrupted output.
A new option quote_boundary: controls when a " character marks the start or end of a quoted field versus when it's a literal character inside the field.
quote_boundary: :standard(the new default) — quotes are only recognized as field delimiters at field boundaries (start of a field, or immediately beforecol_sep/ end of line). A"that appears in the middle of an unquoted field is treated as a literal character. This matches RFC 4180 and Ruby's standardCSVlibrary.quote_boundary: :legacy— not recommended. Restores the pre-1.16.0 behavior, where any"could open a quoted region. This is the behavior that produced silently corrupt output on files with stray mid-field quotes; it exists only as an escape hatch for code that built workarounds on top of the buggy output. New code should never use this.
In practice, the old :legacy behavior was silently producing corrupt output whenever a CSV file contained a stray mid-field " — so for most users this change makes output correct where it was wrong before, not the other way around.
- Your CSV files don't contain any
"characters mid-field (the common case). - Your CSV files quote fields cleanly per RFC 4180 (well-formed
"..."around each quoted field, no stray quotes inside other fields).
- Your CSV files contain stray
"characters in the middle of unquoted fields (e.g.5'6",Joe "the Hat" Smithwithout surrounding quotes), and you had downstream code that compensated for the previously-corrupted parse output.
For almost everyone: do nothing. Upgrade and observe that the output is the same or more correct.
The quote_boundary: :legacy option exists only as a short-term escape hatch — we do not advise using it, because it re-enables the buggy parse behavior that motivated this change. If your code built workarounds on top of the previously-corrupted output, the right fix is to remove those workarounds and rely on the new :standard behavior, not to opt back into the bug:
# Only as a temporary escape hatch — not recommended for new code:
SmarterCSV.process('file.csv', quote_boundary: :legacy)See Parsing Strategy for details on how each mode handles edge cases.
- 1.8×–8.6× faster than Ruby
CSV.read(raw tokenization only; no post-processing) - 7×–129× faster than Ruby
CSV.table(nearest equivalent output) - up to 2.4× faster for accelerated path vs 1.15.2 (15/19 benchmark files faster)
- up to 2× faster for Ruby path vs 1.15.2
- 9×–65× faster for accelerated path vs 1.14.4
Measured on 19 benchmark files, Apple M1, Ruby 3.4.7. See benchmarks.
SmarterCSV.parse(csv_string, options): can now parse a CSV string directly. See Migrating from Ruby CSV.SmarterCSV.each/Reader#each: row-by-row enumerator;Readernow includesEnumerable.SmarterCSV.each_chunk/Reader#each_chunk: chunked enumerator yielding(Array<Hash>, chunk_index).
on_bad_row:— bad row quarantine::skip,:collect,:raise, or callable. See Bad Row Quarantine.bad_row_limit: N— raisesSmarterCSV::TooManyBadRowsafter N bad rows.collect_raw_lines:(default:true) — include raw line in bad-row error records.field_size_limit: N— cap field size in bytes; prevents DoS from unclosed quotes. RaisesSmarterCSV::FieldSizeLimitExceeded.headers: { only: [...] }/headers: { except: [...] }— column selection; excluded columns skipped in C hot path. See Column Selection.nil_values_matching:— replaces deprecatedremove_values_matching:.missing_headers:(default::auto) — replaces deprecatedstrict:.verbose: :quiet/:normal/:debug— replaces deprecatedverbose: true/false.on_start:/on_chunk:/on_complete:— instrumentation hooks. See Instrumentation.
- IO/StringIO support:
SmarterCSV.generateandWriter.newnow accept anyIO-compatible object. See Write API. SmarterCSV.generatereturns a String when called without a destination argument.- Streaming mode: when
headers:ormap_headers:is provided upfront, Writer skips the temp file and streams directly. encoding:/write_nil_value:/write_empty_value:/write_bom:— new writer options.
remove_values_matching:→ usenil_values_matching:strict:→ usemissing_headers: :raise/:autoverbose: true/false→ useverbose: :debug/:normal
- Empty headers (#324, #312): empty/whitespace-only header fields now auto-generate names via
missing_header_prefix. - All library output now goes to
$stderr— nothing written to$stdout. SmarterCSV.generateraisesArgumentError(not blankRuntimeError) when called without a block.- Writer temp file no longer hardcoded to
/tmp(fixes Windows); properly cleaned up withTempfile#close!. - Writer
StringIO:finalizeno longer attempts to close a caller-ownedStringIO.
- fixing issue with
remove_empty_hashes: falsenot being honored in accelerated path (does not affect you when you use default settings)
- 1.6× to 7.2× faster than CSV.read
- 6× to 113× faster than Ruby’s CSV.table
- 5.4× to 37.4× faster than SmarterCSV 1.14.4 (with C-acceleration)
- 1.4× to 9.5× faster than SmarterCSV 1.14.4 (without C-acceleration, pure Ruby path)
- Fix for quoted fields ending with backslash (issue #316, issue #252): Since v1.8.5, SmarterCSV unconditionally treated
\"as an escaped quote, which causedMalformedCSVorEOFErrorfor CSV files containing literal backslashes in quoted fields (e.g. Windows paths like"C:\Users\").
- New option
quote_escaping: Controls how quotes are escaped inside quoted fields. Default::auto. See Parsing Strategy for details.:auto(default): Tries backslash-escape interpretation first, falls back to RFC 4180 if parsing fails. This handles both conventions automatically without breaking existing data.:double_quotes(RFC 4180): Only doubled quotes ("") escape a quote character. Backslash is always literal.:backslash(MySQL/Unix):\"is treated as an escaped quote.
-
Dropping support for Ruby 2.5
-
Performance Optimizations
- 39% less memory allocated
- 43% fewer objects created
- ~5× faster at P90 vs SmarterCSV 1.14.4
- ~3–7× faster at P90 vs Ruby CSV
-
Chunk index in block processing: When using block-based processing, an optional second parameter
chunk_indexis now passed to the block. This 0-based index is useful for progress tracking and debugging. The change is backwards compatible - existing code continues to work.SmarterCSV.process(file, chunk_size: 100) do |chunk, chunk_index| puts "Processing chunk #{chunk_index}..." Model.import(chunk) end
-
MissingKeys#keys- programmatic access to missing keys without parsing error messages (PR #314, thanks to Skye Shaw) -
DuplicateHeaders#headers- programmatic access to duplicate headers without parsing error messages# Example: accessing missing keys programmatically rescue SmarterCSV::MissingKeys => e e.keys # => [:employee_id, :department] end # Example: accessing duplicate headers programmatically rescue SmarterCSV::DuplicateHeaders => e e.headers # => [:email] end
-
New
parse_line_to_hash_cfunction: Builds Ruby hash directly during parsing, eliminating intermediate array allocations. Previously, parsing created a values array, thenzip()created pairs array, thento_h()built the hash. Now done in a single pass. -
Shared empty string optimization: Reuses a single frozen empty string for all empty CSV fields, reducing object allocations and GC pressure.
-
Faster quote counting: New
count_quote_chars_cfunction replaces Ruby'seach_chariteration, eliminating one String object allocation per character. -
Conditional nil padding: Missing columns only padded with
nilwhenremove_empty_values: false, avoiding unnecessary work in the default case.
-
Frozen regex constants: Numeric conversion patterns (
FLOAT_REGEX,INTEGER_REGEX,ZERO_REGEX) are now pre-compiled and frozen, eliminating millions of regex compilations for large files. This alone reduced numeric conversion overhead from +75% to +4%. -
In-place hash modification: Hash transformations now modify hashes in-place instead of creating copies, reducing memory allocations by 39% and object count by 43%.
Benchmarks using Ruby 3.4.7 on M1 Apple Silicon. All times in seconds.
Summary:
| Comparison | Range | Comments | P90 |
|---|---|---|---|
| vs SmarterCSV 1.14.4 | 2.6x - 3.5x faster | up to 20.5x for some | ~5x |
| vs CSV hashes | 1.9x - 3.8x faster | up to 6.7x for some | ~3x |
| vs CSV.table | 4.3x - 10.1x faster | up to 12.0x for some | ~7..8x |
P90 measured over the full set of benchmarked files
These gains come while returning fully usable hashes with conversions, not raw arrays that require post-processing.
Memory improvements: 39% less memory allocated, 43% fewer objects created
vs SmarterCSV 1.14.4:
| File | Size | Rows | 1.14.4 | 1.15.0 | Speedup |
|---|---|---|---|---|---|
| worldcities.csv | 5 MB | 48K | 1.27s | 0.49s | 2.6x |
| LANDSAT_ETM_C2_L1_50k.csv | 31 MB | 50K | 6.73s | 1.99s | 3.4x |
| PEOPLE_IMPORT.csv | 62 MB | 50K | 8.43s | 2.43s | 3.5x |
| wide_500_cols_20k.csv | 98 MB | 20K | 19.38s | 5.09s | 3.8x |
| long_fields_20k.csv | 22 MB | 20K | 3.05s | 0.15s | 20.5x |
| embedded_newlines_20k.csv | 1.5 MB | 20K | 0.59s | 0.12s | 5.1x |
vs Ruby CSV 3.3.5:
For an apples-to-apples comparison, we must compare parsers that return the same result structure and perform comparable work. SmarterCSV returns an array of hashes with symbol keys and type conversion applied, so raw CSV array parsing is not a fair comparison.
Beware of comparisons that focus solely on raw CSV parsing. Such benchmarks measure only tokenization, while real-world usage still requires substantial post-processing to produce usable data. Leaving this work out -- hash construction, normalization, type conversion, and edge-case handling to produce usable data -- consistently understates the actual cost of CSV ingestion.
For this reason, CSV.table is the closest equivalent to SmarterCSV.
| File | Size | Rows | CSV hashes | CSV.table | 1.15.0 | vs hashes | vs table |
|---|---|---|---|---|---|---|---|
| worldcities.csv | 5 MB | 48K | 1.06s | 2.12s | 0.49s | 2.2x | 4.3x |
| LANDSAT_ETM_C2_L1_50k.csv | 31 MB | 50K | 3.85s | 9.25s | 1.99s | 1.9x | 4.7x |
| PEOPLE_IMPORT.csv | 62 MB | 50K | 9.10s | 24.39s | 2.43s | 3.8x | 10.1x |
| wide_500_cols_20k.csv | 98 MB | 20K | 34.24s | 61.24s | 5.09s | 6.7x | 12.0x |
| long_fields_20k.csv | 22 MB | 20K | 0.34s | 0.81s | 0.15s | 2.3x | 5.5x |
| whitespace_heavy_20k.csv | 3.3 MB | 20K | 0.30s | 0.83s | 0.12s | 2.5x | 7.0x |
CSV hashes = CSV.read(file, headers: true).map(&:to_h) (string keys, no conversion, still requires post-processing)
CSV.table = CSV.table(file).map(&:to_h) (symbol keys + numeric conversion, still requires post-processing)
worldcities.csv is from here
- Fix compilation error on ARM macOS (
-march=nativeunsupported) (PR #313, thanks to Skye Shaw) - CI improvements: Ruby 3.4 support, Codecov action update (PR #311, thanks to Mark Bumiller)
- Bugfix: SmarterCSV::Reader fixing issue with header containing spaces (PR 305 thanks to Felipe Cabezudo)
- Improved C-extension parsing logic:
- Added fast path for unquoted fields to avoid unnecessary quote checks.
- Aded inline whitespace stripping inside the C parser
- Performance
- Significantly reduced per-line overhead in non-quoted, wide CSVs (e.g. fixed-width data exports).
- Benchmarks show ~10–40% speedup over v1.14.2 depending on structure and quoting.
- bugfix: SmarterCSV::Writer fixing corner case with
quote_headers: true(issue 301) - new option:
header_converterallows to programatically modify the headers
- bugfix: SmarterCSV::Writer empty hash results in a blank line (issue 299)
- bugfix: SmarterCSV::Writer need to automatically quote problematic headers (issue #300)
- new option:
quote_headersallows to explicitly quote all headers
- adding advanced configuration options for writing CSV files. (issue 297 thanks to Robert Reiz, issue 296)
- fix bug with SmarterCSV.generate with
force_quotes: true(issue 294)
This release flipped three defaults so that SmarterCSV no longer silently loses data in three specific edge cases. For most users this is a quiet improvement — files that used to lose rows or columns silently now parse correctly with no code changes. Each change below has a short "affected if / not affected if" so you can skip past it quickly.
The motivation for all three changes is the same: data loss should never be silent. Either parse it correctly, or raise loudly.
(Thanks to James Fenley, issue #284.)
If a CSV row had more columns than the header (e.g. header has 6 columns, a row has 8), the extras used to be silently dropped. As of 1.13.0 they survive as :column_7, :column_8, etc.
- Your CSV files have exactly as many columns per row as headers (the common case).
- Your CSV files have rows with extra columns past the header and your code expects only the header-listed keys.
If you want the old "ignore extras" behavior, drop the extra keys yourself. If you want loud failure instead, use the strict mode:
# Raise SmarterCSV::MalformedCSV on extra columns:
SmarterCSV.process('file.csv', strict: true)(In 1.16.0 this option was renamed to missing_headers: :raise, but strict: true still works.)
(Thanks to Simon Rentzke, James Fenley, Randall B, and Matthew Kennedy. Issues #283, #288.)
Files with an unbalanced quote_char (an opening " with no matching close) used to parse to corrupted output. As of 1.13.0 they raise SmarterCSV::MalformedCSV.
- Your CSV files have well-formed quotes (the common case).
- Some of your input files have unbalanced quotes and you used to silently live with the garbled output.
If you need to keep processing other files even when one is malformed, rescue the new exception:
begin
SmarterCSV.process('file.csv')
rescue SmarterCSV::MalformedCSV => e
warn "Skipping malformed file: #{e.message}"
end(Issue #282.)
This one fixes a quiet footgun: if you passed user_provided_headers: and the file had no header row, SmarterCSV used to treat the first data row as a header and silently drop it. As of 1.13.0, setting user_provided_headers: automatically sets headers_in_file: false, so the first row is treated as data — which is what you almost always wanted.
- You don't use
user_provided_headers:. - You use
user_provided_headers:with files that have no header line (the common case — that's what the option is for).
- You pass
user_provided_headers:and your CSV file does have a header line that needs to be skipped.
If your file has a header line and you're overriding it with user_provided_headers:, add headers_in_file: true explicitly so the existing header line is skipped:
# File has a header row that you want to override:
SmarterCSV.process(
'file.csv',
user_provided_headers: [:id, :name, :email],
headers_in_file: true, # skip the header row in the file
)Without headers_in_file: true, you will get an extra hash at the top of your results containing the file's original header strings as values — that's the symptom to look for.
- Improved documentation for handling numeric columns with leading zeroes (e.g. ZIP codes). Use
convert_values_to_numeric: { except: [:zip] }to keep that column as a string. (Available since 1.10.x.) Thanks to David Moles, issue #151.
- Improved column separator detection by ignoring quoted sections #276 (thanks to Nicolas Castellanos)
-
Added Thread-Safety: added SmarterCSV::Reader to process CSV files in a thread-safe manner (issue #277)
-
SmarterCSV::Writer changed default row separator to the system's row separator (
\non Linux,\r\non Windows) -
added a doc tree
-
POTENTIAL ISSUE:
Version 1.12.x has a change of the underlying implementation of
SmarterCSV.process(file_or_input, options, &block). Underneath it now uses this interface:reader = SmarterCSV::Reader.new(file_or_input, options) # either simple one-liner: data = reader.process # or block format: data = reader.process do # do something here endIt still supports calling
SmarterCSV.processfor backwards-compatibility, but it no longer provides access to the internal state, e.g. raw_headers.SmarterCSV.raw_headers->reader.raw_headersSmarterCSV.headers->reader.headersIf you need these features, please update your code to create an instance of
SmarterCSV::Readeras shown above.
- fixing missing errors definition
- improved behavior of Writer class
- added SmarterCSV.generate shortcut for CSV writing
- added SmarterCSV::Writer to output CSV files (issue #44)
- fixed issue when frozen options are handed in (thanks to Daniel Pepper)
- cleaned-up rspec tests (thanks to Daniel Pepper)
- fixed link in README (issue #251)
- improve error message for missing keys
- fix incorrect warning about UTF-8 (issue #268, thanks hirowatari)
Two small behavior changes plus performance and memory improvements. Most users are not affected. Read on for who needs to look closer.
Change 1 (Improvement): user_provided_headers: is now taken literally (no transformations, no duplicates)
In short — if you use user_provided_headers:, write the list in the exact form you want the result keys (all symbols or all strings), and make sure there are no duplicates. For most users this is already what you were doing.
Before 1.10.0, any list you passed as user_provided_headers: was run through the same header pipeline as in-file headers — strings_as_keys could flip strings to symbols, etc. Duplicates were silently accepted. As of 1.10.0, the list is used literally: no transformations are applied, and duplicates raise SmarterCSV::DuplicateHeaders.
This is almost always what people actually wanted: if you're explicitly listing the headers, you want those headers, not a transformed version of them.
- You don't use
user_provided_headers:. - Your
user_provided_headers:list is already in the form you want (all symbols or all strings, no duplicates). In these cases, you can just upgrade without any code changes.
- You pass
user_provided_headers:and relied onstrings_as_keys:to flip between string/symbol keys. - You pass
user_provided_headers:and had accidental duplicates in the list that the library used to silently accept (this case would be very odd).
# If you want symbol keys, write symbols directly:
SmarterCSV.process('file.csv', user_provided_headers: [:id, :name, :email])
# If you want string keys, write strings directly:
SmarterCSV.process('file.csv', user_provided_headers: ['id', 'name', 'email'])Drop any strings_as_keys: option you used alongside user_provided_headers: — it's ignored in that case now.
If you see SmarterCSV::DuplicateHeaders after upgrading, your list has a repeat in it — fix the duplicate and you're done.
In short — if your input CSV has duplicate column headers, they now Just Work instead of colliding. If your files don't have duplicate headers, you are not affected.
duplicate_header_suffix: used to default to nil. Now it defaults to '' (empty string), which means a file with headers like name,name,name becomes keys name, name2, name3 automatically — no more silently overwriting earlier columns.
- You depended on SmarterCSV raising or failing fast when a CSV has duplicate headers (e.g. as a data-quality check at the boundary of your pipeline).
- Your CSVs don't have duplicate headers.
- You already explicitly set
duplicate_header_suffix:in your code.
If you want the old strict behavior, set the option explicitly to nil:
SmarterCSV.process('file.csv', duplicate_header_suffix: nil)- Performance and memory improvements
- Internal code refactor
- raise SmarterCSV::IncorrectOption when
user_provided_headersare empty - code refactor / no functional changes
- added test cases
- fixed bug with '\' at end of line (issue #252, thanks to averycrespi-moz)
- fixed require statements (issue #249, thanks to PikachuEXE, courtsimas)
- yanked
- no functional changes
- refactored directory structure
- re-added JRuby and TruffleRuby to CI tests
- no C-accelleration for JRuby
- refactored options parsing
- code coverage / rubocop
-
fixed issue #139
-
Error
SmarterCSV::MissingHeaderswas renamed toSmarterCSV::MissingKeys -
CHANGED BEHAVIOR: When
key_mappingoption is used. (issue #139) Previous versions just printed an error message when a CSV header was missing during key mapping. Versions >= 1.9 will throwSmarterCSV::MissingHeaderslisting all headers that were missing during mapping. -
Notable details for
key_mappingandrequired_headers:key_mappingis applied to the headers early on duringSmarterCSV.process, and raises an error if a header in the input CSV file is missing, and we can not map that header to its desired name.
Mapping errors can be surpressed by using:
-
silence_missing_keysset totrue, which silence all such errors, making all headers for mapping optional. -
silence_missing_keysgiven an Array with the specific header keys that are optional The use case is that some header fields are optional, but we still want them renamed if they are present. -
required_headerschecks which headers are present afterkey_mappingwas applied.
- fix parsing of escaped quote characters (thanks to JP Camara)
- fix gem loading issue (issue #232, #234)
- bugfix: windows one-column files were raising NoColSepDetected (issue #229)
- bugfix: do not raise
NoColSepDetectedfor CSV files with only one column in most cases (issue #222) If the first lines contain non-ASCII characters, and no col_sep is detected, it will still raiseNoColSepDetected
- added validation against invalid values for :col_sep, :row_sep, :quote_char (issue #216)
- deprecating
required_headersand replace withrequired_keys(issue #140) - fixed issue with require statement
-
NEW DEFAULTS:
col_sep: :auto,row_sep: :auto. Fully automatic detection by default.MAKE SURE to rescue
NoColSepDetectedif your CSV files can have unexpected formats, e.g. from users uploading them to a service, and handle those cases. -
ignore Byte Order Marker (BOM) in first line in file (issues #27, #219)
- improved guessing of the column separator, thanks to Alessandro Fazzi
- new option :silence_missing_keys; if set to true, it ignores missing keys in
key_mapping
- new option :with_line_numbers; if set to true, it adds :csv_line_number to each data hash (issue #130)
- bugfix for issue #195 #197 #200 which only appeared when called from Rails (thanks to Viacheslav Markin, Nicolas Rodriguez)
- added native code to accellerate line parsing by >10x over 1.6.0
- added option
acceleration, defaulting totrue, to enable native code. Disable this option to use the ruby code for line parsing. - increased test coverage to 100%
- rubocop changes
- fixed compiling
- rubocop changes
- published pre-release
- added native code to accellerate line parsing by >10x over 1.6.0
- added option
acceleration, defaulting totrue, to enable native code. Disable this option to use the ruby code for line parsing. - increased test coverage to 100%
- unused keys in
key_mappingnow generate a warning, no longer raise an exception This is preferable whenkey_mappingis done defensively for variabilities in the CSV files.
- completely rewrote line parser
- added methods
SmarterCSV.raw_headersandSmarterCSV.headersto allow easy examination of how the headers are processed.
- added missing keys to the SmarterCSV::KeyMappingError exception message #189 (thanks to John Dell)
- added raising of
KeyMappingErrorifkey_mappingrefers to a non-existent key - added option
duplicate_header_suffix(thanks to Skye Shaw) When given a non-nil string, it uses the suffix to append numbering 2..n to duplicate headers. If your code will need to process arbitrary CSV files, please setduplicate_header_suffix.
-
fixed bug with trailing col_sep characters, introduced in 1.4.0
-
Fix deprecation warning in Ruby 3.0.3 / $INPUT_RECORD_SEPARATOR (thanks to Joel Fouse )
-
changed default for
comment_regexpto benilfor a safer default behavior (thanks to David Lazar) Note This no longer assumes that lines starting with#are comments. If you want to treat lines starting with '#' as comments, usecomment_regexp: /\A#/
- fixed issue with simplecov
- minor fix: also support
col_sep: :auto - added simplecov
- dropped GPL license, smarter_csv is now only using the MIT License
- added experimental option
col_sep: 'autoto auto-detect the column separator (issue #183) The default behavior is still to assume,is the column separator. - fixed buggy behavior when using
remove_empty_values: false(issue #168) - fixed Ruby 3.0 deprecation
In short — if you use key_mapping:, this is a one-character fix per mapping. If you don't use key_mapping:, you are not affected.
Previously, the values in a key_mapping: hash were silently coerced to symbols, so 'new_name' and :new_name produced the same result key. As of 1.3.0, the values are used as-is — strings stay strings, symbols stay symbols. This gives you direct control over whether the result hashes use string or symbol keys.
- You don't use
key_mapping:. - Your
key_mapping:already uses symbol values (e.g.:new_name). - Your downstream code already reads result hashes with string keys. In these cases, you can just upgrade without any code changes.
- You pass
key_mapping:toSmarterCSV.process(orprocess_csvin older code), and - The values in that hash are strings (e.g.
'new_name', not:new_name), and - Your downstream code reads the result hashes with symbol keys (e.g.
row[:new_name]). This needs a small code-change
Pick whichever is the smaller diff in your code:
# Option A — keep symbol keys in the result (one extra colon per line):
SmarterCSV.process('file.csv', key_mapping: { 'Old Header' => :new_name })
# ^ add the colon
# Option B — switch your reads to string keys:
row['new_name'] # instead of row[:new_name]That's the whole migration. Everything else in 1.3.0 is source-compatible with 1.2.x.
- fix bug for key_mappings (issue #181)
The values of the
key_mappingshash will now be used "as is", and no longer forced to be symbols
- fix deprecation warnings on Ruby 2.7 (thank to Diego Salido)
- fixing error caused by calling f.close when we do not hand in a file
- fixing issue #136 with comments in CSV files
- fixing error class hierarchy
- using Rails blank? if it's available
- fixed regression / test
- fuxed quote_char interpolation for headers, but not data (thanks to Colin Petruno)
- bugfix (thanks to Joshua Smith for reporting)
- add default validation that a header can only appear once; raises
SmarterCSV::DuplicateHeaderswhen it doesn't - add option
required_headers
- fix issue with invalid byte sequences in header (issue #103, thanks to Dave Myron)
- fix issue with invalid byte sequences in multi-line data (thanks to Ivan Ushakov)
- analyze only 500 characters by default when
:row_sep => :autois used. added optionrow_sep_auto_charsto change the default if necessary. (thanks to Matthieu Paret)
- fixing UTF-8 related bug which was introduced in 1.1.2 (thanks to Tirdad C.)
- added warning when options indicate UTF-8 processing, but input filehandle is not opened with r:UTF-8 option
- added option
invalid_byte_sequence(thanks to polycarpou) - added comments on handling of UTF-8 encoding when opening from File vs. OpenURI (thanks to KevinColemanInc)
- added option to
skip_lines(thanks to wal) - added option to
force_utf8encoding (thanks to jordangraft) - bugfix if no headers in input data (thanks to esBeee)
- ensure input file is closed (thanks to waldyr)
- improved verbose output (thankd to benmaher)
- improved documentation
- added feature :value_converters, which allows parsing of dates, money, and other things (thanks to Raphaël Bleuse, Lucas Camargo de Almeida, Alejandro)
- added error if :headers_in_file is set to false, and no :user_provided_headers are given (thanks to innhyu)
- added support to convert dashes to underscore characters in headers (thanks to César Camacho)
- fixing automatic detection of \r\n line-endings (thanks to feens)
- added option :keep_original_headers to keep CSV-headers as-is (thanks to Benjamin Thouret)
- added support for multi-line fields / csv fields containing CR (thanks to Chris Hilton) (issue #31)
- added option to set :row_sep to :auto , for automatic detection of the row-separator (issue #22)
- :convert_values_to_numeric option can now be qualified with :except or :only (thanks to Hugo Lepetit)
- removed deprecated
process_csvmethod
- new option:
- :remove_unmapped_keys to completely ignore columns which were not mapped with :key_mapping (thanks to Dave Sanders)
- added GPL-2 and MIT license to GEM spec file; if you need another license contact me
- added RSpec tests
- bugfix : fixed issue #18 - fixing issue with last chunk not being properly returned (thanks to Jordan Running)
- added RSpec tests
- bugfix : fixed issue #14 - passing options along to CSV.parse (thanks to Marcos Zimmermann)
- bugfix : fixed issue #13 with negative integers and floats not being correctly converted (thanks to Graham Wetzler)
- bugfix : fixed issue with nil values in inputs with quote-char (thanks to Félix Bellanger)
- new options:
- :force_simple_split : to force simiple splitting on :col_sep character for non-standard CSV-files. e.g. without properly escaped :quote_char
- :verbose : print out line number while processing (to track down problems in input files)
- allowing process to work with objects with a 'readline' method (thanks to taq)
- added options:
- :file_encoding : defaults to utf8 (thanks to MrTin, Paxa)
- bugfix : quoted fields are now correctly parsed
- bugfix : for :headers_in_file option
- renamed the following options:
- :strip_whitepace_from_values => :strip_whitespace - removes leading/trailing whitespace from headers and values
- added the following options:
- :strip_whitepace_from_values - removes leading/trailing whitespace from values
- added more options for dealing with headers:
- :user_provided_headers ,user provided Array with header strings or symbols, to precisely define what the headers should be, overriding any in-file headers (default: nil)
- :headers_in_file , if the file contains headers as the first line (default: true)
-
added the following options:
- :downcase_header
- :strings_as_keys
- :remove_zero_values
- :remove_values_matching
- :remove_empty_hashes
- :convert_values_to_numeric
-
renamed the following options:
- :remove_empty_fields => :remove_empty_values
- renamed
SmarterCSV.process_csvtoSmarterCSV.process.