Group users by age range in rails - ruby-on-rails

Based on 'PESEL" number I have to group user by their age. I created something like this and it is working, but... To be honest, it look bad for me.
HELPER:
def years(pesel)
years = (0..99).to_a
birth_year = []
case pesel[2..3].to_i
when 0..19
20.times do |index|
first_number = index % 2 == 0 ? (5 * index) : ((5 * index))
second_number = index % 2 == 0 ? (5 * index + 4) : ((5 * index) + 4)
first_year = Date.today.year - second_number.to_s.rjust(4,'1900').to_i
second_year = Date.today.year - first_number.to_s.rjust(4,'1900').to_i
birth_year += ["#{first_year}-#{second_year}"]
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
hash = Hash[years.zip multiplied_birth_years]
hash.fetch(pesel[0..1].to_i)
when 20..39
20.times do |index|
first_number = index % 2 == 0 ? (5 * index) : ((5 * index))
second_number = index % 2 == 0 ? (5 * index + 4) : ((5 * index) + 4)
first_year = Date.today.year - second_number.to_s.rjust(4,'2000').to_i
second_year = Date.today.year - first_number.to_s.rjust(4,'2000').to_i
birth_year += ["#{first_year}-#{second_year}"]
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
hash = Hash[years.zip multiplied_birth_years]
hash.fetch(pesel[0..1].to_i)
when 40..59
20.times do |index|
first_number = index % 2 == 0 ? (5 * index) : ((5 * index))
second_number = index % 2 == 0 ? (5 * index + 4) : ((5 * index) + 4)
first_year = Date.today.year - second_number.to_s.rjust(4,'2100').to_i
second_year = Date.today.year - first_number.to_s.rjust(4,'2100').to_i
birth_year += ["#{first_year}-#{second_year}"]
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
hash = Hash[years.zip multiplied_birth_years]
hash.fetch(pesel[0..1].to_i)
end
end
CONTROLLER:
def grouped_by_age
#yearsbook = #study_participations.includes(user: :profile).group_by do |study_participation|
years(study_participation.user.profile.pesel)
end
end
A small explanation and example. I am interested in first 6 numbers that correspond sequentially: Year of birth, month, day
So if my PESEL == '980129(...)', then I was born twenty-ninth of January 1998
If someone was born in year 2000, then we add 20 to pesel-month number(for example '002129(...)' it is twenty-ninth of January 2000. If someone was born 2100, then we add 40 to pesel-month number.
I have explained what the pesel number is all about, now what I want to do with it.
I need to group users by their age range. Function from above returns has like this:
{0=>"118-122",
1=>"118-122",
2=>"118-122",
3=>"118-122",
4=>"118-122",
5=>"113-117",
6=>"113-117",
7=>"113-117",
8=>"113-117",
9=>"113-117",
10=>"108-112",
11=>"108-112",
12=>"108-112",
13=>"108-112",
14=>"108-112",
15=>"103-107",
16=>"103-107",
17=>"103-107",
18=>"103-107",
19=>"103-107",(...)}
Unfortunately this is not very efficient, because for each user (4000 max) I have to execute the functions from scratch. Is there any way to increase efficiency of this? I thought about storing this hash as const and changing it once a year, but I don't really know how to do that or if it is possible.
EDIT:
Forgot to mention: I need to compare user age with hash, so I can extract age range
EDIT2:
Based on #yoones answer I created something like this:
HELPER:
def years_cache
years = []
201.times do |index|
years += [Date.today.year - (1900 + index)]
end
birth_year = []
60.times do |index|
year = if index < 20
'1900'
elsif index < 40
'2000'
else
'2100'
end
first_number = 5 * (index % 20)
second_number = (5 * (index % 20)) + 4
first_year = Date.today.year - second_number.to_s.rjust(4, year).to_i
second_year = Date.today.year - first_number.to_s.rjust(4, year).to_i
birth_year += ["#{first_year}-#{second_year}"]
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
#hash = (years.zip multiplied_birth_years).to_h
end
def years(cache, pesel)
day = pesel[4..5]
case pesel[2..3].to_i
when 0..19
month = pesel[2..3]
year = pesel[0..1].prepend('19')
when 20..39
month = (pesel[2..3].to_i - 20).to_s
year = pesel[0..1].prepend('20')
when 40..59
month = (pesel[2..3].to_i - 40).to_s
year = pesel[0..1].prepend('21')
end
birth_date = Time.strptime("#{day}/#{month}/#{year}", '%d/%m/%Y')
age = ((Time.zone.now - birth_date) / 1.year.seconds).floor
cache.fetch(age)
end
CONTROLLER:
def grouped_by_age
cache = years_cache()
#yearsbook = #study_participations.includes(user: :profile).group_by do |study_participation|
years(cache, study_participation.user.profile.pesel)
end
end

Instead of doing the complicated calculating of birth date from PESEL every time you want to view the page, do it once and store it in the database. Having a birth date column on the user makes a lot of sense.
Then when you want to group them, you can even do it via the database. If you still need to do it in ruby, then getting the birth year is as easy as user.birth_date.year
In order to then group users into ranges of 5 years according to age, add an age_range method to the model and group by that.
#study_participations.includes(user: :profile).group_by do |study_participation|
study_participation.user.age_range
end
Where age_range can be for example
def age_range
(Date.today.year - birth_date.year) / 5) * 5
end
Format that however you like

