💎

【Ruby 17日目】基本文法 - 範囲(Range)

に公開

はじめに

Rubyの範囲(Range)について、基本的な使い方から実践的なテクニックまで詳しく解説します。

範囲は、開始値と終了値で定義される連続した値の集合を表現する便利なオブジェクトです。Ruby 3.4でも引き続き重要な役割を果たしています。

基本概念

範囲(Range)の主な特徴:

  • 連続した値の表現 - 開始値と終了値で範囲を定義
  • 包含範囲と排他範囲 - 終了値を含む/含まない範囲
  • メモリ効率 - 範囲全体を展開せずに表現できる
  • 様々な型に対応 - 整数、文字、日付など比較可能なオブジェクト
  • イテレーション可能 - eachやmapなどのメソッドが使える

範囲の作成

# 包含範囲(.. を使用)- 終了値を含む
range1 = 1..5
puts range1.to_a.inspect  #=> [1, 2, 3, 4, 5]

# 排他範囲(... を使用)- 終了値を含まない
range2 = 1...5
puts range2.to_a.inspect  #=> [1, 2, 3, 4]

# Rangeクラスを使用
range3 = Range.new(1, 5)        # 包含範囲
range4 = Range.new(1, 5, true)  # 排他範囲(第3引数がtrue)
puts range3.to_a.inspect  #=> [1, 2, 3, 4, 5]
puts range4.to_a.inspect  #=> [1, 2, 3, 4]

# 文字の範囲
alpha_range = 'a'..'e'
puts alpha_range.to_a.inspect  #=> ["a", "b", "c", "d", "e"]

# 逆順の範囲(作成は可能だが注意が必要)
reverse = 5..1
puts reverse.to_a.inspect  #=> [](展開すると空配列)

範囲の基本操作

range = 1..10

# 範囲の開始値と終了値
puts range.begin  #=> 1(first のエイリアス)
puts range.end    #=> 10(last のエイリアス)
puts range.first  #=> 1
puts range.last   #=> 10

# 最初のN個、最後のN個
puts range.first(3).inspect  #=> [1, 2, 3]
puts range.last(3).inspect   #=> [8, 9, 10]

# 排他範囲かどうか
puts (1..5).exclude_end?   #=> false
puts (1...5).exclude_end?  #=> true

# 要素数(整数範囲のみ)
puts (1..10).size   #=> 10
puts (1...10).size  #=> 9

# 配列に変換
puts (1..5).to_a.inspect  #=> [1, 2, 3, 4, 5]

# 範囲に値が含まれるか
puts (1..10).include?(5)   #=> true
puts (1..10).include?(15)  #=> false
puts (1..10).cover?(5)     #=> true

# member? は include? のエイリアス
puts (1..10).member?(5)  #=> true

包含判定(include? vs cover?)

# include? - 実際に範囲内の値として存在するか
range = 1..10
puts range.include?(5)    #=> true
puts range.include?(5.5)  #=> false(整数範囲に5.5は含まれない)

# cover? - 値が範囲の境界内にあるか
puts range.cover?(5)      #=> true
puts range.cover?(5.5)    #=> true(5.5は1と10の間にある)

# 文字列範囲での違い
str_range = 'a'..'z'
puts str_range.include?('abc')  #=> false('abc'は範囲を展開した中に存在しない)
puts str_range.cover?('abc')    #=> true('abc'は'a'と'z'の間にある)

# パフォーマンスの違い
# include? は範囲を展開して検索するため遅い
# cover? は境界チェックのみなので高速
large_range = 1..1_000_000
puts large_range.cover?(500_000)  # 高速

イテレーションと変換

# each - 各要素を繰り返し
(1..5).each { |n| puts n }
# 出力: 1, 2, 3, 4, 5

# map - 各要素を変換
squares = (1..5).map { |n| n ** 2 }
puts squares.inspect  #=> [1, 4, 9, 16, 25]

# select - 条件に一致する要素を抽出
evens = (1..10).select { |n| n.even? }
puts evens.inspect  #=> [2, 4, 6, 8, 10]

# reject - 条件に一致しない要素を抽出
odds = (1..10).reject { |n| n.even? }
puts odds.inspect  #=> [1, 3, 5, 7, 9]

# reduce - 畳み込み
sum = (1..10).reduce(:+)
puts sum  #=> 55

product = (1..5).reduce(:*)
puts product  #=> 120

# step - ステップを指定して繰り返し
(1..10).step(2) { |n| puts n }
# 出力: 1, 3, 5, 7, 9

puts (1..10).step(2).to_a.inspect  #=> [1, 3, 5, 7, 9]

よくあるユースケース

ケース1: 配列のスライス

実務でよく使われる配列操作のパターンです。

# 配列の部分取得
arr = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# インデックス範囲で取得
puts arr[2..5].inspect   #=> [30, 40, 50, 60]
puts arr[2...5].inspect  #=> [30, 40, 50]

# 負のインデックスも使える
puts arr[-3..-1].inspect  #=> [80, 90, 100]

