twitter/twitter-cldr-rb

View on GitHub
lib/twitter_cldr/timezones/generic_location.rb

Summary

Maintainability
C
1 day
Test Coverage
# encoding: UTF-8

# Copyright 2012 Twitter, Inc
# http://www.apache.org/licenses/LICENSE-2.0

module TwitterCldr
  module Timezones
    class GenericLocation < Location
      DEFAULT_CITY_EXCLUSION_PATTERN = /Etc\/.*|SystemV\/.*|.*\/Riyadh8[7-9]/
      DST_CHECK_RANGE = 184 * 24 * 60 * 60
      UNKNOWN_DEFAULT = 'Unknown'.freeze
      FORMATS = [
        :generic_location,
        :generic_short,
        :generic_long,
        :specific_short,
        :specific_long,
        :exemplar_location
      ].freeze

      Territories = TwitterCldr::Shared::Territories
      Utils = TwitterCldr::Utils

      def display_name_for(date, fmt = :generic_location, dst = TZInfo::Timezone.default_dst, &block)
        case fmt
          when :generic_location
            generic_location_display_name
          when :generic_short
            generic_short_display_name(date, dst, &block) || generic_location_display_name
          when :generic_long
            generic_long_display_name(date, dst, &block) || generic_location_display_name
          when :specific_short
            specific_short_display_name(date, dst, &block)
          when :specific_long
            specific_long_display_name(date, dst, &block)
          when :exemplar_location
            exemplar_city
          else
            raise ArgumentError, "'#{fmt}' is not a valid generic timezone format, "\
              "must be one of #{FORMATS.join(', ')}"
        end
      end

      private

      def generic_location_display_name
        if region_code = ZoneMeta.canonical_country_for(tz.identifier)
          if ZoneMeta.is_primary_region?(region_code, tz_id)
            region_name = Territories.from_territory_code_for_locale(region_code, tz.locale)
            return region_formats[:generic].sub('{0}', region_name || region_code)
          else
            # From ICU source, TimeZoneGenericNames.java, getGenericLocationName():
            #
            # exemplar location should return non-empty String
            # if the time zone is associated with a location
            return region_formats[:generic].sub('{0}', exemplar_city || region_code)
          end
        end
      end

      def generic_short_display_name(date, dst = TZInfo::Timezone.default_dst, &block)
        format_display_name(date, :generic, :short, dst, &block)
      end

      def generic_long_display_name(date, dst = TZInfo::Timezone.default_dst, &block)
        format_display_name(date, :generic, :long, dst, &block)
      end

      def specific_short_display_name(date, dst = TZInfo::Timezone.default_dst, &block)
        format_display_name(date, :specific, :short, dst, &block)
      end

      def specific_long_display_name(date, dst = TZInfo::Timezone.default_dst, &block)
        format_display_name(date, :specific, :long, dst, &block)
      end

      # From ICU source, TimeZoneGenericNames.java, formatGenericNonLocationName():
      #
      # 1. If a generic non-location string is available for the zone, return it.
      # 2. If a generic non-location string is associated with a meta zone and
      #    the zone never use daylight time around the given date, use the standard
      #    string (if available).
      # 3. If a generic non-location string is associated with a meta zone and
      #    the offset at the given time is different from the preferred zone for the
      #    current locale, then return the generic partial location string (if available)
      # 4. If a generic non-location string is not available, use generic location
      #    string.
      #
      def format_display_name(date, type, fmt, dst = TZInfo::Timezone.default_dst, &block)
        date_int = date.strftime('%s').to_i
        period = tz.period_for_local(date, dst, &block)

        flavor = if type == :generic
          :generic
        elsif type == :specific
          period.std_offset > 0 ? :daylight : :standard
        end

        if explicit = (timezone_data[fmt] || {})[flavor]
          return explicit
        end

        if tz_metazone = ZoneMeta.tz_metazone_for(tz_id, date)
          if use_standard?(date_int, period)
            std_name = tz_name_for(fmt, :standard) || mz_name_for(fmt, :standard, tz_metazone.mz_id)
            mz_generic_name = mz_name_for(fmt, :generic, tz_metazone.mz_id)

            # From ICU source, TimeZoneGenericNames.java, formatGenericNonLocationName():
            #
            # In CLDR, the same display name is used for both generic and standard
            # for some meta zones in some locales. This looks like data bugs. For
            # now, we check if the standard name is different from its generic name.
            return std_name if std_name && std_name != mz_generic_name
          end

          mz_name = mz_name_for(fmt, flavor, tz_metazone.mz_id)

          # don't go through all the golden zone logic if we're not computing the
          # generic format
          return mz_name if type == :specific

          golden_zone_id = tz_metazone.metazone.reference_tz_id

          if golden_zone_id != tz_id
            golden_zone = TZInfo::Timezone.get(golden_zone_id)
            golden_period = golden_zone.period_for_local(date)

            if period.utc_offset != golden_period.utc_offset || period.std_offset != golden_period.std_offset
              return nil unless mz_name
              return partial_location_name_for(tz_metazone.metazone, mz_name)
            else
              return mz_name
            end
          else
            return mz_name
          end
        end
      end

      def partial_location_name_for(metazone, mz_name)
        region_code = ZoneMeta.canonical_country_for(tz_id)

        location = if region_code
          if region_code == metazone.reference_region_code
            Territories.from_territory_code_for_locale(region_code)
          else
            exemplar_city
          end
        else
          exemplar_city ? exemplar_city : tz_id
        end

        fallback_formats[:generic]
          .sub('{0}', location)
          .sub('{1}', mz_name || '')
      end

      def target_region_code
        @target_region_code ||= tz.orig_locale.region || tz.max_locale.region
      end

      def exemplar_city
        @exemplar_city ||=
          timezone_data[:city] ||
          default_exemplar_city ||
          unknown_city ||
          UNKNOWN_DEFAULT
      end

      def tz_name_for(fmt, flavor)
        Utils.traverse_hash(timezone_data[:timezones], [tz_id.to_sym, fmt, flavor])
      end

      def mz_name_for(fmt, flavor, mz_id)
        Utils.traverse_hash(metazone_data, [mz_id.to_sym, fmt, flavor])
      end

      def use_standard?(date_int, transition_offset)
        prev_trans = tz.transitions_up_to(Time.at(date_int - DST_CHECK_RANGE)).last
        next_trans = tz.transitions_up_to(Time.at(date_int + DST_CHECK_RANGE)).last

        return false if transition_offset.std_offset != 0
        return false if prev_trans && prev_trans.offset.std_offset != 0
        return false if next_trans && next_trans.offset.std_offset != 0

        true
      end

      def default_exemplar_city
        @default_exemplar_city ||= begin
          return nil if tz_id =~ DEFAULT_CITY_EXCLUSION_PATTERN

          sep = tz_id.rindex('/')

          if sep && sep + 1 < tz_id.length
            return tz_id[(sep + 1)..-1].gsub('_', ' ')
          end

          nil
        end
      end

      def unknown_city
        @unknown_city ||= resource[:timezones][:'Etc/Unknown'][:city]
      end

      def timezone_data
        @timezone_data ||= (resource[:timezones][tz_id.to_sym] || {})
      end

      def metazone_data
        @metazone_data ||= resource[:metazones]
      end

      def region_formats
        @region_format ||= resource[:formats][:region_formats]
      end

      def fallback_formats
        @fallback_formats ||= resource[:formats][:fallback_formats]
      end
    end
  end
end