Skip to content
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

Add Enumerable#each_step and Iterable#each_step #13610

Merged
merged 8 commits into from
Oct 18, 2023
28 changes: 28 additions & 0 deletions spec/std/enumerable_spec.cr
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "spec"
require "spec/helpers/iterate"

private class SpecEnumerable
include Enumerable(Int32)
Expand Down Expand Up @@ -393,6 +394,33 @@ describe "Enumerable" do
end
end

describe "each_step" do
it_iterates "yields every 2nd element", %w[a c e], %w[a b c d e f].each_step(2)
it_iterates "accepts an optional offset parameter", %w[b d f], %w[a b c d e f].each_step(2, offset: 1)
it_iterates "accepts an optional offset parameter of 0", %w[a c e], %w[a b c d e f].each_step(2, offset: 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like extra spec(s) where n > 0 && (n <= offset < size), which would skip some elements at the beginning


it_iterates "accepts a step larger then the enumerable size", %w[a], %w[a b c d e f].each_step(7)
it_iterates "accepts an offset larger then the enumerable size", %w[], %w[a b c d e f].each_step(1, offset: 7)

it "doesn't accept a negative step" do
expect_raises(ArgumentError) do
%w[a b c d e f].each_step(-2)
end
end

it "doesn't accept a step of 0" do
expect_raises(ArgumentError) do
%w[a b c d e f].each_step(0)
end
end

it "doesn't accept a negative offset" do
expect_raises(ArgumentError) do
%w[a b c d e f].each_step(2, offset: -2)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These specs only cover exceptions raised by Iterator#skip and Iterator#step, the Enumerable#each_step forms are not tested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, I had it in my mind that the iterator validation would carry over. I've added the validation to Enumerable#each_step.

end

describe "each_with_index" do
it "yields the element and the index" do
collection = [] of {String, Int32}
Expand Down
36 changes: 36 additions & 0 deletions src/enumerable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,42 @@ module Enumerable(T)
nil
end

# Iterates over the collection, yielding every *n*th element, starting with the first.
#
# ```
# %w[Alice Bob Charlie David].each_step(2) do |user|
# puts "User: #{user}"
# end
# ```
#
# Prints:
#
# ```text
# User: Alice
# User: Charlie
# ```
#
# Accepts an optional *offset* parameter
#
# ```
# %w[Alice Bob Charlie David].each_step(2, offset: 1) do |user|
# puts "User: #{user}"
# end
# ```
#
# Which would print:
#
# ```text
# User: Bob
# User: David
# ```
def each_step(n : Int, *, offset : Int = 0, & : T ->) : Nil
offset_mod = offset % n
each_with_index do |elem, i|
yield elem if i >= offset && i % n == offset_mod
end
end

# Iterates over the collection, yielding both the elements and their index.
#
# ```
Expand Down
14 changes: 14 additions & 0 deletions src/iterable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ module Iterable(T)
each.cons_pair
end

# Same as `each.step(n)`.
#
# See also: `Iterator#step`.
def each_step(n : Int)
each.step(n)
end

# Same as `each.skip(offset).step(n)`.
#
# See also: `Iterator#step`.
def each_step(n : Int, *, offset : Int)
each.skip(offset).step(n)
end

# Same as `each.with_index(offset)`.
#
# See also: `Iterator#with_index(offset)`.
Expand Down