Creating a range from one column - ruby-on-rails

I have a column called "Marks" which contains values like
Marks = [100,200,150,157,....]
I need to assign Grades to those marks using the following key
<25=0, <75=1, <125=2, <250=3, <500=4, >500=5
If Marks < 25, then Grade = 0, if marks < 75 then grade = 1.
I can sort the results and find the first record that matches using Ruby's find function. Is it the best method ? Or is there a way by which I can prepare a range using the key by adding Lower Limit and Upper Limit columns to the table and by populating those ranges using the key? Marks can have decimals too Ex: 99.99

Without using Rails, you could do it like this:
marks = [100, 200, 150, 157, 692, 12]
marks_to_grade = { 25=>0, 75=>1, 125=>2, 250=>3, 500=>4, Float::INFINITY=>5 }
Hash[marks.map { |m| [m, marks_to_grade.find { |k,_| m <= k }.last] }]
#=> {100=>2, 200=>3, 150=>3, 157=>3, 692=>5, 12=>0}
With Ruby 2.1, you could write this:
marks.map { |m| [m, marks_to_grade.find { |k,_| m <= k }.last] }.to_h
Here's what's happening:
Enumerable#map (a.k.a collect) converts each mark m to an array [m, g], where g is the grade computed for that mark. For example, when map passes the first element of marks into its block, we have:
m = 100
a = marks_to_grade.find { |k,_| m <= k }
#=> marks_to_grade.find { |k,_| 100 <= k }
#=> [125, 2]
a.last
#=> 2
so the mark 100 is mapped to [100, 2]. (I've replaced the block variable for the value of the key-value pair with the placeholder _ to draw attention to the fact that the value is not being used in the calculation within the block. One could also use, say, _v as the placeholder.) The remaining marks are similarly mapped, resulting in:
b = marks.map { |m| [m, marks_to_grade.find { |k,_| m <= k }.last] }
#=> [[100, 2], [200, 3], [150, 3], [157, 3], [692, 5], [12, 0]]
Lastly
Hash[b]
#=> {100=>2, 200=>3, 150=>3, 157=>3, 692=>5, 12=>0}
or, for Ruby 2.1+
b.to_h
#=> {100=>2, 200=>3, 150=>3, 157=>3, 692=>5, 12=>0}

You can make use of update_all:
Student.where(:mark => 0...25).update_all(grade: 0)
Student.where(:mark => 25...75).update_all(grade: 1)
Student.where(:mark => 75...125).update_all(grade: 2)
Student.where(:mark => 125...250).update_all(grade: 3)
Student.where(:mark => 250...500).update_all(grade: 4)
Student.where("mark > ?", 500).update_all(grade: 5)

Related

How to do a single-line cumulative count for hash values in Ruby?

I've got the following data set:
{
Nov 2020=>1,
Dec 2020=>2,
Jan 2021=>3,
Feb 2021=>4,
Mar 2021=>5,
Apr 2021=>6
}
Using the following code:
cumulative_count = 0
count_data = {}
data_set.each { |k, v| count_data[k] = (cumulative_count += v) }
I'm producing the following set of data:
{
Nov 2020=>1,
Dec 2020=>3,
Jan 2021=>6,
Feb 2021=>10,
Mar 2021=>15,
Apr 2021=>21
}
Even though I've got the each as a single line, I feel like there's got to be some way to do the entire thing as a one-liner. I've tried using inject with no luck.
This would do the trick:
input.each_with_object([]) { |(key, value), arr| arr << [key, arr.empty? ? value : value + arr.last[1]] }.to_h
=> {"Nov 2020"=>1, "Dec 2020"=>3, "Jan 2021"=>6, "Feb 2021"=>10, "Mar 2021"=>15, "Apr 2021"=>21}
for input defined as:
input = {
'Nov 2020' => 1,
'Dec 2020' => 2,
'Jan 2021' => 3,
'Feb 2021' => 4,
'Mar 2021' => 5,
'Apr 2021' => 6
}
The idea is to inject an array (via each_with_object) to keep the processed data, and to allow us to easily get which is value of the the previous pair, and therefore allows us to accumulate the value. At the end, we transform this array into a hash so that we have the data structure we want to have.
Just to add a disclaimer, as the data being processed is a Hash (and therefore not a data structure that preserves order), a full one-liner to consider also a Hash ignoring any possible ordering would be the following:
input.to_a.sort_by { |pair| Date.parse(pair[0]) }.each_with_object([]) { |pair, arr| arr << [pair[0], arr.empty? ? pair[1] : pair[1] + arr.last[1]] }.to_h
=> {"Nov 2020"=>1, "Dec 2020"=>3, "Jan 2021"=>6, "Feb 2021"=>10, "Mar 2021"=>15, "Apr 2021"=>21}
In this case, we apply the same idea, but first converting the original data into an ordered array by date.
input = {
'Nov 2020' => 1,
'Dec 2020' => 2,
'Jan 2021' => 3,
'Feb 2021' => 4,
'Mar 2021' => 5,
'Apr 2021' => 6
}
If it must be on one physical line, and semicolons are allowed:
t = 0; input.each_with_object({}) { |(k, v), a| t += v; a[k] = t }
If it must be on one physical line, and semicolons are not allowed:
input.each_with_object({ t: 0, data: {}}) { |(k, v), a| (a[:t] += v) and (a[:data][k] = a[:t]) }[:data]
But in real practice, I think it's easier to read on multiple physical lines :)
t = 0
input.each_with_object({}) { |(k, v), a|
t += v
a[k] = t
}
TL;DR
This is what I ultimately ended up going with:
input.each_with_object({}) { |(k, v), h| h[k] = v + h.values.last.to_i }
Hats off to Marcos Parreiras (the accepted answer) for pointing me in the direction of each_with_object and the idea to pull the last value for accumulation instead of using += on a cumulative variable initialized as 0.
Details
I ended up with 3 potential solutions (listed below). My original code plus two options utilizing each_with_object – one of which depending on an array and the other on a hash.
Original
cumulative_count = 0
count_data = {}
input.each { |k, v| count_data[k] = (cumulative_count += v) }
Using array
input.each_with_object([]) { |(k, v), a| a << [k, v + a.last&.last.to_i] }.to_h
Using hash
input.each_with_object({}) { |(k, v), h| h[k] = v + h.values.last.to_i }
I settled on the option using the hash because I think it's the cleanest. However, it's worth noting that it's not the most performant. Based purely on performance, the original solution is hands-down the winner. Naturally, they're all extremely fast, so in order to really see the performance difference I had to run the options a very high number of times (displayed below). But since my actual solution will only be run once at a time in Production, I decided to go for succinctness over nanoseconds of performance. :-)
Performance
Each solution was run inside of 2_000_000.times { }.
Original
#<Benchmark::Tms:0x00007fde00fb72d8 #real=2.5452079999959096, #stime=0.09558999999999962, #total=2.5108440000000005, #utime=2.415254000000001>
Using array
#<Benchmark::Tms:0x00007fde0a1f58e8 #real=7.3623509999597445, #stime=0.08986500000000053, #total=7.250730000000002, #utime=7.160865000000001>
Using hash
#<Benchmark::Tms:0x00007f9e19ca7678 #real=5.903417999972589, #stime=0.057482000000000255, #total=5.830285999999999, #utime=5.772803999999999>
input = {
'Nov 2020' => 1,
'Dec 2020' => 2,
'Jan 2021' => 3,
'Feb 2021' => 4,
'Mar 2021' => 5,
'Apr 2021' => 6
}
If, as in the example, the values begin at 1 and each after the first is 1 greater than the previous value (recall key/value insertion order is guaranteed in hashes), the value n is to be converted to 1 + 2 +...+ n, which, being the sum of an arithmetic series, equals the following.
input.transform_values { |v| (1+v)*v/2 }
#=> {"Nov 2020"=>1, "Dec 2020"=>3, "Jan 2021"=>6, "Feb 2021"=>10,
# "Mar 2021"=>15, "Apr 2021"=>21}
Note that this does not require Hash#transform_values to process key-value pairs in any particular order.

