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