Skip to content

Add Array#indexes (index_all) #294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions lib/core/facets/array/indexes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
class Array
# Returns an array of _indexes_ of all objects in receiver such that the object is == to obj.
#
# If a block is given instead of an argument, returns the _indexes_ of all objects for which the
# block returns true.
#
# If neither a block nor argument is given, an Enumerator for _all_ indexes (each_index) is returned.
#
# Returns [] if no match is found.
#
# a = [ "a", "b", "c" ]
# a.indexes("b").to_a #=> [1]
# a.indexes("z").to_a #=> []
# a.indexes { |x| x == "b" }.to_a #=> [1]
#
# Like Array#index/rindex and Enumerable#find_index but returns _all_ indexes instead of just the
# first/last.
#
# See also: proposal to add Array#indexes to Ruby language: https://bugs.ruby-lang.org/issues/6596
#
# @author Tyler Rick
def indexes(*args)
case args.length
when 0
if block_given?
each_index.select {|i| yield(self[i]) }
else
each_index
end
when 1
other = args.first
each_index.select {|i| self[i] == other }
else
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0..1)"
end
end
alias_method :index_all, :indexes
end

module Enumerable
# Returns an enumerator of _indexes_ of all objects in receiver such that the object is == to obj.
#
# If a block is given instead of an argument, returns the _indexes_ of all objects for which the
# block returns true.
#
# If neither a block nor argument is given, an Enumerator for _all_ indexes is returned.
#
# Returns [] if no match is found.
#
# a = ("a".."c")
# a.indexes("b").to_a #=> [1]
# a.indexes("z").to_a #=> []
# a.indexes { |x| x == "b" }.to_a #=> [1]
#
# Like Array#index/rindex and Enumerable#find_index but returns _all_ indexes instead of just the
# first/last.
#
# See also: proposal to add Array#indexes to Ruby language: https://bugs.ruby-lang.org/issues/6596
#
# @author Tyler Rick
#
def indexes(*args)
# Enumerable doesn't have each_index like Array has, so this uses each_with_index instead.
case args.length
when 0
if block_given?
each_with_index.select {|el, i| yield(el) }.map {|el, i| i }
else
each_with_index. map {|el, i| i }.to_enum
end
when 1
other = args.first
each_with_index. select {|el, i| el == other }.map {|el, i| i }
else
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0..1)"
end
end
alias_method :index_all, :indexes
end

class Enumerator::Lazy
# Returns an enumerator of _indexes_ of all object in receiver such that the object is == to obj.
#
# If a block is given instead of an argument, returns the _indexes_ of all objects for which the
# block returns true.
#
# If neither a block nor argument is given, an Enumerator for _all_ indexes is returned.
#
# enum = (42 .. Float::INFINITY).lazy.indexes
# #=> #<Enumerator::Lazy: ...>
# enum.next #=> 0
# enum.next #=> 1
# enum.first(5) #=> [0, 1, 2, 3, 4]
#
# @author Tyler Rick
#
def indexes(*args)
case args.length
when 0
if block_given?
each_with_index.lazy.select {|el, i| yield(el) }.map {|el, i| i }
else
each_with_index.lazy. map {|el, i| i }.to_enum
end
when 1
other = args.first
each_with_index.lazy. select {|el, i| el == other }.map {|el, i| i }
else
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0..1)"
end
end
alias_method :index_all, :indexes
end
182 changes: 182 additions & 0 deletions test/core/array/test_indexes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
covers 'facets/array/indexes'

module IndexAllCommon
end

test_case Array do
method :indexes do
test "no argument or block" do
%w[a b c].indexes.class.assert == Enumerator
%w[a b c].indexes.to_a.assert == [0, 1, 2]
end

test "with argument, no match" do
%w[a b c].indexes('z').assert == []
end

test "with block, no match" do
%w[a b c].indexes {|x| x == 'z' }.assert == []
end

test "with nil argument, 1 match" do
[1, nil, 3].indexes(nil).assert == [1]
end

test "with argument, 1 match" do
[1, 2, 3].indexes(2). assert == [1]
%w[a b c].indexes('b').assert == [1]
end

test "with 2 arguments" do
ArgumentError.assert.raised? {
[1, 2, 3].indexes(1, 2)
}
end

test "with block, 1 match" do
[1, 2, 3].indexes {|x| x == 2 }.assert == [1]
%w[a b c].indexes {|x| x.upcase == 'B' }.assert == [1]
end

test "with argument and block, 1 match" do
# This should raise an error, but for now ignoring the block to maintain behavior as close to
# index as possible.
[1, 2, 3].indexes(2) {|x| x == 1 }.assert == [1]
%w[a b c].indexes('b') {|x| x.upcase == 'A' }.assert == [1]
end

