# 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) case fmt when :generic_location generic_location_display_name when :generic_short generic_short_display_name(date) || generic_location_display_name when :generic_long generic_long_display_name(date) || generic_location_display_name when :specific_short specific_short_display_name(date) when :specific_long specific_long_display_name(date) 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) format_display_name(date, :generic, :short) end def generic_long_display_name(date) format_display_name(date, :generic, :long) end def specific_short_display_name(date) format_display_name(date, :specific, :short) end def specific_long_display_name(date) format_display_name(date, :specific, :long) 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) date_int = date.strftime('%s').to_i period = tz.period_for_local(date) 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