diff --git a/ChangeLog b/ChangeLog index fc196cd8ca..4802b696f1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +Sat Sep 20 15:39:11 2014 Tanaka Akira + + * enum.c (enum_slice_when): New method: Enumerable#slice_when. + (slicewhen_i): New function. + (slicewhen_ii): New function. + + * enumerator.c (InitVM_Enumerator): New method: + Enumerator::Lazy#slice_when. + + [ruby-core:62499] [Feature #9826] + Sat Sep 20 11:55:19 2014 SHIBATA Hiroshi * .travis.yml: added new configurations for osx on travis ci. diff --git a/NEWS b/NEWS index f8c08c4ecb..aadfd47388 100644 --- a/NEWS +++ b/NEWS @@ -33,6 +33,7 @@ with all sufficient information, see the ChangeLog file. * Enumerable * New methods: * Enumerable#slice_after + * Enumerable#slice_when * Extended methods: * min, min_by, max and max_by supports optional argument to return multiple elements. diff --git a/enum.c b/enum.c index 7920b93c55..7657e0437a 100644 --- a/enum.c +++ b/enum.c @@ -3206,6 +3206,138 @@ enum_slice_after(int argc, VALUE *argv, VALUE enumerable) return enumerator; } +struct slicewhen_arg { + VALUE pred; + VALUE prev_elt; + VALUE prev_elts; + VALUE yielder; +}; + +static VALUE +slicewhen_ii(RB_BLOCK_CALL_FUNC_ARGLIST(i, _memo)) +{ +#define UPDATE_MEMO ((void)(memo = MEMO_FOR(struct slicewhen_arg, _memo))) + struct slicewhen_arg *memo; + int split_p; + UPDATE_MEMO; + + ENUM_WANT_SVALUE(); + + if (memo->prev_elt == Qundef) { + /* The first element */ + memo->prev_elt = i; + memo->prev_elts = rb_ary_new3(1, i); + } + else { + split_p = RTEST(rb_funcall(memo->pred, id_call, 2, memo->prev_elt, i)); + UPDATE_MEMO; + + if (split_p) { + rb_funcall(memo->yielder, id_lshift, 1, memo->prev_elts); + UPDATE_MEMO; + memo->prev_elts = rb_ary_new3(1, i); + } + else { + rb_ary_push(memo->prev_elts, i); + } + + memo->prev_elt = i; + } + + return Qnil; +#undef UPDATE_MEMO +} + +static VALUE +slicewhen_i(RB_BLOCK_CALL_FUNC_ARGLIST(yielder, enumerator)) +{ + VALUE enumerable; + VALUE arg; + struct slicewhen_arg *memo = NEW_MEMO_FOR(struct slicewhen_arg, arg); + + enumerable = rb_ivar_get(enumerator, rb_intern("slicewhen_enum")); + memo->pred = rb_attr_get(enumerator, rb_intern("slicewhen_pred")); + memo->prev_elt = Qundef; + memo->prev_elts = Qnil; + memo->yielder = yielder; + + rb_block_call(enumerable, id_each, 0, 0, slicewhen_ii, arg); + memo = MEMO_FOR(struct slicewhen_arg, arg); + if (!NIL_P(memo->prev_elts)) + rb_funcall(memo->yielder, id_lshift, 1, memo->prev_elts); + return Qnil; +} + +/* + * call-seq: + * enum.slice_when {|elt_before, elt_after| bool } -> an_enumerator + * + * Creates an enumerator for each chunked elements. + * The beginnings of chunks are defined by the block. + * + * This method split each chunk using adjacent elements, + * _elt_before_ and _elt_after_, + * in the receiver enumerator. + * This method split chunks between _elt_before_ and _elt_after_ where + * the block returns true. + * + * The block is called the length of the receiver enumerator minus one. + * + * The result enumerator yields the chunked elements as an array. + * So +each+ method can be called as follows: + * + * enum.slice_when { |elt_before, elt_after| bool }.each { |ary| ... } + * + * Other methods of the Enumerator class and Enumerable module, + * such as +map+, etc., are also usable. + * + * For example, one-by-one increasing subsequence can be chunked as follows: + * + * a = [1,2,4,9,10,11,12,15,16,19,20,21] + * b = a.slice_when {|i, j| i+1 != j } + * p b.to_a #=> [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]] + * c = b.map {|a| a.length < 3 ? a : "#{a.first}-#{a.last}" } + * p c #=> [[1, 2], [4], "9-12", [15, 16], "19-21"] + * d = c.join(",") + * p d #=> "1,2,4,9-12,15,16,19-21" + * + * Increasing (non-decreasing) subsequence can be chunked as follows: + * + * a = [0, 9, 2, 2, 3, 2, 7, 5, 9, 5] + * p a.slice_when {|i, j| i > j }.to_a + * #=> [[0, 9], [2, 2, 3], [2, 7], [5, 9], [5]] + * + * Adjacent evens and odds can be chunked as follows: + * (Enumerable#chunk is another way to do it.) + * + * a = [7, 5, 9, 2, 0, 7, 9, 4, 2, 0] + * p a.slice_when {|i, j| i.even? != j.even? }.to_a + * #=> [[7, 5, 9], [2, 0], [7, 9], [4, 2, 0]] + * + * Paragraphs (non-empty lines with trailing empty lines) can be chunked as follows: + * (See Enumerable#chunk to ignore empty lines.) + * + * lines = ["foo\n", "bar\n", "\n", "baz\n", "qux\n"] + * p lines.slice_when {|l1, l2| /\A\s*\z/ =~ l1 && /\S/ =~ l2 }.to_a + * #=> [["foo\n", "bar\n", "\n"], ["baz\n", "qux\n"]] + * + */ +static VALUE +enum_slice_when(VALUE enumerable) +{ + VALUE enumerator; + VALUE pred; + + pred = rb_block_proc(); + + enumerator = rb_obj_alloc(rb_cEnumerator); + rb_ivar_set(enumerator, rb_intern("slicewhen_enum"), enumerable); + rb_ivar_set(enumerator, rb_intern("slicewhen_pred"), pred); + + rb_block_call(enumerator, idInitialize, 0, 0, slicewhen_i, enumerator); + return enumerator; +} + /* * The Enumerable mixin provides collection classes with * several traversal and searching methods, and with the ability to @@ -3275,6 +3407,7 @@ Init_Enumerable(void) rb_define_method(rb_mEnumerable, "chunk", enum_chunk, -1); rb_define_method(rb_mEnumerable, "slice_before", enum_slice_before, -1); rb_define_method(rb_mEnumerable, "slice_after", enum_slice_after, -1); + rb_define_method(rb_mEnumerable, "slice_when", enum_slice_when, 0); id_next = rb_intern("next"); id_call = rb_intern("call"); diff --git a/enumerator.c b/enumerator.c index eaa3d73464..75de8eb7e8 100644 --- a/enumerator.c +++ b/enumerator.c @@ -2040,6 +2040,7 @@ InitVM_Enumerator(void) rb_define_method(rb_cLazy, "chunk", lazy_super, -1); rb_define_method(rb_cLazy, "slice_before", lazy_super, -1); rb_define_method(rb_cLazy, "slice_after", lazy_super, -1); + rb_define_method(rb_cLazy, "slice_when", lazy_super, -1); rb_define_alias(rb_cLazy, "force", "to_a"); diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb index d6795e49dd..edd389b071 100644 --- a/test/ruby/test_enum.rb +++ b/test/ruby/test_enum.rb @@ -574,6 +574,61 @@ class TestEnumerable < Test::Unit::TestCase assert_equal([["foo", ""], ["bar"]], e.to_a) end + def test_slice_when_0 + e = [].slice_when {|a, b| flunk "should not be called" } + assert_equal([], e.to_a) + end + + def test_slice_when_1 + e = [1].slice_when {|a, b| flunk "should not be called" } + assert_equal([[1]], e.to_a) + end + + def test_slice_when_2 + e = [1,2].slice_when {|a,b| + assert_equal(1, a) + assert_equal(2, b) + true + } + assert_equal([[1], [2]], e.to_a) + + e = [1,2].slice_when {|a,b| + assert_equal(1, a) + assert_equal(2, b) + false + } + assert_equal([[1, 2]], e.to_a) + end + + def test_slice_when_3 + block_invocations = [ + lambda {|a, b| + assert_equal(1, a) + assert_equal(2, b) + true + }, + lambda {|a, b| + assert_equal(2, a) + assert_equal(3, b) + false + } + ] + e = [1,2,3].slice_when {|a,b| + block_invocations.shift.call(a, b) + } + assert_equal([[1], [2, 3]], e.to_a) + assert_equal([], block_invocations) + end + + def test_slice_when_noblock + assert_raise(ArgumentError) { [].slice_when } + end + + def test_slice_when_contiguously_increasing_integers + e = [1,4,9,10,11,12,15,16,19,20,21].slice_when {|i, j| i+1 != j } + assert_equal([[1], [4], [9,10,11,12], [15,16], [19,20,21]], e.to_a) + end + def test_detect @obj = ('a'..'z') assert_equal('c', @obj.detect {|x| x == 'c' }) diff --git a/test/ruby/test_lazy_enumerator.rb b/test/ruby/test_lazy_enumerator.rb index 549d0f104e..7bf9903ddc 100644 --- a/test/ruby/test_lazy_enumerator.rb +++ b/test/ruby/test_lazy_enumerator.rb @@ -481,6 +481,7 @@ EOS assert_equal Enumerator::Lazy, [].lazy.send(method, *arg).class, bug7507 end assert_equal Enumerator::Lazy, [].lazy.chunk{}.class, bug7507 + assert_equal Enumerator::Lazy, [].lazy.slice_when{}.class, bug7507 end def test_no_warnings