summing values in a hash - ruby

What is the best way to sum hash values in ruby:
#price = { :price1 => "100", :price2 => "100", :price3 => "50" }
I do something like this right now:
#pricepackage = #price[:price1] + #price[:price2] + #price[:price3] + 500
Please explain your answer, I want to learn why and not just how. =)

You can do:
#price.values.map(&:to_i).inject(0, &:+)
EDIT: adding explanation
So #price.values returns an array that collects all the values of the hash. That is for instance ["1", "12", "4"]. Then .map(&:to_i) applies to_i to each of the elements of this array and thus you get [1,12,4]. Lastly .inject(0,&:+) preforms inject with initial value 0 and accumulating using the function +, so it sums all the elements of the array in the last step.

If your data set looks like this:
prices = {price1: 100, price2: 100, price3: 50}
then this will sum the values:
prices.values.inject(0) {|total, v| total += v}

Keep it simple
#prices = ....
#price = 0.0
#prices.each do |p|
#price += p
end

Related

Trouble about iterating over an array to generate frequencies in a hash

I have an array with some different data (in string format) and I would like to count the frequencies of each value and store it in hash/dictonary, but i'm getting error trying to do it.
I would like to do something like this:
words = ["foo", "var", "spam", "egg", "foo", "foo", "egg"]
frequency = {}
words.each{|word| frequency[word] += 1}
and get the following output:
puts(frequency) #{"foo" => 3, "var" => 1, "spam" => 1, "egg" => 2}
but I'm getting the following problem:
prueba.rb:3:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)
Is there another way of accomplishing the same result?
If you query the hash for a key not present you get nil, that's the problem. Let's make 0 the default if a key is not present
frequency = Hash.new(0)
When you write:
words.each{|word| frequency[word] += 1}
It's equivalent to:
words.each{|word| frequency[word] = frequency[word] + 1}
However, frequency hash is empty and frequency[word] returns nil -- essentially, you are trying to do nil + 1, which results in the error you are getting.
You can initialize new elements of the hash manually:
words.each{|word| frequency[word] ||= 0; frequency[word] += 1}
Or, as other answers have already suggested, use frequency = Hash.new(0) as a shortcut.
words = ["foo", "var", "spam", "egg", "foo", "foo", "egg"]
words.each_with_object(Hash.new(0)) { |w, h| h[w] += 1 }
#=> {"foo"=>3, "var"=>1, "spam"=>1, "egg"=>2}
#each_with_object is the enumerator which iterates by passing each element with an object supplied (here Hash.new(0)) and returns that supplied object in next iteration.
Hash.new(0) creates a hash where default value of any key is 0.
hash = Hash.new(0)
hash['a'] #=> 0
We use this property of default value and simply pass each word to the object and increment the counter.
:)
Issue in your code: You don't have default value in your case, so you would need to do that manually by checking if a key exists in the hash or not.
You can use this. By initializing it with Hash.new(0)
names = ["foo", "var", "spam", "egg", "foo", "foo", "egg"]
names.inject(Hash.new(0)) { |total, e| total[e] += 1 ;total}
# {"foo"=>3, "var"=>1, "spam"=>1, "egg"=>2}

How to get the highest and the lowest points in the hash?

