Ruby標準添付CSVライブラリのCSV::Convertersについて調べてみた

CSVの書き出しでちょっとやりたいことがあって標準添付のCSVライブラリについて調べてたんだけど、結局標準のままだと出来なそうという結論に至りました。その際にCSV::Convertersについて色々ほじったのでメモがわりに残しておきます。

CSV::Convertersって何?

ドキュメントを読むと「このハッシュは名前でアクセスできる組み込みの変換器を保持しています。」と書かれています。変換器と言われてもどう使えばいいのかピンと来ませんでした。ググッてもサンプルとかあんまり無いし。

とりあえず使ってみる

まぁ、こんな感じで使えます。

require 'csv'
CSV.parse("2011-11-23,123,456",:converters => :date) do |csv|
  p csv
end

実行すると

[#<Date: 2011-11-23 (4911777/2,0,2299161)>, "123", "456"]

って感じで自動的にDate型に変換されてますね。ここではparseメソッドでconvertersを指定していますがforeachとかでも同じように使えますよ。

convertersの種類

csv.rbの中で定義されてます。

  Converters  = { integer:   lambda { |f|
                    Integer(f.encode(ConverterEncoding)) rescue f
                  },
                  float:     lambda { |f|
                    Float(f.encode(ConverterEncoding)) rescue f
                  },
                  numeric:   [:integer, :float],
                  date:      lambda { |f|
                    begin
                      e = f.encode(ConverterEncoding)
                      e =~ DateMatcher ? Date.parse(e) : f
                    rescue  # encoding conversion or date parse errors
                      f
                    end
                  },
                  date_time: lambda { |f|
                    begin
                      e = f.encode(ConverterEncoding)
                      e =~ DateTimeMatcher ? DateTime.parse(e) : f
                    rescue  # encoding conversion or date parse errors
                      f
                    end
                  },
                  all:       [:date_time, :numeric] }

rescueとか書かれてるので見ていただければ分かるんですが、この変換ルールって「変換できそうなら変換して、ムリならそのまま返す」っての基本なんですね。ちなみに:dateの箇所で定義されてるDateMatcherってRegexpなんですがかなりキツめに書かれていてハイフン区切りじゃないとdate型に変換してくれません。後ろに書かれてるDate.parseはスラッシュ区切りでも変換してくれるのにね。

独自の変換器を定義してみる。

たとえば「3列目の数字の後ろにyenを付ける」って変換器を書いてみます。

proc = Proc.new do |field,field_info|
  if field_info.index == 2
    "#{field}yen"
  else
    field
  end
end

CSV.parse("100,200,300",:converters => proc) do |csv|
  p csv
end

実行すると

["100", "200", "300yen"]

って感じです。

CSV::Convertersに登録してみよう

さっき作ったprocオブジェクトをCSV::Convertersに登録してみましょう。

CSV::Converters[:yen] = proc

一度登録してしまえば次からは :convertersに:yenを指定するだけで使えます。

CSV.parse("100,200,300",:converters => :yen) do |csv|
  p csv
end

複数の変換器を適用してみよう

convertersと複数形になっているので当然配列も指定できます。配列を指定すると頭から順番に変換器が適用されます。

hoge = Proc.new do |v|
  "hoge#{v}"
end
fuga = Proc.new do |v|
  "fuga#{v}"
end
CSV.parse("100,200,300",:converters =>[hoge,fuga]) do |csv|
  p csv
end

実行すると

["fugahoge100", "fugahoge200", "fugahoge300"]

ですね。ではhogeとfugaを逆にすると

CSV.parse("100,200,300",:converters =>[fuga,hoge]) do |csv|
  p csv
end
["hogefuga100", "hogefuga200", "hogefuga300"]

となるわけです。

まとめ

CSV::Convertersを使うと特定のルールに従ってCSVに格納されているテキストを加工したり、特定のオブジェクトに変換したりできます。テキストからデータを読み込んだタイミングで変換されていると無駄なループが減って良い感じですね。

私が本当にやりたかったこと(余談)

本当は特定の列の値は必ずダブルクォートで囲んで出力するってのがやりたかったんだけど、この変換器は出力の対応はしてないみたい。モンキーパッチするしかないのかな...