Ruby: Converting a nested Ruby hash to an un-nested one

久未见 提交于 2020-01-01 05:40:09

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!