401

I am looking for a more elegant way of concatenating strings in Ruby.

I have the following line:

source = "#{ROOT_DIR}/" << project << "/App.config"

Is there a nicer way of doing this?

And for that matter what is the difference between << and +?

idmean
  • 14,540
  • 9
  • 54
  • 83
dagda1
  • 26,856
  • 59
  • 237
  • 450

16 Answers16

623

You can do that in several ways:

  1. As you shown with << but that is not the usual way
  2. With string interpolation

    source = "#{ROOT_DIR}/#{project}/App.config"
    
  3. with +

    source = "#{ROOT_DIR}/" + project + "/App.config"
    

The second method seems to be more efficient in term of memory/speed from what I've seen (not measured though). All three methods will throw an uninitialized constant error when ROOT_DIR is nil.

When dealing with pathnames, you may want to use File.join to avoid messing up with pathname separator.

In the end, it is a matter of taste.

scarver2
  • 7,887
  • 2
  • 53
  • 61
Keltia
  • 14,535
  • 3
  • 29
  • 30
  • 8
    I'm not very experienced with ruby. But generally in cases where you concatenate lots of strings you often can gain performance by appending the strings to an array and then at the end put the string together atomically. Then << could be useful? – PEZ Dec 18 '08 at 13:12
  • 1
    You'll have to add memory an copy the longer string into it anyway. << is more or less the same as + except that you can << with a single character. – Keltia Dec 18 '08 at 13:20
  • 10
    Instead of using << on the elements of an array, use Array#join, it's much faster. – Grant Hutchins Dec 22 '08 at 09:49
107

The + operator is the normal concatenation choice, and is probably the fastest way to concatenate strings.

The difference between + and << is that << changes the object on its left hand side, and + doesn't.

irb(main):001:0> s = 'a'
=> "a"
irb(main):002:0> s + 'b'
=> "ab"
irb(main):003:0> s
=> "a"
irb(main):004:0> s << 'b'
=> "ab"
irb(main):005:0> s
=> "ab"
Rimian
  • 36,864
  • 16
  • 117
  • 117
Matt Burke
  • 3,306
  • 1
  • 22
  • 16
  • 38
    The + operator is definitely not the fastest way to concatenate strings. Every time you use it, it makes a copy, whereas << concatenates in place and is much more performant. – Evil Trout Jun 06 '12 at 21:00
  • 6
    For most uses, interpolation, `+` and `<<` are going to be about the same. If you're dealing with a lot of strings, or really big ones, then you might notice a difference. I was surprised by how similar they performed. https://gist.github.com/2895311 – Matt Burke Jun 08 '12 at 12:15
  • 9
    Your jruby results are skewed against interpolation by the early-run JVM overload. If you run the test suite several times (in the same process -- so wrap everything in say a `5.times do ... end` block) for each interpreter, you'd end up with more accurate results. My testing has shown interpolation is the fastest method, across all Ruby interpreters. I would have expected `<<` to be the quickest, but that's why we benchmark. – womble Jun 10 '12 at 04:26
  • Not being too versed on Ruby, I'm curious whether the mutation is performed on the stack or heap? If on heap, even a mutation operation, which seems like it should be quicker, probably involves some form of malloc. Without it, I'd expect a buffer overflow. Using the stack could be pretty fast but the resultant value is probably placed on the heap anyway, requiring a malloc operation. In the end, I expect the memory pointer to be a new address, even if the variable reference makes it look like an in-place mutation. So, really, is there a difference? – Robin Coe Jan 21 '16 at 18:33
81

If you are just concatenating paths you can use Ruby's own File.join method.

source = File.join(ROOT_DIR, project, 'App.config')
georg
  • 2,199
  • 16
  • 11
  • 5
    This seems to be the way to go since then ruby will take care of creating the correct string on system with different path separators. – PEZ Dec 18 '08 at 13:28
32

from http://greyblake.com/blog/2012/09/02/ruby-perfomance-tricks/

Using << aka concat is far more efficient than +=, as the latter creates a temporal object and overrides the first object with the new object.

require 'benchmark'

N = 1000
BASIC_LENGTH = 10

