エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

整数値でも浮動小数点数値でもパースしたい

Elixir で文字列から数値に変換するときに String.to_integer/1String.to_float/1 を使いますが、前者は浮動小数点数値の文字列を与えると、後者は整数値の文字列を与えると例外を投げてしまいます。

iex(1)> String.to_integer("123")  
123
iex(2)> String.to_integer("123.4")
** (ArgumentError) argument error
    :erlang.binary_to_integer("123.4")
iex(2)> String.to_float("123.4")
123.4
iex(3)> String.to_float("123")  
** (ArgumentError) argument error
    :erlang.binary_to_float("123")

例外を投げない関数として、Erlangstring モジュールにそれぞれ同名の関数があります。こちらはパースに成功すると、成功した部分の数値とパースできなかった部分を残りの文字列のタプルを返します。 パースできる文字列がなかった場合には :error とその理由のタプルを返します。

iex(1)> :string.to_integer("123+987")
{123, "+987"}
iex(2)> :string.to_float("123.4+987.6")
{123.4, "+987.6"}
iex(3)> :string.to_integer("abc")      
{:error, :no_integer}
iex(4)> :string.to_float("abc")  
{:error, :no_float}

ここまではいいのですが。任意の数値の文字列を返す関数がなくて困りました。string:to_integer/1浮動小数点数値の文字列を与えると小数点の手前までしかパースしませんし、string:to_float/1 に整数値の文字列を与えるとエラーになります。

iex(1)> :string.to_integer("123.4")
{123, ".4"}
iex(2)> :string.to_float("123")    
{:error, :no_float}

対策としては。最初に string:to_float/1 でパースして失敗したら string:to_integer/1 でパースするという手があります。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} -> :string.to_integer(source)
      result -> result
    end
  end
end

これでだいたいうまくいきます。

iex(1)> MyParser.to_number("123")         
{123, ""}
iex(2)> MyParser.to_number("123.4")
{123.4, ""}
iex(3)> MyParser.to_number("123+987")
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5) MyParser.to_number("abc")
{:error, :no_integer}

ほぼ問題ないのですが、エラーのときに {:error, :no_integer} と返ってくるのでこれを整えたい。

case をもう一段重ねればそれなりに動きます。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} ->
        case :string.to_integer(source) do
          {:error, _} -> {:error, :no_number}
          result -> result
        end
      result -> result
    end
  end
end
iex(1)> MyParser.to_number("abc")
{:error, :no_number}

もうちょっとどうにかならないかとライブラリを読んでいたら。Enum.reduce_while/3 という関数を見つけました。

名前の通り条件が満たされるまで第 3 引数で与える関数を適用し、満たされたら適用を中断するというものです。

第 3 引数で与える関数は 2 要素のタプルを返す必要があります。タプルの第 1 要素が :cont だったばあいは処理を継続しタプルの第 2 要素が次の処理に引き継がれます。タプルの第 1 要素が :halt だったばあいは処理を中断しタプルの第 2 要素が Enum.reduce_while/3 の戻り値になります。

それらをふまえて。

defmodule MyParser do
  def to_number(source) do
    [&:string.to_float/1, &:string.to_integer/1]
    |> Enum.reduce_while(nil, fn f, _ ->
      case f.(source) do
        {:error, _} -> {:cont, {:error, :no_number}}
        result -> {:halt, result}
      end
    end)
  end
end

第 2 引数の値は利用していないので nil を与え、適用する関数の第 2 引数も _ にしています。

実行。

iex(1)> MyParser.to_number("123")        
{123, ""}
iex(2)> MyParser.to_number("123.4")      
{123.4, ""}
iex(3)> MyParser.to_number("123+987")    
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5)> MyParser.to_number("abc")        
{:error, :no_number}

今回は適用する関数がふたつだったので Enum.reduce_while/3 を利用する利点があまりありませんでしたが、適用する関数の数がもっと多くなったばあいには重宝しそうです。

いつか読むはずっと読まない:二十世紀前半のイギリスの赤毛の女性の物語

図らずしも。偶然に続けてよんだ小説が、どちらも二十世紀前半の(とはいえ一方は初頭、一方は第二次大戦時)のイギリスを舞台にした赤毛の女性が主人公の物語。

こちらは既刊が 6 巻。

チャーチル閣下の秘書 (創元推理文庫)

チャーチル閣下の秘書 (創元推理文庫)

こちらは全 3 巻。第 3 巻はもうじき刊行予定。

紙の魔術師 (ハヤカワ文庫FT)

紙の魔術師 (ハヤカワ文庫FT)