Issue comparing DateTime/Time in different zones in Rails - ruby-on-rails

In my application the user sets a specific end time for a task. When a task is loaded, it creates a corresponding CheckIn using that task's information (self in this case is the task):
def create_checkin_end_time(day)
week = (Date.today .. Date.today + 6)
week.each do |day_of_week|
if day_of_week.strftime("%A") == day
return DateTime.new(day_of_week.year, day_of_week.month, day_of_week.day, self.end_time.hour, self.end_time.min)
end
end
end
I've set config.time_zone equal to "Eastern Time (US & Canada") but instead of creating the task as UTC it submits as EST then subtracts 4 hours from that.
[2] pry(main)> CheckIn.last.end_time
CheckIn Load (0.7ms) SELECT "check_ins".* FROM "check_ins" ORDER BY "check_ins"."id" DESC LIMIT 1
=> Sun, 20 Oct 2013 19:30:00 EDT -04:00
[3] pry(main)> Task.last.end_time
Task Load (0.7ms) SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT 1
=> Sun, 20 Oct 2013 23:30:00 EDT -04:00
The end time for that instance of the model should be 11 PM, based on the task that was used to create it. This is my first go at working with time in Rails so any help would be really appreciated. Thank you.
UPDATE:
Switched it back to UTC and added the method to ApplicationController, but the real problem here was the way I was creating the CheckIn. Because Task.end_time was used as reference in the creation and was called out of the database as EST, the offset was doubled in the creation of the CheckIn. Real weird. My solution was to use .change on creation:
CheckIn.create(task_id: self.id, start_time: self.create_checkin_start_time(day).change(:hour => self.start_time.hour + 4), end_time: self.create_checkin_end_time(day).change(:hour => self.end_time.hour + 4))
This is by NO MEANS a great/permanent solution. I'm in a bit of a time-crunch at the moment and this will suit my needs.

I'd recommend leaving config.time_zone as UTC so you don't see these kind of weird changes happening in the database.
If you want to force a particular timezone on the users of the application, then your best bet is as a before_filter inside ApplicationController:
before_filter :set_timezone
def set_timezone
Time.zone = "Eastern Time (US & Canada)"
end
That way then, all the times will be saved to the database as UTC times, but will be displayed on the site as being in Eastern time.
The reasons for doing this are pretty simple: it's easier to convert from one universal central time (UTC) than it is to convert from some arbitrary point back to UTC and then to some other arbitrary point.

Related

Why does Date.yesterday() return the day before yesterday in rails?

I am a little bit confused by the Date.yesterday(), for example:
$ rails c
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> Date.today
=> Fri, 10 Jul 2020
irb(main):002:0> Date.yesterday
=> Wed, 08 Jul 2020
irb(main):003:0> Time.now
=> 2020-07-10 03:54:46.02207138 +0530
irb(main):004:0>
But if I am not wrong, if today is Friday, which is true, the previous day should be Thursday as I learnt in my primary school..
What's going on in here?
tl;dr: Use Date.current and Time.current instead of Date.today and Time.now.
Your application has its own time zone, Time.zone. Ruby is not aware of time zones, but Rails is. Rails partially updates Time and Date to be aware of time zones, but not completely. Any methods Rails adds will be time zone aware. Date.yesterday and Date.tomorrow, for example. Built-in Ruby methods it leaves alone, like Date.today. This causes some confusion.
Date.today is giving today according to your local time zone, +0530. Date.yesterday is giving yesterday according to your application's time zone which I'm guessing is +0000 (UTC). 2020-07-10 03:54:46 +0530 is 2020-07-09 22:24:46 UTC so Date.yesterday is 2020-07-08.
Use Date.current instead of Date.today. Date.yesterday is a thin wrapper around Date.current.yesterday. Similarly, use Time.current instead of Time.now.
The ThoughtBot article It's About Time (Zones) discusses Rails time zones in detail and has simple DOs and DON'Ts to avoid time zone confusion.
DON'T USE
Time.now
Date.today
Date.today.to_time
Time.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
DO USE
Time.current
2.hours.ago
Time.zone.today
Date.current
1.day.from_now
Time.zone.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone
Make sure to check the location of the server where the application is running. The value of Date.yesterday will likely depend on the location of the server with respect to the international date line. For example, while it is still Thursday, 07/09/2020 in New York City, it is Friday, 07/10/2020 in New Zealand.
In your Rails app, you may have a value set for Time.zone or in the configuration for config.time_zone.
If one of these values is configured, then Rails uses this as follows:
def yesterday
::Date.current.yesterday
end
def current
::Time.zone ? ::Time.zone.today : ::Date.today
end
These are ActiveSupport Date helpers
So it's indeed a result of wrong Timezone, in my case Time.zone.name returned "UTC". The correct zone for me is Asia/Kolkata, to check the presence of the timezone:
ActiveSupport::TimeZone::MAPPING.select { |x| x[/^kol/i] }
Which returned {"Kolkata"=>"Asia/Kolkata"} in my case.
For it to take effect temporarily, I set Time.zone = 'Kolkata' in the rails console.
For a permanent effect, added config.time_zone = 'Kolkata' to {project_root}/config/application.rb.
This fixed the problem, and Date.yesterday(), Date.today(), and Date.tomorrow() are working as expected.

