概要
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]