From 121991c37f92d6388f1b1ba776070926584a5fab Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Wed, 29 Apr 2026 10:52:30 +0300 Subject: [PATCH 01/12] Support block in CSV::Row#to_h --- lib/csv/row.rb | 9 +++++++-- test/csv/test_row.rb | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 86323f7d..b42031d9 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -652,8 +652,13 @@ def ==(other) # row.to_h # => {"Name"=>"Foo"} def to_h hash = {} - each do |key, _value| - hash[key] = self[key] unless hash.key?(key) + each do |key, value| + new_key, new_value = if block_given? + yield(key, value) + else + [key, value] + end + hash[new_key] = new_value unless hash.key?(new_key) end hash end diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index b7179450..33b70b62 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -339,6 +339,9 @@ def test_to_hash assert_predicate(string_key, :frozen?) assert_same(string_key, @row.headers[h]) end + row2 = CSV::Row.new(%w{A B C}, [1, 2, 3]) + hash2 = row2.to_hash { |k, v| [k, v ** 2] } + assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash2) end def test_to_csv From 070786982f1989086108b95654022cd6c7fdf488 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Thu, 30 Apr 2026 12:35:54 +0300 Subject: [PATCH 02/12] Simplify block_given? case based on @kou feedback --- lib/csv/row.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index b42031d9..1befc7a1 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -653,12 +653,9 @@ def ==(other) def to_h hash = {} each do |key, value| - new_key, new_value = if block_given? - yield(key, value) - else - [key, value] - end - hash[new_key] = new_value unless hash.key?(new_key) + key, value = yield(key, value) if block_given? + + hash[key] = value unless hash.key?(key) end hash end From a11c20f89e33e3750b76131d6b33f7f3c4e98037 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Thu, 30 Apr 2026 12:36:13 +0300 Subject: [PATCH 03/12] Separate block test case --- test/csv/test_row.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 33b70b62..53b0fbc0 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -339,9 +339,12 @@ def test_to_hash assert_predicate(string_key, :frozen?) assert_same(string_key, @row.headers[h]) end - row2 = CSV::Row.new(%w{A B C}, [1, 2, 3]) - hash2 = row2.to_hash { |k, v| [k, v ** 2] } - assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash2) + end + + def test_to_hash_with_block + row = CSV::Row.new(%w{A B C}, [1, 2, 3]) + hash = row.to_hash { |k, v| [k, v ** 2] } + assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash) end def test_to_csv From 7dbaa45a0ab5d43c617d66385e1a005268ef3980 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Thu, 30 Apr 2026 13:11:31 +0300 Subject: [PATCH 04/12] Get value from self --- lib/csv/row.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 1befc7a1..e0e61e44 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -652,7 +652,8 @@ def ==(other) # row.to_h # => {"Name"=>"Foo"} def to_h hash = {} - each do |key, value| + each do |key, _value| + value = self[key] key, value = yield(key, value) if block_given? hash[key] = value unless hash.key?(key) From 8257d8e0d93588a35934d159b31fccd089b35bbd Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 30 Apr 2026 14:44:08 +0300 Subject: [PATCH 05/12] Fix formatting Co-authored-by: Sutou Kouhei --- test/csv/test_row.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 53b0fbc0..2df0b6e3 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -342,7 +342,7 @@ def test_to_hash end def test_to_hash_with_block - row = CSV::Row.new(%w{A B C}, [1, 2, 3]) + row = CSV::Row.new(%w{A B C}, [1, 2, 3]) hash = row.to_hash { |k, v| [k, v ** 2] } assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash) end From a01ef69b256fd29c6b33cf045c863b9da99f6f55 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Thu, 30 Apr 2026 14:59:43 +0300 Subject: [PATCH 06/12] Improve condition and test, add documentation --- lib/csv/row.rb | 10 +++++++++- test/csv/test_row.rb | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index e0e61e44..d29ba66a 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -650,13 +650,21 @@ def ==(other) # table = CSV.parse(source, headers: true) # row = table[0] # row.to_h # => {"Name"=>"Foo"} + # + # If a block is given, will call it with (key, value) arguments and use result as a hash entry: + # source = "Name,Value\nfoo,1\nbar,2\nbaz,3\n" + # table = CSV.parse(source, headers: true) + # row = table[0] + # row.to_h { |key, value| [key, value.to_i * 2] } # => {"Name"=>"foo", "Value"=>2} def to_h hash = {} each do |key, _value| + next if hash.key?(key) + value = self[key] key, value = yield(key, value) if block_given? - hash[key] = value unless hash.key?(key) + hash[key] = value end hash end diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 53b0fbc0..2d725978 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -342,8 +342,8 @@ def test_to_hash end def test_to_hash_with_block - row = CSV::Row.new(%w{A B C}, [1, 2, 3]) - hash = row.to_hash { |k, v| [k, v ** 2] } + row = CSV::Row.new(%w{A A B C}, [1, 2, 2, 3]) + hash = row.to_hash { |k, v| [k, v**2] } assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash) end From 1298ed9ec338ceb83d01a5a1aa2b8de965d86c63 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Thu, 30 Apr 2026 15:20:34 +0300 Subject: [PATCH 07/12] Add call-seq doc --- lib/csv/row.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index d29ba66a..7e561200 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -637,6 +637,7 @@ def ==(other) # :call-seq: # row.to_h -> hash + # row.to_h {|key, value| ... } -> hash # # Returns the new \Hash formed by adding each header-value pair in +self+ # as a key-value pair in the \Hash. From f1bbb996d2571ee395e7d9935e7d437855ba99ab Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Fri, 1 May 2026 10:35:44 +0300 Subject: [PATCH 08/12] Handle repeated headers in block, validate block result type --- lib/csv/row.rb | 18 +++++++++++++----- test/csv/test_row.rb | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 7e561200..f711bf10 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -659,14 +659,22 @@ def ==(other) # row.to_h { |key, value| [key, value.to_i * 2] } # => {"Name"=>"foo", "Value"=>2} def to_h hash = {} - each do |key, _value| - next if hash.key?(key) - value = self[key] - key, value = yield(key, value) if block_given? + if block_given? + each do |key, _value| + result = yield(key, self[key]) + raise TypeError, "wrong element type #{result.class} (expected array)" unless result.is_a?(Array) + raise ArgumentError, "wrong array length (expected 2, was #{result.size})" unless result.size == 2 - hash[key] = value + key, value = result + hash[key] = value unless hash.key?(key) + end + else + each do |key, _value| + hash[key] = self[key] unless hash.key?(key) + end end + hash end alias_method :to_hash, :to_h diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 20cd81fb..260b1dd3 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -343,8 +343,20 @@ def test_to_hash def test_to_hash_with_block row = CSV::Row.new(%w{A A B C}, [1, 2, 2, 3]) - hash = row.to_hash { |k, v| [k, v**2] } - assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash) + new_keys_map = {"A" => "A", "B" => "B", "C" => "B"} + hash = row.to_hash { |k, v| [new_keys_map[k], v**2] } + assert_equal({"A" => 1, "B" => 4}, hash) + hash.keys.each_with_index do |string_key, h| + assert_predicate(string_key, :frozen?) + end + + assert_raise TypeError do + row.to_hash { "foo" } + end + + assert_raise ArgumentError do + row.to_hash { [1] } + end end def test_to_csv From ff84e8ad2dda4256d21b055ce46d8129f2e5116f Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Fri, 1 May 2026 15:29:58 +0300 Subject: [PATCH 09/12] Improve validation of block result --- lib/csv/row.rb | 5 +++-- test/csv/test_row.rb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index f711bf10..e5c18bf3 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -662,11 +662,12 @@ def to_h if block_given? each do |key, _value| - result = yield(key, self[key]) - raise TypeError, "wrong element type #{result.class} (expected array)" unless result.is_a?(Array) + result = Array.try_convert(yield(key, self[key])) + raise TypeError, "wrong element type #{result.class} (expected array)" if result.nil? raise ArgumentError, "wrong array length (expected 2, was #{result.size})" unless result.size == 2 key, value = result + key.freeze if key.is_a?(String) && !key.frozen? hash[key] = value unless hash.key?(key) end else diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 260b1dd3..7b5df39c 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -346,7 +346,7 @@ def test_to_hash_with_block new_keys_map = {"A" => "A", "B" => "B", "C" => "B"} hash = row.to_hash { |k, v| [new_keys_map[k], v**2] } assert_equal({"A" => 1, "B" => 4}, hash) - hash.keys.each_with_index do |string_key, h| + hash.each_key do |string_key| assert_predicate(string_key, :frozen?) end From a8797a1a7189a76f64c3a08593f85d1779394b6d Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Sat, 2 May 2026 10:02:35 +0300 Subject: [PATCH 10/12] Fix class in error and doc --- lib/csv/row.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index e5c18bf3..323ad2bc 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -656,18 +656,19 @@ def ==(other) # source = "Name,Value\nfoo,1\nbar,2\nbaz,3\n" # table = CSV.parse(source, headers: true) # row = table[0] - # row.to_h { |key, value| [key, value.to_i * 2] } # => {"Name"=>"foo", "Value"=>2} + # row.to_h { |key, value| [key, "#{key}-#{value}"] } # => {"Name"=>"Name-foo", "Value"=>"Value-1"} def to_h hash = {} if block_given? each do |key, _value| - result = Array.try_convert(yield(key, self[key])) - raise TypeError, "wrong element type #{result.class} (expected array)" if result.nil? - raise ArgumentError, "wrong array length (expected 2, was #{result.size})" unless result.size == 2 + result = yield(key, self[key]) + result_array = Array.try_convert(result) + raise TypeError, "wrong element type #{result.class} (expected array)" if result_array.nil? + raise ArgumentError, "wrong array length (expected 2, was #{result_array.size})" unless result_array.size == 2 - key, value = result - key.freeze if key.is_a?(String) && !key.frozen? + key, value = result_array + key.freeze if key.is_a?(String) hash[key] = value unless hash.key?(key) end else From 929c20d706e11bd997216ce6d6f75861e9b3fe05 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Sat, 2 May 2026 10:58:56 +0300 Subject: [PATCH 11/12] Skip duplicate transformed key earlier, add spec for value transformations only --- lib/csv/row.rb | 6 ++++-- test/csv/test_row.rb | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 323ad2bc..40988001 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -668,8 +668,10 @@ def to_h raise ArgumentError, "wrong array length (expected 2, was #{result_array.size})" unless result_array.size == 2 key, value = result_array - key.freeze if key.is_a?(String) - hash[key] = value unless hash.key?(key) + next if hash.key?(key) + + key.freeze if key.is_a?(String) && !key.frozen? + hash[key] = value end else each do |key, _value| diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 7b5df39c..b39f234a 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -343,10 +343,16 @@ def test_to_hash def test_to_hash_with_block row = CSV::Row.new(%w{A A B C}, [1, 2, 2, 3]) + hash1 = row.to_hash { |k, v| [k, v**2] } + assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash1) + hash1.each_key do |string_key| + assert_predicate(string_key, :frozen?) + end + new_keys_map = {"A" => "A", "B" => "B", "C" => "B"} - hash = row.to_hash { |k, v| [new_keys_map[k], v**2] } - assert_equal({"A" => 1, "B" => 4}, hash) - hash.each_key do |string_key| + hash2 = row.to_hash { |k, v| [new_keys_map[k], v**2] } + assert_equal({"A" => 1, "B" => 4}, hash2) + hash2.each_key do |string_key| assert_predicate(string_key, :frozen?) end From bc0fe31eba65802a59eab65523fe192d966ec1e3 Mon Sep 17 00:00:00 2001 From: tsymbalenkovlad Date: Sun, 3 May 2026 09:43:46 +0300 Subject: [PATCH 12/12] Separate tests --- test/csv/test_row.rb | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index b39f234a..86ccaa19 100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -341,27 +341,26 @@ def test_to_hash end end - def test_to_hash_with_block - row = CSV::Row.new(%w{A A B C}, [1, 2, 2, 3]) - hash1 = row.to_hash { |k, v| [k, v**2] } - assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash1) - hash1.each_key do |string_key| + def test_to_hash_with_block_transform_values + hash = @row.to_hash { |k, v| [k, v**2] } + assert_equal({"A" => 1, "B" => 4, "C" => 9}, hash) + hash.each_key do |string_key| assert_predicate(string_key, :frozen?) end - - new_keys_map = {"A" => "A", "B" => "B", "C" => "B"} - hash2 = row.to_hash { |k, v| [new_keys_map[k], v**2] } - assert_equal({"A" => 1, "B" => 4}, hash2) - hash2.each_key do |string_key| - assert_predicate(string_key, :frozen?) - end - assert_raise TypeError do - row.to_hash { "foo" } + @row.to_hash { "foo" } end - assert_raise ArgumentError do - row.to_hash { [1] } + @row.to_hash { [1] } + end + end + + def test_to_hash_with_block_transform_entries + new_keys_map = {"A" => "A", "B" => "B", "C" => "B"} + hash = @row.to_hash { |k, v| [new_keys_map[k], v**2] } + assert_equal({"A" => 1, "B" => 4}, hash) + hash.each_key do |string_key| + assert_predicate(string_key, :frozen?) end end