Check for integer in a collection of ranges - ruby

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).

Related

How to find the most frequent value in array of hashes

I have the array of objects "orders". I want to obtain a first most frequent value in the my array, who often takes a book:
orders = [
{'book' => '1', 'reader' => 'Denis' },
{'book' => '2', 'reader' => 'Mike' },
{'book' => '3', 'reader' => 'Denis' },
{'book' => '3', 'reader' => 'Mike' },
{'book' => '5', 'reader' => '2' }
]
I tried this method, but it's good only for arrays of strings: ['string', 'string'...]:
def most_common_value(a)
a.group_by(&:itself).values.max_by(&:size).first
end
Expected result:
=> "Denis"
I'd do it like this:
orders = [
{'book' => '1', 'reader' => 'Denis' },
{'book' => '2', 'reader' => 'Mike' },
{'book' => '3', 'reader' => 'Denis' },
{'book' => '3', 'reader' => 'Mike' },
{'book' => '5', 'reader' => '2' }
]
orders.each_with_object(Hash.new(0)) { |order, hash| hash[order['reader']] += 1 }.max_by { |k, v| v }
# => ["Denis", 2]
The problem with this is, if there are multiple "max" then the result will be returned based on the order the data is found. For instance, if the order is different:
orders.push(orders.shift)
# => [{"book"=>"2", "reader"=>"Mike"},
# {"book"=>"3", "reader"=>"Denis"},
# {"book"=>"3", "reader"=>"Mike"},
# {"book"=>"5", "reader"=>"2"},
# {"book"=>"1", "reader"=>"Denis"}]
the result changes:
orders.each_with_object(Hash.new(0)) { |order, hash| hash[order['reader']] += 1 }.max_by { |k, v| v }
# => ["Mike", 2]
key = 'reader'
x = orders.inject({}) do |a,i|
a[i[key]] = 0 unless a.has_key? i[key]
a[i[key]] +=1
a
end.max_by{|k,v| v}
Which returns:
=> ["Denis", 2]
I saw this a few days ago and thought this would be easy if Array or Enumerable had a mode_by! Well I finally got around to whipping one up.
Implementing mode_by
A true mode_by would probably return a subarray of the items matching a block:
orders.mode_by{|order| order['reader']}
#=> [{'book'=>'1', 'reader'=>'Denis'}, {'book'=>'3', 'reader'=>'Denis'}]
and to get your desired result:
orders.mode_by{|order| order['reader']}.first['reader']
#=> "Denis"
So let's implement a mode_by:
class Array
def mode_by(&block)
self.group_by(&block).values.max_by(&:size)
end
end
et voila!
A custom alternative
In your case, you don't need to return array elements. Let's simplify further by implementing something that returns exactly what you want, the first result of the block that appears the most. We'll call it mode_of:
class Array
def mode_of(&block)
self.group_by(&block).max_by{|k,v| v.size}.first
end
end
Now you can simply do this:
orders.mode_of{|order| order['reader']}
#=> "Denis"
using group_by and max_by :
orders.group_by { |h| h['reader']}.to_a.max_by {|x| x[1].length}.first
Output :
=> "Denis"

Most performant way to group/summarise two hashes?

