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