How Does Ruby Handle Array Range Accessing

How does ruby handle array range accessing?

This is a known ugly odd corner. Take a look at the examples in rdoc for Array#slice.

This specific issue is listed as a "special case"

   a = [ "a", "b", "c", "d", "e" ]
a[2] + a[0] + a[1] #=> "cab"
a[6] #=> nil
a[1, 2] #=> [ "b", "c" ]
a[1..3] #=> [ "b", "c", "d" ]
a[4..7] #=> [ "e" ]
a[6..10] #=> nil
a[-3, 3] #=> [ "c", "d", "e" ]
# special cases
a[5] #=> nil
a[5, 1] #=> []
a[5..10] #=> []

If the start is exactly one item beyond the end of the array, then it will return [], an empty array. If the start is beyond that, nil. It's documented, though I'm not sure of the reason for it.

How to select array elements in a given range in Ruby?

You can use ranges in the array subscript:

arr[100..200]

Why does a Ruby array allow access to invalid range index?

Ruby is not a static language, forcing you to pre-declare the size of the array. It expands arrays as you assign to a particular element.

Normally we'd append to the end of the array:

array = []       # => []
array << 1 # => [1]
array += [2] # => [1, 2]
array.push(3) # => [1, 2, 3]

Or push onto the front of it:

array.unshift(0) # => [0, 1, 2, 3]

to add elements, which keeps the array accumulating the values without gaps.

We can do it randomly too, which can be useful:

array[10] = 10 # => 10
array # => [0, 1, 2, 3, nil, nil, nil, nil, nil, nil, 10]

And that's what you've encountered.


You can predefine the array to a size, but it remains dynamic:

ary = Array.new(10, nil) # => [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
ary[0] = 0 # => 0
ary[10] = 10 # => 10
ary # => [0, nil, nil, nil, nil, nil, nil, nil, nil, nil, 10]
ary[12]=12 # => 12
ary # => [0, nil, nil, nil, nil, nil, nil, nil, nil, nil, 10, nil, 12]

How do you check an array for a range in Ruby?

Edit 2: This is my absolutely final solution:

require 'set'
STRAIGHTS = ['A',*2..9,'T','J','Q','K','A'].each_cons(5).map(&:to_set)
#=> [#<Set: {"A", 2, 3, 4, 5}>, #<Set: {2, 3, 4, 5, 6}>,
# ...#<Set: {9, "T", "J", "Q", "K"}>, #<Set: {"T", "J", "Q", "K", "A"}>]

def straight?(hand)
STRAIGHTS.include?(hand.to_set)
end

STRAIGHTS.include?([6,3,4,5,2].to_set)
# STRAIGHTS.include?(#<Set: {6, 3, 4, 5, 2}>)
#=> true

straight?([6,5,4,3,2]) #=> true
straight?(["T","J","Q","K","A"]) #=> true
straight?(["A","K","Q","J","T"]) #=> true
straight?([2,3,4,5,"A"]) #=> true

straight?([6,7,8,9,"J"]) #=> false
straight?(["J",7,8,9,"T"]) #=> false

Edit 1: @mudasobwa upset the apple cart by pointing out that 'A',2,3,4,5 is a valid straight. I believe I've fixed my answer. (I trust he's not going to tell me that 'K','A',2,3,4 is also valid.)

I would suggest the following:

CARDS     = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
STRAIGHTS = CARDS.each_cons(5).to_a
#=>[[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8],
# [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"],
# [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"],
# ["T", "J", "Q", "K", "A"]]

def straight?(hand)
(hand.map {|c| CARDS.index(c)}.sort == [0,1,2,3,12]) ||
STRAIGHTS.include?(hand.sort {|a,b| CARDS.index(a) <=> CARDS.index(b)})
end

Array of indexes to array of ranges

(New and improved. Stays fresh in your refrigerator for up to two weeks!):

a = [1, 2, 3, 10, 11, 20, 20, 4]

ranges = a.sort.uniq.inject([]) do |spans, n|
if spans.empty? || spans.last.last != n - 1
spans + [n..n]
else
spans[0..-2] + [spans.last.first..n]
end
end

p ranges # [1..4, 10..11, 20..20]

How do I summarize array of integers as an array of ranges?

Functional approach using Enumerable#chunk:

ranges = [1, 2, 4, 5, 6, 7, 9, 13]
.enum_for(:chunk) # .chunk for Ruby >= 2.4
.with_index { |x, idx| x - idx }
.map { |_diff, group| [group.first, group.last] }

#=> [[1, 2], [4, 7], [9, 9], [13, 13]]

How it works: once indexed, consecutive elements in the array have the same x - idx, so we use that value to chunk (grouping of consecutive items) the input array. Finally we just need to take the first and last elements of each group to build the pairs.

Efficiency of Arrays vs Ranges in Ruby

TL;DR

Ranges are generally faster and more memory-efficient than reifying Arrays. However, specific use cases may vary.

If in doubt, benchmark. You can use irb's relatively new measure command, or use the Benchmark module to compare and contrast different approaches. In general, reifying a Range as an Array takes more memory and is slower than comparing against a Range (or even a small Array of Range objects), but unless you loop over this code a lot this seems like a premature optimization.

Benchmarks

Using Ruby 3.1.0, the Range approach is around 3,655.77% faster on my system. For example:

require 'benchmark'

n = 100_000

Benchmark.bmbm do
_1.report("Range") do
n.times do
client_error = [200..299, 400..499]
client_error.include? 404
end
end

_1.report("Array") do
n.times do
client_error = [*(200..299), *(400..499)]
client_error.include? 404
end
end
end
Rehearsal -----------------------------------------
Range 0.022570 0.000107 0.022677 ( 0.022832)
Array 0.707742 0.041499 0.749241 ( 0.750012)
-------------------------------- total: 0.771918sec

user system total real
Range 0.020184 0.000043 0.020227 ( 0.020245)
Array 0.701911 0.037541 0.739452 ( 0.740037)

While the overall total times are better with Jruby and TruffleRuby, the performance differences between the approaches are only about 3-7x faster with Ranges. Meanwhile, Ruby 3.0.1 shows an approximate 37x speed improvement using a non-reified Range rather than an Array, so the Range approach is the clear winner here either way.

Your specific values will vary based on system specs, system load, and Ruby version and engine. For smaller values of n, I can't imagine it will make any practical difference, but you should definitely benchmark against your own systems to determine if the juice is worth the squeeze.

Ruby - Arrays, bounds, and raising exceptions

Accessing an array element that's outside the range of existing elements returns nil. That's just the way Ruby works.

You could add the following line before the "puts" to trap that condition...

raise StandardError if index.to_i >= arr.size


Related Topics



Leave a reply



Submit