Rails advance month to february sometimes fails - ruby-on-rails

I am making a payments arrangement, for an order, where a client can choose the day of the month to pay. And can also change it afterwards. For this I used the advance method for date, and it works correctly when creating, but fails when updating. Here is my code:
Creating, if day is 31, it properly changes to last day of month if the month doesn't have a day 31.
months = params[:payment][:months].to_i
pay_day = params[:payment][:pay_day].to_i
today = Date.today
first_date = today.change(months: 1).change(day: pay_day)
months_between = (today.year * 12 + today.month) - (first_date.year * 12 + first_date.month)
quota = (invoice_total)/months
payment_number = 1
months.times do |i|
payment = Payment.new(invoice: current_invoice, payment_type: PaymentType.find(params[:payment][:payment_type_id]), payment_number: payment_number, value: quota, max_payment_date: first_date.advance(months: +(months_between+i+1)))
payment.save
payment_number = payment_number + 1
end
Update code, it fails in, for example, February with "invalid date", but updates january correctly.
pay_day = params[:payment][:pay_day].to_i
#invoice.payments.each do |payment|
if payment.actual_payment_date.nil?
reference_month = payment.max_payment_date.change(months: 1).change(day: pay_day)
months_between = (payment.max_payment_date.year * 12 + payment.max_payment_date.month) - (reference_month.year * 12 + reference_month.month)
payment.update_attributes(max_payment_date: reference_month.advance(months: +(months_between)))
payment.save
end
end
It is basically the same code, does anyone know why in the first part it works correctly, but not the second part?
Thanks