I guess you could at least build the cache once then use it in your loop. The following code is not pretty, it's just to illustrate what I mean:
def build_year_cache(index, rjust_str)
first_number = 5 * index
second_number = index % 2 == 0 ? (5 * index + 4) : ((5 * index) + 4)
first_year = Date.today.year - second_number.to_s.rjust(4, rjust_str).to_i
second_year = Date.today.year - first_number.to_s.rjust(4, rjust_str).to_i
"#{first_year}-#{second_year}"
end
def build_years_cache
cache = {}
years = (0..99).to_a
[
[0..19, '1900'],
[20..39, '2000'],
[40..59, '2100']
].each do |range, rjust_str|
birth_year = []
20.times do |index|
birth_year.append(build_year_cache(index, rjust_str))
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
cache[range] = Hash[years.zip multiplied_birth_years]
end
cache
end
def years(pesel, cache)
year = pesel[0..1].to_i
month = pesel[2..3].to_i
range = cache.keys.find { |k, v| k.include?(month) }
cache[range].fetch(year)
end
def grouped_by_age
cache = build_years_cache
#yearsbook = #study_participations.includes(user: :profile).group_by do |study_participation|
years(study_participation.user.profile.pesel, cache)
end
end

Related

Lua Script: Convert Multiple "if" into simpler form

I am trying to make a condition where the percentage would be calculated based on the number of fan operated and the amount of airflow. This is what I come out with
function System01()
CFM_SHOP1 = addr_getword("#W_HDW1")
CFM_SHOP2 = addr_getword("#W_HDW2")
STATUS_SHOP1 = addr_getbit("#B_M1")
STATUS_SHOP2 = addr_getbit("#B_M2")
OUTPUT_SHOP1 = addr_getword("#W_HDW10")
OUTPUT_SHOP2 = addr_getword("#W_HDW11")
CFM_1 = CFM_SHOP1 + CFM_SHOP2
if STATUS_SHOP1 == 1 then
OUTPUT_SHOP1 = CFM_SHOP1 * 10000 / CFM_1
addr_setword("#W_HDW10", OUTPUT_SHOP1)
if STATUS_SHOP2 == 1 then
OUTPUT_SHOP2 = CFM_SHOP2 * 10000 / CFM_1
addr_setword("#W_HDW11", OUTPUT_SHOP2)
end
TOTAL_1 = OUTPUT_SHOP1 + OUTPUT_SHOP2
addr_setword("#W_HDW19", TOTAL_1)
end
if STATUS_SHOP2 == 1 then
OUTPUT_SHOP2 = CFM_SHOP2 * 10000 / CFM_1
addr_setword("#W_HDW11", OUTPUT_SHOP2)
if STATUS_SHOP1 == 1 then
OUTPUT_SHOP1 = CFM_SHOP1 * 10000 / CFM_1
addr_setword("#W_HDW10", OUTPUT_SHOP1)
end
TOTAL_1 = OUTPUT_SHOP1 + OUTPUT_SHOP2
addr_setword("#W_HDW19", TOTAL_1)
end
addr_setbit("#B_M1", STATUS_SHOP1)
addr_setbit("#B_M2", STATUS_SHOP2)
addr_setbit("#B_M3", STATUS_SHOP3)
end
Is there any way that I can simplified it? Please note that this is only two example I give. There is total of 9 fan so it will be really complicated if i just use 'if'. Thanks in advance
To simplify the code use for-loop
function System01()
local CFM_SHOP = {}
local CFM = 0
for j = 1, 9 do
CFM_SHOP[j] = addr_getword("#W_HDW"..tostring(j))
CFM = CFM + CFM_SHOP[j]
end
local STATUS_SHOP = {}
for j = 1, 9 do
STATUS_SHOP[j] = addr_getbit("#B_M"..tostring(j))
end
local OUTPUT_SHOP = {}
for j = 1, 9 do
OUTPUT_SHOP[j] = addr_getword("#W_HDW"..tostring(j+9))
end
local TOTAL = 0
for j = 1, 9 do
if STATUS_SHOP[j] == 1 then
OUTPUT_SHOP[j] = CFM_SHOP[j] * 10000 / CFM
addr_setword("#W_HDW"..tostring(j+9), OUTPUT_SHOP[j])
end
TOTAL = TOTAL + OUTPUT_SHOP[j]
end
addr_setword("#W_HDW19", TOTAL)
for j = 1, 9 do
addr_setbit("#B_M"..tostring(j), STATUS_SHOP[j])
end
end

