LoginSignup
0
0

More than 5 years have passed since last update.

数値の文字列を適切な数値クラスにパース(実数クラスの包括的取り扱い)

Posted at

他の記事中で用いた小技だが、独立した記事にしたほうが良さそうなので作成した。

与えられた数値の文字列が、 Integer 形式(整数)なら Integer に、 Rational 形式("整数/自然数")なら Rational に、 Float 形式(小数点や指数表記)なら Float に変換せよ。

func("+1")  #=> 1
func("2/3") #=> (2/3)
func("4.5") #=> 4.5
func("6e7") #=> 60000000.0

なお、上記に無い形式や、そもそも変換できない文字列に対する動作は好きに決めてよい。

面倒な方法

「それぞれのクラスに特徴的な文字」が文字列に含まれているか調べて、パースする数値クラスを決める。

def func(str)
    case str
    when %r{[/]}   then str.to_r   # Rational
    when %r{[.eE]} then str.to_f   # Float
    else                str.to_i   # Integer
    end
end

素直な方法

モジュール関数 Kernel.#Integer などは、文字列をパースできなければ例外を出す。それをキャッチして徐々に複雑な形式でパースを試せばいい。

def func(str)
    Integer(str) rescue Float(str) rescue Rational(str)
end

Kernel.#Rational は小数を分数に直せてしまうので、 Kernel.#Float の後にしている。

Rational("4.5")   #=> (9/2)
Rational("4.5/3") #=> (3/2)

ちなみに Integer は二進・八進・十六進数表記も対応できる。 Float は十六進整数に対応できる。

楽な方法

Kernel.#Complex で一旦複素数クラスに変換し、実部を取り出す。

def func(str)
    Complex(str).real
end

String#to_c でもいける。ただし文字列の形式がおかしいときに例外は出ず、パースできるところまでを数値に変える。

def func(str)
    str.to_c.real
end

Complex の能力

Complex は複素数の数値を扱うためのクラスだが、虚部をゼロにして実数を扱うことに特化しても面白いことができる。

数値クラス概要

Rubyで用意されている数値クラスは、 Numeric を抽象クラスとしてその下にいくつかのサブクラスがある。

クラス 表せる数 特徴
Integer
(Fixnum)
整数の一部
int型に類似
-262〜262-1 の整数を表す1
Integer
(Bignum)
全ての整数 Fixnum の範囲外の整数をカバーする
Rational 全ての有理数 整数のペアで表す
Float 実数の一部
double型に類似
十進15桁程度の精度で
10-308〜10308程度までの幅広い数を扱う
BigDecimal 全ての実数 精度も扱える大きさも自由
(標準ライブラリ)
Complex 全ての複素数 実数のペアで表す

注釈:

  • Integer はRuby2.3まで抽象クラスであり、サブクラスとして FixnumBignum に分かれていた。とはいえ数値の大きさに応じて勝手に切り替えてくれるので、現在と同じくユーザーが違いを意識する必要はあまり無かった。
  • FloatBigDecimal を「実数」扱いしているが、厳密にはメモリが有限のため無理数を正確に表すことができない。「精度」という曖昧さによって近似的に実数を表す。
  • 「全ての整数」などと言っているのも、実際にはメモリが有限のため限界がある。

数学的に同じ数であっても、例えば 3, (3/1), ((3/1)+0i) というように、異なるクラスで表すことができる2。このような状況ではなるべく単純なクラスで表したほうが効率がいい。その辺を考慮してくれるライブラリに mathn があったが、プログラム全体で演算の動作を変えてしまうため、Ruby2.2で非推奨になりRuby2.5で廃止された。

数学においては

自然数 \mathbb{N} \subset 整数 \mathbb{Z} \subset 有理数 \mathbb{Q} \subset 実数 \mathbb{R} \subset 複素数 \mathbb{C}

という階層構造があるが、数値クラスはそのような継承(is-a)関係は無いし、包含(has-a)関係があるとも限らない。そのため実数クラス同士がほぼ独立していて、うまく纏めて扱うことが難しい。

しかし幸いなことに、複素数クラス Complex任意の実数クラスを持てるので、これをラッパーのように利用することで実数クラスを纏めて扱える。

文字列のパース

任意の実数クラスを持てるという性質上、文字列のパースでは自動で適切な実数クラス( Integer, Rational, Float )を選んでくれる。

