Conditional value extraction from ruby hash - ruby

The users of my app have their points. I want to assign them different ranks based on their points. This is my rank mapping hash:
RANKS = { (1..20) => 'Private'
(21..40) => 'Corporal'
(41..60) => 'Sergeant'
(61..80) => 'Lieutenant'
(81..100) => 'Captain'
(101..150) => 'Major'
(151..200) => 'Colonel'
201 => 'General'
}
I need to check if the users' points are in a range key of the hash, and extract the necessary value. Is there any elegant solution for this? I could use 'case' operator, but that wouldn't be as elegant as I want.

You can just iterate all key/value pairs and check.
RANKS = { (1..20) => 'Private',
(21..40) => 'Corporal',
(41..60) => 'Sergeant',
(61..80) => 'Lieutenant',
(81..100) => 'Captain',
(101..150) => 'Major',
(151..200) => 'Colonel',
(201..+1.0/0.0) => 'General', # from 201 to infinity
}
def get_rank score
RANKS.each do |k, v|
return v if k.include?(score)
end
nil
end
get_rank 1 # => "Private"
get_rank 50 # => "Sergeant"
get_rank 500 # => "General"
get_rank -1 # => nil
Update:
I don't know why you think case isn't elegant. I think it's pretty elegant.
def get_rank score
case score
when (1..10) then 'Private'
when (21..40) then 'Corporal'
when (41..1.0/0.0) then 'Sergeant or higher'
else nil
end
end
get_rank 1 # => "Private"
get_rank 50 # => "Sergeant or higher"
get_rank 500 # => "Sergeant or higher"
get_rank -1 # => nil

Related

How do I increment count when there are multiple instances of an item in the cart? I have to iterate over the existing AoH and return a new one

All this is returning is a new AoH(array of hashes) with all the items from the original AoH. Also, duplicate items have to appear only once in the new AoH with the count increased
groceries = [
{:item => "AVOCADO", :price => 3.00, :clearance => true },
{:item => "AVOCADO", :price => 3.00, :clearance => true },
{:item => "KALE", :price => 3.00, :clearance => false}
]
def consolidate_cart(cart)
new_array_of_hashes = []
my_index = 0
while my_index < cart.length do
name = cart[my_index][:item]
#new_array_of_hashes[my_index][:count] = 1
if new_array_of_hashes[my_index]
new_array_of_hashes[my_index][:count] += 1
else
new_array_of_hashes.push(cart[my_index])
new_array_of_hashes[my_index][:count] = 1
end
my_index += 1
end
new_array_of_hashes
end
consolidate_cart(groceries)
All this is returning is a new AoH(array of hashes) with all the items from the original AoH. Also, duplicate items have to appear only once in the new AoH with the count increased
When using Ruby you generally don't want to manually iterate collections. Ruby offers a great amount of iterator methods, which mostly can be found in the Enumerable module, included by most collection classes like Array, Hash, Set and others. That being said, let's get to the answer.
You could group the items based upon the item itself, then count the total number of items present in each group.
groceries = [
{:item => "AVOCADO", :price => 3.00, :clearance => true },
{:item => "AVOCADO", :price => 3.00, :clearance => true },
{:item => "KALE", :price => 3.00, :clearance => false}
]
def consolidate_cart(cart)
cart.group_by(&:itself).map { |item, items| item.merge(count: items.count) }
end
consolidate_cart(groceries)
#=> [{:item=>"AVOCADO", :price=>3.0, :clearance=>true, :count=>2},
# {:item=>"KALE", :price=>3.0, :clearance=>false, :count=>1}]
This only groups items with the exact same keys-value pairs together. Meaning that if there is an item:
{:item => "AVOCADO", :price => 2.00, :clearance => false}
It will be placed in a different group than:
{:item => "AVOCADO", :price => 3.00, :clearance => true}
References:
Enumerable#group_by
Object#itself
Enumerable#map
Hash#merge
Array#count

Convert array into hash and add a counter value to the new hash

