73

What is a shorter version of this?:

from = hash.fetch(:from) to = hash.fetch(:to) name = hash.fetch(:name) # etc 

Note the fetch, I want to raise an error if the key doesn't exist.

There must be shorter version of it, like:

from, to, name = hash.fetch(:from, :to, :name) # <-- imaginary won't work 

It is OK to use ActiveSupport if required.

4
  • One important and unasked question is. What for do you want to reassign values from hash to vars? Commented Jul 15, 2013 at 11:05
  • @MichaelSzyndel I cannot parse your comment above. Commented Jul 15, 2013 at 12:01
  • 1
    Why do you want to do from = hash.fetch(:from); to = hash.fetch(:to);... instead of using hash[:from]? Commented Jul 15, 2013 at 12:17
  • This is a general question that has too different use cases to mention, just as there sometimes a need for avoiding silent failure by using fetch instead of [], there is sometimes a need for using a fetch version of values_at. Commented Apr 5, 2017 at 3:04

8 Answers 8

127

Use Hash's values_at method:

from, to, name = hash.values_at(:from, :to, :name) 

This will return nil for any keys that don't exist in the hash.

Sign up to request clarification or add additional context in comments.

6 Comments

This does not satisfy the OP's requirement.
@MichaelSzyndel Read the question by yourself.
There's no such method that will fetch multiple keys and railse exceptions for missing ones. One of the reasons may be - when an exception should be raised - when key is nonexistent (it may leave unassigned variables for other keys) or after fetching all keys? It's easy to add such check by yourself and I can't really see any reason why insist on raising exception here.
@MichaelSzyndel "why insist on raising exception here" - because the keys MUST be present. "when an exception should be raised - when key is nonexistent (it may leave unassigned variables for other keys)" - yes, that would leave unassigned variable in case of a normal fetch too.
yes, but why not just use raise MyException if hsh_values.include? nil
|
55

Ruby 2.3 finally introduces the fetch_values method for hashes that straightforwardly achieves this:

{a: 1, b: 2}.fetch_values(:a, :b) # => [1, 2] {a: 1, b: 2}.fetch_values(:a, :c) # => KeyError: key not found: :c 

1 Comment

So it does exists now, awesome 🤯 This should be marked as the answer if possible because it's not the easiest to find.
5
hash = {from: :foo, to: :bar, name: :buz} [:from, :to, :name].map{|sym| hash.fetch(sym)} # => [:foo, :bar, :buz] [:frog, :to, :name].map{|sym| hash.fetch(sym)} # => KeyError 

Comments

3
my_array = {from: 'Jamaica', to: 'St. Martin'}.values_at(:from, :to, :name) my_array.keys.any? {|key| element.nil?} && raise || my_array 

This will raise an error like you requested

 my_array = {from: 'Jamaica', to: 'St. Martin', name: 'George'}.values_at(:from, :to, :name) my_array.keys.any? {|key| element.nil?} && raise || my_array 

This will return the array.

But OP asked for failing on a missing key...

class MissingKeyError < StandardError end my_hash = {from: 'Jamaica', to: 'St. Martin', name: 'George'} my_array = my_hash.values_at(:from, :to, :name) my_hash.keys.to_a == [:from, :to, :name] or raise MissingKeyError my_hash = {from: 'Jamaica', to: 'St. Martin'} my_array = my_hash.values_at(:from, :to, :name) my_hash.keys.to_a == [:from, :to, :name] or raise MissingKeyError 

4 Comments

` my_array.any? {|element| element.nil?} && raise` would be incorrect because a key may exist with a nil value.
You are correct. I checked the value rather than the key. I will edit for key.
keys.to_a == [:from, :to, :name] is incorrect. The order will matter here, but the Hash keys have no particular order it should be at least keys.to_a - [:from, :to, :name] == []. Overall, it's just more effort than it's worth.
And if you do the subtraction and the key simply isn't there you still end up with an empty array, so that is not good either. So matching an empty array will not mean that the key was necessarily there. Missed it.
1

You could initialise your hash with a default value of KeyError object. This will return an instance of KeyError if the key you are trying to fetch is not present. All you need to do then is check its (value's) class and raise it if its a KeyError.

hash = Hash.new(KeyError.new("key not found")) 

Let's add some data to this hash

hash[:a], hash[:b], hash[:c] = "Foo", "Bar", nil 

Finally look at the values and raise an error if key not found

hash.values_at(:a,:b,:c,:d).each {|v| raise v if v.class == KeyError} 

This will raise an exception if and only if key is not present. It'll not complain in case you have a key with nil value.

1 Comment

I don't control how the Hash is created. So I'll have to recreate/copy just to use a "special" value as a marker of no-key.
1

The simplest thing I would go for would be

from, to, name = [:from, :to, :name].map {|key| hash.fetch(key)} 

Alternatively, if you want to use values_at, you can use a Hash with a default value block:

hash=Hash.new {|h, k| raise KeyError.new("key not found: #{k.inspect}") } # ... populate hash from, to, name = hash.values_at(:from, :to, :name) # raises KeyError on missing key 

Or, if you're so inclined, monkey-patch Hash

class ::Hash def fetch_all(*args) args.map {|key| fetch(key)} end end from, to, name = hash.fetch_all :from, :to, :name 

2 Comments

How is this different from mine?
@sawa obviously, it assigns the results LOL :)
1

Pattern matching is an experimental feature in Ruby 2.7.

hash = { from: 'me', to: 'you', name: 'experimental ruby' } hash in { from:, to:, name: } 

See https://rubyreferences.github.io/rubyref/language/pattern-matching.html

Comments

0

I needed to have a hash being returned so I did a patch:

class Hash def multi_fetch(*keys) keys.map { |key| [key, fetch(key)] }.to_h end end 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.