はじめに
この記事では、RubyでCSVを読み込むときに converters: :integer を指定したことで起きた予期せぬ挙動について紹介します。
RubyでCSVクラスを使って、読み込みを行っている方はぜひ参考にしてください
背景・動機
CSVを読み込んでデータベースに登録する処理を作成していました。
CSV.read を使う際に、便利そうなオプションとして converters: :integer を指定してみたところ、
電話番号のデータが意図しない値に変換されてしまうという現象に出会いました。
調べてみると、Rubyの数値変換 (Integer() / to_i) に関係する仕様が原因でした。
実際にやってみたこと
実際にサンプルコードを書いてみます。
require 'csv' csv_text = <<~CSV id,phone 1,0702233344 2,012 3,098 CSV csv_rows = CSV.parse(csv_text, headers: true, converters: :integer) csv_rows.each do |row| puts "Phone: #{row['phone']} (#{row['phone'].class})" end
何が出力されるでしょうか?
期待する動作
=> Phone: 0702233344(Integer) => Phone: 012 (Integer) => Phone: 098 (integer)
実際の動作
=> Phone: 118044388 (Integer) => Phone: 10 (Integer) => Phone: 098 (String)
特定の数値が、自動的に8進数に変換されてしまっていたのです。
学び・気づき
Rubyの挙動
Rubyでは、数値リテラルが「0」から始まる場合(かつ8, 9の数字がない場合)は、8進数として解釈されます
0123 => 83 0o123 => 83 0x14 => 20
一方、to_iだと単純に、10進数で変換されます。
'0123'.to_i => 123
しかし、CSVのconverters: :integerの内部では、Integer()を呼び出しています。
実際のCSVクラスのconvertersオプションの定義はこちら
Integer('0123')
=> 83
Integer(0123)
=> 83
つまり、Integer()は、文字列を数値リテラルのように評価して、結果的に8進数に変換していました。
リテラルとは?
- プログラミングで直接書かれた値のこと。
123 # 数値リテラル "hello" # 文字列リテラル
安全な変換方法
CSVクラスでの自動変換は行わず、アプリケーション側で必要な箇所だけを変換するのが良さそうです。
e.g.
CSV.parse(csv_text, headers: true) do |row| puts "id: #{row['id']}" puts "phone: #{row['phone'].to_i}" # 必要な箇所だけ数値変換 end
Railsを使っている場合、
ActiveRecordがカラム定義を見て、自動的に型キャストを信頼するのが良さそうです。
下記のようなクラスがあると、Railsは自動的にDBスキーマを参照して、(型変換が可能な入力値であれば、)型を変換してくれます
class User << ActiveRecord # age: integer # name: string end user = User.new(age: "20", name: "taro") user.age => 20 user.age.class => Integer
なので、CSVで無理に変換する必要はありません。
まとめ
- Rubyでは、0から始まる数値リテラルは8進数として解釈されます。
- CSV.read(converters: :integer) は内部で Integer() を使うため、先頭0の値が破壊的に変換されて、数値が変わってしまうリスクがある。
- CSVの自動変換は使わず、必要な箇所だけ手動で変換する。もしくは、ActiveRecordの型変換を利用する
CSVで変換するときは、「できるだけそのままの形で受け取る」のが安心です。 むやみに変換せず、必要な箇所だけ別で変換しましょう。
最後まで、お読みいただきありがとうございました。