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