How to map Rails timezone names to PostgreSQL? - ruby-on-rails

I need to use the query similar to
SELECT * FROM items WHERE to_char(created_at AT TIME ZONE 'RAILS_GIVEN_ZONE', 'DD/MM/YYYY') ILIKE '%5/02%'
where the RAILS_GIVEN_ZONE value should always use the time zone from the Rails 4 app (which could be changed by the user), not the PG's timezone option.
But the problem is that the timezones from Rails and PG do not correspond 1-to-1 exactly.
Using the offset (as in +10:00 from Time.zone.now.formatted_offset) isn't good enough since in this case PG will not be able to deal with the daylight saving time correctly.
So the question is what is the best way to automatically map Rails current time zone (Time.zone) to the PostgreSQL's named time zone?
NOTE: the create_at column is timestamptz (stored as UTC, displayed in whatever zone is necessary)

There seems to be a MAPPING constant defined in ActiveSupport::TimeZone, which contains values that, unless I am mistaking, should all be supported by Postgres:
http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html
Per the docs:
Keys are Rails TimeZone names, values are TZInfo identifiers.
If you don't want to use the MAPPING constant directly, there's a find_tzinfo method in there, which seems to return aTZInfo::TimezoneProxy:
http://rubydoc.info/gems/tzinfo/TZInfo/TimezoneProxy
The latter sports an identifier method that should contain the needed string.

Related

Rails messing up with my Postgres TIME columns

I am using postgres TIME type to represent a branch office opening hour.
I am aware Rails does not handle the TIME column alone, initializing the values with a default date: 01 Jan 2000.
I have no problem with that except for the fact that my user timezone is -03, so when the user picks a time, let's say 22:00:00, Rails actually changes it to 01:00:00 and stores that value instead. Then, when I load it from the db it might even load a different date (the day before or after 01 Jan, which is a headache when I am comparing times).
How can I tell Rails to not change the original input to the server timezone? Or is there any other better workaround?
Rails always have TZ associated with times, it also stores everything in utc in DB. Out of the box, it does correct encoding/decoding. My assumption is - you have done your own decoding when you retrieve values from the DB. In this case, what you can do is to write helper like this:
def strip_tz(time)
Time.new(time.year, time.month, time.day, time.hour, time.min, time.sec, "+00:00")
end
And use it when you pass time value to the model creation.

Postgresql date difference in table and on web

I have weird situation with my PostgreSQL db date value.
On my web site I have calendar for selecting a date and when I select some future date like "2018-09-23" in PostgreSQL table column it is saved as "2018-09-22 22:00:00"?
Obviously I am missing something. On web site all the time it shows okay time "2018-09-23" but at the table it is minus one day as you see above. Why?
Rails stores DateTime fields in UTC, but without marking their time zone as UTC. This corresponds to the timestamp without time zone type in postgres. So if your time zone is +2, it'll store the time as UTC (+0).
In Rails, Time.zone will return the current local timezone (you can add logic to change this by user, for example). When persisting a datetime, Rails will automatically convert the current Time.zone to UTC. However, it doesn't use the Postgres type that actually includes the time zone data, so it relies on convention to convert back and forth to the user's time zone.
If you really only care about the date, use the date type in your migration instead of Timestamp or DateTime.
Times and dates have a lot of subtle quirks and the "right" behavior depends on your use case. In some applications, you need to deal with "local" time when considering date transitions, and sometimes you need to finesse your application or database logic to think in terms of local time and sometimes you care about UTC time.

Rails, Postgres and Timezone