5.times do |factor|
  length = BASIC_LENGTH * (10 ** factor)
  puts "_" * 60 + "\nLENGTH: #{length}"

  Benchmark.bm(10, '+= VS <<') do |x|
    concat_report = x.report("+=")  do
      str1 = ""
      str2 = "s" * length
      N.times { str1 += str2 }
    end

    modify_report = x.report("<<")  do
      str1 = "s"
      str2 = "s" * length
      N.times { str1 << str2 }
    end

    [concat_report / modify_report]
  end
end

output:

____________________________________________________________
LENGTH: 10
                 user     system      total        real
+=           0.000000   0.000000   0.000000 (  0.004671)
<<           0.000000   0.000000   0.000000 (  0.000176)
+= VS <<          NaN        NaN        NaN ( 26.508796)
____________________________________________________________
LENGTH: 100
                 user     system      total        real
+=           0.020000   0.000000   0.020000 (  0.022995)
<<           0.000000   0.000000   0.000000 (  0.000226)
+= VS <<          Inf        NaN        NaN (101.845829)
____________________________________________________________
LENGTH: 1000
                 user     system      total        real
+=           0.270000   0.120000   0.390000 (  0.390888)
<<           0.000000   0.000000   0.000000 (  0.001730)
+= VS <<          Inf        Inf        NaN (225.920077)
____________________________________________________________
LENGTH: 10000
                 user     system      total        real
+=           3.660000   1.570000   5.230000 (  5.233861)
<<           0.000000   0.010000   0.010000 (  0.015099)
+= VS <<          Inf 157.000000        NaN (346.629692)
____________________________________________________________
LENGTH: 100000
                 user     system      total        real
+=          31.270000  16.990000  48.260000 ( 48.328511)
<<           0.050000   0.050000   0.100000 (  0.105993)
+= VS <<   625.400000 339.800000        NaN (455.961373)
Sevle
  • 3,109
  • 2
  • 19
  • 31
Danny
  • 3,982
  • 1
  • 34
  • 42
11

Since this is a path I'd probably use array and join:

source = [ROOT_DIR, project, 'App.config'] * '/'
Dejan Simic
  • 7,820
  • 2
  • 28
  • 15
10

Here's another benchmark inspired by this gist. It compares concatenation (+), appending (<<) and interpolation (#{}) for dynamic and predefined strings.

require 'benchmark'

# we will need the CAPTION and FORMAT constants:
include Benchmark

count = 100_000


puts "Dynamic strings"

Benchmark.benchmark(CAPTION, 7, FORMAT) do |bm|
  bm.report("concat") { count.times { 11.to_s +  '/' +  12.to_s } }
  bm.report("append") { count.times { 11.to_s << '/' << 12.to_s } }
  bm.report("interp") { count.times { "#{11}/#{12}" } }
end


puts "\nPredefined strings"

s11 = "11"
s12 = "12"
Benchmark.benchmark(CAPTION, 7, FORMAT) do |bm|
  bm.report("concat") { count.times { s11 +  '/' +  s12 } }
  bm.report("append") { count.times { s11 << '/' << s12 } }
  bm.report("interp") { count.times { "#{s11}/#{s12}"   } }
end

output:

Dynamic strings
              user     system      total        real
concat    0.050000   0.000000   0.050000 (  0.047770)
append    0.040000   0.000000   0.040000 (  0.042724)
interp    0.050000   0.000000   0.050000 (  0.051736)

Predefined strings
              user     system      total        real
concat    0.030000   0.000000   0.030000 (  0.024888)
append    0.020000   0.000000   0.020000 (  0.023373)
interp    3.160000   0.160000   3.320000 (  3.311253)

Conclusion: interpolation in MRI is heavy.

Adobe
  • 12,967
  • 10
  • 85
  • 126
7

I'd prefer using Pathname:

require 'pathname' # pathname is in stdlib
Pathname(ROOT_DIR) + project + 'App.config'

about << and + from ruby docs:

+: Returns a new String containing other_str concatenated to str

<<: Concatenates the given object to str. If the object is a Fixnum between 0 and 255, it is converted to a character before concatenation.

