Mini-RCR: Extra Argument for Array#join

I've been looking at the to_sentence() method in Rails. It's basically a join(), but you can give a different final separator. I personally use this kind of functionality often enough to believe it would make a good addition to the core language.

Even better, I think we can make join() smart enough to eliminate any need for to_sentence() by adding a second argument. Here's a sample implementation:

#!/usr/bin/env ruby -w

require "enumerator"

class Array
   alias_method :old_join, :join
   def join(sep = $, last_sep = sep)
     return "" if empty?
     enum_with_index.inject("") do |str, (e, i)|
       "#{str}#{i == size - 1 ? last_sep : sep}#{e}"
     end[sep.to_s.length…-1]
   end
end

if __FILE__ == $PROGRAM_NAME
   require "test/unit"

   class TestJoin < Test::Unit::TestCase
     def test_old_matches_new
       assert_equal([].old_join, [].join)
       assert_equal([1].old_join, [1].join)
       assert_equal([1, 2].old_join, [1, 2].join)
       assert_equal((1..5).to_a.old_join, (1..5).to_a.join)

       assert_equal([].old_join("|"), [].join("|"))
       assert_equal([1].old_join("|"), [1].join("|"))
       assert_equal([1, 2].old_join("|"), [1, 2].join("|"))
       assert_equal((1..5).to_a.old_join("|"), (1..5).to_a.join("|"))
     end

     def test_new_last_arg_behavior
       assert_equal("1, 2, 3, 4, and 5", (1..5).to_a.join(", ", ", and "))
       assert_equal("1, 2, 3, 4 and 5", (1..5).to_a.join(", ", " and "))
       assert_equal("1, 2, 3, 4 & 5", (1..5).to_a.join(", ", " & "))

       assert_equal([1, 2].join(","), [1, 2].join("ignored", ","))
     end
   end
end

__END__

Does anyone else like this?

James Edward Gray II

It's neat. I think it's better than to_sentence.

But maybe not something necessary for core.

I tend to think of join() to be a delimiter-generator, and not an
english language generator.

But that's just my two cents. Wouldn't be opposed to an adjustment,
but personally have never needed it for my day to day Ruby usage.

···

On 12/31/06, James Edward Gray II <james@grayproductions.net> wrote:

Does anyone else like this?

James Edward Gray II wrote:

I've been looking at the to_sentence() method in Rails. It's
basically a join(), but you can give a different final separator. I
personally use this kind of functionality often enough to believe it
would make a good addition to the core language.

What makes the final separator so special that it deserves inclusion,
while making no allowance for a pre-separator? Just the English
language?

(At the same time as I ask this, I'm wondering if asking for
completeness for the sake of completeness, without a corresponding use
case, is the sort of foolish consistency that is the hobgoblin of
little minds.)

problems:

1) calculating a static value inside a loop. (only costs about a hundredth of a second for 10k)
2) iterating when 1/2 of your values are completely static.
3) being clever by using enumerator.

class Array
   alias_method :old_join, :join
   def join(sep = $, last_sep = sep)
     return "" if empty?

     seperators = Array.new(size-1, sep)
     seperators[-1] = last_sep unless seperators.empty?
     self.zip(seperators).old_join
   end
end

# of iterations = 1000000
                           user system total real
null_time 0.140000 0.000000 0.140000 ( 0.140482)
jeg 29.690000 0.080000 29.770000 ( 30.144816)
ryan 19.780000 0.050000 19.830000 ( 19.998077)

···

On Dec 31, 2006, at 12:05 PM, James Edward Gray II wrote:

I've been looking at the to_sentence() method in Rails. It's basically a join(), but you can give a different final separator. I personally use this kind of functionality often enough to believe it would make a good addition to the core language.

Even better, I think we can make join() smart enough to eliminate any need for to_sentence() by adding a second argument. Here's a sample implementation:

#!/usr/bin/env ruby -w

require "enumerator"

class Array
  alias_method :old_join, :join
  def join(sep = $, last_sep = sep)
    return "" if empty?
    enum_with_index.inject("") do |str, (e, i)|
      "#{str}#{i == size - 1 ? last_sep : sep}#{e}"
    end[sep.to_s.length..-1]
  end
end

James Gray wrote:

I've been looking at the to_sentence() method in Rails. It's
basically a join(), but you can give a different final separator. I
personally use this kind of functionality often enough to believe it
would make a good addition to the core language.

-1
join is simple to understand and use. It's used in lots of things
besides English generation. Adding this complicates it and confuses the
intention.

