Sort a hash by key containing string format - ruby

I have an array of hashes containing strings like this:
array = [
{name: "Gad", color: "blue"},
{name: "Lad", color: "red"},
{name: "Mad", color: "green"},
{name: "Sad", color: "blue"},
{name: "Sad", color: "green"}
]
I tried:
require 'pp'
array = [
{name: "Gad", color: "blue"},
{name: "Lad", color: "red"},
{name: "Mad", color: "green"},
{name: "Sad", color: "blue"},
{name: "Sad", color: "green"}
]
pp array.partition { |x| x[:name] }
but the result is quiet wrong for me:
[[{:name=>"Gad", :color=>"blue"},
{:name=>"Lad", :color=>"red"},
{:name=>"Mad", :color=>"green"},
{:name=>"Sad", :color=>"blue"},
{:name=>"Sad", :color=>"green"}],
[]]
The expected result is following:
[[{:name=>"Gad", :color=>"blue"}],
[{:name=>"Lad", :color=>"red"}],
[{:name=>"Mad", :color=>"green"}],
[{:name=>"Sad", :color=>"blue"}, {:name=>"Sad", :color=>"green"}]]

a.group_by { |hash| hash[:name] }.values
# [
# [{:name=>"Gad", :color=>"blue"}],
# [{:name=>"Lad", :color=>"red"}],
# [{:name=>"Mad", :color=>"green"}],
# [{:name=>"Sad", :color=>"blue"}, {:name=>"Sad", :color=>"green"}]
# ]
References:
Enumerable#group_by
Hash#values

If I may say so, an array of arrays of hashes with the same name and different colors probably isn't the best format for your data.
It contains redudant information and lookup is slow and verbose.
You could build a single hash with name as key and and an array of colors :
array = [
{ name: 'Gad', color: 'blue' },
{ name: 'Lad', color: 'red' },
{ name: 'Mad', color: 'green' },
{ name: 'Sad', color: 'blue' },
{ name: 'Sad', color: 'green' }
]
colors = Hash.new { |h, k| h[k] = [] }
colors = array.each_with_object(colors) do |hash, colors|
colors[hash[:name]] << hash[:color]
end
p colors
#=> {"Gad"=>["blue"], "Lad"=>["red"], "Mad"=>["green"], "Sad"=>["blue", "green"]}

Using group_by might be better suited to this. For example:
array.group_by { |x| x[:name] }.map(&:last)
# => [[{:name=>"Gad", :color=>"blue"}], [{:name=>"Lad", :color=>"red"}], [{:name=>"Mad", :color=>"green"}], [{:name=>"Sad", :color=>"blue"}, {:name=>"Sad", :color=>"green"}]]

This uses the form of Hash.update (aka merge!) that employs a hash to determine the values of keys that are present in both hashes being merged.
array.each_with_object({}) { |g,h| h.update(g[:name]=>[g]) { |_,o,n| o+n } }.values
#=> [[{:name=>"Gad", :color=>"blue"}],
# [{:name=>"Lad", :color=>"red"}],
# [{:name=>"Mad", :color=>"green"}],
# [{:name=>"Sad", :color=>"blue"}, {:name=>"Sad", :color=>"green"}]]
To create the data structure #Eric suggested, this would be:
array.each_with_object({}) { |g,h| h.update(g[:name]=>[g[:color]]) { |_,o,n| o+n } }
#=> {"Gad"=>["blue"], "Lad"=>["red"], "Mad"=>["green"], "Sad"=>["blue", "green"]}

Related

How to concatenate differents values for one keys

