Frequency of pairs in an array ruby - ruby

I have an array of pairs like this:
arr = [
{lat: 44.456, lng: 33.222},
{lat: 42.456, lng: 31.222},
{lat: 44.456, lng: 33.222},
{lat: 44.456, lng: 33.222},
{lat: 42.456, lng: 31.222}
]
There are some geographical coordinates of some places. I want to get an array with these coordinates grouped and sorted by frequency. The result should look like this:
[
{h: {lat: 44.456, lng: 33.222}, fr: 3},
{h: {lat: 42.456, lng: 31.222}, fr: 2},
]
How can I do this?

The standard ways of approaching this problem are to use Enumerable#group_by or a counting hash. As others have posted answers using the former, I'll go with the latter.
arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }.map { |k,v| { h: k, fr: v } }
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]
First, count instances of the hashes:
counts = arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }
#=> {{:lat=>44.456, :lng=>33.222}=>3,
# {:lat=>42.456, :lng=>31.222}=>2}
Then construct the array of hashes:
counts.map { |k,v| { h: k, fr: v } }
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]
g = Hash.new(0) creates an empty hash with a default value of zero. That means that if g does not have a key k, g[k] returns zero. (The hash is not altered.) g[k] += 1 is first expanded to g[k] = g[k] + 1. If g does not have a key k, g[k] on the right side returns zero, so the expression becomes:
g[k] = 1.
Alternatively, you could write:
counts = arr.each_with_object({}) { |f,g| g[f] = (g[f] ||= 0) + 1 }
If you want the elements (hashes) of the array returned to be in decreasing order of the value of :fr (here it's coincidental), tack on Enumerable#sort_by:
arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }.
map { |k,v| { h: k, fr: v } }.
sort_by { |h| -h[:fr] }

arr.group_by(&:itself).map{|k, v| {h: k, fr: v.length}}.sort_by{|h| h[:fr]}.reverse
# =>
# [
# {:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}
# ]

arr.group_by{|i| i.hash}.map{|k, v| {h: v[0], fr: v.size}
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3}, {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]

Related

How to sum arrays based on the first field

