ruby reorder array of symbols in one pass

Viewed 97

I am looking for the most efficient way to do this. Anything with a hyphen must be in before any symbols without a hyphen in the array. My naive solution filters the array twice and concats. I feel like there should be a way to do this in one pass instead of two.

input = [:en, :de, :es, :"es-MX", :fr, :ko, :"ko-KR", :"en-GB"]

output = [:"es-MX", :"ko-KR", :"en-GB", :en, :de, :es, :fr]

Naive solution:

def reorder(input)
  ## find everything with a hypen
  output = input.select { |l| 
    l.to_s.include?('-')
  }

  # find everything without a hyphen and concat to output
  output.concat(input.reject { |l| 
    l.to_s.include?('-')
  })
end
7 Answers

This is a sorting task, so it makes the most sense to me to simply use Enumerable#sort_by:

# Put symbols with hyphens first
input.sort_by { |s| s.to_s.include?('-') ? 0 : 1 }

If you want to sort by something else within those categories, you can do something like this:

# Put symbols with hyphens first, then sort in alphabetical order
input.sort_by { |s| [s.to_s.include?('-') ? 0 : 1, s] }

You can filter the array in one pass with Array#partition:

def reorder(input)
  a, b = input.partition { |l| l.to_s.include?('-') }

  a.concat(b)
end
input = [:en, :de, :es, :"es-MX", :fr, :ko, :"ko-KR", :"en-GB"]

code

p input.partition{|x|x.to_s.include?'-'}.flatten

Output

[:"es-MX", :"ko-KR", :"en-GB", :en, :de, :es, :fr, :ko]

You're doing it in two passes as I can see.

With one pass only, you could add an if statement and check each element then push it on the left array if it contains the hypen, on the right array otherwise.

Then concat the arrays.

def reorder(input)
  left = []
  right = []
  input.each { |l| 
    if l.to_s.include?('-')
      left.push(l)
    else
      right.push(l)
    end
  }

  left.concat(right)
end

Here's a way that makes a single pass through the array and preserves the ordering of symbols with hyphens and the ordering of symbols without hyphens.

def rearrange(arr)
  a = arr.dup
  pos = -1
  a.size.times { |i| a.insert(pos+=1, a.delete_at(i)) if a[i].to_s.include?('-') }
  a
end
rearrange [:en, :de, :es, :"es-MX", :fr, :ko, :"ko-KR", :"en-GB"]
  #=> [:"es-MX", :"ko-KR", :"en-GB", :en, :de, :es, :fr, :ko]

The the use of Kernel#dup (to avoid mutating the original array) does not constitute a pass through the array because (as I understand) the C code copies blocks of memory.

How about:

input.sort do |a,b|
  next a <=> b if a.to_s.include?('-') == b.to_s.include?('-')
  a.to_s.include?('-') ? -1 : 1
end

Try this:

def sort(arr)
  with_hyphen = []
  without_hyphen = []
  for item in arr do
    if item.include?("-")
       with_hyphen << item
    else
       without_hyphen << item
    end
  end
  return with_hyphen + without_hyphen
end
Related