Skip to content

Instantly share code, notes, and snippets.

@mnishiguchi
Last active February 5, 2024 17:04
Show Gist options
  • Save mnishiguchi/cafd9c24f417037fffc348f60beb2739 to your computer and use it in GitHub Desktop.
Save mnishiguchi/cafd9c24f417037fffc348f60beb2739 to your computer and use it in GitHub Desktop.
Rails, JS - Time zone

Time zone considerations for a Rails app

Time zones in Rails (3 types)

1. system time

  • Our machine's time zone.
# Check our system time zone.
Time.now.getlocal.zone

sysdate in SQL

2. application time

  • Default: UTC
# Check our Rails time zone.
Time.zone.name
# List all the timezone names that Rails recognizes.
$ rake time:zones:all
# List all the timezone names that Rails recognizes.
ActiveSupport::TimeZone.all.map(&:name).sort
# List all the US timezone names that Rails recognizes.
ActiveSupport::TimeZone.us_zones.map(&:name).sort
class Application < Rails::Application
  # We could override the Rails default time zone but in general it is not a good idea.
  config.time_zone = 'Eastern Time (US & Canada)' # bad
end
# We can temporarily apply a given time zone to the Rails time zone.
Time.use_zone "UTC" do

  # Do stuff with the specified time zone for the duration of this block.

end

3. database time

Sets the time zone for displaying and interpreting time stamps. The built-in default is GMT, but that is typically overridden in postgresql.conf; initdb will install a setting there corresponding to its system environment. See Section 8.5.3 for more information.


Ruby and Rails methods for time

  • In order to ensure consistency:
    • always work with UTC
    • always use the methods that use Rails application's time zone, and let them translate correct times

Do not use

* Time.now
* Date.today
* Date.today.to_time
* Time.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")

Do use

* Time.current
* 2.hours.ago
* Time.zone.today
* Date.current
* 1.day.from_now
* Time.zone.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone

temporarily set time zone

      time_zone = "Eastern Time (US & Canada)"
      Time.use_zone(time_zone) do 
         create(:property_container, legacy_id: "1355")
         instance = described_class.new(data_array)
         expect(instance.created_at.iso8601).to eq("2018-09-25T08:18:24-04:00")
      end

issues


Detect timezone and set timezone on every request

TL;DR

    1. By default, we always use UTC.
    1. Use the jstimezonedetect library to detect the user's time zone on the front end.
    1. Store the time zone name to a temporary cookie store.
    1. Apply that time zone to Rails time zone for the duration of each request.

Install jstz

HTML

    = javascript_include_tag 'https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.6/jstz.min.js'
    javascript:
       setTimezoneCookie()

JS

function setTimezoneCookie() {

  setCookie('tz', getTimezoneName())

  function getTimezoneName() {
    // https://medium.com/@jonathanabrams/be-aware-of-browsers-internationalization-api-db94bb32f9a8#.hjgg1453j

    // Temporarily remove Intl object before calling jstz in order to ensure that jstz uses older timezone format.
    // If the Intl object exists, jstz will detect a newer timezone format.
    // We want to use older timezone format so that Rails can understand.
    var oldIntl = window.Intl
    window.Intl = undefined

    // Get the usee's timezone name.
    var tzName  = jstz.determine().name()

    // Restore the Intl object.
    window.Intl = oldIntl

    return tzName
  }

  function setCookie(key, value) {
    // http://nithinbekal.com/posts/rails-user-timezones
    var expires     = new Date()
    var currentTime = expires.getTime()
    var duration    = 24 * 60 * 60 * 1000
    expires.setTime(currentTime + duration)

    document.cookie = key + '=' + value + ';expires=' + expires.toUTCString()
  }
}

Controller

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  around_action :set_time_zone

  private
  
    def set_notifications
      @notifications = Notification.where(recipient: current_user).recent
    end

    # Sets the time zone for the duration of a request.
    # When the request completes, the original time zone is set back.
    # https://robots.thoughtbot.com/its-about-time-zones
    def set_time_zone(&block)
      if cookies[:tz]
        puts "*" * 60
        puts "Time zone: #{cookies[:tz]}"
        puts "*" * 60
        Time.use_zone(cookies[:tz], &block)
      else
        # The around method must yield to execute the action.
        # http://guides.rubyonrails.org/action_controller_overview.html#after-filters-and-around-filters
        yield
      end
    end
end

References

@mnishiguchi
Copy link
Author

The Problem with Time & Timezones - Computerphile
https://www.youtube.com/watch?v=-5wpm-gesOY

@mnishiguchi
Copy link
Author

Using cron format for date

it's obtuse as hell, but also ubiquitous
https://github.com/floraison/fugit
https://crontab.guru

@mnishiguchi
Copy link
Author

memo

# today at 4pm
Date.current.beginning_of_day.change(hour: 16)

@mnishiguchi
Copy link
Author

      started_at = Time.iso8601("2023-01-11T11:11:11-05:00")
      completed_at = Time.iso8601("2023-02-22T22:22:22-05:00")

@mnishiguchi
Copy link
Author

remove milliseconds

irb(main):001:0> Time.now
=> 2023-06-28 11:10:05.559006429 -0400
irb(main):002:0> Time.now.change(usec: 0)
=> 2023-06-28 11:10:21 -0400

@mnishiguchi
Copy link
Author

query by time range

jobs = Something::Job.where(
  Something::Job.arel_table[:started_at].
    between(
      Time.zone.parse("2023-09-09T00:00:00-04:00")..Time.zone.parse("2023-09-10T23:59:59-04:00")
    )
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment