Arrays

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. An Array is Ruby’s ordered, integer-indexed, growable collection of object references. It is the collection the language reaches for first: String#split, Enumerable#map, Hash#to_a, every *rest parameter and every multiple assignment produce or consume one. This page covers the class in depth: how arrays are created and indexed, the exact slicing rules, every way to add, remove and replace elements, the methods Array defines itself and the ones it gets from Enumerable, searching (including binary search), sorting, set-like operations, flattening and combining, packing binary data with pack and unpack, equality and copying, destructuring, subclassing, and what changes when several threads or Ractors touch the same array. The literal syntax ([], %w, %i) is introduced in Literals, hash tables are in Hashes, the unordered unique collection is Sets, ranges (which share the slicing syntax) are in Ranges, and the lazy and external iteration protocol behind each_slice and friends is in Enumerators and Lazy Evaluation. Matching array shapes with case/in is the subject of Pattern Matching. For comparison with other languages, the Rust Vec is in Rust collections (Go slices have a page of their own on this site).

Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output shown in # => comments is the real output, copied from running the file. Examples that need the library (require "shelf") use the shelf catalogue used throughout this section (Shelf::Book, Shelf::Isbn, Shelf::Catalog; see the shelf gem source); the other examples are self-contained and can be run on their own.

What an Array is

An Array is an object of class Array that holds an ordered sequence of references to other objects. Four properties explain almost every behaviour on this page:

  • Ordered and indexed. Each element has an integer position starting at 0. Negative indexes count from the end (-1 is the last element). The order is the insertion order and is never rearranged unless you ask for it.

  • Heterogeneous. An array stores references, not values of one type, so [1, "two", :three, nil, [5, 6]] is valid. There is no element type to declare (a signature such as Array[String] exists only in RBS, see Type Checking with RBS, TypeProf, Steep and Sorbet).

  • Growable. There is no capacity to declare. Appending beyond the end grows the underlying storage automatically and, amortised over many appends, costs constant time per element.

  • A collection of references. Copying an array copies the references, not the objects they point to. This is the source of the shared-default pitfall and of the shallow-copy behaviour covered later in this page.

Array includes Enumerable, so map, select, sort_by, group_by, each_slice and the rest are available on every array. Array also defines its own versions of many of them (each, map, select, first, include?, sum, min, max, sort, uniq, zip, take, drop, …​) that are faster or that return an Array where the Enumerable version would return an Enumerator; Ruby 4.0 added Array#find and Array#rfind the same way (see the iteration section). Other core classes convert to and from arrays through the implicit to_ary and the explicit to_a, described in the conversion section and in Conversions and Coercion.

flowchart LR A["Array object"] --> E0["index 0 → reference"] A --> E1["index 1 → reference"] A --> E2["index 2 → reference"] E0 --> O0["a String"] E1 --> O1["an Integer"] E2 --> O2["another Array"] O2 --> O3["its own elements"] B["dup / clone"] -. "copies the three references only" .-> A

The diagram shows why dup is shallow: the copy gets its own row of references but the objects at the end of them (including the nested array) are shared.

Creating arrays

Literals, %w and %i

The array literal is a comma-separated list in square brackets; a trailing comma is allowed and the elements can span lines. %w[...] builds an array of strings split on whitespace (no interpolation, %W interpolates), and %i[...] builds an array of symbols (%I interpolates). Both are listed with the other % literals in Literals. RuboCop’s Style/WordArray and Style/SymbolArray ask for them once an array has several plain words or symbols (default threshold 2).

Array.new, Array(), Array.try_convert and the splat

Array.new accepts four forms: no arguments (an empty array, which RuboCop prefers written as []), a size with an optional single default object, a size with a block that computes each element from its index, and an existing array to copy. Array.[] is the method behind the literal (Array[1, 2] equals [1, 2]); RuboCop’s Style/RedundantArrayConstructor asks for the literal. Array() (a Kernel method) is the forgiving converter: nil becomes [], an Array is returned unchanged (the same object), anything responding to to_ary or to_a is converted, and everything else is wrapped in a one-element array. That makes it the idiomatic way to normalise an argument that may be one value or many. Array.try_convert calls only to_ary and returns nil instead of raising, which is what library code uses to detect "is this array-like?". The splat * inside a literal splices any to_a-able object (ranges, hashes, sets) into the new array.

# frozen_string_literal: true

# Creating arrays
titles = %w[Dune Emma Ulysses] # => ["Dune", "Emma", "Ulysses"]
words = %w[red green blue] # => ["red", "green", "blue"]
symbols = %i[open closed lost] # => [:open, :closed, :lost]
mixed = [1, "two", :three, 4.0, nil, [5, 6]] # => [1, "two", :three, 4.0, nil, [5, 6]]
[titles.class, titles.size, words.size, symbols.size, mixed.size] # => [Array, 3, 3, 3, 6]
Array.new(0) # => []
Array.new(3) # => [nil, nil, nil]
Array.new(3, 0) # => [0, 0, 0]
Array.new(4) { |i| i * i } # => [0, 1, 4, 9]
Array.new(words) # => ["red", "green", "blue"]
(1..5).to_a # => [1, 2, 3, 4, 5]
("a".."e").to_a # => ["a", "b", "c", "d", "e"]
[*1..3, *"x".."z"] # => [1, 2, 3, "x", "y", "z"]
Array(nil) # => []
Array(words).equal?(words) # => true
Array(1..3) # => [1, 2, 3]
Array("text") # => ["text"]
Array({ a: 1 }) # => [[:a, 1]]
Array.try_convert([1]) # => [1]
Array.try_convert("no") # => nil
"a,b,c".split(",") # => ["a", "b", "c"]
"abc".chars # => ["a", "b", "c"]
{ a: 1, b: 2 }.to_a # => [[:a, 1], [:b, 2]]

Array(1..3) expanded the range and Array({ a: 1 }) turned a hash into key/value pairs, while Array("text") did not split the string: Kernel#Array calls to_ary then to_a, and String defines neither. Strings are split explicitly with chars, lines or split.

The shared-default pitfall

Array.new(size, object) stores the same object in every slot. For an immutable default (0, nil, a symbol) that is harmless, but for a mutable one ([], {}, a String) every slot aliases the same object, so a change through one index appears in all of them. The block form evaluates the block once per slot and therefore creates a fresh object each time. The same trap exists for Hash.new([]), covered on Hashes and listed again in Common Mistakes and Gotchas.

# frozen_string_literal: true

