rasputin 0.10.2 → 0.10.3

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