From a9770bac6375c9d4ff8ba8bb1842841aec7c59e3 Mon Sep 17 00:00:00 2001 From: marcandre Date: Sun, 10 Dec 2017 22:36:28 +0000 Subject: Add case equality arity to Enumerable#all?, any?, none? and one?, and specialized Array#any? and Hash#any? Based on patch by D.E. Akers [#11286] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@61098 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- NEWS | 4 ++ array.c | 12 +++-- enum.c | 59 ++++++++++++++++----- hash.c | 42 +++++++++++---- spec/ruby/core/enumerable/any_spec.rb | 96 ++++++++++++++++++++++++++++++++--- test/ruby/test_enum.rb | 18 +++++++ 6 files changed, 198 insertions(+), 33 deletions(-) diff --git a/NEWS b/NEWS index 20a35c93ad..96ea8f83a9 100644 --- a/NEWS +++ b/NEWS @@ -38,6 +38,10 @@ with all sufficient information, see the ChangeLog file or Redmine * Dir.children [Feature #11302] * Dir.each_child [Feature #11302] +* Enumerable + + * Enumerable#any?, all?, none? and one? now accept a pattern argument [Feature #11286] + * File * :newline option to File.open implies text mode now. [Bug #13350] diff --git a/array.c b/array.c index 425e28aa69..b533e81055 100644 --- a/array.c +++ b/array.c @@ -5771,13 +5771,19 @@ rb_ary_drop_while(VALUE ary) */ static VALUE -rb_ary_any_p(VALUE ary) +rb_ary_any_p(int argc, VALUE *argv, VALUE ary) { long i, len = RARRAY_LEN(ary); const VALUE *ptr = RARRAY_CONST_PTR(ary); + rb_check_arity(argc, 0, 1); if (!len) return Qfalse; - if (!rb_block_given_p()) { + if (argc) { + for (i = 0; i < RARRAY_LEN(ary); ++i) { + if (RTEST(rb_funcall(argv[0], idEqq, 1, RARRAY_AREF(ary, i)))) return Qtrue; + } + } + else if (!rb_block_given_p()) { for (i = 0; i < len; ++i) if (RTEST(ptr[i])) return Qtrue; } else { @@ -6329,7 +6335,7 @@ Init_Array(void) rb_define_method(rb_cArray, "drop_while", rb_ary_drop_while, 0); rb_define_method(rb_cArray, "bsearch", rb_ary_bsearch, 0); rb_define_method(rb_cArray, "bsearch_index", rb_ary_bsearch_index, 0); - rb_define_method(rb_cArray, "any?", rb_ary_any_p, 0); + rb_define_method(rb_cArray, "any?", rb_ary_any_p, -1); rb_define_method(rb_cArray, "dig", rb_ary_dig, -1); rb_define_method(rb_cArray, "sum", rb_ary_sum, -1); diff --git a/enum.c b/enum.c index 086e3aa13c..aaf4e33386 100644 --- a/enum.c +++ b/enum.c @@ -1150,7 +1150,9 @@ enum_sort_by(VALUE obj) return ary; } -#define ENUMFUNC(name) rb_block_given_p() ? name##_iter_i : name##_i +#define ENUMFUNC(name) argc ? name##_eqq : rb_block_given_p() ? name##_iter_i : name##_i + +#define MEMO_ENUM_NEW(v1) (rb_check_arity(argc, 0, 1), MEMO_NEW((v1), (argc ? *argv : 0), 0)) #define DEFINE_ENUMFUNCS(name) \ static VALUE enum_##name##_func(VALUE result, struct MEMO *memo); \ @@ -1168,6 +1170,13 @@ name##_iter_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \ } \ \ static VALUE \ +name##_eqq(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \ +{ \ + ENUM_WANT_SVALUE(); \ + return enum_##name##_func(rb_funcallv(MEMO_CAST(memo)->v2, id_eqq, 1, &i), MEMO_CAST(memo)); \ +} \ +\ +static VALUE \ enum_##name##_func(VALUE result, struct MEMO *memo) DEFINE_ENUMFUNCS(all) @@ -1182,6 +1191,7 @@ DEFINE_ENUMFUNCS(all) /* * call-seq: * enum.all? [{ |obj| block } ] -> true or false + * enum.all?(pattern) -> true or false * * Passes each element of the collection to the given block. The method * returns true if the block never returns @@ -1190,17 +1200,22 @@ DEFINE_ENUMFUNCS(all) * cause #all? to return +true+ when none of the collection members are * +false+ or +nil+. * + * If instead a pattern is supplied, the method returns whether + * pattern === element for every element of enum. + * * %w[ant bear cat].all? { |word| word.length >= 3 } #=> true * %w[ant bear cat].all? { |word| word.length >= 4 } #=> false + * %w[ant bear cat].all?(/t/) #=> false + * [1, 2i, 3.14].all?(Numeric) #=> true * [nil, true, 99].all? #=> false * [].all? #=> true * */ static VALUE -enum_all(VALUE obj) +enum_all(int argc, VALUE *argv, VALUE obj) { - struct MEMO *memo = MEMO_NEW(Qtrue, 0, 0); + struct MEMO *memo = MEMO_ENUM_NEW(Qtrue); rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo); return memo->v1; } @@ -1217,6 +1232,7 @@ DEFINE_ENUMFUNCS(any) /* * call-seq: * enum.any? [{ |obj| block }] -> true or false + * enum.any?(pattern) -> true or false * * Passes each element of the collection to the given block. The method * returns true if the block ever returns a value other @@ -1225,17 +1241,22 @@ DEFINE_ENUMFUNCS(any) * will cause #any? to return +true+ if at least one of the collection * members is not +false+ or +nil+. * + * If instead a pattern is supplied, the method returns whether + * pattern === element for any element of enum. + * * %w[ant bear cat].any? { |word| word.length >= 3 } #=> true * %w[ant bear cat].any? { |word| word.length >= 4 } #=> true + * %w[ant bear cat].any?(/d/) #=> false + * [nil, true, 99].any?(Integer) #=> true * [nil, true, 99].any? #=> true * [].any? #=> false * */ static VALUE -enum_any(VALUE obj) +enum_any(int argc, VALUE *argv, VALUE obj) { - struct MEMO *memo = MEMO_NEW(Qfalse, 0, 0); + struct MEMO *memo = MEMO_ENUM_NEW(Qfalse); rb_block_call(obj, id_each, 0, 0, ENUMFUNC(any), (VALUE)memo); return memo->v1; } @@ -1476,6 +1497,7 @@ rb_nmin_run(VALUE obj, VALUE num, int by, int rev, int ary) /* * call-seq: * enum.one? [{ |obj| block }] -> true or false + * enum.one?(pattern) -> true or false * * Passes each element of the collection to the given block. The method * returns true if the block returns true @@ -1483,17 +1505,22 @@ rb_nmin_run(VALUE obj, VALUE num, int by, int rev, int ary) * true only if exactly one of the collection members is * true. * + * If instead a pattern is supplied, the method returns whether + * pattern === element for exactly one element of enum. + * * %w{ant bear cat}.one? { |word| word.length == 4 } #=> true * %w{ant bear cat}.one? { |word| word.length > 4 } #=> false * %w{ant bear cat}.one? { |word| word.length < 4 } #=> false + * %w{ant bear cat}.one?(/t/) #=> false * [ nil, true, 99 ].one? #=> false * [ nil, true, false ].one? #=> true + * [ nil, true, 99 ].one?(Integer) #=> true * */ static VALUE -enum_one(VALUE obj) +enum_one(int argc, VALUE *argv, VALUE obj) { - struct MEMO *memo = MEMO_NEW(Qundef, 0, 0); + struct MEMO *memo = MEMO_ENUM_NEW(Qundef); VALUE result; rb_block_call(obj, id_each, 0, 0, ENUMFUNC(one), (VALUE)memo); @@ -1514,23 +1541,29 @@ DEFINE_ENUMFUNCS(none) /* * call-seq: * enum.none? [{ |obj| block }] -> true or false + * enum.none?(pattern) -> true or false * * Passes each element of the collection to the given block. The method * returns true if the block never returns true * for all elements. If the block is not given, none? will return * true only if none of the collection members is true. * + * If instead a pattern is supplied, the method returns whether + * pattern === element for none of the elements of enum. + * * %w{ant bear cat}.none? { |word| word.length == 5 } #=> true * %w{ant bear cat}.none? { |word| word.length >= 4 } #=> false + * %w{ant bear cat}.none?(/d/) #=> true + * [1, 3.14, 42].none?(Float) #=> false * [].none? #=> true * [nil].none? #=> true * [nil, false].none? #=> true * [nil, false, true].none? #=> false */ static VALUE -enum_none(VALUE obj) +enum_none(int argc, VALUE *argv, VALUE obj) { - struct MEMO *memo = MEMO_NEW(Qtrue, 0, 0); + struct MEMO *memo = MEMO_ENUM_NEW(Qtrue); rb_block_call(obj, id_each, 0, 0, ENUMFUNC(none), (VALUE)memo); return memo->v1; } @@ -3969,10 +4002,10 @@ Init_Enumerable(void) 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, "first", enum_first, -1); - rb_define_method(rb_mEnumerable, "all?", enum_all, 0); - rb_define_method(rb_mEnumerable, "any?", enum_any, 0); - rb_define_method(rb_mEnumerable, "one?", enum_one, 0); - rb_define_method(rb_mEnumerable, "none?", enum_none, 0); + rb_define_method(rb_mEnumerable, "all?", enum_all, -1); + rb_define_method(rb_mEnumerable, "any?", enum_any, -1); + rb_define_method(rb_mEnumerable, "one?", enum_one, -1); + rb_define_method(rb_mEnumerable, "none?", enum_none, -1); rb_define_method(rb_mEnumerable, "min", enum_min, -1); rb_define_method(rb_mEnumerable, "max", enum_max, -1); rb_define_method(rb_mEnumerable, "minmax", enum_minmax, 0); diff --git a/hash.c b/hash.c index cd1cc1437f..627f207d8e 100644 --- a/hash.c +++ b/hash.c @@ -2983,6 +2983,17 @@ any_p_i_fast(VALUE key, VALUE value, VALUE arg) return ST_CONTINUE; } +static int +any_p_i_pattern(VALUE key, VALUE value, VALUE arg) +{ + VALUE ret = rb_funcall(((VALUE *)arg)[1], idEqq, 1, rb_assoc_new(key, value)); + if (RTEST(ret)) { + *(VALUE *)arg = Qtrue; + return ST_STOP; + } + return ST_CONTINUE; +} + /* * call-seq: * hsh.any? [{ |(key, value)| block }] -> true or false @@ -2991,20 +3002,29 @@ any_p_i_fast(VALUE key, VALUE value, VALUE arg) */ static VALUE -rb_hash_any_p(VALUE hash) +rb_hash_any_p(int argc, VALUE *argv, VALUE hash) { - VALUE ret = Qfalse; + VALUE args[2]; + args[0] = Qfalse; + rb_check_arity(argc, 0, 1); if (RHASH_EMPTY_P(hash)) return Qfalse; - if (!rb_block_given_p()) { - /* yields pairs, never false */ - return Qtrue; + if (argc) { + args[1] = argv[0]; + + rb_hash_foreach(hash, any_p_i_pattern, (VALUE)args); } - if (rb_block_arity() > 1) - rb_hash_foreach(hash, any_p_i_fast, (VALUE)&ret); - else - rb_hash_foreach(hash, any_p_i, (VALUE)&ret); - return ret; + else { + if (!rb_block_given_p()) { + /* yields pairs, never false */ + return Qtrue; + } + if (rb_block_arity() > 1) + rb_hash_foreach(hash, any_p_i_fast, (VALUE)args); + else + rb_hash_foreach(hash, any_p_i, (VALUE)args); + } + return args[0]; } /* @@ -4663,7 +4683,7 @@ Init_Hash(void) rb_define_method(rb_cHash, "compare_by_identity", rb_hash_compare_by_id, 0); rb_define_method(rb_cHash, "compare_by_identity?", rb_hash_compare_by_id_p, 0); - rb_define_method(rb_cHash, "any?", rb_hash_any_p, 0); + rb_define_method(rb_cHash, "any?", rb_hash_any_p, -1); rb_define_method(rb_cHash, "dig", rb_hash_dig, -1); rb_define_method(rb_cHash, "<=", rb_hash_le, 1); diff --git a/spec/ruby/core/enumerable/any_spec.rb b/spec/ruby/core/enumerable/any_spec.rb index 4a7511f649..edf7e36519 100644 --- a/spec/ruby/core/enumerable/any_spec.rb +++ b/spec/ruby/core/enumerable/any_spec.rb @@ -20,12 +20,19 @@ describe "Enumerable#any?" do {}.any? { nil }.should == false end - it "raises an ArgumentError when any arguments provided" do - lambda { @enum.any?(Proc.new {}) }.should raise_error(ArgumentError) - lambda { @enum.any?(nil) }.should raise_error(ArgumentError) - lambda { @empty.any?(1) }.should raise_error(ArgumentError) - lambda { @enum1.any?(1) {} }.should raise_error(ArgumentError) - lambda { @enum2.any?(1, 2, 3) {} }.should raise_error(ArgumentError) + it "raises an ArgumentError when more than 1 argument is provided" do + lambda { @enum.any?(1, 2, 3) }.should raise_error(ArgumentError) + lambda { [].any?(1, 2, 3) }.should raise_error(ArgumentError) + lambda { {}.any?(1, 2, 3) }.should raise_error(ArgumentError) + end + + ruby_version_is ""..."2.5" do + it "raises an ArgumentError when any arguments provided" do + lambda { @enum.any?(Proc.new {}) }.should raise_error(ArgumentError) + lambda { @enum.any?(nil) }.should raise_error(ArgumentError) + lambda { @empty.any?(1) }.should raise_error(ArgumentError) + lambda { @enum1.any?(1) {} }.should raise_error(ArgumentError) + end end it "does not hide exceptions out of #each" do @@ -138,4 +145,81 @@ describe "Enumerable#any?" do end end + + ruby_version_is "2.5" do + describe 'when given a pattern argument' do + class EnumerableSpecs::Pattern + attr_reader :yielded + def initialize(&block) + @block = block + @yielded = [] + end + def ===(*args) + @yielded << args + @block.call(*args) + end + end + + it "calls `===` on the pattern the return value " do + pattern = EnumerableSpecs::Pattern.new { |x| x == 2 } + @enum1.any?(pattern).should == true + pattern.yielded.should == [[0], [1], [2]] + end + + it "ignores block" do + @enum2.any?(NilClass) { raise }.should == true + [1, 2, nil].any?(NilClass) { raise }.should == true + {a: 1}.any?(Array) { raise }.should == true + end + + it "always returns false on empty enumeration" do + @empty.any?(Integer).should == false + [].any?(Integer).should == false + {}.any?(NilClass).should == false + end + + it "does not hide exceptions out of #each" do + lambda { + EnumerableSpecs::ThrowingEach.new.any?(Integer) + }.should raise_error(RuntimeError) + end + + it "returns true if the pattern ever returns a truthy value" do + @enum2.any?(NilClass).should == true + pattern = EnumerableSpecs::Pattern.new { |x| 42 } + @enum.any?(pattern).should == true + + [1, 42, 3].any?(pattern).should == true + + pattern = EnumerableSpecs::Pattern.new { |x| x == [:b, 2] } + {a: 1, b: 2}.any?(pattern).should == true + end + + it "any? should return false if the block never returns other than false or nil" do + pattern = EnumerableSpecs::Pattern.new { |x| nil } + @enum1.any?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + + [1, 2, 3].any?(pattern).should == false + {a: 1}.any?(pattern).should == false + end + + it "does not hide exceptions out of the block" do + pattern = EnumerableSpecs::Pattern.new { raise "from pattern" } + lambda { + @enum.any?(pattern) + }.should raise_error(RuntimeError) + end + + it "calls the pattern with gathered array when yielded with multiple arguments" do + pattern = EnumerableSpecs::Pattern.new { false } + EnumerableSpecs::YieldsMixed2.new.any?(pattern).should == false + pattern.yielded.should == EnumerableSpecs::YieldsMixed2.gathered_yields.map { |x| [x] } + + pattern = EnumerableSpecs::Pattern.new { false } + {a: 1, b: 2}.any?(pattern).should == false + pattern.yielded.should == [[[:a, 1]], [[:b, 2]]] + end + end + end end diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb index ae119c73bf..12fc2ee66a 100644 --- a/test/ruby/test_enum.rb +++ b/test/ruby/test_enum.rb @@ -310,6 +310,8 @@ class TestEnumerable < Test::Unit::TestCase assert_equal(false, [true, true, false].all?) assert_equal(true, [].all?) assert_equal(true, @empty.all?) + assert_equal(true, @obj.all?(Fixnum)) + assert_equal(false, @obj.all?(1..2)) end def test_any @@ -319,27 +321,43 @@ class TestEnumerable < Test::Unit::TestCase assert_equal(false, [false, false, false].any?) assert_equal(false, [].any?) assert_equal(false, @empty.any?) + assert_equal(true, @obj.any?(1..2)) + assert_equal(false, @obj.any?(Float)) + assert_equal(false, [1, 42].any?(Float)) + assert_equal(true, [1, 4.2].any?(Float)) + assert_equal(false, {a: 1, b: 2}.any?(->(kv) { kv == [:foo, 42] })) + assert_equal(true, {a: 1, b: 2}.any?(->(kv) { kv == [:b, 2] })) end def test_one assert(@obj.one? {|x| x == 3 }) assert(!(@obj.one? {|x| x == 1 })) assert(!(@obj.one? {|x| x == 4 })) + assert(@obj.one?(3..4)) + assert(!(@obj.one?(1..2))) + assert(!(@obj.one?(4..5))) assert(%w{ant bear cat}.one? {|word| word.length == 4}) assert(!(%w{ant bear cat}.one? {|word| word.length > 4})) assert(!(%w{ant bear cat}.one? {|word| word.length < 4})) + assert(%w{ant bear cat}.one?(/b/)) + assert(!(%w{ant bear cat}.one?(/t/))) assert(!([ nil, true, 99 ].one?)) assert([ nil, true, false ].one?) assert(![].one?) assert(!@empty.one?) + assert([ nil, true, 99 ].one?(Integer)) end def test_none assert(@obj.none? {|x| x == 4 }) assert(!(@obj.none? {|x| x == 1 })) assert(!(@obj.none? {|x| x == 3 })) + assert(@obj.none?(4..5)) + assert(!(@obj.none?(1..3))) assert(%w{ant bear cat}.none? {|word| word.length == 5}) assert(!(%w{ant bear cat}.none? {|word| word.length >= 4})) + assert(%w{ant bear cat}.none?(/d/)) + assert(!(%w{ant bear cat}.none?(/b/))) assert([].none?) assert([nil].none?) assert([nil,false].none?) -- cgit v1.2.3