Ruby

Ruby Rangeクラスで範囲を扱うTips

概要

RubyのRangeクラスを雰囲気で使っていて、知識が曖昧だったので使い方の備忘録を残す。

前提

$ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]

.. と ... の違い

p (1..5).to_a  # [1,2,3,4,5]
p (1...5).to_a # [1,2,3,4]

.. は、終端の数字を含める。
... は、終端の数字を含めない。

なんとなく点が多いほうが範囲が広いイメージもあって覚えられない。

Range.new

... と .. がどうしても覚えられないならRangeクラスのイニシャライザが使える。

p Range.new(1, 5, false).to_a # [1,2,3,4,5]
p Range.new(1, 5, true).to_a  # [1,2,3,4]

第三引数はexclude_end(終端を除外するのか)を指定するので、falseなら含む、trueなら除外する。
…どのみちtrueにしたらどうなるのか覚えてなきゃならないのであんまり意味がない。

日付でも使える

ちゃんと日にちベースで範囲が取れる。

require 'date'
range = (Date.parse('2018/02/26')..Date.parse('2018/03/02'))
range.each do |date|
  p date.to_s
end
"2018-02-26"
"2018-02-27"
"2018-02-28"
"2018-03-01"
"2018-03-02"

文字なんかもお手のもの

1文字はもちろん2文字以上でも可能。

p ('AA'..'BZ').to_a
["AA", "AB", "AC", "AD", "AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP", "AQ", "AR", "AS", "AT", "AU", "AV", "AW", "AX", "AY", "AZ", "BA", "BB", "BC", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BK", "BL", "BM", "BN", "BO", "BP", "BQ", "BR", "BS", "BT", "BU", "BV", "BW", "BX", "BY", "BZ"]

日本語もイケるけど、辞書順なので微妙。

p ('あ'..'こ').to_a
["あ", "ぃ", "い", "ぅ", "う", "ぇ", "え", "ぉ", "お", "か", "が", "き", "ぎ", "く", "ぐ", "け", "げ", "こ"]

自作のクラスでもできる

自分で作ったクラスに、succと、<=>を実装すれば、自由にRangeを扱えるように出来る。

class Number
  attr_reader :value

  def initialize(number)
    @value = number
  end

  def <=> (other)
    @value <=> other.value
  end

  def succ
    Number.new(@value * 5)
  end
end

(Number.new(1)..Number.new(200)).each do |number|
  p number.value
end
1
5
25
125
  • Numberクラスは、数値でオブジェクトを生成でき、valueでそれを参照できる
  • Numberオブジェクト同士は、valueの大小で比較できる
  • succメソッドは、valueを5倍した数値で新たにNumberオブジェクトを作成して戻す

以上を元に、(Number.new(1)..Number.new(200)) でRangeを作成すると、valueが5倍ずつ増えるNumberオブジェクトのRangeになる。

ちなみにRangeは遅延評価になっているので、

def initialize(number)
  p 'initialize!!'
  @value = number
end
(Number.new(1)..Number.new(200))

とRangeを定義しただけの場合、先頭と終端で2回のみオブジェクトが生成される。to_aしたりeachしたりするタイミングで、範囲に含まれるオブジェクトも生成される。

"initialize!!"
"initialize!!"

Rangeに特定の値が含まれているか調べる①

Range#member? メソッドを使う。

p (1..100).member? 100   # true
p (1...100).member? 100  # false

Range#include? や Range#=== も同様。 === はどこか不自然に見える。

range = (1..100)
p range.include? 50   # true
p range.include? 200  # false
p range === 30        # true
p range === 0         # false

ちなみに先程のNumberクラスを使って以下のようにした場合、falseになる。見た感じRangeに含まれてそうだが、異なるオブジェクトを指してるのでfalseになる。

range = (Number.new(1)..Number.new(200))
p range.member? Number.new(1) # false

Rangeに特定の値が含まれているか調べる①

Range#cover? という似たようなメソッドがあるが、こちらはmember?とはRangeに含まれているかの判別方法が異なる。

まず、member? の挙動を見てみる。member? は、succメソッドに基いて生成されるRangeを対象に、それが存在するかを判別する。
そのため、下記の例では、'b'は存在するが、'b!'という文字列は存在しないので後者はfalseを戻す。

range = ('a'..'d')
p range.to_a   # ['a', 'b', 'c', 'd']

p range.member? 'b'  # true
p range.member? 'b!' # false

対してcover?の場合は、succメソッドに関係なく、<=>メソッドを用いて比較した場合に、先頭と終端の間にあるかを判別する。
下記のRangeは文字列のRangeのため、<=>メソッドは辞書順比較を行う。そのため、'b!'は辞書順で'a'以上'd'未満を満たすことからtrueを戻す。

range = ('a'..'d')
p range.to_a   # ['a', 'b', 'c', 'd']

p range.cover? 'b'  # true
p range.cover? 'b!' # true

Range内の特定の値を取る

Range#firstやRange#endを使う。

range = (1..100)
p range.first    # 1
p range.first(3) # [1,2,3]
p range.last     # 100
p range.last(3)  # [98,99,100]

参考