If you feel to_sentence() should be made core, put up a RCR for it. But
let's keep join as join.

···

--
Posted via http://www.ruby-forum.com/\.

Phrogz wrote:

What makes the final separator so special that it deserves inclusion,
while making no allowance for a pre-separator? Just the English
language?

Well, just for poops and giggles, here's a less racist alternative (JEG-compatible):

require "enumerator"

class Array
   alias old_join join
   def join(sep = $, last_sep = nil, &each_prefix)
     each_prefix ||= proc do |e,i|
       (last_sep if i == length - 1) || sep
     end
     enum_with_index.map do |e,i|
       i == 0 ? e : "#{each_prefix[e,i]}#{e}"
     end.old_join
   end
end

if __FILE__ == $PROGRAM_NAME
   require "test/unit"

   class TestJoin < Test::Unit::TestCase
     def test_old_matches_new
       assert_equal(.old_join, .join)
       assert_equal([1].old_join, [1].join)
       assert_equal([1, 2].old_join, [1, 2].join)
       assert_equal((1..5).to_a.old_join, (1..5).to_a.join)

       assert_equal(.old_join("|"), .join("|"))
       assert_equal([1].old_join("|"), [1].join("|"))
       assert_equal([1, 2].old_join("|"), [1, 2].join("|"))
       assert_equal((1..5).to_a.old_join("|"), (1..5).to_a.join("|"))
     end

     def test_new_last_arg_behavior
       assert_equal("1, 2, 3, 4, and 5", (1..5).to_a.join(", ", ", and "))
       assert_equal("1, 2, 3, 4 and 5", (1..5).to_a.join(", ", " and "))
       assert_equal("1, 2, 3, 4 & 5", (1..5).to_a.join(", ", " & "))

       assert_equal([1, 2].join(","), [1, 2].join("ignored", ","))
     end

     def test_new_block_behavior
       assert_equal '1 dna, 2, 3, 4, 5',
                    (1..5).to_a.join {|e,i| i == 1 ? ' dna, ' : ', ' }
     end
   end
end

This was intended as a point of discussion, not my final offer as the ideal implementation. Thank you for cleaning it up though.

James Edward Gray II

···

On Dec 31, 2006, at 4:16 PM, Ryan Davis wrote:

On Dec 31, 2006, at 12:05 PM, James Edward Gray II wrote:

I've been looking at the to_sentence() method in Rails. It's basically a join(), but you can give a different final separator. I personally use this kind of functionality often enough to believe it would make a good addition to the core language.

Even better, I think we can make join() smart enough to eliminate any need for to_sentence() by adding a second argument. Here's a sample implementation:

#!/usr/bin/env ruby -w

require "enumerator"

class Array
  alias_method :old_join, :join
  def join(sep = $, last_sep = sep)
    return "" if empty?
    enum_with_index.inject("") do |str, (e, i)|
      "#{str}#{i == size - 1 ? last_sep : sep}#{e}"
    end[sep.to_s.length..-1]
  end
end

problems:

1) calculating a static value inside a loop. (only costs about a hundredth of a second for 10k)
2) iterating when 1/2 of your values are completely static.
3) being clever by using enumerator.

Ryan Davis wrote:

class Array
   alias_method :old_join, :join
   def join(sep = $, last_sep = sep)
     return "" if empty?

     seperators = Array.new(size-1, sep)
     seperators[-1] = last_sep unless seperators.empty?
     self.zip(seperators).old_join
   end
end

# of iterations = 1000000
                           user system total real
null_time 0.140000 0.000000 0.140000 ( 0.140482)
jeg 29.690000 0.080000 29.770000 ( 30.144816)
ryan 19.780000 0.050000 19.830000 ( 19.998077)

