Add `Enumerable#sole` (#40914)

* Add `Enumerable#sole`, from `ActiveRecord::FinderMethods#sole`

* distinguish single-item Enumerable and two-item with nil last

Add a test for same.

* add symmetry, against rubocop's wishes
This commit is contained in:
Asherah Connor 2021-04-19 19:26:34 +10:00 committed by GitHub
parent 9a263e9a0f
commit 2e14c53fc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 33 additions and 0 deletions

View File

@ -1,3 +1,9 @@
* Add `Enumerable#sole`, per `ActiveRecord::FinderMethods#sole`. Returns the
sole item of the enumerable, raising if no items are found, or if more than
one is.
*Asherah Connor*
* Freeze `ActiveSupport::Duration#parts` and remove writer methods.
Durations are meant to be value objects and should not be mutated.

View File

@ -4,6 +4,10 @@ module Enumerable
INDEX_WITH_DEFAULT = Object.new
private_constant :INDEX_WITH_DEFAULT
# Error generated by +sole+ when called on an enumerable that doesn't have
# exactly one item.
class SoleItemExpectedError < StandardError; end
# Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
# when we omit an identity.
@ -215,6 +219,20 @@ module Enumerable
def in_order_of(key, series)
index_by(&key).values_at(*series).compact
end
# Returns the sole item in the enumerable. If there are no items, or more
# than one item, raises +Enumerable::SoleItemExpectedError+.
#
# ["x"].sole # => "x"
# Set.new.sole # => Enumerable::SoleItemExpectedError: no item found
# { a: 1, b: 2 }.sole # => Enumerable::SoleItemExpectedError: multiple items found
def sole
case count
when 1 then return first # rubocop:disable Style/RedundantReturn
when 0 then raise SoleItemExpectedError, "no item found"
when 2.. then raise SoleItemExpectedError, "multiple items found"
end
end
end
class Hash

View File

@ -319,4 +319,13 @@ class EnumerableTests < ActiveSupport::TestCase
values = [ Payment.new(5), Payment.new(1), Payment.new(3) ]
assert_equal [ Payment.new(1), Payment.new(5) ], values.in_order_of(:price, [ 1, 5 ])
end
def test_sole
expected_raise = Enumerable::SoleItemExpectedError
assert_raise(expected_raise) { GenericEnumerable.new([]).sole }
assert_equal 1, GenericEnumerable.new([1]).sole
assert_raise(expected_raise) { GenericEnumerable.new([1, 2]).sole }
assert_raise(expected_raise) { GenericEnumerable.new([1, nil]).sole }
end
end