I was looking around for a clean way to do this and I found some workarounds but did not find anything like the slice (some people recommended to use a gem but I think is not needed for this operations, pls correct me if I am wrong), so I found myself with a hash that contains a bunch of hashes and I wanted a way to perform the Slice operation over this hash and get also the key/value pairs from nested hashes, so the question:
Is there something like deep_slice in ruby?
Example:
input: a = {b: 45, c: {d: 55, e: { f: 12}}, g: {z: 90}}, keys = [:b, :f, :z]
expected output: {:b=>45, :f=>12, :z=>90}
Thx in advance! 👍
After looking around for a while I decided to implement this myself, this is how I fix it:
a = {b: 45, c: {d: 55, e: { f: 12}}, g: {z: 90}}
keys = [:b, :f, :z]
def custom_deep_slice(a:, keys:)
result = a.slice(*keys)
a.keys.each do |k|
if a[k].class == Hash
result.merge! custom_deep_slice(a: a[k], keys: keys)
end
end
result
end
c_deep_slice = custom_deep_slice(a: a, keys: keys)
p c_deep_slice
The code above is a classic DFS, which takes advantage of the merge! provided by the hash class.
You can test the code above here
require 'set'
def recurse(h, keys)
h.each_with_object([]) do |(k,v),arr|
if keys.include?(k)
arr << [k,v]
elsif v.is_a?(Hash)
arr.concat(recurse(v,keys))
end
end
end
hash = { b: 45, c: { d: 55, e: { f: 12 } }, g: { b: 21, z: 90 } }
keys = [:b, :f, :z]
arr = recurse(hash, keys.to_set)
#=> [[:b, 45], [:f, 12], [:b, 21], [:z, 90]]
Notice that hash differs slightly from the example hash given in the question. I added a second nested key :b to illustrate the problem of returning a hash rather than an array of key-value pairs. Were we to convert arr to a hash the pair [:b, 45] would be discarded:
arr.to_h
#=> {:b=>21, :f=>12, :z=>90}
If desired, however, one could write:
arr.each_with_object({}) { |(k,v),h| (h[k] ||= []) << v }
#=> {:b=>[45, 21], :f=>[12], :z=>[90]}
I converted keys from an array to a set merely to speed lookups (keys.include?(k)).
A slightly modified approach could be used if the hash contained nested arrays of hashes as well as nested hashes.
My version
maybe it should help
def deep_slice( obj, *args )
deep_arg = {}
slice_args = []
args.each do |arg|
if arg.is_a? Hash
arg.each do |hash|
key, value = hash
if obj[key].is_a? Hash
deep_arg[key] = deep_slice( obj[key], *value )
elsif obj[key].is_a? Array
deep_arg[key] = obj[key].map{ |arr_el| deep_slice( arr_el, *value) }
end
end
elsif arg.is_a? Symbol
slice_args << arg
end
end
obj.slice(*slice_args).merge(deep_arg)
end
Object to slice
obj = {
"id": 135,
"kind": "transfer",
"customer": {
"id": 1,
"name": "Admin",
},
"array": [
{
"id": 123,
"name": "TEST",
"more_deep": {
"prop": "first",
"prop2": "second"
}
},
{
"id": 222,
"name": "2222"
}
]
}
Schema to slice
deep_slice(
obj,
:id,
customer: [
:name
],
array: [
:name,
more_deep: [
:prop2
]
]
)
Result
{
:id=>135,
:customer=>{
:name=>"Admin"
},
:array=>[
{
:name=>"TEST",
:more_deep=>{
:prop2=>"second"
}
},
{
:name=>"2222"
}
]
}
Related
Im trying to to create a hash with one key per each type of extension on a directory. To every key I would like to add two values: number of times that extension is repeated and total size of all the files with that extension.
Something similar to this:
{".md" => {"ext_reps" => 6, "ext_size_sum" => 2350}, ".txt" => {"ext_reps" => 3, "ext_size_sum" => 1300}}
But I´m stuck on this step:
hash = Hash.new{|hsh,key| hsh[key] = {}}
ext_reps = 0
ext_size_sum = 0
Dir.glob("/home/computer/Desktop/**/*.*").each do |file|
hash[File.extname(file)].store "ext_reps", ext_reps
hash[File.extname(file)].store "ext_size_sum", ext_size_sum
end
p hash
With this result:
{".md" => {"ext_reps" => 0, "ext_size_sum" => 0}, ".txt" => {"ext_reps" => 0, "ext_size_sum" => 0}}
And I can't finde the way to increment ext_reps and ext_siz_sum
Thanks
Suppose the file name extensions and files sizes drawn are as follows.
files = [{ ext: 'a', size: 10 },
{ ext: 'b', size: 20 },
{ ext: 'a', size: 30 },
{ ext: 'c', size: 40 },
{ ext: 'b', size: 50 },
{ ext: 'a', size: 60 }]
You can use Hash#group_by and Hash#transform_values.
files.group_by { |h| h[:ext] }.
transform_values do |arr|
{ "ext_reps"=>arr.size, "ext_size_sum"=>arr.sum { |h| h[:size] } }
end
#=> {"a"=>{"ext_reps"=>3, "ext_size_sum"=>100},
# "b"=>{"ext_reps"=>2, "ext_size_sum"=>70},
# "c"=>{"ext_reps"=>1, "ext_size_sum"=>40}}
Note that the first calculation is as follows.
files.group_by { |h| h[:ext] }
#=> {"a"=>[{:ext=>"a", :size=>10}, {:ext=>"a", :size=>30},
# {:ext=>"a", :size=>60}],
# "b"=>[{:ext=>"b", :size=>20}, {:ext=>"b", :size=>50}],
# "c"=>[{:ext=>"c", :size=>40}]}
Another way is use the forms of Hash#update (aka Hash#merge!) and Hash#merge that employ a block to compute the values of keys that are present in both hashes being merged. (Ruby does not consult that block when a key-value pair with key k is being merged into the hash being built (h) when h does not have a key k.)
See the docs for an explanation of the three parameters of the block that returns the values of common keys of hashes being merged.
files.each_with_object({}) do |g,h|
h.update(g[:ext]=>{"ext_reps"=>1, "ext_size_sum"=>g[:size]}) do |_k,o,n|
o.merge(n) { |_kk, oo, nn| oo + nn }
end
end
#=> {"a"=>{"ext_reps"=>3, "ext_size_sum"=>100},
# "b"=>{"ext_reps"=>2, "ext_size_sum"=>70},
# "c"=>{"ext_reps"=>1, "ext_size_sum"=>40}}
I've chosen names for the common keys of the "outer" and "inner" hashes (_k and _kk, respectively) that begin with an underscore to signal to the reader that they are not used in the block calculation. This is common practive.
Note that this approach avoids the creation of a temporary hash similar to that created by group_by and therefore tends to use less memory than the first approach.
Here is a solution inspired by the answers given by Cary Swoveland and BenFenner
hash = {}
Dir.glob("/home/computer/Desktop/**/*.*").each do |file|
(hash[File.extname(file)] ||= []) << file.size
end
hash.transform_values! { |sizes| { "ext_reps" => sizes.count, "ext_size_sum" => sizes.sum } }
With each_with_object and nested Hash.new
files = [{ ext: 'a', size: 10 },
{ ext: 'b', size: 20 },
{ ext: 'a', size: 30 },
{ ext: 'c', size: 40 },
{ ext: 'b', size: 50 },
{ ext: 'a', size: 60 }]
files.each_with_object(Hash.new(Hash.new(0))) do |el, hash|
h = hash[el[:ext]]
hash[el[:ext]] =
{ "ext_reps" => h["ext_reps"] + 1, "ext_size_sum" => h["ext_size_sum"] + el[:size] }
end
#=> {"a"=>{"ext_reps"=>3, "ext_size_sum"=>100},
# "b"=>{"ext_reps"=>2, "ext_size_sum"=>70},
# "c"=>{"ext_reps"=>1, "ext_size_sum"=>40}}
It's not the most "Ruby-like" solution, but going along with your provided example this is probably what you'd ultimately end up with as a solution. Your main problem was that you were never incrementing the ext_reps value, nor were you ever accumulating the ext_size_sum value.
hash = {}
Dir.glob('/home/computer/Desktop/**/*.*').each do |file|
file_extension = File.extname(file)
if hash[file_extension].nil?
# This is the first time this file extension has been seen, so initialize things for it.
hash[file_extension] = {}
hash[file_extension]['ext_reps'] = 0
hash[file_extension]['ext_size_sum'] = 0
end
# Increment/accumulate values.
hash[file_extension]['ext_reps'] += 1
hash[file_extension]['ext_size_sum'] += file.size
end
This is pretty much a reiteration of Cary's and others' answers without temporary variables. (Which is more Ruby-like IMHO.)
Dir.glob("*.*")
.group_by { |f| File.extname(f) }
.transform_values do |files|
{
"count" => files.count,
"size" => files.sum { |f| File.size(f) }
}
end
=> {".app"=>{"count"=>1, "size"=>96},
".sh-builder"=>{"count"=>1, "size"=>192},
".sh-names"=>{"count"=>1, "size"=>288},
".json"=>{"count"=>2, "size"=>5362},
".rb"=>{"count"=>1, "size"=>132}}
I want to dynamically create a Hash without overwriting keys from an array of arrays. Each array has a string that contains the nested key that should be created. However, I am running into the issue where I am overwriting keys and thus only the last key is there
data = {}
values = [
["income:concessions", 0, "noi", "722300", "purpose", "refinancing"],
["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]
What it should look like:
{
"income" => {
"concessions" => 0,
"gross-income" => "900000"
},
"expenses" => {
"admin" => "7500",
"other" => "0"
}
"noi" => "722300",
"purpose" => "refinancing",
"fees" => {
"fee-one" => 0,
"fee-two" => 0
},
"address" => {
"zip" => "10019"
}
}
This is the code that I currently, have how can I avoid overwriting keys when I merge?
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
a = data.merge!(hh)
end
end
end
The code you've provided can be modified to merge hashes on conflict instead of overwriting:
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
data.merge!(hh) { |_, old, new| old.merge(new) }
end
end
end
But this code only works for the two levels of nesting.
By the way, I noted ruby-on-rails tag on the question. There's deep_merge method that can fix the problem:
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
data.deep_merge!(hh)
end
end
end
values.flatten.each_slice(2).with_object({}) do |(f,v),h|
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
end
#=> {"income"=>{"concessions"=>0, "gross-income"=>"900000"},
# "noi"=>"722300",
# "purpose"=>"refinancing",
# "fees"=>{"fee-one"=>"0", "fee-two"=>"0"},
# "expenses"=>{"admin"=>"7500", "other"=>"0"},
# "address"=>{"zip"=>"10019"}}
The steps are as follows.
values = [
["income:concessions", 0, "noi", "722300", "purpose", "refinancing"],
["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]
a = values.flatten
#=> ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
# "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
# "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
enum1 = a.each_slice(2)
#=> #<Enumerator: ["income:concessions", 0, "noi", "722300",
# "purpose", "refinancing", "fees:fee-one", "0", "income:gross-income", "900000",
# "expenses:admin", "7500", "fees:fee-two", "0", "address:zip", "10019",
# "expenses:other","0"]:each_slice(2)>
We can see what values this enumerator will generate by converting it to an array.
enum1.to_a
#=> [["income:concessions", 0], ["noi", "722300"], ["purpose", "refinancing"],
# ["fees:fee-one", "0"], ["income:gross-income", "900000"],
# ["expenses:admin", "7500"], ["fees:fee-two", "0"],
# ["address:zip", "10019"], ["expenses:other", "0"]]
Continuing,
enum2 = enum1.with_object({})
#=> #<Enumerator: #<Enumerator:
# ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
# "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
# "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
# :each_slice(2)>:with_object({})>
enum2.to_a
#=> [[["income:concessions", 0], {}], [["noi", "722300"], {}],
# [["purpose", "refinancing"], {}], [["fees:fee-one", "0"], {}],
# [["income:gross-income", "900000"], {}], [["expenses:admin", "7500"], {}],
# [["fees:fee-two", "0"], {}], [["address:zip", "10019"], {}],
# [["expenses:other", "0"], {}]]
enum2 can be thought of as a compound enumerator (though Ruby has no such concept). The hash being generated is initially empty, as shown, but will be filled in as additional elements are generated by enum2
The first value is generated by enum2 and passed to the block, and the block values are assigned values by a process called array decomposition.
(f,v),h = enum2.next
#=> [["income:concessions", 0], {}]
f #=> "income:concessions"
v #=> 0
h #=> {}
We now perform the block calculation.
f.is_a?(String)
#=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
#=> ["income", "concessions"]
e.nil?
#=> false
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
#=> {"concessions"=>0}
h[k] equals nil if h does not have a key k. In that case (h[k] || {}) #=> {}. If h does have a key k (and h[k] in not nil).(h[k] || {}) #=> h[k].
A second value is now generated by enum2 and passed to the block.
(f,v),h = enum2.next
#=> [["noi", "722300"], {"income"=>{"concessions"=>0}}]
f #=> "noi"
v #=> "722300"
h #=> {"income"=>{"concessions"=>0}}
Notice that the hash, h, has been updated. Recall it will be returned by the block after all elements of enum2 have been generated. We now perform the block calculation.
f.is_a?(String)
#=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
#=> ["noi"]
e #=> nil
e.nil?
#=> true
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
#=> "722300"
h #=> {"income"=>{"concessions"=>0}, "noi"=>"722300"}
The remaining calculations are similar.
merge overwrites a duplicate key by default.
{ "income"=> { "concessions" => 0 } }.merge({ "income"=> { "gross-income" => "900000" } } completely overwrites the original value of "income". What you want is a recursive merge, where instead of just merging the top level hash you're merging the nested values when there's duplication.
merge takes a block where you can specify what to do in the event of duplication. From the documentation:
merge!(other_hash){|key, oldval, newval| block} → hsh
Adds the contents of other_hash to hsh. If no block is specified, entries with duplicate keys are overwritten with the values from other_hash, otherwise the value of each duplicate key is determined by calling the block with the key, its value in hsh and its value in other_hash
Using this you can define a simple recursive_merge in one line
def recursive_merge!(hash, other)
hash.merge!(other) { |_key, old_val, new_val| recursive_merge!(old_val, new_val) }
end
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
a = recursive_merge!(data, hh)
end
end
end
A few more lines will give you a more robust solution, that will overwrite duplicate keys that are not hashes and even take a block just like merge
def recursive_merge!(hash, other, &block)
hash.merge!(other) do |_key, old_val, new_val|
if [old_val, new_val].all? { |v| v.is_a?(Hash) }
recursive_merge!(old_val, new_val, &block)
elsif block_given?
block.call(_key, old_val, new_val)
else
new_val
end
end
end
h1 = { a: true, b: { c: [1, 2, 3] } }
h2 = { a: false, b: { x: [3, 4, 5] } }
recursive_merge!(h1, h2) { |_k, o, _n| o } # => { a: true, b: { c: [1, 2, 3], x: [3, 4, 5] } }
Note: This method reproduces the results you would get from ActiveSupport's Hash#deep_merge if you're using Rails.
This is how I would handle this:
def new_h
Hash.new{|h,k| h[k] = new_h}
end
values.flatten.each_slice(2).each_with_object(new_h) do |(k,v),obj|
keys = k.is_a?(String) ? k.split(':') : [k]
if keys.count > 1
set_key = keys.pop
obj.merge!(keys.inject(new_h) {|memo,k1| memo[k1] = new_h})
.dig(*keys)
.merge!({set_key => v})
else
obj[k] = v
end
end
#=> {"income"=>{
"concessions"=>0,
"gross-income"=>"900000"},
"noi"=>"722300",
"purpose"=>"refinancing",
"fees"=>{
"fee-one"=>"0",
"fee-two"=>"0"},
"expenses"=>{
"admin"=>"7500",
"other"=>"0"},
"address"=>{
"zip"=>"10019"}
}
Explanation:
Define a method (new_h) for setting up a new Hash with default new_h at any level (Hash.new{|h,k| h[k] = new_h})
First flatten the Array (values.flatten)
then group each 2 elements together as sudo key value pairs (.each_slice(2))
then iterate over the pairs using an accumulator where each new element added is defaulted to a Hash (.each_with_object(new_h.call) do |(k,v),obj|)
split the sudo key on a colon (keys = k.is_a?(String) ? k.split(':') : [k])
if there is a split then create the parent key(s) (obj.merge!(keys.inject(new_h.call) {|memo,k1| memo[k1] = new_h.call}))
merge the last child key equal to the value (obj.dig(*keys.merge!({set_key => v}))
other wise set the single key equal to the value (obj[k] = v)
This has infinite depth as long as the depth chain is not broken say [["income:concessions:other",12],["income:concessions", 0]] in this case the latter value will take precedence (Note: this applies to all the answers in one way or anther e.g. the accepted answer the former wins but a value is still lost dues to inaccurate data structure)
repl.it Example
I have a hash, say,
account = {
name: "XXX",
email: "xxx#yyy.com",
details: {
phone: "9999999999",
dob: "00-00-00",
address: "zzz"
}
}
Now I want to convert account to a hash like this:
account = {
name: "XXX",
email: "xxx#yyy.com",
phone: "9999999999",
dob: "00-00-00",
address: "zzz"
}
I'm a beginner and would like to know if there is any function to do it? (Other than merging the nested hash and then deleting it)
You could implement a generic flatten_hash method which works roughly similar to Array#flatten in that it allows to flatten Hashes of arbitrary depth.
def flatten_hash(hash, &block)
hash.dup.tap do |result|
hash.each_pair do |key, value|
next unless value.is_a?(Hash)
flattened = flatten_hash(result.delete(key), &block)
result.merge!(flattened, &block)
end
end
end
Here, we are still performing the delete / merge sequence, but it would be required in any such implementation anyway, even if hidden below further abstractions.
You can use this method as follows:
account = {
name: "XXX",
email: "xxx#yyy.com",
details: {
phone: "9999999999",
dob: "00-00-00",
address: "zzz"
}
}
flatten(account)
# => {:name=>"XXX", :email=>"xxx#yyy.com", :phone=>"9999999999", :dob=>"00-00-00", :address=>"zzz"}
Note that with this method, any keys in lower-level hashes overwrite existing keys in upper-level hashes by default. You can however provide a block to resolve any merge conflicts. Please refer to the documentation of Hash#merge! to learn how to use this.
This will do the trick:
account.map{|k,v| k==:details ? v : {k => v}}.reduce({}, :merge)
Case 1: Each value of account may be a hash whose values are not hashes
account.flat_map { |k,v| v.is_a?(Hash) ? v.to_a : [[k,v]] }.to_h
#=> {:name=>"XXX", :email=>"xxx#yyy.com", :phone=>"9999999999",
# :dob=>"00-00-00", :address=>"zzz"}
Case 2: account may have nested hashes
def doit(account)
recurse(account.to_a).to_h
end
def recurse(arr)
arr.each_with_object([]) { |(k,v),a|
a.concat(v.is_a?(Hash) ? recurse(v.to_a) : [[k,v]]) }
end
account = {
name: "XXX",
email: "xxx#yyy.com",
details: {
phone: "9999999999",
dob: { a: 1, b: { c: 2, e: { f: 3 } } },
address: "zzz"
}
}
doit account
#=> {:name=>"XXX", :email=>"xxx#yyy.com", :phone=>"9999999999", :a=>1,
# :c=>2, :f=>3, :address=>"zzz"}
Explanation for Case 1
The calculations progress as follows.
One way to think of Enumerable#flat_map, as it is used here, is that if, for some method g,
[a, b, c].map { |e| g(e) } #=> [f, g, h]
where a, b, c, f, g and h are all arrays, then
[a, b, c].flat_map { |e| g(e) } #=> [*f, *g, *h]
Let's start by creating an enumerator to pass elements to the block.
enum = account.to_enum
#=> #<Enumerator: {:name=>"XXX", :email=>"xxx#yyy.com",
# :details=>{:phone=>"9999999999", :dob=>"00-00-00",
# :address=>"zzz"}}:each>
enum generates an element which is passed to the block and the block variables are set equal to those values.
k, v = enum.next
#=> [:name, "XXX"]
k #=> :name
v #=> "XXX"
v.is_a?(Hash)
#=> false
a = [[k,v]]
#=> [[:name, "XXX"]]
k, v = enum.next
#=> [:email, "xxx#yyy.com"]
v.is_a?(Hash)
#=> false
b = [[k,v]]
#=> [[:email, "xxx#yyy.com"]]
k,v = enum.next
#=> [:details, {:phone=>"9999999999", :dob=>"00-00-00", :address=>"zzz"}]
v.is_a?(Hash)
#=> true
c = v.to_a
#=> [[:phone, "9999999999"], [:dob, "00-00-00"], [:address, "zzz"]]
d = account.flat_map { |k,v| v.is_a?(Hash) ? v.to_a : [[k,v]] }
#=> [*a, *b, *c]
#=> [[:name, "XXX"], [:email, "xxx#yyy.com"], [:phone, "9999999999"],
# [:dob, "00-00-00"], [:address, "zzz"]]
d.to_h
#=> <the return value shown above>
I am returning a response of user fields in JSON. I am creating JSON as below.
def user_response(users)
users_array = []
users.each do |user|
uhash = {}
uhash[:id] = user.id,
uhash[:nickname] = user.nickname,
uhash[:online_sharing] = user.online_sharing,
uhash[:offline_export] = user.offline_export,
uhash[:created_at] = user.created_at,
uhash[:app_opens_count] = user.app_opens_count,
uhash[:last_activity] = user.last_activity,
uhash[:activity_goal] = user.activity_goal,
uhash[:last_activity] = user.last_activity,
uhash[:region] = user.region
users_array << uhash
end
users_array
end
But the response is pretty weird. The :id key in hash has an array of all the fields don't know why.
{
"nickname": "adidas",
"online_sharing": null,
"offline_export": null,
"created_at": "2016-08-26T09:03:54.000Z",
"app_opens_count": 29,
"last_activity": "2016-08-26T09:13:01.000Z",
"activity_goal": 3,
"region": "US",
"id": [
9635,
"adidas",
null,
null,
"2016-08-26T09:03:54.000Z",
29,
"2016-08-26T09:13:01.000Z",
3,
"2016-08-26T09:13:01.000Z",
"US"
]
}
That's due to your , at the end of each line
The problem consists of two things:
An assignment evaluates as the value being assigned:
puts (foo = 42) # => prints 42
Multiple values, separated with comma on the right hand side of an assignment form an array:
bar = 1, 2, 3
bar # => [1, 2, 3]
The new lines don't change that, so you basically do something like this:
sonne = (foo = :eins), (bar = :zwei), (baz = :drei), (qux = :vier)
sonne # => [:eins, :zwei, :drei, :vier]
The fix is indeed to remove the commas.
You have comma , at the end of each line
uhash[:id] = user.id,
Also, You may change the above code to:
def user_response(users)
users.map do |user|
user.attributes.slice(:id, :nickname, :online_sharing, :offline_export, :created_at, :app_opens_count, :last_activity, :activity_goal, :last_activity, :region)
end
end
I have got
#my_objects = [ #<MyObject id: 1, title: "Blah1">,
#<MyObject id: 2, title: "Blah2">,
#<MyObject id: 3, title: "Blah3">,
#<MyObject id: 4, title: "Blah4"> ]
I need to turn it into:
#my_objects = { :id => [ 1, 2, 3, 4],
:title => [ "Blah1" ... ] }
Is there built in method or some standart approach?
I can imagine only this
#my_objects.inject({}){ |h, c| c.attributes.each{ |k,v| h[k] ||= []; h[k] << v }; h }
This question was born while I was thinking on this particular question
First, use Enumerable#map (something like #o.map { |e| [e.id, e.title] }) to get the ActiveRecord array into a simplified pure Ruby object that looks like this:
a = [[1, "Blah1"], [2, "Blah2"], [3, "Blah3"], [4, "Blah4"]]
Then:
a.transpose.zip([:id, :title]).inject({}) { |m, (v,k)| m[k] = v; m }
Alternate solution: It might be less tricky and easier to read if instead you just did something prosaic like:
i, t = a.transpose
{ :id => i, :title => t }
Either way you get:
=> {:title=>["Blah1", "Blah2", "Blah3", "Blah4"], :id=>[1, 2, 3, 4]}
Update: Tokland has a refinement that's worth citing:
Hash[[:id, :title].zip(a.transpose)]
You're on the right track there, there's no custom method for this sort of pivot, and it should work, but remember that ActiveRecord attribute keys are strings:
#my_objects.inject({ }) { |h, c| c.attributes.each { |k,v| (h[k.to_sym] ||= [ ]) << v }; h }
You can use the (x ||= [ ]) << y pattern to simplify that a bit if you're not too concerned with it being super readable to a novice.
Functional approach (no eachs!):
pairs = #my_objects.map { |obj| obj.attributes.to_a }.flatten(1)
Hash[pairs.group_by(&:first).map { |k, vs| [k, vs.map(&:second)] }]
#=> {:title=>["Blah1", "Blah2", "Blah3", "Blah4"], :id=>[1, 2, 3, 4]}
As usual, Facets allows to write nicer code; in this case Enumerable#map_by would avoid using the ugly and convoluted pattern group_by+map+map:
#my_objects.map { |obj| obj.attributes.to_a }.flatten(1).map_by { |k, v| [k, v] }
#=> {:title=>["Blah1", "Blah2", "Blah3", "Blah4"], :id=>[1, 2, 3, 4]}