I wonder how this would fair.

  class Array
    alias :old_join :join
    def join( sep=$, last_sep=nil )
      s = old_join(sep)
      if last_sep
        rsep = Regexp.escape(sep.to_s)
        rlast = Regexp.escape(last.to_s)
        s.sub!(/#{rsep}#{rlast}$/,"#{last_sep}#{last}")
      end
      return s
    end
  end

Sorry, the bencmark script wasn't posted and I didn't feel like
recreating it.

T.

Hmm... this seems powerful and generally useful.

···

On 12/31/06, Devin Mullins <twifkak@comcast.net> wrote:

     def test_new_block_behavior
       assert_equal '1 dna, 2, 3, 4, 5',
                    (1..5).to_a.join {|e,i| i == 1 ? ' dna, ' : ', ' }
     end

% ./blah.rb 1_000_000
# of iterations = 1000000
                           user system total real
null_time 0.140000 0.000000 0.140000 ( 0.139480)
ryan 19.760000 0.020000 19.780000 ( 19.810269)
trans 21.290000 0.040000 21.330000 ( 21.398592)
Loaded suite ./blah
Started
....
Finished in 0.001109 seconds.

4 tests, 24 assertions, 0 failures, 0 errors

···

On Jan 1, 2007, at 8:40 AM, Trans wrote:

I wonder how this would fair.

  class Array
    alias :old_join :join
    def join( sep=$, last_sep=nil )
      s = old_join(sep)
      if last_sep
        rsep = Regexp.escape(sep.to_s)
        rlast = Regexp.escape(last.to_s)
        s.sub!(/#{rsep}#{rlast}$/,"#{last_sep}#{last}")
      end
      return s
    end
  end

Sorry, the bencmark script wasn't posted and I didn't feel like
recreating it.

Ryan Davis wrote:

> I wonder how this would fair.
>
> class Array
> alias :old_join :join
> def join( sep=$, last_sep=nil )
> s = old_join(sep)
> if last_sep
> rsep = Regexp.escape(sep.to_s)
> rlast = Regexp.escape(last.to_s)
> s.sub!(/#{rsep}#{rlast}$/,"#{last_sep}#{last}")
> end
> return s
> end
> end
>
> Sorry, the bencmark script wasn't posted and I didn't feel like
> recreating it.

% ./blah.rb 1_000_000
# of iterations = 1000000
                           user system total real
null_time 0.140000 0.000000 0.140000 ( 0.139480)
ryan 19.760000 0.020000 19.780000 ( 19.810269)
trans 21.290000 0.040000 21.330000 ( 21.398592)
Loaded suite ./blah
Started
....
Finished in 0.001109 seconds.

4 tests, 24 assertions, 0 failures, 0 errors

Eek. That was worse then I thought it would be. Thanks for showing me
though, Ryan.

Of course now that I'm looking at it again I'm wondering how we missed:

  class Array
    def join( sep=$, last_sep=nil )
      return old_join(sep) unless last_sep
      [slice(0...-1).old_join(sep), last].old_join(last_sep)
    end
  end

That must to be faster.

T.

···

On Jan 1, 2007, at 8:40 AM, Trans wrote:

Ryan Davis wrote:
>
> > I wonder how this would fair.
> >
> > class Array
> > alias :old_join :join
> > def join( sep=$, last_sep=nil )
> > s = old_join(sep)
> > if last_sep
> > rsep = Regexp.escape(sep.to_s)
> > rlast = Regexp.escape(last.to_s)
> > s.sub!(/#{rsep}#{rlast}$/,"#{last_sep}#{last}")
> > end
> > return s
> > end
> > end
> >
> > Sorry, the bencmark script wasn't posted and I didn't feel like
> > recreating it.
>
> % ./blah.rb 1_000_000
> # of iterations = 1000000
> user system total real
> null_time 0.140000 0.000000 0.140000 ( 0.139480)
> ryan 19.760000 0.020000 19.780000 ( 19.810269)
> trans 21.290000 0.040000 21.330000 ( 21.398592)
> Loaded suite ./blah
> Started
> ....
> Finished in 0.001109 seconds.
>
> 4 tests, 24 assertions, 0 failures, 0 errors

Eek. That was worse then I thought it would be. Thanks for showing me
though, Ryan.

Of course now that I'm looking at it again I'm wondering how we missed:

  class Array
    def join( sep=$, last_sep=nil )
      return old_join(sep) unless last_sep
      [slice(0...-1).old_join(sep), last].old_join(last_sep)

slice(0..-2).old_join(sep) << last_sep << last.to_s
or maybe
"#{slice(0..-2).old_join(sep)}#{last_sep}#{last}"

I like your approach of using old_join again, it seems so abstraction based,
but as we are looking for performance :wink:

    end

  end

That must to be faster.

T.

Robert

···

On 1/3/07, Trans <transfire@gmail.com> wrote:

> On Jan 1, 2007, at 8:40 AM, Trans wrote:

--
"The real romance is out ahead and yet to come. The computer revolution
hasn't started yet. Don't be misled by the enormous flow of money into bad
defacto standards for unsophisticated buyers using poor adaptations of
incomplete ideas."

- Alan Kay

I got so taken away by optimizing, I missed my point!
I feel that #join should be left alone, if a more powerful
String#<something> is to be needed maybe it would be better to call it
<something> than I feel too that having a prefix or postfix join available.
A *strange* syntax would be possible (that's why it is not good, but I love
it)
def <something> sep=$, joins={}

list = %{Something blue smth new smth borrowed}
list.something( ', ' -2 => "and" ) => "Something, blue, smth, new, smth and
borrowed"
list.something( ', ' 0 => "{", -1 => "}" ) => "{Something, blue, smth, new,
smth, borrowed}"

now that is nonesense of course and might not work in 1.9/2.0???
let us be reasonable

list.something(:sep => ", ", :odd => "") => "Something blue, smth new, smth
borrowed"
list.something(:sep => ", ", :odd => "", :last_even => "and" ) =>
              "Something blue, smth new and smth borrowed"
and in full form we would have
(0..9).something( :pre => "<", :suff => ">", :even => "_", :odd => "-",
:index => [5, "*"] ) =>
             "<0-1_2-3_4*5_6-7_8-9>"

That all still might be nonsense/bloat but I have gone to extremes to show
why I think it would not be String#join anymore and thus should not be
called String#join

If OTOH a majority thinks enhancing String#join, what about a block
enhancement

list.join do
   >ele, index|
   (index % 2).zero? ? : "," ? "*" # might be nice to change sep on value of
ele too.
end

cheers
Robert

···

--
"The real romance is out ahead and yet to come. The computer revolution
hasn't started yet. Don't be misled by the enormous flow of money into bad
defacto standards for unsophisticated buyers using poor adaptations of
incomplete ideas."

- Alan Kay

Robert Dober wrote:

slice(0..-2).old_join(sep) << last_sep << last.to_s
or maybe
"#{slice(0..-2).old_join(sep)}#{last_sep}#{last}"

I like your approach of using old_join again, it seems so abstraction based,

:slight_smile:

but as we are looking for performance :wink:

Yep, even better.

I'll credit you and add this to Facets, but I'd rather name it
something other than #join. Right now I have #join_sentence, but that's
rather long. Any suggestions?

And for what it's worth, IMO a block additon to join certainly couldn't
hurt. In fact it would be quite nice for alternating separators.

  hash.to_a.flatten.join{ |index| index % 2 == 0 ? ':' : "\n" }

Or something like that.

T.

Robert Dober wrote:

If OTOH a majority thinks enhancing String#join, what about a block
enhancement

Dude... read the whole thread. You're not killfiling me, too, are you?

Not that I'm a fan of the block form... if join gets augmented, it should be something fairly simple and 80/20, such as last_sep.

Devin

Robert Dober wrote:
> slice(0..-2).old_join(sep) << last_sep << last.to_s
> or maybe
> "#{slice(0..-2).old_join(sep)}#{last_sep}#{last}"
>
> I like your approach of using old_join again, it seems so abstraction
based,

:slight_smile:

> but as we are looking for performance :wink:

Yep, even better.

I'll credit you and add this to Facets, but I'd rather name it
something other than #join. Right now I have #join_sentence, but that's
rather long. Any suggestions?

And for what it's worth, IMO a block additon to join certainly couldn't
hurt. In fact it would be quite nice for alternating separators.

  hash.to_a.flatten.join{ |index| index % 2 == 0 ? ':' : "\n" }

Or something like that.

I am flattered , thx :slight_smile: concerning the block I have to admit that Devin has
come up with that idea early in the thread already!
Cheers
Robert

T.

···

On 1/3/07, Trans <transfire@gmail.com> wrote:

--
"The real romance is out ahead and yet to come. The computer revolution
hasn't started yet. Don't be misled by the enormous flow of money into bad
defacto standards for unsophisticated buyers using poor adaptations of
incomplete ideas."

- Alan Kay

Robert Dober wrote:
> If OTOH a majority thinks enhancing String#join, what about a block
> enhancement
Dude... read the whole thread. You're not killfiling me, too, are you?

No I am not, just got taken away by hacking Ruby, very nice that code of
yours :wink:

Not that I'm a fan of the block form... if join gets augmented, it

should be something fairly simple and 80/20, such as last_sep.

Devin

Robert
BTW I was called Mr. Dober and Dude recently, is there something wrong with
my name? :wink:

···

On 1/3/07, Devin Mullins <twifkak@comcast.net> wrote:
--
"The real romance is out ahead and yet to come. The computer revolution
hasn't started yet. Don't be misled by the enormous flow of money into bad
defacto standards for unsophisticated buyers using poor adaptations of
incomplete ideas."

- Alan Kay