Assigning strings to ranges of numbers - ruby-on-rails

This is a really simple problem. I have the following code:
def age_color
age = Time.now() - created_at
age_color = 'green' if age < 2.days
age_color = 'yellow' if age >= 2.days && age <= 5.days
age_color = 'red' if age > 5.days
end
which is not working properly anyway. I think it's ugly and reminds me of my PHP days. How can I write this more elegantly? It must never be nil.

Your construction doesn't work because you put it in wrong order. Try this
def age_color
if created_at < 5.days.ago then 'red'
elsif created_at > 2.days.ago then 'green'
else 'yellow'
end

You could use case:
age_in_days = (Time.now() - created_at).days
age_color = case age_in_days
when 0..1: 'green'
when 2..5: 'yellow'
else 'red'
end
The days method: http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html#M000322

Related

Greater/Lower than works but equals doesn't on >= and <= on .where()

I have a Rails 4 app and I'm trying to make a simple search for my invoices with 3 optional arguments: Name of the client, Start Date, End Date.
The search works fine mostly, if I put a start date and an end date it works for < and >, but eventhough i used >= and <=, if the invoice date is the same to either start or end, it just won't show on the result list.
The tables used look like this:
Client Table
ID
Name
The rest of the fields aren't necessary
Invoice Table
ID
Client_ID
Total_Price
Created_At *only here for relevance*
My Invoice Controller Search method looks like this:
def search
if request.post?
#count = 0
#invoices = Invoice.all
if params[:start_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:start_date], 'start')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:end_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:end_date], 'end')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:name].present?
#invoices = Invoice.invoices_by_client(#invoices, params[:name])
if #invoices.present?
#count = 1
else
#count = 2
end
end
if #count == 2
flash.now[:danger] = "No results found."
#invoices = nil
end
#name = params[:name]
#start_date = params[:start_date]
#end_date = params[:end_date]
end
end
And the Invoice Model methods i use look like this:
def self.invoices_by_client(invoices, name)
invoices= invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("clients.name LIKE ?", "%#{name}%")
.references(:client)
return invoices
end
def self.invoices_by_date(invoices, date, modifier)
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", date)
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", date)
.references(:client)
end
return invoices
end
It probably isn't the best solution overall and I don't know if i did anything wrong so it would be great if you guys could help me with this.
I followed Alejandro's advice and messed around with the time aswell as the date, something like this:
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", "#{date} 00:00:00") // Added the start time
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", "#{date} 23:59:59") // Added end time aswell
.references(:client)
end
I forced the time for the start date as 00:00:00 and the time for the end date as 23:59:59 and it worked as desired. Thank you for the help man and i hope this helps other people!

Ruby on Rails, Select Users with age range from Active Record

User.rb
# Attributes
# (..)
# birthdate (string)
# format "mm/yyyy"
def age
dob = self.birthdate.to_date
now = Time.now.utc.to_date
now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end
In the console:
irb(main):002:0> current_user.age
=> 7
I'd be able to do the following:
age_range = "25-65"
User.where(:age => between age_range)
I'm stuck at the point how to get the value from age (class method) into the where call
First of all: use a date as type for birthdate in the database.
Then you can just use:
User.where(birthdate: 65.years.ago..25.years.ago)
If you can't change the birthdate type convert it using SQL (example with PostrgeSQL):
User.where('to_date(birthdate, 'MM/YYYY') between ? and ?', 65.years.ago, 25.years.ago)
But you may still have to correct it since you don't have the exact day and only the month.
With PostgreSQL you can use that Rails scope
scope :for_age_range, -> min, max {
where("date_part('year', age(birthdate)) >= ? AND date_part('year', age(birthdate)) <= ?", min, max)
}
User.for_age_range(18, 24)

Calculate Age and place inside age group by Date of Birth