#participants[id] = {nick: nick, points: 1}
=> {"1"=>{:nick=>"Test", :points=>3}, "30"=>{:nick=>"AnotherTest", :points=>5}, "20"=>{:nick=>"Newtest", :points=>3}}
I want my the lowest points (ID: 1 and 20). How do I get the lowest points go first and then ID 30 go last?
If you use Enumerable#max_by() or Enumerable#min_by() you can do following;
data = {
"1" => {nick: "U1", points: 3},
"30" => {nick: "U30", points: 5},
"20" => {nick: "U20", points: 3}
}
max_id, max_data = data.max_by {|k,v| v[:points]}
puts max_id # => 30
puts max_data # => {nick: "U30", points: 5}
Same thing works with #min_by() and if you want to get back Hash you do this:
minimal = Hash[*data.min_by {|k,v| v[:points]}]
puts minimal # => {"1"=>{:nick=>"U1", :points=>3}}
Functions min_by() and max_by() will always return one record. If you want to get all records with same points then you have to use min / max data an do another "lookup" like this:
min_id, min_data = data.min_by {|k,v| v[:points]}
all_minimal = data.select {|k,v| v[:points] == min_data[:points]}
puts all_minimal
# => {"1"=>{:nick=>"U1", :points=>3}, "20"=>{:nick=>"U20", :points=>3}}
Hashes are not a suitable data structure for this operation. They are meant when you need to get a value in O(1) complexity.
You are better off using a sorted array or a tree if you are interested in comparisons or Heap (in case you are interested in only maximum or minimum value) as #Vadim suggested
Use Enumerable#minmax_by:
h = { "1"=>{:nick=>"Test", :points=>3},
"30"=>{:nick=>"AnotherTest", :points=>5},
"20"=>{:nick=>"Newtest", :points=>3}}
h.minmax_by { |_,g| g[:points] }
#=> [[ "1", {:nick=>"Test", :points=>3}],
# ["30", {:nick=>"AnotherTest", :points=>5}]]
You can work around the fact that min_by and max_by are returning only one result by combining sort and chunk:
data.sort_by{|_,v| v[:points]}.chunk{|(_,v)| v[:points]}.first.last.map(&:first)
#=> ["1", "20"]

Merge duplicate values in json using ruby

I have the following item.json file
{
"items": [
{
"brand": "LEGO",
"stock": 55,
"full-price": "22.99",
},
{
"brand": "Nano Blocks",
"stock": 12,
"full-price": "49.99",
},
{
"brand": "LEGO",
"stock": 5,
"full-price": "199.99",
}
]
}
There are two items named LEGO and I want to get output for the total number of stock for the individual brand.
In ruby file item.rb i have code like:
require 'json'
path = File.join(File.dirname(__FILE__), '../data/products.json')
file = File.read(path)
products_hash = JSON.parse(file)
products_hash["items"].each do |brand|
puts "Stock no: #{brand["stock"]}"
end
I got output for stock no individually for each brand wherein I need the stock to be summed for two brand name "LEGO" displayed as one.
Anyone has solution for this?
json = File.open(path,'r:utf-8',&:read) # in case the JSON uses UTF-8
items = JSON.parse(json)['items']
stock_by_brand = items
.group_by{ |h| h['brand'] }
.map do |brand,array|
[ brand,
array
.map{ |item| item['stock'] }
.inject(:+) ]
end
.to_h
#=> {"LEGO"=>60, "Nano Blocks"=>12}
It works like this:
Enumerable#group_by takes the array of items and creates a hash mapping the brand name to an array of all item hashes with that brand
Enumerable#map turns each brand/array pair in that hash into an array of the brand (unchanged) followed by:
Enumerable#map on the array of items picks out just the "stock" counts, and then
Enumerable#inject sums them all together
Array#to_h then turns that array of two-value arrays into a hash, mapping the brand to the sum of stock values.
If you want simpler code that's less functional and possibly easier to understand:
stock_by_brand = {} # an empty hash
items.each do |item|
stock_by_brand[ item['brand'] ] ||= 0 # initialize to zero if unset
stock_by_brand[ item['brand'] ] += item['stock']
end
p stock_by_brand #=> {"LEGO"=>60, "Nano Blocks"=>12}
To see what your JSON string looks like, let's create it from your hash, which I've denoted h:
require 'json'
j = JSON.generate(h)
#=> "{\"items\":[{\"brand\":\"LEGO\",\"stock\":55,\"full-price\":\"22.99\"},{\"brand\":\"Nano Blocks\",\"stock\":12,\"full-price\":\"49.99\"},{\"brand\":\"LEGO\",\"stock\":5,\"full-price\":\"199.99\"}]}"
After reading that from a file, into the variable j, we can now parse it to obtain the value of "items":
arr = JSON.parse(j)["items"]
#=> [{"brand"=>"LEGO", "stock"=>55, "full-price"=>"22.99"},
# {"brand"=>"Nano Blocks", "stock"=>12, "full-price"=>"49.99"},
# {"brand"=>"LEGO", "stock"=>5, "full-price"=>"199.99"}]
One way to obtain the desired tallies is to use a counting hash:
arr.each_with_object(Hash.new(0)) {|g,h| h.update(g["brand"]=>h[g["brand"]]+g["stock"])}
#=> {"LEGO"=>60, "Nano Blocks"=>12}
Hash.new(0) creates an empty hash (represented by the block variable h) with with a default value of zero1. That means that h[k] returns zero if the hash does not have a key k.
For the first element of arr (represented by the block variable g) we have:
g["brand"] #=> "LEGO"
g["stock"] #=> 55
Within the block, therefore, the calculation is:
g["brand"] => h[g["brand"]]+g["stock"]
#=> "LEGO" => h["LEGO"] + 55
Initially h has no keys, so h["LEGO"] returns the default value of zero, resulting in { "LEGO"=>55 } being merged into the hash h. As h now has a key "LEGO", h["LEGO"], will not return the default value in subsequent calculations.
Another approach is to use 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:
arr.each_with_object({}) {|g,h| h.update(g["brand"]=>g["stock"]) {|_,o,n| o+n}}
#=> {"LEGO"=>60, "Nano Blocks"=>12}
1 k=>v is shorthand for { k=>v } when it appears as a method's argument.