認識される文字列の構文はマニュアルで確認できる。具体的にどの実数クラスになるかは、ソースを読み解くか実際に試してみるしかない。

Syntax of string form:
string form = extra spaces , complex , extra spaces ;
complex = real part | [ sign ] , imaginary part
        | real part , sign , imaginary part
        | rational , "@" , rational ;
real part = rational ;
imaginary part = imaginary unit | unsigned rational , imaginary unit ;
rational = [ sign ] , unsigned rational ;
unsigned rational = numerator | numerator , "/" , denominator ;
numerator = integer part | fractional part | integer part , fractional part ;
denominator = digits ;
integer part = digits ;
fractional part = "." , digits , [ ( "e" | "E" ) , [ sign ] , digits ] ;
imaginary unit = "i" | "I" | "j" | "J" ;
sign = "-" | "+" ;
digits = digit , { digit | "_" , digit };
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
extra spaces = ? \s* ? ;

整数 / 整数

多くの言語では 整数 / 整数 を計算すると小数点以下を切り捨てた整数を返す。Rubyの Integer も同じであり、もし切り捨てでない正確な値を返してほしければ、演算子でなく #quo を明示的に呼び出す必要がある(結果は必ず Rational )。当たり前な話に聞こえるが、 a / b のように変数の中身が Integer かそれ以外か分からないときにはバグの原因になる。かといって mathn ライブラリのように Integer#/ の挙動を(グローバルに)変えてしまうと、別の場所でバグとなる危険がある。

Complex でラップしてあれば、演算子が #quo を使うようになっているので間違えることは無い。演算子の右側が Complex でさえあれば、クラス変換によって Complex 同士の演算に帰着する。

# Integer 同士の除算
Integer("10")  /  Integer("3")   #=> 3
Integer("10").quo Integer("3")   #=> (10/3)

# Complex が絡む除算
Complex("10")  /  Complex("3")   #=> ((10/3)+0i)
Complex("10")  /  Integer("3")   #=> ((10/3)+(0/1)*i)
Integer("10")  /  Complex("3")   #=> ((10/3)+0i)

有理数の正規化

演算の結果が Rational で分母が1のときというのは結局のところ整数であるため、 Integer に変換したほうがその後の処理を効率化できる。(mathnがしていたことのひとつ)

Ruby2.6.0では特定の条件でその正規化処理をするようになった。前節の除算の例で虚部が 0i だったり (0/1)*i だったりしているが、この違いは除数が Complex かどうかである。Rubyのソースコードからも確認できる。
https://github.com/ruby/ruby/commit/929e971

あくまで Complex で割ったときの動作であり、有理数( Integer, Rational )で割ったり他の演算をしたりしたときには働かない。とはいえ Complex(1) で割ることで明示的に正規化できる。

Complex("3/5-4/5i") / Complex("1/5")                #=> (3-4i)
Complex("3/5-4/5i") / Rational("1/5")               #=> ((3/1)-(4/1)*i)
Complex("3/5-4/5i") / Rational("1/5") / Complex(1)  #=> (3-4i)

応用例

仕事算をプログラムで計算する際に Complex を使用した。

R = \frac{1}{\frac{1}{R_1} + \frac{1}{R_2} + \cdots}
terminal
$ ruby -v
ruby 2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]
$ PG='puts (1 / ARGV.sum { |s| 1 / Complex(s) }).real'

$ ruby -e "$PG" --  2.4 9/3 4
1.0
$ ruby -e "$PG" -- 12/5 9/3 4
1
  • 引数が Integer, Rational, Float いずれの文字列形式でも動作する
    • Complex("2.4") #=> (2.4+0i)
    • Complex("9/3") #=> ((3/1)+0i)
    • Complex("4") #=> (4+0i)
  • 除数が常に Complex なので #/ を使っても整商になってしまうことは無い
    • 1 / Integer("4") #=> 0
    • 1 / Complex("4") #=> ((1/4)+0i)
  • 除算の答えの成分が Rational で分母が1なら Integer に直される(Ruby2.6)
    • 1 / Complex("1/1+0i") #=> (1+0i)

  1. 一般的なint型より1ビット分だけ少ないのはCRubyの実装によるもの 

  2. 浮動小数点数 3.0 は、厳密に 3 ではなく限りなく近いだけかもしれないので、 3 と同一視していいかは都度考える必要がある 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0