Order date on day and month

I have users with a birth_date and now I want to query all users who have their birthday within the next 10 days. I can't just order on birth_date because then I will get this:
01-01-1960
18-12-1975
16-12-1998
Instead of the desired result:
16-12-1998
18-12-1975
01-01-1960
So how can I only order on the day and month, but not the year?
This works in postgreSQL...
start_month = Date.today.month
start_day = Date.today.day
end_month = (Date.today + 10).month
end_day = (Date.today + 10).day
if start_month == end_month
#users = User.where("DATE_PART('month', birth_date) = ? AND DATE_PART('day', birth_date) >= ? AND DATE_PART('day', birth_date) <= ?", start_month, start_day, end_day)
else
#users = User.where("(DATE_PART('month', birth_date) = ? AND DATE_PART('day', birth_date) >= ?) OR (DATE_PART('month', birth_date) = ? AND DATE_PART('day', birth_date) <= ?)", start_month, start_day, end_month, end_day)
end
#users.order("DATE_PART('month', birth_date), DATE_PART('day', birth_date)")
This selects all records with birthdays within the next 10 days and sorts them.
If ten days in the future is still the same month (say on December 14) it selects December between 14 and 24... if it's in a future month (say on December 25) it selects December to end of month and January from beginning to the 4th.
A ruby implementation. Note that it may not be as performant as the DB version...
ids_of_next_10_days = User.all.pluck(:id, :birth_date).map do |user_info|
next_birthday = user_info[1].change(year: Time.now.year)
next_birthday = next_birthday.change(year: Time.now.year + 1) if next_birthday < Date.today
next_birthday.between?(Date.today, Date.today + 10) ? user_info[0] : nil
end.compact
User.where(id: ids_of_next_10_days)
On the Ruby side you can select needed users at first(Postgresql to_char is used):
today = Date.current
dates = (today ... today + 10.days).map { |d| d.strftime('%m%d') }
dates << '0229' if dates.include?('0228') # update by SteveTurczyn
users = User.where("to_char(birth_date, 'MMDD') in (?)", dates).to_a
then sort them:
users.sort_by! do |user|
user_yday = user.birth_date.yday
user_yday >= today.yday ? user_yday : user_yday + 366
end

How can I make query simpler?

I am creating a Rails 5 app.
In this app I got a method that gets values from child objects and adds them to an hash. The below method/code works perfectly fine but I how can I make it better in terms of speed and structure?
def generated_values(period, year, month, quarter)
count = 0
score = 0
actual = 0
goal = 0
red = 0
if stype == "measure"
measures.period(period, year, month, quarter).each do |measure|
count += 1
score += measure.score
actual += measure.value_actual
goal += measure.value_goal
red += measure.value_redflag
end
elsif stype == "objective"
children.each do |child|
child.measures.period(period, year, month, quarter).each do |measure|
count += 1
score += measure.score
actual += measure.value_actual
goal += measure.value_goal
red += measure.value_redflag
end
end
elsif stype == "scorecard"
children.each do |child|
child.children.each do |child2|
child2.measures.period(period, year, month, quarter).each do |measure|
count += 1
score += measure.score
actual += measure.value_actual
goal += measure.value_goal
red += measure.value_redflag
end
end
end
end
values = { :score => score == 0 ? 0 : (score / count).round, :actual => actual, :goal => goal, :red => red }
end
I think I would be tempted to do something like:
def generated_values(period, year, month, quarter)
case stype
when "measure"
selected_measures = measures
when "objective"
selected_measures = child_measures
when "scorecard"
selected_measures = grandchild_measures
end
count = selected_measures.count
{
score: count > 0 ? (selected_measures.sum(:score)/count).round : 0,
actual: selected_measures.sum(:value_actual)
goal: selected_measures.sum(:value_goal)
red: selected_measures.sum(:value_redflag)
}
end
Untested and off-the-cuff.
Naturally, the most interesting part is the selected_measures = bits. But, you haven't provided enough information to help with the proper formulation of those queries.

Opposite of Ruby's number_to_human