I have two hashes with some data that I need to aggregate. The first one is a mapping of which ids (id_1, id_2, id_3, id_4) belong under what category (a, b, c):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
The second hash holds values of how many events happened per id for a given date (date_1, date_2, date_3):
hash_2 = {
'id_1' => {'date_1' => 5, 'date_2' => 6, 'date_3' => 8},
'id_2' => {'date_1' => 0, 'date_3' => 6},
'id_3' => {'date_1' => 0, 'date_2' => nil, 'date_3' => 1},
'id_4' => {'date_1' => 10, 'date_2' => 1}
}
What I want is to get the total event per category (a,b,c). For the above example, the result would look something like:
hash_3 = {'a' => (5+6+8+0+6), 'b' => (0+0+1), 'c' => (10+1)}
My problem is, that there are about 5000 categories, each pointing to typically 1 to 3 ids, and each ID having event counts for 30 dates or more. So this takes quite a bit of computation. What will be the most performant (time effective) way to do this grouping in Ruby?
update
This is what I tried so far (took like 6-8 seconds!, horribly slow):
def total_clicks_per_category
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
end
def total_event_per_ids(ids)
ids.reduce(0) do |memo, id|
events = hash_2.fetch(id, {})
memo + (events.values.reduce(:+) || 0)
end
end
P.S. I’m using Ruby 2.3.
I'm writing this on a phone so I cannot test right now, but it looks OK.
g = hash_2.each_with_object({}) { |(k,v),g| g[k] = v.values.compact.sum }
hash_3 = hash_1.each_with_object({}) { |(k,v),h| h[k] = g.values_at(*v).sum }
First, create an intermediate hash that holds the sum of hash_2:
hash_4 = hash_2.map{|k, v| [k, v.values.inject(:+)]}.to_h
# => {"id_1"=>19, "id_2"=>6, "id_3"=>1, "id_4"=>11}
Then do the final summation:
hash_3 = hash_1.map{|k, v| [k, v.map{|k| hash_4[k]}.inject(:+)]}.to_h
# => {"a"=>25, "b"=>1, "c"=>11}
Theory
5000*3*30 isn't that many. Ruby probably will need a second at most for this kind of job.
Hash lookup is fast by default, you won't be able to optimize much.
You could pre-calculate hash_2_sum, though :
hash_2_sum = {
'id_1' => 5+6+8,
'id_2' => 0+6,
'id_3' => 0+0+1,
'id_4' => 10+1
}
A loop on hash1 with hash_2_sum lookup, and you're done.
Code
Your example has been updated with some nil values. You need to remove them with compact, and make sure the sum is 0 when no element is found with inject(0, :+):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
hash_2 = {
'id_1' => { 'date_1' => 5, 'date_2' => 6, 'date_3' => 8 },
'id_2' => { 'date_1' => 0, 'date_3' => 6 },
'id_3' => { 'date_1' => 0, 'date_2' => nil, 'date_3' => 1 },
'id_4' => { 'date_1' => 10, 'date_2' => 1 }
}
hash_2_sum = hash_2.each_with_object({}) do |(key, dates), sum|
sum[key] = dates.values.compact.inject(0, :+)
end
hash_3 = hash_1.each_with_object({}) do |(key, ids), sum|
sum[key] = hash_2_sum.values_at(*ids).inject(0, :+)
end
# {"a"=>25, "b"=>1, "c"=>11}
Note
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
isn't very readable IMHO.
You can either use each_with_object or Array#to_h :
result = [1, 2, 3].each_with_object({}) do |i, hash|
hash[i] = i * i
end
#=> {1=>1, 2=>4, 3=>9}
result = [1, 2, 3].map { |i| [i, i * i] }.to_h
#=> {1=>1, 2=>4, 3=>9}

I can't find a way to create a simple multidimensional array or hash in Ruby

