Ruby: Apply time and day of week to a DateTime - ruby-on-rails

Question:
What's the easiest way to take a DateTime object "A" and apply the day of week and time of another DateTime object "B" so that I have a new DateTime object that's in the week of DateTime object "A" but has the day of week and time of DateTime object "B"?
Some context:
I'm creating a scheduling app where users have recurring appointments. All users set their default day of week and time in form of a DateTime object (I save it as a DateTime but actually I don't care about the date itself).
When I want to extend their recurring appointments (e.g. for another 6 months), I need to get the week of their last appointment, jump to the default day of week and time within that week and extend from there.

require 'date'
def new_date_time(a,b)
a_date = a.to_date
new_date = a_date + (b.to_date.wday - a_date.wday)
DateTime.new(new_date.year, new_date.month, new_date.day,
b.hour, b.minute, b.second)
end
a = DateTime.new(2015,8,6,4,5,6) # Thursday
#=> #<DateTime: 2015-08-06T04:05:06+00:00 ((2457241j,14706s,0n),+0s,2299161j)>
b = DateTime.new(2015,8,3,13,21,16) # Monday
new_date_time(a,b)
#=> #<DateTime: 2015-08-03T13:21:16+00:00 ((2457238j,48076s,0n),+0s,2299161j)>
b = DateTime.new(2015,11,17,13,21,16) # Tuesday
new_date_time(a,b)
#=> #<DateTime: 2015-08-04T13:21:16+00:00 ((2457239j,48076s,0n),+0s,2299161j)>

Related

How to create new Date instance only by a day

In my rails project I want to get a Date instance only by a day. Year and month are used current value.
I could write like these:
day = 3
date = Date.new(Date.current.year, Date.current.month, day)
and
date = Date.current.beginning_of_month + (day - 1).days
How would you write function like this?
Is there a better implementation?
If you already have a Date object, you can do:
date.change(day: 3)
where date is a Date or DateTime object. Also you can do:
Date.today.change(day: 3)

Rails - change default time in Time datatype

In my view, I will be ordering a list of items by their start_time. There are instances, however, where there might be a start_time of 1:00 AM (the following day) that should show up as after a start_time of 11:00 PM (previous day). Since the Time datatype stores a default date of 2001-01-01, it considers an entry of 1:00 AM to be on 2001-01-01, not 2001-01-02.
Thus, my question is, is there a way to change the default date in the Time datatype?
I suppose an obvious solution would be to instead store the start and end times in a DateTime datatype and enter a corresponding date. For this application, however, it is customary to refer to a start time of 1:00 AM as "belonging to" the previous day and would thus be confusing to enter the following day's date. (E.g. When attending your favorite band's concert on a Friday night, their set might start at 12:30 am Saturday morning, but you would still consider the concert to be on a Friday night). Thank you for your help.
Not a direct answer to your question, but a possible solution to your problem: you could create a custom sort that sorts times < 1 AM last.
sorted_concerts = Concert.order('CASE WHEN start_time <= "2001-01-01 01:00:00" THEN 2 ELSE 1 END, start_time')
This way you can leave the db columns as is but get your expected order for concerts. Excepting for this 1 AM inversion, the times will sort normally in ascending order.
After much more research, it doesn't seem like the default date of "2001-01-01" that Ruby applies to a time stored in a Time datatype can be adjusted. I guess this answers my question - but - to solve the problem, I changed the db columns to datetime datatype and logged a default date, that would adjust if the time entry was after midnight. Controller action below:
def create
start_time = DateTime.parse("#{run_of_show_item_params[:date]} #{run_of_show_item_params[:start_time]}")
end_time = DateTime.parse("#{run_of_show_item_params[:date]} #{run_of_show_item_params[:end_time]}")
#run_of_show_item = RunOfShowItem.new(run_of_show_item_params)
#run_of_show_item.start_time = start_time
#run_of_show_item.end_time = end_time
#run_of_show_item.start_time+=1.days if run_of_show_item_params[:start_time] < "07:00"
#run_of_show_item.end_time+=1.days if run_of_show_item_params[:start_time] < "07:00"
if #run_of_show_item.save
flash[:success] = "Performance added!"
redirect_to :back
else
render 'new'
end
end

The best way to compare two dates without a respect to their timezone difference?

I have two dates: one in 'UTC' and the other 'America/Los_Angeles'. How can I compare these two ignoring their timezone difference i.e. I want to check if their year, month, day, hour, ... are equal to each other?
local_date_string = '2014-09-19T09:00:00'
local_date = local_date_string.to_datetime
time_zone_name = 'America/Los_Angeles'
timezone = ActiveSupport::TimeZone[time_zone_name]
date_with_zone = ActiveSupport::TimeWithZone.new(nil, timezone, local_date)
date_utc = date_with_zone.utc
local_date_with_zone_from_utc = date_utc.in_time_zone(timezone)
puts local_date.hour => 9
puts local_date_with_zone_from_utc.hour => 9
how to compare_ignoring_tz_diff(local_date, local_date_with_zone_from_utc) ?
Sounds like you are looking for the time method from ActiveSupport::TimeWithZone. This returns a Time object with a time zone of +0000 but with all the components (hour, minute, day) etc. unchanged.
If you have two time with zones, then call this on both and compare the result ie
def compare_without_tz(first, second)
first.time == second.time
end
This assumes both arguments are ActiveSupport::TimeWithZone - if you want to also be able to pass normal instances of Time, DateTime then you'll need to check whether their type first

