Loop in hash for ruby - ruby

I have:
blockchain = [
{ from_user: nil, to_user: "brian", amount: 21000 },
{ from_user: "brian", to_user: "ben", amount: 9000 },
{ from_user: "brian", to_user: "jeff", amount: 7000 },
{ from_user: "ben", to_user: "jeff", amount: 400 },
{ from_user: "brian", to_user: "jeff", amount: 1500 },
{ from_user: "jeff", to_user: "brian", amount: 4500 },
{ from_user: "jeff", to_user: "ben", amount: 1750 }
]
I want to get the final amounts for each person, total but coin remaining. It should print out:
Brian's balance is 8000
Ben's balance is 10350
Jeff's balance is 2650
I am trying to figure out how to write the code. Could someone help?

You can do it in the following way :
blockchain = [
{ from_user: nil, to_user: "brian", amount: 21000 },
{ from_user: "brian", to_user: "ben", amount: 9000 },
{ from_user: "brian", to_user: "jeff", amount: 7000 },
{ from_user: "ben", to_user: "jeff", amount: 400 },
{ from_user: "brian", to_user: "jeff", amount: 1500 },
{ from_user: "jeff", to_user: "brian", amount: 4500 },
{ from_user: "jeff", to_user: "ben", amount: 1750 }
]
users = {}
blockchain.each do |block|
users[block[:from_user]] = 0 if !users.keys.include?(block[:from_user]) && block[:from_user].present?
users[block[:to_user]] = 0 if !users.keys.include?(block[:to_user])
users[block[:to_user]] = users[block[:to_user]] + block[:amount]
users[block[:from_user]] = users[block[:from_user]] - block[:amount] if block[:from_user].present?
end
puts users
The users hash will contain your required output
{"brian"=>8000, "ben"=>10350, "jeff"=>2650}

Below is simple implementation to achieve balance for each person,
add = blockchain.group_by { |x| x[:to_user] }.reject { |k,v| k.nil? }.transform_values { |v| v.map { |x| x[:amount] }.sum }
# => {"brian"=>25500, "ben"=>10750, "jeff"=>8900}
sub = blockchain.group_by { |x| x[:from_user] }.reject { |k,v| k.nil? }.transform_values { |v| v.map { |x| x[:amount] }.sum }
# => {"brian"=>17500, "ben"=>400, "jeff"=>6250}
people = (add.keys + sub.keys).uniq
# => ["brian", "ben", "jeff"]
people.each { |x| puts "#{x.capitalize}'s balance is #{add[x].to_i - sub[x].to_i}" }
# Brian's balance is 8000
# Ben's balance is 10350
# Jeff's balance is 2650
# => ["brian", "ben", "jeff"]

Step 1: Create an empty hash with a default value of zero
The keys of this hash will be the users and whose values will be the amounts each user has at any given time.
For a hash h to have a default value of 0 means that h[k] returns 0 if h does not have a key k. Here we will be writing h[k] += 1, which expands to
h[k] = h[k] + 1
If (before this expression is executed) h does not have a key k h[k] on the right returns zero, so we have
h[k] = 0 + 1
#=> 1
Thereafter, h[k] on the right will be a positive integer.
If you are wondering why h[k] on the left of the expression does not return 0, recall that h[k] = h[k] + 1 is syntactic sugar for the actual expression
h.[]=(h.[](k) + 1)
We have the method []= on the left and the method [] on the right. It is h.[](k) that returns the default value when h does not have a key k.
Find a Hash method m (not actually "m") to do that.
h = Hash.m(?)
The question mark means that you must supply an argument for the method m.
Step 2: Step through the hashes in blockchain to update the hash h
blockchain.each do |g|
h[?] += ?
h[?] -= ? unless ? == nil
end
h #=> {"brian"=>8000, "ben"=>10350, "jeff"=>2650}
We now have the information needed to print the desired results.
In practice we would chain these two steps using the method Enumerable#each_with_object:
blockchain.each_with_object(Hash.m(?)) do |g,h|
h[?] += ??
h[?] -= ?? unless ?? == nil
end
#=> {"brian"=>8000, "ben"=>10350, "jeff"=>2650}
Here ? and ?? are respectively placeholders for an argument and an expression.