count the same hash values in ruby

Suppose I have the following hash table in ruby,
H = {"I"=>3, "Sam"=>2, "am"=>2, "do"=>1, "not"=>1, "eat"=> 1}
I want to construct the following hash table N from H
N = {"1" => 3, "2"=>2, "3"=>1}
where 3 in value signifies number of hash entires with value "1" in H (e.g. "do"=>1, "not"=>1, "eat"=> 1, therefore "1" => 3)
Is there any easy way in ruby to construct the N hash table from H??
Thanks in advance!!!
Hash[H.values.group_by{|i| i}.map {|k,v| [k, v.count]}]
Update: to get strings as keys and sorted by key:
Hash[h.values.group_by{|i| i}.map {|k,v| [k.to_s, v.count]}.sort]
Try this,
N = {}
H.values.each { |t| N["#{t}"] = (N["#{t}"] || 0) + 1}
Alternate solution:
H.values.sort.inject(Hash.new(0)) {|ac,e| ac[e.to_s]+=1;ac}
# {"1"=>3, "2"=>2, "3"=>1}

Sorting an array of hashes by a date field

I have an object with many arrays of hashes, one of which I want to sort by a value in the 'date' key.
#array['info'][0] = {"name"=>"personA", "date"=>"23/09/1980"}
#array['info'][1] = {"name"=>"personB", "date"=>"01/04/1970"}
#array['info'][2] = {"name"=>"personC", "date"=>"03/04/1975"}
I have tried various methods using Date.parse and with collect but an unable to find a good solution.
Edit:
To be clear I want to sort the original array in place
#array['info'].sort_by { |i| Date.parse i['date'] }.collect
How might one solve this elegantly the 'Ruby-ist' way. Thanks
Another way, which doesn't require converting the date strings to date objects, is the following.
Code
def sort_by_date(arr)
arr.sort_by { |h| h["date"].split('/').reverse }
end
If arr is to be sorted in place, use Array#sort_by! rather than Enumerable#sort_by.
Example
arr = [{ "name"=>"personA", "date"=>"23/09/1980" },
{ "name"=>"personB", "date"=>"01/04/1970" },
{ "name"=>"personC", "date"=>"03/04/1975" }]
sort_by_date(arr)
#=> [{ "name"=>"personB", "date"=>"01/04/1970" },
# { "name"=>"personC", "date"=>"03/04/1975" },
# { "name"=>"personA", "date"=>"23/09/1980" }]
Explanation
For arr in the example, sort_by passes the first element of arr into its block and assigns it to the block variable:
h = { "name"=>"personA", "date"=>"23/09/1980" }
then computes:
a = h["date"].split('/')
#=> ["23", "09", "1980"]
and then:
b = a.reverse
#=> ["1980", "09", "23"]
Similarly, we obtain b equal to:
["1970", "04", "01"]
and
["1975", "04", "03"]
for each of the other two elements of arr.
If you look at the docs for Array#<=> you will see that these three arrays are ordered as follows:
["1970", "04", "01"] < ["1975", "04", "03"] < ["1980", "09", "23"]
There is no need to convert the string elements to integers.
Looks fine overall. Although you can drop the collect call since it's not needed and use sort_by! to modify the array in-place (instead of reassigning):
#array['info'].sort_by! { |x| Date.parse x['date'] }

Resources