Posted at
PORTDay 8

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

More than 1 year has passed since last update.

この記事はPORT Advent Calendar8日目の記事(実質1日目)です。

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


概要

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

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

x = 25

y = 75

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


はじめに

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

x = 50

# 例1
x > 1 && x < 100 # => true

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

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

x = 50

# こんな書き方はできない
if 1 < x < 100 # x in 1 ~ 100
print "x in 1 ~ 100"
end

しかし当然ではありますが、このような書き方は標準のRubyでは使うことができません。

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


実際のコード

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

コード(折りたたんであります)

{ '<' => [-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



解説


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

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

x = 50

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

SyntaxErrorではなくNoMethodErrorになりました。つまり、この構文はRubyでは使用可能だということです。

<.<のシンタックスシュガーなので、1 < x < 100は以下のように書き換えることができます。

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

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


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

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

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#<をオーバーライドして、これを実現可能にします。

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 < 100true or falseを返すようになりました!

x = 50

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

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


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

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

{ '<' => [-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

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

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


最後に

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

最後になりますが、PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています(Rubyエンジニア以外も)。

興味のある方は、masa2229に連絡するといいかもしれません。

PORT Advent Calendarは明日以降も続きます。

明日は私の新卒メンターだった@masa2229が素晴らしい記事を書いてくれます。