Sort strings and numbers in Ruby

19,046

Solution 1

A general trick for solving tricky sorts is to use #sort_by, with the block returning an array having the primary and secondary sort order (and, if you need it, tertiary, etc.)

a = ['foo', 'bar', '1', '2', '10']  
b = a.sort_by do |s|
  if s =~ /^\d+$/
    [2, $&.to_i]
  else
    [1, s]
  end
end
p b    # => ["bar", "foo", "1", "2", "10"]

This works because of the way array comparison is defined by Ruby. The comparison is defined by the Array#<=> method:

Arrays are compared in an “element-wise” manner; the first element of ary is compared with the first one of other_ary using the <=> operator, then each of the second elements, etc… As soon as the result of any such comparison is non zero (i.e. the two corresponding elements are not equal), that result is returned for the whole array comparison.

Solution 2

Sort an array of mixed numbers and strings by putting the numbers first, and in order, followed by the strings second, and in order.

>> a = [1, 2, "b", "a"]

>> a.partition{|x| x.is_a? String}.map(&:sort).flatten
=> ["a", "b", 1, 2]

Solution 3

a = ['1', '10', '100', '2', '42', 'hello', 'x1', 'x20', 'x100', '42x', '42y', '10.1.2', '10.10.2', '10.8.2']
a.map {|i| i.gsub(/\d+/) {|s| "%08d" % s.to_i } }.zip(a).sort.map{|x,y| y}
# => ["1", "2", "10", "10.1.2", "10.8.2", "10.10.2", "42", "42x", "42y", "100", "hello", "x1", "x20", "x100"] 

Solution 4

Normally, alphabetization is done with numbers first. If you want to alphabetize something where letters are alphabetized before numbers, you will need to alter the compare function used.

# I realize this function could be done with less if-then-else logic,
# but I thought this would be clearer for teaching purposes.
def String.mysort(other)
  length = (self.length < other.length) ? self.length : other.length
  0.upto(length-1) do |i|
    # normally we would just return the result of self[i] <=> other[i]. But
    # you need a custom sorting function.
    if self[i] == other[i]
      continue # characters the same, skip to next character.
    else
      if self[i] ~= /[0-9]/
        if other[i] ~= /[0-9]/
          return self[i] <=> other[i]  # both numeric, sort normally.
        else
          return 1  # self is numeric, other is not, so self is sorted after.
        end
      elsif other[i] ~= /[0-9]/
        return  -1  # self is not numeric, other is, so self is sorted before.
      else
        return self[i] <=> other[i]    # both non-numeric, sort normally.
      end
    end
  end

  # if we got this far, the segments were identical. However, they may
  # not be the same length. Short sorted before long.
  return self.length <=> other.length
end

['0','b','1','a'].sort{|x,y| x.mysort(y) } # => ['a', 'b', '0', '1']

Solution 5

Here is a somewhat verbose answers. Divide the array into two sub arrays: strings and numbers, sort them and concat them.

array = [1, 'b', 'a', 'c', 'd', 2, 4, 3]
strings = []
numbers = []
array.each do |element|
  if element.is_a? String
    strings << element
  else
    numbers << element
  end
end
sorted_array = strings.sort + numbers.sort
sorted_array # ['a', 'b', 'c', 'd', 1, 2, 3, 4]
Share:
19,046
thenengah
Author by

thenengah

I taught ESL after university, then I started making things for the internet. My favorite tools are mac, ubuntu, vim, tmux, bash, git, javascript/node/express, ruby/rails, react, redux, bootstrap, sass, webpack, gulp, babel, jest, mysql, mongodb, redis, neo4j, rabbitMQ, ELK, nginx, jenkins, AWS.

Updated on June 06, 2022

Comments

  • thenengah
    thenengah almost 2 years

    I want to sort an array by strings first and then numbers. How do I do this?