Awesome answer by Amogh Hegde, but somehow It was not working on my system with ruby 2.5.1 but if you add require 'active_support/all' on the top script, it will work. But I tried to do it without including anything. So have done very minor change. Following is the complete code I just changed if condition to check :from_user is present or not, so I have converted to a string and then checked it with the empty method.
blockchain = [
{ from_user: nil, to_user: "brian", amount: 21000 },
{ from_user: "brian", to_user: "ben", amount: 9000 },
{ from_user: "brian", to_user: "jeff", amount: 7000 },
{ from_user: "ben", to_user: "jeff", amount: 400 },
{ from_user: "brian", to_user: "jeff", amount: 1500 },
{ from_user: "jeff", to_user: "brian", amount: 4500 },
{ from_user: "jeff", to_user: "ben", amount: 1750 }
]
users = {}
blockchain.each do |block|
users[block[:from_user]] = 0 if !users.keys.include?(block[:from_user]) && !block[:from_user].to_s.empty?
users[block[:to_user]] = 0 if !users.keys.include?(block[:to_user])
users[block[:to_user]] = users[block[:to_user]] + block[:amount]
users[block[:from_user]] = (users[block[:from_user]] - block[:amount]) if !block[:from_user].to_s.empty?
end
puts users
I am getting following response.
{"brian"=>8000, "ben"=>10350, "jeff"=>2650}

In one iteration:
blockchain.each_with_object(Hash.new(0)) do |h, nh|
nh[h[:from_user]] -= h[:amount]
nh[h[:to_user]] += h[:amount]
end
#=> {nil=>-21000, "brian"=>8000, "ben"=>10350, "jeff"=>2650}
To keep track of the full movements:
blockchain.each_with_object(Hash.new { |h,k| h[k] = Hash.new(0) }) do |h, nh|
nh[h[:from_user]][:out] -= h[:amount]
nh[h[:to_user]][:in] += h[:amount]
end
#=> {nil=>{:out=>-21000}, "brian"=>{:in=>25500, :out=>-17500}, "ben"=>{:in=>10750, :out=>-400}, "jeff"=>{:in=>8900, :out=>-6250}}
Other option: could calculate incomes and outcomes using Enumerable#group_by and Hash#transform_values, then merge together.
Incomes are:
incomes = blockchain.group_by { |k,v| k[:to_user] }.transform_values{ |a| a.sum { |h| h[:amount] }}
#=> {"brian"=>25500, "ben"=>10750, "jeff"=>8900}
Outcomes are (negatives):
outcomes = blockchain.group_by { |k,v| k[:from_user] }.transform_values{ |a| -a.sum { |h| h[:amount]}}
#=> {nil=>-21000, "brian"=>-17500, "ben"=>-400, "jeff"=>-6250}
Then merge passing a block using Hash#merge:
incomes.merge(outcomes) { |k, income, outcome| income + outcome }
#=> {"brian"=>8000, "ben"=>10350, "jeff"=>2650, nil=>-21000}

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"}]

Ruby, unique hashes in array based on multiple fields

I'd like to get back an array of hashes based on sport and type combination
I've got the following array:
[
{ sport: "football", type: 11, other_key: 5 },
{ sport: "football", type: 12, othey_key: 100 },
{ sport: "football", type: 11, othey_key: 700 },
{ sport: "basketball", type: 11, othey_key: 200 },
{ sport: "basketball", type: 11, othey_key: 500 }
]
I'd like to get back:
[
{ sport: "football", type: 11, other_key: 5 },
{ sport: "football", type: 12, othey_key: 100 },
{ sport: "basketball", type: 11, othey_key: 200 },
]
I tried to use (pseudocode):
[{}, {}, {}].uniq { |m| m.sport and m.type }
I know I can create such array with loops, I'm quite new to ruby and I'm curious if there's a better (more elegant) way to do it.
Try using Array#values_at to generate an array to uniq by.
sports.uniq{ |s| s.values_at(:sport, :type) }
One solution is to build some sort of key with the sport and type, like so:
arr.uniq{ |m| "#{m[:sport]}-#{m[:type]}" }
The way uniq works is that it uses the return value of the block to compare elements.
require 'pp'
data = [
{ sport: "football", type: 11, other_key: 5 },
{ sport: "football", type: 12, othey_key: 100 },
{ sport: "football", type: 11, othey_key: 700 },
{ sport: "basketball", type: 11, othey_key: 200 },
{ sport: "basketball", type: 11, othey_key: 500 }
]
results = data.uniq do |hash|
[hash[:sport], hash[:type]]
end
pp results
--output:--
[{:sport=>"football", :type=>11, :other_key=>5},
{:sport=>"football", :type=>12, :othey_key=>100},
{:sport=>"basketball", :type=>11, :othey_key=>200}]

Resources