user3189916
user3189916

Reputation: 768

How to use sort_by to sort alphabetically then numerically then by special characters

I have an array:

arr = ["Bar", "abc", "foo", "1", "20”, "10", "_def"]

I need to sort using case-insensitive alphabetically first, then numerically followed by special characters.

I am trying to use sort_by:

irb(main):071:0> arr.sort_by {|s| [s[/[0-9a-z]+/], s.to_i]}
=> ["1", "10", "20", "abc", "Bar", "_def", "foo"]

The output has to be:

arr = ["abc", "Bar", "foo", "1", “10”, “20", "_def"]

Upvotes: 4

Views: 818

Answers (3)

the Tin Man
the Tin Man

Reputation: 160551

A little benchmark is needed:

require 'active_support/core_ext/array/access.rb'
require 'fruity'

ARR = ["Bar", "abc", "foo", "1", "20", "10", "_def"]

def run_demir(ary)
  ary.each_with_object(Array.new(3) { Array.new }) do |word, group|
    if word.match /^[A-Za-z]/
      group.first
    elsif word.match /^[0-9]/
      group.second
    else
      group.third
    end << word
  end.flat_map{ |group| group.sort_by{ |x| x.downcase } }
end

def run_stefan(ary)
  ary.sort_by do |s|
    case s
    when /^[a-z]/i
      [1, s.downcase]
    when /^\d/
      [2, s.to_i]
    else
      [3, s]
    end
  end
end

run_demir(ARR)  # => ["abc", "Bar", "foo", "1", "10", "20", "_def"]
run_stefan(ARR) # => ["abc", "Bar", "foo", "1", "10", "20", "_def"]

compare do
  demir  { run_demir(ARR)  }
  Stefan { run_stefan(ARR) }
end

Which results in:

# >> Running each test 512 times. Test will take about 1 second.
# >> Stefan is faster than demir by 2x ± 0.1

Upvotes: 1

Stefan
Stefan

Reputation: 114178

From the docs:

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…

You can take advantage of this behavior by creating sorting groups:

arr = ["Bar", "abc", "foo", "1", "20", "10", "_def"]

arr.sort_by do |s|
  case s
  when /^[a-z]/i
    [1, s.downcase]
  when /^\d/
    [2, s.to_i]
  else
    [3, s]
  end
end
#=> ["abc", "Bar", "foo", "1", "10", "20", "_def"]

The first element (1, 2, 3) defines the group's position: strings with letters on 1st position, numeric strings on 2nd position and the remaining on 3rd position. Within each group, the elements are sorted by the second element: strings with letters by their lowercase value, numeric strings by their integer value and the remaining by themselves.

Upvotes: 5

demir
demir

Reputation: 4709

You can create groups first and then sort groups.

arr.each_with_object(Array.new(3) { Array.new }) do |word, group|
  if word.match /^[A-Za-z]/
    group.first
  elsif word.match /^[0-9]/
    group.second
  else
    group.third
  end << word
end.flat_map{ |group| group.sort_by{ |x| x.downcase } }

#=> ["abc", "Bar", "foo", "1", "10", "20", "_def"]

Upvotes: 4

Related Questions