How do I get my own TimeZone in ruby? - ruby-on-rails

looking for a way to get my own time zone that I can pass to config.time_zone in application.rb order to set it in Rails. Background: I'm allowing the user to specify their timezone in a yaml config, but if it's not set, I want to explicitly use the box's timezone. I have to have a value set because I'm using it as reference to convert timezones for display -- each user logging into the Rails app can set their personal timezone and I will do the conversion for them.
However, there seems to be no good API for doing this in Ruby or Rails. Time.now.zone returns the 3-letter code (EDT or CDT, for example), but you can't pass this because it's not specific enough -- the TZInfo class will only accept the "long" descriptions.
Here's what I'm doing now, which seems pretty hacky:
time_zone = CONFIG[:time_zone] # set your local time zone here. use rake time:zones:local to choose a value, or use UTC.
unless time_zone
# try to detect one
if File.exists?('/etc/localtime')
path = File.readlink('/etc/localtime')
items = path.split("zoneinfo/")
if items.length == 2
time_zone = items[1]
end
end
unless time_zone
puts "*** Warning: no time zone is set in config/config.yaml and could not detect system time. Using UTC as the default time; behavior may be unexpected."
time_zone = "UTC"
end
end
config.time_zone = time_zone
Any better ideas guys?

Any reason why this wouldn't work?:
Time.now.zone
Edit:
Before getting the zone, you can do Time.now.gmt_offset to get your GMT offset in seconds. After that, you can do Time.now.zone to get your zone code.

You can try the following:
off_set = Time.now.gmt_offset
p ActiveSupport::TimeZone[off_set].name # "Atlantic Time (Canada)"
or
p ActiveSupport::TimeZone[off_set].tzinfo.name # "America/Halifax"

Since the 3-letter codes are not precise enough, have the user specify the full name from the IANA time zone database.
Then you can use Time.zone = 'America/Halifax' (for example).
Time.zone.name returns back the IANA name, and Time.zone.now returns the current time in the specified timezone.
To find the timezone in this format programically, you may be able to use /etc/timezone if it's available. Other systems have other methods (if there is even one available).

If you want the time zone offset in hours, as some databases do (like Postgres), you can use this method:
2.3.1 :063 > Time.zone.formatted_offset
"-07:00"
or this approach if you need the offset in seconds:
2.3.1 :059 > Time.zone.utc_offset
-25200

Related

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

Converting a date string into a time zone specific format

Rails 5.2.3
I have a date:
"Apr-03-2013 17:47:00"
I have a date zone:
"America/Los_Angeles"
I am trying to turn it into a string:
"Apr-03-2013 17:47:00 Pacific Daylight Time (GMT-07)"
The best I can come up with is:
time_obj = ActiveSupport::TimeZone["America/Los_Angeles"].parse("2013-04-03 17:47:00")
Time.at(time_obj).strftime("%b-%d-%Y %H:%M:%S %Z")
Which gives me:
"Apr-03-2013 17:47:00 PDT"
Any ideas?
I believe you need custom logic and/or you own database of timezones to get it exactly like that.
Using %Z with strftime is going to give you what ever your OS likes and there are a few disclaimers in the ruby docs.
One idea that you might get some mileage out of: If you are starting with a time zone identifier like "America/Los_Angeles" then you can use ActiveSupport::TimeZone::MAPPING to get a friendlier name, or at least a Rails time zone name.
eg:
ActiveSupport::TimeZone::MAPPING.key("America/Los_Angeles")
=> "Pacific Time (US & Canada)"
But that won't work for every identifier:
ActiveSupport::TimeZone::MAPPING.key("America/Detroit")
=> nil
You can see which ones will map like this:
TZInfo::Country.get('US').zone_identifiers.map {|ident| [ident, ActiveSupport::TimeZone::MAPPING.key(ident)] }
So in that case you need to fall back to the identifier you have, or perhaps this approach might work.
Then you'd need to deal with the daylight savings part, here you can use dst?
ActiveSupport::TimeZone["America/Los_Angeles"].parse("2013-04-03 17:47:00").dst?
Then you'd need to splice all that together! ... and add the offset as well.

Timezone independent due date comparison in rails scope

Consider the following case. I have a Voucher model with a datetime activation_due_date field and user model that has up-to-date information about his location (timezone, UTC offset).
I want to check if he requests voucher activation before due date in any of available time zones. For instance, If a due date is set to 28.08.2018 23:59 UTC
I want my scope before_activation_due to check if he requests something before 28.08.2018 - 23:59 in his current time zone so my due date is not something fixed - it depends on users location - In one place it can be after due date and in the other before.
I have tried the following approach.
models/voucher.rb
scope :before_activation_due, lambda { |user|
where('activation_due_date > ? ', Time.current.to_utc + user.utc_offset)
}
My questions are:

