skip to content

Standard Containers

Ruby's core containers are Array, Hash, Range and Set, with stack, queue and binary-search idioms built on them. Interviewers probe shared default objects, slicing and membership cost.

on this pageshow

explore

questions

23

In Ruby, what do row[-1], row[1, 2] and row[1..2] return, and what happens when an index is out of range?

level: juniorimportance: must knowfreq 72%

answer

  1. negative counts back from the end
  2. second integer is a length
  3. two dots include the end
  4. start equal to size gives []
  5. start past size gives nil

basics

~20 s

row[-1] is the last element; row[1, 2] and row[1..2] both return a new two-element array starting at index 1. An out-of-range index returns nil, and so does a slice whose start lies beyond the array's size.

solid answer

~40 s

`Array#[]` with one integer returns one element, counting back from the end when the index is negative, so `row[-1]` is the last seat. An out-of-range index returns `nil` rather than raising; `Array#fetch` is the variant that raises `IndexError`. With two integers the second is a **length**: `row[1, 2]` returns a new array of up to two elements from index 1, truncated at the end. With a range, `1..2` includes the end and `1...3` excludes it, and a negative range end counts from the back, so `row[1..-2]` drops the first and last seats. The edges: a start equal to `row.size` returns `[]`, a start beyond it returns `nil`, and a negative length returns `nil`.

code

ruby · 15 lines
ruby
row = %w[A1 A2 A3 A4 A5]

row[-1]      # => "A5"
row[99]      # => nil
row[1, 2]    # => ["A2", "A3"]
row[1..2]    # => ["A2", "A3"]
row[1...3]   # => ["A2", "A3"]
row[1..-2]   # => ["A2", "A3", "A4"]
row[3, 10]   # => ["A4", "A5"]

row[5, 1]    # => []   start == size
row[6, 1]    # => nil  start > size
row[1, -1]   # => nil  negative length

row.fetch(9) # IndexError: index 9 outside of array bounds: -5...5

go deeper

for a junior

Recall that a second integer is a length, that two dots include the end and three exclude it, and that a negative index counts back from the last element.

for a middle

Explain the edge where a start equal to the size gives an empty array but one step further gives nil, and when to reach for Array#fetch instead.

for a senior

Show how nil from an out-of-range read hides bugs when nil is also a valid element, and choose fetch or explicit bounds checks where silence would corrupt data.

for a principal

Weigh lenient nil-returning reads against strict fetch in shared code, and set a convention so callers are not guessing which one a helper uses.

