1 package org.robolectric.util; 2 3 import java.text.SimpleDateFormat; 4 import java.util.Date; 5 import java.util.Locale; 6 import java.util.TimeZone; 7 8 /** 9 * An implementation of the Unix strftime with some glibc extensions. 10 */ 11 public class Strftime { 12 13 /** 14 * Format a date string. 15 * 16 * @param format The format in strftime syntax. 17 * @param date The date to format. 18 * @param locale The locale to use for formatting. 19 * @param zone The timezone to use for formatting. 20 * @return The formatted datetime. 21 */ format(String format, final Date date, Locale locale, TimeZone zone)22 public static String format(String format, final Date date, Locale locale, TimeZone zone) { 23 StringBuilder buffer = new StringBuilder(); 24 25 class Formatter { 26 SimpleDateFormat formatter; 27 28 public Formatter( 29 Date date, 30 Locale locale, 31 TimeZone timeZone) { 32 if (locale != null) { 33 formatter = new SimpleDateFormat("", locale); 34 } else { 35 formatter = new SimpleDateFormat(""); 36 } 37 if (timeZone != null) { 38 formatter.setTimeZone(timeZone); 39 } 40 } 41 42 public String format(String format) { 43 formatter.applyPattern(format); 44 return formatter.format(date); 45 } 46 } 47 48 Formatter formatter = new Formatter(date, locale, zone); 49 50 Boolean inside = false; 51 52 Boolean removePad = false; 53 Boolean zeroPad = false; 54 Boolean spacePad = false; 55 56 Boolean upperCase = false; 57 Boolean swapCase = false; 58 59 StringBuilder padWidthBuffer = new StringBuilder(); 60 61 for (int i = 0; i < format.length(); i++) { 62 Character c = format.charAt(i); 63 64 if (!inside && c == '%') { 65 inside = true; 66 removePad = false; 67 zeroPad = false; 68 spacePad = false; 69 upperCase = false; 70 swapCase = false; 71 padWidthBuffer = new StringBuilder(); 72 } else if(inside) { 73 inside = false; 74 switch (c) { 75 // %a Abbreviated weekday name according to locale. 76 case 'a': 77 buffer.append( 78 correctCase( 79 formatter.format("EEE"), 80 upperCase, 81 swapCase)); 82 break; 83 84 // %A Full weekday name according to locale. 85 case 'A': 86 buffer.append( 87 correctCase( 88 formatter.format("EEEE"), 89 upperCase, 90 swapCase)); 91 break; 92 93 // %b Abbreviated month name according to locale. 94 case 'b': 95 buffer.append( 96 correctCase( 97 formatter.format("MMM"), 98 upperCase, 99 swapCase)); 100 break; 101 102 // %B Full month name according to locale. 103 case 'B': 104 buffer.append( 105 correctCase( 106 formatter.format("MMMM"), 107 upperCase, 108 swapCase)); 109 break; 110 111 // %c Preferred date and time representation for locale. 112 case 'c': 113 // NOTE: en_US locale 114 buffer.append( 115 formatter.format("EEE dd MMM yyyy hh:mm:ss aa z")); 116 break; 117 118 // %C Year divided by 100 and truncated to integer (00-99). 119 case 'C': 120 buffer.append( 121 formatter.format("y").substring(0, 2)); 122 break; 123 124 // %d Day of the month as decimal number (01-31). 125 case 'd': 126 buffer.append( 127 formatter.format("dd")); 128 break; 129 130 // %D Same as "%m/%d/%y" 131 case 'D': 132 buffer.append( 133 formatter.format("MM/dd/yy")); 134 break; 135 136 // %e Day of the month as decimal number, padded with space. 137 case 'e': 138 buffer.append( 139 correctPad( 140 formatter.format("dd"), 141 zeroPad, 142 true, 143 removePad, 144 (padWidthBuffer.length() <= 0 145 ? new StringBuilder("2") 146 : padWidthBuffer))); 147 break; 148 149 // %E Modifier, use a locale-dependent alternative representation. 150 case 'E': 151 inside = true; 152 throw new UnsupportedOperationException("Not implemented yet"); 153 // break; 154 155 // %F ISO 8601 date format: "%Y-%m-%d". 156 case 'F': 157 buffer.append( 158 formatter.format("yyyy-MM-dd")); 159 break; 160 161 // %g 2-digit year version of %G, (00-99) 162 case 'g': 163 buffer.append( 164 formatter.format("YY")); 165 break; 166 167 // %G ISO 8601 week-based year. 168 case 'G': 169 buffer.append( 170 formatter.format("YYYY")); 171 break; 172 173 // %h Like %b. 174 case 'h': 175 buffer.append( 176 formatter.format("MMM")); 177 break; 178 179 // %H Hour (24-hour clock) as decimal number (00-23). 180 case 'H': 181 buffer.append( 182 formatter.format("HH")); 183 break; 184 185 // %I Hour (12-hour clock) as decimal number (01-12). 186 case 'I': 187 buffer.append( 188 formatter.format("hh")); 189 break; 190 191 // %j Day of the year as decimal number (001-366). 192 case 'j': 193 buffer.append( 194 formatter.format("DDD")); 195 break; 196 197 // %k Hour (24-hour clock) as decimal number (0-23), space padded. 198 case 'k': 199 buffer.append( 200 correctPad( 201 formatter.format("HH"), 202 zeroPad, 203 spacePad, 204 removePad, 205 (padWidthBuffer.length() <= 0 206 ? new StringBuilder("2") 207 : padWidthBuffer))); 208 break; 209 210 // %l Hour (12-hour clock) as decimal number (1-12), space padded. 211 case 'l': 212 buffer.append( 213 correctPad( 214 formatter.format("hh"), 215 zeroPad, 216 spacePad || !zeroPad, 217 removePad, 218 (padWidthBuffer.length() <= 0 219 ? new StringBuilder("2") 220 : padWidthBuffer))); 221 break; 222 223 // %m Month as decimal number (01-12). 224 case 'm': 225 buffer.append( 226 correctPad( 227 formatter.format("MM"), 228 zeroPad, 229 spacePad, 230 removePad, 231 (padWidthBuffer.length() <= 0 232 ? new StringBuilder("2") 233 : padWidthBuffer))); 234 break; 235 236 // %M Minute as decimal number (00-59). 237 case 'M': 238 buffer.append( 239 correctCase( 240 formatter.format("mm"), 241 upperCase, 242 swapCase)); 243 break; 244 245 // %n Newline. 246 case 'n': 247 buffer.append( 248 formatter.format("\n")); 249 break; 250 251 // %O Modifier, use alternative numeric symbols (say, Roman numerals). 252 case 'O': 253 inside = true; 254 throw new UnsupportedOperationException("Not implemented yet"); 255 // break; 256 257 // %p "AM", "PM", or locale string. Noon = "PM", midnight = "AM". 258 case 'p': 259 buffer.append( 260 correctCase( 261 formatter.format("a"), 262 upperCase, 263 swapCase)); 264 break; 265 266 // %P "am", "pm", or locale string. Noon = "pm", midnight = "am". 267 case 'P': 268 buffer.append( 269 correctCase( 270 formatter.format("a").toLowerCase(), 271 upperCase, 272 swapCase)); 273 break; 274 275 // %r 12-hour clock time. 276 case 'r': 277 buffer.append( 278 formatter.format("hh:mm:ss a")); 279 break; 280 281 // %R 24-hour clock time, "%H:%M". 282 case 'R': 283 buffer.append( 284 formatter.format("HH:mm")); 285 break; 286 287 // %s Number of seconds since Epoch, 1970-01-01 00:00:00 +0000 (UTC). 288 case 's': 289 buffer.append( 290 ((Long) (date.getTime() / 1000)).toString()); 291 break; 292 293 // %S Second as decimal number (00-60). 60 for leap seconds. 294 case 'S': 295 buffer.append( 296 formatter.format("ss")); 297 break; 298 299 // %t Tab. 300 case 't': 301 buffer.append( 302 formatter.format("\t")); 303 break; 304 305 // %T 24-hour time, "%H:%M:%S". 306 case 'T': 307 buffer.append( 308 formatter.format("HH:mm:ss")); 309 break; 310 311 // %u The day of the week as a decimal, (1-7). Monday being 1. 312 case 'u': 313 buffer.append( 314 formatter.format("u")); 315 break; 316 317 // %U week number of the current year as a decimal number, (00-53). 318 // Starting with the first Sunday as the first day of week 01. 319 case 'U': 320 throw new UnsupportedOperationException("Not implemented yet"); 321 // buffer.append( 322 // formatter.format("ww")); 323 // break; 324 325 // %V ISO 8601 week number (00-53). 326 // Week 1 is the first week that has at least 4 days in the new year. 327 case 'V': 328 buffer.append( 329 formatter.format("ww")); 330 break; 331 332 // %w Day of the week as a decimal, (0-6). Sunday being 0. 333 case 'w': 334 String dayNumberOfWeek = formatter.format("u"); // (1-7) 335 buffer.append( 336 (dayNumberOfWeek.equals("7") ? "0" : dayNumberOfWeek)); 337 break; 338 339 // %W Week number of the current year as a decimal number, (00-53). 340 // Starting with the first Monday as the first day of week 01. 341 case 'W': 342 throw new UnsupportedOperationException("Not implemented yet"); 343 // buffer.append( 344 // formatter.format("ww")); 345 // break; 346 347 // %x Locale date without time. 348 case 'x': 349 buffer.append( 350 formatter.format("MM/dd/yyyy")); 351 break; 352 353 // %X Locale time without date. 354 case 'X': 355 buffer.append( 356 formatter.format("hh:mm:ss aa")); 357 // buffer.append( 358 // formatter.format("HH:mm:ss")); 359 break; 360 361 // %y Year as decimal number without century (00-99). 362 case 'y': 363 buffer.append( 364 formatter.format("yy")); 365 break; 366 367 // %Y Year as decimal number with century. 368 case 'Y': 369 buffer.append( 370 formatter.format("yyyy")); 371 break; 372 373 // %z Numeric timezone as hour and minute offset from UTC "+hhmm" or "-hhmm". 374 case 'z': 375 buffer.append( 376 formatter.format("Z")); 377 break; 378 379 // %Z Timezone, name, or abbreviation. 380 case 'Z': 381 buffer.append( 382 formatter.format("z")); 383 break; 384 385 // %% Literal '%'. 386 case '%': 387 buffer.append( 388 formatter.format("%")); 389 break; 390 391 // glibc extension 392 393 // %^ Force upper case. 394 case '^': 395 inside = true; 396 upperCase = true; 397 break; 398 399 // %# Swap case. 400 case '#': 401 inside = true; 402 swapCase = true; 403 break; 404 405 // %- Remove padding. 406 case '-': 407 inside = true; 408 removePad = true; 409 break; 410 411 // %_ Space pad. 412 case '_': 413 inside = true; 414 spacePad = true; 415 break; 416 417 // %0 Zero pad. 418 // 0 Alternatively if preceded by another digit, defines padding width. 419 case '0': 420 inside = true; 421 if (padWidthBuffer.length() == 0) { 422 zeroPad = true; 423 spacePad = false; 424 } else { 425 padWidthBuffer.append(c); 426 } 427 break; 428 429 // %1 Padding width. 430 case '1': 431 case '2': 432 case '3': 433 case '4': 434 case '5': 435 case '6': 436 case '7': 437 case '8': 438 case '9': 439 inside = true; 440 // zeroPad = !spacePad; // Default to zero padding. 441 padWidthBuffer.append(c); 442 break; 443 444 default: 445 buffer.append(c.toString()); 446 break; 447 } 448 } else { 449 buffer.append(c.toString()); 450 } 451 } 452 453 return buffer.toString(); 454 } 455 correctCase( String simple, Boolean upperCase, Boolean swapCase)456 private static String correctCase( 457 String simple, 458 Boolean upperCase, 459 Boolean swapCase) { 460 if (upperCase) { 461 return simple.toUpperCase(); 462 } 463 464 if (!swapCase) { 465 return simple; 466 } 467 468 // swap case 469 StringBuilder buffer = new StringBuilder(); 470 for (int i = 0; i < simple.length(); i++) { 471 Character c = simple.charAt(i); 472 buffer.append( 473 (Character.isLowerCase(c) 474 ? Character.toUpperCase(c) 475 : Character.toLowerCase(c)) 476 ); 477 } 478 479 return buffer.toString(); 480 } 481 correctPad( String simple, Boolean zeroPad, Boolean spacePad, Boolean removePad, StringBuilder padWidthBuffer)482 private static String correctPad( 483 String simple, 484 Boolean zeroPad, 485 Boolean spacePad, 486 Boolean removePad, 487 StringBuilder padWidthBuffer) { 488 String unpadded = simple.replaceFirst("^(0+| +)(?!$)", ""); 489 490 if (removePad) { 491 return unpadded; 492 } 493 494 int padWidth = 0; 495 if (padWidthBuffer.length() > 0) { 496 padWidth = ( 497 Integer.parseInt(padWidthBuffer.toString()) - unpadded.length()); 498 } 499 500 if (spacePad || zeroPad) { 501 StringBuilder buffer = new StringBuilder(); 502 char padChar = (spacePad ? ' ' : '0'); 503 for (int i = 0 ; i < padWidth ; i++) { 504 buffer.append(padChar); 505 } 506 buffer.append(unpadded); 507 return buffer.toString(); 508 } 509 510 return simple; 511 } 512 } 513