From 50fbcde6aa782dc7f9a71df65b0df0ef17b090a5 Mon Sep 17 00:00:00 2001 From: Tyler Rick Date: Thu, 9 Aug 2018 20:06:22 -0700 Subject: [PATCH] Add Array#indexes --- lib/core/facets/array/indexes.rb | 113 +++++++++++++++++++ test/core/array/test_indexes.rb | 182 +++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 lib/core/facets/array/indexes.rb create mode 100644 test/core/array/test_indexes.rb diff --git a/lib/core/facets/array/indexes.rb b/lib/core/facets/array/indexes.rb new file mode 100644 index 00000000..b5639adb --- /dev/null +++ b/lib/core/facets/array/indexes.rb @@ -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 + # #=> # + # 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 diff --git a/test/core/array/test_indexes.rb b/test/core/array/test_indexes.rb new file mode 100644 index 00000000..8227738d --- /dev/null +++ b/test/core/array/test_indexes.rb @@ -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