LoginSignup
14
4

More than 5 years have passed since last update.

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

Posted at

この記事は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が素晴らしい記事を書いてくれます。

14
4
2

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
14
4