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

Rubyにおいて `1 / x` を実行すると、`x = 0` であれば例外 [`ZeroDivisionError`](https://docs.ruby-lang.org/ja/latest/class/ZeroDivisionError.html) が発生する。

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

考えた結果だけ見たい方は[まとめ](#まとめ)へワープ。

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

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

```ruby:exact_zero.rb
class Numeric
	def exact_zero?
		1 / self
	rescue ZeroDivisionError
		true
	else
		false
	end
end
```

```ruby: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` に対応できないのだが、クラスによる性質の違いを見るのに役立つ。

```ruby
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` が*正確に*ゼロのときに限る**。ならば等価判定すればいいだろう。

```ruby
class Numeric
	def exact_zero?
		self.eql?(-self)
	end
end
```

しかし、Rubyにおいてほとんどの場合**ゼロの符号は区別されない**ので、このような単純な等価判定ではうまくいかない。

#### 文字列として比較
`(-0.0).to_s` と文字列化すると `"-0.0"` を返すので、ゼロの符号を区別できる。

```ruby
class Numeric
	def exact_zero?
		to_s == (-self).to_s
	end
end
```

これならいけると思ったのだが、複素数の虚部が `BigDecimal` ではダメだった。

#### Hashのキーとして比較
`hash[0.0]` と `hash[-0.0]` は異なるものを指していた記憶がある。

```ruby
class Numeric
	def exact_zero?
		{self => nil, -self => nil}.size == 1
	end
end
```

この仕様はRuby 2.1〜2.2の頃に直されたようで今は利用できなかった。また、古いバージョンで試しても、`Float` を持つ複素数に対しては誤判定していた。

#### ビット列に変換
`0.0` と `-0.0` はメモリ上のデータとしては当然異なる。なのでバイナリデータとして書き出してしまえば違いを抽出できる。

```ruby
class Numeric
	def exact_zero?
		[self].pack('f') == [-self].pack('f')
	end
end
```

ただし、このコードだとまず `Float` に変換しようとするため、複素数は虚部がゼロでないと `RangeError` を起こしてしまう。

#### `Marshal.dump` で変換
ビット列まで低レベルに落とさなくても、[`Marshal.dump`](https://docs.ruby-lang.org/ja/latest/method/Marshal/m/dump.html) で変換すれば区別できるのでは？

```ruby
class Numeric
	def exact_zero?
		Marshal.dump(self) == Marshal.dump(-self)
	end
end
```

やってみたら意外なことに、`Complex(a = 0/1r, a)` という場合で判定漏れがあった。実部と虚部とで実数のオブジェクトIDが一致しているかどうかで文字列表現が異なるらしい。

対策として、生の `self` を `-(-self)` に直せばうまくいった。

### 複素数を実数として処理
実数ではうまくいく判定も、複素数に対してはダメな場合があると分かった。ならば複素数を実数に変換してしまえばいい。

#### 実部と虚部で別々に判定
すぐ思いつくのは実部と虚部に分けて考えること。`#rect` で実数配列化して、上で試した判定を適用すればいい。

```ruby
class Numeric
	def exact_zero?
		rect.all? { |x| x.to_s == (-x).to_s }
	end
end
```

実数に対して `#rect` を作用させる（虚部が `0` 扱いになる）のが無駄であれば、`#real?` で場合分けしてもいい。この方法では再帰的にも実装できる。

```ruby
class Numeric
	def exact_zero?
		real? ? (to_s == (-self).to_s) : rect.all?(&:exact_zero?)
	end
end
```

#### 絶対値を求めて判定
別の手段では絶対値がある。特に `Complex#abs2` は単に「実部の2乗＋虚部の2乗」を計算しているだけで、実部と虚部の両方とも浮動小数点数でなければ結果も浮動小数点数ではないという実装になっている。これを利用すれば、「実部と虚部の両方とも正確にゼロ」⇔「絶対値の2乗が正確にゼロ」が成り立つ。

```ruby
class Numeric
	def exact_zero?
		x = abs2
		x.to_s == (-x).to_s
	end
end
```

#### 絶対値の副次効果
絶対値をとると結果は必ずゼロ以上の数になる。これを利用すれば面白いことができる。

```ruby
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` に統一してパターンを減らすためだが、削除しても問題ないかもしれない。

### ゼロのみを対象
ゼロでない数はここまで誤判定を起こさなかったが、扱いに困るようなら真っ先に除外してしまってもいい。現実にはゼロでない数のほうが圧倒的に多いので、平均すれば判定速度が向上するかもしれない。

```ruby
class Numeric
	def exact_zero?
		zero? && Marshal.dump(-self) == Marshal.dump(--self)
	end
end
```

## まとめ
いくつか[条件](#ルール設定)はおいたが、表題を満たすコードとして以下を思いついた。

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

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