Artem Pakk
Artem Pakk

Reputation: 263

How to join some (not all) array elements based on condition in ruby?

Say I have an array of strings like this:

array = ["foo", "(bar)", "baaz", "quux", "herp", "(derp)"]

And I need to join items starting with "(" with the previous item to get output like this:

["foo (bar)", "baaz", "quux", "herp (derp)"]

I guess it has to be something like get indices of array items that match /^\(/ and then iterate over original array in a block, joining items at index-1..index, and deleting at index

Upvotes: 3

Views: 1571

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110685

Here's another way, using Enumerable#chunk. I assume that the first character of the first element of the array is not (, but the method could of course be modified if that assumption is not correct.

Code

def doit(array)  
  array.chunk { |s| s[0] == ?( }
       .map(&:last)
       .each_slice(2)
       .map { |arr| (arr.size == 2) ? [arr.first[0..-2],
                      [arr.first.last, *arr.last].join(' ')] : arr }
       .flatten
end

Examples

array = ["foo", "(bar)", "baaz", "quux", "herp", "(derp)"]
doit(array) #=> ["foo (bar)", "baaz", "quux", "herp (derp)"]

array = ["foo", "(bar)", "(anther bar)", "quux"]    
doit(array) #=> ["foo (bar) (anther bar)", "quux"]

Explanation

array = ["foo", "(bar)", "baaz", "quux", "herp", "(derp)"]

enum1 = array.chunk { |s| s[0] == ?( }
  #=> #<Enumerator: #<Enumerator::Generator:0x00000101142ce0>:each>

enum1.to_a # elements to be enumerated (for information only)
  #=> [[false, ["foo"]], [true, ["(bar)"]],
  #    [false, ["baaz", "quux", "herp"]], [true, ["(derp)"]]]
a = enum1.map(&:last)
  #=> [["foo"], ["(bar)"], ["baaz", "quux", "herp"], ["(derp)"]]
enum2 = a.each_slice(2)
  #=> #<Enumerator: [["foo"], ["(bar)"], ["baaz", "quux", "herp"],
  #                  ["(derp)"]]:each_slice(2)>
enum2.to_a # elements to be enumerated (for information only)
  #=> [[["foo"], ["(bar)"]], [["baaz", "quux", "herp"], ["(derp)"]]]
c = enum2.map { |arr| (arr.size==2) ? [arr.first[0..-2],
                        [arr.first.last, *arr.last].join(' ')] : arr }
  #=> [[[], "foo (bar)"], [["baaz", "quux"], "herp (derp)"]]
c.flatten
  #=> ["foo (bar)", "baaz", "quux", "herp (derp)"]

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110685

I cannot improve upon @sawa's answer, but I can offer an alternative approach that some readers may find useful at another time in another place:

array = ["foo", "(bar)", "baaz", "quux", "herp", "(derp)"]

arr = []
enum = array.each
loop do
  arr << enum.next
  next_up = enum.peek
  if next_up[0] == ?(
    arr[-1] += (" " + next_up)
    enum.next
  end
end

arr #=> ["foo (bar)", "baaz", "quux", "herp (derp)"]

This is what's happening.

arr = []
enum = array.each
  #=> #<Enumerator: ["foo", "(bar)", "baaz", "quux", "herp", "(derp)"]:each>

Let's now step through the loop until a StopIteration exception is raised:

s = enum.next                #=> "foo"
arr << s                     #=> ["foo"]
next_up = enum.peek          #=> "(bar)"
next_up[0] == ?(             #=> true
  arr[-1] += (" " + next_up) #=> "foo (bar)"
    arr                      #=> ["foo (bar)"]
  enum.next                  #=> "(bar)" (discard)

s = enum.next                #=> "baaz"
arr << s                     #=> ["foo (bar)", "baaz"]
next_up = enum.peek          #=> "quux"
next_up[0] == ?(             #=> false

s = enum.next                #=> "quux"
arr << s                     #=> ["foo (bar)", "baaz", "quux"]
next_up = enum.peek          #=> "herp"
next_up[0] == ?(             #=> false

s = enum.next                #=> "herp"
arr << s                     #=> ["foo (bar)", "baaz", "quux", "herp"]
next_up = enum.peek          #=> "(derp)"
next_up[0] == ?(             #=> true
  arr[-1] += (" " + next_up) #=> "herp (derp)"
    arr                      #=> ["foo (bar)", "baaz", "quux", "herp (derp)"]
  enum.next                  #=> "(derp)" (discard)

s = enum.next                #=> StopIteration: iteration reached an end

The StopIteration exception is handled by Kernel#loop by breaking the loop.

arr                          #=> ["foo (bar)", "baaz", "quux", "herp (derp)"]

Upvotes: 2

Uri Agassi
Uri Agassi

Reputation: 37409

Looking at it from another direction - joining the whole string, then splitting it along spaces which do not have ( after them:

array.join(' ').split(/ (?!\()/)
# => ["foo (bar)", "baaz", "quux", "herp (derp)"]

Upvotes: 5

sawa
sawa

Reputation: 168101

array.slice_before{|s| !s.start_with?("(")}.map{|a| a.join(" ")}
# => ["foo (bar)", "baaz", "quux", "herp (derp)"]

Upvotes: 5

Related Questions