content: /*
   * Based on the UCB version of strftime.c with the copyright notice appearing below.
   */
  
  /*
  ** Copyright (c) 1989 The Regents of the University of California.
  ** All rights reserved.
  **
  ** Redistribution and use in source and binary forms are permitted
  ** provided that the above copyright notice and this paragraph are
  ** duplicated in all such forms and that any documentation,
  ** advertising materials, and other materials related to such
  ** distribution and use acknowledge that the software was developed
  ** by the University of California, Berkeley. The name of the
  ** University may not be used to endorse or promote products derived
  ** from this software without specific prior written permission.
  ** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
  ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
  ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
  */
  package android.text.format;
  
  import android.content.res.Resources;
  
  import libcore.icu.LocaleData;
  import libcore.util.ZoneInfo;
  
  import java.nio.CharBuffer;
  import java.util.Formatter;
  import java.util.Locale;
  import java.util.TimeZone;
  
  /**
   * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java.
   *
   * <p>This class is not thread safe.
   */
  class TimeFormatter {
      // An arbitrary value outside the range representable by a char.
      private static final int FORCE_LOWER_CASE = -1;
  
      private static final int SECSPERMIN = 60;
      private static final int MINSPERHOUR = 60;
      private static final int DAYSPERWEEK = 7;
      private static final int MONSPERYEAR = 12;
      private static final int HOURSPERDAY = 24;
      private static final int DAYSPERLYEAR = 366;
      private static final int DAYSPERNYEAR = 365;
  
      /**
       * The Locale for which the cached LocaleData and formats have been loaded.
       */
      private static Locale sLocale;
      private static LocaleData sLocaleData;
      private static String sTimeOnlyFormat;
      private static String sDateOnlyFormat;
      private static String sDateTimeFormat;
  
      private final LocaleData localeData;
      private final String dateTimeFormat;
      private final String timeOnlyFormat;
      private final String dateOnlyFormat;
  
      private StringBuilder outputBuilder;
      private Formatter numberFormatter;
  
      public TimeFormatter() {
          synchronized (TimeFormatter.class) {
              Locale locale = Locale.getDefault();
  
              if (sLocale == null || !(locale.equals(sLocale))) {
                  sLocale = locale;
                  sLocaleData = LocaleData.get(locale);
  
                  Resources r = Resources.getSystem();
                  sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day);
                  sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year);
                  sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time);
              }
  
              this.dateTimeFormat = sDateTimeFormat;
              this.timeOnlyFormat = sTimeOnlyFormat;
              this.dateOnlyFormat = sDateOnlyFormat;
              localeData = sLocaleData;
          }
      }
  
      /**
       * Format the specified {@code wallTime} using {@code pattern}. The output is returned.
       */
      public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
          try {
              StringBuilder stringBuilder = new StringBuilder();
  
              outputBuilder = stringBuilder;
              // This uses the US locale because number localization is handled separately (see below)
              // and locale sensitive strings are output directly using outputBuilder.
              numberFormatter = new Formatter(stringBuilder, Locale.US);
  
              formatInternal(pattern, wallTime, zoneInfo);
              String result = stringBuilder.toString();
              // This behavior is the source of a bug since some formats are defined as being
              // in ASCII and not localized.
              if (localeData.zeroDigit != '0') {
                  result = localizeDigits(result);
              }
              return result;
          } finally {
              outputBuilder = null;
              numberFormatter = null;
          }
      }
  
      private String localizeDigits(String s) {
          int length = s.length();
          int offsetToLocalizedDigits = localeData.zeroDigit - '0';
          StringBuilder result = new StringBuilder(length);
          for (int i = 0; i < length; ++i) {
              char ch = s.charAt(i);
              if (ch >= '0' && ch <= '9') {
                  ch += offsetToLocalizedDigits;
              }
              result.append(ch);
          }
          return result.toString();
      }
  
      /**
       * Format the specified {@code wallTime} using {@code pattern}. The output is written to
       * {@link #outputBuilder}.
       */
      private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
          CharBuffer formatBuffer = CharBuffer.wrap(pattern);
          while (formatBuffer.remaining() > 0) {
              boolean outputCurrentChar = true;
              char currentChar = formatBuffer.get(formatBuffer.position());
              if (currentChar == '%') {
                  outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfo);
              }
              if (outputCurrentChar) {
                  outputBuilder.append(formatBuffer.get(formatBuffer.position()));
              }
              formatBuffer.position(formatBuffer.position() + 1);
          }
      }
  
      private boolean handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime,
              ZoneInfo zoneInfo) {
  
          // The char at formatBuffer.position() is expected to be '%' at this point.
          int modifier = 0;
          while (formatBuffer.remaining() > 1) {
              // Increment the position then get the new current char.
              formatBuffer.position(formatBuffer.position() + 1);
              char currentChar = formatBuffer.get(formatBuffer.position());
              switch (currentChar) {
                  case 'A':
                      modifyAndAppend((wallTime.getWeekDay() < 0
                                      || wallTime.getWeekDay() >= DAYSPERWEEK)
                                      ? "?" : localeData.longWeekdayNames[wallTime.getWeekDay() + 1],
                              modifier);
                      return false;
                  case 'a':
                      modifyAndAppend((wallTime.getWeekDay() < 0
                                      || wallTime.getWeekDay() >= DAYSPERWEEK)
                                      ? "?" : localeData.shortWeekdayNames[wallTime.getWeekDay() + 1],
                              modifier);
                      return false;
                  case 'B':
                      if (modifier == '-') {
                          modifyAndAppend((wallTime.getMonth() < 0
                                          || wallTime.getMonth() >= MONSPERYEAR)
                                          ? "?"
                                          : localeData.longStandAloneMonthNames[wallTime.getMonth()],
                                  modifier);
                      } else {
                          modifyAndAppend((wallTime.getMonth() < 0
                                          || wallTime.getMonth() >= MONSPERYEAR)
                                          ? "?" : localeData.longMonthNames[wallTime.getMonth()],
                                  modifier);
                      }
                      return false;
                  case 'b':
                  case 'h':
                      modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
                                      ? "?" : localeData.shortMonthNames[wallTime.getMonth()],
                              modifier);
                      return false;
                  case 'C':
                      outputYear(wallTime.getYear(), true, false, modifier);
                      return false;
                  case 'c':
                      formatInternal(dateTimeFormat, wallTime, zoneInfo);
                      return false;
                  case 'D':
                      formatInternal("%m/%d/%y", wallTime, zoneInfo);
                      return false;
                  case 'd':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              wallTime.getMonthDay());
                      return false;
                  case 'E':
                  case 'O':
                      // C99 locale modifiers are not supported.
                      continue;
                  case '_':
                  case '-':
                  case '0':
                  case '^':
                  case '#':
                      modifier = currentChar;
                      continue;
                  case 'e':
                      numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                              wallTime.getMonthDay());
                      return false;
                  case 'F':
                      formatInternal("%Y-%m-%d", wallTime, zoneInfo);
                      return false;
                  case 'H':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              wallTime.getHour());
                      return false;
                  case 'I':
                      int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
                      return false;
                  case 'j':
                      int yearDay = wallTime.getYearDay() + 1;
                      numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
                              yearDay);
                      return false;
                  case 'k':
                      numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                              wallTime.getHour());
                      return false;
                  case 'l':
                      int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
                      numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
                      return false;
                  case 'M':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              wallTime.getMinute());
                      return false;
                  case 'm':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              wallTime.getMonth() + 1);
                      return false;
                  case 'n':
                      outputBuilder.append('\n');
                      return false;
                  case 'p':
                      modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
                              : localeData.amPm[0], modifier);
                      return false;
                  case 'P':
                      modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
                              : localeData.amPm[0], FORCE_LOWER_CASE);
                      return false;
                  case 'R':
                      formatInternal("%H:%M", wallTime, zoneInfo);
                      return false;
                  case 'r':
                      formatInternal("%I:%M:%S %p", wallTime, zoneInfo);
                      return false;
                  case 'S':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              wallTime.getSecond());
                      return false;
                  case 's':
                      int timeInSeconds = wallTime.mktime(zoneInfo);
                      outputBuilder.append(Integer.toString(timeInSeconds));
                      return false;
                  case 'T':
                      formatInternal("%H:%M:%S", wallTime, zoneInfo);
                      return false;
                  case 't':
                      outputBuilder.append('\t');
                      return false;
                  case 'U':
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                              (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay())
                                      / DAYSPERWEEK);
                      return false;
                  case 'u':
                      int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay();
                      numberFormatter.format("%d", day);
                      return false;
                  case 'V':   /* ISO 8601 week number */
                  case 'G':   /* ISO 8601 year (four digits) */
                  case 'g':   /* ISO 8601 year (two digits) */
                  {
                      int year = wallTime.getYear();
                      int yday = wallTime.getYearDay();
                      int wday = wallTime.getWeekDay();
                      int w;
                      while (true) {
                          int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
                          // What yday (-3 ... 3) does the ISO year begin on?
                          int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3;
                          // What yday does the NEXT ISO year begin on?
                          int top = bot - (len % DAYSPERWEEK);
                          if (top < -3) {
                              top += DAYSPERWEEK;
                          }
                          top += len;
                          if (yday >= top) {
                              ++year;
                              w = 1;
                              break;
                          }
                          if (yday >= bot) {
                              w = 1 + ((yday - bot) / DAYSPERWEEK);
                              break;
                          }
                          --year;
                          yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
                      }
                      if (currentChar == 'V') {
                          numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
                      } else if (currentChar == 'g') {
                          outputYear(year, false, true, modifier);
                      } else {
                          outputYear(year, true, true, modifier);
                      }
                      return false;
                  }
                  case 'v':
                      formatInternal("%e-%b-%Y", wallTime, zoneInfo);
                      return false;
                  case 'W':
                      int n = (wallTime.getYearDay() + DAYSPERWEEK - (
                                      wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1)
                                              : (DAYSPERWEEK - 1))) / DAYSPERWEEK;
                      numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
                      return false;
                  case 'w':
                      numberFormatter.format("%d", wallTime.getWeekDay());
                      return false;
                  case 'X':
                      formatInternal(timeOnlyFormat, wallTime, zoneInfo);
                      return false;
                  case 'x':
                      formatInternal(dateOnlyFormat, wallTime, zoneInfo);
                      return false;
                  case 'y':
                      outputYear(wallTime.getYear(), false, true, modifier);
                      return false;
                  case 'Y':
                      outputYear(wallTime.getYear(), true, true, modifier);
                      return false;
                  case 'Z':
                      if (wallTime.getIsDst() < 0) {
                          return false;
                      }
                      boolean isDst = wallTime.getIsDst() != 0;
                      modifyAndAppend(zoneInfo.getDisplayName(isDst, TimeZone.SHORT), modifier);
                      return false;
                  case 'z': {
                      if (wallTime.getIsDst() < 0) {
                          return false;
                      }
                      int diff = wallTime.getGmtOffset();
                      char sign;
                      if (diff < 0) {
                          sign = '-';
                          diff = -diff;
                      } else {
                          sign = '+';
                      }
                      outputBuilder.append(sign);
                      diff /= SECSPERMIN;
                      diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
                      numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
                      return false;
                  }
                  case '+':
                      formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfo);
                      return false;
                  case '%':
                      // If conversion char is undefined, behavior is undefined. Print out the
                      // character itself.
                  default:
                      return true;
              }
          }
          return true;
      }
  
      private void modifyAndAppend(CharSequence str, int modifier) {
          switch (modifier) {
              case FORCE_LOWER_CASE:
                  for (int i = 0; i < str.length(); i++) {
                      outputBuilder.append(brokenToLower(str.charAt(i)));
                  }
                  break;
              case '^':
                  for (int i = 0; i < str.length(); i++) {
                      outputBuilder.append(brokenToUpper(str.charAt(i)));
                  }
                  break;
              case '#':
                  for (int i = 0; i < str.length(); i++) {
                      char c = str.charAt(i);
                      if (brokenIsUpper(c)) {
                          c = brokenToLower(c);
                      } else if (brokenIsLower(c)) {
                          c = brokenToUpper(c);
                      }
                      outputBuilder.append(c);
                  }
                  break;
              default:
                  outputBuilder.append(str);
          }
      }
  
      private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) {
          int lead;
          int trail;
  
          final int DIVISOR = 100;
          trail = value % DIVISOR;
          lead = value / DIVISOR + trail / DIVISOR;
          trail %= DIVISOR;
          if (trail < 0 && lead > 0) {
              trail += DIVISOR;
              --lead;
          } else if (lead < 0 && trail > 0) {
              trail -= DIVISOR;
              ++lead;
          }
          if (outputTop) {
              if (lead == 0 && trail < 0) {
                  outputBuilder.append("-0");
              } else {
                  numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
              }
          }
          if (outputBottom) {
              int n = ((trail < 0) ? -trail : trail);
              numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
          }
      }
  
      private static String getFormat(int modifier, String normal, String underscore, String dash,
              String zero) {
          switch (modifier) {
              case '_':
                  return underscore;
              case '-':
                  return dash;
              case '0':
                  return zero;
          }
          return normal;
      }
  
      private static boolean isLeap(int year) {
          return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
      }
  
      /**
       * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII codes in
       * order to be compatible with the old native implementation.
       */
      private static boolean brokenIsUpper(char toCheck) {
          return toCheck >= 'A' && toCheck <= 'Z';
      }
  
      /**
       * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII codes in
       * order to be compatible with the old native implementation.
       */
      private static boolean brokenIsLower(char toCheck) {
          return toCheck >= 'a' && toCheck <= 'z';
      }
  
      /**
       * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII codes in
       * order to be compatible with the old native implementation.
       */
      private static char brokenToLower(char input) {
          if (input >= 'A' && input <= 'Z') {
              return (char) (input - 'A' + 'a');
          }
          return input;
      }
  
      /**
       * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII codes in
       * order to be compatible with the old native implementation.
       */
      private static char brokenToUpper(char input) {
          if (input >= 'a' && input <= 'z') {
              return (char) (input - 'a' + 'A');
          }
          return input;
      }
  
  }
  