Hisakeyのブログ

エンジニアが色々呟くブログです。

RubyのCSVで「070xxxxxxxx」の電話番号が壊れる!? converters: :integer の落とし穴

はじめに

この記事では、RubyCSVを読み込むときに converters: :integer を指定したことで起きた予期せぬ挙動について紹介します。

RubyCSVクラスを使って、読み込みを行っている方はぜひ参考にしてください

背景・動機

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オプションの定義はこちら

github.com

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で変換するときは、「できるだけそのままの形で受け取る」のが安心です。 むやみに変換せず、必要な箇所だけ別で変換しましょう。

最後まで、お読みいただきありがとうございました。

参考リンク

docs.ruby-lang.org

docs.ruby-lang.org

docs.ruby-lang.org