summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md3
-rw-r--r--enum.c28
-rw-r--r--spec/ruby/core/enumerable/tally_spec.rb28
-rw-r--r--test/ruby/test_enum.rb11
4 files changed, 64 insertions, 6 deletions
diff --git a/NEWS.md b/NEWS.md
index f8d2179a88..cf79ab7ab7 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -23,6 +23,8 @@ Outstanding ones only.
* Enumerable#compact is added. [[Feature #17312]]
+ * Enumerable#tally now accepts an optional hash to count. [[Feature #17744]]
+
* Enumerator::Lazy
* Enumerator::Lazy#compact is added. [[Feature #17312]]
@@ -99,3 +101,4 @@ Excluding feature bug fixes.
[Feature #17411]: https://bugs.ruby-lang.org/issues/17411
[Bug #17423]: https://bugs.ruby-lang.org/issues/17423
[Feature #17479]: https://bugs.ruby-lang.org/issues/17479
+[Feature #17744]: https://bugs.ruby-lang.org/issues/17744
diff --git a/enum.c b/enum.c
index dab2469f78..c7828cdb01 100644
--- a/enum.c
+++ b/enum.c
@@ -688,14 +688,19 @@ enum_to_a(int argc, VALUE *argv, VALUE obj)
}
static VALUE
-enum_hashify(VALUE obj, int argc, const VALUE *argv, rb_block_call_func *iter)
+enum_hashify_into(VALUE obj, int argc, const VALUE *argv, rb_block_call_func *iter, VALUE hash)
{
- VALUE hash = rb_hash_new();
rb_block_call(obj, id_each, argc, argv, iter, hash);
return hash;
}
static VALUE
+enum_hashify(VALUE obj, int argc, const VALUE *argv, rb_block_call_func *iter)
+{
+ return enum_hashify_into(obj, argc, argv, iter, rb_hash_new());
+}
+
+static VALUE
enum_to_h_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash))
{
ENUM_WANT_SVALUE();
@@ -1020,6 +1025,7 @@ tally_up(st_data_t *group, st_data_t *value, st_data_t arg, int existing)
tally += INT2FIX(1) & ~FIXNUM_FLAG;
}
else {
+ Check_Type(tally, T_BIGNUM);
tally = rb_big_plus(tally, INT2FIX(1));
RB_OBJ_WRITTEN(hash, Qundef, tally);
}
@@ -1045,19 +1051,29 @@ tally_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash))
/*
* call-seq:
- * enum.tally -> a_hash
+ * enum.tally -> a_hash
+ * enum.tally(a_hash) -> a_hash
*
* Tallies the collection, i.e., counts the occurrences of each element.
* Returns a hash with the elements of the collection as keys and the
* corresponding counts as values.
*
* ["a", "b", "c", "b"].tally #=> {"a"=>1, "b"=>2, "c"=>1}
+ *
+ * If a hash is given, the number of occurrences is added to each value
+ * in the hash, and the hash is returned. The value corresponding to
+ * each element must be an integer.
*/
static VALUE
-enum_tally(VALUE obj)
+enum_tally(int argc, VALUE *argv, VALUE obj)
{
- return enum_hashify(obj, 0, 0, tally_i);
+ VALUE hash;
+ if (rb_check_arity(argc, 0, 1))
+ hash = rb_check_hash_type(argv[0]);
+ else
+ hash = rb_hash_new();
+ return enum_hashify_into(obj, 0, 0, tally_i, hash);
}
NORETURN(static VALUE first_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, params)));
@@ -4393,7 +4409,7 @@ Init_Enumerable(void)
rb_define_method(rb_mEnumerable, "reduce", enum_inject, -1);
rb_define_method(rb_mEnumerable, "partition", enum_partition, 0);
rb_define_method(rb_mEnumerable, "group_by", enum_group_by, 0);
- rb_define_method(rb_mEnumerable, "tally", enum_tally, 0);
+ rb_define_method(rb_mEnumerable, "tally", enum_tally, -1);
rb_define_method(rb_mEnumerable, "first", enum_first, -1);
rb_define_method(rb_mEnumerable, "all?", enum_all, -1);
rb_define_method(rb_mEnumerable, "any?", enum_any, -1);
diff --git a/spec/ruby/core/enumerable/tally_spec.rb b/spec/ruby/core/enumerable/tally_spec.rb
index 363b3def21..1367453f44 100644
--- a/spec/ruby/core/enumerable/tally_spec.rb
+++ b/spec/ruby/core/enumerable/tally_spec.rb
@@ -33,3 +33,31 @@ ruby_version_is "2.7" do
end
end
end
+
+ruby_version_is "3.1" do
+ describe "Enumerable#tally with a hash" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a hash with counts according to the value" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally({ 'foo' => 1 }).should == { 'foo' => 3, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "ignores the default value" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally(Hash.new(100)).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "ignores the default proc" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally(Hash.new {100}).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "needs the values counting each elements to be an integer" do
+ enum = EnumerableSpecs::Numerous.new('foo')
+ -> { enum.tally({ 'foo' => 'bar' }) }.should raise_error(TypeError)
+ end
+ end
+end
diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb
index 3b0e0f79b2..b6d96f1379 100644
--- a/test/ruby/test_enum.rb
+++ b/test/ruby/test_enum.rb
@@ -394,6 +394,17 @@ class TestEnumerable < Test::Unit::TestCase
def test_tally
h = {1 => 2, 2 => 2, 3 => 1}
assert_equal(h, @obj.tally)
+
+ h = {1 => 5, 2 => 2, 3 => 1, 4 => "x"}
+ assert_equal(h, @obj.tally({1 => 3, 4 => "x"}))
+
+ assert_raise(TypeError) do
+ @obj.tally({1 => ""})
+ end
+
+ h = {1 => 2, 2 => 2, 3 => 1}
+ assert_equal(h, @obj.tally(Hash.new(100)))
+ assert_equal(h, @obj.tally(Hash.new {100}))
end
def test_first