skip to content

Arrays & Slicing

Ruby arrays grow on demand, take negative indexes and range slices, and many methods come as a returning and a bang pair. Interviewers probe Array.new(3, []) sharing one object.

on this pageshow

explore

questions

5

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, 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 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, 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

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