1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** 9 Standard error thrown by `SC.Scanner` when it runs out of bounds 10 11 @static 12 @constant 13 @type Error 14 */ 15 SC.SCANNER_OUT_OF_BOUNDS_ERROR = SC.$error("Out of bounds."); 16 17 /** 18 Standard error thrown by `SC.Scanner` when you pass a value not an integer. 19 20 @static 21 @constant 22 @type Error 23 */ 24 SC.SCANNER_INT_ERROR = SC.$error("Not an int."); 25 26 /** 27 Standard error thrown by `SC.SCanner` when it cannot find a string to skip. 28 29 @static 30 @constant 31 @type Error 32 */ 33 SC.SCANNER_SKIP_ERROR = SC.$error("Did not find the string to skip."); 34 35 /** 36 Standard error thrown by `SC.Scanner` when it can any kind a string in the 37 matching array. 38 39 @static 40 @constant 41 @type Error 42 */ 43 SC.SCANNER_SCAN_ARRAY_ERROR = SC.$error("Did not find any string of the given array to scan."); 44 45 /** 46 Standard error thrown when trying to compare two dates in different 47 timezones. 48 49 @static 50 @constant 51 @type Error 52 */ 53 SC.DATETIME_COMPAREDATE_TIMEZONE_ERROR = SC.$error("Can't compare the dates of two DateTimes that don't have the same timezone."); 54 55 /** 56 Standard ISO8601 date format 57 58 @static 59 @type String 60 @default '%Y-%m-%dT%H:%M:%S%Z' 61 @constant 62 */ 63 SC.DATETIME_ISO8601 = '%Y-%m-%dT%H:%M:%S%Z'; 64 65 66 /** @class 67 68 A Scanner reads a string and interprets the characters into numbers. You 69 assign the scanner's string on initialization and the scanner progresses 70 through the characters of that string from beginning to end as you request 71 items. 72 73 Scanners are used by `DateTime` to convert strings into `DateTime` objects. 74 75 @extends SC.Object 76 @since SproutCore 1.0 77 @author Martin Ottenwaelter 78 */ 79 SC.Scanner = SC.Object.extend( 80 /** @scope SC.Scanner.prototype */ { 81 82 /** 83 The string to scan. You usually pass it to the create method: 84 85 SC.Scanner.create({string: 'May, 8th'}); 86 87 @type String 88 */ 89 string: null, 90 91 /** 92 The current scan location. It is incremented by the scanner as the 93 characters are processed. 94 The default is 0: the beginning of the string. 95 96 @type Integer 97 */ 98 scanLocation: 0, 99 100 /** 101 Reads some characters from the string, and increments the scan location 102 accordingly. 103 104 @param {Integer} len The amount of characters to read 105 @throws {SC.SCANNER_OUT_OF_BOUNDS_ERROR} If asked to read too many characters 106 @returns {String} The characters 107 */ 108 scan: function(len) { 109 if (this.scanLocation + len > this.length) SC.SCANNER_OUT_OF_BOUNDS_ERROR.throw(); 110 var str = this.string.substr(this.scanLocation, len); 111 this.scanLocation += len; 112 return str; 113 }, 114 115 /** 116 Reads some characters from the string and interprets it as an integer. 117 118 @param {Integer} min_len The minimum amount of characters to read 119 @param {Integer} [max_len] The maximum amount of characters to read (defaults to the minimum) 120 @throws {SC.SCANNER_INT_ERROR} If asked to read non numeric characters 121 @returns {Integer} The scanned integer 122 */ 123 scanInt: function(min_len, max_len) { 124 if (max_len === undefined) max_len = min_len; 125 var str = this.scan(max_len); 126 var re = new RegExp("^\\d{" + min_len + "," + max_len + "}"); 127 var match = str.match(re); 128 if (!match) SC.SCANNER_INT_ERROR.throw(); 129 if (match[0].length < max_len) { 130 this.scanLocation += match[0].length - max_len; 131 } 132 return parseInt(match[0], 10); 133 }, 134 135 /** 136 Attempts to skip a given string. 137 138 @param {String} str The string to skip 139 @throws {SC.SCANNER_SKIP_ERROR} If the given string could not be scanned 140 @returns {Boolean} YES if the given string was successfully scanned, NO otherwise 141 */ 142 skipString: function(str) { 143 if (this.scan(str.length) !== str) SC.SCANNER_SKIP_ERROR.throw(); 144 return YES; 145 }, 146 147 /** 148 Attempts to scan any string in a given array. 149 150 @param {Array} ary the array of strings to scan 151 @throws {SC.SCANNER_SCAN_ARRAY_ERROR} If no string of the given array is found 152 @returns {Integer} The index of the scanned string of the given array 153 */ 154 scanArray: function(ary) { 155 for (var i = 0, len = ary.length; i < len; i++) { 156 if (this.scan(ary[i].length) === ary[i]) { 157 return i; 158 } 159 this.scanLocation -= ary[i].length; 160 } 161 SC.SCANNER_SCAN_ARRAY_ERROR.throw(); 162 } 163 164 }); 165 166 167 /** @class 168 169 A class representation of a date and time. It's basically a wrapper around 170 the Date javascript object, KVO-friendly and with common date/time 171 manipulation methods. 172 173 This object differs from the standard JS Date object, however, in that it 174 supports time zones other than UTC and that local to the machine on which 175 it is running. Any time zone can be specified when creating an 176 `SC.DateTime` object, e.g. 177 178 // Creates a DateTime representing 5am in Washington, DC and 10am in London 179 var d = SC.DateTime.create({ hour: 5, timezone: 300 }); // -5 hours from UTC 180 var e = SC.DateTime.create({ hour: 10, timezone: 0 }); // same time, specified in UTC 181 182 and it is true that `d.isEqual(e)`. 183 184 The time zone specified upon creation is permanent, and any calls to 185 `get()` on that instance will return values expressed in that time zone. So, 186 187 d.get('hour') returns 5. 188 e.get('hour') returns 10. 189 190 but 191 192 d.get('milliseconds') === e.get('milliseconds') 193 194 is true, since they are technically the same position in time. 195 196 You can also use SC.DateTime as a record attribute on a data model. 197 198 SC.Record.attr(SC.DateTime); // Default format is ISO8601. See `SC.DateTime.recordFormat` 199 SC.Record.attr(SC.DateTime, { format: '%d/%m/%Y' }); // Attribute stored as a string in '%d/%m/%Y' format 200 SC.Record.attr(SC.DateTime, { useUnixTime: YES }); // Attribute stored as a number in Unix time 201 202 @extends SC.Object 203 @extends SC.Freezable 204 @extends SC.Copyable 205 @author Martin Ottenwaelter 206 @author Jonathan Lewis 207 @author Josh Holt 208 @since SproutCore 1.0 209 */ 210 SC.DateTime = SC.Object.extend(SC.Freezable, SC.Copyable, 211 /** @scope SC.DateTime.prototype */ { 212 213 /** 214 @private 215 216 Internal representation of a date: the number of milliseconds 217 since January, 1st 1970 00:00:00.0 UTC. 218 219 @property 220 @type {Integer} 221 */ 222 _ms: 0, 223 224 /** @read-only 225 The offset, in minutes, between UTC and the object's timezone. 226 All calls to `get()` will use this time zone to translate date/time 227 values into the zone specified here. 228 229 @type Integer 230 */ 231 timezone: 0, 232 233 /** 234 A `SC.DateTime` instance is frozen by default for better performance. 235 236 @type Boolean 237 */ 238 isFrozen: YES, 239 240 /** 241 Returns a new `SC.DateTime` object where one or more of the elements have been adjusted 242 according to the `options` parameter. The possible options that can be adjusted are `timezone`, 243 `year`, `month`, `day`, `hour`, `minute`, `second` and `millisecond`. 244 245 This is particularly useful when we want to get to a certain date or time based on an existing 246 date or time without having to know all the elements of the existing date. 247 248 For example, say we needed a datetime for midnight on whatever day we are given. The easiest 249 way to do this is to take the datetime we are given and adjust it. For example, 250 251 var midnight = someDate.adjust({ hour: 24 }); // Midnight on whatever day `someDate` is. 252 253 ### Adjusting Time 254 255 The time options, `hour`, `minute`, `second`, `millisecond`, are reset cascadingly by default. 256 So for example, if only the hour is passed, then the minute, second, and millisecond will be set 257 to 0. Or for another example, if the hour and minute are passed, then second and millisecond 258 would be set to 0. To disable this simply pass `false` as the second argument to `adjust`. 259 260 ### Adjusting Timezone 261 262 If a time zone is passed in the options hash, all dates and times are assumed to be local to it, 263 and the returned `SC.DateTime` instance has that time zone. If none is passed, it defaults to 264 the value of `SC.DateTime.timezone`. 265 266 Note that passing only a time zone does not affect the actual milliseconds since Jan 1, 1970, 267 only the time zone in which it is expressed when displayed. 268 269 @param {Object} options the amount of date/time to advance the receiver 270 @param {Boolean} [resetCascadingly] whether to reset the time elements cascadingly from hour down to millisecond. Default `true`. 271 @returns {SC.DateTime} copy of receiver 272 */ 273 adjust: function(options, resetCascadingly) { 274 var timezone; 275 276 options = options ? SC.clone(options) : {}; 277 timezone = (options.timezone !== undefined) ? options.timezone : (this.timezone !== undefined) ? this.timezone : 0; 278 279 return this.constructor._adjust(options, this._ms, timezone, resetCascadingly)._createFromCurrentState(); 280 }, 281 282 /** 283 Returns a new `SC.DateTime` object where one or more of the elements have been advanced 284 according to the `options` parameter. The possible options that can be advanced are `year`, 285 `month`, `day`, `hour`, `minute`, `second` and `millisecond`. 286 287 Note, you should not use floating point values as it might give unpredictable results. 288 289 @param {Object} options the amount of date/time to advance the receiver 290 @returns {DateTime} copy of the receiver 291 */ 292 advance: function(options) { 293 return this.constructor._advance(options, this._ms, this.timezone)._createFromCurrentState(); 294 }, 295 296 /** 297 Generic getter. 298 299 The properties you can get are: 300 301 - `year` 302 - `month` (January is 1, contrary to JavaScript Dates for which January is 0) 303 - `day` 304 - `dayOfWeek` (Sunday is 0) 305 - `hour` 306 - `minute` 307 - `second` 308 - `millisecond` 309 - `milliseconds`, the number of milliseconds since 310 January, 1st 1970 00:00:00.0 UTC 311 - `elapsed`, the number of milliseconds until (-), or since (+), the date. 312 - `isLeapYear`, a boolean value indicating whether the receiver's year 313 is a leap year 314 - `daysInMonth`, the number of days of the receiver's current month 315 - `dayOfYear`, January 1st is 1, December 31th is 365 for a common year 316 - `week` or `week1`, the week number of the current year, starting with 317 the first Sunday as the first day of the first week (00..53) 318 - `week0`, the week number of the current year, starting with 319 the first Monday as the first day of the first week (00..53) 320 - `lastMonday`, `lastTuesday`, etc., `nextMonday`, 321 `nextTuesday`, etc., the date of the last or next weekday in 322 comparison to the receiver. 323 324 @param {String} key the property name to get 325 @return the value asked for 326 */ 327 unknownProperty: function(key) { 328 return this.constructor._get(key, this._ms, this.timezone); 329 }, 330 331 /** 332 Formats the receiver according to the given format string. Should behave 333 like the C strftime function. 334 335 The format parameter can contain the following characters: 336 337 - `%a` -- The abbreviated weekday name ("Sun") 338 - `%A` -- The full weekday name ("Sunday") 339 - `%b` -- The abbreviated month name ("Jan") 340 - `%B` -- The full month name ("January") 341 - `%c` -- The preferred local date and time representation 342 - `%d` -- Day of the month (01..31) 343 - `%D` -- Day of the month (0..31) 344 - `%E` -- Elapsed time, according to localized text formatting strings. See below. 345 - `%h` -- Hour of the day, 24-hour clock (0..23) 346 - `%H` -- Hour of the day, 24-hour clock (00..23) 347 - `%i` -- Hour of the day, 12-hour clock (1..12) 348 - `%I` -- Hour of the day, 12-hour clock (01..12) 349 - `%j` -- Day of the year (001..366) 350 - `%m` -- Month of the year (01..12) 351 - `%M` -- Minute of the hour (00..59) 352 - `%o` -- The day's ordinal abbreviation ('st', 'nd', 'rd', etc.) 353 - `%p` -- Meridian indicator ("AM" or "PM") 354 - `%S` -- Second of the minute (00..60) 355 - `%s` -- Milliseconds of the second (000..999) 356 - `%U` -- Week number of the current year, 357 starting with the first Sunday as the first 358 day of the first week (00..53) 359 - `%W` -- Week number of the current year, 360 starting with the first Monday as the first 361 day of the first week (00..53) 362 - `%w` -- Day of the week (Sunday is 0, 0..6) 363 - `%x` -- Preferred representation for the date alone, no time 364 - `%X` -- Preferred representation for the time alone, no date 365 - `%y` -- Year without a century (00..99) 366 - `%Y` -- Year with century 367 - `%Z` -- Time zone (ISO 8601 formatted) 368 - `%%` -- Literal "%" character 369 370 The Elapsed date format is a special, SproutCore specific formatting 371 feature which will return an accurate-ish, human readable indication 372 of elapsed time. For example, it might return "In 5 minutes", or "A year 373 ago". 374 375 For example, 376 377 var date = SC.DateTime.create(); 378 379 date.toFormattedString("%E"); // "Right now" 380 381 date.advance({ minute: 4 }); 382 date.toFormattedString("%E"); // "In 4 minutes" 383 384 date.advance({ day: -7 }); 385 date.toFormattedString("%E"); // "About a week ago" 386 387 To customize the output for the %E formatter, override the date 388 localization strings inside of in your app. The English localization 389 strings used are: 390 391 '_SC.DateTime.now' : 'Right now', 392 '_SC.DateTime.secondIn' : 'In a moment', 393 '_SC.DateTime.secondsIn' : 'In %e seconds', 394 '_SC.DateTime.minuteIn' : 'In a minute', 395 '_SC.DateTime.minutesIn' : 'In %e minutes', 396 '_SC.DateTime.hourIn' : 'An hour from now', 397 '_SC.DateTime.hoursIn' : 'In about %e hours', 398 '_SC.DateTime.dayIn' : 'Tomorrow at %i:%M %p', 399 '_SC.DateTime.daysIn' : '%A at %i:%M %p', 400 '_SC.DateTime.weekIn' : 'Next week', 401 '_SC.DateTime.weeksIn' : 'In %e weeks', 402 '_SC.DateTime.monthIn' : 'Next month', 403 '_SC.DateTime.monthsIn' : 'In %e months', 404 '_SC.DateTime.yearIn' : 'Next year', 405 '_SC.DateTime.yearsIn' : 'In %e years', 406 407 '_SC.DateTime.secondAgo' : 'A moment ago', 408 '_SC.DateTime.secondsAgo' : '%e seconds ago', 409 '_SC.DateTime.minuteAgo' : 'A minute ago', 410 '_SC.DateTime.minutesAgo' : '%e minutes ago', 411 '_SC.DateTime.hourAgo' : 'An hour ago', 412 '_SC.DateTime.hoursAgo' : 'About %e hours ago', 413 '_SC.DateTime.dayAgo' : 'Yesterday at %i:%M %p', 414 '_SC.DateTime.daysAgo' : '%A at %i:%M %p', 415 '_SC.DateTime.weekAgo' : 'About a week ago', 416 '_SC.DateTime.weeksAgo' : '%e weeks ago', 417 '_SC.DateTime.monthAgo' : 'About a month ago', 418 '_SC.DateTime.monthsAgo' : '%e months ago', 419 '_SC.DateTime.yearAgo' : 'Last year', 420 '_SC.DateTime.yearsAgo' : '%e years ago' 421 422 Notice the special "%e" parameter in the localized strings. This will be 423 replaced with the number of intervals (seconds, minutes, weeks, etc) that 424 are appropriate. 425 426 @param {String} format the format string 427 @return {String} the formatted string 428 */ 429 toFormattedString: function(fmt) { 430 return this.constructor._toFormattedString(fmt, this._ms, this.timezone); 431 }, 432 433 /** 434 Formats the receiver according ISO 8601 standard. It is equivalent to 435 calling toFormattedString with the `'%Y-%m-%dT%H:%M:%S%Z'` format string. 436 437 @return {String} the formatted string 438 */ 439 toISO8601: function(){ 440 return this.constructor._toFormattedString(SC.DATETIME_ISO8601, this._ms, this.timezone); 441 }, 442 443 /** 444 Returns the suffix of the date for use in english eg 21st 22nd 22rd 445 speech. 446 447 @return {String} 448 */ 449 dayOrdinal: function(){ 450 return this.get('day').ordinal(); 451 }.property(), 452 453 /** 454 @private 455 456 Creates a string representation of the receiver. 457 458 (Debuggers often call the `toString` method. Because of the way 459 `SC.DateTime` is designed, calling `SC.DateTime._toFormattedString` would 460 have a nasty side effect. We shouldn't therefore call any of 461 `SC.DateTime`'s methods from `toString`) 462 463 @returns {String} 464 */ 465 toString: function() { 466 return "UTC: " + 467 new Date(this._ms).toUTCString() + 468 ", timezone: " + 469 this.timezone; 470 }, 471 472 /** 473 Returns `YES` if the passed `SC.DateTime` is equal to the receiver, ie: if their 474 number of milliseconds since January, 1st 1970 00:00:00.0 UTC are equal. 475 This is the preferred method for testing equality. 476 477 @see SC.DateTime#compare 478 @param {SC.DateTime} aDateTime the DateTime to compare to 479 @returns {Boolean} 480 */ 481 isEqual: function(aDateTime) { 482 return this.constructor.compare(this, aDateTime) === 0; 483 }, 484 485 /** 486 Returns a copy of the receiver. Because of the way `SC.DateTime` is designed, 487 it just returns the receiver. 488 489 @returns {SC.DateTime} 490 */ 491 copy: function() { 492 return this; 493 }, 494 495 /** 496 Returns a copy of the receiver with the timezone set to the passed 497 timezone. The returned value is equal to the receiver (ie `SC.Compare` 498 returns 0), it is just the timezone representation that changes. 499 500 If you don't pass any argument, the target timezone is assumed to be 0, 501 ie UTC. 502 503 Note that this method does not change the underlying position in time, 504 but only the time zone in which it is displayed. In other words, the underlying 505 number of milliseconds since Jan 1, 1970 does not change. 506 507 @return {SC.DateTime} 508 */ 509 toTimezone: function(timezone) { 510 if (timezone === undefined) timezone = 0; 511 return this.advance({ timezone: timezone - this.timezone }); 512 } 513 514 }); 515 516 SC.DateTime.mixin(SC.Comparable, 517 /** @scope SC.DateTime */ { 518 519 /** 520 The default format (ISO 8601) in which DateTimes are stored in a record. 521 Change this value if your backend sends and receives dates in another 522 format. 523 524 This value can also be customized on a per-attribute basis with the format 525 property. For example: 526 527 SC.Record.attr(SC.DateTime, { format: '%d/%m/%Y %H:%M:%S' }) 528 529 @type String 530 @default SC.DATETIME_ISO8601 531 */ 532 recordFormat: SC.DATETIME_ISO8601, 533 534 /** 535 @type Array 536 @default ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] 537 */ 538 dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 539 540 /** 541 @private 542 543 The English day names used for the 'lastMonday', 'nextTuesday', etc., getters. 544 545 @type Array 546 */ 547 _englishDayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 548 549 /** 550 @type Array 551 @default ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 552 */ 553 abbreviatedDayNames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 554 555 /** 556 @type Array 557 @default ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] 558 */ 559 monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 560 561 /** 562 @type Array 563 @default ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 564 */ 565 abbreviatedMonthNames: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 566 567 /** 568 @type Array 569 @default ['AM', 'PM'] 570 */ 571 AMPMNames:['AM', 'PM'], 572 573 /** 574 @private 575 576 The unique internal `Date` object used to make computations. Better 577 performance is obtained by having only one Date object for the whole 578 application and manipulating it with `setTime()` and `getTime()`. 579 580 Note that since this is used for internal calculations across many 581 `SC.DateTime` instances, it is not guaranteed to store the date/time that 582 any one `SC.DateTime` instance represents. So it might be that 583 584 this._date.getTime() !== this._ms 585 586 Be sure to set it before using for internal calculations if necessary. 587 588 @type Date 589 */ 590 _date: new Date(), 591 592 /** 593 @private 594 595 The offset, in minutes, between UTC and the currently manipulated 596 `SC.DateTime` instance. 597 598 @type Integer 599 */ 600 _tz: 0, 601 602 /** 603 The offset, in minutes, between UTC and the local system time. This 604 property is computed at loading time and should never be changed. 605 606 @type Integer 607 @default new Date().getTimezoneOffset() 608 @constant 609 */ 610 timezone: new Date().getTimezoneOffset(), 611 612 /** 613 @private 614 615 A cache of `SC.DateTime` instances. If you attempt to create a `SC.DateTime` 616 instance that has already been created, then it will return the cached 617 value. 618 619 @type Array 620 */ 621 _dt_cache: {}, 622 623 /** 624 @private 625 626 The index of the latest cached value. Used with `_DT_CACHE_MAX_LENGTH` to 627 limit the size of the cache. 628 629 @type Integer 630 */ 631 _dt_cache_index: -1, 632 633 /** 634 @private 635 636 The maximum length of `_dt_cache`. If this limit is reached, then the cache 637 is overwritten, starting with the oldest element. 638 639 @type Integer 640 */ 641 _DT_CACHE_MAX_LENGTH: 1000, 642 643 /** 644 @private 645 646 Both args are optional, but will only overwrite `_date` and `_tz` if 647 defined. This method does not affect the DateTime instance's actual time, 648 but simply initializes the one `_date` instance to a time relevant for a 649 calculation. (`this._date` is just a resource optimization) 650 651 This is mainly used as a way to store a recursion starting state during 652 internal calculations. 653 654 'milliseconds' is time since Jan 1, 1970. 655 'timezone' is the current time zone we want to be working in internally. 656 657 Returns a hash of the previous milliseconds and time zone in case they 658 are wanted for later restoration. 659 */ 660 _setCalcState: function(ms, timezone) { 661 var previous = { 662 milliseconds: this._date.getTime(), 663 timezone: this._tz 664 }; 665 666 if (ms !== undefined) this._date.setTime(ms); 667 if (timezone !== undefined) this._tz = timezone; 668 669 return previous; 670 }, 671 672 /** 673 @private 674 675 By this time, any time zone setting on 'hash' will be ignored. 676 'timezone' will be used, or the last this._tz. 677 */ 678 _setCalcStateFromHash: function(hash, timezone) { 679 var tz = (timezone !== undefined) ? timezone : this._tz; // use the last-known time zone if necessary 680 var ms = this._toMilliseconds(hash, this._ms, tz); // convert the hash (local to specified time zone) to milliseconds (in UTC) 681 return this._setCalcState(ms, tz); // now call the one we really wanted 682 }, 683 684 /** 685 @private 686 @see SC.DateTime#unknownProperty 687 */ 688 _get: function(key, start, timezone) { 689 var ms, doy, m, y, firstDayOfWeek, dayOfWeek, dayOfYear, prefix, suffix; 690 var currentWeekday, targetWeekday; 691 var d = this._date; 692 var originalTime, v = null; 693 694 // Set up an absolute date/time using the given milliseconds since Jan 1, 1970. 695 // Only do it if we're given a time value, though, otherwise we want to use the 696 // last one we had because this `_get()` method is recursive. 697 // 698 // Note that because these private time calc methods are recursive, and because all DateTime instances 699 // share an internal this._date and `this._tz` state for doing calculations, methods 700 // that modify `this._date` or `this._tz` should restore the last state before exiting 701 // to avoid obscure calculation bugs. So we save the original state here, and restore 702 // it before returning at the end. 703 originalTime = this._setCalcState(start, timezone); // save so we can restore it to how it was before we got here 704 705 // Check this first because it is an absolute value -- no tweaks necessary when calling for milliseconds 706 if (key === 'milliseconds') { 707 v = d.getTime(); 708 } 709 else if (key === 'timezone') { 710 v = this._tz; 711 } 712 713 // 'nextWeekday' or 'lastWeekday'. 714 // We want to do this calculation in local time, before shifting UTC below. 715 if (v === null) { 716 prefix = key.slice(0, 4); 717 suffix = key.slice(4); 718 if (prefix === 'last' || prefix === 'next') { 719 currentWeekday = this._get('dayOfWeek', start, timezone); 720 targetWeekday = this._englishDayNames.indexOf(suffix); 721 if (targetWeekday >= 0) { 722 var delta = targetWeekday - currentWeekday; 723 if (prefix === 'last' && delta >= 0) delta -= 7; 724 if (prefix === 'next' && delta < 0) delta += 7; 725 this._advance({ day: delta }, start, timezone); 726 v = this._createFromCurrentState(); 727 } 728 } 729 } 730 731 if (v === null) { 732 // need to adjust for alternate display time zone. 733 // Before calculating, we need to get everything into a common time zone to 734 // negate the effects of local machine time (so we can use all the 'getUTC...() methods on Date). 735 if (timezone !== undefined) { 736 this._setCalcState(d.getTime() - (timezone * 60000), 0); // make this instance's time zone the new UTC temporarily 737 } 738 739 // simple keys 740 switch (key) { 741 case 'year': 742 v = d.getUTCFullYear(); //TODO: investigate why some libraries do getFullYear().toString() or getFullYear()+"" 743 break; 744 case 'month': 745 v = d.getUTCMonth()+1; // January is 0 in JavaScript 746 break; 747 case 'day': 748 v = d.getUTCDate(); 749 break; 750 case 'dayOfWeek': 751 v = d.getUTCDay(); 752 break; 753 case 'hour': 754 v = d.getUTCHours(); 755 break; 756 case 'minute': 757 v = d.getUTCMinutes(); 758 break; 759 case 'second': 760 v = d.getUTCSeconds(); 761 break; 762 case 'millisecond': 763 v = d.getUTCMilliseconds(); 764 break; 765 case 'elapsed': 766 v = +new Date() - d.getTime() - (timezone * 60000); 767 break; 768 } 769 770 // isLeapYear 771 if ((v === null) && (key === 'isLeapYear')) { 772 y = this._get('year'); 773 v = (y%4 === 0 && y%100 !== 0) || y%400 === 0; 774 } 775 776 // daysInMonth 777 if ((v === null) && (key === 'daysInMonth')) { 778 switch (this._get('month')) { 779 case 4: 780 case 6: 781 case 9: 782 case 11: 783 v = 30; 784 break; 785 case 2: 786 v = this._get('isLeapYear') ? 29 : 28; 787 break; 788 default: 789 v = 31; 790 break; 791 } 792 } 793 794 // dayOfYear 795 if ((v === null) && (key === 'dayOfYear')) { 796 ms = d.getTime(); // save time 797 doy = this._get('day'); 798 this._setCalcStateFromHash({ day: 1 }); 799 for (m = this._get('month') - 1; m > 0; m--) { 800 this._setCalcStateFromHash({ month: m }); 801 doy += this._get('daysInMonth'); 802 } 803 d.setTime(ms); // restore time 804 v = doy; 805 } 806 807 // week, week0 or week1 808 if ((v === null) && (key.slice(0, 4) === 'week')) { 809 // firstDayOfWeek should be 0 (Sunday) or 1 (Monday) 810 firstDayOfWeek = key.length === 4 ? 1 : parseInt(key.slice('4'), 10); 811 dayOfWeek = this._get('dayOfWeek'); 812 dayOfYear = this._get('dayOfYear') - 1; 813 if (firstDayOfWeek === 0) { 814 v = parseInt((dayOfYear - dayOfWeek + 7) / 7, 10); 815 } 816 else { 817 v = parseInt((dayOfYear - (dayOfWeek - 1 + 7) % 7 + 7) / 7, 10); 818 } 819 } 820 } 821 822 // restore the internal calculation state in case someone else was in the 823 // middle of a calculation (we might be recursing). 824 this._setCalcState(originalTime.milliseconds, originalTime.timezone); 825 826 return v; 827 }, 828 829 /** 830 @private 831 832 Sets the internal calculation state to something specified. 833 */ 834 _adjust: function(options, start, timezone, resetCascadingly) { 835 var ms = this._toMilliseconds(options, start, timezone, resetCascadingly); 836 this._setCalcState(ms, timezone); 837 return this; // for chaining 838 }, 839 840 /** 841 @private 842 @see SC.DateTime#advance 843 */ 844 _advance: function(options, start, timezone) { 845 var opts = options ? SC.clone(options) : {}; 846 var tz; 847 848 for (var key in opts) { 849 opts[key] += this._get(key, start, timezone); 850 } 851 852 // The time zone can be advanced by a delta as well, so try to use the 853 // new value if there is one. 854 tz = (opts.timezone !== undefined) ? opts.timezone : timezone; // watch out for zero, which is acceptable as a time zone 855 856 return this._adjust(opts, start, tz, NO); 857 }, 858 859 /* 860 @private 861 862 Converts a standard date/time options hash to an integer representing that position 863 in time relative to Jan 1, 1970 864 */ 865 _toMilliseconds: function(options, start, timezone, resetCascadingly) { 866 var opts = options ? SC.clone(options) : {}; 867 var d = this._date; 868 var previousMilliseconds = d.getTime(); // rather than create a new Date object, we'll reuse the instance we have for calculations, then restore it 869 var ms, tz; 870 871 // Initialize our internal for-calculations Date object to our current date/time. 872 // Note that this object was created in the local machine time zone, so when we set 873 // its params later, it will be assuming these values to be in the same time zone as it is. 874 // It's ok for start to be null, in which case we'll just keep whatever we had in 'd' before. 875 if (!SC.none(start)) { 876 d.setTime(start); // using milliseconds here specifies an absolute location in time, regardless of time zone, so that's nice 877 } 878 879 // We have to get all time expressions, both in 'options' (assume to be in time zone 'timezone') 880 // and in 'd', to the same time zone before we can any calculations correctly. So because the Date object provides 881 // a suite of UTC getters and setters, we'll temporarily redefine 'timezone' as our new 882 // 'UTC', so we don't have to worry about local machine time. We do this by subtracting 883 // milliseconds for the time zone offset. Then we'll do all our calculations, then convert 884 // it back to real UTC. 885 886 // (Zero time zone is considered a valid value.) 887 tz = (timezone !== undefined) ? timezone : (this.timezone !== undefined) ? this.timezone : 0; 888 d.setTime(d.getTime() - (tz * 60000)); // redefine 'UTC' to establish a new local absolute so we can use all the 'getUTC...()' Date methods 889 890 // the time options (hour, minute, sec, millisecond) 891 // reset cascadingly (see documentation) 892 if (resetCascadingly === undefined || resetCascadingly === YES) { 893 if ( !SC.none(opts.hour) && SC.none(opts.minute)) { 894 opts.minute = 0; 895 } 896 if (!(SC.none(opts.hour) && SC.none(opts.minute)) && 897 SC.none(opts.second)) { 898 opts.second = 0; 899 } 900 if (!(SC.none(opts.hour) && SC.none(opts.minute) && SC.none(opts.second)) && 901 SC.none(opts.millisecond)) { 902 opts.millisecond = 0; 903 } 904 } 905 906 // Get the current values for any not provided in the options hash. 907 // Since everything is in 'UTC' now, use the UTC accessors. We do this because, 908 // according to javascript Date spec, you have to set year, month, and day together 909 // if you're setting any one of them. So we'll use the provided Date.UTC() method 910 // to get milliseconds, and we need to get any missing values first... 911 if (SC.none(opts.year)) opts.year = d.getUTCFullYear(); 912 if (SC.none(opts.month)) opts.month = d.getUTCMonth() + 1; // January is 0 in JavaScript 913 if (SC.none(opts.day)) opts.day = d.getUTCDate(); 914 if (SC.none(opts.hour)) opts.hour = d.getUTCHours(); 915 if (SC.none(opts.minute)) opts.minute = d.getUTCMinutes(); 916 if (SC.none(opts.second)) opts.second = d.getUTCSeconds(); 917 if (SC.none(opts.millisecond)) opts.millisecond = d.getUTCMilliseconds(); 918 919 // Ask the JS Date to calculate milliseconds for us (still in redefined UTC). It 920 // is best to set them all together because, for example, a day value means different things 921 // to the JS Date object depending on which month or year it is. It can now handle that stuff 922 // internally as it's made to do. 923 ms = Date.UTC(opts.year, opts.month - 1, opts.day, opts.hour, opts.minute, opts.second, opts.millisecond); 924 925 // Now that we've done all our calculations in a common time zone, add back the offset 926 // to move back to real UTC. 927 d.setTime(ms + (tz * 60000)); 928 ms = d.getTime(); // now get the corrected milliseconds value 929 930 // Restore what was there previously before leaving in case someone called this method 931 // in the middle of another calculation. 932 d.setTime(previousMilliseconds); 933 934 return ms; 935 }, 936 937 /** 938 Returns a new `SC.DateTime` object advanced according the the given parameters. 939 The parameters can be: 940 941 - none, to create a `SC.DateTime` instance initialized to the current 942 date and time in the local timezone, 943 - a integer, the number of milliseconds since 944 January, 1st 1970 00:00:00.0 UTC 945 - a options hash that can contain any of the following properties: year, 946 month, day, hour, minute, second, millisecond, timezone 947 948 Note that if you attempt to create a `SC.DateTime` instance that has already 949 been created, then, for performance reasons, a cached value may be 950 returned. 951 952 The timezone option is the offset, in minutes, between UTC and local time. 953 If you don't pass a timezone option, the date object is created in the 954 local timezone. If you want to create a UTC+2 (CEST) date, for example, 955 then you should pass a timezone of -120. 956 957 @param options one of the three kind of parameters described above 958 @returns {SC.DateTime} the SC.DateTime instance that corresponds to the 959 passed parameters, possibly fetched from cache 960 */ 961 create: function() { 962 var arg = arguments.length === 0 ? {} : arguments[0]; 963 var timezone; 964 965 // if simply milliseconds since Jan 1, 1970 are given, just use those 966 if (SC.typeOf(arg) === SC.T_NUMBER) { 967 arg = { milliseconds: arg }; 968 } 969 970 // Default to local machine time zone if none is given 971 timezone = (arg.timezone !== undefined) ? arg.timezone : this.timezone; 972 if (timezone === undefined) timezone = 0; 973 974 // Desired case: create with milliseconds if we have them. 975 // If we don't, convert what we have to milliseconds and recurse. 976 if (!SC.none(arg.milliseconds)) { 977 978 // quick implementation of a FIFO set for the cache 979 var key = 'nu' + arg.milliseconds + timezone, cache = this._dt_cache; 980 var ret = cache[key]; 981 if (!ret) { 982 var previousKey, idx = this._dt_cache_index, C = this; 983 ret = cache[key] = new C([{ _ms: arg.milliseconds, timezone: timezone }]); 984 idx = this._dt_cache_index = (idx + 1) % this._DT_CACHE_MAX_LENGTH; 985 previousKey = cache[idx]; 986 if (previousKey !== undefined && cache[previousKey]) delete cache[previousKey]; 987 cache[idx] = key; 988 } 989 return ret; 990 } 991 // otherwise, convert what we have to milliseconds and try again 992 else { 993 var now = new Date(); 994 995 return this.create({ // recursive call with new arguments 996 milliseconds: this._toMilliseconds(arg, now.getTime(), timezone, arg.resetCascadingly), 997 timezone: timezone 998 }); 999 } 1000 }, 1001 1002 /** 1003 @private 1004 1005 Calls the `create()` method with the current internal `_date` value. 1006 1007 @return {SC.DateTime} the SC.DateTime instance returned by create() 1008 */ 1009 _createFromCurrentState: function() { 1010 return this.create({ 1011 milliseconds: this._date.getTime(), 1012 timezone: this._tz 1013 }); 1014 }, 1015 1016 /** 1017 Returns a `SC.DateTime` object created from a given string parsed with a given 1018 format. Returns `null` if the parsing fails. 1019 1020 @see SC.DateTime#toFormattedString for a description of the format parameter 1021 @param {String} str the string to parse 1022 @param {String} fmt the format to parse the string with 1023 @returns {DateTime} the DateTime corresponding to the string parameter 1024 */ 1025 parse: function(str, fmt) { 1026 // Declared as an object not a literal since in some browsers the literal 1027 // retains state across function calls 1028 var re = new RegExp('(?:%([aAbBcdDhHiIjmMpsSUWwxXyYZ%])|(.))', "g"); 1029 var d, parts, opts = {}, check = {}, scanner = SC.Scanner.create({string: str}); 1030 1031 if (SC.none(fmt)) fmt = SC.DATETIME_ISO8601; 1032 1033 try { 1034 while ((parts = re.exec(fmt)) !== null) { 1035 switch(parts[1]) { 1036 case 'a': check.dayOfWeek = scanner.scanArray(this.abbreviatedDayNames); break; 1037 case 'A': check.dayOfWeek = scanner.scanArray(this.dayNames); break; 1038 case 'b': opts.month = scanner.scanArray(this.abbreviatedMonthNames) + 1; break; 1039 case 'B': opts.month = scanner.scanArray(this.monthNames) + 1; break; 1040 case 'c': throw new Error("%c is not implemented"); 1041 case 'd': 1042 case 'D': opts.day = scanner.scanInt(1, 2); break; 1043 case 'e': throw new Error("%e is not implemented"); 1044 case 'E': throw new Error("%E is not implemented"); 1045 case 'h': 1046 case 'H': opts.hour = scanner.scanInt(1, 2); break; 1047 case 'i': 1048 case 'I': opts.hour = scanner.scanInt(1, 2); break; 1049 case 'j': throw new Error("%j is not implemented"); 1050 case 'm': opts.month = scanner.scanInt(1, 2); break; 1051 case 'M': opts.minute = scanner.scanInt(1, 2); break; 1052 case 'p': opts.meridian = scanner.scanArray(this.AMPMNames); break; 1053 case 'S': opts.second = scanner.scanInt(1, 2); break; 1054 case 's': opts.millisecond = scanner.scanInt(1, 3); break; 1055 case 'U': throw new Error("%U is not implemented"); 1056 case 'W': throw new Error("%W is not implemented"); 1057 case 'w': throw new Error("%w is not implemented"); 1058 case 'x': throw new Error("%x is not implemented"); 1059 case 'X': throw new Error("%X is not implemented"); 1060 case 'y': opts.year = scanner.scanInt(2); opts.year += (opts.year > 70 ? 1900 : 2000); break; 1061 case 'Y': opts.year = scanner.scanInt(4); break; 1062 case 'Z': 1063 var modifier = scanner.scan(1); 1064 if (modifier === 'Z') { 1065 opts.timezone = 0; 1066 } else if (modifier === '+' || modifier === '-' ) { 1067 var h = scanner.scanInt(2); 1068 if (scanner.scan(1) !== ':') scanner.scan(-1); 1069 var m = scanner.scanInt(2); 1070 opts.timezone = (modifier === '+' ? -1 : 1) * (h*60 + m); 1071 } 1072 break; 1073 case '%': scanner.skipString('%'); break; 1074 default: scanner.skipString(parts[0]); break; 1075 } 1076 } 1077 } catch (e) { 1078 SC.Logger.log('SC.DateTime.createFromString ' + e.toString()); 1079 return null; 1080 } 1081 1082 if (!SC.none(opts.meridian) && !SC.none(opts.hour)) { 1083 if ((opts.meridian === 1 && opts.hour !== 12) || 1084 (opts.meridian === 0 && opts.hour === 12)) { 1085 opts.hour = (opts.hour + 12) % 24; 1086 } 1087 delete opts.meridian; 1088 } 1089 1090 d = this.create(opts); 1091 1092 if (!SC.none(check.dayOfWeek) && d.get('dayOfWeek') !== check.dayOfWeek) { 1093 return null; 1094 } 1095 1096 return d; 1097 }, 1098 1099 /** 1100 @private 1101 1102 Converts the x parameter into a string padded with 0s so that the string’s 1103 length is at least equal to the len parameter. 1104 1105 @param {Object} x the object to convert to a string 1106 @param {Integer} the minimum length of the returned string 1107 @returns {String} the padded string 1108 */ 1109 _pad: function(x, len) { 1110 var str = '' + x; 1111 if (len === undefined) len = 2; 1112 while (str.length < len) str = '0' + str; 1113 return str; 1114 }, 1115 1116 /** 1117 @private 1118 @see SC.DateTime#_toFormattedString 1119 */ 1120 __toFormattedString: function(part, start, timezone) { 1121 var hour, offset; 1122 1123 // Note: all calls to _get() here should include only one 1124 // argument, since _get() is built for recursion and behaves differently 1125 // if arguments 2 and 3 are included. 1126 // 1127 // This method is simply a helper for this._toFormattedString() (one underscore); 1128 // this is only called from there, and _toFormattedString() has already 1129 // set up the appropriate internal date/time/timezone state for it. 1130 1131 switch(part[1]) { 1132 case 'a': return this.abbreviatedDayNames[this._get('dayOfWeek')]; 1133 case 'A': return this.dayNames[this._get('dayOfWeek')]; 1134 case 'b': return this.abbreviatedMonthNames[this._get('month')-1]; 1135 case 'B': return this.monthNames[this._get('month')-1]; 1136 case 'c': return this._date.toString(); 1137 case 'd': return this._pad(this._get('day')); 1138 case 'D': return this._get('day'); 1139 case 'E': return this._toFormattedString(this.__getElapsedStringFormat(start, timezone), start, timezone); 1140 case 'h': return this._get('hour'); 1141 case 'H': return this._pad(this._get('hour')); 1142 case 'i': 1143 hour = this._get('hour'); 1144 return (hour === 12 || hour === 0) ? 12 : (hour + 12) % 12; 1145 case 'I': 1146 hour = this._get('hour'); 1147 return this._pad((hour === 12 || hour === 0) ? 12 : (hour + 12) % 12); 1148 case 'j': return this._pad(this._get('dayOfYear'), 3); 1149 case 'm': return this._pad(this._get('month')); 1150 case 'M': return this._pad(this._get('minute')); 1151 case 'o': return this._get('day').ordinal(); 1152 case 'p': return this._get('hour') > 11 ? this.AMPMNames[1] : this.AMPMNames[0]; 1153 case 'S': return this._pad(this._get('second')); 1154 case 's': return this._pad(this._get('millisecond'), 3); 1155 case 'u': return this._pad(this._get('utc')); //utc 1156 case 'U': return this._pad(this._get('week0')); 1157 case 'W': return this._pad(this._get('week1')); 1158 case 'w': return this._get('dayOfWeek'); 1159 case 'x': return this._date.toDateString(); 1160 case 'X': return this._date.toTimeString(); 1161 case 'y': return this._pad(this._get('year') % 100); 1162 case 'Y': return this._get('year'); 1163 case 'Z': 1164 offset = -1 * timezone; 1165 return (offset >= 0 ? '+' : '-') + 1166 this._pad(parseInt(Math.abs(offset)/60, 10)) + 1167 ':' + 1168 this._pad(Math.abs(offset)%60); 1169 case '%': return '%'; 1170 } 1171 }, 1172 1173 /** 1174 @private 1175 @see SC.DateTime#toFormattedString 1176 */ 1177 _toFormattedString: function(format, start, timezone) { 1178 var that = this; 1179 1180 // need to move into local time zone for these calculations 1181 this._setCalcState(start - (timezone * 60000), 0); // so simulate a shifted 'UTC' time 1182 1183 return format.replace(/\%([aAbBcdeEDhHiIjmMopsSUWwxXyYZ\%])/g, function() { 1184 var v = that.__toFormattedString.call(that, arguments, start, timezone); 1185 return v; 1186 }); 1187 }, 1188 1189 /** 1190 @private 1191 @see SC.DateTime#toFormattedString 1192 */ 1193 __getElapsedStringFormat: function(start, timezone) { 1194 return ""; 1195 }, 1196 1197 /** 1198 This will tell you which of the two passed `DateTime` is greater by 1199 comparing their number of milliseconds since 1200 January, 1st 1970 00:00:00.0 UTC. 1201 1202 @param {SC.DateTime} a the first DateTime instance 1203 @param {SC.DateTime} b the second DateTime instance 1204 @returns {Integer} -1 if a < b, 1205 +1 if a > b, 1206 0 if a == b 1207 */ 1208 compare: function(a, b) { 1209 if (SC.none(a) || SC.none(b)) throw new Error("You must pass two valid dates to compare()"); 1210 var ma = a.get('milliseconds'); 1211 var mb = b.get('milliseconds'); 1212 return ma < mb ? -1 : ma === mb ? 0 : 1; 1213 }, 1214 1215 /** 1216 This will tell you which of the two passed DateTime is greater 1217 by only comparing the date parts of the passed objects. Only dates 1218 with the same timezone can be compared. 1219 1220 @param {SC.DateTime} a the first DateTime instance 1221 @param {SC.DateTime} b the second DateTime instance 1222 @returns {Integer} -1 if a < b, 1223 +1 if a > b, 1224 0 if a == b 1225 @throws {SC.DATETIME_COMPAREDATE_TIMEZONE_ERROR} if the passed arguments 1226 don't have the same timezone 1227 */ 1228 compareDate: function(a, b) { 1229 if (SC.none(a) || SC.none(b)) throw new Error("You must pass two valid dates to compareDate()"); 1230 if (a.get('timezone') !== b.get('timezone')) SC.DATETIME_COMPAREDATE_TIMEZONE_ERROR.throw(); 1231 var ma = a.adjust({hour: 0}).get('milliseconds'); 1232 var mb = b.adjust({hour: 0}).get('milliseconds'); 1233 return ma < mb ? -1 : ma === mb ? 0 : 1; 1234 }, 1235 1236 /** 1237 Returns the interval of time between the two passed in DateTimes in units 1238 according to the format. 1239 1240 You can display the difference in weeks (w), days (d), hours (h), minutes (M) 1241 or seconds (S). 1242 1243 @param {SC.DateTime} a the first DateTime instance 1244 @param {SC.DateTime} b the second DateTime instance 1245 @param {String} format the interval to get the difference in 1246 */ 1247 difference: function(a, b, format) { 1248 if (SC.none(a) || SC.none(b)) throw new Error("You must pass two valid dates to difference()"); 1249 var ma = a.get('milliseconds'), 1250 mb = b.get('milliseconds'), 1251 diff = mb - ma, 1252 divider; 1253 1254 switch(format) { 1255 case 'd': 1256 case 'D': 1257 divider = 864e5; // day: 1000 * 60 * 60 * 24 1258 break; 1259 case 'h': 1260 case 'H': 1261 divider = 36e5; // hour: 1000 * 60 * 60 1262 break; 1263 case 'M': 1264 divider = 6e4; // minute: 1000 * 60 1265 break; 1266 case 'S': 1267 divider = 1e3; // second: 1000 1268 break; 1269 case 's': 1270 divider = 1; 1271 break; 1272 case 'W': 1273 divider = 6048e5; // week: 1000 * 60 * 60 * 24 * 7 1274 break; 1275 default: 1276 throw new Error(format + " is not supported"); 1277 } 1278 1279 var ret = diff/divider; 1280 1281 return Math.round(ret); 1282 } 1283 1284 1285 1286 }); 1287 1288 /** 1289 Adds a transform to format the DateTime value to a String value according 1290 to the passed format string. 1291 1292 valueBinding: SC.Binding.dateTime('%Y-%m-%d %H:%M:%S') 1293 .from('MyApp.myController.myDateTime'); 1294 1295 @param {String} format format string 1296 @returns {SC.Binding} this 1297 */ 1298 SC.Binding.dateTime = function (format) { 1299 return this.transform(function (value) { 1300 return value ? value.toFormattedString(format) : null; 1301 }); 1302 }; 1303