# 範囲で置き換え
arr[2..4] = [300, 400, 500]
puts arr.inspect  #=> [10, 20, 300, 400, 500, 60, 70, 80, 90, 100]

# 文字列のスライス
str = "Hello, World!"
puts str[0..4]   #=> "Hello"
puts str[7..11]  #=> "World"
puts str[7..-2]  #=> "World"(最後の文字の1つ前まで)

# 部分文字列の置き換え
str = "Hello, World!"
str[7..11] = "Ruby"
puts str  #=> "Hello, Ruby!"

ケース2: 条件分岐とcase文

# case文での範囲使用
def categorize_score(score)
  case score
  when 90..100
    "A"
  when 80...90
    "B"
  when 70...80
    "C"
  when 60...70
    "D"
  else
    "F"
  end
end

puts categorize_score(95)  #=> "A"
puts categorize_score(85)  #=> "B"
puts categorize_score(75)  #=> "C"
puts categorize_score(50)  #=> "F"

# 年齢による分類
def age_category(age)
  case age
  when 0..12
    "子供"
  when 13..19
    "10代"
  when 20...30
    "20代"
  when 30...40
    "30代"
  when 40...65
    "中年"
  else
    "シニア"
  end
end

puts age_category(8)   #=> "子供"
puts age_category(25)  #=> "20代"
puts age_category(70)  #=> "シニア"

ケース3: 日付と時刻の範囲

require 'date'

# 日付の範囲
start_date = Date.new(2025, 1, 1)
end_date = Date.new(2025, 1, 7)
date_range = start_date..end_date

# 日付の繰り返し
date_range.each do |date|
  puts date.strftime("%Y-%m-%d (%a)")
end
# 出力:
# 2025-01-01 (Wed)
# 2025-01-02 (Thu)
# ...

# 特定の日付が範囲内か
target_date = Date.new(2025, 1, 5)
puts date_range.cover?(target_date)  #=> true

# 営業日のカウント(土日を除外)
weekdays = date_range.select { |d| !(d.saturday? || d.sunday?) }
puts "営業日数: #{weekdays.count}日"

# 月の範囲
def days_in_month(year, month)
  start_date = Date.new(year, month, 1)
  end_date = Date.new(year, month, -1)  # 月末
  start_date..end_date
end

january = days_in_month(2025, 1)
puts "1月の日数: #{january.size}日"  #=> 1月の日数: 31日

ケース4: 文字の範囲と文字列生成

# アルファベットの範囲
lowercase = ('a'..'z').to_a
puts lowercase.inspect  #=> ["a", "b", "c", ..., "z"]

uppercase = ('A'..'Z').to_a
puts uppercase.inspect  #=> ["A", "B", "C", ..., "Z"]

# ランダムな文字列生成
def random_string(length)
  chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
  Array.new(length) { chars.sample }.join
end

puts random_string(10)  #=> 例: "aBc3De7Fg9"

# パスワード生成
def generate_password(length = 12)
  lowercase = ('a'..'z').to_a
  uppercase = ('A'..'Z').to_a
  digits = ('0'..'9').to_a
  symbols = ['!', '@', '#', '$', '%', '&', '*']

  all_chars = lowercase + uppercase + digits + symbols

  # 最低1つずつ含める
  password = [
    lowercase.sample,
    uppercase.sample,
    digits.sample,
    symbols.sample
  ]

  # 残りをランダムに
  (length - 4).times { password << all_chars.sample }

  password.shuffle.join
end

puts generate_password  #=> 例: "aB3$9cDefGhI"

# 文字コードの範囲
hiragana = ("\u3041".."\u3096").to_a
puts hiragana.first  #=> "ぁ"
puts hiragana.last   #=> "ゖ"
puts hiragana.size   #=> 86

ケース5: 数値範囲の応用

# ページネーション
def pagination_range(current_page, total_pages, visible_pages = 5)
  half = visible_pages / 2
  start_page = [current_page - half, 1].max
  end_page = [start_page + visible_pages - 1, total_pages].min

  # 終了ページから逆算して開始ページを調整
  if end_page - start_page < visible_pages - 1
    start_page = [end_page - visible_pages + 1, 1].max
  end

  start_page..end_page
end

# 現在5ページ目、全10ページの場合
pages = pagination_range(5, 10, 5)
puts pages.to_a.inspect  #=> [3, 4, 5, 6, 7]

# 価格帯フィルタ
def filter_by_price(items, price_range)
  items.select { |item| price_range.cover?(item[:price]) }
end

products = [
  { name: "商品A", price: 1000 },
  { name: "商品B", price: 2500 },
  { name: "商品C", price: 5000 },
  { name: "商品D", price: 15000 }
]

affordable = filter_by_price(products, 0..3000)
puts affordable.map { |p| p[:name] }.inspect  #=> ["商品A", "商品B"]

# グループ分け
def group_by_range(numbers, range_size)
  min = numbers.min
  max = numbers.max
  ranges = []

  current = min
  while current <= max
    range_end = current + range_size - 1
    ranges << (current..range_end)
    current += range_size
  end

  ranges.map do |range|
    { range: range, values: numbers.select { |n| range.cover?(n) } }
  end
