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