7

I have a hash:

sample = { bar: 200, foo: 100, baz: 100 }

How do I sort sample using the order of keys in sort_order:

sort_order = [:foo, :bar, :baz, :qux, :quux]

Expected result:

sample #=> { foo: 100, bar: 200, baz: 100 }

All I can come up with is

new_hash = {}
sort_order.each{|k| new_hash[k] = sample[k] unless sample[k].nil? }
sample = new_hash

There's got to be a better way. Hints?

Keys without values should not be present, i.e. number of keys remain the same, which isn't the case with Sort Hash Keys based on order of same keys in array

6
  • What do you want in a better way? Shorter? Better performance? Commented May 24, 2016 at 6:20
  • 1. Shorter 2. Better performance In that order Commented May 24, 2016 at 6:22
  • @Amit How is this different from my solution? Commented May 24, 2016 at 6:25
  • 1
    sample.sort_by { |k,_| sort_order.index(k) }.to_h or sort_order.each_with_object({}) { |sym,h| h[sym] = sample[sym] if sample.key?(sym) }. Commented May 24, 2016 at 6:31
  • Could there be keys in sample that do not exist in sort_order? Commented May 24, 2016 at 7:02

4 Answers 4

14

A functional approach using the intersection of keys:

new_sample = (sort_order & sample.keys).map { |k| [k, sample[k]] }.to_h
#=> {:foo=>100, :bar=>200, :baz=>100}

As @Stefan noted, the abstraction Hash#slice from ActiveSupport's pretty much does the job:

require 'active_support/core_ext/hash'
new_sample = sample.slice(*sort_order)
#=> {:foo=>100, :bar=>200, :baz=>100}
Sign up to request clarification or add additional context in comments.

1 Comment

slice checks if the keys exist (similar to Keith Bennett's answer), so you can just write sample.slice(*sort_order)
3

Please, see my this answer:

sort_order = [:foo, :bar, :baz, :qux, :quux, :corge, :grault,
              :garply, :waldo, :fred, :plugh, :xyzzy, :thud]
sample = { bar: 200, foo: 100, baz: 100 }

sample.sort_by {|k, _| sort_order.index(k)}.to_h
=> {:foo=>100, :bar=>200, :baz=>100}

1 Comment

Not a problem for small inputs, but note that this is quadratic.
3

The code below does this. Note that I used has_key? because you want the output hash to contain all the keys in the input hash, even if their values are nil.

#!/usr/bin/env ruby

def sorted_hash(input_hash, key_sort_order)
  new_hash = {}
  key_sort_order.each do |key|
    if input_hash.has_key?(key)
      new_hash[key] = input_hash[key]
    end
  end
  new_hash
end

sort_order = [:foo, :bar, :baz, :qux, :quux]
sample = { bar: 200, foo: 100, baz: 100 }

puts sorted_hash(sample, sort_order)
# Outputs: {:foo=>100, :bar=>200, :baz=>100}

A simplification is to use each_with_object:

def sorted_hash_two(input_hash, key_sort_order)
  key_sort_order.each_with_object({}) do |key, result_hash|
    if input_hash.has_key?(key)
      result_hash[key] = input_hash[key]
    end
  end
end

puts sorted_hash_two(sample, sort_order)
# Outputs: {:foo=>100, :bar=>200, :baz=>100}

I like @tokland's idea of array intersection (&) better because it elmiinates the need for an if condition:

def sorted_hash_ewo_intersection(input_hash, key_sort_order)
  (key_sort_order & input_hash.keys).each_with_object({}) do |key, result_hash|
    result_hash[key] = input_hash[key]
  end
end # produces: {:foo=>100, :bar=>200, :baz=>100}

4 Comments

The each_with_object looks simpler, however, I don't see how its any better than my solution.
As I mention in the answer, I think it's important to use has_key? rather than test for nil. As an aside, I recommend expressing this as a method; that separates the logic in question from the inputs and outputs; and your last name sample = new_hash is unnecessary.
I just added @tokland's approach of using array intersection (using &) but using each_with_object, and like that one the best.
Note that & is really set intersection; if there were duplicates then those duplicates would be stripped, e.g.: [1] & [1,1] => [1] and [1,1] & [1] => [1]. In this case, both arrays are guaranteed not to have duplicates since hash keys are always unique, but it's something to be aware of in other contexts.
1

Here is one more way this can be done:

(sort_order & sample.keys).zip([nil]).to_h.merge(sample)
#=> {:foo=>100, :bar=>200, :baz=>100}

Explanation:

First we create a hash that contains only desired keys in the right order.

(sort_order & sample.keys).zip([nil]).to_h
#=> {:foo=>nil, :bar=>nil, :baz=>nil}

And then, we merge this hash with sample to get the values from sample.

1 Comment

Sorry, please forget about my comments that I have deleted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.