Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Troubled by Timezones in Rails

I have an application which uses the following config:

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.
  config.time_zone = 'Amsterdam'
  config.active_record.default_timezone = :utc
end

I'm offering the user a form that asks for a separate date (datepicker) and time field (drop down). On the before method I'm concatenating the date & time into a single datetime field. It seems though that the Time object is somehow in UTC, while the Date object doesn't care about that.

When storing this into the database, somehow the timezone gets corrected, but the time is 2 hours off. Is this due to the fact that the time is still in UTC? How can I compensate for that in a way also daylight savings is respected?

Code sample

The form:

= simple_form_for @report do |f|
  .row
    .col.s6
      = f.association :report_category
    .col.s6
      = f.association :account
  .row
    .col.s6.l4
      = f.input :day, as: :string, input_html: {class: 'datepicker current-date'}
    .col.s6.l4
      = f.input :time, as: :string, input_html: {class: 'timepicker current-date'}

The callback in the model

  def create_timestamp
    self.date = day.to_datetime + time.seconds_since_midnight.seconds
  end

After saving, there's a 2 hour difference between de time I've chosen in the form.

This is how it looks like after creating a new record where time is just the same time as the record was created, to see the difference:

 date: Sat, 20 May 2017 16:10:00 CEST +02:00,
 created_at: Sat, 20 May 2017 14:10:33 CEST +02:00,
 updated_at: Sat, 20 May 2017 14:10:33 CEST +02:00,
 deleted_at: nil,
 time: 2000-01-01 14:10:00 UTC,
 day: Sat, 20 May 2017,

As you van see, the time is stored in UTC, while it is actually 14:10 CET +0200 where I created the record. So that could be causing the discrepancy. Which you can see in date, where the time is +2 hours from the created_at.

Thanks

like image 425
bo-oz Avatar asked May 15 '17 19:05

bo-oz


2 Answers

Keep everything in UTC and back out the differences.

When the form submits the fields and you concatenate the DateTime object it will be interpreted as UTC... so you must do the conversions after the information is submitted.

Model.rb

    # Set the Proper DateTie
    def create_timestamp
        # Create the DateTime
        date = day.to_datetime + time.seconds_since_midnight.seconds
        # Set the timezone in which you want to interpret the time
        zone = 'Amsterdam'
        # Find the offset from UTC taking into account daylight savings
        offset = DateTime.now.in_time_zone(zone).utc_offset / 3600
        # Back out the difference to find the adjusted UTC time and save it as UTC with the correct time
        self.date = date - offset.hours
    end
like image 185
blnc Avatar answered Nov 05 '22 12:11

blnc


PART 1

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:

obj.updated_at.localtime

PART 2:

Ruby won’t consider Rails’ time zone configuration. This means that if your system is using the America/Sao_Paulo time zone and your application is using America/Los_Angeles, Ruby will still consider the former configuration.

Time.now.zone
#=> "BRST"

Time.zone = "America/Los_Angeles"
#=> "America/Los_Angeles"

Time.now.zone
#=> "BRST"

It is best to leave the application-wide time zone as UTC and instead allow each individual user to set their own time zone.

Add Time Zone attribute to the user

create_table :users do |t|
  t.string :time_zone, default: "UTC"
  ...
end

Get time zone from the form

<%= f.input :time_zone %>

We can then use around_action to set user's preferred time zone:

# app/controllers/application_controller.rb
around_action :set_time_zone, if: :current_user

private

def set_time_zone(&block)
  Time.use_zone(current_user.time_zone, &block)
end

We pass the current user’s time zone to use_zone method on the Time class (a method which was added by ActiveSupport). This method expects a block to be passed to it and sets the time zone for the duration of that block, so that when the request completes, the original time zone is set back.

DOs:

Get current time: Time.zone.now

Get Day: Time.zone.today

Time from timestamp: Time.zone.at(timestamp)

Parse time: Time.zone.parse(str)

DONT'S:

Time.now

DateTime.now

Date.today

Time.at(timestamp)

Time.parse(str)

Use Time.current instead of Time.now.

Use Date.current instead of Date.today.

Check out this post for more details on Rails Timezone:

working-with-time-zones-in-ruby-on-rails

like image 31
Raman Kumar Sharma Avatar answered Nov 05 '22 10:11

Raman Kumar Sharma