他の記事中で用いた小技だが、独立した記事にしたほうが良さそうなので作成した。
問
与えられた数値の文字列が、 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まで抽象クラスであり、サブクラスとしてFixnum
とBignum
に分かれていた。とはいえ数値の大きさに応じて勝手に切り替えてくれるので、現在と同じくユーザーが違いを意識する必要はあまり無かった。 -
Float
とBigDecimal
を「実数」扱いしているが、厳密にはメモリが有限のため無理数を正確に表すことができない。「精度」という曖昧さによって近似的に実数を表す。 - 「全ての整数」などと言っているのも、実際にはメモリが有限のため限界がある。
数学的に同じ数であっても、例えば 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
)を選んでくれる。
認識される文字列の構文はマニュアルで確認できる。具体的にどの実数クラスになるかは、ソースを読み解くか実際に試してみるしかない。
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}
$ 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)