# The shared-default pitfall
rows = Array.new(2, [])
rows[0] << "x"
rows.to_a # => [["x"], ["x"]]
rows[0].equal?(rows[1]) # => true
grid = Array.new(2) { [] }
grid[0] << "x"
grid.to_a # => [["x"], []]
text = Array.new(2, +"ab")
text[0] << "!"
text.to_a # => ["ab!", "ab!"]

With # frozen_string_literal: true a bare "ab" would be frozen, so the example uses +"ab" (a mutable copy) to show the aliasing: Array.new(2, +"ab") still holds a single string object twice. RuboCop does not flag this one, so the habit has to be: mutable default, use the block.

Indexing and slicing

The [] method (also spelled slice) is the single entry point for reading by position. Its result depends on what it is given:

Call Returns Out of range

a[i], a.at(i)

the element at index i; a negative i counts from the end

nil

a[start, length]

a new array of up to length elements beginning at start

[] when start == a.size, nil when start > a.size

a[range]

a new array covering the range (inclusive .., exclusive ..., endless 2.., beginless ..2)

same rule as start of the range

a[arithmetic_sequence] (3.0)

elements at every step-th position, for example a[(0..).step(2)] or a[(..-1) % 3]

RangeError when the sequence starts outside the array

a.first, a.first(n), a.last, a.last(n)

the first or last element, or an array of up to n of them

nil for the no-argument form on an empty array, [] for the counted form

a.values_at(*indexes)

an array of the elements at those indexes (ranges allowed)

nil in the position of an index that is out of range

a.fetch(i), a.fetch(i, default), a.fetch(i) { ... }

the element at i

IndexError unless a default or a block is given

a.fetch_values(*indexes) (3.4)

the elements at those indexes, as fetch would

IndexError

The two out-of-range rows for a[start, length] and a[range] are the ones that surprise people: a slice that starts exactly one past the last element is an empty array, not nil, because that position is a valid insertion point. Starting any further out returns nil. The figure shows the same rule: a slice names a start position and a length, and the position just after the last element (index 6 here) is still on the array.

A six element array with positive indexes 0 to 5 above the cells and negative indexes -6 to -1 below. Brackets show that a[1, 3] and a[1..3] select b, c and d, a[1…​3] selects b and c, a[4, 10] selects e and f, a[6, 1] is an empty array and a[7, 1] is nil.
# frozen_string_literal: true

# Indexing and slicing
a = %w[a b c d e f] # => ["a", "b", "c", "d", "e", "f"]
a[0] # => "a"
a[-1] # => "f"
a[6] # => nil
a[-7] # => nil
a.at(2) # => "c"
a.first # => "a"
a.first(2) # => ["a", "b"]
a.last(2) # => ["e", "f"]
a[1, 3] # => ["b", "c", "d"]
a[1..3] # => ["b", "c", "d"]
a[1...3] # => ["b", "c"]
a[2..] # => ["c", "d", "e", "f"]
a[..2] # => ["a", "b", "c"]
a[-3..] # => ["d", "e", "f"]
a[4, 10] # => ["e", "f"]
a[6, 1] # => []
a[7, 1] # => nil
a[6..] # => []
a[7..] # => nil
a[(0..).step(2)] # => ["a", "c", "e"]
a[(..-1) % 3] # => ["a", "d"]
a.values_at(0, 2, -1, 9) # => ["a", "c", "f", nil]

The arithmetic-sequence forms in the last two slice lines are Ruby 3.0 additions: (0..).step(2) and (..-1) % 3 build an Enumerator::ArithmeticSequence, and Array#[] accepts it. A negative step reverses the direction. The % on a range is a shorthand for step that exists for exactly this use.

fetch, dig, values_at and sampling

[] returns nil for a missing position, which cannot be told apart from a stored nil. When a missing position is a bug, use fetch: it raises IndexError with the valid bounds (index 9 outside of array bounds: -6...6), or returns a default or the value of a block. fetch_values (Ruby 3.4) is values_at with fetch semantics, and Ruby 4.0 made the error it raises name Array#fetch_values instead of an internal frame. dig follows a path through nested arrays and hashes and stops with nil at the first nil; the rules are described in the official dig methods page. sample and shuffle take a random: keyword so tests can seed them, which is how the output below is reproducible.

# frozen_string_literal: true

a = %w[a b c d e f] # => ["a", "b", "c", "d", "e", "f"]
a.fetch(1) # => "b"
a.fetch(9, "none") # => "none"
a.fetch(9) { |i| "no #{i}" } # => "no 9"
begin
  a.fetch(9)
rescue IndexError => e
  e.message # => "index 9 outside of array bounds: -6...6"
end
a.fetch_values(0, -1) # => ["a", "f"]
begin
  a.fetch_values(0, 9)
rescue IndexError => e
  e.message # => "index 9 outside of array bounds: -6...6"
end
a.take(2) # => ["a", "b"]
a.drop(4) # => ["e", "f"]
a.take_while { |s| s < "c" } # => ["a", "b"]
a.drop_while { |s| s < "e" } # => ["e", "f"]
a.sample.class # => String
a.shuffle(random: Random.new(1)) # => ["c", "b", "e", "a", "d", "f"]
a.sample(2, random: Random.new(7)) # => ["e", "b"]
nested = [[1, [2, 3]], { name: "Ana", tags: %w[x y] }]
nested.dig(0, 1, 0) # => 2
nested.dig(1, :tags, 1) # => "y"
nested.dig(0, 5, 0) # => nil
a.slice(1, 2) # => ["b", "c"]
copy = a.dup
copy.slice!(1, 2) # => ["b", "c"]
copy.join # => "adef"

The slice! call at the end is the destructive twin of slice: it removes the selected elements and returns them, so copy is left with the rest.

Modifying an array

Adding, removing and replacing

An array is mutable (unless frozen, see below), and most of its methods exist in a pure form and a mutating form. The mutating methods that add or remove elements return the receiver or the removed element, which decides how they chain:

Method Effect Returns

push(*objs), append, <<

add to the end (<< takes exactly one object)

the array

unshift(*objs), prepend

add to the front

the array

insert(index, *objs)

insert before index; a negative index counts from the end and inserts after that element

the array

concat(*arrays)

append the elements of every argument array

the array

pop(n=nil), shift(n=nil)

remove from the end or the front (one element, or an array of n)

the element or array, nil or [] when empty

delete(obj), delete(obj) { ... }

remove every element == to obj

obj or nil (the block’s value if given)

delete_at(i), slice!(...)

remove by position

the removed element or array, nil if out of range

