“natural” sort an array of hashes in Ruby
-
16-01-2021 - |
質問
There are workable answers for sorting an array of hashes and for natural sorting, but what is the best way to do both at once?
my_array = [ {"id":"some-server-1","foo":"bar"},{"id":"some-server-2","foo":"bat"},{"id":"some-server-10","foo":"baz"} ]
I would like to sort on "id" such that the final ordering is:
some-server-1
some-server-2
some-server-10
I feel like there must be a clever and efficient way to do this, though personally I don't need to break any speed records and will only be sorting a few hundred items. Can I implement a comparison function in sort_by?
解決
First of all, your my_array
is JavaScript/JSON so I'll assume that you really have this:
my_array = [
{"id" => "some-server-1", "foo" => "bar"},
{"id" => "some-server-2", "foo" => "bat"},
{"id" => "some-server-10", "foo" => "baz"}
]
Then you just need to sort_by
the numeric suffix of the 'id'
values:
my_array.sort_by { |e| e['id'].sub(/^some-server-/, '').to_i }
If the "some-server-" prefixes aren't always "some-server-" then you could try something like this:
my_array.sort_by { |e| e['id'].scan(/\D+|\d+/).map { |x| x =~ /\d/ ? x.to_i : x } }
That would split the 'id'
values into numeric and non-numeric pieces, convert the numeric pieces to integers, and then compare the mixed string/integers arrays using the Array <=>
operator (which compares component-wise); this will work as long as the numeric and non-numeric components always match up. This approach would handle this:
my_array = [
{"id" => "some-server-1", "foo" => "bar"},
{"id" => "xxx-10", "foo" => "baz"}
]
but not this:
my_array = [
{"id" => "11-pancakes-23", "foo" => "baz"},
{"id" => "some-server-1", "foo" => "bar"}
]
If you need to handle this last case then you'd need to compare the arrays entry-by-entry by hand and adjust the comparison based on what you have. You could still get some of the advantages of the sort_by
Schwartzian Transform with something like this (not very well tested code):
class NaturalCmp
include Comparable
attr_accessor :chunks
def initialize(s)
@chunks = s.scan(/\D+|\d+/).map { |x| x =~ /\d/ ? x.to_i : x }
end
def <=>(other)
i = 0
@chunks.inject(0) do |cmp, e|
oe = other.chunks[i]
i += 1
if(cmp == 0)
cmp = e.class == oe.class \
? e <=> oe \
: e.to_s <=> oe.to_s
end
cmp
end
end
end
my_array.sort_by { |e| NaturalCmp.new(e['id']) }
The basic idea here is to push the comparison noise off to another class to keep the sort_by
from degenerating into an incomprehensible mess. Then we use the same scanning as before to break the strings into pieces and implement the array <=>
comparator by hand. If we have two things of the same class then we let that class's <=>
deal with it otherwise we force both components to String and compare them as such. And we only care about the first non-0 result.
他のヒント
@mu gives a more than adequate answer for my case, but I also figured out the syntax for introducing arbitrary comparisons:
def compare_ids(a,b)
# Whatever code you want here
# Return -1, 0, or 1
end
sorted_array = my_array.sort { |a,b| compare_ids(a["id"],b["id"] }
I think that if you are sorting on the id
field, you could try this:
my_array.sort { |a,b| a["id"].to_i <=> b["id"].to_i }