Why do my time-related unit tests break every day at 4pm? - ruby-on-rails

For a long time I've been having this issue that at a certain time of the day, a TON of my tests break. I have a lot of tests that are doing simple date comparisons and everything runs fine from midnight to like 4:00 in the afternoon. Any idea why this is happening? I've set my timezone in my environment file too.
It seems like some of my calls like 5.days.from_now.to_date are adding an extra day.
Edit
For instance, this test fails:
# Widget that creates items for how many days the trip is gone.
def test_should_create_correct_amount_of_days_for_trip
w = DayWidget.create(:trip => trips(:hawaii))
assert_equal w.days.size, 5
end
# Code in trip model that calculates amount of days
def number_of_days
(self.return_date.to_date - self.depart_date.to_date).to_i + 1
end
# Test fixture yaml for Hawaii
hawaii:
depart_date: <%= Time.now.tomorrow.to_s(:db) %>
return_date: <%= 5.days.from_now.to_s(:db) %>
After 4:00 pm, the test above fails and says it created 6 days instead of 5. :(

You're probably in the Pacific time zone, 8 hours behind UTC (which is why at 4:00p they start breaking, since that's when it hits midnight UTC).
Without seeing your test/comparison code, all I could say is to make sure you're comparing dates/times with the same location (UTC to UTC or localtime to localtime).
Update: Ok, it looks like Time.now returns a Time object, whereas using XXX.days.from_now returns an ActiveSupport::TimeWithZone object, resulting in different handling of timezones:
ruby-1.9.2-p136 :009 > (Time.now+5.days).to_s(:db)
=> "2011-02-08 19:40:24"
ruby-1.9.2-p136 :010 > 5.days.from_now.to_s(:db)
=> "2011-02-09 03:40:29"
My recommendation would be to either call .utc on your times in your fixtures like so:
ruby-1.9.2-p136 :017 > 5.days.from_now.utc.to_s(:db)
=> "2011-02-09 03:42:39"
ruby-1.9.2-p136 :018 > (Time.now+5.days).utc.to_s(:db)
=> "2011-02-09 03:42:39"
or to just switch to using 1.day.from_now instead of Time.now.tomorrow to keep the types consistent.

You can also use Time.current instead of Time.now.
Time.current returns ActiveSupport::TimeWithZone just like #ago and #from_now, so you can safely compare dates returned by them.

If you are truly dealing with just dates, be sure to set the hours, minutes and seconds to 0, otherwise you are at the mercy of the time of day you create the dates.

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.

Rails Upgrade makes default Time format change. How to revert?

I am upgrading a Rails app from
Rails 4.2 -> 5.2 (a subsequent upgrade to Rails 6 is pending)
Ruby 2.2 -> 2.5
Postgres 9.1 -> 10
in various steps. Since the Rails upgrade requires the Postgres upgrade I can't separate the upgrades in a sensible way.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
This makes a lot of API tests fail which were previously all returning UTC times.
Example for Rails 4.2, Ruby 2.2, PG 9.1 for a PhoneNumber object:
2.2.6 :002 > p.time_weekdays_from
=> 2000-01-01 07:00:00 UTC
2.2.6 :003 > p.time_weekdays_from.class
=> Time
Example for Rails 5.2, Ruby 2.5, PG 10:
irb(main):016:0> p.time_weekdays_from
=> Sat, 01 Jan 2000 11:15:00 CET +01:00
irb(main):018:0> p.time_weekdays_from.class
=> ActiveSupport::TimeWithZone
I have added an initializer to override this for the time being and this seems to work fine, but I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
# This works, but why is it necessary?
module ActiveSupport
class TimeWithZone
def as_json(options = nil)
self.utc.iso8601
end
end
end
PS: I don't always want UTC, I just want it for the API because that's what our API clients expect.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
This change was made because Ruby's default Time has no understanding of time zones. ActiveSupport::TimeWithZone can. This solves a lot of problems when working with times, time zones, and databases.
For example, let's say your application's time zone is America/Chicago. Previously you had to decide whether you're going to store your times with or without time zones. If you opt for without a time zone, do you store it as UTC or as America/Chicago? If you store it as UTC, do you convert it to America/New York on load or on display? Conversion means adding and subtracting hours from the Time. When you save Time objects you have to be careful to remember what time zone the Time was converted to and to convert it back to the database's time zone. Coordinating all this leads to many bugs.
Rails 5 introduces ActiveSupport::TimeWithZone. This stores the time as UTC and the desired time zone to represent it in. Now handling time is simpler: store it as UTC (ie. timestamp) and add the application's time zone on load. No conversion is necessary. Rails handles this for you.
The change is now timestamp columns, by default, will be formatted in the application's time zone. This takes some getting used to, but ultimately will make your handling of times and time zones more robust.
> Time.zone.tzinfo.name
=> "America/Chicago"
> Time.zone.utc_offset
=> -21600
# Displayed in the application time zone
> Foo.last.created_at
=> Tue, 31 Dec 2019 17:16:14 CST -06:00
# Stored as UTC
> Foo.last.created_at.utc
=> 2019-12-31 23:16:14 UTC
If you have code which manually does time zone conversions, get rid of it. Work in UTC. Time zones are now just formatting.
As much as possible...
Work with objects, not strings.
Work in UTC, time zones are for formatting.
If you need to turn a time into a string, make the formatting explicit.
def get_api_time
'2000-01-01 07:00:00 UTC'
end
# bad: downgrading to strings, implicit formatting
expected_time = Time.utc(2000, 1, 1, 7)
expect( get_api_time ).to eq expected_time
# good: upgrading to objects, format is irrelevant
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect(
Time.zone.parse(get_api_time)
).to eq expected_time
# better: refactor method to return ActiveSupport::TimeWithZone
def get_api_time
Time.zone.parse('2000-01-01 07:00:00 UTC')
end
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect( get_api_time ).to eq expected_time
I recommend reading these articles, they clear things up.
It's About Time (Zones)
The Exhaustive Guide to Rails Time Zones

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.

