🦁

Ruby の Integer() について

2023/02/16に公開

はじめに

Ruby で数字のみ文字列であることが期待される値が本当にそうであるかを Integer() に渡してエラーが出るかどうかで確かめるような実装をしていました。
テストも書いて万全な気でいたのですが、Integer() の仕様により思わぬ不具合が発生してしまったので忘備録的に書き残しておきます。

Integer() の仕様

結論から書くと Integer() は変換対象の文字列が進数を表す prefix で始まっていた場合、後に続く数字をその進数の値として数値変換します。
このことはリファレンスマニュアルにもしっかりと記載されています。

文字列の場合は、進数を表す接頭辞を含む整数表現とみなせる文字列のみ変換します。

進数の prefix

進数 prefix
2進数 0b
8進数 0o, 0
10進数 0d, 1 ~ 9 (prefix を指定しない)
16進数 0x

こちらもリファレンスマニアルに記載してありました。

省略するか0を指定した場合はプリフィクスから基数を判断します。その場合に認識できるプリフィクスは、0b (2 進数)、0 (8 進数)、0o (8 進数)、0d (10 進数)、0x (16 進数) です。

具体的に

はじめに書いた通り今回は Integer() で文字列がすべて数字であるかを判定したかったのですが、上記の進数変換の仕様を把握せずに実装していたため一部の条件で判定を正しく行えませんでした。
具体的には以下のようなコードで文字列が数字のみであるかを判定していました。

def only_num?
  Integer(postcode) rescue return false
  true
end

また、こちらも前述した通り今回はこのメソッドに対するテストも用意しており、0 始まりの文字列であっても正しく数字のみの文字列であると判定できるように 0120001 で true が返ること というテストケースも用意し、問題なくテストをパスしていました。

今回起きた不具合

テストも書いて安心していたのですが 03911610300845 などの一部の値で正しい判定ができていないとの報告がありました。
それらの文字列は全て数字であるので本来は true と判定されるべきところ、Integer() でエラーが発生し false と判定されてしまっているようでした。

原因

先に書いた通り、Integer() は進数 prefix で始まる文字列はその進数で数値変換するという仕様になっています。
今回不具合が起きてしまった文字列はどれも 0 から始まっており、8進数として変換されることになります。
8進数なので扱える数字は 0 ~ 7 までですが、該当の文字列には 8, 9 が含まれてしまっておりエラーが発生していました。
テストケースとして用意していた 0120001 は問題なく8進数として変換が可能なため、正しく数値変換ができる -> すべて数字の文字列である となっていたようです。
テストにおいては 文字列が数値に変換できるかどうか のみを気にしており、変換後の数値が期待通りか という点については関与していなかったためこの落とし穴に気付けませんでした。

対応

Integer() を利用するのはやめて正規表現での判定に切り替え対応しました。

def only_num?
  /\A\d{7}\z/.match?(postcode)
end

以下のように10進数の prefix を明示的につけてあげることで Integer() で対応することも可能です。

def only_num?
 Integer("0d#{postcode}") rescue return false
 true
end

教訓

自分で開発していないメソッドを雰囲気で使うと危ない

Discussion