end

data = [1, 5, 15, 23, 34, 45, 56, 67, 78, 89, 95]
groups = group_by_range(data, 20)
groups.each do |g|
  puts "#{g[:range]}: #{g[:values].inspect}"
end
# 出力:
# 1..20: [1, 5, 15]
# 21..40: [23, 34]
# 41..60: [45, 56]
# ...

注意点とベストプラクティス

注意点

  1. 無限範囲の扱い
# Ruby 2.6以降、無限範囲が使える
infinite = (1..)      # 1から無限大まで
negative_infinite = (..10)  # 負の無限大から10まで

# 無限範囲は to_a できない
# infinite.to_a  #=> RangeError

# cover? は使える
puts infinite.cover?(1000000)  #=> true

# 配列のスライスで使える
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
puts arr[5..].inspect   #=> [6, 7, 8, 9, 10]
puts arr[..5].inspect   #=> [1, 2, 3, 4, 5, 6]
  1. 逆順範囲の注意
# BAD: 逆順範囲は展開すると空
reverse = 5..1
puts reverse.to_a.inspect  #=> []

# GOOD: reverseメソッドを使用
puts (1..5).to_a.reverse.inspect  #=> [5, 4, 3, 2, 1]

# または downto を使用
puts 5.downto(1).to_a.inspect  #=> [5, 4, 3, 2, 1]
  1. 浮動小数点数の範囲
# BAD: 浮動小数点数の範囲は繰り返しできない
# (1.0..5.0).each { |n| puts n }  #=> TypeError

# GOOD: stepを使用
(1.0..5.0).step(0.5) { |n| puts n }
# 出力: 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0

# または整数範囲をmapで変換
(10..50).step(5).map { |n| n / 10.0 }
#=> [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]

ベストプラクティス

  1. 包含範囲と排他範囲の使い分け
# GOOD: 配列のインデックスには排他範囲が自然
arr = [1, 2, 3, 4, 5]
puts arr[0...3].inspect  #=> [1, 2, 3](0から2まで、つまり3個)

# GOOD: 数値の範囲には包含範囲が自然
(1..10).each { |n| puts n }  # 1から10まで

# 年齢範囲などは排他範囲が便利
case age
when 0...13   # 0-12歳
  "子供"
when 13...20  # 13-19歳
  "10代"
end
  1. cover?とinclude?の使い分け
# GOOD: 大きな範囲ではcover?を使用(高速)
large_range = 1..1_000_000
puts large_range.cover?(500_000)  # O(1)

# GOOD: 正確な判定が必要な場合はinclude?
str_range = 'a'..'z'
puts str_range.include?('m')  # 正確に'a'から'z'までのアルファベットに含まれるか
  1. 範囲を定数として定義
# GOOD: よく使う範囲は定数化
VALID_PORT_RANGE = 1024..65535
WORKING_HOURS = 9..18
VALID_SCORE_RANGE = 0..100

def valid_port?(port)
  VALID_PORT_RANGE.cover?(port)
end

def working_hour?(hour)
  WORKING_HOURS.cover?(hour)
end
  1. メモリ効率を考慮
# GOOD: 大きな範囲は配列に変換せずに使用
(1..1_000_000).each { |n| process(n) }

# BAD: メモリを大量消費
# arr = (1..1_000_000).to_a  # 避ける

Ruby 3.4での改善点

  • 範囲オブジェクトのパフォーマンスが最適化
  • 無限範囲の扱いがより洗練
  • Prismパーサーにより範囲リテラルの解析が高速化
# Ruby 3.4での最適化例
require 'benchmark'

n = 1_000_000
Benchmark.bmbm do |x|
  x.report("Range#cover?:") { n.times { (1..1000).cover?(500) } }
  x.report("Range#include?:") { n.times { (1..1000).include?(500) } }
end
# cover? の方が圧倒的に高速

# 無限範囲の活用
def paginate(items, page:, per_page: 10)
  start_index = (page - 1) * per_page
  items[start_index, per_page]
end

# または無限範囲で
def paginate_v2(items, page:, per_page: 10)
  start_index = (page - 1) * per_page
  items[start_index..(start_index + per_page - 1)]
end

まとめ

この記事では、Rubyの範囲(Range)について以下の内容を学びました:

  • 範囲の基本概念(包含範囲と排他範囲)
  • 範囲の作成方法(..と...、Range.new)
  • 基本操作(begin、end、size、include?、cover?)
  • イテレーションと変換(each、map、select、step)
  • 実践的なユースケース(配列スライス、条件分岐、日付範囲、文字列生成、数値応用)
  • 注意点(無限範囲、逆順範囲、浮動小数点数)
  • ベストプラクティス(包含/排他の使い分け、cover?の活用、定数化、メモリ効率)

範囲は、連続した値を効率的に表現し、様々な場面で活用できる強力な機能です。適切に使うことで、読みやすく効率的なコードを書くことができます。

参考資料

Discussion