Rails timestamps don't use the right timezone

I'm a bit confused about timezones in rails. I want my rails app to use British Summer Time (like daylight savings in the US) for the timestamps set in updated_at and created_at in my models. I changed my environment.rb to say
config.time_zone = 'London'
The ubuntu server my app is on seems to use BST for it's time: in the command line, for example, if i type 'date' i get the current time (not offset by an hour). In the rails console, i see the following:
>> time = Time.now
=> Wed Oct 27 16:29:17 +0100 2010
>> time.zone
=> "BST"
All fine. However, if i make a new AR model object and save it, the timestamps are from an hour ago. So, it looks like this is using UTC. Now, i can see the logic in this: since the timestamps might be used in the model logic, you want them to be based on an unvarying yardstick time, ie UTC. But, this is a weird bit of behaviour that i don't understand:
#change a record and save it
>> someobj.save
=> true
#object's updated_at is one hour ago
>> someobj.updated_at
=> Wed, 27 Oct 2010 15:34:22 UTC +00:00
>> Time.now
=> Wed Oct 27 16:34:31 +0100 2010
#however, Time.now - object's updated at is just a few seconds.
>> Time.now - someobj.updated_at
=> 15.305549
So, before doing the subtraction, updated_at is converted into the current time zone.
The reason i want to show the date in the current time zone is just for status reports etc in the views: if someone updates something i want them to see that it was updated '1 minute ago' not 'one hour ago'.
Can anyone unconfuse me? cheers, max
EDIT: My immediate problem, of showing the right time in the status, is solved by using the 'time_ago_in_words' helper, which adjusts for time zone. I'd still like someone to explain what's going on with the timestamps though :)
Timestamps are stored in UTC by default, and this is probably the best way to do it. If you move from one server environment to another, you don't want all of your times shifting around just because you switched time zones.
If you want to know what the timestamp is in your local time zone, you just have to ask for it that way:
someobj.updated_at.localtime
Note the offset listed at the end of the times -- the first offset is 0, the second is 1. When the time calculation occurs, the offset is included automatically, so that the subtraction gives you the correct result. someobj.updated_at and Time.now each displays its value in a different time zone, so they are really only 9 seconds apart, not 1 hour and 9 seconds.

Adding times in ruby on rails

I currently have a model called Job which has an attribute called CPU. This corresponds to the CPU time that a job was running. I would like to add all the time attributes of all the jobs for a specific date. This column is in the time format 00:00:00. Therefore I thought this would work:
def self.cpu_time
sum(:cpu)
end
Which returns the following "Sat Jan 01 00:00:00 UTC 2000".
For my test data, I used the following cpu times:
00:00:46
00:26:46
Any help would be appreciated
This solved my problem, although it doesnt seem to be the rails way:
def self.cput
#times = find(:all,
:select => 'cput')
#total_time =0
for time in #times do
#total_time += time.cput.to_i - 946684800
end
#total_time
end
You don't state what data type you're using for the cpu column. Personally I would store it in seconds and then convert to hours, minutes and seconds after summing it.

Resources