I have the following array of hashes:
[
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
And I would like to translate this array into a hash that includes the counts for each item:
Output:
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
I have tried two approaches to translate the array into a hash.
new_cart = cart.inject(:merge)
hash = Hash[cart.collect { |item| [item, ""] } ]
Both work but then I am stumped at how to capture and pass the count value.
Expected output
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
We are given the array:
arr = [
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
and make the assumption that each hash has a single key and if two hashes have the same (single) key, the value of that key is the same in both hashes.
The first step is create an empty hash to which will add key-value pairs:
h = {}
Now we loop through arr to build the hash h. I've added a puts statement to display intermediate values in the calculation.
arr.each do |g|
k, v = g.first
puts "k=#{k}, v=#{v}"
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
displays:
k=BREAD, v={:price=>1.5, :discount=>true}
k=BREAD, v={:price=>1.5, :discount=>true}
k=MARMITE, v={:price=>1.6, :discount=>false}
and returns:
#=> [{"BREAD" =>{:price=>1.5, :discount=>true}},
# {"BREAD" =>{:price=>1.5, :discount=>true}},
# {"MARMITE"=>{:price=>1.6, :discount=>false}}]
each always returns its receiver (here arr), which is not what we want.
h #=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
is the result we need. See Hash#key? (aka, has_key?), Hash#[], Hash#[]= and Hash#merge.
Now let's wrap this in a method.
def hashify(arr)
h = {}
arr.each do |g|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count=>1 })
end
end
h
end
hashify(arr)
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
Rubyists would often use the method Enumerable#each_with_object to simplify.
def hashify(arr)
arr.each_with_object({}) do |g,h|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
end
Compare the two methods to identify their differences. See Enumerable#each_with_object.
When, as here, the keys are symbols, Ruby allows you to use the shorthand { count: 1 } for { :count=>1 }. Moreover, she permits you to write :count = 1 or count: 1 without the braces when the hash is an argument. For example,
{}.merge('cat'=>'meow', dog:'woof', :pig=>'oink')
#=> {"cat"=>"meow", :dog=>"woof", :pig=>"oink"}
It's probably more common to see the form count: 1 when keys are symbols and for the braces to be omitted when a hash is an argument.
Here's a further refinement you might see. First create
h = arr.group_by { |h| h.keys.first }
#=> {"BREAD" =>[{"BREAD"=>{:price=>1.5, :discount=>true}},
# {"BREAD"=>{:price=>1.5, :discount=>true}}],
# "MARMITE"=>[{"MARMITE"=>{:price=>1.6, :discount=>false}}]}
See Enumerable#group_by. Now convert the values (arrays) to their sizes:
counts = h.transform_values { |arr| arr.size }
#=> {"BREAD"=>2, "MARMITE"=>1}
which can be written in abbreviated form:
counts = h.transform_values(&:size)
#=> {"BREAD"=>2, "MARMITE"=>1}
See Hash#transform_values. We can now write:
uniq_arr = arr.uniq
#=> [{"BREAD"=>{:price=>1.5, :discount=>true}},
#= {"MARMITE"=>{:price=>1.6, :discount=>false}}]
uniq_arr.each_with_object({}) do |g,h|
puts "g=#{g}"
k,v = g.first
puts " k=#{k}, v=#{v}"
h[k] = v.merge(counts: counts[k])
puts " h=#{h}"
end
which displays:
g={"BREAD"=>{:price=>1.5, :discount=>true}}
k=BREAD, v={:price=>1.5, :discount=>true}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2}}
g={"MARMITE"=>{:price=>1.6, :discount=>false}}
k=MARMITE, v={:price=>1.6, :discount=>false}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
"MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
and returns:
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
See Array#uniq.
This did the trick:
arr = [
{ bread: { price: 1.50, discount: true } },
{ bread: { price: 1.50, discount: true } },
{ marmite: { price: 1.60, discount: false } }
]
Get the count for each occurrence of hash, add as key value pair and store:
h = arr.uniq.each { |x| x[x.first.first][:count] = arr.count(x) }
Then convert hashes into arrays, flatten to a single array then construct a hash:
Hash[*h.collect(&:to_a).flatten]
#=> {:bread=>{:price=>1.50, :discount=>true, :count=>2}, :marmite=>{:price=>1.60, :discount=>false, :count=>1}}
Combined a couple of nice ideas from here:
https://raycodingdotnet.wordpress.com/2013/08/05/array-of-hashes-into-single-hash-in-ruby/
and here:
http://carol-nichols.com/2015/08/07/ruby-occurrence-couting/