Time in DB compared to current time

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.

Rails 3 timezone confusions

I'm confused on how rails 3 timezones are supposed to work.
So I config rails to use Pacific time, and tell active record to store in Pacific time.
# application.rb
config.time_zone = 'Pacific Time (US & Canada)'
config.active_record.default_timezone = 'Pacific Time (US & Canada)'
Now I submit update a model and this comes through in the params:
"start_at"=>"2013-07-24 00:00:00"
From the console now:
>> Sale.last
=> #<Sale id: 24, start_at: "2013-07-24 00:00:00", ...snipped... >
>> Sale.last.start_at
=> Tue, 23 Jul 2013 17:00:00 PDT -07:00
>> Sale.last.start_at.in_time_zone
=> Tue, 23 Jul 2013 17:00:00 PDT -07:00
So after trying to force everything to Pacific time, its creating time objects form the database by factoring in the -7 hours of Pacific time.
If I set a time to 2013-07-24 00:00:00 I would expect Tue, 24 Jul 2013 00:00:00 PDT -07:00 to come back out. And yet it does not. I was having similar confusing issues when active record was using UTC to store dates. I had a few tests that would fail only after 5pm when time to date conversions yielded a different date.
How do I tame this? Storing UTC dates in the database seems like a good idea, and I can use in_time_zone on time objects for display, but does that means that times in forms must be UTC?
Our application functionality is very tied to server time, and certain thing happen every day as specific times. So forcing everything to Pacific time seems like it should be fine. But so far I can't seem to make this behave consistently.
How do I make all this not suck?
This is going to be a 1/2 answer, re-iterating from comment thread above with some additional information
I hope to update with more later. There are many gotchas with this stuff
UPDATE: finally did a blog post on rails timezones
http://jessehouse.com/blog/2013/11/15/working-with-timezones-and-ruby-on-rails/
See also: http://www.elabs.se/blog/36-working-with-time-zones-in-ruby-on-rails
i would use UTC for the application timezone and the activerecord default. Use I18n.localize method (setup formats in config/locales/en.yml) for display of dates and datetimes; set the current threads timezone based on user settings or a default (whatever makes sense for your app)
if the current thread is set to the users timezone activerecord should do the right magic and save the offset UTC value in the db, then when you display that value to the user with I18n it will display it as the user entered it (converting from UTC to the users timezone) - it gets a bit tricky when you are entering a time for say an event that is taking place in another timezone - in that case the thread needs to be set to the timezone of that location and using I18n displays need to be for the location instead of the user
setting current users timezone
see http://railscasts.com/episodes/106-time-zones-revised which uses around filter
example below uses before_filter
both assume a time_zone column on the user account
some code
class ApplicationController < ActionController::Base
protect_from_forgery
# auth the user
before_filter :authenticate_user!
# Set user's time zone
before_filter :set_user_time_zone
# ....
def set_user_time_zone
if current_user and current_user.time_zone.present?
Time.zone = current_user.time_zone
# else some default?
end
end
end
Alternatively - set based on browser settings, etc...
see http://guides.rubyonrails.org/i18n.html
specifically: http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization
display date times
Use I18n.localize aka I18n.l or just l - see http://guides.rubyonrails.org/i18n.html#adding-date-time-formats
changing the timezone display for part of a view
User is set to Pacific but showing an event time for an event taking place in Eastern
Time.use_zone(event.location.time_zone) do
puts event.start_time
end
WARNING: I have found the above does not work correctly if event was pulled using find_by_sql method, regular active record queries work well

