@kyanny's blog

My thoughts, my life. Views/opinions are my own.

Ruby: CSV.filter のオプションに return_headers と write_headers の両方を指定するとヘッダが 2 行でる

TL;DR

headers: truereturn_headers: true を指定すること(write_headers は指定しないこと)。

ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, return_headers: true, write_headers: true){ |row| p [row.class, row]}'
[Array, ["a", "b"]]
a,b
[CSV::Row, #<CSV::Row "a":"a" "b":"b">]
a,b
[CSV::Row, #<CSV::Row "a":"1" "b":"2">]
1,2

write_headers を特別扱いしているが、これがかえって悪さをしている気がする。

ruby/csv.rb at 5921bfc7ce91aa8079dd8ac4faf873ec911ce320 · ruby/ruby · GitHub

CSV.filter のブロック内でヘッダ行を CSV::Row インスタンスとして処理したい場合は return_headers: true が必要なので、write_headers を指定しないのが正解。

echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, return_headers: true){ |row| p [row.class, row]}'
[CSV::Row, #<CSV::Row "a":"a" "b":"b">]
a,b
[CSV::Row, #<CSV::Row "a":"1" "b":"2">]
1,2
echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, return_headers: true){ |row| row.header_row? && row[0]=row[0].upcase }'
A,b
1,2

return_headers: true の代わりに write_headers: true でもヘッダ行はブロックに渡されるが、CSV::Row インスタンスではなく Array になるので適切なインスタンスメソッドが使えない。

echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, write_headers: true){ |row| p [row.class, row]}'
[Array, ["a", "b"]]
a,b
[CSV::Row, #<CSV::Row "a":"1" "b":"2">]
1,2
echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, write_headers: true){ |row| row.header_row? && row[0]=row[0].upcase }'
-e:1:in `block in <main>': undefined method `header_row?' for ["a", "b"]:Array (NoMethodError)

CSV.filter(headers: true, write_headers: true){ |row| row.header_row? && row[0]=row[0].upcase }
                                                         ^^^^^^^^^^^^
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:1093:in `filter'
    from -e:1:in `<main>'

CSV.filter (Ruby 3.1 リファレンスマニュアル) のサンプルコードのように return_headers: truewrite_headers: true を両方指定すると、冒頭の実行例のようにヘッダ行が 2 行出力されるばかりか、最初のヘッダ行は Array のインスタンスなので適切なインスタンスメソッドが使えない。サンプルコードどおりに実装しても動作せず、ハマる。

echo "a,b\n1,2" | ruby -r csv -e 'CSV.filter(headers: true, return_headers: true, write_headers: true){ |row| row.header_row? && row[0]=row[0].upcase }'
-e:1:in `block in <main>': undefined method `header_row?' for ["a", "b"]:Array (NoMethodError)

CSV.filter(headers: true, return_headers: true, write_headers: true){ |row| row.header_row? && row[0]=row[0].upcase }
                                                                               ^^^^^^^^^^^^
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:1093:in `filter'
    from -e:1:in `<main>'

CSV.filter (Ruby 3.1 リファレンスマニュアル) のサンプルコードは他にも間違っていて、CSV.filterHash インスタンスとして options を渡しているが、これだとキーワード引数ではなく第一引数と解釈され、第一引数は StringIO のインスタンスを期待しているので、実行時エラーになる。

echo "a,b\n1,2" | ruby -e 'require "csv"

options = { headers: true, return_headers: true, write_headers: true }

CSV.filter(options) do |row|
  if row.header_row?
    row << "header3"
    next
  end
  row << "row1_3"
end'
/Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv/parser.rb:237:in `read_chunk': private method `gets' called for {:headers=>true, :return_headers=>true, :write_headers=>true}:Hash (NoMethodError)

          chunk = input.gets(@row_separator, @chunk_size)
                       ^^^^^
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv/parser.rb:95:in `initialize'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv/parser.rb:795:in `new'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv/parser.rb:795:in `build_scanner'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv/parser.rb:332:in `parse'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:2365:in `each'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:2365:in `each'
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:1102:in `filter'
    from -e:5:in `<main>'

**options と展開しないといけない(それでも前述の Array インスタンスが渡される問題で実行時エラーになるが)。

echo "a,b\n1,2" | ruby -e 'require "csv"

options = { headers: true, return_headers: true, write_headers: true }

CSV.filter(**options) do |row|
  if row.header_row?
    row << "header3"
    next
  end
  row << "row1_3"
end'
-e:6:in `block in <main>': undefined method `header_row?' for ["a", "b"]:Array (NoMethodError)

  if row.header_row?
        ^^^^^^^^^^^^
    from /Users/kyanny/.rbenv/versions/3.1.2/lib/ruby/3.1.0/csv.rb:1093:in `filter'
    from -e:5:in `<main>'