1
1
/*global define*/
2
- define ( [ './DeveloperError' , './binarySearch' , './TimeConstants' , './LeapSecond' , './TimeStandard' ] , function ( DeveloperError , binarySearch , TimeConstants , LeapSecond , TimeStandard ) {
2
+ define ( [ 'Core/DeveloperError' , 'Core/binarySearch' , 'Core/TimeConstants' , 'Core/LeapSecond' , 'Core/TimeStandard' , 'Core/isLeapYear' ] ,
3
+ function ( DeveloperError , binarySearch , TimeConstants , LeapSecond , TimeStandard , isLeapYear ) {
3
4
"use strict" ;
4
5
5
- function computeJulianDateComponents ( date ) {
6
+ var daysInMonth = [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ] ;
7
+ var daysInLeapFeburary = 29 ;
8
+
9
+ function computeJulianDateComponents ( year , month , day , hour , minute , second , millisecond ) {
6
10
// Algorithm from page 604 of the Explanatory Supplement to the
7
11
// Astronomical Almanac (Seidelmann 1992).
8
12
9
- var month = date . getUTCMonth ( ) + 1 ; // getUTCMonth returns a value 0-11.
10
- var day = date . getUTCDate ( ) ;
11
- var year = date . getUTCFullYear ( ) ;
12
-
13
13
var a = ( ( month - 14 ) / 12 ) | 0 ;
14
14
var b = ( year + 4800 + a ) | 0 ;
15
-
16
15
var dayNumber = ( ( ( ( 1461 * b ) / 4 ) | 0 ) + ( ( ( 367 * ( month - 2 - 12 * a ) ) / 12 ) | 0 ) - ( ( ( 3 * ( ( b + 100 ) / 100 ) ) / 4 ) | 0 ) + day - 32075 ) | 0 ;
17
16
18
- var hour = date . getUTCHours ( ) ;
19
- var minute = date . getUTCMinutes ( ) ;
20
- var second = date . getUTCSeconds ( ) ;
21
- var millisecond = date . getUTCMilliseconds ( ) ;
22
-
23
17
// JulianDates are noon-based
24
18
hour = hour - 12 ;
25
19
if ( hour < 0 ) {
@@ -35,6 +29,32 @@ define(['./DeveloperError', './binarySearch', './TimeConstants', './LeapSecond',
35
29
return [ dayNumber , secondsOfDay ] ;
36
30
}
37
31
32
+ function computeJulianDateComponentsFromDate ( date ) {
33
+ return computeJulianDateComponents ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) + 1 , date . getUTCDate ( ) , date . getUTCHours ( ) , date . getUTCMinutes ( ) , date . getUTCSeconds ( ) , date
34
+ . getUTCMilliseconds ( ) ) ;
35
+ }
36
+
37
+ //Regular expressions used for ISO8601 date parsing.
38
+
39
+ //YYYY
40
+ var matchCalendarYear = / ^ ( \d { 4 } ) $ / ;
41
+ //YYYY-MM (YYYYMM is invalid)
42
+ var matchCalendarMonth = / ^ ( \d { 4 } ) - ( \d { 2 } ) $ / ;
43
+ //YYYY-DDD or YYYYDDD
44
+ var matchOrdinalDate = / ^ ( \d { 4 } ) - * ( \d { 3 } ) $ / ;
45
+ //YYYY-Www or YYYYWww or YYYY-Www-D or YYYYWwwD
46
+ var matchWeekDate = / ^ ( \d { 4 } ) - * W ( \d { 2 } ) - * ( \d { 1 } ) * $ / ;
47
+ //YYYY-MM-DD or YYYYMMDD
48
+ var matchCalendarDate = / ^ ( \d { 4 } ) - * ( \d { 2 } ) - * ( \d { 2 } ) $ / ;
49
+ // Match utc offset
50
+ var utcOffset = / ( [ Z + \- ] ) * ( \d { 2 } ) * : * ( \d { 2 } ) * $ / ;
51
+ // Match hours HH or HH.xxxxx
52
+ var matchHours = / ^ ( \d { 2 } ) ( \. \d + ) * / . source + utcOffset . source ;
53
+ // Match hours/minutes HH:MM HHMM.xxxxx
54
+ var matchHoursMinutes = / ^ ( \d { 2 } ) : * ( \d { 2 } ) ( \. \d + ) * / . source + utcOffset . source ;
55
+ // Match hours/minutes HH:MM:SS HHMMSS.xxxxx
56
+ var matchHoursMinutesSeconds = / ^ ( \d { 2 } ) : * ( \d { 2 } ) : * ( \d { 2 } ) ( \. \d + ) * / . source + utcOffset . source ;
57
+
38
58
/**
39
59
* <p>Constructs an immutable JulianDate instance from a Julian day number and the number of seconds elapsed
40
60
* into that day as arguments (along with an optional time standard). Passing no parameters will
@@ -102,7 +122,7 @@ define(['./DeveloperError', './binarySearch', './TimeConstants', './LeapSecond',
102
122
} else {
103
123
//Create a new date from the current time.
104
124
var date = new Date ( ) ;
105
- var components = computeJulianDateComponents ( date ) ;
125
+ var components = computeJulianDateComponentsFromDate ( date ) ;
106
126
wholeDays = components [ 0 ] ;
107
127
secondsOfDay = components [ 1 ] ;
108
128
timeStandard = TimeStandard . UTC ;
@@ -157,22 +177,22 @@ define(['./DeveloperError', './binarySearch', './TimeConstants', './LeapSecond',
157
177
throw new DeveloperError ( "Valid JavaScript Date required." , "date" ) ;
158
178
}
159
179
160
- var components = computeJulianDateComponents ( date ) ;
161
- var wholeDays = components [ 0 ] ;
162
- var secondsOfDay = components [ 1 ] ;
163
- var result = new JulianDate ( wholeDays , secondsOfDay , timeStandard ) ;
180
+ var components = computeJulianDateComponentsFromDate ( date ) ;
181
+ var result = new JulianDate ( components [ 0 ] , components [ 1 ] , timeStandard ) ;
164
182
result . _date = date ;
165
183
return result ;
166
184
} ;
167
185
168
186
/**
169
- * Creates an immutable JulianDate instance from a ISO 8601 date string.
170
- * <br/>
187
+ * <p>
188
+ * Creates an immutable JulianDate instance from an ISO 8601 date string. Unlike Date.parse,
189
+ * this method will properly account for all valid formats defined by the ISO 8601
190
+ * specification. It will also properly handle leap seconds and sub-millisecond times.
191
+ * <p/>
171
192
*
172
193
* @memberof JulianDate
173
194
*
174
195
* @param {String } iso8601String The ISO 8601 date string representing the time to be converted to a Julian date.
175
- * @param {TimeStandard } [timeStandard = TimeStandard.UTC] Indicates the time standard in which this Julian date is represented.
176
196
*
177
197
* @return {JulianDate } The new {@Link JulianDate} instance.
178
198
*
@@ -181,23 +201,212 @@ define(['./DeveloperError', './binarySearch', './TimeConstants', './LeapSecond',
181
201
* @see JulianDate
182
202
* @see JulianDate.fromTotalDays
183
203
* @see JulianDate.fromDate
184
- * @see TimeStandard
185
204
* @see LeapSecond
186
205
* @see <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>.
187
206
*
188
207
* @example
189
- * // Example 1. Construct a Julian date using the default UTC TimeStandard.
208
+ * // Example 1. Construct a Julian date in UTC at April 24th, 2012 6:08PM UTC
190
209
* var julianDate = JulianDate.fromIso8601("2012-04-24T18:08Z");
210
+ * // Example 2. Construct a Julian date in local time April 24th, 2012 12:00 AM
211
+ * var localDay = JulianDate.fromIso8601("2012-04-24");
212
+ * // Example 3. Construct a Julian date 5 hours behind UTC April 24th, 2012 5:00 pm UTC
213
+ * var localDay = JulianDate.fromIso8601("2012-04-24T12:00-05:00");
191
214
*/
192
- JulianDate . fromIso8601 = function ( iso8601String , timeStandard ) {
193
- //FIXME Date.parse is only accurate to the millisecond and fails
194
- //completely on leap seconds. We should parse the string directly.
215
+ JulianDate . fromIso8601 = function ( iso8601String ) {
216
+ if ( typeof iso8601String !== 'string' ) {
217
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
218
+ }
219
+
220
+ //Comma and decimal point both indicate a fractional number according to ISO 8601,
221
+ //start out by blanket replacing , with . which is the only valid such symbol in JS.
222
+ iso8601String = iso8601String . replace ( ',' , '.' ) ;
223
+
224
+ //Split the string into it's date and time components, denoted by a mandatory T
225
+ var tokens = iso8601String . split ( 'T' ) , year , month = 1 , day = 1 , hours = 0 , minutes = 0 , seconds = 0 , milliseconds = 0 ;
226
+
227
+ //Lacking a time is okay, but a missing date is illegal.
228
+ if ( typeof tokens [ 0 ] === 'undefined' ) {
229
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
230
+ }
231
+
232
+ var date = tokens [ 0 ] ;
233
+ var time = tokens [ 1 ] ;
234
+ var leapYear ;
235
+ if ( typeof date === 'undefined' ) {
236
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
237
+ }
238
+
239
+ tokens = date . match ( matchCalendarDate ) ;
240
+ if ( tokens !== null ) {
241
+ year = + tokens [ 1 ] ;
242
+ month = + tokens [ 2 ] ;
243
+ day = + tokens [ 3 ] ;
244
+ } else {
245
+ tokens = date . match ( matchCalendarMonth ) ;
246
+ if ( tokens !== null ) {
247
+ year = + tokens [ 1 ] ;
248
+ month = + tokens [ 2 ] ;
249
+ } else {
250
+ tokens = date . match ( matchCalendarYear ) ;
251
+ if ( tokens !== null ) {
252
+ year = + tokens [ 1 ] ;
253
+ } else {
254
+ tokens = date . match ( matchOrdinalDate ) ;
255
+ if ( tokens !== null ) {
256
+ year = + tokens [ 1 ] ;
257
+ var dayOfYear = + tokens [ 2 ] ;
258
+ leapYear = isLeapYear ( year ) ;
259
+
260
+ if ( dayOfYear < 1 || ( leapYear && dayOfYear > 366 ) || ( ! leapYear && dayOfYear > 365 ) ) {
261
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
262
+ }
263
+
264
+ var jsDate = new Date ( Date . UTC ( year , 0 , 1 ) ) ;
265
+ jsDate . setUTCDate ( dayOfYear ) ;
266
+ month = jsDate . getUTCMonth ( ) + 1 ;
267
+ day = jsDate . getUTCDate ( ) ;
268
+ } else {
269
+ tokens = date . match ( matchWeekDate ) ;
270
+ if ( tokens !== null ) {
271
+ year = + tokens [ 1 ] ;
272
+ var weekNumber = + tokens [ 2 ] ;
273
+ var dayOfWeek = + tokens [ 3 ] || 0 ;
274
+
275
+ var january4 = new Date ( Date . UTC ( year , 0 , 4 ) ) ;
276
+ var utcDay = january4 . getUTCDay ( ) ;
277
+ var ordinalDate = ( weekNumber * 7 ) + dayOfWeek - utcDay - 3 ;
278
+
279
+ var sdf = new Date ( Date . UTC ( year , 0 , 1 ) ) ;
280
+ sdf . setUTCDate ( ordinalDate ) ;
281
+ month = sdf . getUTCMonth ( ) + 1 ;
282
+ day = sdf . getUTCDate ( ) ;
283
+ } else {
284
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
195
290
196
- var totalMilliseconds = Date . parse ( iso8601String ) ;
197
- if ( totalMilliseconds === null || isNaN ( totalMilliseconds ) ) {
291
+ leapYear = isLeapYear ( year ) ;
292
+ if ( month < 1 || month > 12 || day < 1 || ( ( month !== 2 || ! leapYear ) && day > daysInMonth [ month - 1 ] ) || ( leapYear && month === 2 && day > daysInLeapFeburary ) ) {
198
293
throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
199
294
}
200
- return JulianDate . fromDate ( new Date ( totalMilliseconds ) , timeStandard ) ;
295
+
296
+ var offsetIndex ;
297
+ if ( typeof time !== 'undefined' ) {
298
+ tokens = time . match ( matchHoursMinutesSeconds ) ;
299
+ if ( tokens !== null ) {
300
+ hours = + tokens [ 1 ] ;
301
+ minutes = + tokens [ 2 ] ;
302
+ seconds = + tokens [ 3 ] ;
303
+ milliseconds = + ( tokens [ 4 ] || 0 ) * 1000.0 ;
304
+ offsetIndex = 5 ;
305
+ } else {
306
+ tokens = time . match ( matchHoursMinutes ) ;
307
+ if ( tokens !== null ) {
308
+ hours = + tokens [ 1 ] ;
309
+ minutes = + tokens [ 2 ] ;
310
+ seconds = + ( tokens [ 3 ] || 0 ) * 60.0 ;
311
+ offsetIndex = 4 ;
312
+ } else {
313
+ tokens = time . match ( matchHours ) ;
314
+ if ( tokens !== null ) {
315
+ hours = + tokens [ 1 ] ;
316
+ minutes = + ( tokens [ 2 ] || 0 ) * 60.0 ;
317
+ offsetIndex = 3 ;
318
+ } else {
319
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
320
+ }
321
+ }
322
+ }
323
+
324
+ if ( minutes >= 60 || seconds > 60 || hours > 24 || ( hours === 24 && ( minutes > 0 || seconds > 0 || milliseconds > 0 ) ) ) {
325
+ throw new DeveloperError ( "Valid ISO 8601 date string required." , "iso8601String" ) ;
326
+ }
327
+
328
+ var offset = tokens [ offsetIndex ] ;
329
+ var offsetHours = + ( tokens [ offsetIndex + 1 ] ) ;
330
+ var offsetMinutes = + ( tokens [ offsetIndex + 2 ] || 0 ) ;
331
+ switch ( offset ) {
332
+ case '+' :
333
+ hours = hours - offsetHours ;
334
+ minutes = minutes - offsetMinutes ;
335
+ break ;
336
+ case '-' :
337
+ hours = hours + offsetHours ;
338
+ minutes = minutes + offsetMinutes ;
339
+ break ;
340
+ case 'Z' :
341
+ break ;
342
+ default :
343
+ minutes = minutes + new Date ( Date . UTC ( year , month - 1 , day , hours , minutes ) ) . getTimezoneOffset ( ) ;
344
+ break ;
345
+ }
346
+ } else {
347
+ //If no time is specified, we it is considered the beginning of the day, local time.
348
+ minutes = minutes + new Date ( Date . UTC ( year , month - 1 , day ) ) . getTimezoneOffset ( ) ;
349
+ }
350
+
351
+ //ISO8601 denotes a leap second by any time having a seconds component of exactly 60 seconds.
352
+ //If that's the case, we need to temporarily subtract a second in order to build a UTC date.
353
+ //Then we add it back in after converting to TAI.
354
+ var isLeapSecond = seconds === 60 ;
355
+ if ( isLeapSecond ) {
356
+ seconds -- ;
357
+ }
358
+
359
+ //Even if we successfully parsed the string into it's components, after applying UTC offset or
360
+ //special cases like 24:00:00 denoting midnight, we need to normalize the data appropriately.
361
+
362
+ //milliseconds can never be greater than 1000, so we start with seconds
363
+ while ( seconds >= 60 ) {
364
+ seconds -= 60 ;
365
+ hours ++ ;
366
+ }
367
+ while ( hours >= 24 ) {
368
+ hours -= 24 ;
369
+ day ++ ;
370
+ }
371
+ if ( leapYear && month === 2 ) {
372
+ while ( day >= daysInLeapFeburary ) {
373
+ day -= daysInLeapFeburary ;
374
+ month ++ ;
375
+ }
376
+ } else {
377
+ var monthCount = daysInMonth [ month - 1 ] ;
378
+ while ( day >= monthCount ) {
379
+ day -= monthCount ;
380
+ month ++ ;
381
+ }
382
+ }
383
+ while ( month >= 12 ) {
384
+ month -= 12 ;
385
+ year ++ ;
386
+ }
387
+
388
+ //If UTC offset is at the beginning/end of the day, hours can be negative.
389
+ while ( hours < 0 ) {
390
+ hours += 24 ;
391
+ day -- ;
392
+ }
393
+ while ( day < 0 ) {
394
+ day += daysInMonth [ month - 1 ] ;
395
+ month -- ;
396
+ }
397
+ while ( month < 0 ) {
398
+ month += 12 ;
399
+ year -- ;
400
+ }
401
+
402
+ var components = computeJulianDateComponents ( year , month , day , hours , minutes , seconds , milliseconds ) ;
403
+ var result = new JulianDate ( components [ 0 ] , components [ 1 ] , TimeStandard . UTC ) ;
404
+
405
+ if ( isLeapSecond ) {
406
+ result = TimeStandard . convertUtcToTai ( result ) . addSeconds ( 1 ) ;
407
+ }
408
+
409
+ return result ;
201
410
} ;
202
411
203
412
/**
@@ -642,56 +851,6 @@ define(['./DeveloperError', './binarySearch', './TimeConstants', './LeapSecond',
642
851
return new JulianDate ( newJulianDayNumber , this . _secondsOfDay , this . _timeStandard ) ;
643
852
} ;
644
853
645
- /**
646
- * Computes the fraction of the year corresponding to this Julian date. Leap years
647
- * are taken into account.
648
- *
649
- * @memberof JulianDate
650
- *
651
- * @return {Number } The fraction of the current year that has passed.
652
- *
653
- * @example
654
- * var date = new Date(2011, 0, 2); // January 2, 2011 @ 0:00
655
- * date.setUTCHours(0, 0, 0, 0);
656
- * var julianDate = JulianDate.fromDate(date);
657
- * var yearFraction = julianDate.toYearFraction(); //1.0/365.0
658
- */
659
- JulianDate . prototype . toYearFraction = function ( ) {
660
- var commonYearCumulativeMonthTable = [ 0 , 31 , 59 , 90 , 120 , 151 , 181 , 212 , 243 , 273 , 304 , 334 ] ;
661
- var leapYearCumulativeMonthTable = [ 0 , 31 , 60 , 91 , 121 , 152 , 182 , 213 , 244 , 274 , 305 , 335 ] ;
662
- var dayInYear ;
663
- var fractionOfDay ;
664
-
665
- function isLeapYear ( year ) {
666
- return ( ( year % 4 === 0 ) && ( year % 100 !== 0 ) ) || ( year % 400 === 0 ) ;
667
- }
668
-
669
- function dayOfYear ( date ) {
670
- var day = date . getDate ( ) ;
671
- var month = date . getMonth ( ) ;
672
- if ( isLeapYear ( date . getFullYear ( ) ) ) {
673
- return day + leapYearCumulativeMonthTable [ month ] ;
674
- }
675
- return day + commonYearCumulativeMonthTable [ month ] ;
676
- }
677
-
678
- var date = this . toDate ( ) ;
679
- if ( this . _secondsOfDay / TimeConstants . SECONDS_PER_DAY < 0.5 ) {
680
- dayInYear = dayOfYear ( date ) - 1 ;
681
- fractionOfDay = ( this . _secondsOfDay / TimeConstants . SECONDS_PER_DAY ) + 0.5 ;
682
- } else {
683
- date . setDate ( date . getDate ( ) + 1 ) ;
684
- dayInYear = dayOfYear ( date ) - 1 ;
685
- fractionOfDay = ( this . _secondsOfDay / TimeConstants . SECONDS_PER_DAY ) - 0.5 ;
686
- }
687
-
688
- if ( isLeapYear ( date . getFullYear ( ) ) ) {
689
- return ( dayInYear + fractionOfDay ) / 366.0 ;
690
- }
691
-
692
- return ( dayInYear + fractionOfDay ) / 365.0 ;
693
- } ;
694
-
695
854
/**
696
855
* Returns true if <code>other</code> occurs after this Julian date.
697
856
*
0 commit comments