Performance - Ruby - Compare large array of hashes (dictionary) to primary hash; update resulting value

I'm attempting to compare my data, which is in the format of an array of hashes, with another large array of hashes (~50K server names and tags) which serves as a dictionary. The dictionary is stripped down to only include the absolutely relevant information.
The code I have works but it is quite slow on this scale and I haven't been able to pinpoint why. I've done verbose printing to isolate the issue to a specific statement (tagged via comments below)--when it is commented out, the code runs ~30x faster.
After reviewing the code extensively, I feel like I'm doing something wrong and perhaps Array#select is not the appropriate method for this task. Thank you so much in advance for your help.
Code:
inventory = File.read('inventory_with_50k_names_and_associate_tag.csv')
# Since my CSV is headerless, I'm forcing manual headers
#dictionary_data = CSV.parse(inventory).map do |name|
Hash[ [:name, :tag].zip(name) ]
end
# ...
# API calls to my app to return an array of hashes is not shown (returns '#app_data')
# ...
#app_data.each do |issue|
# Extract base server name from FQDN (e.g. server_name1.sub.uk => server_name1)
derived_name = issue['name'].split('.').first
# THIS IS THE BLOCK OF CODE that slows down execution 30 fold:
#dictionary_data.select do |src_server|
issue['tag'] = src_server[:tag] if src_server[:asset_name].start_with?(derived_name)
end
end
Sample Data Returned from REST API (#app_data):
#app_data = [{'name' => 'server_name1.sub.emea', 'tag' => 'Europe', 'state' => 'Online'}
{'name' => 'server_name2.sub.us', 'tag' => 'US E.', 'state' => 'Online'}
{'name' => 'server_name3.sub.us', 'tag' => 'US W.', 'state' => 'Failover'}]
Sample Dictionary Hash Content:
#dictionary_data = [{:asset_name => 'server_name1-X98765432', :tag => 'Paris, France'}
{:asset_name => 'server_name2-Y45678920', :tag => 'New York, USA'}
{:asset_name => 'server_name3-Z34534224', :tag => 'Portland, USA'}]
Desired Output:
#app_data = [{'name' => 'server_name1', 'tag' => 'Paris, France', 'state' => 'Up'}
{'name' => 'server_name2', 'tag' => 'New York, USA', 'state' => 'Up'}
{'name' => 'server_name3', 'tag' => 'Portland, USA', 'state' => 'F.O'}]
Assuming "no" on both of my questions in the comments:
#!/usr/bin/env ruby
require 'csv'
#dictionary_data = CSV.open('dict_data.csv') { |csv|
Hash[csv.map { |name, tag| [name[/^.+(?=-\w+$)/], tag] }]
}
#app_data = [{'name' => 'server_name1.sub.emea', 'tag' => 'Europe', 'state' => 'Online'},
{'name' => 'server_name2.sub.us', 'tag' => 'US E.', 'state' => 'Online'},
{'name' => 'server_name3.sub.us', 'tag' => 'US W.', 'state' => 'Failover'}]
STATE_MAP = {
'Online' => 'Up',
'Failover' => 'F.O.'
}
#app_data = #app_data.map do |server|
name = server['name'][/^[^.]+/]
{
'name' => name,
'tag' => #dictionary_data[name],
'state' => STATE_MAP[server['state']],
}
end
p #app_data
# => [{"name"=>"server_name1", "tag"=>"Paris, France", "state"=>"Up"},
# {"name"=>"server_name2", "tag"=>"New York, USA", "state"=>"Up"},
# {"name"=>"server_name3", "tag"=>"Portland, USA", "state"=>"F.O."}]
EDIT: I find it more convenient here to read the CSV without headers, as I don't want it to generate an array of hashes. But to read a headerless CSV as if it had headers, you don't need to touch the data itself, as Ruby's CSV is quite powerful:
CSV.read('dict_data.csv', headers: %i(name tag)).map(&:to_hash)

