38

I want to replace the last occurrence of a substring in Ruby. What's the easiest way? For example, in abc123abc123, I want to replace the last abc to ABC. How do I do that?

likethesky
  • 846
  • 3
  • 12
  • 28
Just a learner
  • 26,690
  • 50
  • 155
  • 234

10 Answers10

40

How about

new_str = old_str.reverse.sub(pattern.reverse, replacement.reverse).reverse

For instance:

irb(main):001:0> old_str = "abc123abc123"
=> "abc123abc123"
irb(main):002:0> pattern="abc"
=> "abc"
irb(main):003:0> replacement="ABC"
=> "ABC"
irb(main):004:0> new_str = old_str.reverse.sub(pattern.reverse, replacement.reverse).reverse
=> "abc123ABC123"
Chowlett
  • 45,935
  • 20
  • 116
  • 150
22
"abc123abc123".gsub(/(.*(abc.*)*)(abc)(.*)/, '\1ABC\4')
#=> "abc123ABC123"

But probably there is a better way...

Edit:

...which Chris kindly provided in the comment below.

So, as * is a greedy operator, the following is enough:

"abc123abc123".gsub(/(.*)(abc)(.*)/, '\1ABC\3')
#=> "abc123ABC123"

Edit2:

There is also a solution which neatly illustrates parallel array assignment in Ruby:

*a, b = "abc123abc123".split('abc', -1)
a.join('abc')+'ABC'+b
#=> "abc123ABC123"
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • 15
    Due to greedy matching, just `str.sub(/(.*)abc/, '\1ABC')` should suffice. – Chris Johnsen Jul 06 '10 at 10:04
  • Thank you very much. I also thought this problem can be solved by regular expression, but don't know how. You did it. Thanks again! – Just a learner Jul 06 '10 at 11:53
  • to build upon @ChrisJohnsen answer: `class String def sub_last(str_match, str_sub) self.sub(/(.*)#{Regexp.quote(str_match)}/, '\1' + str_sub) end end` – coconup Sep 29 '17 at 14:58
19

Since Ruby 2.0 we can use \K which removes any text matched before it from the returned match. Combine with a greedy operator and you get this:

'abc123abc123'.sub(/.*\Kabc/, 'ABC')
#=> "abc123ABC123"

This is about 1.4 times faster than using capturing groups as Hirurg103 suggested, but that speed comes at the cost of lowering readability by using a lesser-known pattern.

more info on \K: https://www.regular-expressions.info/keep.html

CRandER
  • 266
  • 2
  • 3
6

You can achieve this with String#sub and greedy regexp .* like this:

'abc123abc123'.sub(/(.*)abc/, '\1ABC')
Hirurg103
  • 4,783
  • 2
  • 34
  • 50
5

When searching in huge streams of data, using reverse will definitively* lead to performance issues. I use string.rpartition*:

sub_or_pattern = "!"
replacement = "?"
string = "hello!hello!hello"

array_of_pieces = string.rpartition sub_or_pattern
( array_of_pieces[(array_of_pieces.find_index sub_or_pattern)] =  replacement ) rescue nil
p array_of_pieces.join
# "hello!hello?hello"

The same code must work with a string with no occurrences of sub_or_pattern:

string = "hello_hello_hello"
# ...
# "hello_hello_hello"

*rpartition uses rb_str_subseq() internally. I didn't check if that function returns a copy of the string, but I think it preserves the chunk of memory used by that part of the string. reverse uses rb_enc_cr_str_copy_for_substr(), which suggests that copies are done all the time -- although maybe in the future a smarter String class may be implemented (having a flag reversed set to true, and having all of its functions operating backwards when that is set), as of now, it is inefficient.

Moreover, Regex patterns can't be simply reversed. The question only asks for replacing the last occurrence of a sub-string, so, that's OK, but readers in the need of something more robust won't benefit from the most voted answer (as of this writing)

ribamar
  • 1,435
  • 1
  • 16
  • 26
5

Here's another possible solution:

>> s = "abc123abc123"
=> "abc123abc123"

>> s[s.rindex('abc')...(s.rindex('abc') + 'abc'.length)] = "ABC"
=> "ABC"

>> s
=> "abc123ABC123"
Daniel Pietzsch
  • 1,369
  • 14
  • 21
2

simple and efficient:

s = "abc123abc123abc"
p = "123"
s.slice!(s.rindex(p), p.size)
s == "abc123abcabc"
grosser
  • 14,707
  • 7
  • 57
  • 61
1
string = "abc123abc123"
pattern = /abc/
replacement = "ABC"

matches = string.scan(pattern).length
index = 0
string.gsub(pattern) do |match|
  index += 1
  index == matches ? replacement : match
end
#=> abc123ABC123
matsadler
  • 189
  • 4
1

I've used this handy helper method quite a bit:

def gsub_last(str, source, target)
  return str unless str.include?(source)
  top, middle, bottom = str.rpartition(source)
  "#{top}#{target}#{bottom}"
end

If you want to make it more Rails-y, extend it on the String class itself:

class String
  def gsub_last(source, target)
    return self unless self.include?(source)
    top, middle, bottom = self.rpartition(source)
    "#{top}#{target}#{bottom}"
  end
end

Then you can just call it directly on any String instance, eg "fooBAR123BAR".gsub_last("BAR", "FOO") == "fooBAR123FOO"

Steve Craig
  • 411
  • 4
  • 6
0
.gsub /abc(?=[^abc]*$)/, 'ABC'

Matches a "abc" and then asserts ((?=) is positive lookahead) that no other characters up to the end of the string are "abc".

Daniel Garmoshka
  • 5,849
  • 39
  • 40