How do I fetch multiple hash values at once?
Ruby on-RailsRubyHashDestructuringRuby on-Rails Problem Overview
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.
Ruby on-Rails Solutions
Solution 1 - Ruby on-Rails
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.
Solution 2 - Ruby on-Rails
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
Solution 3 - Ruby on-Rails
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
Solution 4 - Ruby on-Rails
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
Solution 5 - Ruby on-Rails
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
Solution 6 - Ruby on-Rails
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.
Solution 7 - Ruby on-Rails
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