1

Variable fav_food can be either "pie", "cake", or "cookie", which will be inputed by the users. However, I want my food_qty Hash to list the fav_food as the first key.

Thus, I come out with

food_order = ([fav_food] + food_qty.keys).uniq

However, is there a better way to do this?

food_qty = {"pie" => 0, "cake" => 0, "cookie" => 0}
# make the fav_food listed first in the hash
food_order = ([fav_food] + food_qty.keys).uniq
Jordan Running
  • 102,619
  • 17
  • 182
  • 182
Yumiko
  • 448
  • 5
  • 16
  • So if `fav_food` is `"cookie"`, then the hash should be `{"cookie"=>0, "pie"=>0, "cake"=>0}`? – August Dec 16 '14 at 23:05

2 Answers2

4

Why do you want a particular key/value pair to be first in a hash? Hashes don't need to be ordered, because you can directly access any element at any time without any extra cost.

If you need to retrieve elements in an order, then get the keys and sort that list, then iterate over that list, or use values_at:

foo = {
  'z' => 1,
  'a' => 2
}

foo_keys = foo.keys.sort # => ["a", "z"]

foo_keys.map{ |k| foo[k] } # => [2, 1]
foo.values_at(*foo_keys) # => [2, 1]

Hashes remember their insertion order, but you shouldn't rely on that; Ordering a hash doesn't help if you insert something later, and other languages don't support it. Instead, order the keys however you want, and use that list to retrieve the values.

If you want to force a key to be first so its value is retrieved first, then consider this:

foo = {
  'z' => 1,
  'a' => 2,
  'w' => 3,
}

foo_keys = foo.keys # => ["z", "a", "w"]
foo_keys.unshift(foo_keys.delete('w')) # => ["w", "z", "a"]

foo_keys.map{ |k| foo[k] } # => [3, 1, 2]
foo.values_at(*foo_keys) # => [3, 1, 2]

If you want a sorted list of keys with one forced to a position:

foo_keys = foo.keys.sort # => ["a", "w", "z"]
foo_keys.unshift(foo_keys.delete('w')) # => ["w", "a", "z"]

foo_keys.map{ |k| foo[k] } # => [3, 2, 1]
foo.values_at(*foo_keys) # => [3, 2, 1]

RE your first paragraph: Hashes are ordered though, specifically because this is such a common requirement and hashes fill so many roles in Ruby. There is no harm relying on hashes being ordered in Ruby, even if other languages don't support this behavior.

Not ordered, as in sorted, instead they remember their insertion order. From the documentation:

Hashes enumerate their values in the order that the corresponding keys were inserted.

This is easily tested/proven:

foo = {z:0, a:-1} # => {:z=>0, :a=>-1}
foo.to_a # => [[:z, 0], [:a, -1]]
foo[:b] = 3
foo.merge!({w:2})
foo # => {:z=>0, :a=>-1, :b=>3, :w=>2}
foo.to_a # => [[:z, 0], [:a, -1], [:b, 3], [:w, 2]]
foo.keys # => [:z, :a, :b, :w]
foo.values # => [0, -1, 3, 2]

If a hash was ordered foo.to_a would be collated somehow, even after adding additional key/value pairs. Instead, it remains in its insertion order. An ordered hash based on keys would move a:-1 to be the first element, just as an ordered hash based on the values would do.

If hashes were ordered, and, if it was important, we'd have some way of telling a hash what its ordering is, ascending or descending or of having some sort of special order based on the keys or values. Instead we have none of those things, and only have the sort and sort_by methods inherited from Enumerable, both of which convert the hash into an array and sort it and return the array, because Arrays can benefit from having an order.

Perhaps you are thinking of Java, which has SortedMap, and provides those sort of capabilities:

A Map that further provides a total ordering on its keys. The map is ordered according to the natural ordering of its keys, or by a Comparator typically provided at sorted map creation time. This order is reflected when iterating over the sorted map's collection views (returned by the entrySet, keySet and values methods). Several additional operations are provided to take advantage of the ordering.

Again, because Ruby's Hash does not sort ordering beyond its insertion order, we have none of those capabilities.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • 2
    RE your first paragraph: Hashes *are* ordered though, specifically because this is such a common requirement and hashes fill so many roles in Ruby. There is no harm relying on hashes being ordered in Ruby, even if other languages don't support this behavior. – user229044 Dec 16 '14 at 23:20
  • Hashes only remember their insertion order, but that has no resemblance to any form of collation. See the added example. – the Tin Man Dec 17 '14 at 18:35
2

You could use Hash#merge:

food_qty = { "pie" => 0, "cake" => 0, "cookie" => 0 }
fav_food = "cookie"

{ fav_food => nil }.merge(food_qty)
# => { "cookie" => 0, "pie" => 0, "cake" => 0 }

This works because Hash#merge first duplicates the original Hash and then, for keys that already exist (like "cookie"), updates the values—which preserves the order of existing keys. In case it's not clear, the above is equivalent to this:

{ "cookie" => nil }.merge("pie" => 0, "cake" => 0, "cookie" => 0)

Edit: The below is my original answer, before I realized that I had also stumbled upon the "real" answer above.

I don't really advocate this (the Tin Man's advice should be taken instead), but if you're using Ruby 2.0+, I present the following Stupid Ruby Trick:

food_qty = { :pie => 0, :cake => 0, :cookie => 0}
fav_food = :cookie

{ fav_food => nil, **food_qty }
# => { :cookie => 0, :pie => 0, :cake => 0 }

This works because Ruby 2.0 added the "double splat" or "keyword splat" operator, as an analogue to the splat in an Array:

arr = [ 1, 2, *[3, 4] ] # => [ 1, 2, 3, 4 ]
hsh = { a: 1, b: 2, **{ c: 3 } } # => { :a => 1, :b => 2, :c => 3 }

...but it appears to do a reverse merge (a la ActiveSupport's Hash#reverse_merge), merging the "outer" hash into the "inner."

{ a: 1, b: 2, **{ a: 3 } }        # => { :a => 1, :b => 2 }
# ...is equivalent to:
{ a: 3 }.merge( { a: 1, b: 2 } )  # => { :a => 1, :b => 2 }

The double splat was implemented to support keyword arguments in Ruby 2.0, which is presumably the reason why it only works if the "inner" Hash's keys are all Symbols.

Like I said, I don't recommend actually doing it, but I find it interesting nonetheless.

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • 1
    Be careful with this Stupid Ruby Trick if you use Ruby versions 2.1.0 through 2.1.2. There was a bug where the hash you double-splatted would be destructively modified: cf. http://stackoverflow.com/questions/23282342/ruby-2-1-bug-double-splat-operator-destructively-modifies-hash – user513951 Dec 16 '14 at 23:43
  • @Jordan, I missed your edit and posted the same `merge` solution, which I subsequently deleted. I suggest you make that front and center in you answer; surely, it's the best way to do it. Alternatively, make your `merge` solution a separate answer. – Cary Swoveland Dec 17 '14 at 03:15
  • 1
    I would suggest a small change: `{ fav_food => "Bananas! But, yes, they have no bananas, they have no bananas today!"}.merge(food_qty)`. It works just as well and is more interesting. – Cary Swoveland Dec 17 '14 at 06:43