Time in DB compared to current time - ruby-on-rails

I have a couple of stores that I'd like to display if they're open or not.
The issue is that I have my current time.
Time.current
=> Sat, 11 Jun 2016 11:57:41 CEST +02:00
and then if I for example take out the open_at for a store I get:
2000-01-01 00:00:00 +0000
so what I have now is:
def current_business_hour(current_time: Time.current)
business_hours.where('week_day = ? AND open_at <= ? AND close_at >= ?',
current_time.wday, current_time, current_time).first
end
and then I check if a current_business_hour is present. However this is calculating it wrong by what seems like two hours. The open_at and close_at should be within the time zone of Time.current.

In Rails, dates and times are normally saved in UTC in the database and Rails automatically converts the times to/from the local time zone when working with the record.
However, for pure time type columns, Rails doesn't do such automatic conversion if the time is specified as a string only. It must be specified as a Time object instead, which includes the local time zone.
So, for example, if you wanted to store the open_at time as 14:00 local time, you should not set the attribute with a plain string, because it will be saved to the db verbatim, not converted to UTC:
business_hour.open_at = '14:00'
business_hour.save
# => UPDATE `business_hours` SET `open_at` = '2000-01-01 14:00:00.000000', `updated_at` = '2016-06-11 15:32:14' WHERE `business_hours`.`id` = 1
business_hour.open_at
# => 2000-01-01 14:00:00 UTC
When Rails reads such record back, it indeed thinks it's '14:00' UTC, which is off by 2 hours in the CEST zone.
You should convert the time from string to a Time object instead, because it will contain the proper local time zone:
business_hour.open_at = Time.parse('14:00')
business_hour.save
# => UPDATE `business_hours` SET `open_at` = '2000-01-01 12:00:00.000000', `updated_at` = '2016-06-11 15:32:29' WHERE `business_hours`.`id` = 1
business_hour.open_at
# => 2016-06-11 14:00:00 +0200
Note that the column is now stored in UTC time. Now, you can safely compare the time columns with any other rails datetime objects, such as Time.current.

Related

UTC conversion is different between local and server

I am building a Rails 5 app.
In this app I have a query that checks if an Event is already in the database. This works perfectly locally but not on the server. The problem, as I see it is that I convert the date I want to check into UTC and then do the query. The strange thing is that the output of the conversion is different between local and server for the exact same date.
This is the query
scope :available_holidays, -> (date) { where("events.starts_at > ? AND events.starts_at < ? AND ttype = ?", date.beginning_of_day, date.end_of_day, TYPE_NATIONAL) }
This is how I convert to UTC
Time.parse(holiday[:date].to_s).utc
This is the output locally
2017-12-31 00:00:00
This is the output on the server
2018-01-01 00:00:00
This is my method in the controller
def holidays
output = []
from = Date.civil(params["year"].to_i,1,1)
to = Date.civil(params["year"].to_i,12,31)
holidays = Holidays.between(from, to, params["country"])
holidays.each do |holiday|
date = Time.parse(holiday[:date].to_s).utc
event = Event.available_holidays(date).count
if event == 0
output.push(holiday)
end
end
render json: output.to_json
end
What is going wrong?
I see a number of possible problems.
holidays.each do |holiday|
date = Time.parse(holiday[:date].to_s).utc
Why is Holidays.between returning hashes rather than objects?
Why is holiday[:date] a string and not a Date object?
Why is a date being parsed with Time.parse?
Why are time zones involved with a date?
The first two are structural problems. The second two I think are the real problems. Dates don't have time zones.
When you run, for example, Time.parse("2018-05-06") Ruby will return a Time for midnight at that date with a time zone. For example...
> Time.parse("2018-01-01")
=> 2018-01-01 00:00:00 -0800
> Time.parse("2018-01-01").utc
=> 2018-01-01 08:00:00 UTC
As a Date that's 2018-01-01 so no harm. But if you were in +0100...
> Time.parse("2018-01-01 00:00:00 +0100").utc
=> 2017-12-31 23:00:00 UTC
As a Date that's 2017-12-31.
Don't convert dates to times and back to dates. Just keep them as dates.
holidays.each do |holiday|
date = Date.parse(holiday[:date])
Better yet, have Holidays store holiday[:date] as a Date object.

