rubyguides

Array#uniq

What does uniq do?

uniq returns a new array with duplicate elements removed. By default, elements are compared using == — if two elements are equal, only the first occurrence is kept and subsequent duplicates are discarded.

Array#uniq is defined directly on the Array class but behaves identically to Enumerable#uniq — Array includes Enumerable and overrides uniq with an optimised implementation.

Basic Usage

Removing duplicate numbers

[1, 2, 2, 3, 1, 4, 3, 5].uniq
# => [1, 2, 3, 4, 5]

The first occurrence of each value is preserved. 1 appears at index 0, 2 at index 1, 3 at index 3, etc. uniq uses eql? and hash internally to determine whether two elements are equal, which means it works correctly with any Ruby object that implements those methods consistently — numbers, strings, symbols, and even custom classes.

With strings

names = ["Alice", "Bob", "Alice", "Charlie", "Bob", "Diana"]
names.uniq
# => ["Alice", "Bob", "Charlie", "Diana"]

String comparison uses the same eql? logic as number comparison. "Alice" at index 0 and the duplicate "Alice" at index 2 are treated as equal, so only the first occurrence survives. The order of the remaining names matches the order in which they first appeared in the original array.

Empty and single-element arrays

[].uniq
# => []

[42].uniq
# => [42]

Edge cases are straightforward: an empty array returns an empty array, and a single-element array returns itself. There is no error condition — uniq never raises an exception on any input, regardless of size or content. This makes it safe to call on arrays of unknown provenance without guard clauses.

With a Block

Pass a block to determine uniqueness based on the block’s return value instead of the elements themselves:

words = ["apple", "apricot", "banana", "blueberry", "cherry"]
words.uniq { |word| word[0] }
# => ["apple", "banana", "cherry"]

"apple"[0] and "apricot"[0] both return "a", so only the first "a"-word survives. The block result drives uniqueness — elements producing the same block value are considered duplicates. Internally, Ruby builds a hash keyed by the block return values rather than the elements themselves, which means the block result must respond to hash and eql? correctly.

Practical example: unique by attribute

users = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "member" },
  { name: "Charlie", role: "admin" },
  { name: "Diana", role: "member" }
]

users.uniq { |u| u[:role] }
# => [{:name=>"Alice", :role=>"admin"},
#     {:name=>"Bob", :role=>"member"}]

One record per role — useful when you need a representative from each category. Alice and Charlie share the "admin" role, so only Alice (the first admin) is kept. Bob and Diana share "member", so only Bob survives. The result gives you exactly one user per distinct role, preserving the order in which each role first appeared in the original list.

Order Preservation

uniq preserves the original order of elements. The first occurrence of each unique element is kept in its original position:

[3, 1, 4, 1, 5, 9, 2, 6].uniq
# => [3, 1, 4, 5, 9, 2, 6]

This matters when the sequence of elements carries meaning — unlike sorting or other transformations that reorder results. The output preserves the first-occurrence order of each value: 3 appears before 1, 4 before 5, and so on. If you sort the result, you lose this positional information, which may be significant if the array represents a timeline, a ranked list, or a series of events.

The bang variant: uniq!

The uniq! method mutates the original array in place. If duplicates were removed, it returns the same array. If no duplicates existed, it returns nil.

numbers = [1, 2, 2, 3, 1, 4, 3, 5]

result = numbers.uniq!

numbers  # => [1, 2, 3, 4, 5]
result   # => [1, 2, 3, 4, 5]
result.equal?(numbers)  # => true  (same object)

The exclamation mark signals in-place mutation: uniq! modifies the receiver instead of allocating a new array. The equal? check confirms that result and numbers reference the same object in memory, which means any other variable pointing to the original array will also see the deduplicated content. This is powerful but can be surprising if other code was relying on the duplicates.

When uniq! returns nil

If the array already contains only unique elements, uniq! returns nil and leaves the array unchanged:

numbers = [1, 2, 3]

result = numbers.uniq!
result   # => nil
numbers  # => [1, 2, 3]

When no duplicates exist to remove, uniq! returns nil and leaves the array unchanged. This nil-on-no-change convention is shared by many Ruby bang methods and lets you write conditional logic like if arr.uniq! to detect whether duplicates were actually present and removed.

Comparing uniq vs uniq!

original = [1, 2, 2, 3, 1]

# uniq returns a new array, original unchanged
deduped = original.uniq
original  # => [1, 2, 2, 3, 1]
deduped   # => [1, 2, 3]

# uniq! modifies the original
original.uniq!
original  # => [1, 2, 3]

The non-bang uniq is the safer default: it never alters the input, so you can call it on a frozen array or in functional pipelines without side effects. The bang variant saves an allocation by mutating in place, which matters when the array is large and you are discarding the original anyway. Choose based on whether the surrounding code expects the original array to stay intact.

Difference from & (set intersection)

Ruby has two ways to work with unique elements. uniq removes duplicates from a single array. & finds elements present in both of two arrays:

a = [1, 2, 2, 3, 4]
b = [2, 3, 3, 5]

# uniq removes duplicates from ONE array
a.uniq
# => [1, 2, 3, 4]

# & finds elements present in BOTH arrays
a & b
# => [2, 3]

uniq operates on one array and deduplicates it. & requires two arrays and returns their intersection. The intersection operator & also removes duplicates from the result, so a & b returns [2, 3] rather than [2, 2, 3, 3]. Both methods use the same eql? and hash comparisons under the hood, making their uniqueness logic consistent.

When to use each

  • Use uniq when you want to remove duplicates from one list
  • Use & when you want common elements between two lists
# Deduplicate one list
["cat", "dog", "cat", "mouse"].uniq
# => ["cat", "dog", "mouse"]

