Rubyにおいて 1 / x
を実行すると、x = 0
であれば例外 ZeroDivisionError
が発生する。
では、x = 0.0
ならどうか? この場合はエラーは発生しない。x = Complex(0, 0)
なら? これはエラーとなる。これらの違いについて、実際に割る前に x.exact_zero?
1 という風に問い合わせることで区別できないだろうか。
考えた結果だけ見たい方はまとめへワープ。
ルール設定
もちろん以下のように、メソッド内で試し割りすれば簡単に判定できる。さらにいえば、ユーザーが除算を再定義してしまえば結果は変わるので、あらゆる状況に追従してゼロ除算エラーを正確に判定するならこれしかないはず。
class Numeric
def exact_zero?
1 / self
rescue ZeroDivisionError
true
else
false
end
end
# 実数の場合
require 'bigdecimal'
ary_r = [0, 0/1r, 0.0, -0.0, BigDecimal('0'), BigDecimal('-0'),
1, 1/3r, Float::INFINITY, BigDecimal('1e-1000')]
p ary_r.count(&:zero?) #=> 6
p ary_r.select(&:exact_zero?) #=> [0, (0/1)]
# 複素数の場合
ary_c = ary_r.repeated_permutation(2).map { |x,y| Complex.rect(x, y) }
p ary_c.count(&:zero?) #=> 36
p ary_c.select(&:exact_zero?) #=> [(0+0i), (0+(0/1)*i), ((0/1)+0i), ((0/1)+(0/1)*i)]
しかしそれだとつまらないので、ひとまず以下の条件で考えることにする。
- 判定対象の数値クラスは、組み込み/標準添付ライブラリの
Integer
,Rational
,Float
,BigDecimal
,Complex
に限る。-
BigDecimal
は、ライブラリ等として後から追加されるクラスを代表させている。
-
-
BigDecimal
の読み込みは禁止。ユーザーが使う気のないライブラリを強要しない。 - メソッドが再定義されている可能性は無視してよい。
- 例外処理は禁止。
試行錯誤の記録
簡単に実現できる方法が思いつかず、Rubyの仕様に依存するようなことまで色々と試した。以下のコードは大半が誤判定やエラーを起こすので注意。
サブクラス毎に定義
これだとルール上 BigDecimal
に対応できないのだが、クラスによる性質の違いを見るのに役立つ。
module ExactZeroMixinForInteger
def exact_zero?
zero?
end
end
module ExactZeroMixinForFloat
def exact_zero?
false
end
end
module ExactZeroMixinForComplex
def exact_zero?
rect.all?(&:exact_zero?)
end
end
Integer.include ExactZeroMixinForInteger
Rational.include ExactZeroMixinForInteger
Float.include ExactZeroMixinForFloat
Complex.include ExactZeroMixinForComplex
# 以下はルール違反!
require 'bigdecimal'
BigDecimal.include ExactZeroMixinForFloat
組み込みクラスを3通りに分けた。
-
Integer
とRational
は、ゼロの場合に限りゼロ除算エラーになる。そのため#zero?
で判定すればいい。 -
Float
は浮動小数点数であり、たとえゼロであってもエラーにならない(1 / ±0.0
は±Infinity
を返す)。なので無条件にfalseと判定できる。 -
Complex
は任意の実数を組み合わせられるが、実部と虚部の両方がゼロ除算エラーを起こす数の場合にのみエラーとなる。実数に対してはメソッド定義したのでそれを呼び出す。
BigDecimal
も浮動小数点数であり、性質はゼロ除算などに関して Float
と同じと考えていい。というわけで浮動小数点数のゼロをほかの正確なゼロとうまく見分けられれば目的を達成できる。
0.0
の符号を利用
浮動小数点数のゼロには符号があるので、x
と -x
が完全に一致するのは x
が正確にゼロのときに限る。ならば等価判定すればいいだろう。
class Numeric
def exact_zero?
self.eql?(-self)
end
end
しかし、Rubyにおいてほとんどの場合ゼロの符号は区別されないので、このような単純な等価判定ではうまくいかない。
文字列として比較
(-0.0).to_s
と文字列化すると "-0.0"
を返すので、ゼロの符号を区別できる。
class Numeric
def exact_zero?
to_s == (-self).to_s
end
end
これならいけると思ったのだが、複素数の虚部が BigDecimal
ではダメだった。
Hashのキーとして比較
hash[0.0]
と hash[-0.0]
は異なるものを指していた記憶がある。
class Numeric
def exact_zero?
{self => nil, -self => nil}.size == 1
end
end
この仕様はRuby 2.1〜2.2の頃に直されたようで今は利用できなかった。また、古いバージョンで試しても、Float
を持つ複素数に対しては誤判定していた。
ビット列に変換
0.0
と -0.0
はメモリ上のデータとしては当然異なる。なのでバイナリデータとして書き出してしまえば違いを抽出できる。
class Numeric
def exact_zero?
[self].pack('f') == [-self].pack('f')
end
end
ただし、このコードだとまず Float
に変換しようとするため、複素数は虚部がゼロでないと RangeError
を起こしてしまう。
Marshal.dump
で変換
ビット列まで低レベルに落とさなくても、Marshal.dump
で変換すれば区別できるのでは?
class Numeric
def exact_zero?
Marshal.dump(self) == Marshal.dump(-self)
end
end
やってみたら意外なことに、Complex(a = 0/1r, a)
という場合で判定漏れがあった。実部と虚部とで実数のオブジェクトIDが一致しているかどうかで文字列表現が異なるらしい。
対策として、生の self
を -(-self)
に直せばうまくいった。
複素数を実数として処理
実数ではうまくいく判定も、複素数に対してはダメな場合があると分かった。ならば複素数を実数に変換してしまえばいい。
実部と虚部で別々に判定
すぐ思いつくのは実部と虚部に分けて考えること。#rect
で実数配列化して、上で試した判定を適用すればいい。
class Numeric
def exact_zero?
rect.all? { |x| x.to_s == (-x).to_s }
end
end
実数に対して #rect
を作用させる(虚部が 0
扱いになる)のが無駄であれば、#real?
で場合分けしてもいい。この方法では再帰的にも実装できる。
class Numeric
def exact_zero?
real? ? (to_s == (-self).to_s) : rect.all?(&:exact_zero?)
end
end
絶対値を求めて判定
別の手段では絶対値がある。特に Complex#abs2
は単に「実部の2乗+虚部の2乗」を計算しているだけで、実部と虚部の両方とも浮動小数点数でなければ結果も浮動小数点数ではないという実装になっている。これを利用すれば、「実部と虚部の両方とも正確にゼロ」⇔「絶対値の2乗が正確にゼロ」が成り立つ。
class Numeric
def exact_zero?
x = abs2
x.to_s == (-x).to_s
end
end
絶対値の副次効果
絶対値をとると結果は必ずゼロ以上の数になる。これを利用すれば面白いことができる。
class Numeric
def exact_zero?
1.0 / (-abs2).to_f > 0
end
end
正確にゼロのときに限り -abs2
は負の符号がつかないので、浮動小数点数に変換すると(正の)ゼロになる。そのままでは負のゼロと区別できないが、逆数をとれば正の無限大にとび簡単に判別できる。
self |
abs2 |
-abs2 |
(-abs2).to_f |
1.0 / (-abs2).to_f |
---|---|---|---|---|
0 |
0 |
0 |
0.0 |
Infinity |
0.0 |
0.0 |
-0.0 |
-0.0 |
-Infinity |
0/1+0i |
0/1 |
0/1 |
0.0 |
Infinity |
0-0.0i |
0.0 |
-0.0 |
-0.0 |
-Infinity |
3+4i |
25 |
-25 |
-25.0 |
-0.04 |
-Infinity |
Infinity |
-Infinity |
-Infinity |
-0.0 |
なお #to_f
で変換しているのは Float
に統一してパターンを減らすためだが、削除しても問題ないかもしれない。
ゼロのみを対象
ゼロでない数はここまで誤判定を起こさなかったが、扱いに困るようなら真っ先に除外してしまってもいい。現実にはゼロでない数のほうが圧倒的に多いので、平均すれば判定速度が向上するかもしれない。
class Numeric
def exact_zero?
zero? && Marshal.dump(-self) == Marshal.dump(--self)
end
end
まとめ
いくつか条件はおいたが、表題を満たすコードとして以下を思いついた。
class Numeric
def exact_zero?
zero? && 1.0 / (-abs2).to_f > 0
end
end
浮動小数点数のゼロは難しい。BigDecimal
を名前を出さずに Float
と同様に扱うのも難しい。
-
名称はRubyのソースコード内からとった。ゼロ除算の防止ではなく、主に複素数を実数に変換してよい条件として使われている。 ↩