rubyguides

Hash#min_by

Overview

min_by is defined on Enumerable, so it works on hashes as well as arrays. When called on a hash, it evaluates the block for each key-value pair and returns the pair that produced the smallest value. If multiple pairs tie for the minimum, the first one encountered is returned.

Unlike Hash#min which compares hash keys or values directly, min_by lets you derive a comparison value from each pair.

Signature

min_by { |(key, value)| block } -> [key, value] | nil
min_by(n) { |(key, value)| block } -> Array | nil
min_by -> Enumerator

Return Value

Returns a two-element array [key, value] for the pair that minimizes the block result. Returns nil if the hash is empty.

Basic Usage

Finding the minimum by a value attribute

servers = {
  web1: { cpu: 45, memory: 80 },
  web2: { cpu: 12, memory: 90 },
  web3: { cpu: 33, memory: 55 }
}

servers.min_by { |_name, attrs| attrs[:cpu] }
# => [:web2, { cpu: 12, memory: 90 }]

The servers example picks the machine with the lowest CPU usage by passing a block that extracts attrs[:cpu] from each server’s metadata. The block destructures the two-element array that Hash iteration yields — the first argument is the server name (which the block ignores with _name) and the second is the attributes hash. This destructuring pattern is consistent across all Enumerable methods called on hashes, and the _ prefix convention signals to readers that only the value portion matters for the comparison.

Using a symbol-to-proc shorthand

scores = { alice: 95, bob: 82, carol: 78 }

scores.min_by { |_name, score| score }
# => [:carol, 78]

# equivalent using Enumerable#min_by on array of pairs
scores.min_by(&:last)
# => [:carol, 78]

The &:last shorthand is a concise way to compare hash pairs by their values. Since Hash iteration yields [key, value] arrays, calling .last on each pair extracts the value, and min_by compares those extracted values. This pattern is equivalent to the explicit block { |_k, v| v } and is widely used in Ruby codebases where the comparison criterion is simply the hash value.

With custom comparison logic

files = {
  report: { size: 1024, modified: Time.new(2024, 1, 1) },
  data:   { size: 8192, modified: Time.new(2024, 6, 15) },
  log:    { size: 2048, modified: Time.new(2024, 3, 10) }
}

# Find the smallest file
files.min_by { |_name, attrs| attrs[:size] }
# => [:report, { size: 1024, modified: ... }]

# Find the most recently modified
files.min_by { |_name, attrs| -attrs[:modified].to_i }
# => [:data, { size: 8192, modified: ... }]

The files example demonstrates two different comparison criteria on the same hash. Finding the smallest file uses attrs[:size] directly, while finding the most recently modified file negates the timestamp’s integer value. Negating the value turns min_by into a de facto max_by — the smallest negative number corresponds to the largest original value. This negation trick works with any comparable type that responds to unary -.

Common use cases

Finding the cheapest or lightest option

products = {
  basic:   { price: 29, weight: 200 },
  standard: { price: 49, weight: 350 },
  premium: { price: 89, weight: 150 }
}

# Find the lightest product
products.min_by { |_name, attrs| attrs[:weight] }
# => [:premium, { price: 89, weight: 150 }]

# Find the cheapest product
products.min_by { |_name, attrs| attrs[:price] }
# => [:basic, { price: 29, weight: 200 }]

The products example shows how min_by can answer different questions about the same data just by changing the block. The lightest product happens to be the most expensive one, and the cheapest product is the heaviest — a classic trade-off that min_by surfaces without requiring any sorting or restructuring of the hash. Each call is a one-line answer to a specific question, which keeps the analysis code compact and the intent obvious to anyone reading it later.

Finding the closest match

users = {
  alice:   { lat: 51.5074, lon: -0.1278 },   # London
  bob:     { lat: 40.7128, lon: -74.0060 },   # New York
  carol:   { lat: 35.6762, lon: 139.6503 }     # Tokyo
}

def distance(lat1, lon1, lat2, lon2)
  Math.sqrt((lat1 - lat2)**2 + (lon1 - lon2)**2)
end

# Find the user closest to London (51.5, -0.1)
closest = users.min_by do |_name, loc|
  distance(loc[:lat], loc[:lon], 51.5, -0.1)
end
# => [:alice, { lat: 51.5074, lon: -0.1278 }]

The closest example computes a Euclidean distance from a reference point for each user and picks the one with the smallest distance. The distance helper is a simple Pythagorean formula — good enough for proximity checks on small geographic scales where the curvature of the Earth is negligible. For production geospatial queries, use the Haversine formula or a dedicated library like geocoder.