ActiveSupport to the rescue:
def correct_day(day, date = Date.today)
end_of_month = date.end_of_month.day
day < end_of_month ? day : end_of_month
end
Lets test it with [timecop:
Timecop.freeze(Date.new(2000, 02)) do
correct_day(32) # => 29 - 2000 was a leap year.
correct_day(15) # => 15
end
Timecop.freeze(Date.new(2015, 10, 11)) do
correct_day(32) # => 31
correct_day(15) # => 15
end
This is how you would implement it in a model:
class Invoice < ActiveRecord::Base
has_many :payments
# Creates a payment for each month
# #param [Integer] months
# #param [Ingeger] preferred_day - defaults to the last day of the month
def split_payments(months = 12, preferred_day = 31)
(1..months).map do |i|
payments.create(
pay_day: calculate_payment_date(created_at + i.months, preferred_day),
payment_number: i
# ... More attributes
)
end
end
def change_payment_date(preferred_day)
payments.sort_by(:payment_number).map do |payment|
date = created_at + payment.payment_number.months
payment.update_attributes(
pay_day: calculate_payment_date(date, preferred_day)
)
end
end
private
def calculate_payment_date(date, preferred_day)
end_of_month = date.end_of_month
pref = date.beginning_of_month + (preferred_day - 1).days
pref < end_of_month ? pref : end_of_month
end
end

Related

What is the best way of categorising segments of a time period?

I am developing a staff rota system. For payroll I need to calculate the correct rate of pay depending on the date/time period the shift covers.
How can I check for various date periods (weekend, holidays, weekday) without using a long chain of conditional statements with lengthy, verbose conditions.
Given any time range (a shift):
eg. 2015-01-20 15:00 --> 2015-01-21 17:00
What would be the best (and most efficient way) of categorising segments of the this period?
I would like to know:
The period (if any) between 22:00 and 07:00 on any weekday (Monday -
Friday) evening.
The period (if any) falling between 08:00 on a Saturday and 22:00 on a Sunday.
The period (if any) falling on a public holiday (using the
holidays gem)
So my two questions then are:
1) Knowing that a time period (shift) could span a weekend (although I would prefer a solution that would support a span of many days), how do I calculate which date/time ranges to compare against?
2) Once I have determined the time periods (weekends, holidays etc) to compare against, how do I best determine the intersection of these periods and determine the duration of them?
I don't fully understand your question, so I've put together some code that is based on many assumptions about the problem you face. I hope some of the issues I've addressed, and the way I've dealt with them, may be helpful to you. For example, if a worker is still working when a shift ends, it is necessary to identify the next shift, which may be the next day.
You'll see that my code is very rough and poorly structured. I have many temporary variables that are there just to help you follow what's going on. In a real app, you might want to add some classes, more methods, etc. Also, assuming the data will be stored in a database, you might want to use SQL for some of the calculations.
First, what I've assumed to be the data.
Data
A list of holidays:
holidays = ["2015:04:05", "2015:04:06"]
Information for each employee, including the employee's job classification, with keys being the employee id:
employees = {
123 => { name: 'Bob', job: :worker_bee1 },
221 => { name: 'Sue', job: :worker_bee2 }
}
Groups of days having the same daily periods, with pay rates the same for all days of the group, for each job and period within the day, unless the day falls on a holiday:
day_groups = { weekdays: [1,2,3,4,5], weekends: [6,0] }
Information for each work period:
work_periods = {
weekday_graveyard: {
day_group: :weekdays,
start_time: "00:00",
end_time: "08:00" },
weekday_day: {
day_group: :weekdays,
start_time: "08:00",
end_time: "16:00" },
weekday_swing: {
day_group: :weekdays,
start_time: "16:00",
end_time: "24:00" },
weekend: {
day_group: :weekends,
start_time: "00:00",
end_time: "24:00" } }
A wage schedule by job, dialy period, for non-holidays and holidays:
wage_by_job = {
worker_bee1: {
weekday_graveyard: { standard: 30.00, holiday: 60.00 },
weekday_day: { standard: 20.00, holiday: 40.00 },
weekday_swing: { standard: 25.00, holiday: 50.00 },
weekend: { standard: 22.00, holiday: 44.00 }
},
worker_bee2: {
weekday_graveyard: { standard: 32.00, holiday: 64.00 },
weekday_day: { standard: 22.00, holiday: 44.00 },
weekday_swing: { standard: 27.00, holiday: 54.00 },
weekend: { standard: 24.00, holiday: 48.00 }
}
}
Hours worked by all employees during the pay period:
shifts_worked = [
{ id: 123, date: "2015:04:03", start_time: "15:30", end_time: "00:15" },
{ id: 221, date: "2015:04:04", start_time: "23:30", end_time: "08:30" },
{ id: 123, date: "2015:04:06", start_time: "08:00", end_time: "16:00" },
{ id: 221, date: "2015:04:06", start_time: "23:00", end_time: "07:00" },
{ id: 123, date: "2015:04:07", start_time: "00:00", end_time: "09:00" }
]
Helpers
require 'set'
require 'date'
def date_str_to_obj(date_str)
Date.strptime(date_str, '%Y:%m:%d')
end
date_str_to_obj("2015:04:04")
#=> #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>
def next_day(day)
(day==6) ? 0 : day+1
end
next_day(6)
#=> 0
Convert holidays to date objects and store in a set for fast lookup:
#holiday_set = Set.new(holidays.map { |date_str|
date_str_to_obj(date_str) }.to_set)
#=> #<Set: {#<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>,
# #<Date: 2015-04-06 ((2457119j,0s,0n),+0s,2299161j)>}>
def is_holiday?(date)
#holiday_set.include?(date)
end
is_holiday?(date_str_to_obj("2015:04:04"))
#=> false
is_holiday?(date_str_to_obj("2015:04:05"))
#=> true
Map each day of the week to an element of day_groups:
#day_group_by_dow = day_groups.each_with_object({}) { |(period,days),h|
days.each { |d| h[d] = period } }
#=> {1=>:weekdays, 2=>:weekdays, 3=>:weekdays, 4=>:weekdays,
# 5=>:weekdays, 6=>:weekend, 0=>:weekend}
Map each element of day_groups to an array of work periods:
#work_periods_by_day_group = work_periods.each_with_object({}) { |(k,g),h|
h.update(g[:day_group]=>[k]) { |_,nwp,owp| nwp+owp } }
#=> {:weekdays=>[:weekday_graveyard, :weekday_day, :weekday_swing],
# :weekend=> [:weekends]}
Compute hours worked within a work period:
def start_and_end_times_to_hours(start_time, end_time)
(time_to_minutes_after_midnight(end_time) -
time_to_minutes_after_midnight(start_time))/60.0
end
start_and_end_times_to_hours("10:00", "14:30")
#=> 4.5
A helper for the previous method:
private
def time_to_minutes_after_midnight(time_str)
hrs, mins = time_str.scan(/(\d\d):(\d\d)/).first.map(&:to_i)
60 * hrs + mins
end
public
time_to_minutes_after_midnight("10:00")
#=> 600
time_to_minutes_after_midnight("14:30")
#=> 870
As indicated by the method name:
def date_and_time_to_current_period(date, time, work_periods)
day_grp = #day_group_by_dow[date.wday]
periods = #work_periods_by_day_group[day_grp]
periods.find do |per|
p = work_periods[per]
p[:start_time] <= time && time < p[:end_time]
end
end
date_and_time_to_current_period(date_str_to_obj("2015:04:03"),
#=> :weekday_swing
date_and_time_to_current_period(date_str_to_obj("2015:04:04"),
#=> :weekend
Lastly, another self-explanatory method:
def next_period_and_date_by_period_and_date(work_periods, period, date)
end_time = work_periods[period][:end_time]
if end_time == "24:00" # next_day
day_grp = #day_group_by_dow[next_day(date.wday)]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]=="00:00" }, date+1]
else # same day
day_grp = work_periods[period][:day_group]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]==end_time }, date]
end
end
next_period_and_date_by_period_and_date(work_periods,
:weekday_day, date_str_to_obj("2015:04:03"))
#=> [:weekday_swing, #<Date: 2015-04-03 ((2457116j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:02"))
#=> [:weekday_graveyard, #<Date: 2015-04-03...+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:03"))
#=> [:weekend, #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:04"))
#=> [:weekend, #<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>]
Calculation of payroll
shifts_worked.each_with_object(Hash.new(0.0)) do |shift, payroll|
id = shift[:id]
date = date_str_to_obj(shift[:date])
start_time = shift[:start_time]
end_time = shift[:end_time]
wage_schedule = wage_by_job[employees[id][:job]]
curr_period = date_and_time_to_current_period(date, start_time, work_periods)
while true
end_time_in_period = work_periods[curr_period][:end_time]
end_time_in_period = end_time if
(end_time > start_time && end_time < end_time_in_period)
hours_in_period =
start_and_end_times_to_hours(start_time, end_time_in_period)
wage = wage_schedule[curr_period][is_holiday?(date) ? :holiday : :standard]
payroll[id] += (wage * hours_in_period).round(2)
break if end_time == end_time_in_period
curr_period, date =
next_period_and_date_by_period_and_date(work_periods,
curr_period, date)
start_time = work_periods[curr_period][:start_time]
end
end
#=> {123=>795.5, 221=>698.0}
I've used the following gem:
https://github.com/fnando/recurrence
I haven't done Holidays yet.
Requirement
class Requirement < ActiveRecord::Base
# Model with attributes:
# start - datetime
# end - datetime
# is_sleepin - boolean
def duration
self.end - self.start
end
def to_range
self.start..self.end
end
def spans_multiple_days?
self.end.to_date != self.start.to_date
end
end
Breakdown of duration of requirement (shift)
class Breakdown
attr_reader :requirement,
:standard_total_duration,
:weekend_total_duration,
:wakein_total_duration
def initialize(requirement)
#requirement = requirement
#rules = Array.new
#rules << Breakdown::StandardRule.new(self)
#rules << Breakdown::WeekendRule.new(self)
#standard_total_duration = components[:standard].total_duration
#weekend_total_duration = components[:weekend].total_duration
if #requirement.is_sleepin?
#standard_total_duration = 0
#weekend_total_duration = 0
end
# Following is a special set of rules for certain Locations where staff work
# If a requirement is_sleepin? that means duration is not counted so set to 0
if ['Home 1', 'Home 2'].include?(#requirement.home.name.strip) and
#requirement.spans_multiple_days? and not #requirement.is_sleepin?
#rules << Aspirations::Breakdown::WakeinRule.new(self)
#wakein_total_duration = components[:wakein].total_duration
#standard_total_duration = 0
#weekend_total_duration = 0
end
end
def components
#rules.hmap{ |k,v| [ k.to_sym, k ] }
end
end
Which uses Rules to specify which parts of a shifts duration should be categorised:
module Breakdown
class Rule
def initialize(breakdown)
#requirement = breakdown.requirement
end
def to_sym
# eg 'Breakdown::StandardRule' becomes :standard
self.class.name.split('::').last.gsub("Rule", "").downcase.to_sym
end
def periods
output = []
occurrences = rule.events.map{ |e| rule_time_range(e) }
occurrences.each do |o|
if (o.max > #requirement.start and #requirement.end > o.min)
output << (o & #requirement.to_range)
end
end
return output
end
def total_duration
periods.reduce(0) { |sum, p| sum + (p.max - p.min).to_i }
end
end
end
Example of a Rule (in this case a weekend rule)
module Breakdown
class WeekendRule < Breakdown::Rule
def period_expansion
# This is an attempt to safely ensure that a weekend period
# is detected even though the start date of the requirement
# may be on Sunday evening, maybe could be just 2 days
4.days
end
def period_range
(#requirement.start.to_date - period_expansion)..(#requirement.end.to_date + period_expansion)
end
def rule
Recurrence.new(:every => :week, :on => :saturday, :starts => period_range.min, :until => period_range.max)
end
def rule_time_range(date)
# Saturday 8am
start = date + Time.parse("08:00").seconds_since_midnight.seconds
_end = (date + 1.day) + Time.parse("22:00").seconds_since_midnight.seconds
start.._end
end
end
end
One possible approach might be to break a week up into individual chunks of, say, 15 minutes (depending on how much resolution you need). Instead of ranges of time that are somewhat hard to deal with, you can then work with sets of these chunks, which Ruby supports very nicely.
Number the chunks of time sequentially:
Monday 12:00 AM-12:15 AM = 0
Monday 12:15 AM-12:30 AM = 1
...
Sunday 11:45 PM-12:00 AM = 671
Then prepare sets of integers for holidays, weekends, each shift, etc., whatever you need. Except for the holidays, those are probably all constants.
For instance, for your weekend from Saturday 8 AM to Sunday 10 PM:
weekend = [ 512..663 ]
Similarly, represent each employee's attendance as a set. For instance, somebody who worked from 8 AM to noon on Monday and from 10 AM to 11 AM on Saturday would be:
attendance = [ 32..47, 520..523 ]
With this approach, you can use set intersections to figure out how many hours were on weekends:
weekendAttendance = weekend & attendance

How to create multiple records for each day in given quarter?

I have Shift model.
--- !ruby/object:Shift
attributes:
id:
starts_at:
ends_at:
I want to add singelton method to create shifts for each day in given quarter.
class Shift
def self.open_quarter(number, year)
starts_at = "08:00"
ends_at = "08:00"
...
end
end
How to implement that in best way? I want that each shifts starts_at 8.00 am and finish 8.00 am on next day.
def self.open_quarter(number, year)
start_time = Time.new(year, number*3 - 2, 1, 8)
while start_time.month <= number*3 && start_time.year == year
Shift.create{starts_at: start_time, ends_at: start_time += 24.hours}
end
end
make sure to set the correct timezone when using Time.new. Default is current timezone (see docs). You can also use Time.utc.
def self.open_quarter(number, year)
starts_at = "08:00 am"
ends_at = "08:00 pm"
quarter_start = Date.new(year, (number * 3)).beginning_of_quarter
quarter_end = Date.new(year, (number * 3)).end_of_quarter
(quarter_end - quarter_start).to_i.times do |n|
start_shift = "#{(quarter_start + n).to_s} #{starts_at}".to_datetime
end_shift = "#{(quarter_start + n).to_s} #{ends_at}".to_datetime
Shift.create(starts_at: start_shift, ends_at: end_shift)
end
end

Donations over past 24 months with keys and sums

Having pulled donations from the past two years, I'm trying to derive the sum of those donations per month, storing the keys (each month) and the values (the sum of donations for each month) in an array of hashes. I would like the keys to be numbers 1 to 24 (1 being two years ago and 24 being this month) and if there are no donations for a given month, the value would be zero for that month. How would I do this as an array of hashes in Ruby/Rails?
This is my variable with the donations already in it.
donations = Gift.where(:date => (Date.today - 2.years)..Date.today)
the following gives you a hash, with keys '2013/09" , etc...
monthly_donations = {}
date = Time.now
while date > 2.years.ago do
range = date.beginning_of_month..date.end_of_month
monthly_donations[ "{#date.year}/#{date.month}" ] = Giftl.sum(:column, :conditions => {created_at >= range})
date -= 30.days
end
To select the records in that time-span, this should be enough:
donations = Gift.where("date >= #{2.years.ago}")
you can also do this:
donations = Gift.where("date >= :start_date AND date <= :end_date",
{start_date: 2.years.ago, end_date: Time.now} )
See also: 2.2.1 "Placeholder Conditions"
http://guides.rubyonrails.org/active_record_querying.html
To sum-up a column in the database record, you can then do this:
sum = Gift.sum(:column , :conditions => {created_at >= 2.years.ago})
First, we need a function to find the difference in months from the current time.
def month_diff(date)
(Date.current.year * 12 + Date.current.month) - (date.year * 12 + date.month)
end
Then we iterate through #donation, assuming that :amount is used to store the value of each donation:
q = {}
#donations.each do |donation|
date = month_diff(donation.date)
if q[date].nil?
q[date] = donation.amount
else
q[date] += donation.amount
end
end
I found a good solution that covered all the bases--#user1185563's solution didn't bring in months without donations and #Tilo's called the database 24 times, but I very much appreciated the ideas! I'm sure this could be done more efficiently, but I created the hash with 24 elements (key: beginning of each month, value: 0) and then iterated through the donations and added their amounts to the hash in the appropriate position.
def monthly_hash
monthly_hash = {}
date = 2.years.ago
i = 0
while date < Time.now do
monthly_hash["#{date.beginning_of_month}"] = 0
date += 1.month
i += 1
end
return monthly_hash
end
#monthly_hash = monthly_hash
#donations.each do |donation|
#monthly_hash["#{donation.date.beginning_of_month}"] += donation.amount
end

Calculate days until next birthday in Rails

I have a model with a date column called birthday.
How would I calculate the number of days until the user's next birthday?
Here's a simple way. You'll want to make sure to catch the case where it's already passed this year (and also the one where it hasn't passed yet)
class User < ActiveRecord::Base
attr_accessible :birthday
def days_until_birthday
bday = Date.new(Date.today.year, birthday.month, birthday.day)
bday += 1.year if Date.today >= bday
(bday - Date.today).to_i
end
end
And to prove it! (all I've added is the timecop gem to keep the calculations accurate as of today (2012-10-16)
require 'test_helper'
class UserTest < ActiveSupport::TestCase
setup do
Timecop.travel("2012-10-16".to_date)
end
teardown do
Timecop.return
end
test "already passed" do
user = User.new birthday: "1978-08-24"
assert_equal 313, user.days_until_birthday
end
test "coming soon" do
user = User.new birthday: "1978-10-31"
assert_equal 16, user.days_until_birthday
end
end
Try this
require 'date'
def days_to_next_bday(bday)
d = Date.parse(bday)
next_year = Date.today.year + 1
next_bday = "#{d.day}-#{d.month}-#{next_year}"
(Date.parse(next_bday) - Date.today).to_i
end
puts days_to_next_bday("26-3-1985")
Having a swipe at this:
require 'date'
bday = Date.new(1973,10,8) // substitute your records date here.
this_year = Date.new(Date.today.year, bday.month, bday.day )
if this_year > Date.today
puts this_year - Date.today
else
puts Date.new(Date.today.year + 1, bday.month, bday.day ) - Date.today
end
I'm not sure if Rails gives you anything that makes that much easier.
Here's another way to approach this with lesser-known methods, but they make the code more self-explanatory.
Also, this works with birth dates on a February 29th.
class User < ActiveRecord::Base
attr_accessible :birthday
def next_birthday
options = { year: Date.today.year }
if birthday.month == 2 && birthday.day == 29 && !Date.leap?(Date.today.year)
options[:day] = 28
end
birthday.change(options).tap do |next_birthday|
next_birthday.advance(years: 1) if next_birthday.past?
end
end
end
And of course, the number of days until the next birthday is:
(user.next_birthday - Date.today).to_i

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