Donations over past 24 months with keys and sums - ruby-on-rails

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

Related

For a given period, getting the smallest list of dates, using jokers

I use Elasticsearch where I have one index per day, and I want my Ruby on Rails application to query documents in a given period by specifying the smallest and most precise list of indices.
I can't find the code to get that list of indices. Let me explain it:
Consider a date formatted in YYYY-MM-DD.
You can use the joker * at the end of the date string. E.g. 2016-07-2* describes all the dates from 2016-07-20 to 2016-07-29.
Now, consider a period represented by a start date and an end date.
The code must return the smallest possible array of dates representing the period.
Let's use an example. For the following period:
start date: 2014-11-29
end date: 2016-10-13
The code must return an array containing the following strings:
2014-11-29
2014-11-30
2014-12-*
2015-*
2016-0*
2016-10-0*
2016-10-10
2016-10-11
2016-10-12
2016-10-13
It's better (but I'll still take a unoptimized code rather than nothing) if:
The code returns the most precise list of dates (i.e. doesn't return dates with a joker that describes a period starting before the start date, or ending after the end date)
The code returns the smallest list possible (i.e. ["2016-09-*"] is better than ["2016-09-0*", "2016-09-1*", "2016-09-2*", "2016-09-30"]
Any idea?
Okay, after more thinking and the help of a coworker, I may have a solution. Probably not totally optimized, but still...
def get_indices_from_period(start_date_str, end_date_str)
dates = {}
dates_strings = []
start_date = Date.parse(start_date_str)
end_date = Date.parse(end_date_str)
# Create a hash with, for each year and each month of the period: {:YYYY => {:MMMM => [DD1, DD2, DD3...]}}
(start_date..end_date).collect do |date|
year, month, day = date.year, date.month, date.day
dates[year] ||= {}
dates[year][month] ||= []
dates[year][month] << day
end
dates.each do |year, days_in_year|
start_of_year = Date.new(year, 1, 1)
max_number_of_days_in_year = (start_of_year.end_of_year - start_of_year).to_i + 1
number_of_days_in_year = days_in_year.collect{|month, days_in_month| days_in_month}.flatten.size
if max_number_of_days_in_year == number_of_days_in_year
# Return index formatted as YYYY-* if full year
dates_strings << "#{year}-*"
else
days_in_year.each do |month, days_in_month|
formatted_month = format('%02d', month)
if Time.days_in_month(month, year) == days_in_month.size
# Return index formatted as YYYY-MM-* if full month
dates_strings << "#{year}-#{formatted_month}-*"
else
decades_in_month = {}
days_in_month.each do |day|
decade = day / 10
decades_in_month[decade] ||= []
decades_in_month[decade] << day
end
decades_in_month.each do |decade, days_in_decade|
if (decade == 0 && days_in_decade.size == 9) ||
((decade == 1 || decade == 2) && days_in_decade.size == 10)
# Return index formatted as YYYY-MM-D* if full decade
dates_strings << "#{year}-#{formatted_month}-#{decade}*"
else
# Return index formatted as YYYY-MM-DD
dates_strings += days_in_decade.collect{|day| "#{year}-#{formatted_month}-#{format('%02d', day)}"}
end
end
end
end
end
end
return dates_strings
end
Test call:
get_indices_from_period('2014-11-29', '2016-10-13')
=> ["2014-11-29", "2014-11-30", "2014-12-*", "2015-*", "2016-01-*", "2016-02-*", "2016-03-*", "2016-04-*", "2016-05-*", "2016-06-*", "2016-07-*", "2016-08-*", "2016-09-*", "2016-10-0*", "2016-10-10", "2016-10-11", "2016-10-12", "2016-10-13"]

Generate a list of commercial weeks falling between 2 dates in Ruby / Rails

I am trying to created a list of year-week (equivalent to mySQL's YEARWEEK(date,1)) falling between two date values in Rails. List is generating perfectly if start-date and end-date are falling in same year. Here is my code:
campaign_start_date = "2013-08-02 06:59:00"
campaing_end_date = "2013-09-01 06:59:00"
start_year = DateTime.parse(campaign_start_date).cwyear
start_week = "%04d%02d" % [start_year, DateTime.parse(campaign_start_date).cweek]
end_year = DateTime.parse(campaing_end_date).cwyear
end_week = "%04d%02d" % [end_year, DateTime.parse(campaing_end_date).cweek]
if start_year == end_year
(start_week..end_week).each{ |i| result << i }
else
# need to build a suitable logic here. to handle the case when duration spans over multiple years. for example started in 01-Nov-14 and ended in 01-May-15
end
return result
there will be no problem with above date values, it will fall to if case and the result I will get is:
[
"201331",
"201332",
"201332",
"201333",
"201334",
"201335"
]
which is also what I exactly want. BUT if my start-date and end-date values are these for example:
campaign_start_date = "2014-07-23 06:59:00"
campaing_end_date = "2015-03-01 06:59:00"
means falling in different years, then it need different logic that the one I have in if condition because for these date values (start_week=201430 and end_week=201509) the if condition is not suitable here because it would generate 80 values, which is wrong because number of weeks between these dates are not 80. Need help to develop the logic for else case. May be its easy but right now I am just tired to dig it any deeper.
Special attention: the solutions should care about commercial year and commercial week (refer .cwyear and .cweek functions of rails) For example yearweek of 2016-01-01 would be 201553 not 201601
any help in this regard would be much appreciated.
Thank you to those who replied t. I've finally solved the problem like this:
campaign_weeks = []
campaign_start_date = "2014-07-23 06:59:00" # or any date
campaing_end_date = "2015-03-01 06:59:00" # or any date
start_year = DateTime.parse(campaign_start_date).cwyear
start_cweek_of_the_campaign = "%04d%02d" % [start_year, DateTime.parse(campaign_start_date).cweek]
end_year = DateTime.parse(campaing_end_date).cwyear
end_cweek_of_the_campaign = "%04d%02d" % [end_year, DateTime.parse(campaing_end_date).cweek]
if start_year == end_year
(start_cweek_of_the_campaign..end_cweek_of_the_campaign).each do |w|
campaign_weeks << ("%04d%02d" % [start_year, w])
end
else
(start_year..end_year).each do |y|
first_cweek_number_of_the_year = (y == start_year) ? start_cweek_of_the_campaign : 1
last_cweek_number_of_the_year = (y == end_year) ? end_cweek_of_the_campaign : DateTime.new(y, 12, 28).cweek
(first_cweek_number_of_the_year .. last_cweek_number_of_the_year).each do |w|
campaign_weeks << ("%04d%02d" % [y, w])
end
end
end
return campaign_weeks
Notes: 28th Dec always fall in the last cweek/iso-week of the year. last ISO week of the year is either 52 or 53.
Reference: http://en.wikipedia.org/wiki/ISO_week_date#Last_week
Got some hint from this answer: Calculating the number of weeks in a year with Ruby
The bottleneck is (start_week..end_week) range. It apparently goes through hundred (since we are on decimals):
2014xx ⇒ 201452 ⇒ 201453 ⇒ ... ⇒ 201499 ⇒ 201500 ⇒ ...
You should probably filter your range, like:
r = (start_week..end_week)
r.to_a.reject { |e| e[-2..-1].to_i > 52 }
Depending on how you count weeks (=-based, or 1-based,) the 201500 should be likely filtered as well:/
r.to_a.select { |e| e[-2..-1].to_i.between? 1, 52 }
Try this out; it will work for any set of dates irrespective of whether the years are the same or not:
campaign_start_date = "2014-07-23 06:59:00"
campaign_end_date = "2015-03-01 06:59:00"
start_date = DateTime.parse(campaign_start_date)
end_date = DateTime.parse(campaign_end_date)
while start_date < end_date
puts "%04d%02d" % [start_date.cw_year, start_date.cweek]
start_date = start_date + 7.days
end
A bit late in the discussion but here is what I used to get the number of commercial weeks between two dates:
def cweek_diff(start_date, end_date)
return if end_date < start_date
cweek_diff = (end_date.cweek - start_date.cweek) + 1
cwyear_diff = end_date.cwyear - start_date.cwyear
cyear_diff * 53 + cweek_diff - cwyear_diff
end
It worked perfectly in my case. Hope it helps ;)

Rails: How to loop through month?

I made a scope help me to select objects
scope :best_of_the_month, ->(year, month) do
time = Time.new(year, month)
start_time = time.beginning_of_month
end_time = time.end_of_month
where("created_at > ? AND created_at < ?", start_time, end_time).where("likes > ?", 15).where("rating > ?", 4.85).order_by_rating.to_a.uniq(&:author)
end
Then, I want to loop through this method, from 2014/1 to now. How can I do it?
Maybe something like this:
start_date = Date.create(2014,1).month
end_date = Date.today.month
#monthly_videos = []
(start_date..end_date).each do |year, month|
videos = Video.best_of_the_month(year, month)
#monthly_videos << videos
end
I find a solution here, How to Loop through Months in Ruby on Rails. But it seems about looping through the days. Not the month
With the best_of_the_month scope defined to take month and year as params, the following code should work:
date = Date.new(2014,1,1)
#monthly_videos = []
while true
videos = Video.best_of_the_month(date.year, date.month)
#monthly_videos << videos
date += 1.month
break if date == Date.today.beginning_of_month
end
You could use the Date#next_month method
date = Date.new(2014,1,1)
final_date = Date.new(Date.today.year, Date.today.month)
#monthly_video = []
loop do
#monthly_videos << Video.best_of_the_month(date.year, date.month)
break if date == final_date
date = date.next_month
end

Convert date range to array of weeks and months

I want to take a given date range and convert it to an array of dates that are the end of calendar weeks and months.
So, same range, but would have a weeks and months output.
Range: Date.parse("2014-01-30")..Date.parse("2014-03-27")
Output:
weeks = ["2014-02-02", "2014-02-09", "2014-02-16", "2014-02-23", "2014-03-02", "2014-03-09", "2014-03-16", "2014-03-23", "2014-03-02", "2014-03-30"]
months = ["2014-01-31", "2014-02-28", "2014-03-31"]
I happen to be doing this inside a Rails app, so the Rails helper methods are available here (for example, end_of_week).
I can think of as below :
require 'date'
months = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).group_by(&:month).map { |_,v| v.first.end_of_month.to_s }
# => ["2014-01-31", "2014-02-28", "2014-03-31"]
weeks = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).select(&:sunday?).map(&:to_s)
# => ["2014-02-02",
# "2014-02-09",
# "2014-02-16",
# "2014-02-23",
# "2014-03-02",
# "2014-03-09",
# "2014-03-16",
# "2014-03-23"]
For week use step.
(Date.parse("2014-01-30")..Date.parse("2014-03-27")).step(7).map(&:to_s)
For month it will not work, because number of days varies.
Use .sunday? to find out it is a weekend and end_of_month to find out if it is a month end.
Weekends:
(Date.parse("2014-01-30")..Date.parse("2014-03-27")).each { |d| puts d if d.sunday?}
Month ends:
(Date.parse("2014-01-30")..Date.parse("2014-03-27")).each { |d| puts d if d == d.end_of_month}
Here's how to do it with plain Ruby (no ActiveSupport or Rails):
weeks = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).select(&:sunday?)
months = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).select { |x| (x + 1).month != x.month }
With Rails:
weeks = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).select(&:sunday?)
months = (Date.parse("2014-01-30")..Date.parse("2014-03-27")).map(&:end_of_month).uniq
This answer is considering all months in date range even if date range covers more than 12 months.
d1 = Date.parse("2014-01-30")
d2 = Date.parse("2014-03-27")
For using Ruby:--
months = ((d1)..(d2)).each_with_object([]){|d, o| o << (Date.civil(d.year, d.month, -1)).to_s}.uniq
sundays = ((d1)..(d2.wday < 7 ? d2 + 7 - d2.wday : d2)).each_with_object([]){|d, o| o << d.to_s if d.sunday?}
For using Rails:--
months = ((d1)..(d2.end_of_month)).each_with_object([]){|d, o| o << d.to_s if d.sunday?}
sundays = ((d1)..(d2.end_of_month)).each_with_object([]){|d, o| o << d.end_of_month.to_s}.uniq
While getting array of month's end date, my logic of finding end date of each month is not efficient. But, my logic of finding array of Sundays' date is better.

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