Time zone confusion (1 hour off)

Time.use_zone('Pacific Time (US & Canada)') do
p Time.zone.now
end
I get the following: => Sun, 14 Apr 2013 20:30:53 PDT -07:00
Yet when I do Rails Time Zone Select.... it says -8:00 quite clearly. Why is it -7 in one area and -8 in another?
Other times, time zones like Hawaii which are -10:00 don't get offset by an hour.
I assume this has something to do with DST, but I'm more curious whether it means it's working properly or improperly and there is something else I need to do.
Ultimately I'm using this in a datepicker, and I find it very odd that when I use Time.zone.parse (along with my time zone around filter), its offsetting everything by 1 hour.
THanks
Edit
Heres a similar problem I also just experienced with another piece of code
2.0.0-p0 :006 >
2.0.0-p0 :006 > u.meetups.in_future.first.meetup_time
Meetup Load (0.4ms) SELECT `meetups`.* FROM `meetups` WHERE `meetups`.`user_id` = 1 AND (meetup_time >= '2013-04-23 04:46:48') ORDER BY meetup_time ASC LIMIT 1
=> Tue, 23 Apr 2013 05:43:00 UTC 00:00
2.0.0-p0 :007 >
Notice the discrepancy in the result compared to the where clause.
Edit
It appears to work for CST properly, but PST is off by ~1 hours?? I feel this is all the same problem, I am just missing a piece of the puzzle.
The output is correct. Pacific Daylight Time has an offset of -7, while Pacific Standard Time has an offset of -8. My guess is that "Rails Time Zone Select" (whatever that is) is only is showing you the "standard" offsets, rather than the current ones. This is common in time zone pickers.
Hawaii does not implement Daylight Savings Time of any kind, so that addresses your second point.
On your third point, I would have to know more about your database platform to answer why the values are converted to UTC. Given that these are event times, I would say that they should be in UTC. They could also be in the time zone of the location of the "meetup", but only if the offset from UTC was also stored. But never should they be in the time zone of the server.
On your fourth point, it's difficult to tell what you mean without more details. Expand if you feel necessary.

Rails - Different timezone to limit daily posts

