问题
Right now, I have a server call kicking back the following Ruby hash:
{
"id"=>"-ct",
"factualId"=>"",
"outOfBusiness"=>false,
"publishedAt"=>"2012-03-09 11:02:01",
"general"=>{
"name"=>"A Cote",
"timeZone"=>"EST",
"desc"=>"À Côté is a small-plates restaurant in Oakland's charming
Rockridge district. Cozy tables surround large communal tables in both
the main dining room and on the sunny patio to create a festive atmosphere.
Small plates reflecting the best of seasonal Mediterranean cuisine are served
family-style by a friendly and knowledgeable staff.\nMenu items are paired with
a carefully chosen selection of over 40 wines by the glass as well as a highly
diverse bottled wine menu. Specialty drinks featuring fresh fruits, rare
botaniques and fine liqueurs are featured at the bar.",
"website"=>"http://acoterestaurant.com/"
},
"location"=>{
"address1"=>"5478 College Ave",
"address2"=>"",
"city"=>"Oakland",
"region"=>"CA",
"country"=>"US",
"postcode"=>"94618",
"longitude"=>37.84235,
"latitude"=>-122.25222
},
"phones"=>{
"main"=>"510-655-6469",
"fax"=>nil
},
"hours"=>{
"mon"=>{"start"=>"", "end"=>""},
"tue"=>{"start"=>"", "end"=>""},
"wed"=>{"start"=>"", "end"=>""},
"thu"=>{"start"=>"", "end"=>""},
"fri"=>{"start"=>"", "end"=>""},
"sat"=>{"start"=>"", "end"=>""},
"sun"=>{"start"=>"", "end"=>""},
"holidaySchedule"=>""
},
"businessType"=>"Restaurant"
}
It's got several attributes which are nested, such as:
"wed"=>{"start"=>"", "end"=>""}
I need to convert this object into a unnested hash in Ruby. Ideally, I'd like to detect if an attribute is nested, and respond accordingly, I.E. when it determines the attribute 'wed' is nested, it pulls out its data and stores in the fields 'wed-start' and 'wed-end', or something similar.
Anyone have any tips on how to get started?
回答1:
Here's a first cut at a complete solution. I'm sure you can write it more elegantly, but this seems fairly clear. If you save this in a Ruby file and run it, you'll get the output I show below.
class Hash
def unnest
new_hash = {}
each do |key,val|
if val.is_a?(Hash)
new_hash.merge!(val.prefix_keys("#{key}-"))
else
new_hash[key] = val
end
end
new_hash
end
def prefix_keys(prefix)
Hash[map{|key,val| [prefix + key, val]}].unnest
end
end
p ({"a" => 2, "f" => 5}).unnest
p ({"a" => {"b" => 3}, "f" => 5}).unnest
p ({"a" => {"b" => {"c" => 4}, "f" => 5}}).unnest
Output:
{"a"=>2, "f"=>5}
{"a-b"=>3, "f"=>5}
{"a-b-c"=>4, "a-f"=>5}
回答2:
EDIT: the sparsify gem was released as a general solution to this problem.
Here's an implementation I worked up a couple months ago. You'll need to parse the JSON into a hash, then use Sparsify to sparse the hash.
# Extend into a hash to provide sparse and unsparse methods.
#
# {'foo'=>{'bar'=>'bingo'}}.sparse #=> {'foo.bar'=>'bingo'}
# {'foo.bar'=>'bingo'}.unsparse => {'foo'=>{'bar'=>'bingo'}}
#
module Sparsify
def sparse(options={})
self.map do |k,v|
prefix = (options.fetch(:prefix,[])+[k])
next Sparsify::sparse( v, options.merge(:prefix => prefix ) ) if v.is_a? Hash
{ prefix.join(options.fetch( :separator, '.') ) => v}
end.reduce(:merge) || Hash.new
end
def sparse!
self.replace(sparse)
end
def unsparse(options={})
ret = Hash.new
sparse.each do |k,v|
current = ret
key = k.to_s.split( options.fetch( :separator, '.') )
current = (current[key.shift] ||= Hash.new) until (key.size<=1)
current[key.first] = v
end
return ret
end
def unsparse!(options={})
self.replace(unsparse)
end
def self.sparse(hsh,options={})
hsh.dup.extend(self).sparse(options)
end
def self.unsparse(hsh,options={})
hsh.dup.extend(self).unsparse(options)
end
def self.extended(base)
raise ArgumentError, "<#{base.inspect}> must be a Hash" unless base.is_a? Hash
end
end
usage:
external_data = JSON.decode( external_json )
flattened = Sparsify.sparse( external_data, :separator => '-' )
This was originally created because we were working with storing a set of things in Mongo, which allowed us to use sparse keys (dot-separated) on updates to update some contents of a nested hash without overwriting unrelated keys.
回答3:
One more option:
class Hash
def smash(prefix = nil)
inject({}) do |acc, (k, v)|
key = prefix.to_s + k
if Hash === v
acc.merge(v.smash(key + '-'))
else
acc.merge(key => v)
end
end
end
end
hash = {
'f' => 100,
'z' => {'j' => 25},
'a' => {'b' => {'c' => 1}}
}
puts hash.smash # => {"f"=>100, "z-j"=>25, "a-b-c"=>1}
回答4:
Another way to tackle this is not to flatten the hash, but to access it as though it were flattened. For example, given this hash:
h = {
'a' => 1,
'b' => {
'c' => 2,
'd' => 3,
},
}
then this function:
NESTED_KEY_SEPARATOR = '-'
NESTED_KEY_REGEX = /^(.*?)(?:#{NESTED_KEY_SEPARATOR}(.*))?$/
def nested_fetch(key, hash)
return hash if key.empty?
first_part_of_key, rest_of_key = NESTED_KEY_REGEX.match(key).captures
value = hash[first_part_of_key]
if value.is_a?(Hash)
nested_hash_fetch(value, rest_of_key || '')
elsif rest_of_key
nil
else
value
end
end
Will let you retrieve nested hash elements by concatenating the individual hash keys together with KEY_SEPARATOR (set to dash here, but could be any character that never appears as a key in the hash you need to search):
p nested_fetch('a', h) # => 1
p nested_fetch('b-c', h) # => 2
If you give a partially qualified key, you get the hash that matched at that point:
p nested_fetch('b', h) # => {"c"=>2, "d"=>3}
And if you give a key that doesn't exist, you get nil:
p nested_fetch('b-x', h) # => nil
This could be monkey-patched onto Hash, if desired, by simply enclosing the above code in class Hash, and by giving self as the default to argument hash:
class Hash
NESTED_KEY_SEPARATOR = '-'
NESTED_KEY_REGEX = /^(.*?)(?:#{KEY_SEPARATOR}(.*))?$/
def nested_fetch(key, hash = self)
...
end
来源:https://stackoverflow.com/questions/12064648/ruby-converting-a-nested-ruby-hash-to-an-un-nested-one