I have an ActiveRecord relation that looks something like this:
[
{
timestamp: Tue, 02 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
organization_id: 1,
all_sales: 10,
direct_sales: 7,
referred_sales: 3,
},
{
timestamp: Wed, 03 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
organization_id: 1,
all_sales: 17,
direct_sales: 8,
referred_sales: 9,
},
{
timestamp: Thu, 04 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
all_sales: 3,
direct_sales: 3,
referred_sales: 0,
}
]
What I'd like to do is create a "sum" of all the keys pertaining to sales (for our purposes here, I don't need timestamp, user_id or organization_id, so basically, I'd like to end with something like this:
{
all_sales: 30
direct_sales: 18
referred_sales: 12
}
Is there an elegant ruby-ish way of doing this? I could easily create a set of variables for each sales category and augment them as I iterate over the original relation, but I wanted to see if the community had a cleaner method. In reality each of these hashes have far more than 3 relevant keys and so I fear that approach will get messy very quickly.
Edit: I also have checked out some other answers to similar questions here on SO (for example: Better way to sum values in an array of hashes), but ideally I wouldn't iterate so many times.
This will work:
arr.each_with_object({}) do |obj, hash|
%i[all_sales direct_sales referred_sales].each do |sym|
hash[sym] = hash[sym].to_i + obj[sym]
end
end
It's one iteration, you can write the nested loop as 3 different lines, but it's a bit cleaner this way in my opinion.
Note: calling to_i while getting previous value of hash[sym] as initially it is nil and nil.to_i == 0. Alternatively, you can initialize all unknown counts with 0, like this:
arr.each_with_object(Hash.new(0)) do |obj, hash|
%i[all_sales direct_sales referred_sales].each do |sym|
hash[sym] += obj[sym]
end
end
Since you're starting with an ActiveRecord Relation, you can use pluck to calculate all the sums with SQL and have it return an array with your totals:
SalesModel.pluck('SUM(all_sales)', 'SUM(direct_sales)', 'SUM(referred_sales)')
#=> [30, 18, 12]
Or use functional approach with reduce and merge methods:
keys = %i{all_sales direct_sales referred_sales}
total_sales = items.map {|item| item.select{|key, _| keys.include?(key)}}
.reduce({}) {|all, item| all.merge(item) {|_, sum, value| sum + value}}
# total_sales
# => {:all_sales=>30, :direct_sales=>18, :referred_sales=>12}
Or little bid clearer approach for Ruby 2.5.0 or higher, thanks to #Johan Wentholt
items.map {|item| item.slice(:all_sales, :direct_sales, :referred_sales)}
.reduce({}) {|all, item| all.merge(item) {|_, sum, value| sum + value}}
# => {:all_sales=>30, :direct_sales=>18, :referred_sales=>12}
Use Merge and Reduce function
value = arr.reduce do |h1, h2|
h1.merge(h2) do |k, v1, v2|
[:all_sales, :direct_sales, :referred_sales].include?(k) ? (v1 + v2) : nil
end
end.reject {|_, v| v.nil?}
p value
A couple of more verbose other options (that can be rendered more DRY and general):
result = { all_sales: (array.sum{ |e| e[:all_sales] }), direct_sales: (array.sum{ |e| e[:direct_sales] }), referred_sales: (array.sum{ |e| e[:referred_sales] }) }
or:
result = array.each.with_object(Hash.new(0)) do |h, obj|
obj[:all_sales] += h[:all_sales]
obj[:direct_sales] += h[:direct_sales]
obj[:referred_sales] += h[:referred_sales]
end
To be more DRY and general, starting with the array of the required keys
keys = [:all_sales, :direct_sales, :referred_sales]
The first becomes
keys.map.with_object(Hash.new) { |k, obj| obj[k] = array.sum { |e| e[k] } }
and the second:
array.each.with_object(Hash.new(0)) { |h, obj| keys.each { |k| obj[k] += h[k] } }
Related
I only want to update the date element in a hash.
i.e.
hash = { 'name': 'Albert', 'date_1': "31-01-2017", 'date_2': "31-01-2017" }
## I only want to update `date_1` and `date_2`.
I have tried
hash.select {|k,v| ['date_1','date_2'].include? k }.transform_values!(&:to_date)
but because I do a select before, it will only return me
{ 'date_1': Tue, 31 Jan 2017, 'date_2': Tue, 31 Jan 2017 }
Any suggestion to do keep other attributes as well as transform the selected key?
i.e. { 'name': 'Albert', 'date_1': Tue, 31 Jan 2017, 'date_2': Tue, 31 Jan 2017 }
Just update a value for date_1 and date_2:
hash['date_1'] = hash['date_1'].to_date if hash['date_1']
hash['date_2'] = hash['date_2'].to_date if hash['date_2']
or you can do it with each if you have more a date keys:
date_keys = [:date_1, :date_2]
hash.each do |k,v|
hash[k] = v.to_date if date_keys.include?(k)
end
If you want something a bit more dynamic, without having to hardcode date_1 and date_2 :
require 'date'
hash = { name: 'Albert', date_1: "31-01-2017", date_2: "31-01-2017" }
hash.each do |key, value|
if key.to_s =~ /^date_/ && value.is_a?(String)
hash[key] = Date.strptime(value, '%d-%m-%Y')
end
end
p hash
#=> {:name=>"Albert", :date_1=>#<Date: 2017-01-31 ((2457785j,0s,0n),+0s,2299161j)>, :date_2=>#<Date: 2017-01-31 ((2457785j,0s,0n),+0s,2299161j)>}
Note that for your code :
hash.select {|k,v| ['date_1','date_2'].include? k }.transform_values!(&:to_date)
transform_values! tries to modify a Hash in place which is only short-lived (the new hash returned by select). transform_values would achieve the exact same result.
Quick dynamic way
hash = { 'name' => 'Albert', 'date_1' => "31-01-2017", 'date_2' => "31-01-2017" }
hash.map { |k,v| hash[k] = k['date_'] ? v.to_date : v }
Will work on any key that contains date_, like date_02, date_1234, date_lastmonth, etc
My goal is to take a hash of names and numbers, for example:
hash = {
"Matt" => 30,
"Dave" => 50,
"Alex" => 60
}
and to group them by whether they achieved a "passing" score. I'd like the results to be passed as an array into two separate keys, say :pass and :fail like this:
hash = { "pass" => ["Alex", 60], "fail" => [["Matt", 30]["Dave",60]]}
I know the group_by method is what I need, but am not sure as to how I would pass the values into the new keys.
The passing grade should be decided by the user. For this example, you could use 45.
You can do somthing this in such way:
PASSING_GRADE = 45
hash.group_by {|_, v| v >= PASSING_GRADE ? 'pass' : 'fail'}
Here is result:
{"fail"=>[["Matt", 30], "pass"=>[["Alex", 60], ["Dave", 50]]]}
You could simply do something like this:
sorted = {"pass" => [], "fail" => []}
hash.each do |name, grade|
if grade >= PASSING_GRADE
sorted["pass"] << [name, grade]
else
sorted["fail"] << [name, grade]
end
end
Here are two ways:
#1
a = hash.to_a
{ "pass" => a.select { |_,v| v > 50 }, "fail" => a.reject { |_,v| v > 50 } }
#=> {"pass"=>[["Alex", 60]], "fail"=>[["Matt", 30], ["Dave", 50]]}
#2
[:pass, :fail].zip(hash.to_a.partition { |_,v| v > 50 }).to_h
#=> {:pass=>[["Alex", 60]], :fail=>[["Matt", 30], ["Dave", 50]]}
These both give the return values as arrays of tuples, which you wanted for "fail" but not for "pass". Wouldn't that make it a pain to work with? Is it not better for both to be arrays of tuples?
Consider returning hashes for values:
{"pass" => hash.select { |_,v| v > 50 }, "fail" => hash.reject {|_,v| v > 50 }}
#=> {"pass"=>{"Alex"=>60}, "fail"=>{"Matt"=>30, "Dave"=>50}}
Would that not be more convenient?
I have a collection of product codes in an array: #codes. I then check to see how many instances of each product I have:
#popular = Hash.new(0)
#codes.each do |v|
#popular[v] += 1
end
This produces a hash like { code1 => 5, code2 => 12}. What I really need is a nice array of the form:
[ {:code => code1, :frequency => 5}, {:code => code2, :frequency => 12} ]
How do I build an array like that from the hashes I'm producing? Alternatively, is there a more direct route? The objects in question are ActiveModel objects with :code as an attribute. Thanks in advance!
#popular.map { |k, v| { code: k, frequency: v } }
This will produce an array of Hashes. If you need an array of models, replace the inner {...} with an appropriate constructor.
Change your code to
#codes.each_with_object([]) do
|code, a|
if h = a.find{|h| h[:code] == code}
h[:frequency] += 1
else
a.push(code: code, frequency: 0)
end
end
For speed:
#codes.group_by{|e| e}.map{|k, v| {code: k, frequency: v.length}}
Not the most efficient, but this is another way:
def counts(codes)
codes.uniq.map { |e| { code: e, frequency: codes.count(e) } }
end
codes = %w{code5 code12 code5 code3 code5 code12 code7}
#=> ["code5", "code12", "code5", "code3", "code5", "code12", "code7"]
counts(codes)
#=> [{:code=>"code5", :frequency=>3}, {:code=>"code12", :frequency=>2},
# {:code=>"code3", :frequency=>1}, {:code=>"code7" , :frequency=>1}]
I have a structure with a cartesian product that looks like this (and could go out to arbitrary depth)...
variables = ["var1","var2",...]
myhash = {
{"var1"=>"a", "var2"=>"a", ...}=>1,
{"var1"=>"a", "var2"=>"b", ...}=>2,
{"var1"=>"b", "var2"=>"a", ...}=>3,
{"var1"=>"b", "var2"=>"b", ...}=>4,
}
... it has a fixed structure but I'd like simple indexing so I'm trying to write a method to convert it to this :
nested = {
"a"=> {
"a"=> 1,
"b"=> 2
},
"b"=> {
"a"=> 3,
"b"=> 4
}
}
Any clever ideas (that allow for arbitrary depth)?
Maybe like this (not the cleanest way):
def cartesian_to_map(myhash)
{}.tap do |hash|
myhash.each do |h|
(hash[h[0]["var1"]] ||= {}).merge!({h[0]["var2"] => h[1]})
end
end
end
Result:
puts cartesian_to_map(myhash).inspect
{"a"=>{"a"=>1, "b"=>2}, "b"=>{"a"=>3, "b"=>4}}
Here is my example.
It uses a method index(hash, fields) that takes the hash, and the fields you want to index by.
It's dirty, and uses a local variable to pass up the current level in the index.
I bet you can make it much nicer.
def index(hash, fields)
# store the last index of the fields
last_field = fields.length - 1
# our indexed version
indexed = {}
hash.each do |key, value|
# our current point in the indexed hash
point = indexed
fields.each_with_index do |field, i|
key_field = key[field]
if i == last_field
point[key_field] = value
else
# ensure the next point is a hash
point[key_field] ||= {}
# move our point up
point = point[key_field]
end
end
end
# return our indexed hash
indexed
end
You can then just call
index(myhash, ["var1", "var2"])
And it should look like what you want
index({
{"var1"=>"a", "var2"=>"a"} => 1,
{"var1"=>"a", "var2"=>"b"} => 2,
{"var1"=>"b", "var2"=>"a"} => 3,
{"var1"=>"b", "var2"=>"b"} => 4,
}, ["var1", "var2"])
==
{
"a"=> {
"a"=> 1,
"b"=> 2
},
"b"=> {
"a"=> 3,
"b"=> 4
}
}
It seems to work.
(see it as a gist
https://gist.github.com/1126580)
Here's an ugly-but-effective solution:
nested = Hash[ myhash.group_by{ |h,n| h["var1"] } ].tap{ |nested|
nested.each do |v1,a|
nested[v1] = a.group_by{ |h,n| h["var2"] }
nested[v1].each{ |v2,a| nested[v1][v2] = a.flatten.last }
end
}
p nested
#=> {"a"=>{"a"=>1, "b"=>2}, "b"=>{"a"=>3, "b"=>4}}
You might consider an alternative representation that is easier to map to and (IMO) just as easy to index:
paired = Hash[ myhash.map{ |h,n| [ [h["var1"],h["var2"]], n ] } ]
p paired
#=> {["a", "a"]=>1, ["a", "b"]=>2, ["b", "a"]=>3, ["b", "b"]=>4}
p paired[["a","b"]]
#=> 2
Say:
h = { 1 => 10, 2 => 20, 5 => 70, 8 => 90, 4 => 34 }
I would like to change each value v to foo(v), such that h will be:
h = { 1 => foo(10), 2 => foo(20), 5 => foo(70), 8 => foo(90), 4 => foo(34) }
What is the most elegant way to achieve this ?
You can use update (alias of merge!) to update each value using a block:
hash.update(hash) { |key, value| value * 2 }
Note that we're effectively merging hash with itself. This is needed because Ruby will call the block to resolve the merge for any keys that collide, setting the value with the return value of the block.
Rails (and Ruby 2.4+ natively) have Hash#transform_values, so you can now do {a:1, b:2, c:3}.transform_values{|v| foo(v)}
https://ruby-doc.org/core-2.4.0/Hash.html#method-i-transform_values
If you need it to work in nested hashes as well, Rails now has deep_transform_values(source):
hash = { person: { name: 'Rob', age: '28' } }
hash.deep_transform_values{ |value| value.to_s.upcase }
# => {person: {name: "ROB", age: "28"}}
This will do:
h.each {|k, v| h[k] = foo(v)}
The following is slightly faster than #Dan Cheail's for large hashes, and is slightly more functional-programming style:
new_hash = Hash[old_hash.map {|key, value| key, foo(value)}]
Hash#map creates an array of key value pairs, and Hash.[] converts the array of pairs into a hash.
There's a couple of ways to do it; the most straight-forward way would be to use Hash#each to update a new hash:
new_hash = {}
old_hash.each do |key, value|
new_hash[key] = foo(value)
end