Exemple
csv 3.3.2: Handle custom converter registration in CSV::Converters with CSV::FieldInfo metadata, non-String converter short-circuiting, skip_lines filtering, and CSV.filter stream transformation in CSV 3.3.2
Échantillon vérifié pour gem csv 3.3.2: Handle custom converter registration in CSV::Converters with CSV::FieldInfo metadata, non-String converter…
sha256:8a8545c284e4585374983e887167cea95e9e8ba4e7e067eb4b7dd081d598e40f
Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré.
Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes.
MIT-0
Preuves d'exécution
L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.
- Base de preuve
- Contrat signé réussi
- Reçus de vérification
- 2
- Clés de signature qui l’ont compilé
- 2
Environnement déclaré
ruby linux x64 ruby ruby rubygems
Environnements des exécutions de vérification
| Environnement | Contrat | Étapes | Exécution |
|---|---|---|---|
| ruby 3 · linux debian/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · rubygems@1 |
2026-08-17 |
| ruby 3 · linux debian/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · rubygems@1 |
2026-08-18 |
Cas
HOW- Objectif
- Handle custom converter registration in CSV::Converters with CSV::FieldInfo metadata, non-String converter short-circuiting, skip_lines filtering, and CSV.filter stream transformation in CSV 3.3.2
- Paquets
- Symboles
-
- CSV.parse
- CSV.filter
- CSV::Converters
- CSV::FieldInfo
- CSV::Row
- Environnement
- ruby
- Créé
- 2026-08-17T14:41:39Z
Contrat
- CSV::FieldInfo provides 0-based column index, 1-based logical line number, and transformed header symbol metadata to custom converters registered in CSV::Converters.
- CSV converter pipelines execute sequentially for each field, short-circuiting downstream converters as soon as any converter returns a non-String value.
- CSV.parse with skip_lines pattern filters comments prior to headers and between rows while maintaining accurate 1-based logical line tracking.
- CSV.filter streams data transformations across IO objects, yielding header arrays first when write_headers is true and CSV::Row objects for data lines.
- CSV::Row supports appending header-value pairs or nil-header fields, query presence with header? and field?, and column deletion returning key-value pairs.
Fichiers
- Gemfile
- Gemfile.lock
- NOTES.md
- csx.json
- test/contract.rb
Code source
source "https://rubygems.org"
gem "csv", "3.3.2"
GEM
remote: https://rubygems.org/
specs:
csv (3.3.2)
PLATFORMS
ruby
x64-mingw-ucrt
DEPENDENCIES
csv (= 3.3.2)
CHECKSUMS
bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac
csv (3.3.2) sha256=6ff0c135e65e485d1864dde6c1703b60d34cc9e19bed8452834a0b28a519bd4e
BUNDLED WITH
4.0.18
# Notes
### What `search_known_solution` answered
`search_known_solution` returned existing published samples:
- `sha256:cd77a19583a5e0694a988590f8ce51c6b9a0a08226060f6d68e94be73eacc130` (covering `:time` converter, `CSV.open` with `StringIO`, `CSV::TSV`, and custom `col_sep` with blank lines).
- `sha256:8ff4e3da4a710e341dcf90db0b99e9af669dfab68342fc4011bfefa30d684a1b` (covering `CSV.parse` returning `CSV::Table`, column indexing, `table.to_a`, and `CSV.generate` with `write_headers: true`).
- `sha256:3d12db62adbd2a7953f464fa2bbaaffa6d87ba0e99054b30dad1404f2b997473` (covering `headers: true` combined with `return_headers: true` column prepending).
- `sha256:bd2f2439031898f70ac4d8c0be64eb855887b222f9e161cb86cb4feeca7ebddb` (covering `liberal_parsing: {backslash_quote: true}`, `CSV::Row#to_h` duplicate header resolution, `strip: true`, and `quote_empty: true` roundtripping).
- `sha256:6c5ffc8a79396c5e831eed2edeefc0ee240d543d3937dad970365842c019aef5` (covering `CSV::InvalidEncodingError` inheritance hierarchy, `ArgumentError` on nil inputs, and `liberal_parsing: true` unclosed quote boundaries).
- `sha256:da583d8f8e0b640a9c899c67cd5cee4ed2a94c48cb31e90d6df9dbf2df098cdd` (covering unquoted empty field nil return values when `nil_value` is unset).
This sample tests a distinct set of standard behaviors in `csv` 3.3.2: custom converter registration via `CSV::Converters`, positional and header metadata exposed through `CSV::FieldInfo`, non-`String` converter short-circuiting in sequential pipelines, pattern filtering via `skip_lines` with logical row numbering, streaming I/O transformations using `CSV.filter`, and `CSV::Row` structural mutation methods.
### Exact Release Pinned
`csv` version `3.3.2` pinned in `Gemfile` and `Gemfile.lock`.
### Observed Behaviour
1. **Custom Converter Registration and `CSV::FieldInfo` Metadata**:
- Registering a converter block into `CSV::Converters[:custom_name]` makes it available to pass by symbol in the `converters:` option array.
- When a converter accepts a second block parameter (`|value, field_info|`), `field_info` provides:
- `field_info.index`: 0-based column index within the parsed row.
- `field_info.line`: 1-based logical line index in the parsed sequence.
- `field_info.header`: Transformed header identifier (affected by `header_converters`).
2. **Converter Chaining and Non-String Short-Circuiting**:
- Converters in `converters: [...]` execute sequentially from left to right for each field.
- If a converter returns a modified `String`, downstream string converters receive the transformed string.
- When any converter returns a non-`String` value (such as `Integer`, `Float`, `Time`, or custom objects), `CSV` halts further conversion for that field, skipping subsequent string converters.
3. **`skip_lines` Pattern Filtering and Logical Row Numbering**:
- `skip_lines: Regexp` / `skip_lines: String` drops matched comment lines before the header row and between data rows.
- Logical row numbers reported in `CSV::FieldInfo#line` track parsed rows (row 1 is the header row, row 2 is the first data row, row 3 is the second data row).
4. **`CSV.filter` Stream Transformation**:
- `CSV.filter(input_io, output_io, ...)` streams rows between IO objects.
- When `headers: true, write_headers: true` is enabled, the first iteration yields the header row as a mutable `Array`, while subsequent iterations yield data rows as `CSV::Row` objects.
- In-place row mutations within the block are formatted and written to the output stream.
5. **`CSV::Row` Mutation and Query Semantics**:
- `row << [key, value]` appends a column header and value pair.
- `row << value` appends a field with a `nil` header.
- `row.delete(key)` removes the column and returns `[key, value]`.
- `row.header?(key)` and `row.field?(value)` test header and value presence respectively.
{"case":{"caseId":"case:sha256:05721fdb2a4ef0c9e4ed9ec62f74f8ccfa0ca27f7f107ab304faa1085d172d08","contract":["CSV::FieldInfo provides 0-based column index, 1-based logical line number, and transformed header symbol metadata to custom converters registered in CSV::Converters.","CSV converter pipelines execute sequentially for each field, short-circuiting downstream converters as soon as any converter returns a non-String value.","CSV.parse with skip_lines pattern filters comments prior to headers and between rows while maintaining accurate 1-based logical line tracking.","CSV.filter streams data transformations across IO objects, yielding header arrays first when write_headers is true and CSV::Row objects for data lines.","CSV::Row supports appending header-value pairs or nil-header fields, query presence with header? and field?, and column deletion returning key-value pairs."],"goal":"Handle custom converter registration in CSV::Converters with CSV::FieldInfo metadata, non-String converter short-circuiting, skip_lines filtering, and CSV.filter stream transformation in CSV 3.3.2","kind":"HOW","packages":["pkg:gem/csv@3.3.2"],"schemaVersion":1,"symbols":["CSV.parse","CSV.filter","CSV::Converters","CSV::FieldInfo","CSV::Row"]},"contractCommand":["ruby","test/contract.rb"],"environment":{"arch":"x64","ecosystem":"gem","executionContext":"ruby","language":"ruby","os":"linux","packageManager":"rubygems","runtime":"ruby","schemaVersion":1},"license":"MIT-0","packages":["pkg:gem/csv@3.3.2"],"schemaVersion":1,"symbols":["CSV.parse","CSV.filter","CSV::Converters","CSV::FieldInfo","CSV::Row"],"verifierAdapter":"rubygems@1"}
require "csv"
require "stringio"
# 1. Custom converter registration in CSV::Converters and CSV::FieldInfo metadata
CSV::Converters[:strip_currency] = lambda do |value, field_info|
if field_info && field_info.header == :price && value.is_a?(String)
(value.delete("$").to_f * 100).round
else
value
end
end
metadata_entries = []
metadata_logger = lambda do |value, field_info|
metadata_entries << {
value: value,
index: field_info.index,
line: field_info.line,
header: field_info.header
}
value
end
csv_input = "item,price,qty\nApple,$1.25,10\nBanana,$0.80,25\n"
table = CSV.parse(
csv_input,
headers: true,
header_converters: :symbol,
converters: [metadata_logger, :strip_currency, :integer]
)
# Custom registered converter transformed currency string to integer cents
raise "Price conversion mismatch" unless table[:price] == [125, 80]
raise "Integer converter mismatch" unless table[:qty] == [10, 25]
raise "Item string preserved mismatch" unless table[:item] == ["Apple", "Banana"]
# FieldInfo metadata accuracy: index (0-based column), line (1-based logical parsed row), header (symbol from header_converters)
raise "Expected 6 metadata records" unless metadata_entries.size == 6
raise "First field metadata mismatch" unless metadata_entries[0] == { value: "Apple", index: 0, line: 2, header: :item }
raise "Second field metadata mismatch" unless metadata_entries[1] == { value: "$1.25", index: 1, line: 2, header: :price }
raise "Third field metadata mismatch" unless metadata_entries[2] == { value: "10", index: 2, line: 2, header: :qty }
raise "Fourth field metadata mismatch" unless metadata_entries[3] == { value: "Banana", index: 0, line: 3, header: :item }
raise "Fifth field metadata mismatch" unless metadata_entries[4] == { value: "$0.80", index: 1, line: 3, header: :price }
raise "Sixth field metadata mismatch" unless metadata_entries[5] == { value: "25", index: 2, line: 3, header: :qty }
# 2. Converter chaining and non-String short-circuiting
c_uppercase = ->(v) { v.is_a?(String) ? v.upcase : v }
c_to_int = ->(v) { Integer(v) rescue v }
c_downstream_calls = []
c_downstream = lambda do |v|
c_downstream_calls << v
v
end
chain_parsed = CSV.parse("100,hello\n", converters: [c_to_int, c_uppercase, c_downstream])
# "100" was converted to Integer(100) by c_to_int, so subsequent String converters are short-circuited
raise "Integer conversion mismatch in chain" unless chain_parsed[0][0] == 100
raise "String conversion mismatch in chain" unless chain_parsed[0][1] == "HELLO"
raise "Downstream short-circuit mismatch" unless c_downstream_calls == ["HELLO"]
# 3. skip_lines filtering with header detection and FieldInfo logical line tracking
commented_data = "# Comment header 1\n# Comment header 2\nname,score\n# Interstitial comment\nAlice,100\nBob,95\n"
field_lines = []
field_tracker = lambda do |val, info|
field_lines << { val: val, line: info.line, index: info.index, header: info.header }
val
end
parsed_skip = CSV.parse(commented_data, headers: true, skip_lines: /^#/, converters: [field_tracker])
raise "Skip lines row count mismatch" unless parsed_skip.size == 2
raise "Skip lines table mapping mismatch" unless parsed_skip.map(&:to_h) == [{ "name" => "Alice", "score" => "100" }, { "name" => "Bob", "score" => "95" }]
# Logical line numbers in FieldInfo: row 1 is header (name,score), row 2 is Alice, row 3 is Bob
alice_info = field_lines.find { |r| r[:val] == "Alice" }
bob_info = field_lines.find { |r| r[:val] == "Bob" }
raise "Alice line tracking mismatch" unless alice_info[:line] == 2 && alice_info[:index] == 0
raise "Bob line tracking mismatch" unless bob_info[:line] == 3 && bob_info[:index] == 0
# 4. CSV.filter IO stream transformation
in_io = StringIO.new("id,name,score\n1,Alice,80\n2,Bob,45\n")
out_io = StringIO.new
row_classes = []
CSV.filter(in_io, out_io, headers: true, write_headers: true, converters: :integer) do |row|
row_classes << row.class
if row.is_a?(Array)
row << "grade"
else
grade = row["score"] >= 70 ? "PASS" : "FAIL"
row << ["grade", grade]
end
end
raise "CSV.filter yielded row classes mismatch" unless row_classes == [Array, CSV::Row, CSV::Row]
out_io.rewind
expected_stream = "id,name,score,grade\n1,Alice,80,PASS\n2,Bob,45,FAIL\n"
raise "CSV.filter output stream mismatch" unless out_io.read == expected_stream
# 5. CSV::Row mutation and query methods
row_obj = CSV::Row.new(["k1", "k2"], ["v1", "v2"])
row_obj << ["k3", "v3"]
row_obj << "v4"
raise "Row headers mismatch" unless row_obj.headers == ["k1", "k2", "k3", nil]
raise "Row fields mismatch" unless row_obj.fields == ["v1", "v2", "v3", "v4"]
raise "Row header? positive mismatch" unless row_obj.header?("k1") == true
raise "Row header? negative mismatch" unless row_obj.header?("missing") == false
raise "Row field? positive mismatch" unless row_obj.field?("v2") == true
raise "Row field? negative mismatch" unless row_obj.field?("missing") == false
deleted_pair = row_obj.delete("k2")
raise "Row delete return value mismatch" unless deleted_pair == ["k2", "v2"]
raise "Row state after delete mismatch" unless row_obj.to_h == { "k1" => "v1", "k3" => "v3", nil => "v4" }