Ruby/Rails Timezone conversion - ruby-on-rails

I'd implemented timezone drop down for my project and the values will be stored in database, take for instance i'm in asia/chennai, then the value stored will be GMT +05:30, so when someone from jerusalem checks the database, they'd see my timezone value, rather it should be theirs (GMT +02:00). How to convert the specified value using ruby on rails ?

The easiest way is probably to add a timezone to your user model so that your users can set their timezone. You can do this by using the timezone gem https://github.com/panthomakos/timezone
After you have done this, converting a timestamp to their local zone should be as simple as doing:
timestamp.in_time_zone(current_user.time_zone) (this is assuming you have a method current_user)

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.

Storing a timestamp with a custom timezone

I have to store an "event time" that could be in any timezone. By default rails stores these in the DB as UTC and strips off our custom timezone. When we read the attribute back it is converted to our application's timezone - we lose the original timezone information.
We're working around this by storing the timestamp (as UTC) and an offset (an integer number of seconds) which is fine, but I'd really love to be able to store this as just one column and have rails use that rather than converting to UTC on write and to the application's timezone on read.
Is having rails store the timestamp in non-UTC just for this one field just not possible?

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.

Rails : Given that my database is in UTC, and my Time.zone US Eastern, how do I save a time in US Pacific?

(I'm using Rails 3.2.3 on Ruby MRI 1.9.2)
Everything is stored centrally in UTC. This is good. Each User has a 'timezone' property. My ApplicationController has a before filter to adjust Time.zone to the User's stored timezone. This is also good.
Given the above, if my User completes a datetime select with a time, that time is expected to be in Time.zone and Rails will automatically adjust and save this as UTC. This too, is good.
Now: I wish to enable my Users to fill out a time in another time zone and then have it stored as UTC. However, Rails is expecting the completed date time select to have been filled out in the (previously set by ApplicationController) Time.zone. Therefore Rails adjusts to UTC incorrectly.
How do I achieve my goal of saving a time that has been input as being in a third time zone?
Illustration:
My usage scenario is a User in Florida adjusting a date for a Document belonging to a Hotel on the West Coast. They are looking to enter a time for the West Coast.
I'm using jQuery to style text boxes with a styled picker, so I have a method to output a string to the text box:
<%= f.text_field(:created_at, :value => adjusted_to_hotel_time(#document.created_at), :class => 'text datetime_picker') %>
def adjusted_to_hotel_time(time)
time.in_time_zone(#current_hotel.timezone).to_s(:admin_jquery) # formatted for the jQuery datetime_picker text fields.
end
This is working perfectly, but Rails adjusts to UTC incorrectly when the #document is saved. I don't know what I don't know - how can I 'tag' the data entered in that field as being in #current_hotel.timezone, so that Rails will offset to UTC correctly when it saves the parent object?
Cracked it!
Basically, the string that is submitted to params is representing a time in a Hotel's timezone. We have to use the built in 'use_zone' method to temporarily set the global Time.zone to that Hotel's.
We then pass the method a block where we produce the value that Rails is expecting, by using the timezone of the Hotel, rather than the User. That means that Rails' conversion to UTC results in the correct time in the db - as the time entered on the form has been converted to the Users timezone. The offsets have cancelled each other out, effectively.
#document.created_at = Time.use_zone(#current_hotel.timezone) {Time.zone.parse("#{params[:document][:created_at]}").in_time_zone(#current_hotel.timezone)}
We're basically changing the timezone of the Time object here without converting the actual time of that Time object when the timezone changes. (Good luck parsing that sentence!)
This looks to be working fine for me, but I've been looking at this for too long and I'd love to see a better way/more Rails-ey way of doing this!
The datetime column type only stores a date and time, but not the time-zone. You may need to create a secondary column to preserve the time zone used to interpret the UTC time saved there.
These two values could combine to re-create your initial input.

When is it appropriate to use Time#utc in Rails 2.1?

I am working on a Rails application that needs to handle dates and times in users' time zones. We have recently migrated it to Rails 2.1 and added time zone support, but there are numerous situations in which we use Time#utc and then compare against that time. Wouldn't that be the same as comparing against the original Time object?
When is it appropriate to use Time#utc in Rails 2.1? When is it inappropriate?
If you've set:
config.time_zone = 'UTC'
In your environment.rb (it's there by default), then times will automagically get converted into UTC when ActiveRecord stores them.
Then if you set Time.zone (in a before_filter on application.rb is the usual place) to the user's Time Zone, all the times will be automagically converted into the user's timezone from the utc storage.
Just be careful with Time.now.
Also see:
http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/
http://errtheblog.com/posts/49-a-zoned-defense - you can use the JS here to detect zones
Hope that helps.
If your application has users in multiple time zones, you should always store your times in UTC (any timezone would work, but you should stick with the most common convention).
Let the user input in local time, but convert to UTC to store (and compare, and manipulate). Then, convert back from UTC to display in each users' local time zone.

Resources