diff --git a/spec/std/indexable_spec.cr b/spec/std/indexable_spec.cr index cebd1baf5e2c..344e5bda957b 100644 --- a/spec/std/indexable_spec.cr +++ b/spec/std/indexable_spec.cr @@ -3,7 +3,7 @@ require "spec" private class SafeIndexable include Indexable(Int32) - getter size + property size def initialize(@size : Int32) end @@ -52,4 +52,66 @@ describe Indexable do end.should be_nil is.should eq([0, 1, 2]) end + + it "iterates throught a subset of its elements (#3386)" do + indexable = SafeIndexable.new(5) + last_element = nil + + return_value = indexable.each(start: 2, count: 3) do |elem| + last_element = elem + end + + return_value.should eq(indexable) + last_element.should eq(4) + end + + it "iterates until its size (#3386)" do + indexable = SafeIndexable.new(5) + last_element = nil + + indexable.each(start: 3, count: 999) do |elem| + last_element = elem + end + + last_element.should eq(4) + end + + it "iterates until its size, having mutated (#3386)" do + indexable = SafeIndexable.new(10) + last_element = nil + + indexable.each(start: 3, count: 999) do |elem| + indexable.size += 1 if elem <= 5 + # size is incremented 3 times + last_element = elem + end + + # last was 9, but now is 12. + last_element.should eq(12) + end + + it "iterates until its size, having mutated (#3386)" do + indexable = SafeIndexable.new(10) + last_element = nil + + indexable.each(start: 3, count: 5) do |elem| + indexable.size += 1 + last_element = elem + end + + # last element iterated is still 7. + last_element.should eq(7) + end + + it "iterates within a range of indices (#3386)" do + indexable = SafeIndexable.new(5) + last_element = nil + + return_value = indexable.each(within: 2..3) do |elem| + last_element = elem + end + + return_value.should eq(indexable) + last_element.should eq(3) + end end diff --git a/src/indexable.cr b/src/indexable.cr index 3c29593dff6d..ba33b81dc777 100644 --- a/src/indexable.cr +++ b/src/indexable.cr @@ -165,6 +165,51 @@ module Indexable(T) ItemIterator(self, T).new(self) end + # Calls the given block once for `count` number of elements in `self` + # starting from index `start`, passing each element as a parameter. + # + # Negative indices count backward from the end of the array. (-1 is the + # last element). + # + # Raises `IndexError` if the starting index is out of range. + # Raises `ArgumentError` if `count` is a negative number. + # + # ``` + # array = ["a", "b", "c", "d", "e"] + # array.each(start: 1, count: 3) { |x| print x, " -- " } + # ``` + # + # produces: + # + # ```text + # b -- c -- d -- + # ``` + def each(*, start : Int, count : Int) + each_index(start: start, count: count) do |i| + yield unsafe_at(i) + end + end + + # Calls the given block once for all elements at indices within the given + # `range`, passing each element as a parameter. + # + # Raises `IndexError` if the starting index is out of range. + # + # ``` + # array = ["a", "b", "c", "d", "e"] + # array.each(within: 1..3) { |x| print x, " -- " } + # ``` + # + # produces: + # + # ```text + # b -- c -- d -- + # ``` + def each(*, within range : Range(Int, Int)) + start, count = Indexable.range_to_index_and_count(range, size) + each(start: start, count: count) { |element| yield element } + end + # Calls the given block once for each index in `self`, passing that # index as a parameter. # @@ -201,6 +246,41 @@ module Indexable(T) IndexIterator.new(self) end + # Calls the given block once for `count` number of indices in `self` + # starting from index `start`, passing each index as a parameter. + # + # Negative indices count backward from the end of the array. (-1 is the + # last element). + # + # Raises `IndexError` if the starting index is out of range. + # Raises `ArgumentError` if `count` is a negative number. + # + # ``` + # array = ["a", "b", "c", "d", "e"] + # array.each_index(start: -3, count: 2) { |x| print x, " -- " } + # ``` + # + # produces: + # + # ```text + # 2 -- 3 -- + # ``` + def each_index(*, start : Int, count : Int) + raise ArgumentError.new "negative count: #{count}" if count < 0 + + start += size if start < 0 + raise IndexError.new unless 0 <= start <= size + + i = start + # `count` and size comparison must be done every iteration because + # `self` can mutate in the block. + while i < Math.min(start + count, size) + yield i + i += 1 + end + self + end + # Returns `true` if `self` is empty, `false` otherwise. # # ```