I'm working on Rails application that let users to make max 2 posts for each day, very simple.
My problem is how to manage different timezone where different users lives. For example if I live in London and the day start at 00.00 and finish at 23.59 the posts count will reset at 00.00, but for another users live in New York the counter will then reset at different time. (not at 00.00) How can I manage this situation?
I hope I explained myself.
UPDATE 1
#Cluster
The problem with your code is that time_zone.utc_offset give a wrong time delta:
1.9.3p194 :123 > time_zone = ActiveSupport::TimeZone.new("Prague")
=> (GMT+01:00) Prague
1.9.3p194 :124 > DateTime.now.utc.midnight - time_zone.utc_offset
=> Thu, 10 Apr 2003 00:00:00 +0000
Why that?
Instead of that I've found useful this code:
User.posts.where(:created_at => DateTime.now.in_time_zone(time_zone).beginning_of_day..DateTime.now.in_time_zone(time_zone).end_of_day).count
It seems to get the correct number of messages between the user's day (with user's timezone). What do you think?
UPDATE 2
#Cluster
What's the difference between:
User.posts.where(:created_at => DateTime.now.in_time_zone(utc_time_zone).beginning_of_day..DateTime.now.in_time_zone(utc_time_zone).end_of_day).count
and yours:
1.9.3p327 :015 > time_zone = ActiveSupport::TimeZone.new("Prague")
=> (GMT+01:00) Prague
1.9.3p327 :016 > Time.zone.now.utc.midnight - time_zone.utc_offset
=> 2013-02-15 23:00:00 UTC
in terms of performances and way of doing?
UPDATE 3
Actually the code in your example doesn't work, look here:
# WRONG
1.9.3p194 :096 > user.posts.where('created_at > ?', Time.zone.now.utc.midnight - my_time_zone.utc_offset).count
(0.2ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."sender_id" = 5 AND (created_at > '2013-02-15 23:00:00.000000')
=> 4
# CORRECT
1.9.3p194 :098 > users.posts.where(:created_at => DateTime.now.in_time_zone(my_time_zone).beginning_of_day..DateTime.now.in_time_zone(my_time_zone).end_of_day).count
(0.3ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."sender_id" = 5 AND ("messages"."created_at" BETWEEN '2013-02-16 23:00:00.000000' AND '2013-02-17 22:59:59.000000')
=> 0
Depending on exactly how you want it to work I can think of two solutions.
First, allowing the user to post twice in a 24 hour period, this has the benefit of being timezone agnostic:
User.posts.where('created_at > ?', 24.hours.ago).count
If that returns 2 then don't let them post.
If you really want to ensure that there are no more than 2 posts per day then:
time_zone = ActiveSupport::TimeZone.new(current_user.time_zone)
Users.posts.where('created_at > ?', Time.zone.now.utc.midnight - time_zone.utc_offset).count
User#time_zone needs to hold a valid time zone recognized by Rails.
Since the database datetime (created_at) is already in UTC, first normalize your server time to utc, then set the time to midnight, and apply the utc offset for the timezone. So if you have someone in a -7 timezone, then it will look for posts since 0700 UTC, for someone in a +3 timezone it will look for posts since 2100 UTC.
ActiveSupport::TimeZone
To clarify the difference between the two let me use a couple of examples.
First example is a user posting at 1200 then again at 1800 on Monday.
With the first piece of code, the user will not be able to post again until after 0600 Tuesday. With the second piece of code they will be able to post again at 0000 Tuesday (after midnight).
Second example is a user posts at 2330 then again at 2350 on Monday. Again, in the first example they will not be able to post again until after 2330 Tuesday, but the second example will allow them to post again at midnight, allowing them to post at 0010 and 0020, giving them 4 posts in 50 minutes.
It really depends on what the purpose of the post limitation is for. The first example is going to be much simpler to use and implement as it will work irregardless of time zones.
If your pulling time zone info from FB, then check what format that timezone info is in. I'm guessing it will not be a valid string that ActiveSupport::TimeZone will accept. For example it wants "Mountain Time (US & Canada)" for MST(-0700). If FB is giving you back something like -7 for MST or -25200 (-7 hours in seconds) then you can use those without mucking around with AS:TZ
RE: Update 1
Here's the output using Time.zone instead of DateTime
1.9.3p327 :015 > time_zone = ActiveSupport::TimeZone.new("Prague")
=> (GMT+01:00) Prague
1.9.3p327 :016 > Time.zone.now.utc.midnight - time_zone.utc_offset
=> 2013-02-15 23:00:00 UTC
Consider to use UTC timezone for everyone. I.e. StackOverflow uses that approach for votes and it works fine.

Resources