twitter/twitter-cldr-rb

View on GitHub
lib/twitter_cldr/formatters/calendars/date_time_formatter.rb

Summary

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

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

# This class has been adapted from Sven Fuch's ruby-cldr gem
# See LICENSE for the accompanying license for his contributions

require 'tzinfo'

module TwitterCldr
  module Formatters
    class DateTimeFormatter < Formatter

      WEEKDAY_KEYS = [:sun, :mon, :tue, :wed, :thu, :fri, :sat].freeze

      METHODS = { # ignoring u, l, g, j, A
        'G' => :era,
        'y' => :year,
        'Y' => :year_of_week_of_year,
        'Q' => :quarter,
        'q' => :quarter_stand_alone,
        'M' => :month,
        'L' => :month_stand_alone,
        'w' => :week_of_year,
        'W' => :week_of_month,
        'd' => :day,
        'D' => :day_of_month,
        'F' => :day_of_week_in_month,
        'E' => :weekday,
        'e' => :weekday_local,
        'c' => :weekday_local_stand_alone,
        'a' => :period,
        'B' => :period,
        'h' => :hour,
        'H' => :hour,
        'K' => :hour,
        'k' => :hour,
        'm' => :minute,
        's' => :second,
        'S' => :second_fraction,
        'z' => :timezone,
        'Z' => :timezone,
        'O' => :timezone,
        'v' => :timezone,
        'V' => :timezone,
        'x' => :timezone,
        'X' => :timezone
      }.freeze

      TZ_PATTERNS = {
        'z'     => :specific_short,
        'zz'    => :specific_short,
        'zzz'   => :specific_short,
        'zzzz'  => :specific_long,
        'Z'     => :iso_basic_local_full,
        'ZZ'    => :iso_basic_local_full,
        'ZZZ'   => :iso_basic_local_full,
        'ZZZZ'  => :long_gmt,
        'ZZZZZ' => :iso_extended_local_fixed,
        'OOOO'  => :long_gmt,
        'O'     => :short_gmt,
        'v'     => :generic_short,
        'vvvv'  => :generic_long,
        'V'     => :zone_id_short,
        'VV'    => :zone_id,
        'VVV'   => :exemplar_location,
        'VVVV'  => :generic_location,
        'X'     => :iso_basic_short,
        'XX'    => :iso_basic_fixed,
        'XXX'   => :iso_extended_fixed,
        'XXXX'  => :iso_basic_full,
        'XXXXX' => :iso_extended_full,
        'x'     => :iso_basic_local_short,
        'xx'    => :iso_basic_local_fixed,
        'xxx'   => :iso_extended_local_fixed,
        'xxxx'  => :iso_basic_local_full,
        'xxxxx' => :iso_extended_local_full
      }.freeze

      protected

      def format_pattern(token, index, obj, options)
        send(METHODS[token.value[0].chr], obj, token.value, token.value.size, options)
      end

      def calendar
        data_reader.calendar
      end

      # There is incomplete era data in CLDR for certain locales like Hindi.
      # Fall back if that happens.
      def era(date, pattern, length, options = {})
        choices = case length
          when 0
            ["", ""]
          when 1..3
            calendar.eras(:abbr)
          else
            calendar.eras(:name)
        end

        if result = choices[date.year < 0 ? 0 : 1]
          result
        else
          era(date, pattern[0..-2], length - 1)
        end
      end

      def year(date, pattern, length, options = {})
        year = date.year.to_s
        year = year.length == 1 ? year : year[-2, 2] if length == 2
        year = year.rjust(length, '0') if length > 1
        year
      end

      def year_of_week_of_year(date, pattern, length, options = {})
        week_fields_for(date)[:year_woy].to_s
      end

      def day_of_week_in_month(date, pattern, length, options = {}) # e.g. 2nd Wed in July
        week_fields_for(date)[:day_of_week_in_month].to_s
      end

      def week_of_month(date, pattern, length, options = {})
        week_fields_for(date)[:week_of_month].to_s
      end

      def week_of_year(date, pattern, length, options = {})
        week_fields_for(date)[:week_of_year].to_s
      end

      def quarter(date, pattern, length, options = {})
        quarter = (date.month.to_i - 1) / 3 + 1
        case length
          when 1
            quarter.to_s
          when 2
            quarter.to_s.rjust(length, '0')
          when 3
            calendar.quarters(:abbreviated, :format)[quarter]
          when 4
            calendar.quarters(:wide, :format)[quarter]
        end
      end

      def quarter_stand_alone(date, pattern, length, options = {})
        quarter = (date.month.to_i - 1) / 3 + 1
        case length
          when 1
            quarter.to_s
          when 2
            quarter.to_s.rjust(length, '0')
          when 3
            raise NotImplementedError, 'requires cldr\'s "multiple inheritance"'
            # calendar[:quarters][:'stand-alone'][:abbreviated][key]
          when 4
            raise NotImplementedError, 'requires cldr\'s "multiple inheritance"'
            # calendar[:quarters][:'stand-alone'][:wide][key]
          when 5
            calendar.quarters(:narrow)[quarter]
        end
      end

      def month(date, pattern, length, options = {})
        case length
          when 1
            date.month.to_s
          when 2
            date.month.to_s.rjust(length, '0')
          when 3
            calendar.months(:abbreviated, :format)[date.month - 1]
          when 4
            calendar.months(:wide, :format)[date.month - 1]
          when 5
            raise NotImplementedError, 'requires cldr\'s "multiple inheritance"'
            # calendar[:months][:format][:narrow][date.month]
          else
            # raise unknown date format
        end
      end

      def month_stand_alone(date, pattern, length, options = {})
        case length
          when 1
            date.month.to_s
          when 2
            date.month.to_s.rjust(length, '0')
          when 3
            calendar.months(:abbreviated)[date.month - 1]
          when 4
            calendar.months(:wide)[date.month - 1]
          when 5
            calendar.months(:narrow)[date.month - 1]
          else
            # raise unknown date format
        end
      end

      def day(date, pattern, length, options = {})
        case length
          when 1
            date.day.to_s
          when 2
            date.day.to_s.rjust(length, '0')
        end
      end

      def weekday(date, pattern, length, options = {})
        key = WEEKDAY_KEYS[date.wday]
        case length
          when 1..3
            calendar.weekdays(:abbreviated, :format)[key]
          when 4
            calendar.weekdays(:wide, :format)[key]
          when 5
            calendar.weekdays(:narrow)[key]
        end
      end

      def weekday_local(date, pattern, length, options = {})
        # "Like E except adds a numeric value depending on the local starting day of the week"
        # CLDR does not contain data as to which day is the first day of the week, so we will assume Monday (Ruby default)
        case length
          when 1..2
            date.cwday.to_s
          else
            weekday(date, pattern, length)
        end
      end

      def weekday_local_stand_alone(date, pattern, length, options = {})
        case length
          when 1
            weekday_local(date, pattern, length)
          else
            weekday(date, pattern, length)
        end
      end

      def period(time, pattern, length, options = {})
        if pattern[0] == 'a'
          return calendar.periods(:wide, :format)[time.strftime('%p').downcase.to_sym]
        end

        period_type = TwitterCldr::Shared::DayPeriods
          .instance(data_reader.locale)
          .period_type_for(time)

        if length <= 3
          calendar.periods(:abbreviated, :format)[period_type]
        elsif length == 4 || length > 5
          calendar.periods(:wide, :format)[period_type]
        else
          # length == 5
          calendar.periods(:narrow, :format)[period_type]
        end
      end

      def hour(time, pattern, length, options = {})
        hour = time.hour
        hour = case pattern[0, 1]
          when 'h' # [1-12]
            hour > 12 ? (hour - 12) : (hour == 0 ? 12 : hour)
          when 'H' # [0-23]
            hour
          when 'K' # [0-11]
            hour > 11 ? hour - 12 : hour
          when 'k' # [1-24]
            hour == 0 ? 24 : hour
        end
        length == 1 ? hour.to_s : hour.to_s.rjust(length, '0')
      end

      def minute(time, pattern, length, options = {})
        length == 1 ? time.min.to_s : time.min.to_s.rjust(length, '0')
      end

      def second(time, pattern, length, options = {})
        length == 1 ? time.sec.to_s : time.sec.to_s.rjust(length, '0')
      end

      def second_fraction(time, pattern, length, options = {})
        raise ArgumentError.new('can not use the S format with more than 6 digits') if length > 6
        (time.usec.to_f / 10 ** (6 - length)).round.to_s.rjust(length, '0')
      end

      def timezone(time, pattern, length, options = {})
        tz = TwitterCldr::Timezones::Timezone.instance(
          options[:timezone] || 'UTC', data_reader.locale
        )

        tz.display_name_for(time, TZ_PATTERNS[pattern])
      end

      # ported from icu4j 64.2
      def week_fields_for(date)
        week_data_cache[date] ||= begin
          eyear = date.year
          day_of_week = date.wday + 1
          day_of_year = date.yday

          # this should come from the CLDR's supplemental data set, but we
          # don't have access to it right now
          first_day_of_week = 1  # assume sunday
          minimal_days_in_first_week = 1  # assume US

          # WEEK_OF_YEAR start
          # Compute the week of the year.  For the Gregorian calendar, valid week
          # numbers run from 1 to 52 or 53, depending on the year, the first day
          # of the week, and the minimal days in the first week.  For other
          # calendars, the valid range may be different -- it depends on the year
          # length.  Days at the start of the year may fall into the last week of
          # the previous year; days at the end of the year may fall into the
          # first week of the next year.  ASSUME that the year length is less than
          # 7000 days.
          year_of_week_of_year = eyear
          rel_dow = (day_of_week + 7 - first_day_of_week) % 7 # 0..6
          rel_dow_jan1 = (day_of_week - day_of_year + 7001 - first_day_of_week) % 7 # 0..6
          woy = (day_of_year - 1 + rel_dow_jan1) / 7 # 0..53

          if (7 - rel_dow_jan1) >= minimal_days_in_first_week
            woy += 1
          end

          # Adjust for weeks at the year end that overlap into the previous or
          # next calendar year.
          if woy == 0
            # We are the last week of the previous year.
            # Check to see if we are in the last week; if so, we need
            # to handle the case in which we are the first week of the
            # next year.

            year_length = (Date.new(eyear, 1, 1) - Date.new(eyear - 1, 1, 1)).to_i

            prev_doy = day_of_year + year_length
            woy = week_number(prev_doy, day_of_week)
            year_of_week_of_year -= 1
          else
            last_doy = (Date.new(eyear + 1, 1, 1) - Date.new(eyear, 1, 1)).to_i
            # Fast check: For it to be week 1 of the next year, the DOY
            # must be on or after L-5, where L is yearLength(), then it
            # cannot possibly be week 1 of the next year:
            #          L-5                  L
            # doy: 359 360 361 362 363 364 365 001
            # dow:      1   2   3   4   5   6   7
            if day_of_year >= (last_doy - 5)
              last_rel_dow = (rel_dow + last_doy - day_of_year) % 7

              if (last_rel_dow < 0)
                last_rel_dow += 7
              end

              if ((6 - last_rel_dow) >= minimal_days_in_first_week) && ((day_of_year + 7 - rel_dow) > last_doy)
                woy = 1;
                year_of_week_of_year += 1
              end
            end
          end

          {
            week_of_year: woy,
            year_woy: year_of_week_of_year,
            week_of_month: week_number(date.mday, day_of_week),
            day_of_week_in_month: (date.mday - 1) / 7 + 1
          }
        end
      end

      def week_number(day_of_period, day_of_week)
        # this should come from the CLDR's supplemental data set, but we
        # don't have access to it right now
        first_day_of_week = 1  # assume sunday
        minimal_days_in_first_week = 1  # assume US

        # Determine the day of the week of the first day of the period
        # in question (either a year or a month).  Zero represents the
        # first day of the week on this calendar.
        period_start_day_of_week = (day_of_week - first_day_of_week - day_of_period + 1) % 7

        if (period_start_day_of_week < 0)
          period_start_day_of_week += 7
        end

        # Compute the week number.  Initially, ignore the first week, which
        # may be fractional (or may not be).  We add period_start_day_of_week in
        # order to fill out the first week, if it is fractional.
        week_no = (day_of_period + period_start_day_of_week - 1) / 7

        # If the first week is long enough, then count it.  If
        # the minimal days in the first week is one, or if the period start
        # is zero, we always increment weekNo.
        if (7 - period_start_day_of_week) >= minimal_days_in_first_week
          week_no += 1
        end

        week_no
      end

      def week_data_cache
        @@week_data_cache ||= {}
      end

    end
  end
end