Natalia Natalia - 1 year ago 73
Ruby Question

How do I combine elements in an array matching a pattern?

I have an array of strings

["123", "a", "cc", "dddd", "mi hello", "33"]


I want to join by a space consecutive elements that begin with a letter, have at least two characters, and do not contain a space. Applying that logic to the above would yield

["123", "a", "cc dddd", "mi hello", "33"]


Similarly if my array were

["mmm", "3ss", "foo", "bar", "foo", "55"]


I would want the result to be

["mm", "3ss", "foo bar foo", "55"]


How do I do this operation?

Answer Source

There are many ways to solve this; ruby is a highly expressive language. It would be most beneficial for you to show what you have tried so far, so that we can help debug/fix/improve your attempt.

For example, here is one possible implementation that I came up with:

def combine_words(array)
  array
    .chunk {|string| string.match?(/\A[a-z][a-z0-9]+\z/i) }
    .flat_map {|concat, strings| concat ? strings.join(' ') : strings}
end

combine_words(["aa", "b", "cde", "f1g", "hi", "2j", "l3m", "op", "q r"])
  # => ["aa", "b", "cde f1g hi", "2j", "l3m op", "q r"]

Note that I was a little unclear exactly how to interpret your requirement:

begin with a letter, have at least two characters, and do not contain a space

Can strings contain punctuation? Underscores? Utf-8 characters? I took it to mean "only a-z, A-Z or 0-9", but you may want to tweak this.

A literal interpretation of your requirement could be: /\A[[:alpha:]][^ ]+\z/, but I suspect that's not what you meant.

Explanation:

  • Enumerable#chunk will iterate through the array and collect terms by the block's response value. In this case, it will find sequential elements that match/don't match the required regex.
  • String#match? checks whether the string matches the pattern, and returns a boolean response. Note that if you were using ruby v2.3 or below, you'd have needed some workaround such as !!string.match, to force a boolean response.
  • Enumerable#flat_map then loops through each "result", joining the strings if necessary, and flattens the result to avoid returning any nested arrays.

Here is another, similar, solution:

def word?(string)
  string.match?(/\A[a-z][a-z0-9]+\z/i)
end

def combine_words(array)
  array
    .chunk_while {|x, y| word?(x) && word?(y)}
    .map {|group| group.join(' ')}
end

Or, here's a more "low-tech" solution - which only uses more basic language features. (I'm re-using the same word? method here):

def combine_words(array)
  previous_was_word = false
  result = []
  array.each do |string|
    if previous_was_word && word?(string)
      result.last << " #{string}"
    else
      result << string
    end
    previous_was_word = word?(string)
  end
  result
end
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download