You Ruby pros will laugh but I'm having such a hard time with this. I've searched and searched and tried a lot of different things but nothing seems right. I guess I'm just used to dealing with arrays in js and php. Here is what I want to do; consider this pseudo code:
i = 0
foreach (items as item) {
myarray[i]['title'] = item['title']
myarray[i]['desc'] = item['desc']
i++
}
Right, so then I can loop through myarray or access 'title' and 'desc' by the index (i). Simplest thing in the world. I've found a few ways to make it work in Ruby but they've all been really messy or confusing. I want to know the right way to do it, and the cleanest.
Unless you are actually updating my_array (which implies that there is probably a better way to do this), you probably want map instead:
items = [
{'title' => 't1', 'desc' => 'd1', 'other' => 'o1'},
{'title' => 't2', 'desc' => 'd2', 'other' => 'o2'},
{'title' => 't3', 'desc' => 'd3', 'other' => 'o3'},
]
my_array = items.map do |item|
{'title' => item['title'], 'desc' => item['desc'] }
end
items # => [{"title"=>"t1", "desc"=>"d1", "other"=>"o1"}, {"title"=>"t2", "desc"=>"d2", "other"=>"o2"}, {"title"=>"t3", "desc"=>"d3", "other"=>"o3"}]
my_array # => [{"title"=>"t1", "desc"=>"d1"}, {"title"=>"t2", "desc"=>"d2"}, {"title"=>"t3", "desc"=>"d3"}]
I'm not quite sure why you are trying to do this, as it seems like items is already an array with hashes inside it, and in my code below, myarray is exactly the same as items.
Try using each_with_index instead of a foreach loop:
items.each_with_index do |item, index|
myarray[index] = item
end
If you have extra attributes in each item, such as a id or something, then you would want to remove those extra attributes before you add the item to myarray.
titles = ["t1", "t2", "t3"]
descs = ["d1", "d2", "d3"]
h= Hash.new
titles.each.with_index{ |v,i| h[i] = {title: "#{v}" } }
puts h[0][:title] #=> t1
puts h #=> {0=>{:title=>"t1"}, 1=>{:title=>"t2"}...}
descs.each.with_index{ |v,i| h[i] = h[i].merge( {desc: "#{v}" } ) }
puts h[0][:desc] #=> d1
puts h #=> {0=>{:title=>"t1", :desc=>"d1"}, 1=>...

Conditional value extraction from ruby hash

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

Ruby / Remove everything after a matched key / array of hashes

Let's say I have the following array of hashes:
h = [{"name" => "bob"}, {"car" => "toyota"}, {"age" => "25"}]
And I have the following key to match:
k = 'car'
How do I match the 'k' to 'h' and have delete every element after the match so that it returns:
h = [{"name" => "bob"}, {"car" => "toyota"}]
Just convert hash to array, do your task and then convert back
h = {"name" => "bob", "car" => "toyota", "age" => "25"}
array = h.to_a.flatten
index = array.index('car') + 1
h = Hash[*array[0..index]]
=> {"name"=>"bob", "car"=>"toyota"}
By the way, the hash is ordered only since Ruby 1.9
ar = [{"name" => "bob"}, {"car" => "toyota"}, {"age" => "25"}]
p ar[0 .. ar.index{|h| h.key?('car')}] #=>[{"name"=>"bob"}, {"car"=>"toyota"}]
I like megas' version, as its short and to the point. Another approach, which would be more explicit, would be iterating over the keys array of each hash. The keys of a hash are maintained in an ordered array (http://ruby-doc.org/core-1.9.3/Hash.html). They are ordered by when they were first entered. As a result, you can try the following:
newArray = Array.new
h.each do |hash| # Iterate through your array of hashes
newArray << hash
if hash.has_key?("car") # check if this hash is the "car" hash.
break # exits the block
end
end
This all depends, of course, on whether the array was created in the proper order. If it was, then you're golden.
A hash is unordered set by definition, so what you request is somewhat undefined. However you can do something like a hack:
h = {"name" => "bob", "car" => "toyota", "age" => "25"}
matched = false
key_given = "car"
h.each do |k,v|
if matched
h.delete(k)
end
if k == key_given
matched = true
next
end
end
I'm pretty late to the party here. I was looking for a solution to this same problem, but I didn't love these answers. So, here's my approach:
class Array
def take_until(&blk)
i = find_index &blk
take(i + 1)
end
end
h = [{"name" => "bob"}, {"car" => "toyota"}, {"age" => "25"}]
k = 'car'
h.take_until { |x| x.has_key?(k) }
=> [{"name"=>"bob"}, {"car"=>"toyota"}]

Resources