test "with argument, 2 matches" do
[1, 2, 2]. indexes(2).assert == [1, 2]
[1,2,2,3,3,3].indexes(2).assert == [1, 2]
[1,2,2,3,3,3].indexes(3).assert == [3, 4, 5]
%w[a b B c].indexes {|x| x.upcase == 'B' }.assert == [1, 2]
end
end

method :index_all do
test "with argument, 1 match" do
[1, 2, 3].index_all(2). assert == [1]
%w[a b c].index_all('b').assert == [1]
end
end
end

require 'delegate'
class AnEnumerable < SimpleDelegator
include Enumerable
end

test_case Enumerable do
method :indexes do
test "no argument or block" do
(?a..?c).indexes.class.assert == Enumerator
(?a..?c).indexes.to_a.assert == [0, 1, 2]
end

test "with argument, no match" do
(?a..?c).indexes('z').assert == []
end

test "with block, no match" do
(?a..?c).indexes {|x| x == 'z' }.assert == []
end

test "with nil argument, 1 match" do
AnEnumerable.new([1, nil, 3]).indexes(nil).assert == [1]
end

test "with argument, 1 match" do
(1..3). indexes(2). assert == [1]
(?a..?c).indexes('b').assert == [1]
end

test "with 2 arguments" do
ArgumentError.assert.raised? {
(1..3).indexes(1, 2)
}
end

test "with block, 1 match" do
(1..3). indexes {|x| x == 2 }.assert == [1]
(?a..?c).indexes {|x| x.upcase == 'B' }.assert == [1]
end

test "with argument and block, 1 match" do
# This should raise an error, but for now ignoring the block to maintain behavior as close to
# index as possible.
(1..3).indexes(2) {|x| x == 1 }.assert == [1]
end

test "very large receiver, very many matches" do
(1..2**5).indexes(2).first(1).assert == [1]
end
end

method :index_all do
test "very large receiver, very many matches" do
(1..2**5).index_all(2).first(1).assert == [1]
end
end
end

test_case Enumerator::Lazy do
method :indexes do
test "no argument or block" do
%w[a b c].lazy.indexes.class.assert == Enumerator::Lazy
%w[a b c].lazy.indexes.to_a.assert == [0, 1, 2]
end

test "with argument, no match" do
%w[a b c].lazy.indexes('z').force.assert == []
end

test "with block, no match" do
%w[a b c].lazy.indexes {|x| x == 'z' }.force.assert == []
end

test "with nil argument, 1 match" do
[1, nil, 3].lazy.indexes(nil).class.assert == Enumerator::Lazy
[1, nil, 3].lazy.indexes(nil).force.assert == [1]
end

test "with argument, 1 match" do
[1, 2, 3].lazy.indexes(2). force.assert == [1]
%w[a b c].lazy.indexes('b').force.assert == [1]
end

test "with 2 arguments" do
ArgumentError.assert.raised? {
(1..3).lazy.indexes(1, 2)
}
end

test "with block, 1 match" do
[1, 2, 3].lazy.indexes {|x| x == 2 }.force.assert == [1]
%w[a b c].lazy.indexes {|x| x.upcase == 'B' }.force.assert == [1]
end

test "with argument, 2 matches" do
[1, 2, 2]. lazy.indexes(2).force.assert == [1, 2]
[1,2,2,3,3,3].lazy.indexes(2).force.assert == [1, 2]
[1,2,2,3,3,3].lazy.indexes(3).force.assert == [3, 4, 5]
%w[a b B c].lazy.indexes {|x| x.upcase == 'B' }.force.assert == [1, 2]
end

test "infinitely large enumerable" do
(1..Float::INFINITY).lazy.indexes(2).first(1).assert == [1]
end

test "infinitely large enumerable, infinitely many matches" do
(1..Float::INFINITY).lazy.indexes {|i| i.odd? }.class.assert == Enumerator::Lazy
(1..Float::INFINITY).lazy.indexes {|i| i.odd? }.first(5).assert == [0, 2, 4, 6, 8]
(1..Float::INFINITY).lazy.map {|i| i.odd? ? i - 1 : i }.map(&:to_s).indexes {|el| el.to_i.even? }.first(5).assert == [0, 1, 2, 3, 4]
end
end

method :index_all do
test "infinitely large enumerable, infinitely many matches" do
(1..Float::INFINITY).lazy.index_all {|i| i.odd? }.class.assert == Enumerator::Lazy
(1..Float::INFINITY).lazy.index_all {|i| i.odd? }.first(5).assert == [0, 2, 4, 6, 8]
end
end
end