## One index: an element or nil Ruby's `Array#[]` (also available as `Array#slice`) answers several different questions depending on its arguments. With a **single integer** it returns one element: - A **non-negative index** counts from the front: `row[0]` is the first element. - A **negative index** counts from the back: `row[-1]` is the last element, `row[-2]` the one before it. - An **out-of-range index**, positive or negative, returns `nil`. No exception is raised. That last point matters because `nil` is also a legitimate element. In a theatre seating chart where an empty seat is stored as `nil`, `row[7]` returning `nil` cannot tell you whether seat 8 is empty or does not exist. When a missing index is a bug, use `Array#fetch`: it raises `IndexError` (for example `index 9 outside of array bounds: -5...5`), or returns a default you pass as a second argument or a block's result. ## Two integers: start and length With **two integers**, `row[start, length]`, the second argument is a count, not an end index. The result is always a **new array**: - `row[1, 2]` returns two elements beginning at index 1. - If `start + length` runs past the end, the slice is truncated: `row[3, 10]` returns whatever is left from index 3. - A negative `start` counts from the back: `row[-2, 2]` returns the last two elements. - A negative `length` returns `nil`. ## Range slices A **range** gives start and end positions instead of a count. | Expression (row has 5 elements) | Meaning | Result | |---|---|---| | `row[1..2]` | indexes 1 through 2 | two elements | | `row[1...3]` | indexes 1 up to, not including, 3 | two elements | | `row[1..-2]` | index 1 through the second-to-last | three elements | | `row[2..]` | index 2 to the end (endless range) | three elements | | `row[-2..1]` | start after end | `[]` | Two dots **include** the end, three dots **exclude** it. A negative range end is resolved against the array's size before slicing, which is why `row[1..-2]` is the idiom for "everything but the first and last". Arrays can also be sliced with an arithmetic sequence: `row[(0..).step(2)]` returns every other element. Unlike a range slice, a sequence that runs past the array raises `RangeError` instead of being truncated. ## The edge cases interviewers ask about 1. **Start equal to the size.** `row[5, 1]` on a five-element array returns `[]`. Position 5 is where the next element would go, so a slice there is valid and empty. `row[5..]` returns `[]` for the same reason. 2. **Start beyond the size.** `row[6, 1]` and `row[6..]` return `nil`. 3. **Negative start beyond the front.** `row[-6, 1]` on a five-element array returns `nil`. 4. **Negative length.** `row[1, -1]` returns `nil`. The asymmetry between cases 1 and 2 is the classic trap: code that tests `if slice.empty?` crashes with `NoMethodError` when the slice is `nil`. ## Slices are new arrays A slice is a new `Array` object, so appending to it leaves the original array's length alone. The elements themselves are the **same objects**, not copies. To change the original through a slice position, assign to the slice: - `row[1, 2] = ["X"]` replaces two elements with one, shrinking the array. - `row[1, 0] = ["X", "Y"]` inserts without removing anything. - `row[7] = "A8"` on a five-element array pads indexes 5 and 6 with `nil`. ## Everyday slicing idioms - **Last n elements:** `row[-3..]` or `row.last(3)`. - **Drop the header row of a chart:** `chart[1..]`. - **A window of seats:** `row[start, width]`, which is truncated at the end of the row rather than padded. - **Every other seat:** `row[(0..).step(2)]`. Prefer the start-and-length form when the width is what you know, and a range when both ends are known. Mixing them up is a common slicing bug in review: `row[2, 4]` is four elements from index 2, not indexes 2 through 4. ## Literals you will see in these examples `%w[A1 A2 A3]` builds an array of strings split on whitespace, with no commas or quotes and no interpolation (`%W` interpolates). `%i[a b]` builds an array of symbols. A seat label containing a space needs a backslash: `%w[Box\ 1 A2]` gives `["Box 1", "A2"]`.

  • Why does row[row.size, 1] return [] while row[row.size + 1, 1] returns nil?
    Index `row.size` is the position just past the last element, where the next element would be appended, so a slice starting there is valid and empty. One step further is outside the array altogether, and `Array#[]` signals that with `nil`. Code that calls `.empty?` or `.each` on a slice must handle both.
  • How do you make an out-of-range read fail loudly instead of returning nil?
    Use `Array#fetch`. `row.fetch(9)` on a five-element array raises `IndexError` with `index 9 outside of array bounds: -5...5`. `row.fetch(9, :none)` returns the default, and `row.fetch(9) { |i| ... }` returns the block's value. Negative indexes count from the end, as with `[]`.
  • What does %w[A1 A2 A3] build, and when does it trip you up?
    An array of three strings, split on whitespace with no quotes or commas. `%w` does not interpolate, so `%w[#{seat}]` gives the literal text; use `%W` for interpolation. A label containing a space must escape it: `%w[Box\ 1]`. `%i[...]` is the same literal for symbols.

saying these in an interview costs you the question

  • Reading row[99] on a short array raises an IndexError
  • row[2, 3] returns the elements from index 2 through index 3
  • 1...3 includes index 3 because three dots make a longer range
  • A slice starting exactly at row.size returns nil
  • Negative indexes are invalid in Ruby arrays and raise an error
  • Appending to a slice also appends to the original array
open as a page

In Ruby, how does Hash#fetch differ from Hash#[] when a key is missing, and what do fetch's default argument and block do?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Hash#[] returns the hash's default, nil unless one was set, for a missing key. Hash#fetch raises KeyError instead, or returns its second argument or its block's value, and it ignores the hash's default and default proc.

open as a page

In Ruby, how do you use an Array as a stack and as a queue, and what do push, pop, shift and unshift return?

level: juniorimportance: must knowfreq 60%

basics

~10 s

In single-threaded Ruby an Array is both: push with pop is last-in-first-out, push with shift is first-in-first-out. push and unshift return the array; pop and shift return the removed element, or nil when empty.

open as a page

In Ruby, what is the difference between 1..10 and 1...10, and what do endless (5..) and beginless (..5) ranges mean?

level: juniorimportance: must knowfreq 65%

basics

~20 s

1..10 includes 10; 1...10 stops before it, and exclude_end? reports which. An endless range like (5..) has a nil end and never stops; a beginless range like (..5) has a nil begin and supports comparisons but not iteration.

open as a page

In Ruby, why does a seating chart built with Array.new(3, []) seat a guest in every row at once, and how do you fix it?

level: middleimportance: must knowfreq 68%

basics

~20 s

Array.new(3, []) stores the same array object at all three indexes, so appending to one row with << changes every row. Array.new(3) { [] } runs the block once per index and builds three distinct rows.

open as a page

In Ruby, why does Hash.new([]) lose appended words while Hash.new(0) counts correctly, and what does a default proc change?

level: middleimportance: must knowfreq 70%

basics

~20 s

Hash.new(obj) returns one shared obj for any missing key and stores nothing. counts[w] += 1 works because += assigns; h[k] << x mutates the shared array and adds no key. Hash.new { |h, k| h[k] = [] } stores a fresh value per key.

open as a page

In Ruby 4.0, what do Set#|, &, -, ^ and subset? return, and how do they differ from Array's |, & and - operators?

level: middleimportance: must knowfreq 55%

basics

~20 s

Set#| is union, & intersection, - difference and ^ symmetric difference, each returning a new Set, and subset? (alias <=) requires a Set argument. Array's |, & and - return Arrays in the receiver's order, and Array#- keeps duplicates.

open as a page

In Ruby 4.0, how do Array#first, last, take and drop differ in what they return, and what does the new Array#rfind add?

level: juniorimportance: should knowfreq 42%

basics

~20 s

first and last with no argument return one element or nil; with a count they, like take and drop, return a new array and never nil. Ruby 4.0 adds Array#rfind, which returns the last element matching a block.

open as a page

In Ruby, in what order does a Hash return its entries, and what happens to that order when you update or re-add a key?

level: juniorimportance: should knowfreq 40%

basics

~20 s

A Ruby Hash presents entries in the order their keys were first inserted. Assigning a new value to an existing key keeps its position; deleting the key and adding it again moves it to the end.

open as a page

In Ruby, what does the hash literal {title:, rating:} expand to, and where does each value come from?

level: juniorimportance: should knowfreq 38%

basics

~20 s

{title:, rating:} is shorthand for {title: title, rating: rating}: each key is a symbol and its value is what the bare name evaluates to, a local variable if one exists, otherwise a method call. It needs Ruby 3.1 or later.

open as a page

In Ruby, what do Array#compact and Array#compact! each return, and what does compact! return when there is no nil to remove?

level: middleimportance: should knowfreq 50%

basics

~20 s

Array#compact returns a new array without nil elements and leaves the receiver alone. compact! removes nils in place and returns self, but returns nil when nothing was removed. flatten and flatten! follow the same pattern.

open as a page

In Ruby, what do Hash#transform_keys and transform_values return, and what happens when two old keys map to the same new key?

level: middleimportance: should knowfreq 45%

basics

~20 s

transform_keys returns a new hash with each key replaced by the block's result and the values kept; transform_values keeps the keys and replaces each value. If two old keys map to the same new key, the later entry silently overwrites the earlier.

open as a page

