TheEricMiller
TheEricMiller

Reputation: 373

How to Convert TZInfo identifier to Rails TimeZone name/key

How do you convert js values received as TZInfo identifiers to Rails TimeZone name/key?

FROM: "America/New_York" returned from JavaScript TZinfo detection
TO: "Eastern Time (US & Canada)" convention used in Rails TimeZone


or another example:
"Pacific/Honolulu" => converted to => "Hawaii"

Both are available in ActiveSupport::TimeZone < Object mapping but rails uses the key [i.g. "Eastern Time (US & Canada)"] in drop-downs, validation & storing to Time.use_zone().


Based on what I understand of ActiveSupport::TimeZone.us_zones this seems to be important especially incases of DayLights savings time (which rails sounds to handle well) and matching just the offset would not accomplish. If it is not stored to DB with the rails TimeZone name then validation fails and does not match up properly in the user's profile settings page with the dropdown list of ActiveSupport::TimeZone.zones_map

Goal of this is that the user does not have to select their timezone on signup or are required to change it in their settings after signup. The browser detects it and passes it to hidden_field on signup. In the rare occasion they sign up in a place different than their home/work. they can manually override in their account settings later.

Seems to be a common gap when trying to ingest js timezone detection. This might even become a secondary question of how to pass the returned info from js to rails for conversion and then back to js to store back in the hidden_field of the form? Hopefully I framed the question properly & admittedly a bit green with rails so there may be a simple solution to this...

Thanks so much for all the help!
-E


ActiveSupport Time.zone Documentation
http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse

MAPPING = {"Eastern Time (US & Canada)" => "America/New_York"


Using js packaged gem 'temporal-rails' to detect users timezone:
https://github.com/jejacks0n/temporal

User Time_Zone implement as seen:
http://railscasts.com/episodes/106-time-zones-revised

*Using Devise & Devise-Inevitable


Sign-Up View Script

    <script>
    $(function() {
        var detected_zone = Temporal.detect();
        console.log(detected_zone);  // returns object
        detected_zone = detected_zone.timezone.name;
        console.log(detected_zone);  // returns "America/New_York"
        $('#user_time_zone').val(detected_zone);  // ! need to convert this to rails TimeZone name !
    });
    </script>

User Model

    validates_inclusion_of :time_zone, in: ActiveSupport::TimeZone.zones_map(&:name)

User Account Settings Form

    <%= f.label :time_zone, label: "Time Zone" %><br />
    <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones %>

Upvotes: 31

Views: 13334

Answers (4)

NGobin
NGobin

Reputation: 403

TZInfo is a database of IANA time zones, of which there are over 700. ActiveSupport::TimeZone includes a subset of ~150 of these. You can get the direct mapping from the ActiveSupport source, or as others have pointed out, from ActiveSupport::TimeZone::MAPPING in your code/console.

But this will not help you with the ~600 IANA time zones that are not mapped. Looking them up by offset will produce errors, as some zones do or do not observe daylight savings, some zones observe it on different dates, and some zones observe it according to the southern hemisphere summer (i.e. reversed from northern hemisphere DST).

I have put together a YAML mapping of IANA names to ActiveSupport::TimeZone names here. It is reasonably current and correct, but no guarantees! Of course, with over 600 other time zones, there are some that have no reasonable approximation in ActiveSupport (I found 15, noted in comments in the gist).

With the YAML file, you could build a simple mapper to convert them, something like:

class TimeZoneMapper
  def self.mappings
    @mappings ||= YAML.load_file(Rails.root.join("app/assets/timezones/time_zone_mappings.yml"))
  end

  def self.iana_to_rails(iana_name)
    # first check ActiveSupport Library
    ActiveSupport::TimeZone::MAPPING.select { |_k, v| v == iana_name }.keys.first ||
      mappings[iana_name] || # then mapped value from github gist
      nil # or nil if still no match
  end
end

Upvotes: 1

AlejandroVD
AlejandroVD

Reputation: 1812

Best option IMO is in a comment by Jo P to the accepted answer. You can do the lookup like this:

irb> ActiveSupport::TimeZone::MAPPING.key("America/Chicago")
=> "Central Time (US & Canada)"

Upvotes: 1

agranov
agranov

Reputation: 451

The magic I believe everyone is really after, and solves the problem that @ajbraus raised, is a one-liner that could be one of the strangely hardest things to find discussed anywhere:

timezone = ActiveSupport::TimeZone[TZInfo::Timezone.get('America/Vancouver').period_for_utc(Time.now.utc).utc_offset]
 => #<ActiveSupport::TimeZone:0x00007fc2baa6d900 @name="Pacific Time (US & Canada)", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: America/Los_Angeles>> 

Otherwise, if you try searching through ActiveSupport's entire DB of zones, you get nada:

ActiveSupport::TimeZone.all.find{ |tz| tz.tzinfo == ActiveSupport::TimeZone.find_tzinfo('America/Vancouver') }
 => nil

The complexity boils down to the following:

  • TZInfo gem is rigorously adhering to the IANA specification of timezones
  • Rails' ActiveSupport decided to try and simplify display/usage of zones but isn't an exhaustive DB of zones
  • ActiveSupport can look up an appropriate zone if you give it an offset
  • TZInfo doesn't give you an offset unless you create a TZInfo::TimezonePeriod in the zone of interest
  • Ruby 2.6 is apparently revisiting timezone support to clean this business up

Upvotes: 4

jejacks0n
jejacks0n

Reputation: 6112

Temporal includes the needed logic, but to answer your question:

Time.zone = ActiveSupport::TimeZone.new("America/New_York")

Edit, I guess my answer is incomplete. You want to get it from "America/New_York" to "Eastern Time (US & Canada)", correct? If that's the case this is the best solution I have -- though someone may be able to provide a better one.

ActiveSupport::TimeZone::MAPPING.select {|k, v| v == "America/New_York" }.keys.first

Upvotes: 33

Related Questions