Time.now.beginning_of_week giving two different values

I am trying to get values from a table for a weeks data as below
Answer.where("ct_id = ? AND ot_id = ? AND created_at >= ?",16,72,Time.now.beginning_of_week)
It fires a query
Answer Load (0.3ms) SELECT `answers`.* FROM `answers` WHERE (ct_id = 16 AND ot_id = 72 AND created_at >= '2016-02-28 18:30:00')
time in the above query is '2016-02-28 18:30:00'
But in rails console the value is
Time.now.beginning_of_week
=> 2016-02-29 00:00:00 +0530
So which is the correct value and why is it giving two different value when i try it in console.
I know that Time.now.beginning_of_week starts the week from Sunday ie 18th ,but why is that when i do it in console its showing 19th monday.
Hope some one can clarify my doubt on it.
When you fire Time.now.beginning_of_week in console it gives time in you local timezone i.e +5.30 (Indian standard time).
All times stored in database are stored in utc.
Answer.where("ct_id = ? AND ot_id = ? AND created_at >= ?",16,72,Time.now.beginning_of_week)
When we use Time.now.beginning_of_week in above query, it will get converted to utc as we need to compare with record stored in db, which is in utc.
To convert below time 2016-02-29 00:00:00 +0530 into utc time, we have to subtract -5.30 from it as it is +5.30 from utc.
Time in utc = 2016-02-29 (00:00:00 - 5.30)
Time in utc = 2016-02-29 18.30
Update:
If you want to get beginning_of_week in utc. You can do following in rails 4. Not sure whether this works on rails 3.
Date.today.beginning_of_week.to_time(:utc) # o/p 2016-02-29 00:00:00 UTC

Rails4: Saving and displaying date in user's timezone

I am working on a rail4 app. Where I want to store dates in all mysql tables in UTC. However I store user's timezone in a specific table, called users. When user logs in, I get user's timezone form user table and save in session.
I am able to save date in all tables in UTC as default value of config.time_zone is UTC for activerecords and activemodels. But while displaying I want to show dates in user's timezone. As well as, when any user inputs a date/time in any html form, then I want to save it in the equivalent UTC format.
What is the best way to achieve this?
Rails, activerecord and MySQL will save all the timestamp fields in UTC. Without you having to do anything.
In your application.rb file where the configuration of the Application is done, you define the default time zone if you want the display of timestamps to take place on time zone different from UTC.
Hence
config.time_zone = 'Central Time (US & Canada)'
will display the timestamp fields (without you having to do anything special in other piece of code) using the Central Time.
When you want each of your users to have timestamps displayed in different time zone you can store the time zone in a column along side the user data. The column can be called time_zone and can contain the string of the user preferred time zone.
But, you have to tell the timestamp object to display itself to the specific timezone. This is done with the help of the method in_time_zone(timezone) that DateTime object responds to.
Example (when the default time zone is UTC):
1.9.3-p194 :004 > d = DateTime.new(2012, 9, 1, 6, 30, 0)
=> Sat, 01 Sep 2012 06:30:00 +0000
1.9.3-p194 :005 > d.in_time_zone("Central Time (US & Canada)")
=> Sat, 01 Sep 2012 01:30:00 CDT -05:00
Or you can change the time zone globally for the request at hand on a before or around filter. There is a documentation on internet if you do a google on that.
Read also this one: http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
for various alternatives to approach the problem.
You could store the time in UTC, and store the timezone separately. Timezones are commonly stored as a UTC-offset in seconds (seconds are the SI unit of time).
Then you can display it like so:
utime = Time.now.utc.to_i # this value can be any format that Time.at can understand. In this example I'll use a unix timestamp in UTC. Chances are any time format you store in your DB will work.
=> 1375944780
time = Time.at(utime) # parses the time value (by default, the local timezone is set, e.g. UTC+08:00)
=> 2013-08-08 14:53:00 +0800
time_in_brisbane = time.in_time_zone(ActiveSupport::TimeZone[36000]) # sets the timezone, in this case to UTC+10:00 (see http://stackoverflow.com/a/942865/72176)
=> Thu, 08 Aug 2013 16:53:00 EST +10:00
time_brisbane.strftime("%d %b %Y, %l:%M %p %z") # format with strftime however you like!
=> "08 Aug 2013, 4:53 PM +1000"

