1 /* 2 * Copyright the original author or authors. 3 * 4 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.mozilla.org/MPL/MPL-1.1.html 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 import org.as2lib.core.BasicClass; 18 import org.as2lib.env.except.IllegalArgumentException; 19 20 /** 21 * {@code DateFormatter} formats a given date with a specified pattern. 22 * 23 * <p>Use the declared constants as placeholders for specific parts of the date-time. 24 * 25 * <p>All characters from 'A' to 'Z' and from 'a' to 'z' are reserved, although not 26 * all of these characters are interpreted right now. If you want to include plain 27 * text in the pattern put it into quotes (') to avoid interpretation. If you want 28 * a quote in the formatted date-time, put two quotes directly after one another. 29 * For example: {@code "hh 'o''clock'"}. 30 * 31 * <p>Example: 32 * <code> 33 * var formatter:DateFormatter = new DateFormatter("dd.mm.yyyy HH:nn:ss S"); 34 * trace(formatter.format(new Date(2005, 2, 29, 18, 14, 3, 58))); 35 * </code> 36 * 37 * <p>Output: 38 * <pre> 39 * 29.03.2005 18:14:03 58 40 * </pre> 41 * 42 * @author Simon Wacker 43 */ 44 class org.as2lib.util.DateFormatter extends BasicClass { 45 46 /** The default date format pattern. */ 47 public static var DEFAULT_DATE_FORMAT:String = "dd.mm.yyyy HH:nn:ss"; 48 49 /** Placeholder for year in date format. */ 50 public static var YEAR:String = "y"; 51 52 /** Placeholder for month in year as number in date format. */ 53 public static var MONTH_AS_NUMBER:String = "m"; 54 55 /** Placeholder for month in year as text in date format. */ 56 public static var MONTH_AS_TEXT:String = "M"; 57 58 /** Placeholder for day in month as number in date format. */ 59 public static var DAY_AS_NUMBER:String = "d"; 60 61 /** Placeholder for day in week as text in date format. */ 62 public static var DAY_AS_TEXT:String = "D"; 63 64 /** Placeholder for hour in am/pm (1 - 12) in date format. */ 65 public static var HOUR_IN_AM_PM:String = "h"; 66 67 /** Placeholder for hour in day (0 - 23) in date format. */ 68 public static var HOUR_IN_DAY:String = "H"; 69 70 /** Placeholder for minute in hour in date format. */ 71 public static var MINUTE:String = "n"; 72 73 /** Placeholder for second in minute in date format. */ 74 public static var SECOND:String = "s"; 75 76 /** Placeholder for millisecond in date format. */ 77 public static var MILLISECOND:String = "S"; 78 79 /** Quotation beginning and ending token. */ 80 public static var QUOTE:String = "'"; 81 82 /** Fully written out string for january. */ 83 public static var JANUARY:String = "January"; 84 85 /** Fully written out string for february. */ 86 public static var FEBRUARY:String = "February"; 87 88 /** Fully written out string for march. */ 89 public static var MARCH:String = "March"; 90 91 /** Fully written out string for april. */ 92 public static var APRIL:String = "April"; 93 94 /** Fully written out string for may. */ 95 public static var MAY:String = "May"; 96 97 /** Fully written out string for june. */ 98 public static var JUNE:String = "June"; 99 100 /** Fully written out string for july. */ 101 public static var JULY:String = "July"; 102 103 /** Fully written out string for august. */ 104 public static var AUGUST:String = "August"; 105 106 /** Fully written out string for september. */ 107 public static var SEPTEMBER:String = "September"; 108 109 /** Fully written out string for october. */ 110 public static var OCTOBER:String = "October"; 111 112 /** Fully written out string for november. */ 113 public static var NOVEMBER:String = "November"; 114 115 /** Fully written out string for december. */ 116 public static var DECEMBER:String = "December"; 117 118 /** Fully written out string for monday. */ 119 public static var MONDAY:String = "Monday"; 120 121 /** Fully written out string for tuesday. */ 122 public static var TUESDAY:String = "Tuesday"; 123 124 /** Fully written out string for wednesday. */ 125 public static var WEDNESDAY:String = "Wednesday"; 126 127 /** Fully written out string for thursday. */ 128 public static var THURSDAY:String = "Thursday"; 129 130 /** Fully written out string for friday. */ 131 public static var FRIDAY:String = "Friday"; 132 133 /** Fully written out string for saturday. */ 134 public static var SATURDAY:String = "Saturday"; 135 136 /** Fully written out string for sunday. */ 137 public static var SUNDAY:String = "Sunday"; 138 139 /** The pattern to format the date with. */ 140 private var dateFormat:String; 141 142 /** 143 * Constructs a new {@code DateFormatter} instance. 144 * 145 * <p>If you do not pass-in a {@code dateFormat} or if the passed-in one is 146 * {@code null} or {@code undefined} the {@code DEFAULT_DATE_FORMAT} is used. 147 * 148 * @param dateFormat (optional) the pattern describing the date and time format 149 */ 150 public function DateFormatter(dateFormat:String) { 151 this.dateFormat = dateFormat == null ? DEFAULT_DATE_FORMAT : dateFormat; 152 } 153 154 /** 155 * Formats the passed-in {@code date} with the specified date format pattern into a 156 * date-time string and returns the resulting string. 157 * 158 * <p>If the passed-in {@code date} is {@code null} or {@code undefined}, the current 159 * date-time will be used instead. 160 * 161 * @param date the date-time value to format into a date-time string 162 * @return the formatted date-time string 163 */ 164 public function format(date:Date):String { 165 if (!date) date = new Date(); 166 var result:String = ""; 167 for (var i:Number = 0; i < dateFormat.length; i++) { 168 if (dateFormat.substr(i, 1) == YEAR) { 169 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 170 result += formatYear(date.getFullYear(), tokenCount); 171 i += tokenCount - 1; 172 continue; 173 } 174 if (dateFormat.substr(i, 1) == MONTH_AS_NUMBER) { 175 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 176 result += formatMonthAsNumber(date.getMonth(), tokenCount); 177 i += tokenCount - 1; 178 continue; 179 } 180 if (dateFormat.substr(i, 1) == MONTH_AS_TEXT) { 181 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 182 result += formatMonthAsText(date.getMonth(), tokenCount); 183 i += tokenCount - 1; 184 continue; 185 } 186 if (dateFormat.substr(i, 1) == DAY_AS_NUMBER) { 187 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 188 result += formatDayAsNumber(date.getDate(), tokenCount); 189 i += tokenCount - 1; 190 continue; 191 } 192 if (dateFormat.substr(i, 1) == DAY_AS_TEXT) { 193 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 194 result += formatDayAsText(date.getDay(), tokenCount); 195 i += tokenCount - 1; 196 continue; 197 } 198 if (dateFormat.substr(i, 1) == HOUR_IN_AM_PM) { 199 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 200 result += formatHourInAmPm(date.getHours(), tokenCount); 201 i += tokenCount - 1; 202 continue; 203 } 204 if (dateFormat.substr(i, 1) == HOUR_IN_DAY) { 205 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 206 result += formatHourInDay(date.getHours(), tokenCount); 207 i += tokenCount - 1; 208 continue; 209 } 210 if (dateFormat.substr(i, 1) == MINUTE) { 211 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 212 result += formatMinute(date.getMinutes(), tokenCount); 213 i += tokenCount - 1; 214 continue; 215 } 216 if (dateFormat.substr(i, 1) == SECOND) { 217 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 218 result += formatSecond(date.getSeconds(), tokenCount); 219 i += tokenCount - 1; 220 continue; 221 } 222 if (dateFormat.substr(i, 1) == MILLISECOND) { 223 var tokenCount:Number = getTokenCount(dateFormat.substr(i)); 224 result += formatMillisecond(date.getMilliseconds(), tokenCount); 225 i += tokenCount - 1; 226 continue; 227 } 228 if (dateFormat.substr(i, 1) == QUOTE) { 229 if (dateFormat.substr(i + 1, 1) == QUOTE) { 230 result += "'"; 231 i++; 232 continue; 233 } 234 var nextQuote:Number = i; 235 var oldQuote:Number; 236 while (true) { 237 oldQuote = nextQuote; 238 nextQuote = dateFormat.indexOf("'", nextQuote + 1); 239 if (dateFormat.substr(nextQuote + 1, 1) != QUOTE) { 240 break; 241 } 242 result += dateFormat.substring(oldQuote + 1, nextQuote + 1); 243 nextQuote++; 244 } 245 result += dateFormat.substring(oldQuote + 1, nextQuote); 246 i = nextQuote; 247 continue; 248 } 249 result += dateFormat.substr(i, 1); 250 } 251 return result; 252 } 253 254 /** 255 * Returns the number of tokens that occur in a succession from the beginning of the 256 * passed-in {@code string}. 257 * 258 * <p>If the passed-in {@code string} is {@code null}, {@code undefined} or empty, 259 * 0 is returned. 260 * 261 * @param string the string to search through 262 * @return the number of tokens that occur in a succession 263 */ 264 private function getTokenCount(string:String):Number { 265 if (!string) return 0; 266 var result:Number = 0; 267 var token:String = string.substr(0, 1); 268 while (string.substr(result, 1) == token) { 269 result++; 270 } 271 return result; 272 } 273 274 /** 275 * Returns a string that contains the specified number of 0s. 276 * 277 * <p>A {@code count} less or equal than 0 or a {@code count} of value {@code null} 278 * or {@code undefined} results in en empty string. 279 * 280 * @param count the number of 0s 281 * @return the specified number of 0s 282 */ 283 private function getZeros(count:Number):String { 284 if (count < 1 || count == null) return ""; 285 if (count < 2) return "0"; 286 var result:String = "00"; 287 count -= 2; 288 while (count) { 289 result += "0"; 290 count--; 291 } 292 return result; 293 } 294 295 /** 296 * Formats the passed-in {@code year} into a year string with the specified 297 * {@code digitCount}. 298 * 299 * <p>A {@code digitCount} less or equal than three results in a year string with 300 * two digits. A {@code digitCount} greater or equal than four results in a year 301 * string with four digits plus preceding 0s if the {@code digitCount} is greater 302 * than four. 303 * 304 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 305 * is used instead. 306 * 307 * @param year the year to format to a string 308 * @param digitCount the number of favored digits 309 * @return the string representation of the year 310 * @throws IllegalArgumentException if the passed-in {@code year} is 311 * {@code null} or {@code undefined} 312 */ 313 private function formatYear(year:Number, digitCount:Number):String { 314 if (year == null) { 315 throw new IllegalArgumentException("Argument 'year' [" + year + "] must not be 'null' nor 'undefined'.", this, arguments); 316 } 317 if (digitCount == null) digitCount = 0; 318 if (digitCount < 4) { 319 return year.toString().substr(2); 320 } 321 return (getZeros(digitCount - 4) + year.toString()); 322 } 323 324 /** 325 * Formats the passed-in {@code month} into a month as number string with the 326 * specified {@code digitCount}. 327 * 328 * <p>A {@code digitCount} less or equal than one results in a month with one digit, 329 * if the month is less or equal than nine. Otherwise the month is represented by a 330 * two digit number. A {@code digitCount} greater or equal than two results in a 331 * month with preceding 0s. 332 * 333 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 334 * is used instead. 335 * 336 * @param month the month to format to a number string 337 * @param digitCount the number of favored digits 338 * @return the number representation of the month 339 * @throws IllegalArgumentException if the passed-in {@code month} is 340 * less than 0 or greater than 11 or {@code null} or {@code undefined} 341 */ 342 private function formatMonthAsNumber(month:Number, digitCount:Number):String { 343 if (month < 0 || month > 11 || month == null) { 344 throw new IllegalArgumentException("Argument 'month' [" + month + "] must not be less than 0 nor greater than 11 nor 'null' nor 'undefined'.", this, arguments); 345 } 346 if (digitCount == null) digitCount = 0; 347 var string:String = (month + 1).toString(); 348 return (getZeros(digitCount - string.length) + string); 349 } 350 351 /** 352 * Formats the passed-in {@code month} into a string with the specified 353 * {@code tokenCount}. 354 * 355 * <p>A {@code tokenCount} less or equal than three results in a month with three 356 * tokens. A {@code tokenCount} greater or equal than four results in a fully written 357 * out month. 358 * 359 * <p>If the passed-in {@code tokenCount} is {@code null} or {@code undefined}, 0 360 * is used instead. 361 * 362 * @param month the month to format to a string 363 * @param tokenCount the number of favored tokens 364 * @return the string representation of the month 365 * @throws IllegalArgumentException if the passed-in {@code month} is 366 * less than 0 or greater than 11 or {@code null} or {@code undefined} 367 */ 368 private function formatMonthAsText(month:Number, tokenCount:Number):String { 369 if (month < 0 || month > 11 || month == null) { 370 throw new IllegalArgumentException("Argument 'month' [" + month + "] must not be less than 0 nor greater than 11 nor 'null' nor 'undefined'.", this, arguments); 371 } 372 if (tokenCount == null) tokenCount = 0; 373 var result:String; 374 switch (month) { 375 case 0: 376 result = JANUARY; 377 break; 378 case 1: 379 result = FEBRUARY; 380 break; 381 case 2: 382 result = MARCH; 383 break; 384 case 3: 385 result = APRIL; 386 break; 387 case 4: 388 result = MAY; 389 break; 390 case 5: 391 result = JUNE; 392 break; 393 case 6: 394 result = JULY; 395 break; 396 case 7: 397 result = AUGUST; 398 break; 399 case 8: 400 result = SEPTEMBER; 401 break; 402 case 9: 403 result = OCTOBER; 404 break; 405 case 10: 406 result = NOVEMBER; 407 break; 408 case 11: 409 result = DECEMBER; 410 break; 411 } 412 if (tokenCount < 4) { 413 return result.substr(0, 3); 414 } 415 return result; 416 } 417 418 /** 419 * Formats the passed-in {@code day} into a day as number string with the specified 420 * {@code digitCount}. 421 * 422 * <p>A {@code digitCount} less or equal than one results in a day with one digit, 423 * if the day is less or equal than nine. Otherwise the day is represented by a two 424 * digit number. A {@code digitCount} greater or equal than two results in a day with 425 * preceding 0s. 426 * 427 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 428 * is used instead. 429 * 430 * @param day the day of month to format to a number string 431 * @param digitCount the number of digits 432 * @return the number representation of the day 433 * @throws IllegalArgumentException if the passed-in {@code day} is less 434 * than 1 or greater than 31 or {@code null} or {@code undefined} 435 */ 436 private function formatDayAsNumber(day:Number, digitCount:Number):String { 437 if (day < 1 || day > 31 || day == null) { 438 throw new IllegalArgumentException("Argument 'day' [" + day + "] must not be less than 1 nor greater than 31 nor 'null' nor 'undefined'.", this, arguments); 439 } 440 if (digitCount == null) digitCount = 0; 441 var string:String = day.toString(); 442 return (getZeros(digitCount - string.length) + string); 443 } 444 445 /** 446 * Formats the passed-in {@code day} into a string with the specified 447 * {@code tokenCount}. 448 * 449 * <p>A {@code tokenCount} less or equal than three results in a day with two 450 * tokens. A {@code tokenCount} greater or equal than four results in a fully written 451 * out day. 452 * 453 * <p>If the passed-in {@code tokenCount} is {@code null} or {@code undefined}, 0 454 * is used instead. 455 * 456 * @param day the day to format to a string 457 * @param tokenCount the number of favored tokens 458 * @return the string representation of the day 459 * @throws IllegalArgumentException if the passed-in {@code day} is less 460 * than 0 or greater than 6 or {@code null} or {@code undefined} 461 */ 462 private function formatDayAsText(day:Number, tokenCount:Number):String { 463 if (day < 0 || day > 6 || day == null) { 464 throw new IllegalArgumentException("Argument 'day' [" + day + "] must not be less than 0 nor greater than 6 nor 'null' nor 'undefined'.", this, arguments); 465 } 466 if (tokenCount == null) tokenCount = 0; 467 var result:String; 468 switch (day) { 469 case 0: 470 result = SUNDAY; 471 break; 472 case 1: 473 result = MONDAY; 474 break; 475 case 2: 476 result = TUESDAY; 477 break; 478 case 3: 479 result = WEDNESDAY; 480 break; 481 case 4: 482 result = THURSDAY; 483 break; 484 case 5: 485 result = FRIDAY; 486 break; 487 case 6: 488 result = SATURDAY; 489 break; 490 } 491 if (tokenCount < 4) { 492 return result.substr(0, 2); 493 } 494 return result; 495 } 496 497 /** 498 * Formats the passed-in {@code hour} into a number string from range 1 to 12. 499 * 500 * <p>The resulting string contains only the specified {@code digitCount} if 501 * possible. This means if the hour is 3 and the {@code digitCount} 1 the resulting 502 * string contains one digit. But this is not possible with the hour 12. So in this 503 * case the resulting string contains 2 digits. If {@code digitCount} is greater 504 * than the actual number of digits, preceding 0s are added. 505 * 506 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 507 * is used instead. 508 * 509 * @param hour the hour to format 510 * @param digitCount the number of favored digits 511 * @return the string representation of {@code hour} 512 * @throws IllegalArgumentException if the passed-in {@code hour} is less 513 * than 0 or greater than 23 or {@code null} or {@code undefined} 514 */ 515 private function formatHourInAmPm(hour:Number, digitCount:Number):String { 516 if (hour < 0 || hour > 23 || hour == null) { 517 throw new IllegalArgumentException("Argument 'hour' [" + hour + "] must not be less than 0 nor greater than 23 nor 'null' nor 'undefined'.", this, arguments); 518 } 519 if (digitCount == null) digitCount = 0; 520 var string:String; 521 if (hour == 0) { 522 // 12.toString() causes a compiler error 523 string = (12).toString(); 524 } else if (hour > 12) { 525 string = (hour - 12).toString(); 526 } else { 527 string = hour.toString(); 528 } 529 return (getZeros(digitCount - string.length) + string); 530 } 531 532 /** 533 * Formats the passed-in {@code hour} into a number string from range 0 to 23. 534 * 535 * <p>The resulting string contains only the specified {@code digitCount} if 536 * possible. This means if the hour is 3 and the {@code digitCount} 1 the resulting 537 * string contains one digit. But this is not possible with the hour 18. So in this 538 * case the resulting string contains 2 digits. If {@code digitCount} is greater 539 * than the actual number of digits, preceding 0s are added. 540 * 541 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 542 * is used instead. 543 * 544 * @param hour the hour to format 545 * @param digitCount the number of favored digits 546 * @return the string representation of {@code hour} 547 * @throws IllegalArgumentException if the passed-in {@code hour} is less 548 * than 0 or greater than 23 or {@code null} or {@code undefined} 549 */ 550 private function formatHourInDay(hour:Number, digitCount:Number):String { 551 if (hour < 0 || hour > 23 || hour == null) { 552 throw new IllegalArgumentException("Argument 'hour' [" + hour + "] must not be less than 0 nor greater than 23 nor 'null' nor 'undefined'.", this, arguments); 553 } 554 if (digitCount == null) digitCount = 0; 555 var string:String = hour.toString(); 556 return (getZeros(digitCount - string.length) + string); 557 } 558 559 /** 560 * Formats the passed-in {@code minute} into a number string with range 0 to 59. 561 * 562 * <p>The resulting string contains only the specified {@code digitCount} if 563 * possible. This means if the minute is 3 and the {@code digitCount} 1, the 564 * resulting string contains only one digit. But this is not possible with the 565 * minute 46. So in this case the resulting string contains 2 digits. If 566 * {@code digitCount} is greater than the actual number of digits, preceding 0s are 567 * added. 568 * 569 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 570 * is used instead. 571 * 572 * @param minute the minute to format 573 * @param digitCount the number of favored digits 574 * @return the string representation of the {@code minute} 575 * @throws IllegalArgumentException if the passed-in {@code minute} is 576 * less than 0 or greater than 59 or {@code null} or {@code undefined} 577 */ 578 private function formatMinute(minute:Number, digitCount:Number):String { 579 if (minute < 0 || minute > 59 || minute == null) { 580 throw new IllegalArgumentException("Argument 'minute' [" + minute + "] must not be less than 0 nor greater than 59 nor 'null' nor 'undefined'.", this, arguments); 581 } 582 if (digitCount == null) digitCount = 0; 583 var string:String = minute.toString(); 584 return (getZeros(digitCount - string.length) + string); 585 } 586 587 /** 588 * Formats the passed-in {@code second} into a number string with range 0 to 59. 589 * 590 * <p>The resulting string contains only the specified {@code digitCount} if 591 * possible. This means if the second is 3 and the {@code digitCount} 1, the 592 * resulting string contains only one digit. But this is not possible with the 593 * second 46. So in this case the resulting string contains 2 digits. If 594 * {@code digitCount} is greater than the actual number of digits, preceding 0s are 595 * added. 596 * 597 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 598 * is used instead. 599 * 600 * @param second the second to format 601 * @param digitCount the number of favored digits 602 * @return the string representation of the {@code second} 603 * @throws IllegalArgumentException if the passed-in {@code second} is 604 * less than 0 or greater than 59 or {@code null} or {@code undefined} 605 */ 606 private function formatSecond(second:Number, digitCount:Number):String { 607 if (second < 0 || second > 59 || second == null) { 608 throw new IllegalArgumentException("Argument 'second' [" + second + "] must not be less than 0 nor greater than 59 nor 'null' nor 'undefined'.", this, arguments); 609 } 610 if (digitCount == null) digitCount = 0; 611 var string:String = second.toString(); 612 return (getZeros(digitCount - string.length) + string); 613 } 614 615 /** 616 * Formats the passed-in {@code millisecond} into a number string with range 0 to 617 * 999. 618 * 619 * <p>The resulting string contains only the specified {@code digitCount} if 620 * possible. This means if the millisecond is 7 and the {@code digitCount} 1, the 621 * resulting string contains only one digit. But this is not possible with the 622 * millisecond 588. So in this case the resulting string contains 3 digits. If 623 * {@code digitCount} is greater than the actual number of digits, preceding 0s are 624 * added. 625 * 626 * <p>If the passed-in {@code digitCount} is {@code null} or {@code undefined}, 0 627 * is used instead. 628 * 629 * @param millisecond the millisecond to format 630 * @param digitCount the number of favored digits 631 * @return the string representation of the {@code millisecond} 632 * @throws IllegalArgumentException if the passed-in {@code millisecond} 633 * is less than 0 or greater than 999 or {@code null} or {@code undefined} 634 */ 635 private function formatMillisecond(millisecond:Number, digitCount:Number):String { 636 if (millisecond < 0 || millisecond > 999 || millisecond == null) { 637 throw new IllegalArgumentException("Argument 'millisecond' [" + millisecond + "] must not be less than 0 nor greater than 999 nor 'null' nor 'undefined'.", this, arguments); 638 } 639 if (digitCount == null) digitCount = 0; 640 var string:String = millisecond.toString(); 641 return (getZeros(digitCount - string.length) + string); 642 } 643 644 }