Is this a right approach? If not, what is the proper way for dealing with such cases?
How to test such a scope? The current timestamp is probably taken from a database server when comparing datetimes during query execution so I am not sure how to mock it in my specs.
Thanks in advance.
You can store just the timezone of the user, not the offset, then do:
where('activation_due_date > ? ', Time.now.utc.in_time_zone(user.timezone))
where timezone is any valid timezone shown in
rake time:zones
That'd be the more rails-y way to do things at least. But I don't think storing an offset then manually adding it to the time is a bad approach.
To test this, you can manually insert any date you want in to your database. Then you can use a gem like https://github.com/travisjeffery/timecop to travel in time to that point, and test your scope:
Voucher.create(activation_due_date: '2018-01-02 00:00:00')
format = '%Y-%m-%d %H:%M:%S %Z'
time = DateTime.strptime("2018-01-02 00:00:00 Central Time (US & Canada)",format)
Timecop.travel(time)
Voucher.before_activation_due.all ...
One approach is to convert the activation_due_date into the timezone of the user. As you say "my due date is not something fixed - it depends on users location".
To do this as a scope the easiest thing would be to use your databases timezone functions. This depends on which database you are using, but in PostgreSQL it will be something like:-
where('activation_due_date AT TIME ZONE ? > NOW() ', user.timezone)
An even simpler way would be to do a string comparison
where('to_char(activation_due_date, 'YYYYMMDDHH24MISS') > ?', Time.current.in_time_zone(user.timezone).strftime('%Y%m%d%H%M%S');
In this case we are saying what is the time on the wall for the user, and is it less than time in the database (which is "stored" in UTC).

ruby timezone conversion issues

I have a scenario in which i get a timestamp and i need to search for all bookings for that date in that timestamp. The timestamp is in users respective timezone and all the records in the database are stored in UTC. so naturally i need to convert that timestamp back to UTC and then search.
Here's something that i'm doing:
Booking.where("date_time >= '#{DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day}' and date_time <= '#{DateTime.parse(timestamp).in_time_zone('UTC').end_of_day}'")
which basically means to fetch all bookings from the beginning of day till the end
However, when i use the following query it gives me a different result:
Booking.where("date_time >= '#{DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')}' and date_time <= '#{DateTime.parse(timestamp).end_of_day.in_time_zone('UTC')}'")
I'm wondering which one is actually the correct statement to use in my use case and i would appreciate some input here.
I wouldn't use either one.
This one:
DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day
gives you the beginning of the UTC day, not the beginning of the local-time-zone-day offset to UTC. In short, it is incorrect and won't give you what you're looking for.
This one:
DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')
is correct as it changes the time to the beginning of the day in the local time zone and then converts the timestamp to UTC.
If you let ActiveRecord deal with the quoting using a placeholder, then it will apply the UTC adjustment itself.
I'd also use < t.tomorrow.beginning_of_day rather than <= t.end_of_day to avoid timestamp truncation and precision issues; the end of the day is considered to be at 23:59:59.999... and that could leave a little tiny window for errors to creep in. I'm being pretty pedantic here, you might not care about this.
I'd probably do it more like this:
t = DateTime.parse(timestamp)
Booking.where('date_time >= :start and date_time < :end',
:start => t.beginning_of_day,
:end => t.tomorrow.beginning_of_day
)

Rails and dates, are they stored in UTC by default?

What is the best way for me to handle dates and timezones in rails?
Scenerio: I have customers who purchase products on a website from all over the world, and when they log in they will be able to choose which timezone they are from.
So I believe I should be storing everything in the database at UTC, and then on the front-end I should be converting the dates to the users set timezone preference.
Are their any gotchas with Ruby and Rails and datetimes etc?
I'm new to rails, so I am looking for guidance on how to handle this properly.
Fortunately Rails will pretty much handle things for you. As others pointed out, dates are stored by AR in UTC format. If you have a time_zone field for your users table you can do something like this:
# application.rb
config.time_zone = "Mountain Time (US & Canada)" # Default time zone
-
# application_controller.rb
before_filter :set_time_zone, :if => :logged_in?
protected
def set_time_zone
Time.zone = current_user.time_zone if current_user.time_zone
end
All the datetimes should be shown in the proper time zone in your views.
I have had one production app that didn't like using the default time zone, but I can't remember which version of Rails/Ruby it was running.
Ok so take a look at your config/application.rb file.
You should find commented lines:
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
So default is UTC but you can set whatever ou need there.
Yes, they are. In app, whenever you display date or time for user, everything you need is just adding timezone offset (for example: date + 1.hour for GMT+1). Remember that you need to take care of daylight saving, too. For efficency, consider adding 2 columns in your user table: timezone and time_offset. Then you would on each case do something like
= #order.created_at + session[:user].time_offset
Instead of always checking offset for each timezone set in profile.
I found
rake time:zones:all
to be really useful. It shows a list of offsets and then zone name strings under that. eg:
* UTC +12:00 *
Auckland
Fiji
Kamchatka
Magadan
Marshall Is.
Solomon Is.
Wellington
I needed below in application.rb (not intuitive given default time zone string of "Mountain Time (US & Canada)"):
config.time_zone = 'Wellington'

Resources