TimeFormatter.java revision 3b852e3489e995600fce19dfdbf3a5d769374d74
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*/ 21package android.text.format; 22 23import android.content.res.Resources; 24 25import java.nio.ByteBuffer; 26import java.nio.charset.StandardCharsets; 27import java.util.Formatter; 28import java.util.Locale; 29import java.util.TimeZone; 30import libcore.icu.LocaleData; 31import libcore.util.ZoneInfo; 32 33/** 34 * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java. The 35 * main issue with this implementation is the treatment of characters as ASCII, despite returning 36 * localized (UTF-16) strings from the LocaleData. 37 * 38 * <p>This class is not thread safe. 39 */ 40class TimeFormatter { 41 // An arbitrary value outside the range representable by a byte / ASCII character code. 42 private static final int FORCE_LOWER_CASE = 0x100; 43 44 private static final int SECSPERMIN = 60; 45 private static final int MINSPERHOUR = 60; 46 private static final int DAYSPERWEEK = 7; 47 private static final int MONSPERYEAR = 12; 48 private static final int HOURSPERDAY = 24; 49 private static final int DAYSPERLYEAR = 366; 50 private static final int DAYSPERNYEAR = 365; 51 52 /** 53 * The Locale for which the cached LocaleData and formats have been loaded. 54 */ 55 private static Locale sLocale; 56 private static LocaleData sLocaleData; 57 private static String sTimeOnlyFormat; 58 private static String sDateOnlyFormat; 59 private static String sDateTimeFormat; 60 61 private final LocaleData localeData; 62 private final String dateTimeFormat; 63 private final String timeOnlyFormat; 64 private final String dateOnlyFormat; 65 private final Locale locale; 66 67 private StringBuilder outputBuilder; 68 private Formatter outputFormatter; 69 70 public TimeFormatter() { 71 synchronized (TimeFormatter.class) { 72 Locale locale = Locale.getDefault(); 73 74 if (sLocale == null || !(locale.equals(sLocale))) { 75 sLocale = locale; 76 sLocaleData = LocaleData.get(locale); 77 78 Resources r = Resources.getSystem(); 79 sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day); 80 sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year); 81 sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time); 82 } 83 84 this.dateTimeFormat = sDateTimeFormat; 85 this.timeOnlyFormat = sTimeOnlyFormat; 86 this.dateOnlyFormat = sDateOnlyFormat; 87 this.locale = locale; 88 localeData = sLocaleData; 89 } 90 } 91 92 /** 93 * Format the specified {@code wallTime} using {@code pattern}. The output is returned. 94 */ 95 public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) { 96 try { 97 StringBuilder stringBuilder = new StringBuilder(); 98 99 outputBuilder = stringBuilder; 100 outputFormatter = new Formatter(stringBuilder, locale); 101 102 formatInternal(pattern, wallTime, zoneInfo); 103 String result = stringBuilder.toString(); 104 // This behavior is the source of a bug since some formats are defined as being 105 // in ASCII. Generally localization is very broken. 106 if (localeData.zeroDigit != '0') { 107 result = localizeDigits(result); 108 } 109 return result; 110 } finally { 111 outputBuilder = null; 112 outputFormatter = null; 113 } 114 } 115 116 private String localizeDigits(String s) { 117 int length = s.length(); 118 int offsetToLocalizedDigits = localeData.zeroDigit - '0'; 119 StringBuilder result = new StringBuilder(length); 120 for (int i = 0; i < length; ++i) { 121 char ch = s.charAt(i); 122 if (ch >= '0' && ch <= '9') { 123 ch += offsetToLocalizedDigits; 124 } 125 result.append(ch); 126 } 127 return result.toString(); 128 } 129 130 /** 131 * Format the specified {@code wallTime} using {@code pattern}. The output is written to 132 * {@link #outputBuilder}. 133 */ 134 private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) { 135 // Convert to ASCII bytes to be compatible with old implementation behavior. 136 byte[] bytes = pattern.getBytes(StandardCharsets.US_ASCII); 137 if (bytes.length == 0) { 138 return; 139 } 140 141 ByteBuffer formatBuffer = ByteBuffer.wrap(bytes); 142 while (formatBuffer.remaining() > 0) { 143 boolean outputCurrentByte = true; 144 char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position())); 145 if (currentByteAsChar == '%') { 146 outputCurrentByte = handleToken(formatBuffer, wallTime, zoneInfo); 147 } 148 if (outputCurrentByte) { 149 currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position())); 150 outputBuilder.append(currentByteAsChar); 151 } 152 153 formatBuffer.position(formatBuffer.position() + 1); 154 } 155 } 156 157 private boolean handleToken(ByteBuffer formatBuffer, ZoneInfo.WallTime wallTime, 158 ZoneInfo zoneInfo) { 159 160 // The byte at formatBuffer.position() is expected to be '%' at this point. 161 int modifier = 0; 162 while (formatBuffer.remaining() > 1) { 163 // Increment the position then get the new current byte. 164 formatBuffer.position(formatBuffer.position() + 1); 165 char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position())); 166 switch (currentByteAsChar) { 167 case 'A': 168 modifyAndAppend((wallTime.getWeekDay() < 0 169 || wallTime.getWeekDay() >= DAYSPERWEEK) 170 ? "?" : localeData.longWeekdayNames[wallTime.getWeekDay() + 1], 171 modifier); 172 return false; 173 case 'a': 174 modifyAndAppend((wallTime.getWeekDay() < 0 175 || wallTime.getWeekDay() >= DAYSPERWEEK) 176 ? "?" : localeData.shortWeekdayNames[wallTime.getWeekDay() + 1], 177 modifier); 178 return false; 179 case 'B': 180 if (modifier == '-') { 181 modifyAndAppend((wallTime.getMonth() < 0 182 || wallTime.getMonth() >= MONSPERYEAR) 183 ? "?" 184 : localeData.longStandAloneMonthNames[wallTime.getMonth()], 185 modifier); 186 } else { 187 modifyAndAppend((wallTime.getMonth() < 0 188 || wallTime.getMonth() >= MONSPERYEAR) 189 ? "?" : localeData.longMonthNames[wallTime.getMonth()], 190 modifier); 191 } 192 return false; 193 case 'b': 194 case 'h': 195 modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR) 196 ? "?" : localeData.shortMonthNames[wallTime.getMonth()], 197 modifier); 198 return false; 199 case 'C': 200 outputYear(wallTime.getYear(), true, false, modifier); 201 return false; 202 case 'c': 203 formatInternal(dateTimeFormat, wallTime, zoneInfo); 204 return false; 205 case 'D': 206 formatInternal("%m/%d/%y", wallTime, zoneInfo); 207 return false; 208 case 'd': 209 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 210 wallTime.getMonthDay()); 211 return false; 212 case 'E': 213 case 'O': 214 // C99 locale modifiers are not supported. 215 continue; 216 case '_': 217 case '-': 218 case '0': 219 case '^': 220 case '#': 221 modifier = currentByteAsChar; 222 continue; 223 case 'e': 224 outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), 225 wallTime.getMonthDay()); 226 return false; 227 case 'F': 228 formatInternal("%Y-%m-%d", wallTime, zoneInfo); 229 return false; 230 case 'H': 231 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 232 wallTime.getHour()); 233 return false; 234 case 'I': 235 int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; 236 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour); 237 return false; 238 case 'j': 239 int yearDay = wallTime.getYearDay() + 1; 240 outputFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"), 241 yearDay); 242 return false; 243 case 'k': 244 outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), 245 wallTime.getHour()); 246 return false; 247 case 'l': 248 int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; 249 outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2); 250 return false; 251 case 'M': 252 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 253 wallTime.getMinute()); 254 return false; 255 case 'm': 256 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 257 wallTime.getMonth() + 1); 258 return false; 259 case 'n': 260 modifyAndAppend("\n", modifier); 261 return false; 262 case 'p': 263 modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1] 264 : localeData.amPm[0], modifier); 265 return false; 266 case 'P': 267 modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1] 268 : localeData.amPm[0], FORCE_LOWER_CASE); 269 return false; 270 case 'R': 271 formatInternal("%H:%M", wallTime, zoneInfo); 272 return false; 273 case 'r': 274 formatInternal("%I:%M:%S %p", wallTime, zoneInfo); 275 return false; 276 case 'S': 277 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 278 wallTime.getSecond()); 279 return false; 280 case 's': 281 int timeInSeconds = wallTime.mktime(zoneInfo); 282 modifyAndAppend(Integer.toString(timeInSeconds), modifier); 283 return false; 284 case 'T': 285 formatInternal("%H:%M:%S", wallTime, zoneInfo); 286 return false; 287 case 't': 288 modifyAndAppend("\t", modifier); 289 return false; 290 case 'U': 291 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), 292 (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay()) 293 / DAYSPERWEEK); 294 return false; 295 case 'u': 296 int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay(); 297 outputFormatter.format("%d", day); 298 return false; 299 case 'V': /* ISO 8601 week number */ 300 case 'G': /* ISO 8601 year (four digits) */ 301 case 'g': /* ISO 8601 year (two digits) */ 302 { 303 int year = wallTime.getYear(); 304 int yday = wallTime.getYearDay(); 305 int wday = wallTime.getWeekDay(); 306 int w; 307 while (true) { 308 int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; 309 // What yday (-3 ... 3) does the ISO year begin on? 310 int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3; 311 // What yday does the NEXT ISO year begin on? 312 int top = bot - (len % DAYSPERWEEK); 313 if (top < -3) { 314 top += DAYSPERWEEK; 315 } 316 top += len; 317 if (yday >= top) { 318 ++year; 319 w = 1; 320 break; 321 } 322 if (yday >= bot) { 323 w = 1 + ((yday - bot) / DAYSPERWEEK); 324 break; 325 } 326 --year; 327 yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; 328 } 329 if (currentByteAsChar == 'V') { 330 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w); 331 } else if (currentByteAsChar == 'g') { 332 outputYear(year, false, true, modifier); 333 } else { 334 outputYear(year, true, true, modifier); 335 } 336 return false; 337 } 338 case 'v': 339 formatInternal("%e-%b-%Y", wallTime, zoneInfo); 340 return false; 341 case 'W': 342 int n = (wallTime.getYearDay() + DAYSPERWEEK - ( 343 wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1) 344 : (DAYSPERWEEK - 1))) / DAYSPERWEEK; 345 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); 346 return false; 347 case 'w': 348 outputFormatter.format("%d", wallTime.getWeekDay()); 349 return false; 350 case 'X': 351 formatInternal(timeOnlyFormat, wallTime, zoneInfo); 352 return false; 353 case 'x': 354 formatInternal(dateOnlyFormat, wallTime, zoneInfo); 355 return false; 356 case 'y': 357 outputYear(wallTime.getYear(), false, true, modifier); 358 return false; 359 case 'Y': 360 outputYear(wallTime.getYear(), true, true, modifier); 361 return false; 362 case 'Z': 363 if (wallTime.getIsDst() < 0) { 364 return false; 365 } 366 boolean isDst = wallTime.getIsDst() != 0; 367 modifyAndAppend(zoneInfo.getDisplayName(isDst, TimeZone.SHORT), modifier); 368 return false; 369 case 'z': { 370 if (wallTime.getIsDst() < 0) { 371 return false; 372 } 373 int diff = wallTime.getGmtOffset(); 374 String sign; 375 if (diff < 0) { 376 sign = "-"; 377 diff = -diff; 378 } else { 379 sign = "+"; 380 } 381 modifyAndAppend(sign, modifier); 382 diff /= SECSPERMIN; 383 diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR); 384 outputFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff); 385 return false; 386 } 387 case '+': 388 formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfo); 389 return false; 390 case '%': 391 // If conversion char is undefined, behavior is undefined. Print out the 392 // character itself. 393 default: 394 return true; 395 } 396 } 397 return true; 398 } 399 400 private void modifyAndAppend(CharSequence str, int modifier) { 401 switch (modifier) { 402 case FORCE_LOWER_CASE: 403 for (int i = 0; i < str.length(); i++) { 404 outputBuilder.append(brokenToLower(str.charAt(i))); 405 } 406 break; 407 case '^': 408 for (int i = 0; i < str.length(); i++) { 409 outputBuilder.append(brokenToUpper(str.charAt(i))); 410 } 411 break; 412 case '#': 413 for (int i = 0; i < str.length(); i++) { 414 char c = str.charAt(i); 415 if (brokenIsUpper(c)) { 416 c = brokenToLower(c); 417 } else if (brokenIsLower(c)) { 418 c = brokenToUpper(c); 419 } 420 outputBuilder.append(c); 421 } 422 break; 423 default: 424 outputBuilder.append(str); 425 426 } 427 } 428 429 private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) { 430 int lead; 431 int trail; 432 433 final int DIVISOR = 100; 434 trail = value % DIVISOR; 435 lead = value / DIVISOR + trail / DIVISOR; 436 trail %= DIVISOR; 437 if (trail < 0 && lead > 0) { 438 trail += DIVISOR; 439 --lead; 440 } else if (lead < 0 && trail > 0) { 441 trail -= DIVISOR; 442 ++lead; 443 } 444 if (outputTop) { 445 if (lead == 0 && trail < 0) { 446 modifyAndAppend("-0", modifier); 447 } else { 448 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead); 449 } 450 } 451 if (outputBottom) { 452 int n = ((trail < 0) ? -trail : trail); 453 outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); 454 } 455 } 456 457 private static String getFormat(int modifier, String normal, String underscore, String dash, 458 String zero) { 459 switch (modifier) { 460 case '_': 461 return underscore; 462 case '-': 463 return dash; 464 case '0': 465 return zero; 466 } 467 return normal; 468 } 469 470 private static boolean isLeap(int year) { 471 return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)); 472 } 473 474 /** 475 * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII in order to 476 * be compatible with the old native implementation. 477 */ 478 private static boolean brokenIsUpper(char toCheck) { 479 return toCheck >= 'A' && toCheck <= 'Z'; 480 } 481 482 /** 483 * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII in order to 484 * be compatible with the old native implementation. 485 */ 486 private static boolean brokenIsLower(char toCheck) { 487 return toCheck >= 'a' && toCheck <= 'z'; 488 } 489 490 /** 491 * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII in order to 492 * be compatible with the old native implementation. 493 */ 494 private static char brokenToLower(char input) { 495 if (input >= 'A' && input <= 'Z') { 496 return (char) (input - 'A' + 'a'); 497 } 498 return input; 499 } 500 501 /** 502 * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII in order to 503 * be compatible with the old native implementation. 504 */ 505 private static char brokenToUpper(char input) { 506 if (input >= 'a' && input <= 'z') { 507 return (char) (input - 'a' + 'A'); 508 } 509 return input; 510 } 511 512 /** 513 * Safely convert a byte containing an ASCII character to a char, even for character codes 514 * > 127. 515 */ 516 private static char convertToChar(byte b) { 517 return (char) (b & 0xFF); 518 } 519} 520