I have an input as follows:
input = [
["account1",10,"Dr"],
["account1",20,"Dr"],
["account2",15,"Cr"],
["account1",25,"Cr"],
["account2",10,"Dr"],
["account1",10,"Cr"]
]
I am trying to get the sums by account and transaction type, i.e., Dr or Cr. I need an output as below:
output = {
["account1","Dr"] => 30,
["account1","Cr"] => 35,
["account2","Dr"] => 10,
["account2","Cr"] => 15
}
I can sum the amount based on only account using:
input.each_with_object(Hash.new(0)) {|(f,g), h| h[f] += g}
# => {"account1"=>65, "account2"=>25}
You can do so:
input.each_with_object(Hash.new(0)) {|(f,g,i), h| h[[f,i]] += g}
=> {["account1", "Dr"]=>30, ["account2", "Cr"]=>15, ["account1", "Cr"]=>35, ["account2", "Dr"]=>10}
input.group_by { |acc,_,title| [acc, title] }.
transform_values { |v| v.sum { |a| a[1] } }
#=> {["account1", "Dr"]=>30, ["account2", "Cr"]=>15, ["account1", "Cr"]=>35,
# ["account2", "Dr"]=>10}
The first step is the following.
input.group_by { |acc,_,title| [acc, title] }
#=> {
# ["account1", "Dr"]=>[["account1", 10, "Dr"], ["account1", 20, "Dr"]],
# ["account2", "Cr"]=>[["account2", 15, "Cr"]],
# ["account1", "Cr"]=>[["account1", 25, "Cr"], ["account1", 10, "Cr"]],
# ["account2", "Dr"]=>[["account2", 10, "Dr"]]
# }
output = Hash.new(0) # set a default value of zero, avoiding nil
input.each do |account, amount, transaction|
output[[account, transaction]] += amount
end
output # {["account1", "Dr"]=>30, ["account2", "Cr"]=>15, ["account1", "Cr"]=>35, ["account2", "Dr"]=>10}
Related
With this code I can find most occurrences of items in an array:
letters.max_by { |i| letters.count(i) }
But this will return 2 for
a = [1, 2, 2, 3, 3]
although 3 has the same occurrence. How can I find out, if there really is an item with most occurrences? I would like to get false if there is no single champion.
This is pretty ugly and in need of refinement, but:
def champion(array)
grouped = array.group_by(&:itself).values.group_by(&:length)
best = grouped[grouped.keys.max]
if (best.length == 1)
best[0][0]
else
false
end
end
I'm not sure there's an easy single-shot solution for this, at least not one that's not O(n^2) or worse, which is unusual.
I guess you could do this if you don't care about performance:
def max_occurrences(arr)
arr.sort.max_by { |v| arr.count(v) } != arr.sort.reverse.max_by { |v| arr.count(v) } ? false : arr.max_by { |v| arr.count(v) }
end
I would do something like this:
def max_occurrences(arr)
counts = Hash.new { |h, k| h[k] = 0 }
grouped_by_count = Hash.new { |h, k| h[k] = [] }
arr.each { |el| counts[el] += 1 } # O(n)
counts.each { |el, count| grouped_by_count[count] << el } # O(n)
max = grouped_by_count.sort { |x, y| y[0] <=> x[0] }.first[1] # O(n log n)
max.length == 1 ? max[0] : false
end
It's no snazzy one-liner, but it's readable and runs in less than O(n log n).
a = [1, 2, 2, 3, 3]
occurrences = a.inject(Hash.new(0)){ |h, el| h[el] += 1; h } # => {1=>1, 2=>2, 3=>2}
max_occurences = occurrences.max_by{ |_, v| v } # => [2, 2]
max_occurences.count > 1 ? false : occurrences.key(max_occurences.first)
I want a function that can take an array like [:a, :b, :c] and recursively set hash keys, creating what it needs as it goes.
hash = {}
hash_setter(hash, [:a, :b, :c], 'value')
hash #=> {:a => {:b => {:c => 'value' } } }
hash_setter(hash, [:a, :b, :h], 'value2')
hash #=> {:a => {:b => {:c => 'value', :h => 'value2' } } }
I'm aware that Ruby 2.3's dig can be used for getting in this way, though that doesnt quite get you to an answer. If there was a setter equivalent of dig that'd be what I'm looking for.
Code
def nested_hash(keys, v, h={})
return subhash(keys, v) if h.empty?
return h.merge(subhash(keys, v)) if keys.size == 1
keys[0..-2].reduce(h) { |g,k| g[k] }.update(keys[-1]=>v)
h
end
def subhash(keys, v)
*first_keys, last_key = keys
h = { last_key=>v }
return h if first_keys.empty?
first_keys.reverse_each.reduce(h) { |g,k| g = { k=>g } }
end
Examples
h = nested_hash([:a, :b, :c], 14) #=> {:a=>{:b=>{:c=>14}}}
i = nested_hash([:a, :b, :d], 25, h) #=> {:a=>{:b=>{:c=>14, :d=>25}}}
j = nested_hash([:a, :b, :d], 99, i) #=> {:a=>{:b=>{:c=>14, :d=>99}}}
k = nested_hash([:a, :e], 104, j) #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}}
nested_hash([:f], 222, k) #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}, :f=>222}
Observe that the value of :d is overridden in the calculation of j. Also note that:
subhash([:a, :b, :c], 12)
#=> {:a=>{:b=>{:c=>12}}}
This mutates the hash h. If that is not desired one could insert the line
f = Marshal.load(Marshal.dump(h))
after the line return subhash(keys, v) if h.empty? and change subsequent references to h to f. Methods from the Marshal module can be used to create a deep copy of a hash so the original hash is not be mutated.
Solved it with recursion:
def hash_setter(hash, key_arr, val)
key = key_arr.shift
hash[key] = {} unless hash[key].is_a?(Hash)
key_arr.length > 0 ? hash_setter(hash[key], key_arr, val) : hash[key] = val
end
def set_value_for_keypath(initial, keypath, value)
temp = initial
for key in keypath.first(keypath.count - 1)
temp = (temp[key] ||= {})
end
temp[keypath.last] = value
return initial
end
initial = {:a => {:b => {:c => 'value' } } }
set_value_for_keypath(initial, [:a, :b, :h], 'value2')
initial
Or if you prefer something more unreadable:
def set_value_for_keypath(initial, keypath, value)
keypath.first(keypath.count - 1).reduce(initial) { |hash, key| hash[key] ||= {} }[keypath.last] = value
end
So I know how to add all the values in an array.
Example, the sum of [1,2,3,4]...
[1,2,3,4].inject(&:+)
#=> 10
However, I have an array of arrays and would like to add the values that have the same first element of each sub array.
# example
[["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
Desired output:
"(A : 15) - (B : 10) - (C : 30)"
Any help would be appreciated!
arr = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
h = arr.each_with_object(Hash.new(0)) { |(f,g),h| h[f] += g }
#=> {"A"=>15, "B"=>10, "C"=>30}
Then
h.map { |pair| "(%s : %s)" % pair }.join(" - ")
#=> "(A : 15) - (B : 10) - (C : 30)"
which you can combine like so:
arr.each_with_object(Hash.new(0)) { |(f,g),h| h[f] += g }.
map { |pair| "(%s : %s)" % pair }.join(" - ")
See Hash::new, especially with regards to the use of a default value (here 0).
Try this
arr = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
arr.group_by(&:first).map { |key, group| [key, group.map(&:last).inject(:+)] }
# => [["A", 15], ["B", 10], ["C", 30]]
How does this work?
group_by(&:first) groups the subarrays by first element
map { |key, group| ... } transforms the groups
group.map(&:last).inject(:+) sums up all last elements in a group
a = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
result = a.group_by(&:first).each_with_object({}) do |(k, v), h|
h[k] = v.map(&:last).inject(:+)
# if your on Ruby 2.4+ you can write h[k] = v.sum(&:last)
end
#=> {"A"=>15, "B"=>10, "C"=>30}
Another option would be to build the hash from the beginning:
result = a.each_with_object({}) {|(k, v), h| h[k] = h[k].to_i + v }
#=> {"A"=>15, "B"=>10, "C"=>30}
If your desired output is literally a string "(A : 15) - (B : 10) - (C : 30)":
result.map { |k, v| "(#{k} : #{v})" }.join(' - ')
#=> "(A : 15) - (B : 10) - (C : 30)"
There are more elegant ways of doing this, but here is the solution as a block, so you can understand the logic...
What this does is :
convert the array to a hash, when combining
values.
Then it builds the string, one element at a time, storing
each in an array.
And finally, it combines the array of strings into your desired output.
'
my_array = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15],["C", 15]]
my_hash = {}
output_array = []
my_array.each do |item|
my_hash[item[0]] ||= 0
my_hash[item[0]] += item[1]
end
my_hash.each do |k,v|
output_array.push("(#{k} : #{v})")
end
puts output_array.join(" - ")
I'm wondering how to sum the "analytic" value from this array of hashes with recursion.
Input :
[{"id"=>"1234",
"id_data"=>
[{"segment"=>{"segment_name"=>"Android"},
"metrics"=>
{
"logins"=>[1000, 2000],
"sign_ups_conversion"=>{
"count"=>[500, 200],
"cost"=>[2, 4]
}
},
},
{"segment"=>{"segment_name"=>"iOS"},
"metrics"=>
{
"logins"=>[5000, 10000],
"sign_ups_conversion"=>{
"count"=>[100, 50],
"cost"=>[6, 8]
}
},
}
]
},
{"id"=>"5678",
"id_data"=>
[{"segment"=>{"segment_name"=>"Android"},
"metrics"=>
{
"logins"=>[3000, 2000],
"sign_ups_conversion"=>{
"count"=>[300, 400],
"cost"=>[2, 4]
}
},
},
{"segment"=>{"segment_name"=>"iOS"},
"metrics"=>
{
"logins"=>[5000, 10000],
"sign_ups_conversion"=>{
"count"=>[100, 50],
"cost"=>[6, 8]
}
},
}
]
}]
Output :
{
"Android"=>{
"ids" => ['1234','5678'],
"segment" => {"segment_name"=>"Android"},
"id_data" => [{
"logins" => [4000, 4000], # sum by index from 'Android' logins ("logins"=>[1000, 2000] & "logins"=>[3000, 2000]),
"sign_ups_conversion" => {
"count" => [800, 600], # sum by index from 'Android' sign ups count ("count"=>[500, 200] & "count"=>[300, 400])
"cost" => [4, 8] # sum by index from 'Android' sign ups cost ("cost"=>[2, 4] & "cost"=>[2, 4])
}
}]
},
"iOS"=>{
"ids" => ['1234','5678'],
"segment" => {"segment_name"=>"iOS"},
"id_data" => [{
"logins" => [10000, 20000], # sum by index from 'iOS' logins ("logins"=>[5000, 10000] & "logins"=>[5000, 10000]),
"sign_ups_conversion" => {
"count" => [200, 100], # sum by index from 'iOS' sign ups count ("count"=>[100, 50] & "count"=>[100, 50])
"cost" => [12, 16] # sum by index from 'iOS' sign ups cost ("cost"=>[6, 8] & "cost"=>[6, 8])
}
}]
}
}
Me, trying to solve it with this methods but it is not counting analytics with hash format (sign_ups_conversion) and still figuring it out how the results should be equal to output.
def aggregate_by_segments(stats_array)
results = {}
stats_array.each do |stats|
stats['id_data'].each do |data|
segment_name = data['segment']['segment_name']
results[segment_name] ||= {}
(results[segment_name]['ids'] ||= []) << stats['id']
results[segment_name]['segment'] ||= data['segment']
results[segment_name]['id_data'] ||= [{}]
data['metrics'].each do |metric, values|
next if skip_metric?(values)
(results[segment_name]['id_data'][0][metric] ||= []) << values
end
end
end
sum_segments(results)
end
def sum_segments(segments)
segments.each do |segment, segment_details|
segment_details['id_data'][0].each do |metric, values|
segment_details['id_data'][0][metric] = sum_segment_metric(values)
end
end
segments
end
def sum_segment_metric(metric_value)
metric_value.transpose.map { |x| x.reduce(:+) }
end
# I skipped hash format for now
def skip_metric?(metric_values)
!metric_values.is_a? Array
end
############################################
# calls it with aggregate_by_segments(input)
############################################
I believe we should use recursion but i'm still figuring it out, anyone can help me?
Thanks in advance!
The problem here is how to acces this data structures, a ruby strategy can be iterate over arrays using each and conctenating keys with concatenated hashes like this:
Supposing that your structure is mantained:
Array[hash[array[hash]]
array_hash.each do |stats|
stats["id_data"].each do |h|
puts h["metrics"]["sign_ups_conversion"]
end
end
# => {"count"=>[500, 200], "cost"=>[2, 4]}
# => {"count"=>[100, 50], "cost"=>[6, 8]}
# => {"count"=>[300, 400], "cost"=>[2, 4]}
# => {"count"=>[100, 50], "cost"=>[6, 8]}
I solved it.
def aggregate_by_segments(stats_array)
results = {}
stats_array.each do |stats|
stats['id_data'].each do |data|
segment_name = data['segment']['segment_name']
results[segment_name] ||= {}
(results[segment_name]['ids'] ||= []) << stats['id']
results[segment_name]['segment'] ||= data['segment']
results[segment_name]['id_data'] ||= [{}]
data['metrics'].each do |metric, values|
hash_values(results[segment_name]['id_data'][0], metric, values) if values.is_a? Hash
next if skip_metric?(values)
(results[segment_name]['id_data'][0][metric] ||= []) << values
end
end
end
sum_segments(results)
end
def hash_values(metrics, metric, hash_values)
hash_values.each do |k, v|
next if skip_metric?(v)
metrics[metric] ||= {}
(metrics[metric][k] ||= []) << v
end
end
def sum_segments(segments)
segments.each do |segment, segment_details|
segment_details['id_data'][0].each do |metric, values|
segment_details['id_data'][0][metric] = sum_segment_metric(values)
end
end
segments
end
def sum_segment_metric(metric_value)
result = metric_value.transpose.map { |x| x.reduce(:+) } if metric_value.is_a? Array
result = metric_value.each do |k, v|
metric_value[k] = sum_segment_metric(v)
end if metric_value.is_a? Hash
result
end
def skip_metric?(metric_values)
!metric_values.is_a? Array
end
I know the code is pretty ugly. I will refactor it later :)
Thank you guys for visiting and commenting with constructive feedback.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
For example I have this data:
headings = {
:heading1 => { :weight => 60, :show_count => 0}
:heading2 => { :weight => 10, :show_count => 0}
:heading3 => { :weight => 20, :show_count => 0}
:heading4 => { :weight => 10, :show_count => 0}
}
total_views = 0
Now I want to serve each heading based on their weightages. For instance, for first 10 requests/iterations, heading1, heading3, heading2 and heading4 would be served 6, 2, 1, and 1 times respectively in order (by weight).
For every iteration show_count of served heading will increment by one and total_views will also increment globally.
Could you please suggest an algorithm or some ruby code to handle this.
You can use pickup gem
It accepts hash like this:
require 'pickup'
headings = {
heading1: 60,
heading2: 10,
heading3: 20,
heading4: 10
}
pickup = Pickup.new(headings)
pickup.pick
#=> heading1
pickup.pick
#=> heading1
pickup.pick
#=> heading3
pickup.pick
#=> heading1
pickup.pick
#=> heading4
So you can do something like this:
require 'pickup'
headings = {
heading1: { :weight => 60, :show_count => 0},
heading2: { :weight => 10, :show_count => 0},
heading3: { :weight => 20, :show_count => 0},
heading4: { :weight => 10, :show_count => 0}
}
pickup_headings = headings.inject({}){ |h, (k,v)| h[k] = v[:weight]; h}
pickup = Pickup.new(pickup_headings)
# let's fire it 1000 times
1000.times do
server = pickup.pick
headings[server][:show_count] += 1
end
puts headings
#=> {
#=> :heading1=>{:weight=>60, :show_count=>601},
#=> :heading2=>{:weight=>10, :show_count=>116},
#=> :heading3=>{:weight=>20, :show_count=>176},
#=> :heading4=>{:weight=>10, :show_count=>107}
#=> }
This should work for your basic case and can be modified according to the details of what you need:
class Heading
attr_reader :heading, :weight, :show_count
def initialize(heading,weight=1)
#heading=heading
#weight=weight
#show_count=0
end
def serve
puts "Served #{#heading}! "
#show_count += 1
end
end
class HeadingServer
attr_reader :headings
def initialize(headings_hash)
#headings=headings_hash.map {|h, data| Heading.new(h,data[:weight])}
#total_weight=#headings.inject(0) {|s,h| s+= h.weight}
end
def serve(num_to_serve=#total_weight)
#headings.sort {|a,b| b.weight <=> a.weight}.each do |h|
n = (h.weight * num_to_serve) / #total_weight #possibility of rounding errors
n.times { h.serve }
end
end
def total_views
#headings.inject(0) {|s,h| s += h.show_count}
end
end
headings = {
:heading1 => { :weight => 60, :show_count => 0},
:heading2 => { :weight => 10, :show_count => 0},
:heading3 => { :weight => 20, :show_count => 0},
:heading4 => { :weight => 10, :show_count => 0}
}
# Example Usage:
hs = HeadingServer.new(headings)
hs.serve(10)
hs.headings.each {|h| puts "#{h.heading} : served #{h.show_count} times"}
puts "Total Views: #{hs.total_views}"