List (Array) #
Arrays are the backbone of data manipulation in Ruby. Almost every real program deals with collections of data — user lists, transaction rows, database query results, text file lines — and it all boils down to Arrays. What makes Ruby’s Array special isn’t just its ability to store data, but the extremely rich Enumerable method ecosystem: more than 60 methods for transformation, filtering, grouping, searching, and accumulation that can be chained together. Understanding Ruby Arrays deeply means understanding idiomatic functional thinking — expressing “what you want to achieve” rather than “how to do it step by step”.
Creating Arrays #
There are several ways to create an Array in Ruby, each with a different use:
# Literal — the most common way
empty = []
numbers = [1, 2, 3, 4, 5]
names = ["Rina", "Budi", "Citra"]
mixed = [1, "two", :three, 4.0, nil, true]
# %w — shorthand for string arrays without quotes
fruits = %w[apple mango orange pineapple durian]
# => ["apple", "mango", "orange", "pineapple", "durian"]
# %i — shorthand for symbol arrays
statuses = %i[active inactive pending blocked]
# => [:active, :inactive, :pending, :blocked]
# Array.new — with size and default value
zeros = Array.new(5, 0) # => [0, 0, 0, 0, 0]
empty_strings = Array.new(3, "") # => ["", "", ""]
# Array.new with a block — different value per element
squares = Array.new(6) { |i| i ** 2 }
# => [0, 1, 4, 9, 16, 25]
indices = Array.new(5) { |i| i + 1 }
# => [1, 2, 3, 4, 5]
# Conversion from other types
from_range = (1..10).to_a # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
from_hash = {a: 1, b: 2}.to_a # => [[:a, 1], [:b, 2]]
from_string = "hello".chars # => ["h", "e", "l", "l", "o"]
Be careful withArray.new(n, object)— all elements refer to the same object in memory. If the object is mutable (like an Array or Hash), changing one element changes all of them. Use the block formArray.new(n) { [] }to ensure each element is a different object.
# ANTI-PATTERN: all array elements share the same object
rows = Array.new(3, [])
rows[0] << "a"
puts rows.inspect # => [["a"], ["a"], ["a"]] ← all changed!
# CORRECT: the block creates a new object on every iteration
rows = Array.new(3) { [] }
rows[0] << "a"
puts rows.inspect # => [["a"], [], []] ← only [0] changed
Accessing Elements #
Ruby provides a very flexible way to access elements — from single indexes to slices with ranges:
arr = ["a", "b", "c", "d", "e", "f"]
# Single index
puts arr[0] # => "a" (first)
puts arr[-1] # => "f" (last)
puts arr[-2] # => "e" (second from the end)
# Slice with a range
puts arr[1..3].inspect # => ["b", "c", "d"] (inclusive)
puts arr[1...3].inspect # => ["b", "c"] (exclusive)
puts arr[2..].inspect # => ["c", "d", "e", "f"] (to the end)
puts arr[..2].inspect # => ["a", "b", "c"] (from the start)
# Slice with (start, length)
puts arr[2, 3].inspect # => ["c", "d", "e"] (start at index 2, take 3)
# More expressive methods
puts arr.first.inspect # => "a"
puts arr.last.inspect # => "f"
puts arr.first(3).inspect # => ["a", "b", "c"]
puts arr.last(2).inspect # => ["e", "f"]
puts arr.sample # => random element
puts arr.sample(2).inspect # => 2 random elements
# Out-of-bounds indexes — no error, returns nil
puts arr[100].inspect # => nil
puts arr.fetch(100) # => IndexError!
puts arr.fetch(100, "default") # => "default"
dig — Accessing Nested Array Elements #
For multidimensional arrays, dig accesses nested elements without risking NoMethodError if any level is nil:
data = [
[1, [2, 3]],
[4, [5, 6]],
[7, [8, 9]]
]
puts data[1][1][0] # => 5
puts data.dig(1, 1, 0) # => 5 (safer)
puts data.dig(5, 1, 0) # => nil (doesn't crash if the index doesn't exist)
puts data[5][1][0] # => NoMethodError: nil[][1] (crash!)
Adding and Removing Elements #
arr = [1, 2, 3]
# Adding at the end
arr.push(4) # => [1, 2, 3, 4]
arr << 5 # => [1, 2, 3, 4, 5] (most idiomatic)
arr.append(6, 7) # => [1, 2, 3, 4, 5, 6, 7]
# Adding at the front
arr.unshift(0) # => [0, 1, 2, 3, 4, 5, 6, 7]
arr.prepend(-1, -2) # => [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
# Removing from the back and front
last = arr.pop # remove and return the last element
first = arr.shift # remove and return the first element
# Removing by value
arr = [1, 2, 3, 2, 4, 2]
arr.delete(2) # remove ALL elements with value 2 => [1, 3, 4]
# Removing by index
arr = [10, 20, 30, 40, 50]
arr.delete_at(2) # remove index 2 => [10, 20, 40, 50]
# Removing by condition (destructive)
arr = [1, 2, 3, 4, 5, 6]
arr.delete_if { |n| n.even? } # => [1, 3, 5]
arr.keep_if { |n| n.odd? } # => same, but more expressive
# Insert at a specific position
arr = [1, 2, 4, 5]
arr.insert(2, 3) # insert 3 at index 2 => [1, 2, 3, 4, 5]
# Replace a range of elements
arr = [1, 2, 3, 4, 5]
arr[1..2] = [20, 30] # => [1, 20, 30, 4, 5]
arr[1, 2] = [200] # => [1, 200, 4, 5] (replace 2 elements starting at index 1)
Transformation — map, flat_map, zip, product #
map — Change Every Element #
prices = [15_000_000, 350_000, 3_500_000, 450_000]
# Apply a 10% discount
after_discount = prices.map { |p| (p * 0.9).round }
puts after_discount.inspect
# => [13500000, 315000, 3150000, 405000]
# Transformation into other objects
users = [
{ name: "Rina", score: 85 },
{ name: "Budi", score: 92 },
{ name: "Citra", score: 78 }
]
names_only = users.map { |u| u[:name] }
# => ["Rina", "Budi", "Citra"]
with_grade = users.map do |u|
grade = u[:score] >= 90 ? "A" : u[:score] >= 80 ? "B" : "C"
u.merge(grade: grade)
end
flat_map — Map then Flatten #
sentences = ["ruby is fun", "learn every day"]
# Regular map produces an array of arrays
sentences.map { |s| s.split }
# => [["ruby", "is", "fun"], ["learn", "every", "day"]]
# flat_map flattens one level
all_words = sentences.flat_map { |s| s.split }
# => ["ruby", "is", "fun", "learn", "every", "day"]
# Real-world example: collect all tags from many articles
articles = [
{ title: "Ruby", tags: %i[ruby beginner] },
{ title: "Rails", tags: %i[ruby rails web] },
{ title: "Python", tags: %i[python tips] }
]
all_tags = articles.flat_map { |a| a[:tags] }.uniq
puts all_tags.inspect
# => [:ruby, :beginner, :rails, :web, :python, :tips]
zip — Combine Parallel Arrays #
zip combines arrays by element position:
names = ["Rina", "Budi", "Citra"]
scores = [85, 92, 78]
cities = ["Bandung", "Jakarta", "Surabaya"]
combined = names.zip(scores, cities)
puts combined.inspect
# => [["Rina", 85, "Bandung"], ["Budi", 92, "Jakarta"], ["Citra", 78, "Surabaya"]]
# Very useful for building a Hash
profile = names.zip(scores).to_h
puts profile.inspect
# => {"Rina"=>85, "Budi"=>92, "Citra"=>78}
product — All Combinations #
product generates every combination of elements from two or more arrays — the Cartesian product:
colors = [:red, :blue]
sizes = [:S, :M, :L]
variants = colors.product(sizes)
puts variants.inspect
# => [[:red, :S], [:red, :M], [:red, :L],
# [:blue, :S], [:blue, :M], [:blue, :L]]
# Useful for generating all product option combinations
puts colors.product(sizes).length # => 6 variants
Filtering and Searching #
data = [3, 1, 7, 2, 9, 4, 8, 5, 6]
# select / filter — elements meeting the condition
puts data.select { |n| n > 5 }.inspect # => [7, 9, 8, 6]
puts data.filter { |n| n.odd? }.inspect # => [3, 1, 7, 9, 5] (alias of select)
# reject — elements NOT meeting the condition
puts data.reject { |n| n > 5 }.inspect # => [3, 1, 2, 4, 5]
# find / detect — the first element meeting the condition
puts data.find { |n| n > 5 } # => 7
puts data.detect { |n| n > 5 } # => 7 (alias)
# find_index — index of the first element meeting the condition
puts data.find_index { |n| n > 5 } # => 2
puts data.index(9) # => 4 (find a specific value)
# filter_map — filter AND transform in one go (Ruby 2.7+)
# ANTI-PATTERN: two separate steps
result = data.select { |n| n > 5 }.map { |n| n * 10 }
# CORRECT: filter_map — one step, more efficient
result = data.filter_map { |n| n * 10 if n > 5 }
puts result.inspect # => [70, 90, 80, 60]
# Real-world filter_map example
users = [
{ name: "Rina", active: true, score: 85 },
{ name: "Budi", active: false, score: 92 },
{ name: "Citra", active: true, score: 78 },
{ name: "Deni", active: true, score: 60 }
]
active_high_scorers = users.filter_map do |u|
u[:name] if u[:active] && u[:score] >= 75
end
puts active_high_scorers.inspect # => ["Rina", "Citra"]
Set Operations #
Ruby Arrays support mathematical set operations directly through operators:
a = [1, 2, 3, 4, 5]
b = [3, 4, 5, 6, 7]
puts (a | b).inspect # => [1, 2, 3, 4, 5, 6, 7] — union (unique)
puts (a & b).inspect # => [3, 4, 5] — intersection
puts (a - b).inspect # => [1, 2] — difference
puts (a + b).inspect # => [1, 2, 3, 4, 5, 3, 4, 5, 6, 7] — concat (duplicates allowed)
# Real-world example: find users present in both systems
system_a = ["user1", "user2", "user3", "user4"]
system_b = ["user2", "user4", "user5", "user6"]
in_both = system_a & system_b # => ["user2", "user4"]
only_in_a = system_a - system_b # => ["user1", "user3"]
only_in_b = system_b - system_a # => ["user5", "user6"]
all_combined = system_a | system_b # => all without duplicates
Sorting #
numbers = [5, 2, 8, 1, 9, 3]
puts numbers.sort.inspect # => [1, 2, 3, 5, 8, 9] (ascending)
puts numbers.sort.reverse.inspect # => [9, 8, 5, 3, 2, 1] (descending)
# sort_by — more idiomatic for complex objects
products = [
{ name: "Monitor", price: 3_500_000 },
{ name: "Mouse", price: 350_000 },
{ name: "Laptop", price: 15_000_000 },
{ name: "Keyboard", price: 450_000 }
]
# Sort by price ascending
products.sort_by { |p| p[:price] }.each { |p| puts "#{p[:name]}: #{p[:price]}" }
# => Mouse: 350000, Keyboard: 450000, Monitor: 3500000, Laptop: 15000000
# Sort by price descending
products.sort_by { |p| -p[:price] }.map { |p| p[:name] }
# => ["Laptop", "Monitor", "Keyboard", "Mouse"]
# Sort by multiple criteria — name ascending if prices are equal
people = [
{ name: "Citra", age: 28 },
{ name: "Andi", age: 35 },
{ name: "Budi", age: 28 },
]
people.sort_by { |p| [p[:age], p[:name]] }
# => [{Citra,28}, {Budi,28}, {Andi,35}] — age asc, name asc on ties
Grouping and Aggregation #
transactions = [
{ month: "Jan", amount: 100_000 },
{ month: "Jan", amount: 50_000 },
{ month: "Feb", amount: 200_000 },
{ month: "Feb", amount: 75_000 },
{ month: "Mar", amount: 150_000 }
]
# group_by — group by a criterion
per_month = transactions.group_by { |t| t[:month] }
per_month.each do |month, data|
total = data.sum { |t| t[:amount] }
puts "#{month}: Rp #{total} (#{data.length} transactions)"
end
# => Jan: Rp 150000 (2 transactions)
# => Feb: Rp 275000 (2 transactions)
# => Mar: Rp 150000 (1 transaction)
# tally — count occurrence frequencies
favorite_colors = [:blue, :red, :blue, :green, :blue, :red]
puts favorite_colors.tally.inspect
# => {:blue=>3, :red=>2, :green=>1}
# partition — split into two groups
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even, odd = numbers.partition { |n| n.even? }
puts even.inspect # => [2, 4, 6, 8, 10]
puts odd.inspect # => [1, 3, 5, 7, 9]
# each_slice and each_cons — iterate in chunks
(1..10).each_slice(3) { |slice| print "#{slice} " }
# => [1, 2, 3] [4, 5, 6] [7, 8, 9] [10]
(1..5).each_cons(3) { |cons| print "#{cons} " }
# => [1, 2, 3] [2, 3, 4] [3, 4, 5] (sliding window)
Multidimensional Arrays and Flatten #
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Accessing elements
puts matrix[1][2] # => 6
puts matrix.dig(2, 1) # => 8
# Matrix iteration
matrix.each_with_index do |row, i|
row.each_with_index do |value, j|
print "#{value} "
end
puts
end
# Transpose — swap rows and columns
puts matrix.transpose.inspect
# => [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# flatten — flatten nested arrays
nested = [1, [2, 3], [4, [5, [6, 7]]]]
puts nested.flatten.inspect # => [1, 2, 3, 4, 5, 6, 7] (all levels)
puts nested.flatten(1).inspect # => [1, 2, 3, 4, [5, [6, 7]]] (one level)
puts nested.flatten(2).inspect # => [1, 2, 3, 4, 5, [6, 7]] (two levels)
Array Destructuring #
Ruby supports destructuring — unpacking an array into individual variables:
# Basic destructuring
a, b, c = [1, 2, 3]
puts "#{a}, #{b}, #{c}" # => 1, 2, 3
# With splat — capture the "rest"
first, *rest = [1, 2, 3, 4, 5]
puts first.inspect # => 1
puts rest.inspect # => [2, 3, 4, 5]
*start, last = [1, 2, 3, 4, 5]
puts start.inspect # => [1, 2, 3, 4]
puts last # => 5
head, *middle, tail = [1, 2, 3, 4, 5]
puts head # => 1
puts middle.inspect # => [2, 3, 4]
puts tail # => 5
# Swap without a temporary variable
x, y = 10, 20
x, y = y, x
puts "x=#{x}, y=#{y}" # => x=20, y=10
# Destructuring in iteration
pairs = [["Rina", 85], ["Budi", 92], ["Citra", 78]]
pairs.each do |(name, score)|
puts "#{name}: #{score}"
end
# Or without parentheses
pairs.each do |name, score|
puts "#{name}: #{score}"
end
Check and Utility Methods #
arr = [3, 1, 4, 1, 5, 9, 2, 6]
# Condition checks
puts arr.any? { |n| n > 8 } # => true
puts arr.all? { |n| n > 0 } # => true
puts arr.none? { |n| n > 10 } # => true
puts arr.one? { |n| n > 8 } # => true (exactly one)
# Statistics
puts arr.min # => 1
puts arr.max # => 9
puts arr.sum # => 31
puts arr.count # => 8
puts arr.count(1) # => 2 (how many times value 1 appears)
puts arr.count { |n| n > 4 } # => 3
# Deduplication
duplicates = [1, 2, 2, 3, 3, 3, 4]
puts duplicates.uniq.inspect # => [1, 2, 3, 4]
puts duplicates.uniq.length # => 4
# compact — remove all nils
with_nil = [1, nil, 2, nil, 3, nil]
puts with_nil.compact.inspect # => [1, 2, 3]
# rotate — rotate the array
arr = [1, 2, 3, 4, 5]
puts arr.rotate.inspect # => [2, 3, 4, 5, 1] (rotate 1 to the left)
puts arr.rotate(2).inspect # => [3, 4, 5, 1, 2]
puts arr.rotate(-1).inspect # => [5, 1, 2, 3, 4] (rotate to the right)
# combination and permutation — mathematical combinations
puts [1, 2, 3].combination(2).to_a.inspect
# => [[1, 2], [1, 3], [2, 3]]
puts [1, 2, 3].permutation(2).to_a.inspect
# => [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
Destructive vs Non-Destructive Methods #
Ruby follows a convention: methods ending in ! modify the original array, while those without ! return a new array:
| Method | Non-destructive | Destructive |
|---|---|---|
| sort | arr.sort | arr.sort! |
| reverse | arr.reverse | arr.reverse! |
| flatten | arr.flatten | arr.flatten! |
| compact | arr.compact | arr.compact! |
| uniq | arr.uniq | arr.uniq! |
| map | arr.map | arr.map! |
| select | arr.select | arr.select! |
| reject | arr.reject | arr.reject! |
# ANTI-PATTERN: using ! carelessly
data = [3, 1, 2]
data.sort! # modifies the original data — maybe unintentional
# CORRECT: non-destructive for safety
result = data.sort # data stays [3, 1, 2]
puts result.inspect # => [1, 2, 3]
puts data.inspect # => [3, 1, 2] (unchanged)
# ! is useful when you really want in-place modification and don't need a new variable
buffer = []
1000.times { buffer << rand(100) }
buffer.sort!.uniq! # in-place modification — saves allocating new objects
Expressive Chaining #
Ruby Array’s real power appears when you combine methods into a pipeline that reads like a description of the problem itself:
transactions = [
{ id: 1, user: "Rina", amount: 150_000, valid: true },
{ id: 2, user: "Budi", amount: 50_000, valid: false },
{ id: 3, user: "Rina", amount: 200_000, valid: true },
{ id: 4, user: "Citra", amount: 75_000, valid: true },
{ id: 5, user: "Budi", amount: 300_000, valid: true },
]
# "Which unique users have valid transactions above 100k?"
active_users = transactions
.select { |t| t[:valid] && t[:amount] > 100_000 }
.map { |t| t[:user] }
.uniq
.sort
puts active_users.inspect # => ["Budi", "Rina"]
# "What's the total of valid transactions per user?"
total_per_user = transactions
.select { |t| t[:valid] }
.group_by { |t| t[:user] }
.transform_values { |data| data.sum { |t| t[:amount] } }
.sort_by { |_, total| -total }
.to_h
total_per_user.each do |user, total|
puts "#{user}: Rp #{total}"
end
# => Budi: Rp 300000
# => Rina: Rp 350000 ← Rina has two valid transactions
# => Citra: Rp 75000
Summary #
%w[]and%i[]are concise shorthands for string and symbol arrays — use them instead of writing quotes one by one.Array.new(n) { [] }notArray.new(n, [])— the block ensures each element is a distinct object, avoiding shared references.digfor nested arrays — safer than chaining[][]because it returnsnilinstead of crashing when a level isnil.filter_map=select+mapin one step — more efficient and concise for simultaneous filtering and transformation (Ruby 2.7+).- Set operations
|,&,-are directly available — no manual loops needed for union, intersection, or difference of two arrays.sort_byis more idiomatic thansortwith spaceship — especially for complex objects, and it supports multi-criteria sorting with arrays.zipfor combining parallel arrays — very useful for building a Hash from two arrays or merging data from different sources.flatten(n)for depth control —flattenwithout arguments flattens all levels,flatten(1)only one level.each_sliceandeach_consfor iterating in chunks or sliding windows.- Destructive
!methods only when you really need in-place modification — by default, use the non-destructive versions to prevent unintended side effects.