Looking for an efficient way to detect birthday ranges using Ruby - ruby

Currently working on an app that needs to show the operator a list of staff that have birthdays that fall in the past 7 days or the next 21 days.
We have a staff database that contains the date of births, and selecting them by date range is fairly straightforward. However, we then have to show how many days in the future/past the birthday is from today.
The code currently looks something like:
today = Date.today
dob = staff.dob
days = Date.new(today.year, dob.month, dob.day) - today
if days < 0
"#{ days.abs } days ago"
else
"in #{ days } days"
end
This all works well in nearly every case, except when it comes to late December, or very early January. Because we are using today.year as the year comparator, if the 21 days ahead spills over into the new year, I get a false "days ago" reading. e.g.
Today's date is 20th December 2015, and the staff member's birthday is on 2nd January, instead of getting "in 13 days", I am getting "352 days ago".
I know I can probably wrap the code in another if/then/else clause that checks if the current date is after December 10th, to cater for this edge case, but there is a possibility that the date ranges will change dynamically, and also, the code is starting to look messy and inelegant - not like how I would like ruby code to look.
Does anyone have a better suggestion as to how to handle this issue?
(Note: This is in a Sinatra based project, so I don't have all the ActiveSupport or Rails based magic, although I am not averse to using a gem if it will get me the results I need.)

to get the smallest days for this year, next year and last year
today = Date.today
dob = staff.dob
this_dob = Date.new(today.year, dob.month, dob.day)
next_dob = this_dob.next_year
prev_dob = this_dob.prev_year
days = [this_dob - today, next_dob - today, prev_dob - today].min_by{|i| i.to_i.abs}
if days < 0
"#{ days.abs } days ago"
else
"in #{ days } days"
end

require 'date'
class BirthDates
attr_reader :year, :month, :day
def initialize(year, month, day)
#year = year
#month = month
#day = day
end
end
def count_days(dob)
today = Date.today
this_year = today.year
days = [this_year-1, this_year, this_year+1].map { |year|
(Date.new(year, dob.month, dob.day) - today) }.min_by(&:abs)
if days < 0
"#{ -days } days ago"
elsif days > 0
"in #{ days } days"
else
"Happy Birthday!"
end
end
Let's try it. (Today is 29 Oct 15)
dob = BirthDates.new(1908, 12, 28)
count_days(dob) #=> "in 60 days"
dob = BirthDates.new(1999, 10, 27)
count_days(dob) #=> "2 days ago"
dob = BirthDates.new(2014, 10, 29)
count_days(dob) #=> "Happy Birthday"
If today were 5 Jan 16:
dob = BirthDates.new(1908, 12, 28)
count_days(dob) #=> "8 days ago"
dob = BirthDates.new(1999, 3, 27)
count_days(dob) #=> "in 82 days"

Related

how get the last week start date and end date based on date in current week ruby

how to get the last week start date and end date based on date in current week ruby (Monday - Sunday)
Example: if date is 04-feb-15, then result should be start_date = Monday(26-jan-1015) end_date = Sunday(1-feb-15)
if date is 27-feb-15, , then result should be start_date = Monday(16-feb-1015) end_date = (22-feb-15)
You can try this way:
require 'date'
=> true
date = Date.today
=> #<Date: 2015-02-04 ((2457058j,0s,0n),+0s,2299161j)>
# date.wday return week day
end_date = date-date.wday
=> #<Date: 2015-02-01 ((2457055j,0s,0n),+0s,2299161j)>
start_date = date-date.wday-6
=> #<Date: 2015-01-26 ((2457049j,0s,0n),+0s,2299161j)>
For more operations you can refer this link Ruby Doc.
First date of last week
Date.today.last_week.beginning_of_week
Last date of last week
Date.today.last_week.at_end_of_week
You can try this way
require 'active_support/core_ext/date/calculations'
monday = (Date.today.beginning_of_week.last_week)
sunday = (Date.today.beginning_of_week.last_week + 6)
More details on the ActiveSupport Date extensions: https://edgeguides.rubyonrails.org/active_support_core_extensions.html#extensions-to-date
Use Date#wday that tells with day of the week is for a given Date and then subtract accordinglly:
require 'date'
date = Date.parse("15-02-04")
monday = date - (date.wday + 6) # 2015-01-26
sunday = date - date.wday # 2015-02-01
date = Date.parse("15-02-27")
monday = date - (date.wday + 6) # 2015-02-16
sunday = date - date.wday # 2015-02-22
i have tried the below code, working fine.
monday = (Date.today.beginning_of_week - 7) # 2015-01-26
sunday = (Date.today.end_of_week - 7) # 2015-02-01

Single occurrence event with ice_cube gem using start_time and end_time

There must be something simple being overlooked here...
I've been trying various methods for creating a basic IceCube schedule (https://github.com/seejohnrun/ice_cube). The overall goal is to use IceCube to allow "price schedules" inside a "room reservation" rails application.
The first scenario would be creating a basic schedule with a specific start_time and end_time - occurring only once. IceCube can do this, correct?
The schedule would begin on the start_time and end at the end_time. I would expect to be able to check if dates or times occurs_on? this schedule to determine if a room price should be adjusted.
So in console I've tried creating a basic schedule and would expect it to be occurring 5.days from now since the start_time is Time.now and the end_time is Time.now + 30.days. But it seems to never return true...
1.8.7 :001 > schedule = IceCube::Schedule.new(Time.now, :end_time => Time.now + 30.days)
=> #<IceCube::Schedule:0xb619d604 #all_recurrence_rules=[], #duration=nil, #end_time=Tue Jan 08 09:13:11 -0600 2013, #all_exception_rules=[], #start_time=Sun Dec 09 09:13:11 -0600 2012>
1.8.7 :002 > schedule.occurs_on? Date.today + 5.days
=> false
1.8.7 :005 > schedule.occurs_at? Time.now + 5.days
=> false
1.8.7 :006 > schedule.occurring_at? Time.now + 5.days
=> false
Adding a recurrence rule
1.8.7 :018 > schedule.rrule IceCube::Rule.monthly
=> [#<IceCube::MonthlyRule:0xb687a88c #validations={:base_day=>[#<IceCube::Validations::ScheduleLock::Validation:0xb6875b0c #type=:day>], :base_hour=>[#<IceCube::Validations::ScheduleLock::Validation:0xb6875abc #type=:hour>], :interval=>[#<IceCube::Validations::MonthlyInterval::Validation:0xb6875d28 #interval=1>], :base_min=>[#<IceCube::Validations::ScheduleLock::Validation:0xb6875a6c #type=:min>], :base_sec=>[#<IceCube::Validations::ScheduleLock::Validation:0xb6875a1c #type=:sec>]}, #interval=1>]
Then checking Date.today works...
1.8.7 :025 > schedule.occurs_on? Date.today
=> true
But checking occurs_on? for Date.today + 10.days still returns false... Why?
1.8.7 :026 > schedule.occurs_on? Date.today + 10.days
=> false
So what am I overlooking / doing wrong? Or what is the point of setting an IceCube::Schedule start_time and end_time - they seem to have no effect...?
Does IceCube not work for single occurrence events with a start and end time?
Another example scenario, a room owner wants room prices raised for a holiday season. So the room owner creates a price schedule that starts on Dec 1 2012 and ends Jan 7 2013. (shouldn't have to recur, but could if the owner wanted).
Then when people are searching rooms, the prices would be adjusted if the requested stay occurs_on? a holiday price schedule
Do I need to store the start_time and end_time outside of the schedule and check it manually or something?
Or is there a better suited gem / tool to assist with this kind of schedule management?
You're misunderstanding how schedules and rules work.
Firstly, it's important to understand start_time. Every occurrence of the schedule is based on this, and the schedule returns times that match specific intervals from the start time. The intervals are determined by Rules.
Your example doesn't work because "5 days from now" is not a monthly interval from the schedule's start time. 28, 30, or 31 days from the start time would match, depending on the month.
start = Time.utc(2013, 05, 17, 12, 30, 00) # 2013-05-17 12:30:00 UTC
schedule = IceCube::Schedule.new(start)
schedule.add_recurrence_rule IceCube::Rule.monthly
schedule.occurs_on? start + 5.days #=> false
schedule.occurs_on? start + 31.days #=> true
Secondly, end_time works together with start_time to set the duration of each occurrence. So if your start time is 09:00, and end time is 17:00 then each occurrence will have a duration of 8 hours.
This creates a distinction between occurs_at?(t1) and occurring_at?(t1): the first one is only true when the given time exactly matches the start of an occurrence; the second one is true for any time in the duration. occurs_on?(d1) matches for any time in the given date.
arrival = Time.utc(2013, 5, 1, 9, 0, 0)
departure = Time.utc(2013, 5, 1, 17, 0, 0)
schedule = IceCube::Schedule.new(arrival, end_time: departure)
schedule.add_recurrence_rule IceCube::Rule.weekly.day(1, 2, 3, 4, 5) # M-F
schedule.occurs_at? arrival #=> true
schedule.occurs_at? arrival + 1.second #=> false
schedule.occurring_at? arrival + 1.second #=> true
schedule.occurring_at? departure + 1.second #=> false
For what you're doing, you could try one of two approaches:
A single month-long occurrence
A daily occurrence that ends after a month
This depends on how you need to display or validate times against the schedule. Here's an example of both:
arrival = Time.utc(2013, 5, 1)
departure = Time.utc(2013, 5, 31)
# single occurrence
schedule = IceCube::Schedule.new(arrival, duration: 31.days)
# daily occurrence
schedule = IceCube::Schedule.new(arrival, duration: 1.day)
schedule.add_recurrence_rule IceCube::Rule.daily.until(departure)
After some more testing I think using IceCube's SingleOccurrenceRule is the proper way to have a single occurrence of an event.
To have a schedule that occurs only on the days between the Schedule start_time and end_time I can do something like the following.
Create an IceCube::Schedule with a start and end_time:
1.8.7 :097 > schedule = IceCube::Schedule.new(Time.now, :end_time => Time.now + 30.days)
=> #<IceCube::Schedule:0xb63caabc #all_recurrence_rules=[], #duration=nil, #end_time=Wed Jan 09 00:03:36 -0600 2013, #all_exception_rules=[], #start_time=Mon Dec 10 00:03:36 -0600 2012>
Put all the days that occur within the schedule into an array.
1.8.7 :098 > days_in_schedule = []
=> []
1.8.7 :099 > schedule.start_time.to_date.upto(schedule.end_time.to_date) { |d| puts d; days_in_schedule << d }
Iterate over the array and create a SingleOccurrenceRule for each day in the schedule. Then test a couple dates. Within 30 days, occurs_on? is true, outside of 30 days, occurs_on? is false. This seems correct, except it still returns false when checking if schedule.occurs_on? Date.today. WHY?!?!?
1.8.7 :100 > days_in_schedule.each { |d| schedule.rtime Time.parse(d.to_s) }
1.8.7 :109 > schedule.terminating?
=> true
1.8.7 :110 > schedule.occurs_on? Date.today + 5.days
=> true
1.8.7 :111 > schedule.occurs_on? Date.today + 55.days
=> false
1.8.7 :135 > schedule.occurs_on? Date.today
=> false

Why does Date.new not call initialize?

I want to create a subclass of Date.
A normal, healthy, young rubyist, unscarred by the idiosyncrasy of Date's implementation would go about this in the following manner:
require 'date'
class MyDate < Date
def initialize(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
super(year, 12, 25)
end
end
And proceed to use it in the most expected manner...
require 'my_date'
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
... only to be double-crossed by the fact, that the Date::new method is actually an alias to Date::civil, which doesn't ever call initialize. In this case, the last piece of code prints "2012-01-28" instead of the expected "2012-12-25".
Dear Ruby-community, wtf is this?
Is there some very good reason for aliasing new, so that it ignores initialize, and as a result, any common sense and regard for the client's programmer's mental health?
You define initialize, but you create the new instance with new. new returns a new instance of the class, not the result of initialize.
You may do:
require 'date'
class MyDate < Date
def self.new(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
super(year, 12, 25)
end
end
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
Remark:
#original_month and #original_day are not available in this solution. The following solution extends Date, so you can access the original month and day. For normal dates, the values will be nil.
require 'date'
class Date
attr_accessor :original_month
attr_accessor :original_day
end
class MyDate < Date
def self.new(year, month, day)
# Christmas comes early!
date = super(year, 12, 25)
date.original_month = month
date.original_day = day
date
end
end
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
puts mdt.original_month
But I would recommend:
require 'date'
class MyDate < Date
def self.create(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
new(year, 12, 25)
end
end
mdt = MyDate.create(2012, 1, 28)
puts mdt.to_s
or
require 'date'
class Date
def this_year_christmas
# Christmas comes early!
self.class.new(year, 12, 28)
end
end
mdt = Date.new(2012, 1, 28).this_year_christmas
puts mdt.to_s

How can I find the dates for the same week a year ago?

This is an example of a week, from Sunday to Saturday:
11/21/2010 - 11/27/2010
I would like to find the dates for the same week Sunday-Saturday, only for last year.
require 'date' # Included in Ruby's standard library, no gem needed
now = Date.today
before = Date.civil( now.year-1, now.month, now.day )
sunday = Date.commercial( before.year, before.cweek, 1 ) - 1 # Day 1 is Monday
this_week_last_year = sunday..(sunday+6)
Edit: Though Date.commercial is cool, it's not needed. Here's a Simpler way to find the Sunday starting the week:
require 'date'
now = Date.today
before = Date.civil( now.year-1, now.month, now.day )
sunday = before - before.wday
>> 1.year.ago.beginning_of_week.to_date
Mon, 30 Nov 2009
>> 1.year.ago.end_of_week.to_date
Sun, 06 Dec 2009
require 'chronic'
Chronic.parse '1 year ago'
# => 2009-12-01 14:05:39 -0800
Chronic is a pretty sweet rubygem which can handle a range of things, including being adapted for your particular request.
Try this:
require 'active_support/all'
today = Time.now #=> 2010-12-01 14:58:36 -0700
sunday = (today - today.wday.days).beginning_of_day #=> 2010-11-28 00:00:00 -0700
saturday = sunday + 6.days #=> 2010-12-04 00:00:00 -0700
sunday.wday #=> 0
saturday.wday #=> 6
sunday - 1.year #=> 2009-11-28 00:00:00 -0700
sunday.prev_year #=> 2009-11-28 00:00:00 -0700
saturday - 1.year #=> 2009-12-04 00:00:00 -0700
saturday.prev_year #=> 2009-12-04 00:00:00 -0700
You could also figure out the week of the year for one of the two days, then subtract 365.days
ActiveSupport was actually split into finer granularity with Rails 3, so you don't have to load in the entire suite if you don't want. I did it for simplicity. More info is on the ActiveSupport Core Extensions page.
Or, you could get jiggy with string parsing:
require 'chronic'
Chronic.parse('1 year ago last sunday') #=> 2009-11-28 12:00:00 -0700
Chronic.parse('1 year ago next saturday') #=> 2009-12-04 12:00:00 -0700
I like Chronic, and, for this sort of parsing I think it's a OK solution because the string is something you'd create, and not a user, so there's less chance of your code blowing up with an unparsable string. I'm not sure if there's a speed hit because of the parsing though so some benchmarks might be in order if the parsing was going to be in a loop.

get next/previous month from a Time object

I have a Time object and would like to find the next/previous month. Adding subtracting days does not work as the days per month vary.
time = Time.parse('21-12-2008 10:51 UTC')
next_month = time + 31 * 24 * 60 * 60
Incrementing the month also falls down as one would have to take care of the rolling
time = Time.parse('21-12-2008 10:51 UTC')
next_month = Time.utc(time.year, time.month+1)
time = Time.parse('01-12-2008 10:51 UTC')
previous_month = Time.utc(time.year, time.month-1)
The only thing I found working was
time = Time.parse('21-12-2008 10:51 UTC')
d = Date.new(time.year, time.month, time.day)
d >>= 1
next_month = Time.utc(d.year, d.month, d.day, time.hour, time.min, time.sec, time.usec)
Is there a more elegant way of doing this that I am not seeing?
How would you do it?
Ruby on Rails
Note: This only works in Rails (Thanks Steve!) but I'm keeping it here in case others are using Rails and wish to use these more intuitive methods.
Super simple - thank you Ruby on Rails!
Time.now + 1.month
Time.now - 1.month
Or, another option if it's in relation to the current time (Rails 3+ only).
1.month.from_now
1.month.ago
Personally I prefer using:
Time.now.beginning_of_month - 1.day # previous month
Time.now.end_of_month + 1.day # next month
It always works and is independent from the number of days in a month.
Find more info in this API doc
you can use standard class DateTime
require 'date'
dt = Time.new().to_datetime
=> #<DateTime: 2010-04-23T22:31:39+03:00 (424277622199937/172800000,1/8,2299161)>
dt2 = dt >> 1
=> #<DateTime: 2010-05-23T22:31:39+03:00 (424282806199937/172800000,1/8,2299161)>
t = dt2.to_time
=> 2010-05-23 22:31:39 +0200
There are no built-in methods on Time to do what you want in Ruby. I suggest you write methods to do this work in a module and extend the Time class to make their use simple in the rest of your code.
You can use DateTime, but the methods (<< and >>) are not named in a way that makes their purpose obvious to someone that hasn't used them before.
If you do not want to load and rely on additional libraries you can use something like:
module MonthRotator
def current_month
self.month
end
def month_away
new_month, new_year = current_month == 12 ? [1, year+1] : [(current_month + 1), year]
Time.local(new_year, new_month, day, hour, sec)
end
def month_ago
new_month, new_year = current_month == 1 ? [12, year-1] : [(current_month - 1), year]
Time.local(new_year, new_month, day, hour, sec)
end
end
class Time
include MonthRotator
end
require 'minitest/autorun'
class MonthRotatorTest < MiniTest::Unit::TestCase
describe "A month rotator Time extension" do
it 'should return a next month' do
next_month_date = Time.local(2010, 12).month_away
assert_equal next_month_date.month, 1
assert_equal next_month_date.year, 2011
end
it 'should return previous month' do
previous_month_date = Time.local(2011, 1).month_ago
assert_equal previous_month_date.month, 12
assert_equal previous_month_date.year, 2010
end
end
end
below it works
previous month:
Time.now.months_since(-1)
next month:
Time.now.months_since(1)
I just want to add my plain ruby solution for completeness
replace the format in strftime to desired output
DateTime.now.prev_month.strftime("%Y-%m-%d")
DateTime.now.next_month.strftime("%Y-%m-%d")
You can get the previous month info by this code
require 'time'
time = Time.parse('2021-09-29 12:31 UTC')
time.prev_month.strftime("%b %Y")
You can try convert to datetime.
Time gives you current date, and DateTime allows you to operate with.
Look at this:
irb(main):041:0> Time.new.strftime("%d/%m/%Y")
=> "21/05/2015"
irb(main):040:0> Time.new.to_datetime.prev_month.strftime("%d/%m/%Y")
=> "21/04/2015"
Here is a solution on plain ruby without RoR, works on old ruby versions.
t=Time.local(2000,"jan",1,20,15,1,0);
curmon=t.mon;
prevmon=(Time.local(t.year,t.mon,1,0,0,0,0)-1).mon ;
puts "#{curmon} #{prevmon}"
Some of the solutions assume rails. But, in pure ruby you can do the following
require 'date'
d = Date.now
last_month = d<<1
last_month.strftime("%Y-%m-%d")
Im using the ActiveSupport::TimeZone for this example, but just in case you are using Rails or ActiveSupport it might come in handy.
If you want the previous month you can substract 1 month
time = Time.zone.parse('21-12-2008 10:51 UTC')
time.ago(1.month)
$ irb
irb(main):001:0> time = Time.now
=> 2016-11-21 10:16:31 -0800
irb(main):002:0> year = time.year
=> 2016
irb(main):003:0> month = time.month
=> 11
irb(main):004:0> last_month = month - 1
=> 10
irb(main):005:0> puts time
2016-11-21 10:16:31 -0800
=> nil
irb(main):006:0> puts year
2016
=> nil
irb(main):007:0> puts month
11
=> nil
irb(main):008:0> puts last_month
10
=> nil

Resources