开发者

"natural" sort an array of hashes in Ruby

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 per开发者_如何学Pythonsonally 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 }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