How do I replace each substring in a string?

Viewed 102

I have a string:

story = 'A long foo ago, in a foo bar baz, baz away...foobar'

I also have matches from this string (the dictionary is dynamic, it doesn't depend on me)

string_matches = ['foo', 'foo', 'bar', 'baz', 'baz', 'foobar'] # words can be repeated

How to replace each match with **foo**? to get a result:

story = 'A long **foo** ago, in a **foo** **bar** **baz**, **baz** away...**foobar**'

for example my code:

string_matches.each do |word|
  story.gsub!(/#{word}/, "**#{word}**")
end

returned:

"A long ****foo**** ago, in a ****foo**** **bar** ****baz****, ****baz**** away...****foo******bar**"
2 Answers

If you need to check if the words are matched as whole words, you may use

story.gsub(/\b(?:#{Regexp.union(string_matches.uniq.sort { |a,b| b.length <=> a.length }).source})\b/, '**\0**')

If the whole word check is not necessary use

story.gsub(Regexp.union(string_matches.uniq.sort { |a,b| b.length <=> a.length }), '**\0**')

See the Ruby demo

Details

  • \b - a word boundary
  • (?:#{Regexp.union(string_matches.uniq.sort { |a,b| b.length <=> a.length }).source}) - this creates a pattern like (?:foobar|foo|bar|baz) that matches a single word from the deduplicated list of keywords, and sorts them by length in the descending order. See Order of regular expression operator (..|.. ... ..|..) why this is necessary.
  • \b - a word boundary

The \0 in the replacement pattern is the replacement backreference referring to the whole match.

A slight change will nearly get you there:

irb(main):001:0> string_matches.uniq.each { |word| story.gsub!(/#{word}/, "**#{word}**") }
=> ["foo", "bar", "baz", "foobar"]
irb(main):002:0> story
=> "A long **foo** ago, in a **foo** **bar** **baz**, **baz** away...**foo****bar**"

The trouble with the final part of the resulting string is that foobar has been matched by both foo and foobar.

Related