I have a trouble for several days and I need help to solve it.
I have an hash with multiple values for the same key :
{"answers":
[
{"id":1,"value":true},
{"id":3,"value":false},
{"id":2,"value":3},
{"id":1,"value":false},
{"id":2,"value":false},
{"id":2,"value":1}
]
}
I want a method to group all the values for one key, as an exemple :
{
"answers": [
{
"id":1, "value": [true, false]
},
{
"id":3, "value": [false]
},
{
"id":2, "value":[3, false, 1]
}
]
}
I've tried with the reduce method, but I cant find a way to link values to keys.
Anyone can help me with that ?
Thanks!
It looks like you want Enumerable#group_by to regroup the array of hashes by the :id key in each hash.
This method takes the answers array and returns a new, transformed answers array:
def transform_answers(answers)
answers
.group_by { |h| h[:id] }
.each_value { |a| a.map! { |h| h[:value] } }
.map { |id, value| { id: id, value: value } }
end
You can use it like this:
hash = {
answers: [
{ id: 1, value: true },
{ id: 1, value: false },
{ id: 2, value: 3 },
{ id: 2, value: false },
{ id: 2, value: 1 },
{ id: 3, value: false }
]
}
transformed_answers = transform_answers(hash[:answers]) # => [{:id=>1, :value=>[true, false]}, {:id=>2, :value=>[3, false, 1]}, {:id=>3, :value=>[false]}]
You can easily take the transformed answers and put them back into a hash resembling the original input:
transformed_hash = { answers: transformed_answers }
hash = {
answers: [
{ id: 1, value: true },
{ id: 1, value: false },
{ id: 2, value: 3 },
{ id: 2, value: false },
{ id: 2, value: 1 },
{ id: 3, value: false }
]
}
def doit(answers)
answers.each_with_object({}) do |g,h|
h.update(g[:id]=>{ id: g[:id], value: [g[:value]] }) do |_,o,n|
{ id: o[:id], value: o[:value]+n[:value] }
end
end.values
end
{ answers: doit(hash[:answers]) }
#=> {:answers=>[
# {:id=>1, :value=>[true, false]},
# {:id=>2, :value=>[3, false, 1]},
# {:id=>3, :value=>[false]}
# ]
# }
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged. That block is
do |_k,o,n|
{ id: o[:id], value: o[:value]+n[:value] }
end
See the doc for update for definitions of the three block variables, _k, o and n. I've written the first block variable (the common key) _k, rather than k, to signify that it is not used in the block calculation.
Note that before values is executed in doit the method has constructed the following hash.
{1=>{:id=>1, :value=>[true, false]},
2=>{:id=>2, :value=>[3, false, 1]},
3=>{:id=>3, :value=>[false]}}

Group Array of Hashes by Key followed by values

Assuming I have the following dataset
[
{
:name => "sam",
:animal => "dog",
:gender => "male"
}, {
:name => "max",
:animal => "cat",
:gender => "female"
}, {
:name => "joe",
:animal => "snake",
:gender => "male"
}
]
How would you group the array of hashes to:
{
:name => ["sam", "max", "joe"]
:animal => ["dog", "cat", "snake"]
:gender => ["male", "female", "male"]
}
I've read similar articles such as this and Group array of hashes by key
However, most examples return the values as increment counts where I'm looking for actual separate values.
My attempt
keys = []
values = []
arr.each do |a|
a.each do |k, v|
keys << k
#this is where it goes wrong and where I'm stuck at
values << v
end
end
keys = keys.uniq
I understand where I went wrong is how I'm trying to segment the values. Any direction would be appreciated!
input.reduce { |e, acc| acc.merge(e) { |_, e1, e2| [*e2, *e1] } }
#⇒ {:name=>["sam", "max", "joe"],
# :animal=>["dog", "cat", "snake"],
# :gender=>["male", "female", "male"]}
few more approaches
data.each_with_object({}){ |i,r| i.each{ |k,v| (r[k] ||= []) << v } }
data.flat_map(&:to_a).each_with_object({}){ |(k,v), r| (r[k] ||= []) << v }
data.flat_map(&:to_a).group_by(&:first).inject({}){ |r, (k,v)| r[k] = v.map(&:last); r }

How do I convert a Ruby hash so that all of its keys are strings

I have a ruby hash which looks like:
{ id: 123, name: "test" }
I would like to convert it to:
{ "id" => 123, "name" => "test" }
If you are using Rails or ActiveSupport:
hash = { id: 123, description: "desc" }
hash.stringify #=> { "id" => 123, "name" => "test" }
If you are not:
hash = { id: 123, name: "test" }
Hash[hash.map { |key, value| [key.to_s, value] }] #=> { "id" => 123, "name" => "test" }
I love each_with_object in this case :
hash = { id: 123, name: "test" }
hash.each_with_object({}) { |(key, value), h| h[key.to_s] = value }
#=> { "id" => 123, "name" => "test" }
In pure Ruby (without Rails), you can do this with a combination of Enumerable#map and Array#to_h:
hash = { id: 123, name: "test" }
hash.map{|key, v| [key.to_s, v] }.to_h
{ id: 123, name: "test" }.transform_keys(&:to_s)
#=> {"id"=>123, "name"=>"test"}
See Hash#transform_keys.

Grouping hashes together to concat a key's values

