Reputation: 768
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
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
Reputation: 114178
From the docs:
Arrays are compared in an “element-wise” manner; the first element of
ary
is compared with the first one ofother_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
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