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 */ 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 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 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