so difference is in what becomes to first operand (<< makes changes in place, + returns new string so it is memory heavier) and what will be if first operand is Fixnum (<< will add as if it was character with code equal to that number, + will raise error)

Rimian
  • 36,864
  • 16
  • 117
  • 117
tig
  • 25,841
  • 10
  • 64
  • 96
  • 2
    I just discovered that calling '+' on a Pathname can be dangerous because if the arg is an absolute path, the receiver path is ignored: `Pathname('/home/foo') + '/etc/passwd' # => #`. This is by design, based on the rubydoc example. Seems that File.join is safer. – Kelvin Jul 12 '11 at 15:14
  • also you need to call `(Pathname(ROOT_DIR) + project + 'App.config').to_s` if you want to return a string object. – lacostenycoder Nov 27 '19 at 23:26
6

Let me show to you all my experience with that.

I had an query that returned 32k of records, for each record I called a method to format that database record into a formated string and than concatenate that into a String that at the end of all this process wil turn into a file in disk.

My problem was that by the record goes, around 24k, the process of concatenating the String turned on a pain.

I was doing that using the regular '+' operator.

When I changed to the '<<' was like magic. Was really fast.

So, I remembered my old times - sort of 1998 - when I was using Java and concatenating String using '+' and changed from String to StringBuffer (and now we, Java developer have the StringBuilder).

I believe that the process of + / << in Ruby world is the same as + / StringBuilder.append in the Java world.

The first reallocate the entire object in memory and the other just point to a new address.

Marcio Mangar
  • 156
  • 3
  • 4
5

Here are more ways to do this:

"String1" + "String2"

"#{String1} #{String2}"

String1<<String2

And so on ...

Roddy of the Frozen Peas
  • 14,380
  • 9
  • 49
  • 99
Imran Alavi
  • 67
  • 1
  • 2
5

Concatenation you say? How about #concat method then?

a = 'foo'
a.object_id #=> some number
a.concat 'bar' #=> foobar
a.object_id #=> same as before -- string a remains the same object

In all fairness, concat is aliased as <<.

Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
  • 7
    There is one more way of glueing strings together not mentioned by others, and that is by mere juxtaposition: `"foo" "bar" 'baz" #=> "foobarabaz"` – Boris Stitnicky Jun 12 '13 at 03:51
  • Note to others: That's not supposed to be a single quote, but a double one like the rest. Neat method! – Joshua Pinter Jan 20 '20 at 03:28
2

You may use + or << operator, but in ruby .concat function is the most preferable one, as it is much faster than other operators. You can use it like.

source = "#{ROOT_DIR}/".concat(project.concat("/App.config"))
Ronan Boiteau
  • 9,608
  • 6
  • 34
  • 56
Muhammad Zubair
  • 466
  • 5
  • 17
2

You can also use % as follows:

source = "#{ROOT_DIR}/%s/App.config" % project

This approach works with ' (single) quotation mark as well.

Mark
  • 5,994
  • 5
  • 42
  • 55
2

You can concatenate in string definition directly:

nombre_apellido = "#{customer['first_name']} #{customer['last_name']} #{order_id}"
julianm
  • 2,393
  • 1
  • 23
  • 24
1

Situation matters, for example:

# this will not work
output = ''

Users.all.each do |user|
  output + "#{user.email}\n"
end
# the output will be ''
puts output

# this will do the job
output = ''

Users.all.each do |user|
  output << "#{user.email}\n"
end
# will get the desired output
puts output

In the first example, concatenating with + operator will not update the output object,however, in the second example, the << operator will update the output object with each iteration. So, for the above type of situation, << is better.

Affan Khan
  • 39
  • 1
  • 3
0

For your particular case you could also use Array#join when constructing file path type of string:

string = [ROOT_DIR, project, 'App.config'].join('/')]

This has a pleasant side effect of automatically converting different types to string:

['foo', :bar, 1].join('/')
=>"foo/bar/1"
lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
0

For Puppet:

$username = 'lala'
notify { "Hello ${username.capitalize}":
    withpath => false,
}
qräbnö
  • 2,722
  • 27
  • 40