1 /* 2 * Based on the UCB version of strftime.c with the copyright notice appearing below. 3 */ 4 5 /* 6 ** Copyright (c) 1989 The Regents of the University of California. 7 ** All rights reserved. 8 ** 9 ** Redistribution and use in source and binary forms are permitted 10 ** provided that the above copyright notice and this paragraph are 11 ** duplicated in all such forms and that any documentation, 12 ** advertising materials, and other materials related to such 13 ** distribution and use acknowledge that the software was developed 14 ** by the University of California, Berkeley. The name of the 15 ** University may not be used to endorse or promote products derived 16 ** from this software without specific prior written permission. 17 ** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 18 ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 19 ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 20 */ 21 package android.text.format; 22 23 import android.content.res.Resources; 24 import android.icu.text.DateFormatSymbols; 25 import android.icu.text.DecimalFormatSymbols; 26 27 import com.android.i18n.timezone.WallTime; 28 import com.android.i18n.timezone.ZoneInfoData; 29 30 import java.nio.CharBuffer; 31 import java.time.Instant; 32 import java.time.LocalDateTime; 33 import java.time.ZoneId; 34 import java.util.Formatter; 35 import java.util.Locale; 36 import java.util.TimeZone; 37 38 /** 39 * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java. 40 * 41 * <p>This class is not thread safe. 42 */ 43 class TimeFormatter { 44 // An arbitrary value outside the range representable by a char. 45 private static final int FORCE_LOWER_CASE = -1; 46 47 private static final int SECSPERMIN = 60; 48 private static final int MINSPERHOUR = 60; 49 private static final int DAYSPERWEEK = 7; 50 private static final int MONSPERYEAR = 12; 51 private static final int HOURSPERDAY = 24; 52 private static final int DAYSPERLYEAR = 366; 53 private static final int DAYSPERNYEAR = 365; 54 55 /** 56 * The Locale for which the cached symbols and formats have been loaded. 57 */ 58 private static Locale sLocale; 59 private static DateFormatSymbols sDateFormatSymbols; 60 private static DecimalFormatSymbols sDecimalFormatSymbols; 61 private static String sTimeOnlyFormat; 62 private static String sDateOnlyFormat; 63 private static String sDateTimeFormat; 64 65 private final DateFormatSymbols dateFormatSymbols; 66 private final DecimalFormatSymbols decimalFormatSymbols; 67 private final String dateTimeFormat; 68 private final String timeOnlyFormat; 69 private final String dateOnlyFormat; 70 71 private StringBuilder outputBuilder; 72 private Formatter numberFormatter; 73 TimeFormatter()74 public TimeFormatter() { 75 synchronized (TimeFormatter.class) { 76 Locale locale = Locale.getDefault(); 77 78 if (sLocale == null || !(locale.equals(sLocale))) { 79 sLocale = locale; 80 sDateFormatSymbols = DateFormat.getIcuDateFormatSymbols(locale); 81 sDecimalFormatSymbols = DecimalFormatSymbols.getInstance(locale); 82 83 Resources r = Resources.getSystem(); 84 sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day); 85 sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year); 86 sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time); 87 } 88 89 this.dateFormatSymbols = sDateFormatSymbols; 90 this.decimalFormatSymbols = sDecimalFormatSymbols; 91 this.dateTimeFormat = sDateTimeFormat; 92 this.timeOnlyFormat = sTimeOnlyFormat; 93 this.dateOnlyFormat = sDateOnlyFormat; 94 } 95 } 96 97 /** 98 * The implementation of {@link TimeMigrationUtils#formatMillisWithFixedFormat(long)} for 99 * 2038-safe formatting with the pattern "%Y-%m-%d %H:%M:%S" and including the historic 100 * incorrect digit localization behavior. 101 */ formatMillisWithFixedFormat(long timeMillis)102 String formatMillisWithFixedFormat(long timeMillis) { 103 // This method is deliberately not a general purpose replacement for format(String, 104 // ZoneInfoData.WallTime, ZoneInfoData): It hard-codes the pattern used; many of the 105 // pattern characters supported by Time.format() have unusual behavior which would make 106 // using java.time.format or similar packages difficult. It would be a lot of work to share 107 // behavior and many internal Android usecases can be covered by this common pattern 108 // behavior. 109 110 // No need to worry about overflow / underflow: long millis is representable by Instant and 111 // LocalDateTime with room to spare. 112 Instant instant = Instant.ofEpochMilli(timeMillis); 113 114 // Date/times are calculated in the current system default time zone. 115 LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); 116 117 // You'd think it would be as simple as: 118 // DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", locale); 119 // return formatter.format(localDateTime); 120 // but we retain Time's behavior around digits. 121 122 StringBuilder stringBuilder = new StringBuilder(19); 123 124 // This effectively uses the US locale because number localization is handled separately 125 // (see below). 126 stringBuilder.append(localDateTime.getYear()); 127 stringBuilder.append('-'); 128 append2DigitNumber(stringBuilder, localDateTime.getMonthValue()); 129 stringBuilder.append('-'); 130 append2DigitNumber(stringBuilder, localDateTime.getDayOfMonth()); 131 stringBuilder.append(' '); 132 append2DigitNumber(stringBuilder, localDateTime.getHour()); 133 stringBuilder.append(':'); 134 append2DigitNumber(stringBuilder, localDateTime.getMinute()); 135 stringBuilder.append(':'); 136 append2DigitNumber(stringBuilder, localDateTime.getSecond()); 137 138 String result = stringBuilder.toString(); 139 return localizeDigits(result); 140 } 141 142 /** Zero-pads value as needed to achieve a 2-digit number. */ append2DigitNumber(StringBuilder builder, int value)143 private static void append2DigitNumber(StringBuilder builder, int value) { 144 if (value < 10) { 145 builder.append('0'); 146 } 147 builder.append(value); 148 } 149 150 /** 151 * Format the specified {@code wallTime} using {@code pattern}. The output is returned. 152 */ format(String pattern, WallTime wallTime, ZoneInfoData zoneInfoData)153 public String format(String pattern, WallTime wallTime, 154 ZoneInfoData zoneInfoData) { 155 try { 156 StringBuilder stringBuilder = new StringBuilder(); 157 158 outputBuilder = stringBuilder; 159 // This uses the US locale because number localization is handled separately (see below) 160 // and locale sensitive strings are output directly using outputBuilder. 161 numberFormatter = new Formatter(stringBuilder, Locale.US); 162 163 formatInternal(pattern, wallTime, zoneInfoData); 164 String result = stringBuilder.toString(); 165 // The localizeDigits() behavior is the source of a bug since some formats are defined 166 // as being in ASCII and not localized. 167 return localizeDigits(result); 168 } finally { 169 outputBuilder = null; 170 numberFormatter = null; 171 } 172 } 173 localizeDigits(String s)174 private String localizeDigits(String s) { 175 if (decimalFormatSymbols.getZeroDigit() == '0') { 176 return s; 177 } 178 179 int length = s.length(); 180 int offsetToLocalizedDigits = decimalFormatSymbols.getZeroDigit() - '0'; 181 StringBuilder result = new StringBuilder(length); 182 for (int i = 0; i < length; ++i) { 183 char ch = s.charAt(i); 184 if (ch >= '0' && ch <= '9') { 185 ch += offsetToLocalizedDigits; 186 } 187 result.append(ch); 188 } 189 return result.toString(); 190 } 191 192 /** 193 * Format the specified {@code wallTime} using {@code pattern}. The output is written to 194 * {@link #outputBuilder}. 195 */ formatInternal(String pattern, WallTime wallTime, ZoneInfoData zoneInfoData)196 private void formatInternal(String pattern, WallTime wallTime, 197 ZoneInfoData zoneInfoData) { 198 CharBuffer formatBuffer = CharBuffer.wrap(pattern); 199 while (formatBuffer.remaining() > 0) { 200 boolean outputCurrentChar = true; 201 char currentChar = formatBuffer.get(formatBuffer.position()); 202 if (currentChar == '%') { 203 outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfoData); 204 } 205 if (outputCurrentChar) { 206 outputBuilder.append(formatBuffer.get(formatBuffer.position())); 207 } 208 formatBuffer.position(formatBuffer.position() + 1); 209 } 210 } 211 handleToken(CharBuffer formatBuffer, WallTime wallTime, ZoneInfoData zoneInfoData)212 private boolean handleToken(CharBuffer formatBuffer, WallTime wallTime, 213 ZoneInfoData zoneInfoData) { 214 215 // The char at formatBuffer.position() is expected to be '%' at this point. 216 int modifier = 0; 217 while (formatBuffer.remaining() > 1) { 218 // Increment the position then get the new current char. 219 formatBuffer.position(formatBuffer.position() + 1); 220 char currentChar = formatBuffer.get(formatBuffer.position()); 221 switch (currentChar) { 222 case 'A': 223 modifyAndAppend( 224 (wallTime.getWeekDay() < 0 || wallTime.getWeekDay() >= DAYSPERWEEK) 225 ? "?" 226 : dateFormatSymbols.getWeekdays(DateFormatSymbols.FORMAT, 227 DateFormatSymbols.WIDE)[wallTime.getWeekDay() + 1], 228 modifier); 229 return false; 230 case 'a': 231 modifyAndAppend( 232 (wallTime.getWeekDay() < 0 || wallTime.getWeekDay() >= DAYSPERWEEK) 233 ? "?" 234 : dateFormatSymbols.getWeekdays(DateFormatSymbols.FORMAT, 235 DateFormatSymbols.ABBREVIATED)[wallTime.getWeekDay() + 1], 236 modifier); 237 return false; 238 case 'B': 239 if (modifier == '-') { 240 modifyAndAppend( 241 (wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR) 242 ? "?" 243 : dateFormatSymbols.getMonths(DateFormatSymbols.STANDALONE, 244 DateFormatSymbols.WIDE)[wallTime.getMonth()], 245 modifier); 246 } else { 247 modifyAndAppend( 248 (wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR) 249 ? "?" 250 : dateFormatSymbols.getMonths(DateFormatSymbols.FORMAT, 251 DateFormatSymbols.WIDE)[wallTime.getMonth()], 252 modifier); 253 } 254 return false; 255 case 'b': 256 case 'h': 257 modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR) 258 ? "?" 259 : dateFormatSymbols.getMonths(DateFormatSymbols.FORMAT, 260 DateFormatSymbols.ABBREVIATED)[wallTime.getMonth()], 261 modifier); 262 return false; 263 case 'C': 264 outputYear(wallTime.getYear(), true, false, modifier); 265 return false; 266 case 'c': 267 formatInternal(dateTimeFormat, wallTime, zoneInfoData); 268 return false; 269 case 'D': 270 formatInternal("%m/%d/%y", wallTime, zoneInfoData); 271 return false; 272 case 'd': 273 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 274 wallTime.getMonthDay()); 275 return false; 276 case 'E': 277 case 'O': 278 // C99 locale modifiers are not supported. 279 continue; 280 case '_': 281 case '-': 282 case '0': 283 case '^': 284 case '#': 285 modifier = currentChar; 286 continue; 287 case 'e': 288 numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), 289 wallTime.getMonthDay()); 290 return false; 291 case 'F': 292 formatInternal("%Y-%m-%d", wallTime, zoneInfoData); 293 return false; 294 case 'H': 295 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 296 wallTime.getHour()); 297 return false; 298 case 'I': 299 int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; 300 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour); 301 return false; 302 case 'j': 303 int yearDay = wallTime.getYearDay() + 1; 304 numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"), 305 yearDay); 306 return false; 307 case 'k': 308 numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), 309 wallTime.getHour()); 310 return false; 311 case 'l': 312 int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; 313 numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2); 314 return false; 315 case 'M': 316 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 317 wallTime.getMinute()); 318 return false; 319 case 'm': 320 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 321 wallTime.getMonth() + 1); 322 return false; 323 case 'n': 324 outputBuilder.append('\n'); 325 return false; 326 case 'p': 327 modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) 328 ? dateFormatSymbols.getAmPmStrings()[1] 329 : dateFormatSymbols.getAmPmStrings()[0], modifier); 330 return false; 331 case 'P': 332 modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) 333 ? dateFormatSymbols.getAmPmStrings()[1] 334 : dateFormatSymbols.getAmPmStrings()[0], FORCE_LOWER_CASE); 335 return false; 336 case 'R': 337 formatInternal("%H:%M", wallTime, zoneInfoData); 338 return false; 339 case 'r': 340 formatInternal("%I:%M:%S %p", wallTime, zoneInfoData); 341 return false; 342 case 'S': 343 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 344 wallTime.getSecond()); 345 return false; 346 case 's': 347 int timeInSeconds = wallTime.mktime(zoneInfoData); 348 outputBuilder.append(Integer.toString(timeInSeconds)); 349 return false; 350 case 'T': 351 formatInternal("%H:%M:%S", wallTime, zoneInfoData); 352 return false; 353 case 't': 354 outputBuilder.append('\t'); 355 return false; 356 case 'U': 357 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 358 (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay()) 359 / DAYSPERWEEK); 360 return false; 361 case 'u': 362 int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay(); 363 numberFormatter.format("%d", day); 364 return false; 365 case 'V': /* ISO 8601 week number */ 366 case 'G': /* ISO 8601 year (four digits) */ 367 case 'g': /* ISO 8601 year (two digits) */ 368 { 369 int year = wallTime.getYear(); 370 int yday = wallTime.getYearDay(); 371 int wday = wallTime.getWeekDay(); 372 int w; 373 while (true) { 374 int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; 375 // What yday (-3 ... 3) does the ISO year begin on? 376 int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3; 377 // What yday does the NEXT ISO year begin on? 378 int top = bot - (len % DAYSPERWEEK); 379 if (top < -3) { 380 top += DAYSPERWEEK; 381 } 382 top += len; 383 if (yday >= top) { 384 ++year; 385 w = 1; 386 break; 387 } 388 if (yday >= bot) { 389 w = 1 + ((yday - bot) / DAYSPERWEEK); 390 break; 391 } 392 --year; 393 yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; 394 } 395 if (currentChar == 'V') { 396 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w); 397 } else if (currentChar == 'g') { 398 outputYear(year, false, true, modifier); 399 } else { 400 outputYear(year, true, true, modifier); 401 } 402 return false; 403 } 404 case 'v': 405 formatInternal("%e-%b-%Y", wallTime, zoneInfoData); 406 return false; 407 case 'W': 408 int n = (wallTime.getYearDay() + DAYSPERWEEK - ( 409 wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1) 410 : (DAYSPERWEEK - 1))) / DAYSPERWEEK; 411 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); 412 return false; 413 case 'w': 414 numberFormatter.format("%d", wallTime.getWeekDay()); 415 return false; 416 case 'X': 417 formatInternal(timeOnlyFormat, wallTime, zoneInfoData); 418 return false; 419 case 'x': 420 formatInternal(dateOnlyFormat, wallTime, zoneInfoData); 421 return false; 422 case 'y': 423 outputYear(wallTime.getYear(), false, true, modifier); 424 return false; 425 case 'Y': 426 outputYear(wallTime.getYear(), true, true, modifier); 427 return false; 428 case 'Z': 429 if (wallTime.getIsDst() < 0) { 430 return false; 431 } 432 boolean isDst = wallTime.getIsDst() != 0; 433 modifyAndAppend(TimeZone.getTimeZone(zoneInfoData.getID()) 434 .getDisplayName(isDst, TimeZone.SHORT), modifier); 435 return false; 436 case 'z': { 437 if (wallTime.getIsDst() < 0) { 438 return false; 439 } 440 int diff = wallTime.getGmtOffset(); 441 char sign; 442 if (diff < 0) { 443 sign = '-'; 444 diff = -diff; 445 } else { 446 sign = '+'; 447 } 448 outputBuilder.append(sign); 449 diff /= SECSPERMIN; 450 diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR); 451 numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff); 452 return false; 453 } 454 case '+': 455 formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfoData); 456 return false; 457 case '%': 458 // If conversion char is undefined, behavior is undefined. Print out the 459 // character itself. 460 default: 461 return true; 462 } 463 } 464 return true; 465 } 466 modifyAndAppend(CharSequence str, int modifier)467 private void modifyAndAppend(CharSequence str, int modifier) { 468 switch (modifier) { 469 case FORCE_LOWER_CASE: 470 for (int i = 0; i < str.length(); i++) { 471 outputBuilder.append(brokenToLower(str.charAt(i))); 472 } 473 break; 474 case '^': 475 for (int i = 0; i < str.length(); i++) { 476 outputBuilder.append(brokenToUpper(str.charAt(i))); 477 } 478 break; 479 case '#': 480 for (int i = 0; i < str.length(); i++) { 481 char c = str.charAt(i); 482 if (brokenIsUpper(c)) { 483 c = brokenToLower(c); 484 } else if (brokenIsLower(c)) { 485 c = brokenToUpper(c); 486 } 487 outputBuilder.append(c); 488 } 489 break; 490 default: 491 outputBuilder.append(str); 492 } 493 } 494 outputYear(int value, boolean outputTop, boolean outputBottom, int modifier)495 private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) { 496 int lead; 497 int trail; 498 499 final int DIVISOR = 100; 500 trail = value % DIVISOR; 501 lead = value / DIVISOR + trail / DIVISOR; 502 trail %= DIVISOR; 503 if (trail < 0 && lead > 0) { 504 trail += DIVISOR; 505 --lead; 506 } else if (lead < 0 && trail > 0) { 507 trail -= DIVISOR; 508 ++lead; 509 } 510 if (outputTop) { 511 if (lead == 0 && trail < 0) { 512 outputBuilder.append("-0"); 513 } else { 514 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead); 515 } 516 } 517 if (outputBottom) { 518 int n = ((trail < 0) ? -trail : trail); 519 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); 520 } 521 } 522 getFormat(int modifier, String normal, String underscore, String dash, String zero)523 private static String getFormat(int modifier, String normal, String underscore, String dash, 524 String zero) { 525 switch (modifier) { 526 case '_': 527 return underscore; 528 case '-': 529 return dash; 530 case '0': 531 return zero; 532 } 533 return normal; 534 } 535 isLeap(int year)536 private static boolean isLeap(int year) { 537 return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)); 538 } 539 540 /** 541 * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII codes in 542 * order to be compatible with the old native implementation. 543 */ brokenIsUpper(char toCheck)544 private static boolean brokenIsUpper(char toCheck) { 545 return toCheck >= 'A' && toCheck <= 'Z'; 546 } 547 548 /** 549 * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII codes in 550 * order to be compatible with the old native implementation. 551 */ brokenIsLower(char toCheck)552 private static boolean brokenIsLower(char toCheck) { 553 return toCheck >= 'a' && toCheck <= 'z'; 554 } 555 556 /** 557 * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII codes in 558 * order to be compatible with the old native implementation. 559 */ brokenToLower(char input)560 private static char brokenToLower(char input) { 561 if (input >= 'A' && input <= 'Z') { 562 return (char) (input - 'A' + 'a'); 563 } 564 return input; 565 } 566 567 /** 568 * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII codes in 569 * order to be compatible with the old native implementation. 570 */ brokenToUpper(char input)571 private static char brokenToUpper(char input) { 572 if (input >= 'a' && input <= 'z') { 573 return (char) (input - 'a' + 'A'); 574 } 575 return input; 576 } 577 578 } 579