Given a day name in a string, how can I get a Time object representing that day within the current week?

Given a string with day name, i.e. "Wednesday", how can I get a Time object representing that day within the current week (Monday - Sunday)?
So for example, if the current day is Tuesday 29 May, and the string is "Monday", the time object should represent Monday 28 May. If the string is "Friday", it should be Friday 1 June.
Whereas if the current day is Tuesday 5 June, and the string is "Monday", the time object should represent Monday 4 June. If the string is "Friday", it should be Friday 8 June.
I am using Rails 3.2.0 and Ruby 1.9.2.
There is a pretty handy gem for these types of textual date references called Chronic:
http://chronic.rubyforge.org/
For your purpose you could use the "this week" reference:
Chronic.parse("monday this week")
Chronic.parse("friday this week")
..etc..
..which should work pretty much as expected.
It's simple enough to calculate, but you have to first parse the day names, which means you need a list of them:
WEEKDAY_NAMES = %w<Sunday Monday Tuesday Wednesday Thursday Friday Saturday>
If you want to avoid hard-coding them (and even honor locale settings if you're still on Ruby 1.8), you can do this instead:
WEEKDAY_NAMES = (3..9).map { |i| Time.at( i * 86400 ).utc.strftime '%A' }
Then you can use logic like this to get the number:
wday = WEEKDAY_NAMES.each_with_index.find{|name,number|name == 'Wednesday'}[1]
Or, better, build up a hash of name->number pairs:
wday_from_name = Hash[*WEEKDAY_NAMES.each_with_index.to_a.flatten]
wday = wday_from_name['Wednesday']
Once you have the target weekday in numeric form:
now = Time.now
wday_this_week = now + (wday - now.wday) * 86400
Unlike Chronic, this method considers weeks to run Sunday through Saturday (because that's how the Time#wday numbers work, with 0 meaning Sunday). You can easily adjust that to use a different day as the first day of the week, however.
WEEK_START = wday_from_name['Monday']
wday_this_week = now +
((wday - WEEK_START)%7 - (now.wday - WEEK_START)%7) * 86400
Since you're in Rails, you can use the ActiveSupport core extensions to make this a little bit prettier, with e.g.:
WEEKDAY_NAMES = (3..9).map { |i| Time.at( i.days ).utc.strftime '%A' }
wday_this_week = now +
((wday - WEEK_START)%7 - (now.wday - WEEK_START)%7).days

Rails 3.1: Querying Postgres for records within a time range

In my app I have a Person model. Each Person has an attribute time_zone that specifies their default time zone. I also have an Event model. Each Event has a start_time and end_time timestamp, saved in a Postgres database in UTC time.
I need to create a query that finds events for a particular person that fall between midnight of one day and midnight of the next. The #todays_events controller variable hold the results of the query.
Part of the reason that I'm taking this approach is that I may have people from other time zones looking at the list of events for a person. I want them to see the day as the person would see the day and not based on the time zone they are in as an observer.
For whatever reason, I'm still getting some events from the previous day in my result set for #todays_events. My guess is that I'm comparing a UTC timestamp with a non-UTC parameter, or something along those lines. Generally, only events that begin or end in the evening of the previous day show up on the query result list for today.
Right now, I'm setting up:
#today = Time.now.in_time_zone(#person.time_zone).midnight.to_date
#tomorrow = (#today + 1.day ).to_datetime
#today = #today.to_datetime
My query looks like:
#todays_activities = #person.marks.where("(start_time >= ? AND start_time < ?) OR (end_time >= ? AND end_time < ?);", #today, #tomorrow, #today, #tomorrow ).order("start_time DESC")
How should I change this so that I'm guaranteed only to receive results from today (per the #person.time_zone in the #todays_activities query?
You're losing track of your timezones when you call to_date so don't do that:
#today = Time.now.in_time_zone(#person.time_zone).midnight.utc
#tomorrow = #today + 1.day
When you some_date.to_datetime, you get a DateTime instance that is in UTC so the result of something like this:
Time.now.in_time_zone(#person.time_zone).midnight.to_date.to_datetime
will have a time-of-day of 00:00:00 and a time zone of UTC; the 00:00:00 is the correct time-of-day in #person.time_zone but not right for UTC (unless, of course, #person is in in the +0 time zone).
And you could simplify your query with overlaps:
where(
'(start_time, end_time) overlaps (timestamp :today, timestamp :tomorrow)',
:today => #today, :tomorrow => #tomorrow
)
Note that overlaps works with half-open intervals:
Each time period is considered to represent the half-open interval start <= time < end, unless start and end are equal in which case it represents that single time instant.

Resources