I have table which have a datetime field named date. When doing a POST in order to insert a new row, the date sent from the client (browser) looks like 2015-11-20T14:30:00+10:00 which is actually a correct date and timezone.
However, inside Postgres this date has been inserted as 2015-11-20 04:30:00.000000, which as you can see, is not at all the same as above. I know the problem is related to the timezone. But I cannot seems to figure out a fix.
For information, I have configured my app timezone :
class Application < Rails::Application
config.time_zone = 'Brisbane'
end
Ideas?
2015-11-20T14:30:00+10:00 means that the local time of 14:30 is 10 hours ahead of UTC. Your database field reflects the correct UTC value of 04:30. This is often the desired behavior, especially if the value represent a timestamp - the date and time something occured (past tense).
In PostgreSQL, there are two different types of timestamp fields (reference)
The TIMESTAMP WITH TIME ZONE field accepts an input that contains a time zone offset. It then converts the value to UTC for storage. On retrieval, it uses the session's timezone setting.
The TIMESTAMP, or TIMESTAMP WITHOUT TIME ZONE simply stores the date and time given, ignoring any offset, and not converting to UTC.
Most of the time, you should indeed use TIMESTAMP WITH TIME ZONE. You should only use TIMESTAMP WITHOUT TIME ZONE if you need to retain the local date and time value, such as in scheduling of future events and calculation of business hours. And for those scenarios, it often makes more sense to split date and time into separate DATE and TIME fields.
One last thing - if you can avoid it, avoid using Rails time zones and use standard tzdb zones. "Australia/Brisbane" is the full tzdb identifier equivalent to the Rails "Brisbane" time zone. Refer to the section on Rails time zones at the bottom of the timezone tag wiki.
I found this gem to be incredibly useful and easy for correctly setting the time https://github.com/kbaum/browser-timezone-rails

Store timestamps with timezone in rails 3.2

I'm trying to store all timestamps in a rails application with their included timezone. I'm fine with ActiveRecord converting them to utc, but I have multiple applications hitting the same database, some of which are implemented with a timezone requirement. So what I want to do is get activerecord to convert my timestamps as usual, then write them to the database with the string 'America/Los_Angeles', or whatever appropriate timezone, appended to the timestamp. I am currently running rails 3.2.13 on jruby 1.7.8, which implements the ruby 1.9.3 api. My database is postgres 9.2.4, connected with the activerecord-jdbcpostgresql-adapter gem. The column type is timestamp with time zone.
I have already changed the natural activerecord mappings with the activerecord-native_db_types_override gem, by adding the following lines to my environment.rb:
NativeDbTypesOverride.configure({
postgres: {
datetime: { name: "timestamp with time zone" },
timestamp: { name: "timestamp with time zone" }
}
})
My application.rb currently contains
config.active_record.default_timezone = :utc
config.time_zone = "Pacific Time (US & Canada)"
I suspect I can rewrite ActiveSupport::TimeWithZone.to_s and change it's :db format to output the proper string, but I haven't been able to make that work just yet. Any help is much appreciated.
After banging my head against this same problem, I learned the sad truth of the matter:
Postgres does not support storing time zones in any of its date / time types.
So there is simply no way for you to store both a single moment in time and its time zone in one column. Before I propose an alternative solution, let me just back that up with the Postgres docs:
All timezone-aware dates and times are stored internally in UTC. They are converted to local time in the zone specified by the TimeZone configuration parameter before being displayed to the client.
So there is no good way for you to simply "append" the timezone to the timestamp. But that's not so terrible, I promise! It just means you need another column.
My (rather simple) proposed solution:
Store the timezone in a string column (gross, I know).
Instead of overwriting to_s, just write a getter.
Assuming you need this on the explodes_at column:
def local_explodes_at
explodes_at.in_time_zone(self.time_zone)
end
If you want to automatically store the time zone, overwrite your setter:
def explodes_at=(t)
self.explodes_at = t
self.time_zone = t.zone #Assumes that the time stamp has the correct offset already
end
In order to ensure that t.zone returns the right time zone, Time.zone needs to be set to the correct zone. You can easily vary Time.zone for each application, user, or object using an around filter (Railscast). There are lots of ways to do this, I just like Ryan Bates' approach, so implement it in a way that makes sense for your application.
And if you want to get fancy, and you need this getter on multiple columns, you could loop through all of your columns and define a method for each datetime:
YourModel.columns.each do |c|
if c.type == :datetime
define_method "local_#{c.name}" do
self.send(c.name).in_time_zone(self.time_zone)
end
end
end
YourModel.first.local_created_at #=> Works.
YourModel.first.local_updated_at #=> This, too.
YourModel.first.local_explodes_at #=> Ooo la la
This does not include a setter method because you really would not want every single datetime column to be able to write to self.time_zone. You'll have to decide where this gets used. And if you want to get really fancy, you could implement this across all of your models by defining it within a module and importing it into each model.
module AwesomeDateTimeReader
self.columns.each do |c|
if c.type == :datetime
define_method "local_#{c.name}" do
self.send(c.name).in_time_zone(self.time_zone)
end
end
end
end
class YourModel < ActiveRecord::Base
include AwesomeDateTimeReader
...
end
Here's a related helpful answer: Ignoring timezones altogether in Rails and PostgreSQL
Hope this helps!
May i suggest saving them in iso8601
That will allow you to:
Have the option of storing them as UTC as well
as with a timezone offset
Being international standards compliant
Use the same storage format in both cases with offset and
without.
So one of the db columns can be with a offset one in just UTC form (usual).
From the Ruby side it is as simple as
Time.now.iso8601
Time.now.utc.iso8601
ActiveRecord should work seamlessly with the conversion.
Also, most API's use this format (google) hence best for cross app compatibility.
to_char() for postgresql should give you the right format in case there is any hiccup with the default setup.
One approach, as you suggest, would be to override ActiveSupport::TimeWithZone.to_s
You might try something like this:
def to_s(format = :default)
if format == :db
time_with_timezone_format
elsif formatter = ::Time::DATE_FORMATS[format]
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
else
time_with_timezone_format
end
end
private
def time_with_timezone_format
"#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
end
I haven't tested this but looking at Postgres' docs on Time Stamps, this looks valid and would give a time like: "2013-12-26 10:41:50 +0000"
The problem with this, as far as I can tell, is that you would still have trouble returning the right timezone:
For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT). An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone.
This is exactly what the original ActiveSupport::TimeWithZone.to_s is already doing.
So perhaps the best way to get the correct Time Zone is to set an explicit Time Zone value as a new column in the database.
This would mean that you would be able to keep the native date functionality of both Postgres and Rails while also being able to display the time in the correct timezone where necessary.
You could use this new column to then display the right zone using Ruby's Time::getlocal or Rails' ActiveSupport::TimeWithZone.in_time_zone.