In Ruby, why does [0, 4, 7, 10, 12].bsearch { |x| x == 4 } return nil, and how should find-minimum mode be used?

level: middleimportance: should knowfreq 30%

basics

~20 s

Find-minimum mode needs a block that is false for a prefix and true for the rest; bsearch returns the first true element. x == 4 is true at one spot only, so the probes skip it; use x >= 4, then compare.

open as a page

In Ruby, how do Range#include? and Range#cover? differ, and why can ('1'..'10').include?('5') and cover?('5') disagree?

level: middleimportance: should knowfreq 45%

basics

~10 s

For numeric ranges the two agree. For String ranges, include? walks the String#succ sequence while cover? only checks begin <= value <= end with <=>, so ('1'..'10').include?('5') is true but cover?('5') is false.

open as a page

In Ruby 3.3 and later, how does Range#overlap? decide whether two appointment slots overlap, and how do exclusive ends change the answer?

level: middleimportance: should knowfreq 35%

basics

~10 s

Range#overlap?, added in Ruby 3.3, returns true when two ranges share at least one value. Back-to-back exclusive slots such as (540...600) and (600...660) do not overlap, while an inclusive (540..600) touches 600 and does.

open as a page

A Ruby Hash keyed by word-pair arrays stops finding entries after the key arrays are modified; why, and what do rehash and compare_by_identity change?

level: seniorimportance: should knowfreq 28%

basics

~20 s

A key is filed under its hash value when inserted; mutating an array key changes that value, so lookups search the wrong place and miss. rehash rebuilds the index from current values. compare_by_identity matches keys by object identity instead of hash and eql?.

open as a page

In CRuby, is Array#shift O(n) on a large Array used as a FIFO job queue, and what keeps push/shift and unshift cheap?

level: seniorimportance: should knowfreq 30%

basics

~20 s

No. In CRuby, shift on an Array of 16 or more elements advances a start pointer into a shared buffer instead of moving elements, and push refills the buffer's end. Large arrays also reserve front room, so repeated unshift is amortized cheap.

open as a page

In Ruby, a recursive walk over a deep undo history raises SystemStackError; why does rescue => e miss it, and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

SystemStackError (stack level too deep) inherits directly from Exception, so a bare rescue, which catches StandardError, misses it. The fix is to make depth independent of data: replace the recursion with a loop over an explicit Array stack.

open as a page

In Ruby, a booking check calls blocked_dates.include?(date) on a 50,000-element Array per request; why is that slow, and when do uniq, Set or Hash fit?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Array#include? scans every element with ==, so each lookup is linear. uniq only removes duplicates and still returns an Array. A Set built once, or a Hash when each date needs a value, answers by hash lookup in roughly constant time.

open as a page

In Ruby, how does Array#bsearch's find-any mode work, and why must the block return target <=> element rather than element <=> target?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

When the block returns numbers, bsearch runs in find-any mode: 0 means a match, positive means look further right, negative means look left. target <=> element has that sign; element <=> target is reversed and steers away from the target.

open as a page

In Ruby, what do (1..10).step(3) and (1..10) % 3 return, and what did Ruby 3.4 change about Range#step?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Without a block both return an Enumerator::ArithmeticSequence yielding 1, 4, 7, 10; % is step without a default argument. Since Ruby 3.4, Range#step advances by calling + for every type, not only numbers.

open as a page

A Ruby job builds every row, seat, price band and show date combination with Array#product and memory spikes; why, and what does passing a block change?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Without a block, Array#product builds one result array holding a new sub-array per combination, so memory grows with the product of all input sizes. With a block it yields each combination and returns the receiver, building no result array.

open as a page

In CRuby, what does the tailcall_optimization compile option of RubyVM::InstructionSequence do, how is it enabled, and why is it off by default?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

It makes a call in tail position replace the current frame, so tail recursion stops growing the stack. It is off by default; set RubyVM::InstructionSequence.compile_option = { tailcall_optimization: true } before the code is compiled.

open as a page