简体   繁体   中英

Ruby refactoring: converting array to hash

Here's what I get in Rails params:

obj => {
    "raw_data" =>
        [
            { "id" => "1", "name" => "John Doe" },
            { "id" => "2", "name" => "Jane Doe" }
        ]
}

I have to transform into a following object:

obj => {
    "data" =>
        {
            "1" => { "name" => "John Doe" },
            "2" => { "name" => "Jane Doe" }
        }
}

Here's the code I have working so far:

if obj[:raw_data]
    obj[:data] = Hash.new
    obj[:raw_data].each do |raw|
      obj[:data][raw[:id]] = Hash.new
      obj[:data][raw[:id]][:name] = raw[:name] if raw[:name].present?
    end
end
obj.delete(:raw_data)

Is there a way to refactor it? Maybe using map . Note that data structure has to change from array to hash as well.

Thanks for any tips.

Here's one way:

obj = {
  "raw_data" => [
    { "id" => "1", "name" => "John Doe" },
    { "id" => "2", "name" => "Jane Doe" }
  ]
}

data = obj["raw_data"].map do |item|
  item = item.dup
  [ item.delete('id'), item ]
end

obj2 = { "data" => data.to_h }
# => { "data" =>
#      { "1" => { "name" => "John Doe" },
#        "2" => { "name" => "Jane Doe" }
#      }
#    }

If you're using Rails you can use the Hash#except method from ActiveSupport to make it a little more succinct:

data = obj["raw_data"].map {|item| [ item["id"], item.except("id") ] }
obj2 = { "data" => data.to_h }
d = obj[:raw_data]
keys = d.map { |h| h["id"] }
values = d.map { |h| h.except("id") }
Hash[ keys.zip(values) ]

# or as a oneliner
Hash[ d.map { |h| h["id"] }.zip(d.map { |h| h.except("id")) ]
# => {"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}

This special Hash[] syntax lets you create a hash from a array of keys and an array of values.

Hash.except(*args) is an ActiveSupport addition to the hash class which returns a new key without the keys in the blacklist.

In rails, you can use index_by method:

obj = {raw_data: [{id: "1", name: "John Doe"}, {id: "2", name: "Jane Doe"}]}

obj2 = {
 data: obj[:raw_data].index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}

One downfall of this is that it will modify the original data by deleting id property. If this is unacceptable, here is modified, safe version:

obj2 = {
 data: obj[:raw_data].map(&:clone).index_by {|h| h[:id]}.each {|_,h| h.delete(:id)}
} #=> {:data=>{"1"=>{:name=>"John Doe"}, "2"=>{:name=>"Jane Doe"}}}

I assume you mean obj = {...} and not obj => {...} , as the latter is not a valid object. If so:

{ "data" => obj["raw_data"].each_with_object({}) { |g,h|
  h[g["id"]] = g.reject { |k,_| k == "id" } } }
  #=> {"data"=>{"1"=>{"name"=>"John Doe"}, "2"=>{"name"=>"Jane Doe"}}}

If obj can be mutated, you can simplify a bit:

{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g.delete("id")]=g } }

As an improved non-mutating solution, @Max suggested a Rails' tweak:

{ "data" => obj["raw_data"].each_with_object({}) { |g,h| h[g["id"]] = g.except("id") } }

That looks good to me, but as I don't know rails, I'm taking that advice at face value.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM