LoginSignup
1
1

More than 5 years have passed since last update.

ZeroDivisionErrorを引き起こす数かどうかを例外処理なしに判定

Last updated at Posted at 2017-09-04

Rubyにおいて 1 / x を実行すると、x = 0 であれば例外 ZeroDivisionError が発生する。

では、x = 0.0 ならどうか? この場合はエラーは発生しない。x = Complex(0, 0) なら? これはエラーとなる。これらの違いについて、実際に割る前に x.exact_zero?1 という風に問い合わせることで区別できないだろうか。

考えた結果だけ見たい方はまとめへワープ。

ルール設定

もちろん以下のように、メソッド内で試し割りすれば簡単に判定できる。さらにいえば、ユーザーが除算を再定義してしまえば結果は変わるので、あらゆる状況に追従してゼロ除算エラーを正確に判定するならこれしかないはず。

exact_zero.rb
class Numeric
    def exact_zero?
        1 / self
    rescue ZeroDivisionError
        true
    else
        false
    end
end
test.rb
# 実数の場合
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通りに分けた。

  • IntegerRational は、ゼロの場合に限りゼロ除算エラーになる。そのため #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

まとめ

いくつか条件はおいたが、表題を満たすコードとして以下を思いついた。

exact_zero.rb
class Numeric
    def exact_zero?
        zero? && 1.0 / (-abs2).to_f > 0
    end
end

浮動小数点数のゼロは難しい。BigDecimal を名前を出さずに Float と同様に扱うのも難しい。


  1. 名称はRubyのソースコード内からとった。ゼロ除算の防止ではなく、主に複素数を実数に変換してよい条件として使われている。 

1
1
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
1
1