I'm developing a tournament bracketing app and need to compare some dates together in order to place them in their designated age group. I can't seem to figure out how I would write something like this.
6 - under = 2007-09-01 to present
8 - under = 2005-09-01 to 2007-08-31
10 - under = 2003-09-01 to 20050831
Would it be something like this? and is there a better way to compare the dates to each other.
def age_group
if self.dob <= 20030901
"10"
elsif self.dob <= 20050901
"8"
else self.dob <= 20070901
"6"
end
end
Thank you
You can certainly keep to your age_group method, there's nothing wrong with it. I'd just tweak it like so:
def age_group
if self.dob <= 10.years.ago
"10"
elsif self.dob <= 8.years.ago
"8"
elsif self.dob <= 6.years.ago
"6"
end
end
If you do it like this, you will have to go update the cut-off dates of birth each year. You could calculate the age instead and take it from there:
def age
now = Time.now.utc.to_date
now.year - birthday.year - (birthday.to_date.change(:year => now.year) > now ? 1 : 0)
end
(above from Get person's age in Ruby)
Then define the age group:
def age_group
if self.age <= 10
"10"
elsif ...
etc.
A case statement would work well for this, as it uses === for comparisons.
require 'date'
R10U = (Date.parse("2003-09-01")..Date.parse("2005-08-31"))
R8U = (Date.parse("2005-09-01")..Date.parse("2007-08-31"))
R6U = (Date.parse("2007-09-01")..Date.today)
def age_group(dob)
case Date.parse(dob)
when R6U then "6 - under"
when R8U then "8 - under"
when R10U then "10 - under"
else raise ArgumentError, "dob = '#{dob}' is out-of-range"
end
end
age_group("2006-04-12")
#=> "8 - under"
age_group("2004-11-15")
#=> "10 - under"
age_group("2011-06-01")
#=> "6 - under"
age_group("2002-04-30")
#=> ArgumentError: dob = '2002-04-30' is out-of-range
age_group("2015-06-01")
#=> ArgumentError: dob = '2015-06-01' is out-of-range

Nested ActiveRecords: Find many childrens of many parents

In my Rails 3.2 app a Connector has_many Incidents.
To get all incidents of a certain connector I can do this:
(In console)
c = Connector.find(1) # c.class is Connector(id: integer, name: string, ...
i = c.incidents.all # all good, lists incidents of c
But how can I get all incidents of many connectors?
c = Connector.find(1,2) # works fine, but c.class is Array
i = c.incidents.all #=> NoMethodError: undefined method `incidents' for #<Array:0x4cc15e0>
Should be easy! But I don't get it!
Here’s the complete code in my statistics_controller.rb
class StatisticsController < ApplicationController
def index
#connectors = Connector.scoped
if params['connector_tokens']
logger.debug "Following tokens are given: #{params['connector_tokens']}"
#connectors = #connectors.find_all_by_name(params[:connector_tokens].split(','))
end
#start_at = params[:start_at] || 4.weeks.ago.beginning_of_week
#end_at = params[:end_at] || Time.now
##time_line_data = Incident.time_line_data( #start_at, #end_at, 10) #=> That works, but doesn’t limit the result to given connectors
#time_line_data = #connectors.incidents.time_line_data( #start_at, #end_at, 10) #=> undefined method `incidents' for #<ActiveRecord::Relation:0x3f643c8>
respond_to do |format|
format.html # index.html.haml
end
end
end
Edit with reference to first 3 answers below:
Great! With code below I get an array with all incidents of given connectors.
c = Connector.find(1,2)
i = c.map(&:incidents.all).flatten
But idealy I'd like to get an Active Records object instead of the array, because I'd like to call where() on it as you can see in methode time_line_data below.
I could reach my goal with the array, but I would need to change the whole strategy...
This is my time_line_data() in Incidents Model models/incidents.rb
def self.time_line_data(start_at = 8.weeks.ago, end_at = Time.now, lim = 10)
total = {}
rickshaw = []
arr = []
inc = where(created_at: start_at.to_time.beginning_of_day..end_at.to_time.end_of_day)
# create a hash, number of incidents per day, with day as key
inc.each do |i|
if total[i.created_at.to_date].to_i > 0
total[i.created_at.to_date] += 1
else
total[i.created_at.to_date] = 1
end
end
# create a hash with all days in given timeframe, number of incidents per day, date as key and 0 as value if no incident is in database for this day
(start_at.to_date..end_at.to_date).each do |date|
js_timestamp = date.to_time.to_i
if total[date].to_i > 0
arr.push([js_timestamp, total[date]])
rickshaw.push({x: js_timestamp, y: total[date]})
else
arr.push([js_timestamp, 0])
rickshaw.push({x: js_timestamp, y: 0})
end
end
{ :start_at => start_at,
:end_at => end_at,
:series => rickshaw #arr
}
end
As you only seem to be interested in the time line data you can further expand the map examples given before e.g.:
#time_line_data = #connectors.map do |connector|
connector.incidents.map do |incident|
incident.time_line_data(#start_at, #end_at, 10)
end
end
This will map/collect all the return values of the time_line_data method call on all the incidents in the collection of connectors.
Ref:- map
c = Connector.find(1,2)
i = c.map(&:incidents.all).flatten

Get person's age in Ruby

I'd like to get a person's age from its birthday. now - birthday / 365 doesn't work, because some years have 366 days. I came up with the following code:
now = Date.today
year = now.year - birth_date.year
if (date+year.year) > now
year = year - 1
end
Is there a more Ruby'ish way to calculate age?
I know I'm late to the party here, but the accepted answer will break horribly when trying to work out the age of someone born on the 29th February on a leap year. This is because the call to birthday.to_date.change(:year => now.year) creates an invalid date.
I used the following code instead:
require 'date'
def age(dob)
now = Time.now.utc.to_date
now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end
I've found this solution to work well and be readable for other people:
age = Date.today.year - birthday.year
age -= 1 if Date.today < birthday + age.years #for days before birthday
Easy and you don't need to worry about handling leap year and such.
Use this:
def age
now = Time.now.utc.to_date
now.year - birthday.year - (birthday.to_date.change(:year => now.year) > now ? 1 : 0)
end
One liner in Ruby on Rails (ActiveSupport). Handles leap years, leap seconds and all.
def age(birthday)
(Time.now.to_fs(:number).to_i - birthday.to_time.to_fs(:number).to_i)/10e9.to_i
end
Logic from here - How do I calculate someone's age based on a DateTime type birthday?
Assuming both dates are in same timezone, if not call utc() before to_fs() on both.
(Date.today.strftime('%Y%m%d').to_i - dob.strftime('%Y%m%d').to_i) / 10000
My suggestion:
def age(birthday)
((Time.now - birthday.to_time)/(60*60*24*365)).floor
end
The trick is that the minus operation with Time returns seconds
The answers so far are kinda weird. Your original attempt was pretty close to the right way to do this:
birthday = DateTime.new(1900, 1, 1)
age = (DateTime.now - birthday) / 365.25 # or (1.year / 1.day)
You will get a fractional result, so feel free to convert the result to an integer with to_i. This is a better solution because it correctly treats the date difference as a time period measured in days (or seconds in the case of the related Time class) since the event. Then a simple division by the number of days in a year gives you the age. When calculating age in years this way, as long as you retain the original DOB value, no allowance needs to be made for leap years.
This answer is the best, upvote it instead.
I like #philnash's solution, but the conditional could be compacter. What that boolean expression does is comparing [month, day] pairs using lexicographic order, so one could just use ruby's string comparison instead:
def age(dob)
now = Date.today
now.year - dob.year - (now.strftime('%m%d') < dob.strftime('%m%d') ? 1 : 0)
end
I like this one:
now = Date.current
age = now.year - dob.year
age -= 1 if now.yday < dob.yday
This is a conversion of this answer (it's received a lot of votes):
# convert dates to yyyymmdd format
today = (Date.current.year * 100 + Date.current.month) * 100 + Date.today.day
dob = (dob.year * 100 + dob.month) * 100 + dob.day
# NOTE: could also use `.strftime('%Y%m%d').to_i`
# convert to age in years
years_old = (today - dob) / 10000
It's definitely unique in its approach but makes perfect sense when you realise what it does:
today = 20140702 # 2 July 2014
# person born this time last year is a 1 year old
years = (today - 20130702) / 10000
# person born a year ago tomorrow is still only 0 years old
years = (today - 20130703) / 10000
# person born today is 0
years = (today - 20140702) / 10000 # person born today is 0 years old
# person born in a leap year (eg. 1984) comparing with non-leap year
years = (20140228 - 19840229) / 10000 # 29 - a full year hasn't yet elapsed even though some leap year babies think it has, technically this is the last day of the previous year
years = (20140301 - 19840229) / 10000 # 30
# person born in a leap year (eg. 1984) comparing with leap year (eg. 2016)
years = (20160229 - 19840229) / 10000 # 32
Because Ruby on Rails is tagged, the dotiw gem overrides the Rails built-in distance_of_times_in_words and provides distance_of_times_in_words_hash which can be used to determine the age. Leap years are handled fine for the years portion although be aware that Feb 29 does have an effect on the days portion that warrants understanding if that level of detail is needed. Also, if you don't like how dotiw changes the format of distance_of_time_in_words, use the :vague option to revert to the original format.
Add dotiw to the Gemfile:
gem 'dotiw'
On the command line:
bundle
Include the DateHelper in the appropriate model to gain access to distance_of_time_in_words and distance_of_time_in_words_hash. In this example the model is 'User' and the birthday field is 'birthday.
class User < ActiveRecord::Base
include ActionView::Helpers::DateHelper
Add this method to that same model.
def age
return nil if self.birthday.nil?
date_today = Date.today
age = distance_of_time_in_words_hash(date_today, self.birthday).fetch("years", 0)
age *= -1 if self.birthday > date_today
return age
end
Usage:
u = User.new("birthday(1i)" => "2011", "birthday(2i)" => "10", "birthday(3i)" => "23")
u.age
I believe this is functionally equivalent to #philnash's answer, but IMO more easily understandable.
class BirthDate
def initialize(birth_date)
#birth_date = birth_date
#now = Time.now.utc.to_date
end
def time_ago_in_years
if today_is_before_birthday_in_same_year?
age_based_on_years - 1
else
age_based_on_years
end
end
private
def age_based_on_years
#now.year - #birth_date.year
end
def today_is_before_birthday_in_same_year?
(#now.month < #birth_date.month) || ((#now.month == #birth_date.month) && (#now.day < #birth_date.day))
end
end
Usage:
> BirthDate.new(Date.parse('1988-02-29')).time_ago_in_years
=> 31
class User
def age
return unless birthdate
(Time.zone.now - birthdate.to_time) / 1.year
end
end
Can be checked with the following test:
RSpec.describe User do
describe "#age" do
context "when born 29 years ago" do
let!(:user) { create(:user, birthdate: 29.years.ago) }
it "has an age of 29" do
expect(user.age.round).to eq(29)
end
end
end
end
The following seems to work (but I'd appreciate it if it was checked).
age = now.year - bday.year
age -= 1 if now.to_a[7] < bday.to_a[7]
If you don't care about a day or two, this would be shorter and pretty self-explanitory.
(Time.now - Time.gm(1986, 1, 27).to_i).year - 1970
Ok what about this:
def age
return unless dob
t = Date.today
age = t.year - dob.year
b4bday = t.strftime('%m%d') < dob.strftime('%m%d')
age - (b4bday ? 1 : 0)
end
This is assuming we are using rails, calling the age method on a model, and the model has a date database column dob. This is different from other answers because this method uses strings to determine if we are before this year's birthday.
For example, if dob is 2004/2/28 and today is 2014/2/28, age will be 2014 - 2004 or 10. The floats will be 0228 and 0229. b4bday will be "0228" < "0229" or true. Finally, we will subtract 1 from age and get 9.
This would be the normal way to compare the two times.
def age
return unless dob
t = Date.today
age = today.year - dob.year
b4bday = Date.new(2016, t.month, t.day) < Date.new(2016, dob.month, dob.day)
age - (b4bday ? 1 : 0)
end
This works the same, but the b4bday line is too long. The 2016 year is also unnecessary. The string comparison at the beginning was the result.
You can also do this
Date::DATE_FORMATS[:md] = '%m%d'
def age
return unless dob
t = Date.today
age = t.year - dob.year
b4bday = t.to_s(:md) < dob.to_s(:md)
age - (b4bday ? 1 : 0)
end
If you aren't using rails, try this
def age(dob)
t = Time.now
age = t.year - dob.year
b4bday = t.strftime('%m%d') < dob.strftime('%m%d')
age - (b4bday ? 1 : 0)
end
👍🏼
I think it's alot better to do not count months, because you can get exact day of a year by using Time.zone.now.yday.
def age
years = Time.zone.now.year - birthday.year
y_days = Time.zone.now.yday - birthday.yday
y_days < 0 ? years - 1 : years
end
Came up with a Rails variation of this solution
def age(dob)
now = Date.today
age = now.year - dob.year
age -= 1 if dob > now.years_ago(age)
age
end
DateHelper can be used to get years only
puts time_ago_in_words '1999-08-22'
almost 20 years
def computed_age
if birth_date.present?
current_time.year - birth_date.year - (age_by_bday || check_if_newborn ? 0 : 1)
else
age.presence || 0
end
end
private
def current_time
Time.now.utc.to_date
end
def age_by_bday
current_time.month > birth_date.month
end
def check_if_newborn
(current_time.month == birth_date.month && current_time.day >= birth_date.day)
end```
(Date.today - birth_date).days.seconds.in_years.floor
In Ruby on Rails (thanks to ActiveSupport), there are many ways to solve this problem.
First of all, some clarifications:
The difference between two 'Date' returns the number of days
The difference between two 'Time' returns the number of seconds
in_years() returns the amount of years a duration covers as a float
1.year is equivalent to 365.2425.days.seconds
ActiveSupport constants/methods are more accurate than a "simple" calculation of seconds in a year
1.year.seconds # => 31556952
365.25*24*60*60 # => 31557600.0
365*24*60*60 # => 31536000
So, if you work with Date, you can do :
(Date.today - birth_date).days.seconds.in_years.floor
# or this is also a good way
((Date.today - birth_date).days / 1.year).floor
Note the use of floor method to convert the Float in Integer
But you can also use Time, like this :
(Time.now - birth_date.to_time).seconds.in_years.floor
((Time.now - birth_date.to_time) / 1.year).floor
If you want to use only plain ruby, I suggest this answer:
SECONDS_PER_YEAR = 31556952
SECONDS_PER_DAY = 86400
((Date.today - birth_date) * SECONDS_PER_DAY / SECONDS_PER_YEAR).floor
# or
((Time.now - birth_date.to_time) / SECONDS_PER_YEAR).floor
def birthday(user)
today = Date.today
new = user.birthday.to_date.change(:year => today.year)
user = user.birthday
if Date.civil_to_jd(today.year, today.month, today.day) >= Date.civil_to_jd(new.year, new.month, new.day)
age = today.year - user.year
else
age = (today.year - user.year) -1
end
age
end
Time.now.year - self.birthdate.year - (birthdate.to_date.change(:year => Time.now.year) > Time.now.to_date ? 1 : 0)
To account for leap years (and assuming activesupport presence):
def age
return unless birthday
now = Time.now.utc.to_date
years = now.year - birthday.year
years - (birthday.years_since(years) > now ? 1 : 0)
end
years_since will correctly modify the date to take into account non-leap years (when birthday is 02-29).
Here's my solution which also allows calculating the age at a specific date:
def age on = Date.today
(_ = on.year - birthday.year) - (on < birthday.since(_.years) ? 1 : 0)
end
I had to deal with this too, but for months. Became way too complicated. The simplest way I could think of was:
def month_number(today = Date.today)
n = 0
while (dob >> n+1) <= today
n += 1
end
n
end
You could do the same with 12 months:
def age(today = Date.today)
n = 0
while (dob >> n+12) <= today
n += 1
end
n
end
This will use Date class to increment the month, which will deal with 28 days and leap year etc.

Resources