I have 3 array of hashes:
a = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
b = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
c = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
and I have to merge them into one, based on the name prop of each identifier, so the result will be:
d = [{name: 'Identifier', value: 1500 }, {name: 'Identifier2', value: 150}]
Is there a smart ruby way of doing this, or do I have to do create another hash where the keys are the identifiers, the values the values and then transform it into an array?
Thank you.
When the values of a single key in a collection of hashes are to be totaled I usually begin by constructing a counting hash:
h = (a+b+c).each_with_object({}) do |g,h|
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
end
#=> {"Identifier"=>1500, "Identifier2"=>150}
Note that if h does not have a key g[:name], h[g[:name]] #=> nil, so:
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
= (nil || 0) + g[:value]
= 0 + g[:value]
= g[:value]
We may now easily obtain the desired result:
h.map { |(name,value)| { name: name, value: value } }
#=> [{:name=>"Identifier", :value=>1500},
# {:name=>"Identifier2", :value=>150}]
If desired these two expressions can be chained:
(a+b+c).each_with_object({}) do |g,h|
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
end.map { |(name,value)| { name: name, value: value } }
#=> [{:name=>"Identifier", :value=>1500},
# {:name=>"Identifier2", :value=>150}]
Sometimes you might see:
h[k1] = (h[k1] || 0) + g[k2]
written:
(h[k1] ||= 0) + g[k2]
which expands to the same thing.
Another way to calculate h, which I would say is more "Ruby-like", is the following.
h = (a+b+c).each_with_object(Hash.new(0)) do |g,h|
h[g[:name]] += g[:value]
end
This creates the hash represented by the block variable h using the form of Hash::new that takes an argument called the default value:
h = Hash.new(0)
All this means is that if h does not have a key k, h[k] returns the default value, here 0. Note that
h[g[:name]] += g[:value]
expands to:
h[g[:name]] = h[g[:name]] + g[:value]
so if h does not have a key g[:name] this reduces to:
h[g[:name]] = 0 + g[:value]
If you were wondering why h[g[:name]] on the left of the equality was not replaced by 0, it is because that part of the expression employs the method Hash#[]=, whereas the method Hash#[] is used on he right. Hash::new with a default value only concerns Hash#[].
You can do everything in ruby !
Here is a solution to your problem :
d = (a+b+c).group_by { |e| e[:name] }.map { |f| f[1][0].merge(value: f[1].sum { |g| g[:value] }) }
I encourage you to check the Array Ruby doc for more information: https://ruby-doc.org/core-2.7.0/Array.html
I am assuming that the order of identifiers in all arrays is the same. That is {name: 'Identifier', value: ...} always the first element in all 3 arrays, {name: 'Identifier2', value: ... } always the second, etc. In this simple case, a simple each_with_index is a simple and clear solution:
d = []
a.each_with_index do |hash, idx|
d[idx] = {name: hash[:name], value: a[idx][:value] + b[idx][:value] + c[idx][:value] }
end
# Or a more clear version using map:
a.each_with_index do |hash, idx|
d[idx] = {name: hash[:name], value: [a, b, c].map { |h| h[idx][:value] }.sum }
end
A couple different ways, avoiding any finicky array-indexing and the like, (also functionally, since you've added the tag):
grouped = (a + b + c).group_by { _1[:name] }
name_sums = grouped.transform_values { |hashes| hashes.map { _1[:value] }.sum }
name_vals = (a + b + c).map { Hash[*_1.values_at(:name, :value)] }
name_sums = name_vals.reduce { |l, r| l.merge(r) { |k, lval, rval| lval + rval } }
in either case, finish it off with:
name_sums.map { |name, value| { name: name, value: value } }

Add previous value to each hash value

I have a hash with integer values:
h = {
a: 1,
b: 1,
c: 1,
d: 2,
e: 2,
}
I need to add 100 to the first value, and for the second value and on, I need to add the preceding value to the original value to get:
{
a: 101,
b: 102,
c: 103,
d: 105,
e: 107,
}
Is there a good way to achieve this?
You could use inject to calculate the total sum:
h = { a: 1, b: 1, c: 1, d: 2, e: 2}
h.inject(100) { |s, (k, v)| s + v }
#=> 107
And while doing so, you can also set the hash values to get an accumulated sum:
h.inject(100) { |s, (k, v)| h[k] = s + v }
h #=> {:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}
Immutable solution that does not modify the input:
h.each_with_object({sum: 100, outcome: {}}) do |(k, v), acc|
acc[:outcome][k] = acc[:sum] += v
end
#⇒ {:sum=>107, :outcome=>{:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}}
You can just keep track of the sum as an external variable:
sum = 100
h.transform_values{|v| sum += v} # => {:a=>101, :b=>102, :c=>103, :d=>105, :e=>107}
Maybe this is not the most efficient solution, but it is definitely nice and readable.
accumulated_sum = 0
h.each do |key, value|
accumulated_sum += value
hash[key] = 100 + accumulated_sum
end

easier and shorter way to sum values in a hash of hashes in ruby

Having a hash of hashes like this:
d = {a: {c: 1, d:3 }, b: {c: 2, d: 6}, ...}
What is the easier way to sum all values in c:?
d.values.map { |val| val[:c] }.reduce(&:+)
To explain:
d.values
=> [{:c=>1, :d=>3}, {:c=>2, :d=>6}]
d.values.map { |val| val[:c] }
=> [1, 2]
From this point you can use reduce(&:+) to get the sum, or if you're using rails (or have required active support), you can use Array#sum
reduce(&:+), by the way, is a shorthand for reduce { |memo, val| memo + val }
Just go over the hashes and sum their :c values.
d.values.sum { |h| h[:c] }
=> 3
Even shorter (from Sagar Pandya's comment):
d.sum { |_, v| v[:c] }
=> 3
If you wish to permit nested hashes of arbitrary depth you can use the following recursive method.
def sum_cees(h)
h.sum { |k,v| v.is_a?(Hash) ? sum_cees(v) : k == :c ? v : 0 }
end
sum_cees({ a: { c: 1, d:3 }, b: { d: { m: { c: 2, e: 6 } }, f: { c: 3} },
g: { c: 4 }, n: { r: 3 } })
#=> 10

ruby select inner hash from nested hash

I need to filter a nested hash to return items for a particular combination of attributes. If the attribute is present it returns that hash, if the attribute is not present it returns the default. If the attribute is set to 'none' it returns nothing. Consider the following hash:
{
"size"=>{
"default"=>{
"jeans"=>"boyfriend"
},
"blue"=>"none"
},
"style"=>{
"default"=>{
"shoes"=>"boots"
},
"blue"=>{
"jeans"=>"jeggings"
}
}
}
if the color is 'black', then
{
"size"=>{
"jeans"=>"boyfriend"
},
"style"=>{
"shoes"=>"boots"
}
}
or if the color is 'blue', then
{
"size"=>{
},
"style"=>{
"jeans"=>"jeggings"
}
}
What is best way to do this? I have tried various combinations of select and delete but either end up with an array or a hash with the color key included.
Letting h be the hash given in the question, the following method will return the desired hash if my understanding of the question is correct.
def doit(h, color)
h.each_with_object({}) do |(k,f),g|
c,v = f.find { |kk,_| kk != "default" }
if c == color
g[k] = v.is_a?(Hash) ? v : {}
else
g[k] = f["default"]
end
end
end
doit(h, 'black')
#=> {"size"=>{"jeans"=>"boyfriend"}, "style"=>{"shoes"=>"boots"}}
doit(h, 'blue')
#=> {"size"=>{}, "style"=>{"jeans"=>"jeggings"}}
The steps for the second example are as follows.
color = 'blue'
enum = h.each_with_object({})
#=> #<Enumerator: {"size"=>{"default"=>{"jeans"=>"boyfriend"},
# "blue"=>"none"}, "style"=>{"default"=>{"shoes"=>"boots"},
# "blue"=>{"jeans"=>"jeggings"}}}:each_with_object({})>
The first value of this enumerator is generated:
x = enum.next
#=> [["size", {"default"=>{"jeans"=>"boyfriend"}, "blue"=>"none"}], {}]
and passed to the block. The block variables are set equal to x and their values are determined by "disambiguation":
(k,f),g = x
k #=> "size"
f ##=> {"default"=>{"jeans"=>"boyfriend"}, "blue"=>"none"}
g #=> {}
The block calculation is now performed.
c,v = f.find { |kk,_| kk != "default" }
#=> ["blue", "none"]
c #=> "blue"
v #=> "none"
As
c == color
#=> "blue" == "blue" => true
we compute
v.is_a?(Hash)
#=> false
and therefore perform the assignment
g[k] = {}
#=> {}
so that now
g #=> {"size"=>{}}
The second and last element of h is now generated and passed to the block.
x = enum.next
#=> [["style", {"default"=>{"shoes"=>"boots"},
# "blue"=>{"jeans"=>"jeggings"}}], {"style"=>{"jeans"=>"jeggings"}}]
(k,f),g = x
k #=> "style"
f #=> {"default"=>{"shoes"=>"boots"}, "blue"=>{"jeans"=>"jeggings"}}
g #=> {"size"=>"none"}
c,v = f.find { |kk,_| kk != "default" }
#=> ["blue", {"jeans"=>"jeggings"}]
c #=> "blue"
v #=> {"jeans"=>"jeggings"}
c == color
# "blue" == "blue" => true
v.is_a?(Hash)
#=> true
g[k] = v
#=> {"jeans"=>"jeggings"}
g #=> {"size"=>"none", "style"=>{"jeans"=>"jeggings"}}
and g is returned.
Below is what I ended up with after some refactoring. It works and tests all pass. Could do with more refactoring.
class Filterer
def self.filter(facets, color)
acc = {}
facets.each do |k, facets|
facets.each do |_, facet|
acc[k] = color_facets(color, facets)
end
end
acc
end
def self.color_facets(color, facets)
return {} if no_facets?(color, facets)
facets[color] ? facets[color] : facets['default']
end
def self.no_facets?(color, facets)
facets[color] && facets[color] == 'no facet'
end
end

How to sum values in an array with different hash

I want to sum the total values of the same items in an array.
I have an array as
[{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
I want to get the result as
[{"a"=>3},{"b"=>6},{"c"=>3}]
Which method can do it?
if:
array = [{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
then you can do:
array.inject(Hash.new{|h,k| h[k] = 0})
{ |h, a| k, v = a.flatten; h[k] += v; h }.
map{|arr| Hash[*arr] }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
or:
array.each_with_object(Hash.new{|h,k| h[k] = 0})
{ |a, h| k, v = a.flatten; h[k] += v }.
map{|arr| Hash[*arr] }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
It can be done as follows
array.group_by { |h| h.keys.first }.
values.
map {|x| x.reduce({}) { |h1, h2| h1.merge(h2) { |_, o, n| o + n } }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
Every time you want to transform a collection in not a one-to-one way, it's job for #reduce. For one-to-one transformations we use #map.
array.reduce({}) { |h, acc| acc.merge(h) {|_k, o, n| o+n } }.zip.map(&:to_h)
# => [{"b"=>6}, {"a"=>3}, {"c"=>3}]
Here we use reduce with the initial value {}, which is passed to the block as the acc parameter, and then we use #merge with manual "conflicts resolution". It means that the block is called only when the key we're trying to merge is already present in the method receiver, acc. After that we break the hash into an array of hashes.
There are many ways to do this. It is instructive to see a few, even some that may be unusual and/or not especially efficient.
Here is another way:
arr = [{"a"=>1},{"b"=>2},{"c"=>3},{"a"=>2},{"b"=>4}]
arr.flat_map(&:keys)
.uniq
.map { |k| { k=>arr.reduce(0) { |t,g| t + (g.key?(k) ? g[k] : 0) } } }
#=> [{"a"=>3}, {"b"=>6}, {"c"=>3}]
Since nil.to_i => 0, we could instead write reduce's block as:
{ |t,g| t+g[k].to_i }

Resources