How do I split a string given an array of split positions?

I'm using Ruby 2.4 and Rails 5. I have an array of indexes within a line
[5, 8, 10]
How do I take the above array, and a string, and form anotehr array of strings that are split by the above indexes? FOr instance, if the string is
abcdefghijklmn
and split it based ont eh above indexes, I would have an array with the following strings
abcde
fgh
ij
klmn
Try this
str = "abcdefghijklmn"
positions = [5, 8, 10]
parts = [0, *positions, str.size].each_cons(2).map { |a, b| str[a...b] }
# => ["abcde", "fgh", "ij", "klmn"]
Or,
If the positions are constant and known ahead of runtime (for example if they were the format for a phone number or credit card) just use a regexp
str.match(/(.....)(...)(..)(.*)/).captures
# => ["abcde", "fgh", "ij", "klmn"]
This will get the Job done
str = "abcdefghijklmn"
arr_1 = [5, 8, 10]
arr_2, prev = [], 0
(arr_1.length + 1).times do |x|
if arr_1[x] == nil then arr_1[x] = str.size end
arr_2 << str[prev..arr_1[x] -1]
prev = arr_1[x]
end
p arr_2
---------------------------------------
Program Run Output
["abcde", "fgh", "ij", "klmn"]
---------------------------------------
I hope this Helps

Writting a method to check the number of vowels in a string

