We've have a curious (timezone?) problem around the testing of Date/Time in a Rails application.
The date being tested is created_at on a Rails object.
On the front end (.html.slim page with some AngularJS)
span.date
| {{ comment.createdAt }}
Renders:
2017-01-03T00:00:00.000Z
but...
span.date
| {{ comment.createdAt | moment: 'MMMM Do, YYYY' }}
Renders:
January 2nd, 2017
We are expecting January 3rd, 2017
I've tried moment.utc: 'MMMM Do, YYYY' but yields nothing.
Thoughts?
EDIT Background to how date is set on Comment object in spec.
A date object is created with Date.parse
Timecop freezes time
Comment object is created
Comment.created_at is rendered on a page but it is adjusted for the local time (this is expected behavior)
Perhaps with Timecop frozen there's a better way to set current_date so that when momentJS thaws it out it's in the same timezone that machine where tests are being run is?!?
let!(:current_date) { Date.parse('2017-01-03') }
...
background do
Timecop.freeze(current_date)
end
...
context 'for story' do
background do
open_story_comments_modal
within '.story-comments-container' do
add_comment 'First comment message'
end
end
...
and in a shared helper we have
def add_comment(text)
fill_in 'comment[body]', with: text
find('input[name="comment[body]"]').send_keys(:enter)
expect(page).to have_css '.comments .comment-body', text: text
end
2017-01-03T00:00:00.000Z is an ISO 8601 date string. The Z at the end indicates that it's in UTC timezone. Depending on locale timezone that date may be on January 3rd or January 2nd.
Storing a date with time and timezone is often a good idea but not in some edge cases. If you want to store a day, you shouldn't store a time and must not store a timezone. A date string without timezone is considered to be in locale time:
If the time zone offset is absent, the date-time is interpreted as a local time.
ECMAScript 2015 (6th edition): 20.3.1.16 Date Time String Format
Please note that this has changed since ECMAScript 2015 (6th edition). Until before it was specified as the opposite:
The value of an absent time zone offset is "Z".
ECMAScript 5.1 Edition / June 2011: 15.9.1.15 Date Time String Format
As far as I know all browser followed that change some time ago.
Related
I've got date as string '2020-02-10 8,00' which I want to convert into Monday, 10th of February. I'm aware of this old topic however I cannot find (or use) any related information.
All I have is just parsed string to date - Date.parse '2020-02-10 8,00'
You are halfway there! Date.parse '2020-02-10 8,00' produces a ruby Date object, as you have noted. You now have to apply strftime. However strftime doesn't have any ordinalization so that piece has to be done manually.
date = Date.parse('2020-02-10 8,00')
date.strftime("%A, #{date.day.ordinalize} of %B") #=> Monday, 10th of February
the ordinalize method is provided by ActiveSupport.
If this format will be used multiple times in your app, you may wish to add an app-wide format:
# in config/initializers/time_formats.rb
Date::DATE_FORMATS(:ordinalized_day) = lambda{|date| date.strftime("%A, #{date.day.ordinalize} of %B")}
# anywhere in the app
Date.today.to_formatted_s(:ordinalized_day)
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
For a project I am working on I receive date and time in this format:
2015-08-16 15:00:00 UTC
yyyy-mm-dd hh-mm-ss UTC
How can I make the time display as "Saturday, August 16th 2015 at 3:30PM"? ("15:00" would be fine as well.)
And how would I make it so it checks if the date has already passed or not, so that it only displays dates that have not passed?
How would I make it so I can so that the time display as "Saturday, August 16th 2015 at 3:30PM (15:00 would be fine as well)?
Time.parse('2015-10-20 15:23 UTC').strftime('%A, %B %dth %Y at %l:%M%p')
#=> "Tuesday, October 20th 2015 at 3:23PM"
You might have to tweak it a bit to fix the suffixes (1st, 2nd, 3rd, etc)
And how would I make it so it checks if the date has already pasted or not?
You could do it like this (I'm sure there's a simpler way):
EDIT: Yes, there is a much simpler way -- check Matt's answer.
require 'time'
if Time.parse(my_date).to_i - Time.now.to_i > 0
# my_date is in the future.
end
To start, convert your string to a Time object via Time.parse(string) (APIDock).
After that you have all of the Time class to play with.
time.strftime
time.past?
I'm working with Date strings in Ruby and want to determine if a string was passed with a timezone or not (beyond simply parsing the text). If no timezone is supplied, I want to convert it to a certain timezone dependent on other factors.
Example:
DateTime.parse("2014-01-01T23:59:00")
=> #<DateTime: 2014-01-01T23:59:00+00:00 ((2456659j,86340s,0n),+0s,2299161j)>
DateTime.parse("2014-01-01T23:59:00Z")
=> #<DateTime: 2014-01-01T23:59:00+00:00 ((2456659j,86340s,0n),+0s,2299161j)>
DateTime.parse("2014-01-01T23:59:00PST")
=> #<DateTime: 2014-01-01T23:59:00-08:00 ((2456660j,28740s,0n),-28800s,2299161j)>
The problem is that parsing "2014-01-01T23:59:00" and "2014-01-01T23:59:00Z" (with Zulu) yields the same result, when one is specifying "Zulu" and the other isn't. I'm sure the default behavior is to assume UTC if no zone is supplied.
To check that the date string doesn't end with a time zone, you can check that the minutes and seconds are the end of the string, using this regex: \d{2}:\d{2}$
Here are some examples using debuggex to illustrate:
\d{2}:\d{2}$
Debuggex Demo
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.