Finding the shortest or fastest route

routes = {
  via_north: { distance: 450, time: 6.5 },
  via_coast: { distance: 380, time: 8.0 },
  via_mountain: { distance: 320, time: 9.5 }
}

# Find the fastest route
routes.min_by { |_name, info| info[:time] }
# => [:via_north, { distance: 450, time: 6.5 }]

# Find the shortest route
routes.min_by { |_name, info| info[:distance] }
# => [:via_mountain, { distance: 320, time: 9.5 }]

The routes example highlights a real-world trade-off: the fastest route is also the longest, and the shortest route takes the most time. min_by surfaces this tension clearly because each call answers a single well-defined question — “which route minimizes time?” or “which route minimizes distance?” — without conflating the two criteria.

Using negative values for a “maximum” effect

Since there’s no built-in max_by on Hash directly in some Ruby versions, min_by with a negated value works:

scores = { alice: 95, bob: 82, carol: 78 }

# Using negative to find maximum
scores.min_by { |_name, score| -score }
# => [:alice, 95]

Getting multiple minimums

Pass a number to return the n smallest pairs:

products = {
  widget: { price: 10 },
  gadget: { price: 25 },
  gizmo:  { price: 15 },
  doohickey: { price: 30 }
}

# Find the two cheapest products
products.min_by(2) { |_name, attrs| attrs[:price] }
# => [[:widget, { price: 10 }], [:gizmo, { price: 15 }]]

Passing 2 as the first argument to min_by returns the two cheapest products as an array of [key, value] pairs, sorted by price ascending. The n argument is useful when you need more than just the single minimum — for leaderboards, top-N queries, or any situation where the extreme values carry more meaning than the middle of the distribution.

Return value when empty

{}.min_by { |k, v| v }
# => nil

No exception is raised for an empty hash — it returns nil.

The nil return for an empty hash is consistent with other Enumerable methods like min, max, and find. It means you can safely chain min_by with the safe navigation operator (&.) or a default value: hash.min_by { |k, v| v }&.first || "no data". This pattern avoids the NoMethodError that would occur if min_by raised an exception on empty input.

Comparison with alternatives

vs Enumerable#min

# min_by works on the value (or block result) directly
scores = { alice: 95, bob: 82, carol: 78 }
scores.min_by { |_k, v| v }       # => [:carol, 78]

# min without a block compares keys
scores.min                       # => [:alice, 95]

# min with a block compares by block result
scores.min { |a, b| a.last <=> b.last }  # => [:carol, 78]

vs sort.first

sort arranges the entire hash, which is O(n log n). min_by only makes a single pass, which is O(n):

# Efficient — single pass
scores.min_by { |_k, v| v }

# Wasteful — sorts everything first
scores.sort { |a, b| a.last <=> b.last }.first

For finding a single minimum, min_by is always the better choice.

The performance comparison between min_by and sort.first is worth internalizing. min_by makes a single pass through the hash (O(n)), keeping track of the minimum as it goes. sort.first sorts the entire hash first (O(n log n)) and then discards everything but the first element. For a hash with 10,000 entries, min_by does 10,000 comparisons while sort.first does roughly 130,000 — a 13x difference that widens as the hash grows.

Gotchas

Returns a two-element array, not a single value. If you want just the key or just the value, destructure it:

key, value = scores.min_by { |_k, v| v }
key    # => :carol
value  # => 78

Destructuring the two-element array that min_by returns is the idiomatic way to get the winning key and value separately. The parallel assignment key, value = scores.min_by { ... } unpacks the [:carol, 78] array into two variables in a single line. If you only need one of the two, use _ for the unused position: _, min_val = scores.min_by { |_k, v| v }.

Block argument order for hashes. When iterating a hash, the block receives the key first, then the value:

h.each { |k, v| puts "#{k}: #{v}" }  # a: 1, b: 2

The block argument order for hashes — key first, then value — is standard across Ruby’s Enumerable methods. This means you can copy a block from each directly into min_by, select, or reject without reordering the parameters. Just remember to ignore the key with the _ prefix when you only care about the value.

This is consistent with each, map, and other hash iterators.

Tie-breaking is deterministic but unspecified. When two pairs produce the same block value, min_by returns whichever one it encounters first during iteration. In modern Ruby (3.2+), hash iteration order is the insertion order, so ties go to the pair that was added to the hash earliest. If you need a specific tie-breaking rule — for example, preferring the pair with a shorter key — include that logic inside the block itself rather than relying on iteration order.

See Also