I have an array of hashes like the following:
records = [
{ id: "PU525", note: "Foo" },
{ id: "PU525", note: "Bar" },
{ id: "DT525", note: "Hello World" },
{ id: "PU680", note: "Fubar" }
]
The end result should be:
result = [
{ id: "PU525", note: "FooBar" },
{ id: "DT525", note: "Hello World" },
{ id: "PU680", note: "Fubar" }
]
I started playing a little bit with Enumerable#group_by with the following:
results = records.group_by { |record| record[:id] }
# Results in:
# {
# "PU525": [
# { id: "PU525", note: "Foo" },
# { id: "PU525", note: "Bar" }
# ],
# "DT525": { id: "DT525", note: "Hello World" },
# "PU680": { id: "PU680", note: "Fubar" }
# }
The next step would be to use inject to reduce the data down further, but I'm wondering if there's an easier way to reduce down the original array to what I'm looking for as a result without so many steps?
You are nearly done:
records = [
{ id: "PU525", note: "Foo" },
{ id: "PU525", note: "Bar" },
{ id: "DT525", note: "Hello World" },
{ id: "PU680", note: "Fubar" }
]
arr = records.group_by { |record| record[:id] }
.map do |k, v|
[k, v.reduce('') { |memo, h| memo + h[:note] } ]
end
.map do |a|
{ id: a.first, note: a.last }
end
#⇒ [
# { id: "PU525", note: "FooBar" },
# { id: "DT525", note: "Hello World" },
# { id: "PU680", note: "Fubar" }
# ]
UPD After all, you have me entagled with this silly group_by approach. Everything is just easier.
records.inject({}) do |memo, el|
(memo[el[:id]] ||= '') << el[:note]
memo
end.map { |k, v| { id: k, note: v } }
Here is another way, after using #group_by :
records = [
{ id: "PU525", note: "Foo" },
{ id: "PU525", note: "Bar" },
{ id: "DT525", note: "Hello World" },
{ id: "PU680", note: "Fubar" }
]
hsh = records.group_by { |record| record[:id] }.map do |_, v|
v.inject do |h1, h2|
h1.update(h2) { |k, o, n| k == :note ? o << n : n }
end
end
p hsh
# >> [{:id=>"PU525", :note=>"FooBar"}, {:id=>"DT525", :note=>"Hello World"}, {:id=>"PU680", :note=>"Fubar"}]
Here are a couple of variants of the above.
Using Enumerable#group_by
records.group_by { |h| h[:id] }.map { |id, arr|
{ id: id, note: arr.map { |h| h[:note] }.join } }
#=> [{:id=>"PU525", :note=>"FooBar"},
# {:id=>"DT525", :note=>"Hello World"},
# {:id=>"PU680", :note=>"Fubar"}]
Using Hash#update (aka merge!)
records.each_with_object({}) { |g,h| h.update(g[:id]=>g) { |_,og,ng|
{ id: g[:id], note: og[:note]+ng[:note] } } }.values
#=>[{:id=>"PU525", :note=>"FooBar"},
# {:id=>"DT525", :note=>"Hello World"},
# {:id=>"PU680", :note=>"Fubar"}]

Filtering Ruby array of hashes by another hash

Perhaps I'm missing something obvious. It seems tricky to filter a hash by another hash or multiple key/value pairs.
fruit = [
{ name: "apple", color: "red", pieable: true },
{ name: "orange", color: "orange", pieable: false },
{ name: "grape", color: "purple", pieable: false },
{ name: "cherry", color: "red", pieable: true },
{ name: "banana", color: "yellow", pieable: true },
{ name: "tomato", color: "red", pieable: false }
]
filter = { color: "red", pieable: true }
# some awesome one-liner? would return
[
{ name: "apple", color: "red", pieable: true },
{ name: "cherry", color: "red", pieable: true }
]
The array of hashes I don't think is the problem. I don't even know how to test a hash by another arbitrary hash. I'm using Rails so anything out of active_support etc is fine.
COULD be made into a one liner. But multi-line is cleaner.
fruit.select do |hash| # or use select!
filter.all? do |key, value|
value == hash[key]
end
end
If you allow two lines it could also be made into an efficient "one-liner" like so:
keys, values = filter.to_a.transpose
fruit.select { |f| f.values_at(*keys) == values }
Not the most efficient (you could just use an array form of filter to avoid repeated conversions), but:
fruit.select {|f| (filter.to_a - f.to_a).empty? }
I'd be inclined to use Enumerable#group_by for this:
fruit.group_by { |g| { color: g[:color], pieable: g[:pieable] } }[filter]
#=> [{:name=>"apple", :color=>"red", :pieable=>true},
# {:name=>"cherry", :color=>"red", :pieable=>true}]
Tony Arcieri (#bascule) gave this really nice solution on twitter.
require 'active_support/core_ext' # unneeded if you are in a rails app
fruit.select { |hash| hash.slice(*filter.keys) == filter }
And it works.
# [{:name=>"apple", :color=>"red", :pieable=>true},
# {:name=>"cherry", :color=>"red", :pieable=>true}]
Try this
fruit.select{ |hash| (filter.to_a & hash.to_a) == filter.to_a }
=> [{:name=>"apple", :color=>"red", :pieable=>true},
{:name=>"cherry", :color=>"red", :pieable=>true}]

Resources