# Find overlapping tags
user_tags = ["ruby", "rails", "postgres"]
post_tags = ["ruby", "python", "postgres"]
user_tags & post_tags
# => ["ruby", "postgres"]

Using & for tag intersection is a common pattern in content-based applications where you want to find shared interests, categories, or labels between two entities. The intersection result is also deduplicated, so ["ruby", "ruby"] & ["ruby"] returns ["ruby"] — the operator applies uniq semantics to the output automatically, saving you an extra method call.

Performance Considerations

uniq iterates through the array once and uses a hash internally to track seen elements. This gives it O(n) time complexity, where n is the array size.

require "benchmark"

array = (1..1000).to_a + (1..500).to_a  # 1500 elements, 500 duplicates

Benchmark.measure do
  array.uniq
end.real   # => ~0.0002s

The uniq! variant avoids allocating a new array, which saves memory for large arrays. The iteration cost is the same — the performance difference is solely in allocation. Ruby’s hash-based deduplication makes uniq O(n) for typical inputs: each element is hashed once, and hash lookups are amortised constant time.

For block-based uniqueness, the hash stores the block result as the key rather than the element itself:

data = [1, 2, 3, 4, 5]
data.uniq { |n| n.even? ? "even" : "odd" }
# => [1, 2]

This is still O(n) — hashing the block result is constant time. The block is called once per element, and its return value determines which elements are grouped together. Elements 1, 3, and 5 all produce "odd" from the block, so only the first odd number (1) is kept, while 2 is kept as the first even number.

For block-based uniqueness, Ruby stores the block result as the key in an internal hash rather than the element itself. This means the block is called exactly once per element, and the hash lookup determines whether the result has been seen before. The time complexity remains O(n), with the hashing step adding a small constant overhead per element compared to the non-block form.

Edge Cases

nil values

nil is treated as a regular value and participates in uniqueness checking:

[1, nil, 2, nil, 3].uniq
# => [1, nil, 2, 3]

One nil is kept alongside one occurrence of each unique non-nil value.

With a block returning nil

When you pass a block to uniq, the block’s return value determines uniqueness grouping. If the block returns nil for multiple elements, those elements are treated as duplicates of each other because their block results compare as equal. This can be a deliberate strategy for collapsing categories, but it is worth testing carefully because nil comparisons can surprise readers who expect the block to behave as a filter rather than a grouping function.

data = [1, 2, 3, 4]
data.uniq { |n| n.even? ? "even" : nil }
# => [1, 2]

Odd numbers (1, 3) both produce nil from the block, so only the first odd number survives.

Hash elements

When you put hashes inside an array and call uniq, Ruby uses Hash#eql? and Hash#hash to determine equality, not just object identity with ==. Two separate hash objects with identical key-value pairs are treated as duplicates because their hash codes match and eql? confirms they contain the same data. This makes uniq safe to use on arrays of hashes without worrying about whether the hash objects themselves are the same instance.

[{a: 1}.hash]  # not directly comparable to [{a: 1}]

Ruby’s Hash class handles deduplication internally, but when using uniq on an array of hashes, two hashes with identical contents are considered equal. This works because Hash#eql? compares key-value pairs recursively and Hash#hash produces consistent values for equal hashes. Two hashes with the same keys and values will be treated as duplicates, even if they are different objects.

Practical Examples

Removing duplicate names

attendees = ["Alice", "Bob", "Charlie", "Alice", "Diana", "Bob"]
attendees.uniq
# => ["Alice", "Bob", "Charlie", "Diana"]

This is the simplest and most common use of uniq: deduplicating a list of names, IDs, or any flat collection where duplicates represent redundant entries rather than meaningful repeated values. The original string comparisons use == and eql? under the hood, so case matters — "alice" and "Alice" are treated as distinct values.

Finding unique values by attribute

products = [
  { name: "Laptop", category: "electronics" },
  { name: "Mouse", category: "electronics" },
  { name: "Carrot", category: "food" },
  { name: "Apple", category: "food" }
]

products.uniq { |p| p[:category] }
# => [{:name=>"Laptop", :category=>"electronics"},
#     {:name=>"Carrot", :category=>"food"}]

The block extracts the :category value from each hash, and uniq keeps only the first product in each category. Laptop and Mouse share "electronics", so only Laptop survives. Carrot and Apple share "food", so only Carrot is kept. This is a convenient way to sample one representative from each group without writing an explicit loop or grouping operation.

Cleaning extracted URLs

urls = [
  "https://example.com/page1",
  "https://example.com/page2",
  "https://example.com/page1",
  "https://example.com/page3"
]

urls.uniq
# => ["https://example.com/page1",
#     "https://example.com/page2",
#     "https://example.com/page3"]

URL deduplication is a common practical use of uniq. When crawling a website, processing server logs, or collecting links from user input, duplicates are common and need to be removed before further processing. The string comparison uses == and works correctly for full URLs because identical strings are always equal. If you need case-insensitive URL deduplication, pass a block that normalises the URLs first, such as uniq { |url| url.downcase }.

Chaining with other array methods

numbers = [1, 2, 2, 3, 3, 3, 4, 4, 5]

numbers.uniq.select(&:odd?)        # => [1, 3, 5]
numbers.uniq.map { |n| n * 10 }    # => [10, 20, 30, 40, 50]
numbers.uniq.reject { |n| n > 3 }  # => [1, 2, 3]

Chaining uniq before other array methods is a common pattern because deduplication reduces the number of elements subsequent operations must process. Each of these chains first collapses the array to unique values and then applies a transformation or filter. The result is always a new array, so the original numbers list (with duplicates) remains accessible for other calculations.

See Also