Rails timezone differences between Time and DateTime

I have the timezone set.
config.time_zone = 'Mountain Time (US & Canada)'
Creating a Christmas event from the console...
c = Event.new(:title => "Christmas")
using Time:
c.start = Time.new(2012,12,25)
=> 2012-12-25 00:00:00 -0700 #has correct offset
c.end = Time.new(2012,12,25).end_of_day
=> 2012-12-25 23:59:59 -0700 #same deal
using DateTime:
c.start = DateTime.new(2012,12,25)
=> Tue, 25 Dec 2012 00:00:00 +0000 # no offset
c.end = DateTime.new(2012,12,25).end_of_day
=> Tue, 25 Dec 2012 23:59:59 +0000 # same
I've carelessly been using DateTime thinking that input was assumed to be in the config.time_zone but there's no conversion when this gets saved to the db. It's stored just the same as the return values (formatted for the db).
Using Time is really no big deal but do I need to manually offset anytime I'm using DateTime and want it to be in the correct zone?
Yep. Time.new will intepret parameters as local time in the absence of a specific timezone, and DateTime.new will intepret parameters as UTC in the absence of a specific timezone. As documented. You may want to replace Time.new with Time.local for clarity throughout your code.
What you can do is use DateTime's mixins for Time to invoke Time.local(2012,12,25).to_datetime. But if the year/month/day is coming from a user/browser, then perhaps you should get the user's/browser's timezone instead of using your server's.
If you need to create a migration to fix existing data in the DB, new_date_time = Time.local(old_date_time.year, old_date_time.mon, old_date_time.mday, old_date_time.hour, old_date_time.min, old_date_time.sec).to_datetime

Rails Time objects

I've got a quick-add action in my events controller, as the client really only schedules events at three different time slots in a given day. Date and Time are working fine with the default form, but trying to set the values by hand are giving me some trouble.
def quick_add #params are date like 2012-04-29, timeslot is a string
timeslot = params[:timeslot].to_sym
date = params[:date].to_date
#workout = Workout.new do |w|
w.name = 'Workout!'
w.date = date
case timeslot
when :morning
w.time = Time.local(w.date.year, w.date.month, w.date.day, 6)
when :noon
w.time = Time.local(w.date.year, w.date.month, w.date.day, 12)
when :evening
w.time = Time.local(w.date.year, w.date.month, w.date.day, 18, 15)
else
w.time = Time.now
end
end
The events are getting created, the dates are correct, but times are:
Morning: 2000-01-01 10:00:00 UTC
Expected: 2012-05-02 06:00:00 UTC -400
Noon: 2000-01-01 16:00:00 UTC
Expected: 2012-05-02 12:00:00 UTC -400
Evening: 2000-01-01 22:15:00 UTC
Expected: 2012-05-02 18:15:00 UTC -400
It's worth noting that running the commands in rails console seems to get the results I'd expect.
Time values are stored in UTC/GMT (+0) time zone as an integer so that no time zone data has to be stored with it. Rails always stores times in the database as UTC times. When you read them out, you'll want to convert them back to your local time zone again.
You can use time.getlocal to convert it to your local time zone.

Resources