Rubyで 「1 < x < 100」 形式の範囲チェックを実現する

この記事は[PORT Advent Calendar](https://qiita.com/advent-calendar/2017/port)8日目の記事(実質1日目)です。

業務には全く役に立たないRubyの話です。

# 概要

Rubyで`1 < x < 100`みたいな構文で範囲チェックできるようにします。下記みたいな感じ。

paizaの実行環境準備しました(https://paiza.io/projects/YaFfzau3N6wrFmTs0W0YZQ)

```rb
x = 25
y = 75

if 1 < x < 50 < y < 100
  print 'true'
end
# => true
```

# はじめに

例えば、変数xの値が1から100の間に収まってるかを判定する(`1 < x < 100`の判定)には、以下のようにするのが一般的かと思います。

```rb
x = 50
# 例1
x > 1 && x < 100 # => true

# 例2
x.between?(1, 100) # => true
```

でも、`1 < x < 100`みたいに書ければ、それが一番直感的だし書きやすいですよね。

```rb
x = 50

# こんな書き方はできない
if 1 < x < 100 # x in 1 ~ 100
  print "x in 1 ~ 100"
end
```
しかし当然ではありますが、このような書き方は標準のRubyでは使うことができません。

今回は、このような記法をRubyで無理やり実現させます。

## 実際のコード

こちらが、`1 < x < 100`みたいな記法を実現させるためのコードの全貌です。詳細は以下で解説します。

<details><summary>コード(折りたたんであります)</summary><div>

```rb
{ '<' => [-1], '>' => [1], '<=' => [-1, 0], '>= ' => [1, 0]}.each do |ope, v|
  [TrueClass, FalseClass].each do |klass|
    klass.class_eval("def #{ope}(num); self ? value #{ope} num : false;end")
  end
  [Integer, Float].each do |klass|
    klass.class_eval do
      define_method(ope) do |n|
        return false unless v.include? (self <=> n)
        TrueClass.class_eval("def value; #{n};end")
        true
      end
    end
  end
end
```
</div></details>

# 解説
## 1. なぜこの構文が成立するのか

まず、通常のRubyで`1 < x < 100`を実行してみます。

```rb
x = 50
1 < x < 100 # => NoMethodError: undefined method `<' for true:TrueClass
```

`SyntaxError`ではなく`NoMethodError`になりました。つまり、この構文はRubyでは使用可能だということです。
`<`は`.<`のシンタックスシュガーなので、`1 < x < 100`は以下のように書き換えることができます。

```rb
1.<(x).<(100) # "1 < x < 100" と同じ
```

`1.<(x)`の戻り値は`true`になるので、Rubyは`true.<(100)`を実行しようとします。しかし、`true`に`<`というインスタンスメソッドは存在しないので、`NoMethodError`となるわけです。

## 2. `true`に`<`メソッドを追加する

存在しないなら作るまでです。`true`インスタンスから`<`を呼び出すために、`<`メソッドを定義します。

```rb
TrueClass.class_eval do
  def <(n)
    n
  end
end
```

これで、`true < 100`と書けるようになりました。`x = 50`の場合、`1 < x`の戻り値は`true`なので、`1 < x < 100`も評価可能になります。
しかし、この段階では式の実行を可能にしただけで、値の比較はできていません。

## 3. `Integer#<`の戻り値を改造する

先ほど定義した`TrueClass#<`の中で値を比較するためには、`1 < x`の戻り値である`true`から`x`を取り出す必要があります。`Integer#<`をオーバーライドして、これを実現可能にします。

```rb
Integer.class_eval do
  def <(n)
    if (self <=> n) == -1
      TrueClass.class_eval("def value; #{n};end")
      true
    else
      false
    end
  end
end

TrueClass.class_eval do
  def <(n)
    value < n
  end
end
```

ここでは、`TrueClass#value`メソッドを定義することで、`<`で評価した右辺の値を`true.value`でとり出せるようにしています。前述した`TrueClass`のコードも、これに対応させて書き直してあります。

これで、`1 < x < 100`が`true` or `false`を返すようになりました！

```rb
x = 50

1 < x < 100 # true
1 < x < 25 # false
```

ただし、このままだと`1 < x`がfalseの場合に対応できていなかったりと様々な問題があるので、諸々の調節が必要です。

## 4. `<`以外のオペレータへの対応・調整

ここまでで解説したコードに必要な調整を加え、`<`、`>`、`<=`、`>=`4つのオペレーターに対応させたコードが以下になります。

```rb
{ '<' => [-1], '>' => [1], '<=' => [-1, 0], '>= ' => [1, 0]}.each do |ope, v|
  [TrueClass, FalseClass].each do |klass|
    klass.class_eval("def #{ope}(num); self ? value #{ope} num : false;end")
  end
  [Integer, Float].each do |klass|
    klass.class_eval do
      define_method(ope) do |n|
        return false unless v.include? (self <=> n)
        TrueClass.class_eval("def value; #{n};end")
        true
      end
    end
  end
end
```

今回は、`Integer`と`Float`にのみ対応させてあります。以下の式はいずれも正確に判定可能です。

```rb
1 < 3 <= 3 < 3.5 < 5 < 5.001 # => true

1 < 3 <= 3 < 3.5 < 5.1 < 5.001 # => false

100 > 40 <= 40 < 50 >= 1 > -100 # true
```

# 最後に
同様の手法で、`Time`や`Date`の範囲チェックもできるようにすれば、かなり便利になりそうです。ただし、この改造が及ぼす副作用に関してはあまり検証をしていないので、思わぬところで問題が発生するかもしれません(例えば、この改造がスレッドセーフなのかとか)。

最後になりますが、[PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています](https://www.theport.jp/recruit/)(Rubyエンジニア以外も)。
興味のある方は、[masa2229](https://twitter.com/masa2229)に連絡するといいかもしれません。

[PORT Advent Calendar](https://qiita.com/advent-calendar/2017/port)は明日以降も続きます。
明日は私の新卒メンターだった@masa2229が素晴らしい記事を書いてくれます。