From 56e6f3e4f9ab2a9d10091871e6e0107f673c0d9f Mon Sep 17 00:00:00 2001
From: Greg Sterndale <gsterndale@gmail.com>
Date: Wed, 15 Feb 2012 14:34:50 -0500
Subject: [PATCH] [#5970] Add Enumerable#join with same semantics as
 Array#join

---
 enum.c                 |   80 ++++++++++++++++++++++++++++++++++++++++++++++++
 test/ruby/test_enum.rb |   79 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 159 insertions(+)

diff --git a/enum.c b/enum.c
index 3a85c50..1b7665e 100644
--- a/enum.c
+++ b/enum.c
@@ -11,6 +11,7 @@
 
 #include "ruby/ruby.h"
 #include "ruby/util.h"
+#include "ruby/encoding.h"
 #include "node.h"
 #include "id.h"
 #include "internal.h"
@@ -2215,6 +2216,84 @@ enum_drop_while(VALUE obj)
 }
 
 static VALUE
+enum_join_i(VALUE obj, VALUE *params, int argc, VALUE *argv)
+{
+    VALUE *param = (VALUE *)params;
+    VALUE sep = param[0];
+    VALUE result = param[1];
+    int *first = (int *)param[2];
+    ID join = rb_intern("join");
+    VALUE tmp;
+
+    switch (TYPE(obj)) {
+	case T_STRING:
+	str_join:
+	  if (!NIL_P(sep) && !*first)
+		rb_str_buf_append(result, sep);
+	  if (*first)
+		rb_enc_copy(result, obj);
+	  rb_str_buf_append(result, obj);
+	  *first = FALSE;
+	  break;
+	default:
+	  if (rb_respond_to(obj, join)) {
+		obj = rb_funcall(obj, join, 1, sep);
+	  }
+	  else {
+		obj = rb_obj_as_string(obj);
+	  }
+	  rb_enc_copy(obj, result);
+	  goto str_join;
+    }
+    return result;
+}
+
+/*
+ *  call-seq:
+ *     enum.join(sep=$,)    -> str
+ *
+ *  Returns a string created by converting each element of the 
+ *  <i>enum</i> to a string, separated by <i>sep</i>.
+ *
+ *     (1..3).join        #=> "123"
+ *     (1..3).join("-")   #=> "1-2-3"
+ */
+
+static VALUE
+enum_join(int argc, VALUE *argv, VALUE obj)
+{
+    VALUE sep;
+    VALUE result;
+    VALUE args[3];
+    int first = TRUE;
+    int taint = FALSE;
+    int untrust = FALSE;
+
+    if (!enum_any(obj))
+	return rb_usascii_str_new(0, 0);
+
+    rb_scan_args(argc, argv, "01", &sep);
+    if (NIL_P(sep)) sep = rb_output_fs;
+    if (!NIL_P(sep)) StringValue(sep);
+
+    if (OBJ_TAINTED(obj) || OBJ_TAINTED(sep)) taint = TRUE;
+    if (OBJ_UNTRUSTED(obj) || OBJ_UNTRUSTED(sep)) untrust = TRUE;
+
+    result = rb_usascii_str_new(0, 0);
+
+    if (taint) OBJ_TAINT(result);
+    if (untrust) OBJ_UNTRUST(result);
+
+    args[0] = sep;
+    args[1] = result;
+    args[2] = (VALUE)&first;
+
+    rb_block_call(obj, id_each, 0, 0, enum_join_i, (VALUE)args);
+
+    return result;
+}
+
+static VALUE
 cycle_i(VALUE i, VALUE ary, int argc, VALUE *argv)
 {
     ENUM_WANT_SVALUE();
@@ -2762,6 +2841,7 @@ enum_slice_before(int argc, VALUE *argv, VALUE enumerable)
     rb_define_method(rb_mEnumerable, "cycle", enum_cycle, -1);
     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, "join", enum_join, -1);
 
     id_next = rb_intern("next");
 }
diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb
index 1d509b5..f97939b 100644
--- a/test/ruby/test_enum.rb
+++ b/test/ruby/test_enum.rb
@@ -103,6 +103,85 @@ def test_to_a
     assert_equal([1, 2, 3, 1, 2], @obj.to_a)
   end
 
+  def test_join
+    assert_equal('123', (1..3).join )
+
+    @speach = Object.new
+    class << @speach
+      attr_writer :statements
+      include Enumerable
+      def each
+        @statements.each{|s| yield s }
+      end
+    end
+    @speach.statements = [1,2,3,1,2]
+
+    assert_equal('12312', @speach.join )
+    assert_equal('1;2;3;1;2', @speach.join(';') )
+
+    @speach.statements = [ [9,8,7], 'ohai', @obj ]
+    assert_equal('9;8;7;ohai;1;2;3;1;2', @speach.join(';') )
+
+    @speach.statements = []
+    assert_equal('', @speach.join )
+
+    ["", ":", nil].each do |output_fs|
+      $, = output_fs
+      assert_equal("", @speach.join)
+      assert_equal("", @speach.join(','))
+      assert_equal(Encoding::US_ASCII, @speach.join.encoding)
+    end
+
+    @speach.statements = [1,2,3,1,2]
+    ["", ":", nil].each do |output_fs|
+      assert_equal("1,2,3,1,2", @speach.join(','))
+    end
+
+    $, = ":"
+    assert_equal("1:2:3:1:2", @speach.join)
+
+    $, = nil
+    assert_equal("12312", @speach.join)
+
+    @speach.taint
+    @speach.untrust
+    s = @speach.join
+    assert_equal(true, s.tainted?)
+    assert_equal(true, s.untrusted?)
+
+    e = ''.force_encoding('EUC-JP')
+    u = ''.force_encoding('UTF-8')
+    assert_equal(Encoding::US_ASCII, @speach.join.encoding)
+    @speach.statements = [1, [u]]
+    assert_equal(Encoding::US_ASCII, @speach.join.encoding)
+    @speach.statements = [u, [e]]
+    assert_equal(Encoding::UTF_8, @speach.join.encoding)
+    @speach.statements = [u, [1]]
+    assert_equal(Encoding::UTF_8, @speach.join.encoding)
+    bug5379 = '[ruby-core:39776]'
+    @speach.statements = [[], u, nil]
+    assert_equal(Encoding::US_ASCII, @speach.join.encoding, bug5379)
+    @speach.statements = [[], "\u3042", nil]
+    assert_equal(Encoding::UTF_8, @speach.join.encoding, bug5379)
+
+    @nested = Object.new
+    class << @nested
+      include Enumerable
+      class Nested
+        include Enumerable
+        def each
+          yield Nested.new
+        end
+      end
+      def each
+        yield Nested.new
+      end
+    end
+    assert_raise(SystemStackError){ @nested.join }
+  ensure
+    $, = nil
+  end
+
   def test_inject
     assert_equal(12, @obj.inject {|z, x| z * x })
     assert_equal(48, @obj.inject {|z, x| z * 2 + x })
-- 
1.7.10

