71

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.

Dmytrii Nagirniak
  • 23,696
  • 13
  • 75
  • 130
  • One important and unasked question is. What for do you want to reassign values from hash to vars? – Mike Szyndel Jul 15 '13 at 11:05
  • @MichaelSzyndel I cannot parse your comment above. – sawa Jul 15 '13 at 12:01
  • 1
    Why do you want to do `from = hash.fetch(:from); to = hash.fetch(:to);`... instead of using `hash[:from]`? – Mike Szyndel Jul 15 '13 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`. – matthew.tuck Apr 05 '17 at 03:04

8 Answers8

123

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.

Marcelo De Polli
  • 28,123
  • 4
  • 37
  • 47
Dylan Markow
  • 123,080
  • 26
  • 284
  • 201
  • 10
    This does not satisfy the OP's requirement. – sawa Jul 15 '13 at 04:52
  • @MichaelSzyndel Read the question by yourself. – sawa Jul 15 '13 at 07:41
  • 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. – Mike Szyndel Jul 15 '13 at 07:44
  • 3
    @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. – Dmytrii Nagirniak Jul 15 '13 at 08:26
  • yes, but why not just use `raise MyException if hsh_values.include? nil` – Yo Ludke Mar 26 '15 at 08:58
  • @YoLudke, because in cases where the key doesn't exist at all that wouldn't do anything. – Altonymous Feb 12 '16 at 18:50
52

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
matthew.tuck
  • 1,267
  • 9
  • 11
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
sawa
  • 165,429
  • 45
  • 277
  • 381
2
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
vgoff
  • 10,980
  • 3
  • 38
  • 56
  • 1
    ` my_array.any? {|element| element.nil?} && raise` would be incorrect because a key may exist with a `nil` value. – Dmytrii Nagirniak Jul 15 '13 at 08:23
  • You are correct. I checked the value rather than the key. I will edit for key. – vgoff Jul 15 '13 at 08:33
  • `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. – Dmytrii Nagirniak Jul 16 '13 at 04:02
  • 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. – vgoff Jul 16 '13 at 04:13
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
Alistair A. Israel
  • 6,417
  • 1
  • 31
  • 40
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.

saihgala
  • 5,724
  • 3
  • 34
  • 31
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

Jacquen
  • 986
  • 8
  • 18
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
Dorian
  • 7,749
  • 4
  • 38
  • 57