text: import lightFormatters from '../lightFormatters/index.js'
  import getUTCDayOfYear from '../../../_lib/getUTCDayOfYear/index.js'
  import getUTCISOWeek from '../../../_lib/getUTCISOWeek/index.js'
  import getUTCISOWeekYear from '../../../_lib/getUTCISOWeekYear/index.js'
  import getUTCWeek from '../../../_lib/getUTCWeek/index.js'
  import getUTCWeekYear from '../../../_lib/getUTCWeekYear/index.js'
  import addLeadingZeros from '../../addLeadingZeros/index.js'
  
  var dayPeriodEnum = {
    am: 'am',
    pm: 'pm',
    midnight: 'midnight',
    noon: 'noon',
    morning: 'morning',
    afternoon: 'afternoon',
    evening: 'evening',
    night: 'night'
  }
  
  /*
   * |     | Unit                           |     | Unit                           |
   * |-----|--------------------------------|-----|--------------------------------|
   * |  a  | AM, PM                         |  A* | Milliseconds in day            |
   * |  b  | AM, PM, noon, midnight         |  B  | Flexible day period            |
   * |  c  | Stand-alone local day of week  |  C* | Localized hour w/ day period   |
   * |  d  | Day of month                   |  D  | Day of year                    |
   * |  e  | Local day of week              |  E  | Day of week                    |
   * |  f  |                                |  F* | Day of week in month           |
   * |  g* | Modified Julian day            |  G  | Era                            |
   * |  h  | Hour [1-12]                    |  H  | Hour [0-23]                    |
   * |  i! | ISO day of week                |  I! | ISO week of year               |
   * |  j* | Localized hour w/ day period   |  J* | Localized hour w/o day period  |
   * |  k  | Hour [1-24]                    |  K  | Hour [0-11]                    |
   * |  l* | (deprecated)                   |  L  | Stand-alone month              |
   * |  m  | Minute                         |  M  | Month                          |
   * |  n  |                                |  N  |                                |
   * |  o! | Ordinal number modifier        |  O  | Timezone (GMT)                 |
   * |  p! | Long localized time            |  P! | Long localized date            |
   * |  q  | Stand-alone quarter            |  Q  | Quarter                        |
   * |  r* | Related Gregorian year         |  R! | ISO week-numbering year        |
   * |  s  | Second                         |  S  | Fraction of second             |
   * |  t! | Seconds timestamp              |  T! | Milliseconds timestamp         |
   * |  u  | Extended year                  |  U* | Cyclic year                    |
   * |  v* | Timezone (generic non-locat.)  |  V* | Timezone (location)            |
   * |  w  | Local week of year             |  W* | Week of month                  |
   * |  x  | Timezone (ISO-8601 w/o Z)      |  X  | Timezone (ISO-8601)            |
   * |  y  | Year (abs)                     |  Y  | Local week-numbering year      |
   * |  z  | Timezone (specific non-locat.) |  Z* | Timezone (aliases)             |
   *
   * Letters marked by * are not implemented but reserved by Unicode standard.
   *
   * Letters marked by ! are non-standard, but implemented by date-fns:
   * - `o` modifies the previous token to turn it into an ordinal (see `format` docs)
   * - `i` is ISO day of week. For `i` and `ii` is returns numeric ISO week days,
   *   i.e. 7 for Sunday, 1 for Monday, etc.
   * - `I` is ISO week of year, as opposed to `w` which is local week of year.
   * - `R` is ISO week-numbering year, as opposed to `Y` which is local week-numbering year.
   *   `R` is supposed to be used in conjunction with `I` and `i`
   *   for universal ISO week-numbering date, whereas
   *   `Y` is supposed to be used in conjunction with `w` and `e`
   *   for week-numbering date specific to the locale.
   * - `P` is long localized date format
   * - `p` is long localized time format
   */
  
  var formatters = {
    // Era
    G: function(date, token, localize) {
      var era = date.getUTCFullYear() > 0 ? 1 : 0
      switch (token) {
        // AD, BC
        case 'G':
        case 'GG':
        case 'GGG':
          return localize.era(era, { width: 'abbreviated' })
        // A, B
        case 'GGGGG':
          return localize.era(era, { width: 'narrow' })
        // Anno Domini, Before Christ
        case 'GGGG':
        default:
          return localize.era(era, { width: 'wide' })
      }
    },
  
    // Year
    y: function(date, token, localize) {
      // Ordinal number
      if (token === 'yo') {
        var signedYear = date.getUTCFullYear()
        // Returns 1 for 1 BC (which is year 0 in JavaScript)
        var year = signedYear > 0 ? signedYear : 1 - signedYear
        return localize.ordinalNumber(year, { unit: 'year' })
      }
  
      return lightFormatters.y(date, token)
    },
  
    // Local week-numbering year
    Y: function(date, token, localize, options) {
      var signedWeekYear = getUTCWeekYear(date, options)
      // Returns 1 for 1 BC (which is year 0 in JavaScript)
      var weekYear = signedWeekYear > 0 ? signedWeekYear : 1 - signedWeekYear
  
      // Two digit year
      if (token === 'YY') {
        var twoDigitYear = weekYear % 100
        return addLeadingZeros(twoDigitYear, 2)
      }
  
      // Ordinal number
      if (token === 'Yo') {
        return localize.ordinalNumber(weekYear, { unit: 'year' })
      }
  
      // Padding
      return addLeadingZeros(weekYear, token.length)
    },
  
    // ISO week-numbering year
    R: function(date, token) {
      var isoWeekYear = getUTCISOWeekYear(date)
  
      // Padding
      return addLeadingZeros(isoWeekYear, token.length)
    },
  
    // Extended year. This is a single number designating the year of this calendar system.
    // The main difference between `y` and `u` localizers are B.C. years:
    // | Year | `y` | `u` |
    // |------|-----|-----|
    // | AC 1 |   1 |   1 |
    // | BC 1 |   1 |   0 |
    // | BC 2 |   2 |  -1 |
    // Also `yy` always returns the last two digits of a year,
    // while `uu` pads single digit years to 2 characters and returns other years unchanged.
    u: function(date, token) {
      var year = date.getUTCFullYear()
      return addLeadingZeros(year, token.length)
    },
  
    // Quarter
    Q: function(date, token, localize) {
      var quarter = Math.ceil((date.getUTCMonth() + 1) / 3)
      switch (token) {
        // 1, 2, 3, 4
        case 'Q':
          return String(quarter)
        // 01, 02, 03, 04
        case 'QQ':
          return addLeadingZeros(quarter, 2)
        // 1st, 2nd, 3rd, 4th
        case 'Qo':
          return localize.ordinalNumber(quarter, { unit: 'quarter' })
        // Q1, Q2, Q3, Q4
        case 'QQQ':
          return localize.quarter(quarter, {
            width: 'abbreviated',
            context: 'formatting'
          })
        // 1, 2, 3, 4 (narrow quarter; could be not numerical)
        case 'QQQQQ':
          return localize.quarter(quarter, {
            width: 'narrow',
            context: 'formatting'
          })
        // 1st quarter, 2nd quarter, ...
        case 'QQQQ':
        default:
          return localize.quarter(quarter, {
            width: 'wide',
            context: 'formatting'
          })
      }
    },
  
    // Stand-alone quarter
    q: function(date, token, localize) {
      var quarter = Math.ceil((date.getUTCMonth() + 1) / 3)
      switch (token) {
        // 1, 2, 3, 4
        case 'q':
          return String(quarter)
        // 01, 02, 03, 04
        case 'qq':
          return addLeadingZeros(quarter, 2)
        // 1st, 2nd, 3rd, 4th
        case 'qo':
          return localize.ordinalNumber(quarter, { unit: 'quarter' })
        // Q1, Q2, Q3, Q4
        case 'qqq':
          return localize.quarter(quarter, {
            width: 'abbreviated',
            context: 'standalone'
          })
        // 1, 2, 3, 4 (narrow quarter; could be not numerical)
        case 'qqqqq':
          return localize.quarter(quarter, {
            width: 'narrow',
            context: 'standalone'
          })
        // 1st quarter, 2nd quarter, ...
        case 'qqqq':
        default:
          return localize.quarter(quarter, {
            width: 'wide',
            context: 'standalone'
          })
      }
    },
  
    // Month
    M: function(date, token, localize) {
      var month = date.getUTCMonth()
      switch (token) {
        case 'M':
        case 'MM':
          return lightFormatters.M(date, token)
        // 1st, 2nd, ..., 12th
        case 'Mo':
          return localize.ordinalNumber(month + 1, { unit: 'month' })
        // Jan, Feb, ..., Dec
        case 'MMM':
          return localize.month(month, {
            width: 'abbreviated',
            context: 'formatting'
          })
        // J, F, ..., D
        case 'MMMMM':
          return localize.month(month, { width: 'narrow', context: 'formatting' })
        // January, February, ..., December
        case 'MMMM':
        default:
          return localize.month(month, { width: 'wide', context: 'formatting' })
      }
    },
  
    // Stand-alone month
    L: function(date, token, localize) {
      var month = date.getUTCMonth()
      switch (token) {
        // 1, 2, ..., 12
        case 'L':
          return String(month + 1)
        // 01, 02, ..., 12
        case 'LL':
          return addLeadingZeros(month + 1, 2)
        // 1st, 2nd, ..., 12th
        case 'Lo':
          return localize.ordinalNumber(month + 1, { unit: 'month' })
        // Jan, Feb, ..., Dec
        case 'LLL':
          return localize.month(month, {
            width: 'abbreviated',
            context: 'standalone'
          })
        // J, F, ..., D
        case 'LLLLL':
          return localize.month(month, { width: 'narrow', context: 'standalone' })
        // January, February, ..., December
        case 'LLLL':
        default:
          return localize.month(month, { width: 'wide', context: 'standalone' })
      }
    },
  
    // Local week of year
    w: function(date, token, localize, options) {
      var week = getUTCWeek(date, options)
  
      if (token === 'wo') {
        return localize.ordinalNumber(week, { unit: 'week' })
      }
  
      return addLeadingZeros(week, token.length)
    },
  
    // ISO week of year
    I: function(date, token, localize) {
      var isoWeek = getUTCISOWeek(date)
  
      if (token === 'Io') {
        return localize.ordinalNumber(isoWeek, { unit: 'week' })
      }
  
      return addLeadingZeros(isoWeek, token.length)
    },
  
    // Day of the month
    d: function(date, token, localize) {
      if (token === 'do') {
        return localize.ordinalNumber(date.getUTCDate(), { unit: 'date' })
      }
  
      return lightFormatters.d(date, token)
    },
  
    // Day of year
    D: function(date, token, localize) {
      var dayOfYear = getUTCDayOfYear(date)
  
      if (token === 'Do') {
        return localize.ordinalNumber(dayOfYear, { unit: 'dayOfYear' })
      }
  
      return addLeadingZeros(dayOfYear, token.length)
    },
  
    // Day of week
    E: function(date, token, localize) {
      var dayOfWeek = date.getUTCDay()
      switch (token) {
        // Tue
        case 'E':
        case 'EE':
        case 'EEE':
          return localize.day(dayOfWeek, {
            width: 'abbreviated',
            context: 'formatting'
          })
        // T
        case 'EEEEE':
          return localize.day(dayOfWeek, {
            width: 'narrow',
            context: 'formatting'
          })
        // Tu
        case 'EEEEEE':
          return localize.day(dayOfWeek, {
            width: 'short',
            context: 'formatting'
          })
        // Tuesday
        case 'EEEE':
        default:
          return localize.day(dayOfWeek, { width: 'wide', context: 'formatting' })
      }
    },
  
    // Local day of week
    e: function(date, token, localize, options) {
      var dayOfWeek = date.getUTCDay()
      var localDayOfWeek = (dayOfWeek - options.weekStartsOn + 8) % 7 || 7
      switch (token) {
        // Numerical value (Nth day of week with current locale or weekStartsOn)
        case 'e':
          return String(localDayOfWeek)
        // Padded numerical value
        case 'ee':
          return addLeadingZeros(localDayOfWeek, 2)
        // 1st, 2nd, ..., 7th
        case 'eo':
          return localize.ordinalNumber(localDayOfWeek, { unit: 'day' })
        case 'eee':
          return localize.day(dayOfWeek, {
            width: 'abbreviated',
            context: 'formatting'
          })
        // T
        case 'eeeee':
          return localize.day(dayOfWeek, {
            width: 'narrow',
            context: 'formatting'
          })
        // Tu
        case 'eeeeee':
          return localize.day(dayOfWeek, {
            width: 'short',
            context: 'formatting'
          })
        // Tuesday
        case 'eeee':
        default:
          return localize.day(dayOfWeek, { width: 'wide', context: 'formatting' })
      }
    },
  
    // Stand-alone local day of week
    c: function(date, token, localize, options) {
      var dayOfWeek = date.getUTCDay()
      var localDayOfWeek = (dayOfWeek - options.weekStartsOn + 8) % 7 || 7
      switch (token) {
        // Numerical value (same as in `e`)
        case 'c':
          return String(localDayOfWeek)
        // Padded numerical value
        case 'cc':
          return addLeadingZeros(localDayOfWeek, token.length)
        // 1st, 2nd, ..., 7th
        case 'co':
          return localize.ordinalNumber(localDayOfWeek, { unit: 'day' })
        case 'ccc':
          return localize.day(dayOfWeek, {
            width: 'abbreviated',
            context: 'standalone'
          })
        // T
        case 'ccccc':
          return localize.day(dayOfWeek, {
            width: 'narrow',
            context: 'standalone'
          })
        // Tu
        case 'cccccc':
          return localize.day(dayOfWeek, {
            width: 'short',
            context: 'standalone'
          })
        // Tuesday
        case 'cccc':
        default:
          return localize.day(dayOfWeek, { width: 'wide', context: 'standalone' })
      }
    },
  
    // ISO day of week
    i: function(date, token, localize) {
      var dayOfWeek = date.getUTCDay()
      var isoDayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek
      switch (token) {
        // 2
        case 'i':
          return String(isoDayOfWeek)
        // 02
        case 'ii':
          return addLeadingZeros(isoDayOfWeek, token.length)
        // 2nd
        case 'io':
          return localize.ordinalNumber(isoDayOfWeek, { unit: 'day' })
        // Tue
        case 'iii':
          return localize.day(dayOfWeek, {
            width: 'abbreviated',
            context: 'formatting'
          })
        // T
        case 'iiiii':
          return localize.day(dayOfWeek, {
            width: 'narrow',
            context: 'formatting'
          })
        // Tu
        case 'iiiiii':
          return localize.day(dayOfWeek, {
            width: 'short',
            context: 'formatting'
          })
        // Tuesday
        case 'iiii':
        default:
          return localize.day(dayOfWeek, { width: 'wide', context: 'formatting' })
      }
    },
  
    // AM or PM
    a: function(date, token, localize) {
      var hours = date.getUTCHours()
      var dayPeriodEnumValue = hours / 12 >= 1 ? 'pm' : 'am'
  
      switch (token) {
        case 'a':
        case 'aa':
        case 'aaa':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'abbreviated',
            context: 'formatting'
          })
        case 'aaaaa':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'aaaa':
        default:
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'wide',
            context: 'formatting'
          })
      }
    },
  
    // AM, PM, midnight, noon
    b: function(date, token, localize) {
      var hours = date.getUTCHours()
      var dayPeriodEnumValue
      if (hours === 12) {
        dayPeriodEnumValue = dayPeriodEnum.noon
      } else if (hours === 0) {
        dayPeriodEnumValue = dayPeriodEnum.midnight
      } else {
        dayPeriodEnumValue = hours / 12 >= 1 ? 'pm' : 'am'
      }
  
      switch (token) {
        case 'b':
        case 'bb':
        case 'bbb':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'abbreviated',
            context: 'formatting'
          })
        case 'bbbbb':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'bbbb':
        default:
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'wide',
            context: 'formatting'
          })
      }
    },
  
    // in the morning, in the afternoon, in the evening, at night
    B: function(date, token, localize) {
      var hours = date.getUTCHours()
      var dayPeriodEnumValue
      if (hours >= 17) {
        dayPeriodEnumValue = dayPeriodEnum.evening
      } else if (hours >= 12) {
        dayPeriodEnumValue = dayPeriodEnum.afternoon
      } else if (hours >= 4) {
        dayPeriodEnumValue = dayPeriodEnum.morning
      } else {
        dayPeriodEnumValue = dayPeriodEnum.night
      }
  
      switch (token) {
        case 'B':
        case 'BB':
        case 'BBB':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'abbreviated',
            context: 'formatting'
          })
        case 'BBBBB':
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'narrow',
            context: 'formatting'
          })
        case 'BBBB':
        default:
          return localize.dayPeriod(dayPeriodEnumValue, {
            width: 'wide',
            context: 'formatting'
          })
      }
    },
  
    // Hour [1-12]
    h: function(date, token, localize) {
      if (token === 'ho') {
        var hours = date.getUTCHours() % 12
        if (hours === 0) hours = 12
        return localize.ordinalNumber(hours, { unit: 'hour' })
      }
  
      return lightFormatters.h(date, token)
    },
  
    // Hour [0-23]
    H: function(date, token, localize) {
      if (token === 'Ho') {
        return localize.ordinalNumber(date.getUTCHours(), { unit: 'hour' })
      }
  
      return lightFormatters.H(date, token)
    },
  
    // Hour [0-11]
    K: function(date, token, localize) {
      var hours = date.getUTCHours() % 12
  
      if (token === 'Ko') {
        return localize.ordinalNumber(hours, { unit: 'hour' })
      }
  
      return addLeadingZeros(hours, token.length)
    },
  
    // Hour [1-24]
    k: function(date, token, localize) {
      var hours = date.getUTCHours()
      if (hours === 0) hours = 24
  
      if (token === 'ko') {
        return localize.ordinalNumber(hours, { unit: 'hour' })
      }
  
      return addLeadingZeros(hours, token.length)
    },
  
    // Minute
    m: function(date, token, localize) {
      if (token === 'mo') {
        return localize.ordinalNumber(date.getUTCMinutes(), { unit: 'minute' })
      }
  
      return lightFormatters.m(date, token)
    },
  
    // Second
    s: function(date, token, localize) {
      if (token === 'so') {
        return localize.ordinalNumber(date.getUTCSeconds(), { unit: 'second' })
      }
  
      return lightFormatters.s(date, token)
    },
  
    // Fraction of second
    S: function(date, token) {
      var numberOfDigits = token.length
      var milliseconds = date.getUTCMilliseconds()
      var fractionalSeconds = Math.floor(
        milliseconds * Math.pow(10, numberOfDigits - 3)
      )
      return addLeadingZeros(fractionalSeconds, numberOfDigits)
    },
  
    // Timezone (ISO-8601. If offset is 0, output is always `'Z'`)
    X: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timezoneOffset = originalDate.getTimezoneOffset()
  
      if (timezoneOffset === 0) {
        return 'Z'
      }
  
      switch (token) {
        // Hours and optional minutes
        case 'X':
          return formatTimezoneWithOptionalMinutes(timezoneOffset)
  
        // Hours, minutes and optional seconds without `:` delimiter
        // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets
        // so this token always has the same output as `XX`
        case 'XXXX':
        case 'XX': // Hours and minutes without `:` delimiter
          return formatTimezone(timezoneOffset)
  
        // Hours, minutes and optional seconds with `:` delimiter
        // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets
        // so this token always has the same output as `XXX`
        case 'XXXXX':
        case 'XXX': // Hours and minutes with `:` delimiter
        default:
          return formatTimezone(timezoneOffset, ':')
      }
    },
  
    // Timezone (ISO-8601. If offset is 0, output is `'+00:00'` or equivalent)
    x: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timezoneOffset = originalDate.getTimezoneOffset()
  
      switch (token) {
        // Hours and optional minutes
        case 'x':
          return formatTimezoneWithOptionalMinutes(timezoneOffset)
  
        // Hours, minutes and optional seconds without `:` delimiter
        // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets
        // so this token always has the same output as `xx`
        case 'xxxx':
        case 'xx': // Hours and minutes without `:` delimiter
          return formatTimezone(timezoneOffset)
  
        // Hours, minutes and optional seconds with `:` delimiter
        // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets
        // so this token always has the same output as `xxx`
        case 'xxxxx':
        case 'xxx': // Hours and minutes with `:` delimiter
        default:
          return formatTimezone(timezoneOffset, ':')
      }
    },
  
    // Timezone (GMT)
    O: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timezoneOffset = originalDate.getTimezoneOffset()
  
      switch (token) {
        // Short
        case 'O':
        case 'OO':
        case 'OOO':
          return 'GMT' + formatTimezoneShort(timezoneOffset, ':')
        // Long
        case 'OOOO':
        default:
          return 'GMT' + formatTimezone(timezoneOffset, ':')
      }
    },
  
    // Timezone (specific non-location)
    z: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timezoneOffset = originalDate.getTimezoneOffset()
  
      switch (token) {
        // Short
        case 'z':
        case 'zz':
        case 'zzz':
          return 'GMT' + formatTimezoneShort(timezoneOffset, ':')
        // Long
        case 'zzzz':
        default:
          return 'GMT' + formatTimezone(timezoneOffset, ':')
      }
    },
  
    // Seconds timestamp
    t: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timestamp = Math.floor(originalDate.getTime() / 1000)
      return addLeadingZeros(timestamp, token.length)
    },
  
    // Milliseconds timestamp
    T: function(date, token, _localize, options) {
      var originalDate = options._originalDate || date
      var timestamp = originalDate.getTime()
      return addLeadingZeros(timestamp, token.length)
    }
  }
  
  function formatTimezoneShort(offset, dirtyDelimiter) {
    var sign = offset > 0 ? '-' : '+'
    var absOffset = Math.abs(offset)
    var hours = Math.floor(absOffset / 60)
    var minutes = absOffset % 60
    if (minutes === 0) {
      return sign + String(hours)
    }
    var delimiter = dirtyDelimiter || ''
    return sign + String(hours) + delimiter + addLeadingZeros(minutes, 2)
  }
  
  function formatTimezoneWithOptionalMinutes(offset, dirtyDelimiter) {
    if (offset % 60 === 0) {
      var sign = offset > 0 ? '-' : '+'
      return sign + addLeadingZeros(Math.abs(offset) / 60, 2)
    }
    return formatTimezone(offset, dirtyDelimiter)
  }
  
  function formatTimezone(offset, dirtyDelimiter) {
    var delimiter = dirtyDelimiter || ''
    var sign = offset > 0 ? '-' : '+'
    var absOffset = Math.abs(offset)
    var hours = addLeadingZeros(Math.floor(absOffset / 60), 2)
    var minutes = addLeadingZeros(absOffset % 60, 2)
    return sign + hours + delimiter + minutes
  }
  
  export default formatters
  