Check for integer in a collection of ranges

I'm trying to find the best way of determining in which range a given integer is.
Take this hash for example:
score_levels = {
1 => {'name' => 'Beginner', 'range' => 0..50},
2 => {'name' => 'Intermediate', 'range' => 51..70},
3 => {'name' => 'Pro', 'range' => 71..85},
4 => {'name' => 'Expert', 'range' => 86..96},
5 => {'name' => 'Master', 'range' => 97..100}
}
I would like to run different logic given a score, something like:
case score
when score_levels[1]['range']
level_counters[1] += 1
when score_levels[2]['range']
level_counters[2] += 1
when score_levels[3]['range']
level_counters[3] += 1
end
Is there a more generic way of doing it?
Maybe something in this spirit:
score_levels.each |key, val| {if val['range'].member?(score) then level_counters[key] += 1 }
Thanks!
Since ranges do not overlap and seamlessly cover 0..100 - you do not need explicit ranges, but rather something like:
score_levels = [
{id:1, name: 'Beginner', max_score:50},
{id:2, name: 'Intermediate', max_score:70},
{id:3, name: 'Pro', max_score:85},
{id:4, name: 'Expert', max_score:96},
{id:5, name: 'Master', max_score:100}
].sort_by{|v| v[:max_score]}
sort_by is optional, but left there to indicate that array should be sorted
And find itself (assumes that score does not exceed maximum and is always found)
level_counters[ score_levels.find{|v| score <= v[:max_score]}[:id] ] += 1
Yes, there is.
level_counters[score_levels.find{|_, h| h["range"].include?(score)}.first] += 1
If you will be looking up levels repeatedly and need to do it efficiently, consider constructing a separate hash:
score_to_level = score_levels.each_with_object({}) { |(k,v),h|
v['range'].each { |v| h[v] = k} }
#=> {0=>1, 1=>1, 2=>1, 3=>1, 4=>1, 5=>1, 6=>1, 7=>1, 8=>1, 9=>1,
# 10=>1,..., 91=>4,
# 92=>4, 93=>4, 94=>4, 95=>4, 96=>4, 97=>5, 98=>5, 99=>5, 100=>5}
This of course assumes each range contains a finite number of values (not (1.1..3.3), for example).

Ruby loops. Hash into string

I am working with Ruby. I need to grab each key/value and put it into a string.
So far I have:
values = ['first' => '1', 'second' => '2']
#thelink = values.collect do | key, value |
"#{key}=#{value}&"
end
When I print #thelink I see:
first1second2=&
But Really what I want is
first=1&second=2
Could anybody help/explain please?
There is something subtle you are missing here {} vs [].
See the below taken from IRB tests:
irb(main):002:0> {'first' => 1, 'second' => 2}
=> {"second"=>2, "first"=>1}
irb(main):003:0> ['first' => 1, 'second' => 2]
=> [{"second"=>2, "first"=>1}]
irb(main):004:0> {'first' => 1, 'second' => 2}.class
=> Hash
irb(main):005:0> ['first' => 1, 'second' => 2].class
=> Array
Similar to this:
irb(main):006:0> {'first' => 1, 'second' => 2}.collect { |key,value| puts "#{key}:#{value}" }
second:2
first:1
=> [nil, nil]
irb(main):007:0> ['first' => 1, 'second' => 2].collect { |key,value| puts "#{key}:#{value}" }
second2first1:
=> [nil]
The array has a single element (a hash) that, as a string, is everything concatenated. This is the important thing to note here.
On the other hand, the hash iterates by handing you the key/value pairs that you are expecting.
Hope that helps.
I think your code has a typo (a hash is delimited by {} not by []). Try this
values = {'first' => '1', 'second' => '2'}
r = values.map{|k,v| "#{k}=#{v}"}.join('&')
puts r
#shows: first=1&second=2

Resources