I have an array of hashes, something like
[ {:type=>"Meat", :name=>"one"},
{:type=>"Meat", :name=>"two"},
{:type=>"Fruit", :name=>"four"} ]
and I want to convert it to this
{ "Meat" => ["one", "two"], "Fruit" => ["Four"]}
I tried group_by but then i got this
{ "Meat" => [{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}],
"Fruit" => [{:type=>"Fruit", :name=>"four"}] }
and then I can't modify it to leave just the name and not the full hash. I need to do this in one line because is for a grouped_options_for_select on a Rails form.
array.group_by{|h| h[:type]}.each{|_, v| v.replace(v.map{|h| h[:name]})}
# => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
Following steenslag's suggestion:
array.group_by{|h| h[:type]}.each{|_, v| v.map!{|h| h[:name]}}
# => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
In a single iteration over initial array:
arry.inject(Hash.new([])) { |h, a| h[a[:type]] += [a[:name]]; h }
Using ActiveSuport's Hash#transform_values:
array.group_by{ |h| h[:type] }.transform_values{ |hs| hs.map{ |h| h[:name] } }
#=> {"Meat"=>["one", "two"], "Fruit"=>["four"]}
array = [{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}, {:type=>"Fruit", :name=>"four"}]
array.inject({}) {|memo, value| (memo[value[:type]] ||= []) << value[:name]; memo}
I would do as below :
hsh =[{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}, {:type=>"Fruit", :name=>"four"}]
p Hash[hsh.group_by{|h| h[:type] }.map{|k,v| [k,v.map{|h|h[:name]}]}]
# >> {"Meat"=>["one", "two"], "Fruit"=>["four"]}
#ArupRakshit answer, slightly modified (the function has been added for sake of clarity in the final example):
def group(list, by, at)
list.group_by { |h| h[by] }.map { |k,v| [ k , v.map {|h| h[at]} ] }.to_h
end
sample =[
{:type=>"Meat", :name=>"one", :size=>"big" },
{:type=>"Meat", :name=>"two", :size=>"small" },
{:type=>"Fruit", :name=>"four", :size=>"small" }
]
group(sample, :type, :name) # => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
group(sample, :size, :name) # => {"big"=>["one"], "small"=>["two", "four"]}
Please, notice that, although not mentioned in the question, you may want to preserve the original sample as it is. Some answers kept provision on this, others not as.
After grouping (list.group_by {...}) the part that does the transformation (without modifying the original sample's values) is:
.map { |k,v| [ k , v.map {|h| h[at]} ] }.to_h
Some hints:
iterating the pairs of the Hash of groups (first map), where
for each iteration, we receive |group_key, array] and return an Array of [group_key, new_array] (outer block),
and finally to_h transforms the Array of Arrays into the Hash (this [[gk1,arr1],[gk2,arr2]...] into this { gk1 => arr1, gk2 => arr2, ...})
There is one missing step not explained at step (2) above. new_array is made by v.map {|h| h[at]}, which justs casts the value at of each original Hash (h) element of the array (so we move from Array of Hashes to an Array of elements).
Hope that helps others to understand the example.
Related
Given data:
data = [
{"id":14, "sort":1, "content":"9", foo: "2022"},
{"id":14, "sort":4, "content":"5", foo: "2022"},
{"id":14, "sort":2, "content":"1", foo: "2022"},
{"id":14, "sort":3, "content":"0", foo: "2022"},
{"id":15, "sort":4, "content":"4", foo: "2888"},
{"id":15, "sort":2, "content":"1", foo: "2888"},
{"id":15, "sort":1, "content":"3", foo: "2888"},
{"id":15, "sort":3, "content":"3", foo: "2888"},
{"id":16, "sort":1, "content":"8", foo: "3112"},
{"id":16, "sort":3, "content":"4", foo: "3112"},
{"id":16, "sort":2, "content":"4", foo: "3112"},
{"id":16, "sort":4, "content":"9", foo: "3112"}
]
Got the contents concatenated by their sort and ids with:
formatted = data.group_by { |d| d[:id]}.transform_values do |value_array|
value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
end
puts formatted
#=> {14=>"9105", 15=>"3134", 16=>"8449"}
I know that foo exists inside value_array but wondering how can I include foo to exist inside the formatted variable so I can map through it to get the desired output or if it's possible?
Desired Output:
[
{"id":14, "concated_value":"9105", foo: "2022"},
{"id":15, "concated_value":"3134", foo: "2888"},
{"id":16, "concated_value":"8449", foo: "3112"}
]
Since :foo is unique to :id. You can do this as follows:
data.group_by {|h| h[:id]}.map do |_,sa|
sa.map(&:dup).sort_by {|h| h.delete(:sort) }.reduce do |m,h|
m.merge(h) {|key,old,new| key == :content ? old + new : old }
end.tap {|h| h[:concated_value] = h.delete(:content) }
end
#=> [
# {"id":14, foo: "2022", "concated_value":"9105"},
# {"id":15, foo: "2888", "concated_value":"3134"},
# {"id":16, foo: "3112", "concated_value":"8449"}
# ]
First we group by id. group_by {|h| h[:id]}
Then we dup the hashes in the groups (so as not to destory the original). map(&:dup)
Then we sort by sort and delete it at the same time. .sort_by {|h| h.delete(:sort) }
Then we merge the groups together and concatenate the content key only.
m.merge(h) {|key,old,new| key == :content ? old + new : old }
Then we just change the key for content to concated_value tap {|h| h[:concated_value] = h.delete(:content) }
We can use first value from value_array to get our :id & :foo values
formatted = data.group_by { |d| d[:id]}.values.map do |value_array|
concated_value = value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
value_array.first.slice(:id, :foo)
.merge concated_value: concated_value
end
I think this is a good usecase for reduce, since after grouping you need first to get rid of the ID in the resulting [ID, VALUES] array from group_by and just return a reduced version of the VALUES part - this can all be done without any ActiveSupport etc. dependencies:
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.reduce([]) do |a, v| # Reduce it into a new empty array
# Append a new hash to the new array
a << {
id: v[1].first[:id], # Just take the ID of the first entry
foo: v[1].first[:foo], # Dito for foo
concatenated: v[1]
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
}
end
Output:
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
EDIT
I had some other approach in mind when i started to write the previous solution, reduce was not really necessary, since the size of the array after group_by does not change, so a map is sufficient.
But while rewriting the code, i was thinking that creating a new hash with all the keys and copying all the values from the first hash within VALUES was a bit too much work, so it would be easier to just reject the overhead keys:
keys_to_ignore = [:sort, :content]
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.map do |v|
v[1]
.first # Take the first hash from [VALUES]
.merge({'concatenated': v[1] # Insert the concatenated values
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
})
.select { |k, _| !keys_to_ignore.include? k }
end
Output
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
Online demo here
This will work even without Rails:
$irb> formatted = []
$irb> data.sort_by!{|a| a[:sort]}.map {|z| z[:id]}.uniq.each_with_index { |id, index| formatted << {id: id, concated_value: data.map{|c| (c[:id] == id ? c[:content] : nil)}.join, foo: data[index][:foo]}}
$irb> formatted
[{:id=>14, :concated_value=>"9105", :foo=>"2022"},
{:id=>15, :concated_value=>"3134", :foo=>"2888"},
{:id=>16, :concated_value=>"8449", :foo=>"3112"}]
data.sort_by { |h| h[:sort] }.
each_with_object({}) do |g,h| h.update(g[:id]=>{ id: g[:id],
concatenated_value: g[:content].to_s, foo: g[:foo] }) { |_,o,n|
o.merge(concatenated_value: o[:concatenated_value]+n[:concatenated_value]) }
end.values
#=> [{:id=>14, :concatenated_value=>"9105", :foo=>"2022"},
# {:id=>15, :concatenated_value=>"3134", :foo=>"2888"},
# {:id=>16, :concatenated_value=>"8449", :foo=>"3112"}]
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys (here the value of :id) that are present in both hashes being merged. See the doc for the description of the three block variables (here _, o and n).
Note the receiver of values (at the end) is the following.
{ 14=>{ :id=>14, :concatenated_value=>"9105", :foo=>"2022" },
15=>{ :id=>15, :concatenated_value=>"3134", :foo=>"2888" },
16=>{ :id=>16, :concatenated_value=>"8449", :foo=>"3112" } }
Given an array of hashes, I want to create a method that returns a hash where the keys are the unique values of the hashes in the array.
For example, I'd like to take
[
{foo: 'bar', baz: 'bang'},
{foo: 'rab', baz: 'bang'},
{foo: 'bizz', baz: 'buzz'}
]
and return
{
foo: ['bar', 'rab', 'bizz'],
baz: ['bang', 'buzz']
}
I am currently accomplishing this using:
def my_fantastic_method(data)
response_data = { foo: [], baz: []}
data.each { |data|
data.attributes.each { |key, value|
response_data[key.to_sym] << value
}
}
response_data.each { |key, value| response_data[key] = response_data[key].uniq }
response_data
end
Is there a more elegant way of doing this? Thanks!
Your current approach is already pretty good; I don't see much room for improvement. I would write it like this:
def my_fantastic_method(data_list)
data_list.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |data, result|
data.attributes.each do |key, value|
result[key.to_sym] << value
end
end
end
By setting a default value on each hash value, I have eliminated the need to explicitly declare foo: [], bar: [].
By using each_with_object, I have eliminated the need to declare a local variable and explicitly return it at the end.
By using Set, there is no need to call uniq on the final result. This requires less code, and is more performant. However, if you really want the final result to be a mapping to Arrays rather than Sets, then you would need to call to_a on each value at the end of the method.
I have used different variable names for data_list and data. Call these whatever you like, but it's typically considered bad practice to shadow outer variables.
Here are a couple of one-liners. (I'm pretty sure #eiko was being facetious, but I'm proving him correct)
This one reads well and is easy to follow (caveat: requires Ruby 2.4+ for transform_values):
array.flat_map(&:entries).group_by(&:first).transform_values{|v| v.map(&:last).uniq}
Here's another, using the block form of merge to specify an alternate merge method, which in this case is combining the values into a uniq array:
array.reduce{|h, el| h.merge(el){|k, old, new| ([old]+[new]).flatten.uniq}}
You already have a pretty good answer, but I felt golfy and so here is a shorter one:
def the_combiner(a)
hash = {}
a.map(&:to_a).flatten(1).each do |k,v|
hash[k] ||= []
hash[k].push(v)
end
hash
end
Try this:
array.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| {k => v.map(&:last)} }
OR
a.inject({}) {|old_h, new_h|
new_h.each_pair {|k, v|
old_h.key?(k) ? old_h[k] << v : old_h[k]=[v]};
old_h}
If, as in the example, all hashes have the same keys, you could do as follows.
arr = [{ foo: 'bar', baz: 'bang' },
{ foo: 'rab', baz: 'bang' },
{ foo: 'bizz', baz: 'buzz' }]
keys = arr.first.keys
keys.zip(arr.map { |h| h.values_at(*keys) }.transpose.map(&:uniq)).to_h
#=> {:foo=>["bar", "rab", "bizz"], :baz=>["bang", "buzz"]}
The steps are as follows.
keys = arr.first.keys
#=> [:foo, :baz]
a = arr.map { |h| h.values_at(*keys) }
#=> [["bar", "bang"], ["rab", "bang"], ["bizz", "buzz"]]
b = a.transpose
#=> [["bar", "rab", "bizz"], ["bang", "bang", "buzz"]]
c = b.map(&:uniq)
#=> [["bar", "rab", "bizz"], ["bang", "buzz"]]
d = c.to_h
#=> <array of hashes shown above>
Supposing I have an array that looks like:
[
[["str1"],["val1"],["val2"]],
[["str2"], ["val1"], ["val2"], ["val3"]]
]
Is there a way for me to get a Hash that looks like:
{
"str1" => [["val1"],["val2"]],
"str2" => [["val1"],["val2"],["val3"]]
}
a.map { |a| [a.first.first, a.drop(1)] }.to_h
# or
a.each_with_object({}) {|a, h| h[a.first.first] = a.drop(1) }
#=> {
# "str1"=>[["val1"], ["val2"]],
# "str2"=>[["val1"], ["val2"], ["val3"]]
# }
If you do not want to have each element in a separate array:
Hash[a.map(&:flatten).map { |a| [a.first, a.drop(1)] }]
#=> {"str1"=>["val1", "val2"], "str2"=>["val1", "val2", "val3"]}
If your array is arr, you could write the following.
Marshal.load(Marshal.dump(arr)).map { |a| [a.shift.first, a.map(&:first)] }.to_h
#=> {"str1"=>["val1", "val2"],
# "str2"=>["val1", "val2", "val3"]}
A slight variation would be:
Marshal.load(Marshal.dump(arr)).map { |a| [a.shift.first, a] }.to_h
#=> {"str1"=>["val1", "val2"],
# "str2"=>["val1", "val2", "val3"]}
Note: We're using the Marshal class to make an exact copy of arr to avoid modifying the original array. Object#dup or Object#clone won't work here.
arr = [
[["str1"],["val1"],["val2"]],
[["str2"], ["val1"], ["val2"], ["val3"]]
]
arr.each_with_object({}) { |((k), *values), h| h[k]=values }
#=> {"str1"=>[["val1"], ["val2"]], "str2"=>[["val1"], ["val2"], ["val3"]]}
This illustrates how Ruby's use of parallel assignment for determining the values of block variables can be used to advantage.
I have the ruby array as following :
array = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}, {"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]
I want a new array combining the result of ids having same book_id.
Expected Result:
array = [{"book_id"=>14238, "id"=>[8,5]}, {"book_id"=>10743, "id"=>[7,9]}]
I can't say that this is easy to understand, but it is concise:
array.group_by {|item| item["book_id"] }.map do |k, v|
{ "book_id" => k, "id" => v.map {|item| item["id"] } }
end
=> [{"book_id"=>14238, "id"=>[8, 5]}, {"book_id"=>10743, "id"=>[7, 9]}]
The first transformation done by group_by rearranges your array so that items with the same book_id are grouped together:
array.group_by {|item| item["book_id"] }
=> {14238=>[{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}], 10743=>[{"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]}
The second transformation (map) reformats the hash produced by the group_by into a list of hashes, and the second map collects the id's into a list.
You can also do this using the form of Hash#update (a.k.a. merge!) that employs a block to resolve the values of keys that are contained in both of the hashes being merged.
Code
def aggregate(arr)
arr.each_with_object({}) do |g,h|
f = { g["book_id"]=>{ "id"=>[g["id"]], "book_id"=>g["book_id"] } }
h.update(f) do |_,ov,nv|
ov["id"] << nv["id"].first
ov
end
end.values
end
Example
arr = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238},
{"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743},
{"id"=>6, "book_id"=>10511}]
aggregate(arr)
#=> [{"id"=>[8, 5], "book_id"=>14238},
# {"id"=>[7, 9], "book_id"=>10743},
# {"id"=>[6], "book_id"=>10511}]
Alternative output
Depending on your requirements, you might consider building a single hash instead of another array of hashes:
def aggregate(arr)
arr.each_with_object({}) { |g,h|
h.update({ g["book_id"]=>[g["id"]] }) { |_,ov,nv| ov+nv } }
end
aggregate(arr)
#=> {14238=>[8, 5], 10743=>[7, 9], 10511=>[6]}
I'd use a hash for the output for easier lookups and/or reuse:
array = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}, {"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]
hash = array.group_by{ |h| h['book_id'] }.map{ |k, v| [k, v.flat_map{ |h| h['id'] }]}.to_h
# => {14238=>[8, 5], 10743=>[7, 9]}
The keys are the book_id values, and the associated array contains the id values.
The expected result of
array = [{"book_id"=>14238, "id"=>[8,5]}, {"book_id"=>10743, "id"=>[7,9]}]
isn't a good structure if you're going to do any sort of lookups in it. Imagine having hundreds or thousands of elements and needing to find "book_id" == 10743 in the array, especially if it's not a sorted list; The array would have to be walked until the desired entry was found. That is a slow process.
Instead, simplify the structure to a simple hash, allowing you to easily locate a value using a simple Hash lookup:
hash[10743]
The lookup will never slow down.
If the resulting data is to be iterated in order by sorting, use
sorted_keys = hash.keys.sort
and
hash.values_at(*sorted_keys)
to extract the values in the sorted order. Or iterate over the hash if the key/values need to be extracted, perhaps for insertion into a database.
In Ruby or Rails What's the cleanest way to turn this string:
"[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
into an array of hashes like this:
[{one:1, two:2, three:3, four:4},{five:5, six:6}]
Here is a one-liner on two lines:
s.split(/}\s*,\s*{/).
map{|s| Hash[s.scan(/(\w+):(\d+)/).map{|t| proc{|k,v| [k.to_sym, v.to_i]}.call(*t)}]}
NB I was using split(":") to separate keys from values, but #Cary Swoveland's use of parens in the regex is cooler. He forgot the key and value conversions, however.
Or a bit shorter, but uses array indexing instead of the proc, which some may find unappealing:
s.split(/}\s*,\s*{/).
map{|s| Hash[s.scan(/(\w+):(\d+)/).map{|t| [t[0].to_sym, t[1].to_i]}]}
Result:
=> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
Explanation: Start from the end. The last map processes a list of strings of the form "key: value" and returns a list of [:key, value] pairs. The scan processes one string of comma-separated key-value pairs into a list of "key: value" strings. Finally, the initial split separates the brace-enclosed comma-separated strings.
Try this:
"[{one:1, two:2, three:3, four:4},{five:5, six:6}]".
split(/\}[ ]*,[ ]*\{/).
map do |h_str|
Hash[
h_str.split(",").map do |kv|
kv.strip.
gsub(/[\[\]\{\}]/, '').
split(":")
end.map do |k, v|
[k.to_sym, v.to_i]
end
]
end
Not pretty, not optimized, but solves it. (It was fun to do, though :) )
a = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
array = []
a.gsub(/\[|\]/, '').split(/{|}/).map{ |h| h if h.length > 0 && h != ','}.compact.each do |v|
hsh = {}
v.split(',').each do |kv|
arr = kv.split(':')
hsh.merge!({arr.first.split.join.to_sym => arr.last.to_i})
end
array << hsh
end
If you want me to explain it, just ask.
Another approach: Your string looks like a YAML or JSON -definition:
YAML
A slightly modified string works:
require 'yaml'
p YAML.load("[ { one: 1, two: 2, three: 3, four: 4}, { five: 5, six: 6 } ]")
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]
There are two problems:
The keys are strings, no symbols
You need some more spaces (one:1 is not recognized, you need a one: 1).
For problem 1 you need a gsub(/:/, ': ') (I hope there are no other : in your string)
For problem 2 was already a question: Hash[a.map{|(k,v)| [k.to_sym,v]}]
Full example:
require 'yaml'
input = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
input.gsub!(/:/, ': ') #force correct YAML-syntax
p YAML.load(input).map{|a| Hash[a.map{|(k,v)| [k.to_sym,v]}]}
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
JSON
With json you need additonal ", but the symbolization is easier:
require 'json'
input = '[ { "one":1, "two": 2, "three": 3, "four": 4},{ "five": 5, "six": 6} ]'
p JSON.parse(input)
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]
p JSON.parse(input, :symbolize_names => true)
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
Example with original string:
require 'json'
input = "[{one: 1, two: 2, three:3, four:4},{five:5, six:6}]"
input.gsub!(/([[:alpha:]]+):/, '"\1":')
p JSON.parse(input)
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]
p JSON.parse(input, :symbolize_names => true)
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
You could do as below.
Edit: I originally prepared this answer in haste, while on the road, on a borrowed computer with an unfamiliar operating system (Windows). After #sawa pointed out mistakes, I set about fixing it, but became so frustrated with the mechanics of doing so that I gave up and deleted my answer. Now that I'm home again, I have made what I believe are the necessary corrections.
Code
def extract_hashes(str)
str.scan(/\[?{(.+?)\}\]?/)
.map { |arr| Hash[arr.first
.scan(/\s*([a-z]+)\s*:\d*(\d+)/)
.map { |f,s| [f.to_sym, s.to_i] }
]
}
end
Example
str = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
extract_hashes(str)
#=> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
Explanation
For str in the example above,
a = str.scan(/\[?{(.+?)\}\]?/)
#=> [["one:1, two:2, three:3, four:4"], ["five:5, six:6"]]
Enumerable#map first passes the first element of a into the block and assigns it to the block variable:
arr #=> ["one:1, two:2, three:3, four:4"]
Then
b = arr.first
#=> "one:1, two:2, three:3, four:4"
c = b.scan(/\s*([a-z]+)\s*:\d*(\d+)/)
#=> [["one", "1"], ["two", "2"], ["three", "3"], ["four", "4"]]
d = c.map { |f,s| [f.to_sym, s.to_i] }
#=> [[:one, 1], [:two, 2], [:three, 3], [:four, 4]]
e = Hash[d]
#=> {:one=>1, :two=>2, :three=>3, :four=>4}
In Ruby 2.0, Hash[d] can be replaced with d.to_h.
Thus, the first element of a is mapped to e.
Next, the outer map passes the second and last element of a into the block
arr #=> ["five:5, six:6"]
and we obtain:
Hash[arr.first
.scan(/\s*([a-z]+)\s*:\d*(\d+)/)
.map { |f,s| [f.to_sym, s.to_i] }
]
#=> {:five=>5, :six=>6}
which replaces a.last.