delete_if, reject!, select!, keep_if, filter!

remove by predicate

the array (reject!, select! and filter! return nil if nothing changed)

compact, compact!

remove nil elements (but not false)

new array; compact! returns nil if nothing changed

clear, replace(other)

empty the array; replace its contents with another array’s

the array

fill(obj), fill { |i| ... }, fill(obj, start, length)

overwrite elements in place

the array

The bang-method rule is the most common trap here: methods that end in ! return nil when they made no change, so array.compact!.first raises NoMethodError on a clean array. Do not chain them; call them as statements and use the array afterwards, or use the non-bang form.

# frozen_string_literal: true

# Adding and removing
q = %w[b c] # => ["b", "c"]
q.push("d") # => ["b", "c", "d"]
q << "e" # => ["b", "c", "d", "e"]
q.append("f", "g") # => ["b", "c", "d", "e", "f", "g"]
q.unshift("a") # => ["a", "b", "c", "d", "e", "f", "g"]
q.prepend("x", "y") # => ["x", "y", "a", "b", "c", "d", "e", "f", "g"]
q.pop # => "g"
q.shift # => "x"
q.pop(2) # => ["e", "f"]
q.shift(2) # => ["y", "a"]
q.to_a # => ["b", "c", "d"]
extra = %w[m n]
q.insert(1, "p", "q") # => ["b", "p", "q", "c", "d"]
q.insert(-2, "z") # => ["b", "p", "q", "c", "z", "d"]
q.concat(extra, extra.map(&:upcase)) # => ["b", "p", "q", "c", "z", "d", "m", "n", "M", "N"]
q.delete("z") # => "z"
q.delete("nothing") # => nil
q.delete("nothing") { |v| "no #{v}" } # => "no nothing"
q.delete_at(0) # => "b"
q.delete_at(40) # => nil
q.delete_if { |s| s == "m" } # => ["p", "q", "c", "d", "n", "M", "N"]
q.compact! # => nil
[1, nil, 2, nil].compact # => [1, 2]
q.clear # => []
r = %w[a b c d e]
r[1] = "B" # => "B"
r[1, 2] = %w[x y z] # => ["x", "y", "z"]
r.to_a # => ["a", "x", "y", "z", "d", "e"]
r[1..2] = [] # => []
r.to_a # => ["a", "z", "d", "e"]
r[7] = "h" # => "h"
r.to_a # => ["a", "z", "d", "e", nil, nil, nil, "h"]
r[0, 0] = %w[first] # => ["first"]
r.first # => "first"
r.fill("-") # => ["-", "-", "-", "-", "-", "-", "-", "-", "-"]
Array.new(5).fill { |i| i * 2 } # => [0, 2, 4, 6, 8]
Array.new(5).fill(9, 3) # => [nil, nil, nil, 9, 9]
Array.new(5).fill(1, 1, 2) # => [nil, 1, 1, nil, nil]
r.replace(%w[one two]) # => ["one", "two"]

Assignment through []=

[]= is more capable than reading suggests. a[i] = x replaces one element. a[start, length] = other and a[range] = other replace a whole slice with the elements of other (an array is spliced, any other object is inserted as one element), so the array can grow or shrink: assigning [] deletes a slice, and a zero length inserts without removing (r[0, 0] = %w[first]). Assigning past the end pads the gap with nil (r[7] = "h" on a four-element array adds three nil`s). Negative indexes counted beyond the front raise `IndexError. Ruby 3.4 forbids block and keyword arguments in an index assignment such as a[0, k: 1] = 2; see Assignment and Multiple Assignment.

Frozen arrays and FrozenError

Every mutating method above raises FrozenError on a frozen array (message: can't modify frozen Array: ["x", "y"]). Freezing is shallow: the array’s own slots are locked, but the elements it references are not, so mutable.first << "!" still works on a frozen array of mutable strings (the example builds the string with +"x", because the magic comment froze the literal words of fixed). Constants holding arrays are usually frozen for this reason (NAMES = %w[a b].freeze), and # frozen_string_literal: true freezes plain string literals (including the words of a %w array) but never the array itself. Details and the deep-freeze helpers are in Object Lifecycle, Freezing and Copying.

Iterating and transforming

each is the primitive: it yields every element in order and returns the receiver. each_with_index, each_index, reverse_each, each_slice(n), each_cons(n), each_entry and cycle are variations on it. Every iterator called without a block returns an Enumerator, which is how they chain (each_with_index.map, map.with_index(1)). A for loop over an array is just each without a new scope and is rarely used; see Control Expressions.

The transformation methods come from Enumerable but are what you use on arrays daily:

  • map/collect returns a new array of the block’s results, flat_map flattens one level of results, filter_map maps and drops falsy results in one pass.

  • select/filter and reject keep or drop by predicate, partition returns both halves, group_by returns a hash of arrays, tally counts occurrences, each_with_object threads an accumulator (an alternative to inject that does not need the block to return it).

  • inject/reduce folds to one value: inject(:+) with a symbol, inject(1) { |acc, n| ... } with a seed. sum is the specialised form for addition and is both clearer and faster.

  • count, any?, all?, none?, one? take either a block or a pattern argument tested with === (all?(Integer), none?(0)).

  • find/detect returns the first match; find_index/index and rindex return a position. Ruby 4.0 added Array#find and Array#rfind. rfind is the efficient replacement for reverse_each.find: it scans from the end without building an enumerator.

  • min, max (both accept a count, max(2)), minmax, and min_by, max_by, minmax_by for a key.

  • take, drop, take_while, drop_while, chunk_while, slice_when for prefixes and runs, and lazy to defer the whole pipeline (see Enumerators and Lazy Evaluation).

# frozen_string_literal: true

# Iterating and transforming
nums = [3, 1, 4, 1, 5, 9, 2, 6]
nums.each_with_index.map { |n, i| n * i } # => [0, 1, 8, 3, 20, 45, 12, 42]
nums.each_slice(3).to_a # => [[3, 1, 4], [1, 5, 9], [2, 6]]
nums.each_cons(3).first(2) # => [[3, 1, 4], [1, 4, 1]]
nums.each_with_object([]) { |n, acc| acc.unshift(n) } # => [6, 2, 9, 5, 1, 4, 1, 3]
nums.map.with_index(1) { |n, i| "#{i}:#{n}" }.first(3) # => ["1:3", "2:1", "3:4"]
nums.select(&:even?) # => [4, 2, 6]
nums.reject { |n| n > 4 } # => [3, 1, 4, 1, 2]
nums.partition { |n| n > 3 } # => [[4, 5, 9, 6], [3, 1, 1, 2]]
nums.group_by(&:odd?) # => {true => [3, 1, 1, 5, 9], false => [4, 2, 6]}
nums.filter_map { |n| n * 10 if n > 4 } # => [50, 90, 60]
nums.flat_map { |n| [n] * (n > 5 ? 2 : 0) } # => [9, 9, 6, 6]
nums.tally # => {3 => 1, 1 => 2, 4 => 1, 5 => 1, 9 => 1, 2 => 1, 6 => 1}
nums.sum # => 31
nums.inject(:+) # => 31
nums.reduce(1) { |acc, n| acc * n } # => 6480
nums.count(1) # => 2
nums.count(&:even?) # => 3
nums.any? { |n| n > 8 } # => true
nums.all?(Integer) # => true
nums.none?(0) # => true
nums.one?(9) # => true
nums.find { |n| n > 4 } # => 5
nums.rfind { |n| n < 4 } # => 2
nums.find_index(4) # => 2
nums.index { |n| n > 4 } # => 4
nums.rindex(1) # => 3
nums.reverse_each.first(2) # => [6, 2]
nums.cycle.first(10) # => [3, 1, 4, 1, 5, 9, 2, 6, 3, 1]
nums.min # => 1
nums.max(2) # => [9, 6]
nums.minmax # => [1, 9]
nums.take_while { |n| n < 5 } # => [3, 1, 4, 1]
[1, 2, 4, 9, 10, 11].chunk_while { |a, b| b == a + 1 }.to_a # => [[1, 2], [4], [9, 10, 11]]
nums.slice_when { |a, b| b < a }.to_a # => [[3], [1, 4], [1, 5, 9], [2, 6]]
nums.each_entry.to_a.size # => 8
nums.lazy.select(&:odd?).map { |n| n * 2 }.first(2) # => [6, 2]

The full Enumerable catalogue, with the shelf catalogue as the running example, is on Enumerable. Two array-specific points matter in practice. First, Ruby 3.4 rewrote Array#each, #map and #select in Ruby itself so the JIT can inline their blocks, and 4.0 made Array#find a native override of Enumerable#find; this changes backtraces (frames named <internal:array>), not behaviour. Second, never add or remove elements of an array while iterating over it with each: the loop tracks an index, so deleting an element makes it skip the next one (see the pitfalls section).

Searching

include? is a linear scan using ==, index returns the position of the first match, rindex of the last, assoc and rassoc search an array of pairs by first or second element, and intersect? (Ruby 3.1) answers "do these two arrays share any element" without allocating the intersection array. All of them are O(n); if you search the same array repeatedly, convert it once to a Set or a hash.

Binary search with bsearch

For a sorted array, bsearch finds a value in O(log n) with the block deciding which half to keep. It has two modes, chosen by what the block returns:

  • Find-minimum mode. The block returns true or false (or nil) and the array must be partitioned: every element that gives false comes before every element that gives true. bsearch returns the first element for which the block is true, or nil.

  • Find-any mode. The block returns a number: 0 means "this is the element", a negative number means "look at the left half", a positive number means "look at the right half". The natural block is target <=> element, and bsearch returns the matching element or nil.

bsearch_index returns the index instead of the element. If the array is not actually sorted by the same ordering the block uses, the result is unspecified: it can be nil or an arbitrary element, and no error is raised. The official binary search guide describes the modes in detail; Range#bsearch uses the same contract on numeric ranges.

# frozen_string_literal: true

# Searching: include?, bsearch
a = [3, 1, 4, 1, 5]
a.include?(4) # => true
a.index(1) # => 1
a.intersect?([9, 5]) # => true
pairs = [[:a, 1], [:b, 2]]
pairs.assoc(:b) # => [:b, 2]
pairs.rassoc(1) # => [:a, 1]
sorted = [1, 3, 5, 7, 9, 11]
sorted.bsearch { |x| x >= 6 } # => 7
sorted.bsearch_index { |x| x >= 6 } # => 3
sorted.bsearch { |x| x >= 100 } # => nil
sorted.bsearch { |x| 7 <=> x } # => 7
sorted.bsearch { |x| 4 <=> x } # => nil
sorted.bsearch_index { |x| 11 <=> x } # => 5
unsorted = [9, 1, 7, 3]
unsorted.bsearch { |x| x >= 5 } # => 7

The last line searches an unsorted array and happens to return an element: do not read meaning into it.

Sorting and ordering

sort returns a new array ordered by <=>, sort! sorts in place and returns the receiver. With a block it uses the block’s -1/0/1 result (sort { |a, b| b <=> a } for descending). Elements must be comparable with each other: [3, "a", 1].sort raises ArgumentError ("comparison of Integer with String failed") and so does [3, nil].max, because the comparison returned nil. A class that wants to be sortable defines <=> and, for the comparison operators, includes Comparable (see Equality, Hashing and Comparison). Arrays compare element by element, so a sort_by key that is an array sorts by several fields in order.

sort_by computes the key once per element and then sorts by the keys, which is cheaper than a comparison block when the key is expensive (it is the Schwartzian transform built in). min_by, max_by and minmax_by follow the same key convention. reverse, rotate(n) (a negative n rotates the other way) and the in-place reverse!/rotate! rearrange without sorting. sort_by { |w| -w.size } is a common descending sort for numeric keys; for non-numeric keys use sort_by { ... }.reverse or a block with b <=> a.

# frozen_string_literal: true

# Sorting
words = %w[pear Fig apple fig kiwi]
words.sort # => ["Fig", "apple", "fig", "kiwi", "pear"]
words.sort_by(&:downcase) # => ["apple", "Fig", "fig", "kiwi", "pear"]
words.sort_by { |w| [w.size, w] } # => ["Fig", "fig", "kiwi", "pear", "apple"]
words.sort { |a, b| b <=> a } # => ["pear", "kiwi", "fig", "apple", "Fig"]
words.sort_by { |w| -w.size } # => ["apple", "pear", "kiwi", "Fig", "fig"]
words.max_by(&:size) # => "apple"
words.min_by(2, &:size) # => ["Fig", "fig"]
words.minmax_by(&:size) # => ["Fig", "apple"]
words.max # => "pear"
words.max { |a, b| a.size <=> b.size } # => "apple"
words.reverse # => ["kiwi", "fig", "apple", "Fig", "pear"]
words.rotate # => ["Fig", "apple", "fig", "kiwi", "pear"]
words.rotate(-2) # => ["fig", "kiwi", "pear", "Fig", "apple"]
copy = words.dup
copy.sort! # => ["Fig", "apple", "fig", "kiwi", "pear"]
copy.sort!.equal?(copy) # => true
copy.reverse!.first # => "pear"
begin
  [3, "a", 1].sort
rescue ArgumentError => e
  e.message # => "comparison of Integer with String failed"
end
begin
  [3, nil].max
rescue ArgumentError => e
  e.message # => "comparison of Integer with nil failed"
end
[0.1, 0.2, 0.3].sum # => 0.6
[0.1, 0.2, 0.3].inject(:+) # => 0.6000000000000001
[[2, "b"], [1, "z"], [2, "a"]].sort # => [[1, "z"], [2, "a"], [2, "b"]]
[1, 2, 3, 4].sort_by { |n| n.even? ? 0 : 1 } # => [2, 4, 1, 3]
(1..20).sort_by { |n| n % 3 }.first(8) # => [3, 6, 18, 9, 12, 15, 1, 4]

[0.1, 0.2, 0.3].sum returned 0.6 while inject(:+) returned 0.6000000000000001: Array#sum uses the Kahan-Babuska compensated summation for floats, so prefer it to a manual fold. sum of non-numbers needs an initial value (%w[a b].sum(""), [[1], [2]].sum([])); without one it raises TypeError because the start value is 0.

Sorting is not stable. Ruby does not guarantee that equal keys keep their original order, and sort_by { |n| n % 3 } over 1..20 gave [3, 6, 18, 9, 12, 15, ...] in the run above, not [3, 6, 9, 12, ...]. If you need a stable order, add the original index as a tie-breaker: sort_by.with_index { |x, i| [key(x), i] }. Never rely on the observed order of a particular Ruby version.

Set-like operations

Arrays support the set operators by value, using hash and eql? internally, and keep the order of first appearance and any duplicates in the receiver for the operators that do not deduplicate:

  • a | b (union) and a & b (intersection) return a new array without duplicates; union and intersection take any number of arguments.

  • a - b (difference) keeps the elements of a not in b, including duplicates of a ([1, 2, 2] - [3] keeps both 2s).

  • a + b concatenates and keeps everything; a * n repeats; a * "sep" is join.

  • uniq and uniq { |x| key } deduplicate (the block form keeps the first element for each key), uniq! returns nil if nothing changed.

  • intersect?(other) (Ruby 3.1) is the boolean form of &.

Because the operators use eql? and hash, 1 and 1.0 are different elements ([1, 1.0, 1r].uniq keeps all three), unlike ==. For frequent membership tests or large inputs, use a Set: Set is a core class in Ruby 4.0 (before 3.2 it needed require "set"), and to_set converts an array. See Sets.

# frozen_string_literal: true

# Set operations and uniqueness
a = [1, 2, 2, 3, 4]
b = [3, 4, 4, 5]
[a | b, a & b] # => [[1, 2, 3, 4, 5], [3, 4]]
a.union(b, [9]) # => [1, 2, 3, 4, 5, 9]
a.intersection(b) # => [3, 4]
a.difference(b) # => [1, 2, 2]
a.intersect?(b) # => true
a.uniq # => [1, 2, 3, 4]
%w[apple Avocado banana Blueberry].uniq { |w| w[0].downcase } # => ["apple", "banana"]
a.dup.uniq! # => [1, 2, 3, 4]
[1, 2].uniq! # => nil
words = %w[a b a]
[words - ["a"], words + ["z"]] # => [["b"], ["a", "b", "a", "z"]]
[words * 2, words * "-"] # => [["a", "b", "a", "a", "b", "a"], "a-b-a"]
[[1, 2], [3]].sum([]) # => [1, 2, 3]
[1, 1.0, 1r].uniq # => [1, 1.0, (1/1)]
[[1], [1]].uniq.size # => 1
a.to_set # => Set[1, 2, 3, 4]
Set.new(a).subset?(Set.new(1..5)) # => true

Flattening and combining

flatten removes nesting recursively, or to a depth (flatten(1)); flatten! returns nil when there was nothing to flatten. It flattens anything that responds to to_ary, not only arrays, and raises ArgumentError on a recursive array. zip pairs the elements of the receiver with those of one or more other collections (the result is as long as the receiver, padding with nil, and an endless range works), transpose swaps rows and columns of an array of equal length arrays (IndexError otherwise), product is the Cartesian product, and combination(k), permutation(k), repeated_combination(k) and repeated_permutation(k) enumerate the arrangements (they return enumerators when called without a block; materialising them with to_a is exponential). each_slice and each_cons batch and slide. to_h turns an array of pairs, or the result of a block, into a Hash. join converts every element with to_s and flattens nested arrays recursively; nil becomes an empty string. cycle repeats forever, or n times.

# frozen_string_literal: true

# Flatten, zip, product and friends
nested = [1, [2, [3, [4]]]]
nested.flatten # => [1, 2, 3, 4]
nested.flatten(1) # => [1, 2, [3, [4]]]
[1, [2]].flatten!.nil? # => false
[1, 2].flatten!.nil? # => true
[1, 2, 3].zip([4, 5, 6], [7, 8, 9]) # => [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
[1, 2, 3].zip([4]) # => [[1, 4], [2, nil], [3, nil]]
%w[a b].zip(1..) # => [["a", 1], ["b", 2]]
%w[a b c].zip(1..3).to_h # => {"a" => 1, "b" => 2, "c" => 3}
[[1, 2], [3, 4], [5, 6]].transpose # => [[1, 3, 5], [2, 4, 6]]
[1, 2].product([3, 4]) # => [[1, 3], [1, 4], [2, 3], [2, 4]]
[1, 2].product([3], [5, 6]).size # => 4
[1, 2, 3].combination(2).to_a # => [[1, 2], [1, 3], [2, 3]]
[1, 2, 3].permutation(2).count # => 6
[1, 2].repeated_permutation(2).to_a # => [[1, 1], [1, 2], [2, 1], [2, 2]]
[1, 2, 3].each_cons(2).to_a # => [[1, 2], [2, 3]]
[1, 2, 3, 4].each_slice(2).map(&:sum) # => [3, 7]
[3, 1, 2].sum { |n| n * 2 } # => 12
%w[a b].sum("") # => "ab"
[[1], [2]].sum([]) # => [1, 2]
begin
  %w[a b].sum
rescue TypeError => e
  e.message # => "String can't be coerced into Integer"
end
[1, 2, 3].inject { |a, b| a * b } # => 6
[1, [2, 3]].join(",") # => "1,2,3"
[1, nil, "a"].join("-") # => "1--a"
%w[a b c].join # => "abc"
[1, 2, 3].cycle(2).to_a # => [1, 2, 3, 1, 2, 3]
[1, 2, 3, 4, 5].values_at(0, 2..3) # => [1, 3, 4]
(1..10).each_slice(4).to_a # => [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
[1, 2, 3].to_h { |n| [n, n * n] } # => {1 => 1, 2 => 4, 3 => 9}
[[:a, 1]].to_h # => {a: 1}

Packing binary data: pack and unpack

Array#pack turns an array of values into a binary string according to a template of directive letters, and String#unpack and String#unpack1 do the reverse. The result of pack has the ASCII-8BIT (BINARY) encoding unless the template starts with U, so the same call is the standard way to build a protocol header or read a fixed-width record. The directives that cover most needs:

Directive Meaning Notes

C, c

unsigned / signed 8-bit integer

C* packs a list of bytes

S, s, L, l, Q, q

16, 32 and 64-bit integers (unsigned / signed)

native endianness; add > for big-endian or < for little-endian (S>, L<)

n, N, v, V

16 / 32-bit unsigned, network (big-endian) n/N, little-endian v/V

N is the usual network-order 32-bit integer

U

a Unicode code point, written as UTF-8

the only directive that makes the string UTF-8; w is a BER-compressed integer

a, A, Z

string padded with NUL (a), with space (A) or NUL-terminated (Z)

a count gives the field width, * the whole string

H, h, B, b

hex or bit string, high or low nibble / bit first

H* converts a hex string to bytes

m, m0, M, u

Base64, strict Base64 (no newlines), quoted-printable, uuencode

["x"].pack("m0") is the safe form for URLs and JSON

E, e, G, g, D, d, F, f

IEEE floats and doubles, little or big endian or native

E is a little-endian double

Since Ruby 3.3, an unknown directive raises ArgumentError instead of being silently ignored, and pack accepts buffer: to append into an existing string. Every directive, modifier and the exact semantics are in the official packed data page.

# frozen_string_literal: true

# pack and unpack
[65, 66, 67].pack("C*") # => "ABC"
[65, 66, 67].pack("c3") # => "ABC"
[1, 2].pack("S>L<").bytes # => [0, 1, 2, 0, 0, 0]
[1].pack("N").bytes # => [0, 0, 0, 1]
[1].pack("V").bytes # => [1, 0, 0, 0]
[97, 0x20AC].pack("U*") # => "a€"
%w[abc de].pack("A5a3").bytes # => [97, 98, 99, 32, 32, 100, 101, 0]
["abc"].pack("Z*").bytes # => [97, 98, 99, 0]
["hello"].pack("m") # => "aGVsbG8=\n"
["hello"].pack("m0") # => "aGVsbG8="
["616263"].pack("H*") # => "abc"
["a b"].pack("M") # => "a b=\n"
[1.5].pack("E").unpack1("E") # => 1.5
"AB".unpack("C*") # => [65, 66]
"\x00\x00\x01\x00".unpack1("N") # => 256
"aGVsbG8=\n".unpack1("m") # => "hello"
"abc".unpack1("H*") # => "616263"
"abc".unpack("a1 a1 a1") # => ["a", "b", "c"]
[1, 2, 3].pack("C*").encoding # => #<Encoding:BINARY (ASCII-8BIT)>
["x"].pack("a").encoding # => #<Encoding:BINARY (ASCII-8BIT)>
[1].pack("w").bytes # => [1]
[300].pack("w").bytes # => [130, 44]
begin
  [1].pack("y")
rescue ArgumentError => e
  e.message # => "unknown pack directive 'y' in 'y'"
end
begin
  ["a"].pack("C")
rescue TypeError => e
  e.message # => "no implicit conversion of String into Integer"
end
buffer = +"hdr:"
[1, 2].pack("C*", buffer: buffer) # => "hdr:\u0001\u0002"

Array#pack converts each element with the implicit conversion the directive needs (to_int, to_str, to_f), so a wrong element type fails with TypeError (no implicit conversion of String into Integer) rather than packing garbage.

Equality, copying and freezing

Two arrays are == when they have the same length and every pair of elements at the same index is ==; eql? requires eql? elements and is what Hash uses for keys, and equal? is identity. hash is computed from the elements, so equal arrays are interchangeable as hash keys (as long as nobody mutates them afterwards). <=> compares element by element and then by length, and returns nil when two elements are not comparable, which is why [1, 2] <=> [1, "a"] is nil. See Equality, Hashing and Comparison for the contract.

dup and clone copy the array but not its elements, so the copy and the original share every element object: changing a nested array through the copy changes it in the original. clone additionally keeps the frozen state and the singleton class (clone(freeze: false) unfreezes the copy). For a real deep copy the portable core option is Marshal.load(Marshal.dump(array)), which fails on objects that cannot be dumped (procs, IO, singleton objects); a domain-specific deep_dup is better when you control the types. Ractor.make_shareable freezes an array and everything it references, recursively, which is also what you need to share it with a Ractor.

# frozen_string_literal: true

# Equality, copies, freezing
a = [1, [2, 3]]
b = [1, [2, 3]]
[a == b, a.eql?(b), a.equal?(b), a.hash == b.hash] # => [true, true, false, true]
[[1, 2] == [1.0, 2.0], [1, 2].eql?([1.0, 2.0])] # => [true, false]
[[1, 2] <=> [1, 3], [1, 2] <=> [1], [1, 2] <=> [1, "a"]] # => [-1, 1, nil]
copy = a.dup
copy[1] << 4
a.to_a # => [1, [2, 3, 4]]
deep = Marshal.load(Marshal.dump(a))
deep[1] << 5
[a, deep] # => [[1, [2, 3, 4]], [1, [2, 3, 4, 5]]]
fixed = %w[x y].freeze
begin
  fixed << "z"
rescue FrozenError => e
  e.message # => "can't modify frozen Array: [\"x\", \"y\"]"
end
mutable = [+"x"].freeze
mutable.first << "!"
mutable.to_a # => ["x!"]
fixed.first.frozen? # => true
[fixed.frozen?, fixed.dup.frozen?, fixed.clone.frozen?] # => [true, false, true]
fixed.clone(freeze: false).frozen? # => false
sealed = Ractor.make_shareable([["a"], ["b"]])
[sealed.frozen?, sealed.first.frozen?, sealed.first.first.frozen?] # => [true, true, true]
list = [1, 2, 3, 4]
list.each { |n| list.delete(n) if n.even? }
list.to_a # => [1, 3]
numbers = [1, 2, 3, 4]
numbers.reject!(&:even?)
numbers.to_a # => [1, 3]
total = []
before = total.object_id
total += [1]
rebound = total.object_id != before # => true
current = total.object_id
total << 2
[rebound, total.object_id == current, total] # => [true, true, [1, 2]]

The last four lines show the cost model: total += [1] is total = total + [1], which allocates a new array and rebinds the variable (the object id changes), while << mutates the existing array in place. In a loop that grows an array, += is quadratic; use << or push.

Destructuring, splats and pattern matching

Arrays are the data structure behind multiple assignment. a, b = array calls to_ary on the right side if it responds to it, head, *tail = array and *init, last = array collect the rest, and parentheses destructure nested arrays, in assignment and in block parameters (|a, (b, c)|). A block that takes several parameters receives each element of a yielded array spread across them. The full rules are in Assignment and Multiple Assignment.

case/in has array patterns: [Integer => a, [b, *rest]] matches a fixed shape, [*, "x", "y", *] is a find pattern (Ruby 3.0, experimental until 3.2) that looks for a run anywhere in the array, and => and in are the one-line forms (the rightward point => [px, py] raises NoMatchingPatternError when the shape is wrong, in returns a boolean). A pattern calls deconstruct, which Array defines to return itself; a custom class opts in by defining it. A case without an else raises NoMatchingPatternError when nothing matches. See Pattern Matching.

# frozen_string_literal: true

# Destructuring, pattern matching and conversion
first, second = [10, 20, 30]
[first, second] # => [10, 20]
head, *tail = [1, 2, 3]
[head, tail] # => [1, [2, 3]]
*init, last = [1, 2, 3]
[init, last] # => [[1, 2], 3]
x, (y, z) = [1, [2, 3]]
[x, y, z] # => [1, 2, 3]
[[1, 2], [3, 4]].map { |(p, q)| p + q } # => [3, 7]
[[1, [2, 3]]].map { |a, (b, c)| [a, b, c] } # => [[1, 2, 3]]
[[1, 2], [3, 4]].each_with_index.map { |(m, n), i| (m * n) + i } # => [2, 13]
case [1, [2, 3]]
in [Integer => a, [b, *rest]]
  [a, b, rest] # => [1, 2, [3]]
end
case [:ok, 200, "body"]
in [:ok, 200.. => code, String => body]
  [code, body] # => [200, "body"]
end
steps = %w[start x y end]
case steps
in [*, "x", "y", *]
  [:found].first # => :found
end
result = [1, "two", :three].then do |value|
  case value
  in [Integer, String, Symbol] then :typed
  else :other
  end
end
result.to_s # => "typed"
point = [3, 4]
point => [px, py]
Math.hypot(px, py) # => 5.0
[1, 2] in [_, _] # => true
[1, 2, 3] in [_, _] # => false
class Pair
  def initialize(left, right)
    @left = left
    @right = right
  end

  def to_ary = [@left, @right]
end
l, r = Pair.new(1, 2)
[l, r] # => [1, 2]
[[0, 0], Pair.new(1, 2)].flatten # => [0, 0, 1, 2]
[[0, 0], Pair.new(1, 2)].to_h { |lft, rgt| [lft, rgt] } # => {0 => 0, 1 => 2}

The Pair class at the end shows the implicit conversion protocol: defining to_ary makes an object usable on the right of a multiple assignment and lets flatten flatten it, because both call to_ary rather than to_a. Implement to_ary only for objects that truly are arrays in disguise; objects that merely can be turned into an array should define to_a instead, which Array() and the splat use. The distinction is covered in Conversions and Coercion and in the official implicit conversion page.

Subclassing Array

Subclassing Array is possible but usually the wrong tool, because most methods return a plain Array rather than an instance of the subclass (since Ruby 3.0, drop, drop_while, flatten, slice!, []/slice, take, take_while, uniq and * return Array, not the subclass), while dup and new keep the subclass. The subclass then silently loses its extra methods after a map or select. Prefer composition: a class that holds an array in an instance variable and include`s `Enumerable (like Shelf::Catalog, which wraps its store) or delegates with Forwardable.

# frozen_string_literal: true

# Subclassing Array
class Playlist < Array
  def total = sum
end

list = Playlist.new([3, 4])
[list.class, list.total] # => [Playlist, 7]
[list.map { _1 }.class, list.select(&:odd?).class, list.dup.class] # => [Array, Array, Playlist]
[list.first(1).class, list[0, 1].class, list.reverse.class] # => [Array, Array, Array]
[list.flatten.class, list.uniq.class, list.take(1).class] # => [Array, Array, Array]
Playlist.new(2) { |i| i + 1 }.class # => Playlist
Playlist[1, 2].class # => Playlist

Source: Ruby 3.0.0 NEWS

Arrays in the shelf catalogue

The shelf catalogue stores books in an array inside Shelf::Store::Memory and exposes them through Shelf::Catalog, whose each hands out a copy (@books.dup) so that callers cannot mutate the store. Everything in the next example is an array method chain over that: building the books by zipping titles with ISBN digits, sorting by title, grouping, partitioning, slicing into pages with each_slice, and turning the loans into their state labels.

# frozen_string_literal: true

require "shelf"

# The shelf catalogue as an array pipeline
author = Shelf::Author.new(name: "Ursula K. Le Guin")
isbns = %w[9780306406157 9780131103627 9780201633610]
titles = ["The Dispossessed", "Tehanu", "A Wizard of Earthsea"]
books = titles.zip(isbns).map do |title, digits|
  Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author:)
end
catalog = books.reduce(Shelf::Catalog.new) { |cat, book| cat.add(book) }
catalog.to_a.size # => 3
catalog.map(&:title) # => ["The Dispossessed", "Tehanu", "A Wizard of Earthsea"]
catalog.sort_by(&:title).map(&:title) # => ["A Wizard of Earthsea", "Tehanu", "The Dispossessed"]
catalog.min_by { |b| b.title.size }.title # => "Tehanu"
catalog.group_by { |b| b.title.split.first.downcase.to_sym }.keys # => [:the, :tehanu, :a]
catalog.partition { |b| b.title.include?("of") }.map(&:size) # => [1, 2]
catalog.each_slice(2).map { |pair| pair.map(&:title) } # => [["The Dispossessed", "Tehanu"], ["A Wizard of Earthsea"]]
catalog.first.isbn.to_s # => "9780306406157"
catalog.each_with_index.to_h { |b, i| [i, b.isbn.digits[-4..]] } # => {0 => "6157", 1 => "3627", 2 => "3610"}
loans = books.map { |b| Shelf::Loan.new(b, state: b.title.size > 10 ? :lent : :available) }
loans.map(&:state) # => [:lent, :available, :lent]
loans.map(&:describe) # => ["The Dispossessed is lent", "Tehanu is on the shelf", "A Wizard of Earthsea is lent"]
loans.count { |l| l.state == :lent } # => 2
books.first.to_s # => "The Dispossessed by Ursula K. Le Guin"
books.frozen? # => false

Catalog is Enumerable, so map, sort_by, min_by, group_by, partition, each_slice and each_with_index work on it directly, while to_a returns a true array. Note that Shelf::Loan#describe is the pattern-matching case from the Shelf library; it consumes a one-element hash, not an array.

Arrays and concurrency

An Array is not a thread-safe data structure in the sense of the language specification. CRuby’s global VM lock makes each individual C-implemented method such as push or << atomic with respect to other Ruby threads, so you will not corrupt the array’s internal structure, but compound operations (check then act, read-modify-write, iterating while another thread appends) are not atomic, and other Ruby implementations make weaker guarantees. Guard shared arrays with a Thread::Mutex (or hand work over with Thread::Queue, which is the thread-safe collection), as Shelf::Catalog does. Between Ractors an array cannot be shared unless it is deeply frozen and shareable: Ractor.make_shareable(array) freezes it recursively, and Ractor.shareable? tells you whether it already is. See Threads and the GVL and Ractors.

# frozen_string_literal: true

# Compound operations on a shared array need a lock
queue = []
lock = Thread::Mutex.new
workers = Array.new(4) do |n|
  Thread.new do
    100.times { |i| lock.synchronize { queue << ((n * 100) + i) } }
  end
end
workers.each(&:join)
[queue.size, queue.uniq.size] # => [400, 400]
shared = Ractor.make_shareable(%w[a b])
Ractor.shareable?(shared) # => true
Ractor.shareable?(%w[a b]) # => false

Performance notes

Rules of thumb for CRuby; measure with the tools in Performance, Benchmarking and Profiling before acting on them:

  • push, <<, pop and shift run in amortised constant time (shift is cheap because the array can share its buffer and just advance a start offset); unshift is amortised constant time as well; insert in the middle, delete_at, delete and include?/index are linear.

  • +, -, |, & and uniq allocate new arrays; -, & and uniq build a hash internally for larger inputs, so they are O(n) rather than O(n * m).

  • array += other in a loop is quadratic; concat, push(*other) and << mutate in place.

  • sum beats inject(:+) (and is more accurate for floats); sort_by beats a sort block when the key is not free; min/max beat sort.first/sort.last; rfind beats reverse_each.find; Array.new(n) { ... } and map presize the result.

  • Chaining select.map.reject allocates an intermediate array per step; use filter_map, each_with_object, lazy for very large inputs, or a single each.

  • A one-million element array of small integers stores them inline as tagged values (a fixnum needs no heap object), but an array of strings is an array of heap objects; memory is dominated by the elements, not the 8-byte slots.

  • Array#pack/String#unpack are the fast path for binary formats; avoid building strings byte by byte.

Common pitfalls

Mistake What happens Fix

Array.new(3, [])

all three slots are the same array

Array.new(3) { [] }

deleting while iterating with each

the loop skips the element after each removal ([1, 2, 3, 4] ended as [1, 3] above, because 3 was skipped, not tested)

reject!, delete_if, or select into a new array

chaining a bang method (compact!.first)

bang methods return nil when nothing changed

call it as a statement, or use the non-bang form

a[i] = x far past the end

pads with nil, growing the array silently

check bounds or use push

array.sort on mixed types

ArgumentError: comparison of Integer with String failed

sort_by with a key of one type, or normalise first

assuming a stable sort

equal keys may change relative order

add the index as a tie-breaker

a - b with elements that are == but not eql? (1 and 1.0)

they are not removed

normalise the elements, or use reject { |x| b.include?(x) }

array.first(n) with n negative

ArgumentError: negative array size

validate the argument

[] for a missing element

nil, indistinguishable from a stored nil

use fetch

%w[a#{x}]

no interpolation in %w

use %W or a normal literal

a frozen array assumed deeply frozen

elements remain mutable

Ractor.make_shareable or freeze elements explicitly

array.sum of strings or arrays

TypeError: String can't be coerced into Integer

pass the start value (sum(""), sum([])) or use join and flatten(1)

block-less sort on objects without <=>

ArgumentError or NoMethodError

sort_by(&:attribute)

Older code you may meet

Code written for older Ruby versions, and older tutorials, differ from the examples above in these ways:

  • Before Ruby 3.4, Hash results from group_by and tally were printed as {true=>[...]}; since 3.4 they print as {true => [...]}, and symbol keys as {a: 1}. Outputs in older blog posts and books look different, not wrong.

  • require "set" was needed before Ruby 3.2 (autoload) and is unnecessary in 4.0, where Set is a core class; SortedSet was removed in 3.0 (the sorted_set gem).

  • Array#sum appeared in Ruby 2.4 and replaces inject(:+) for numbers; filter_map appeared in 2.7; intersect? in 3.1; fetch_values in 3.4; find and rfind in 4.0.

  • The ranged arithmetic-sequence index (a[(0..).step(2)]) was added in 3.0.

  • Array#pack ignoring unknown directives was fixed in 3.3.

  • Before Ruby 3.0, Array#flatten, #drop, #take, #uniq and the slicing methods returned the subclass when called on a subclass instance.

Version notes

Version Array-related change

3.0

Array#[] accepts an ArithmeticSequence; drop, drop_while, flatten, slice!, slice/[], take, take_while, uniq and * return Array for subclass instances; find pattern for arrays in case/in.

3.1

Array#intersect?.

3.2

Set is autoloaded (no require "set"); the find pattern is no longer experimental.

3.3

Array#pack raises ArgumentError for unknown directives.

3.4

Array#fetch_values; Array#each, map and select reimplemented in Ruby for JIT performance; index assignment rejects block and keyword arguments; Hash#inspect format {key: value} affects printed results of group_by and tally.

4.0

Array#find and Array#rfind; Set is a core class; fetch_values error messages name Array#fetch_values.

References