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