handling rails + postgres and timezones

I have an application which uses many different timezones... it sets them in a controller and they change depending on the user. All the times are stored in UTC without a timestamp etc.
My understanding is this is the normal way for Rails to handle timezones. This works fine 99% of the time until i need to do something directly with Postgres then the Timezone becomes a problem.
For example this query is completely wrong except for GMT, for example in Central Time Zone, depending on the hour set, it gets the wrong day:
Events.where("EXTRACT(dow FROM start_at)", Date.today.wday)
Where I'm trying to find all the dates that fall on a certain day.
I'm trying to do something like this. I don't need to search between timezones (they won't be mixed), but I do need to specify the timezone if it's not UTC to get correct results.
User.events.where("EXTRACT(dow FROM start_at AT TIME ZONE ?) = ?", 'UTC', Date.today.wday)
But I'm not sure how to use Time.zone to give me something that will work with TIME ZONE in Postgres.
Time.zone.tzinfo sometimes works... Postgres will work with 'Europe/Warsaw' but Rails returns 'Europe - Warsaw'
In general I'm not having much luck with timezones, any pointers would be appreciated.
Maybe someone else has a better overall solution, but what you need for the particular query is
Time.zone.tzinfo.identifier
Or, in your example:
User.events.where("EXTRACT(dow FROM start_at AT TIME ZONE ?) = ?", Time.zone.tzinfo.identifier, Date.today.wday)
Try using the Ruby TZInfo gem directly, instead of using Rails ActiveSupport::TimeZone.
Alternatively, use the MAPPING constant, as shown in the ActiveSupport::TimeZone documentation, which will take you from a Rails time zone key back to the standard IANA time zone identifier used by Postgres and others.
As Matt Johnson suggested use TZInfo gem directly. This way you can get the correctly formatted time zone identifiers you need to query with PostgreSQL.
For example if you use:
TZInfo::Timezone.all_country_zone_identifiers
This will return an array of correct IANA/Olson time zone identifiers. In other words you will get the correct 'Europe/Warsaw' NOT 'Europe - Warsaw'.

Resources