Looking to work with a dataset of strings that store money amounts in these formats. For example:
$217.3M
$1.6B
$34M
€1M
€2.8B
I looked at the money gem but it doesn't look like it handles the "M, B, k"'s back to numbers. Looking for a gem that does do that so I can convert exchange rates and compare quantities. I need the opposite of the number_to_human method.
I would start with something like this:
MULTIPLIERS = { 'k' => 10**3, 'm' => 10**6, 'b' => 10**9 }
def human_to_number(human)
number = human[/(\d+\.?)+/].to_f
factor = human[/\w$/].try(:downcase)
number * MULTIPLIERS.fetch(factor, 1)
end
human_to_number('$217.3M') #=> 217300000.0
human_to_number('$1.6B') #=> 1600000000.0
human_to_number('$34M') #=> 34000000.0
human_to_number('€1M') #=> 1000000.0
human_to_number('€2.8B') #=> 2800000000.0
human_to_number('1000') #=> 1000.0
human_to_number('10.88') #=> 10.88
I decided to not be lazy and actually write my own function if anyone else wants this:
def text_to_money(text)
returnarray = []
if (text.count('k') >= 1 || text.count('K') >= 1)
multiplier = 1000
elsif (text.count('M') >= 1 || text.count('m') >= 1)
multiplier = 1000000
elsif (text.count('B') >= 1 || text.count('b') >= 1)
multiplier = 1000000000
else
multiplier = 1
end
num = text.to_s.gsub(/[$,]/,'').to_f
total = num * multiplier
returnarray << [text[0], total]
return returnarray
end
Thanks for the help!

How can I write this conditional in fewer lines?

I wrote this code in my model:
percentage = 0
if self.date_of_birth.present?
percentage += 15
end
if self.gender.present?
percentage += 15
end
if self.relationship_status.present?
percentage += 10
end
if self.language.present?
percentage += 10
end
if self.qualification.present?
percentage += 10
end
if self.interests.present?
if self.interests.count >= 10
percentage += 10
else
percentage += self.interests.count * 5
end
end
But it does not look good. It is a lot of code for a small thing. I want to reduce the number of lines.
You can do it inline, like this:
percentage += 15 if self.date_of_birth.present?
Instead of this:
if self.interests.count >= 10
percentage += 10
else
percentage += self.interests.count*5
end
You can use a ternary operator:
percentage += self.interests.count >= 10 ? 10 : self.interests.count*5
percentage = [
(15 if date_of_birth.present?),
(15 if gender.present?),
(10 if relationship_status.present?),
(10 if language.present?),
(10 if qualification.present?),
((counts = interests.count.to_i) >= 10 ? 10 : (counts * 5)),
].compact.sum
You could use an instance method in your model:
#app/models/model.rb
class Model < ActiveRecord::Base
def percentage
value = 0
values = [[:date_of_birth, 15], [:gender, 15], [:relationship_status,10], [:language,10], [:qualification, 10]]
values.each do |attr,val|
value += val if self.send(attr).present?
end
value += self.interests.count >= 10 ? 10 : self.interests.count*5 if self.interests.present?
# Rails should return the value of the last line, which is the "value" var
end
end
This would allow you to use #user.percentage, where #user is your instance var for the model.
Personally, I don't think that "less lines" is a good idea, but if you want your code in less lines, you can write it like this:
percentage = 0; if date_of_birth.present? then percentage += 15 end; if gender.present? then percentage += 15 end; if relationship_status.present? then percentage += 10 end; if language.present? then percentage += 10 end; if qualification.present? then percentage += 10 end; if interests.present? then if interests.count >= 10 then percentage += 10 else percentage += interests.count*5 end end
In Ruby, you can (almost) always replace linebreaks with semicolons to make your code fit on less lines. In fact, every Ruby program can always be written on a single line.
inc_att = ["date_of_birth", "gender", "relationship_status" , "language", "qualification", "interests"]
inc_att.each do |s|
if self[s].present? && (s == "date_of_birth" || s == "gender")
percentage += 15
elsif self[s].present? && s == "interests" && self[s].count < 10
percentage += self[s].count * 5
else
percentage += 10 if self[s].present?
end
end
Have a look into it
inc_att = ["date_of_birth", "gender", "relationship_status" , "language", "qualification", "interests"]
inc_att.each do |s|
if self[s].present? && (s == "date_of_birth" || s == "gender")
percentage += 15
elsif self[s].present? && s == "interests" && self[s].count < 10
percentage += self[s].count * 5
else
percentage += 10 if self[s].present?
end
end
Inspired by #sawa's answer:
counts = interests.count.to_i
percentage = (counts >= 10 ? 10 : (counts * 5)) +
[
date_of_birth.present? && 15,
gender.present? && 15,
relationship_status.present? && 10,
language.present? && 10,
qualification.present? && 10,
].select(&:itself).sum

Resources