I have a hash of data that is holding different strings as its Key. I need to create a new method in my class that will count the number of vowels in each key and then return the key with the most vowels. I am very stuck and this is what I have so far.
def favorite_wish
vowels = ["a", "e", "i", "o", "u"]
#submitted_wishes.each_key do |wish|
wish.split(' ')
wish.each do |check|
if check == vowels
end
end
end
Can anyone help?
String#count might help you:
# this will return the key with the max number of vowels
def favorite_wish
#submitted_wishes.keys.max_by { |wish| wish.count('aeiou') }
end
# this will return the value to the key with the max number of vowels
def favorite_wish
max_key = #submitted_wishes.keys.max_by { |wish| wish.count('aeiou') }
#submitted_wishes[max_key]
end
This will get the key with the most vowels:
#submitted_wishes.keys.max_by { |key| key.count('aeiou') }
I would use the following methods:
def count_vowels(str)
str.count 'aeiou'
end
def highest_value_key(hash)
hash.key(hash.values.max)
end
The idea behind these methods is to separate concerns and make it more readable.
h = { "Mary"=>"Mary", "quite"=>"contrary", "how"=>"does your", "garden"=>"grow?" }
h.map { |k,_| [k.count('aeiou'), k] }.max.last
#=> => "quite"
The steps:
a = h.map { |k,_| [k.count('aeiou'), k] }
#=> [[1, "Mary"], [3, "quite"], [1, "how"], [2, "garden"]]
b = a.max
#=> [3, "quite"]
b.last
#=> "quite"
See Array#<=> for an explanation of how arrays are ordered (when computing max).
If keys k1 and k2 tie for the maximum number of vowels, k1 <=> k2 breaks the tie (k1 is returned if k1 <=> k2 #=> -1, k2 is returned if k1 <=> k2 #=> 1, either key might be returned if k1 <=> k2 #=> 0. See String#<=>.

What's a good way to create a string array in Ruby based on integer variables?

The integer variables are:
toonie = 2, loonie = 1, quarter = 1, dime = 0, nickel = 1, penny = 3
I want the final output to be
"2 toonies, 1 loonie, 1 quarter, 1 nickel, 3 pennies"
Is there a way to interpolate this all from Ruby code inside [] array brackets and then add .join(", ")?
Or will I have to declare an empty array first, and then write some Ruby code to add to the array if the integer variable is greater than 0?
I would do something like this:
coins = { toonie: 2, loonie: 1, quarter: 1, dime: 0, nickel: 1, penny: 3 }
coins.map { |k, v| pluralize(v, k) if v > 0 }.compact.join(', ')
#=> "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
Note that pluralize is a ActionView::Helpers::TextHelper method. Therefore it is only available in views and helpers.
When you want to use your example outside of views, you might want to use pluralize from ActiveSupport instead - what makes the solution slightly longer:
coins.map { |k, v| "#{v} #{v == 1 ? k : k.pluralize}" if v > 0 }.compact.join(', ')
#=> "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
Can be done in rails:
hash = {
"toonie" => 2,
"loonie" => 1,
"quarter" => 1,
"dime" => 0,
"nickel" => 1,
"penny" => 3
}
hash.to_a.map { |ele| "#{ele.last} #{ele.last> 1 ? ele.first.pluralize : ele.first}" }.join(", ")
Basically what you do is convert the hash to an array, which will look like this:
[["toonie", 2], ["loonie", 1], ["quarter", 1], ["dime", 0], ["nickel", 1], ["penny", 3]]
Then you map each element to the function provided, which takes the inner array, takes the numeric value in the last entry, places it in a string and then adds the plural or singular value based on the numeric value you just checked. And finally merge it all together
=> "2 toonies, 1 loonie, 1 quarter, 1 nickel, 3 pennies"
I'm not sure what exactly you're looking for, but I would start with a hash like:
coins = {"toonie" => 2, "loonie" => 1, "quarter" => 1, "dime" => 0, "nickel" => 1, "penny" => 3}
then you can use this to print the counts
def coin_counts(coins)
(coins.keys.select { |coin| coins[coin] > 0}.map {|coin| coins[coin].to_s + " " + coin}).join(", ")
end
If you would like appropriate pluralizing, you can do the following:
include ActionView::Helpers::TextHelper
def coin_counts(coins)
(coins.keys.select { |coin| coins[coin] > 0}.map {|coin| pluralize(coins[coin], coin)}).join(", ")
end
This is just for fun and should not be used in production but you can achieve it like
def run
toonie = 2
loonie = 1
quarter = 1
dime = 0
nickel = 1
penny = 3
Kernel::local_variables.each_with_object([]) { |var, array|
next if eval(var.to_s).to_i.zero?
array << "#{eval(var.to_s)} #{var}"
}.join(', ')
end
run # returns "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
The above does not implement the pluralization requirement because it really depends if you will have irregular plural nouns or whatever.
I would go with a hash solution as described in the other answers

about the use of inject in ruby

please somebody if you can explain the 3 rd line of this code. This method is to subtract an array of numbers starting from 2nd no. subtracted from 1st and the 3rd no. subtracted from the resultant and so on...
def subtract(*numbers)
sum = numbers.shift
numbers.inject(sum) { |sum, number| sum - number }
end
http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-inject
What inject does is takes some initial value (sum) and applies some operation to it for each element of your enumerable. So here, we take 1, and subtract the first element from 1, and the subtract the second element from that result, etc...
So below we have 1 - 2 = -1, then -1 - 3 = -4.
>> numbers=[1,2,3]
=> [1, 2, 3]
>> sum = numbers.shift
=> 1
>> numbers
=> [2, 3]
>> numbers.inject(sum) { |sum, number| sum - number }
=> -4

Resources