问题
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