summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md25
-rw-r--r--enumerator.c46
-rw-r--r--test/ruby/test_enumerator.rb36
3 files changed, 89 insertions, 18 deletions
diff --git a/NEWS.md b/NEWS.md
index 09fc053ef0..1d637891d3 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -33,6 +33,30 @@ Note that each entry is kept to a minimum, see links for details.
Note: We're only listing outstanding class updates.
+* Enumerator
+
+ * `Enumerator.produce` now accepts an optional `size` keyword argument
+ to specify the size of the enumerator. It can be an integer,
+ `Float::INFINITY`, a callable object (such as a lambda), or `nil` to
+ indicate unknown size. When not specified, the size is unknown (`nil`).
+ Previously, the size was always `Float::INFINITY` and not specifiable.
+
+ ```ruby
+ # Infinite enumerator
+ enum = Enumerator.produce(1, size: Float::INFINITY, &:succ)
+ enum.size # => Float::INFINITY
+
+ # Finite enumerator with known/computable size
+ abs_dir = File.expand_path("./baz") # => "/foo/bar/baz"
+ traverser = Enumerator.produce(abs_dir, size: -> { abs_dir.count("/") + 1 }) {
+ raise StopIteration if it == "/"
+ File.dirname(it)
+ }
+ traverser.size # => 4
+ ```
+
+ [[Feature #21701]]
+
* Kernel
* `Kernel#inspect` now checks for the existence of a `#instance_variables_to_inspect` method,
@@ -454,3 +478,4 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #21550]: https://bugs.ruby-lang.org/issues/21550
[Feature #21557]: https://bugs.ruby-lang.org/issues/21557
[Bug #21654]: https://bugs.ruby-lang.org/issues/21654
+[Feature #21701]: https://bugs.ruby-lang.org/issues/21701
diff --git a/enumerator.c b/enumerator.c
index cbdd5629cf..c2b2bfa9a0 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -221,6 +221,7 @@ struct yielder {
struct producer {
VALUE init;
VALUE proc;
+ VALUE size;
};
typedef struct MEMO *lazyenum_proc_func(VALUE, struct MEMO *, VALUE, long);
@@ -2876,6 +2877,7 @@ producer_mark_and_move(void *p)
struct producer *ptr = p;
rb_gc_mark_and_move(&ptr->init);
rb_gc_mark_and_move(&ptr->proc);
+ rb_gc_mark_and_move(&ptr->size);
}
#define producer_free RUBY_TYPED_DEFAULT_FREE
@@ -2919,12 +2921,13 @@ producer_allocate(VALUE klass)
obj = TypedData_Make_Struct(klass, struct producer, &producer_data_type, ptr);
ptr->init = Qundef;
ptr->proc = Qundef;
+ ptr->size = Qnil;
return obj;
}
static VALUE
-producer_init(VALUE obj, VALUE init, VALUE proc)
+producer_init(VALUE obj, VALUE init, VALUE proc, VALUE size)
{
struct producer *ptr;
@@ -2936,6 +2939,7 @@ producer_init(VALUE obj, VALUE init, VALUE proc)
RB_OBJ_WRITE(obj, &ptr->init, init);
RB_OBJ_WRITE(obj, &ptr->proc, proc);
+ RB_OBJ_WRITE(obj, &ptr->size, size);
return obj;
}
@@ -2986,12 +2990,18 @@ producer_each(VALUE obj)
static VALUE
producer_size(VALUE obj, VALUE args, VALUE eobj)
{
- return DBL2NUM(HUGE_VAL);
+ struct producer *ptr = producer_ptr(obj);
+ VALUE size = ptr->size;
+
+ if (NIL_P(size)) return Qnil;
+ if (RB_INTEGER_TYPE_P(size) || RB_FLOAT_TYPE_P(size)) return size;
+
+ return rb_funcall(size, id_call, 0);
}
/*
* call-seq:
- * Enumerator.produce(initial = nil) { |prev| block } -> enumerator
+ * Enumerator.produce(initial = nil, size: nil) { |prev| block } -> enumerator
*
* Creates an infinite enumerator from any block, just called over and
* over. The result of the previous iteration is passed to the next one.
@@ -3023,19 +3033,43 @@ producer_size(VALUE obj, VALUE args, VALUE eobj)
* PATTERN = %r{\d+|[-/+*]}
* Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first
* # => ["7", "+", "38", "/", "6"]
+ *
+ * The optional +size+ keyword argument specifies the size of the enumerator,
+ * which can be retrieved by Enumerator#size. It can be an integer,
+ * +Float::INFINITY+, a callable object (such as a lambda), or +nil+ to
+ * indicate unknown size. When not specified, the size is unknown (+nil+).
+ *
+ * # Infinite enumerator
+ * enum = Enumerator.produce(1, size: Float::INFINITY, &:succ)
+ * enum.size # => Float::INFINITY
+ *
+ * # Finite enumerator with known/computable size
+ * abs_dir = File.expand_path("./baz") # => "/foo/bar/baz"
+ * traverser = Enumerator.produce(abs_dir, size: -> { abs_dir.count("/") + 1 }) {
+ * raise StopIteration if it == "/"
+ * File.dirname(it)
+ * }
+ * traverser.size # => 4
*/
static VALUE
enumerator_s_produce(int argc, VALUE *argv, VALUE klass)
{
- VALUE init, producer;
+ VALUE init, producer, opts, size;
+ ID keyword_ids[1];
if (!rb_block_given_p()) rb_raise(rb_eArgError, "no block given");
- if (rb_scan_args(argc, argv, "01", &init) == 0) {
+ keyword_ids[0] = rb_intern("size");
+ rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS, argc, argv, "01:", &init, &opts);
+ rb_get_kwargs(opts, keyword_ids, 0, 1, &size);
+
+ size = UNDEF_P(size) ? Qnil : convert_to_feasible_size_value(size);
+
+ if (argc == 0 || (argc == 1 && !NIL_P(opts))) {
init = Qundef;
}
- producer = producer_init(producer_allocate(rb_cEnumProducer), init, rb_block_proc());
+ producer = producer_init(producer_allocate(rb_cEnumProducer), init, rb_block_proc(), size);
return rb_enumeratorize_with_size_kw(producer, sym_each, 0, 0, producer_size, RB_NO_KEYWORDS);
}
diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb
index ddba1c09ca..5fabea645d 100644
--- a/test/ruby/test_enumerator.rb
+++ b/test/ruby/test_enumerator.rb
@@ -886,12 +886,13 @@ class TestEnumerator < Test::Unit::TestCase
def test_produce
assert_raise(ArgumentError) { Enumerator.produce }
+ assert_raise(ArgumentError) { Enumerator.produce(a: 1, b: 1) {} }
# Without initial object
passed_args = []
enum = Enumerator.produce { |obj| passed_args << obj; (obj || 0).succ }
assert_instance_of(Enumerator, enum)
- assert_equal Float::INFINITY, enum.size
+ assert_nil enum.size
assert_equal [1, 2, 3], enum.take(3)
assert_equal [nil, 1, 2], passed_args
@@ -899,22 +900,14 @@ class TestEnumerator < Test::Unit::TestCase
passed_args = []
enum = Enumerator.produce(1) { |obj| passed_args << obj; obj.succ }
assert_instance_of(Enumerator, enum)
- assert_equal Float::INFINITY, enum.size
+ assert_nil enum.size
assert_equal [1, 2, 3], enum.take(3)
assert_equal [1, 2], passed_args
- # With initial keyword arguments
- passed_args = []
- enum = Enumerator.produce(a: 1, b: 1) { |obj| passed_args << obj; obj.shift if obj.respond_to?(:shift)}
- assert_instance_of(Enumerator, enum)
- assert_equal Float::INFINITY, enum.size
- assert_equal [{b: 1}, [1], :a, nil], enum.take(4)
- assert_equal [{b: 1}, [1], :a], passed_args
-
# Raising StopIteration
words = "The quick brown fox jumps over the lazy dog.".scan(/\w+/)
enum = Enumerator.produce { words.shift or raise StopIteration }
- assert_equal Float::INFINITY, enum.size
+ assert_nil enum.size
assert_instance_of(Enumerator, enum)
assert_equal %w[The quick brown fox jumps over the lazy dog], enum.to_a
@@ -924,7 +917,7 @@ class TestEnumerator < Test::Unit::TestCase
obj.respond_to?(:first) or raise StopIteration
obj.first
}
- assert_equal Float::INFINITY, enum.size
+ assert_nil enum.size
assert_instance_of(Enumerator, enum)
assert_nothing_raised {
assert_equal [
@@ -935,6 +928,25 @@ class TestEnumerator < Test::Unit::TestCase
"abc",
], enum.to_a
}
+
+ # With size keyword argument
+ enum = Enumerator.produce(1, size: 10) { |obj| obj.succ }
+ assert_equal 10, enum.size
+ assert_equal [1, 2, 3], enum.take(3)
+
+ enum = Enumerator.produce(1, size: -> { 5 }) { |obj| obj.succ }
+ assert_equal 5, enum.size
+
+ enum = Enumerator.produce(1, size: nil) { |obj| obj.succ }
+ assert_equal nil, enum.size
+
+ enum = Enumerator.produce(1, size: Float::INFINITY) { |obj| obj.succ }
+ assert_equal Float::INFINITY, enum.size
+
+ # Without initial value but with size
+ enum = Enumerator.produce(size: 3) { |obj| (obj || 0).succ }
+ assert_equal 3, enum.size
+ assert_equal [1, 2, 3], enum.take(3)
end
def test_chain_each_lambda