Skip to content

Commit

Permalink
More clean-up to JulianDate
Browse files Browse the repository at this point in the history
1. Better detect other forms of invalid input.
2. Fix a few cases where valid input is handleded incorrectly.
3. Additional tests for both of the above.
  • Loading branch information
mramato committed May 4, 2012
1 parent 2c59852 commit d4aed3a
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 32 deletions.
99 changes: 70 additions & 29 deletions Source/Core/JulianDate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
/*global define*/
define(['Core/DeveloperError', 'Core/binarySearch', 'Core/TimeConstants', 'Core/LeapSecond', 'Core/TimeStandard', 'Core/isLeapYear'],
function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard, isLeapYear) {
define(['Core/DeveloperError',
'Core/binarySearch',
'Core/TimeConstants',
'Core/LeapSecond',
'Core/TimeStandard',
'Core/isLeapYear'],
function(DeveloperError,
binarySearch,
TimeConstants,
LeapSecond,
TimeStandard,
isLeapYear) {
"use strict";

var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
Expand All @@ -11,8 +21,8 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
// Astronomical Almanac (Seidelmann 1992).

var a = ((month - 14) / 12) | 0;
var b = (year + 4800 + a) | 0;
var dayNumber = ((((1461 * b) / 4) | 0) + (((367 * (month - 2 - 12 * a)) / 12) | 0) - (((3 * ((b + 100) / 100)) / 4) | 0) + day - 32075) | 0;
var b = year + 4800 + a;
var dayNumber = (((1461 * b) / 4) | 0) + (((367 * (month - 2 - 12 * a)) / 12) | 0) - (((3 * ((b + 100) / 100)) / 4) | 0) + day - 32075;

// JulianDates are noon-based
hour = hour - 12;
Expand Down Expand Up @@ -41,19 +51,19 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
//YYYY-MM (YYYYMM is invalid)
var matchCalendarMonth = /^(\d{4})-(\d{2})$/;
//YYYY-DDD or YYYYDDD
var matchOrdinalDate = /^(\d{4})-*(\d{3})$/;
var matchOrdinalDate = /^(\d{4})-?(\d{3})$/;
//YYYY-Www or YYYYWww or YYYY-Www-D or YYYYWwwD
var matchWeekDate = /^(\d{4})-*W(\d{2})-*(\d{1})*$/;
var matchWeekDate = /^(\d{4})-?W(\d{2})-?(\d{1})?$/;
//YYYY-MM-DD or YYYYMMDD
var matchCalendarDate = /^(\d{4})-*(\d{2})-*(\d{2})$/;
var matchCalendarDate = /^(\d{4})-?(\d{2})-?(\d{2})$/;
// Match utc offset
var utcOffset = /([Z+\-])*(\d{2})*:*(\d{2})*$/;
var utcOffset = /([Z+\-])?(\d{2})?:?(\d{2})?$/;
// Match hours HH or HH.xxxxx
var matchHours = /^(\d{2})(\.\d+)*/.source + utcOffset.source;
var matchHours = /^(\d{2})(\.\d+)?/.source + utcOffset.source;
// Match hours/minutes HH:MM HHMM.xxxxx
var matchHoursMinutes = /^(\d{2}):*(\d{2})(\.\d+)*/.source + utcOffset.source;
var matchHoursMinutes = /^(\d{2}):?(\d{2})(\.\d+)?/.source + utcOffset.source;
// Match hours/minutes HH:MM:SS HHMMSS.xxxxx
var matchHoursMinutesSeconds = /^(\d{2}):*(\d{2}):*(\d{2})(\.\d+)*/.source + utcOffset.source;
var matchHoursMinutesSeconds = /^(\d{2}):?(\d{2}):?(\d{2})(\.\d+)?/.source + utcOffset.source;

var iso8601ErrorMessage = "Valid ISO 8601 date string required.";

Expand Down Expand Up @@ -188,8 +198,8 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
/**
* <p>
* Creates an immutable JulianDate instance from an ISO 8601 date string. Unlike Date.parse,
* this method will properly account for all valid formats defined by the ISO 8601
* specification. It will also properly handle leap seconds and sub-millisecond times.
* this method properly accounts for all valid formats defined by the ISO 8601
* specification. It also properly handles leap seconds and sub-millisecond times.
* <p/>
*
* @memberof JulianDate
Expand Down Expand Up @@ -223,7 +233,7 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
//start out by blanket replacing , with . which is the only valid such symbol in JS.
iso8601String = iso8601String.replace(',', '.');

//Split the string into it's date and time components, denoted by a mandatory T
//Split the string into its date and time components, denoted by a mandatory T
var tokens = iso8601String.split('T'), year, month = 1, day = 1, hours = 0, minutes = 0, seconds = 0, milliseconds = 0;

//Lacking a time is okay, but a missing date is illegal.
Expand All @@ -234,9 +244,15 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}

var dashCount;

//First match the date against possible regular expressions.
tokens = date.match(matchCalendarDate);
if (tokens !== null) {
dashCount = date.split('-').length - 1;
if (dashCount > 0 && dashCount !== 2) {

This comment has been minimized.

Copy link
@Carl4

Carl4 May 14, 2013

This line throws an error when time zones are specified as "+00:00". In these cases, dashCount == 3. I was going to take a crack at fixing it, but frankly, I'm intimidated by the scale of this project. To test the failure, try the converting the valid ISO8601 formatted date string:
date_str = "2012-03-16T00:01:20+00:00"

This comment has been minimized.

Copy link
@Carl4

Carl4 May 14, 2013

if (dashCount > 0 && !(dashCount == 2 || dashCount == 3)) { seems to work on my distribution. Not sure it rises to the level of a pull request.

This comment has been minimized.

Copy link
@mramato

mramato May 14, 2013

Author Contributor

Thanks @Carl4 No pull request is too small, and everyone has to start somewhere, so if you would like to contribute, we'd be happy to have you take a stab at fixing this. You would basically fork it, make the change you are suggesting, add a new unit test to Specs\Source\JulianDateSpec.js, and then open a pull request. If you aren't interested or can't contribute for other reasons, then I'll go ahead and make the change myself, no big deal. Just let me know. In either case, thanks for reporting this.

If you decide to contribute, we would need you to sign our CLA and email it to us (individual CLA). (gory details)

throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}
year = +tokens[1];
month = +tokens[2];
day = +tokens[3];
Expand All @@ -254,6 +270,7 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
var dayOfYear;
tokens = date.match(matchOrdinalDate);
if (tokens !== null) {

year = +tokens[1];
dayOfYear = +tokens[2];
inLeapYear = isLeapYear(year);
Expand All @@ -270,6 +287,14 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
year = +tokens[1];
var weekNumber = +tokens[2];
var dayOfWeek = +tokens[3] || 0;

dashCount = date.split('-').length - 1;
if (dashCount > 0 &&
((typeof tokens[3] === 'undefined' && dashCount !== 1) ||
(typeof tokens[3] !== 'undefined' && dashCount !== 2))) {
throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}

var january4 = new Date(Date.UTC(year, 0, 4));
dayOfYear = (weekNumber * 7) + dayOfWeek - january4.getUTCDay() - 3;
} else {
Expand Down Expand Up @@ -297,6 +322,11 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
if (typeof time !== 'undefined') {
tokens = time.match(matchHoursMinutesSeconds);
if (tokens !== null) {
dashCount = time.split(':').length - 1;
if (dashCount > 0 && dashCount !== 2) {
throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}

hours = +tokens[1];
minutes = +tokens[2];
seconds = +tokens[3];
Expand All @@ -305,6 +335,11 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
} else {
tokens = time.match(matchHoursMinutes);
if (tokens !== null) {
dashCount = time.split(':').length - 1;
if (dashCount > 0 && dashCount !== 1) {
throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}

hours = +tokens[1];
minutes = +tokens[2];
seconds = +(tokens[3] || 0) * 60.0;
Expand All @@ -321,8 +356,8 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
}
}

//Validate that all values are in proper range. Minutes and hours have special cases at 24 and 60.
if (minutes >= 60 || seconds > 60 || hours > 24 || (hours === 24 && (minutes > 0 || seconds > 0 || milliseconds > 0))) {
//Validate that all values are in proper range. Minutes and hours have special cases at 60 and 24.
if (minutes >= 60 || seconds >= 61 || hours > 24 || (hours === 24 && (minutes > 0 || seconds > 0 || milliseconds > 0))) {
throw new DeveloperError(iso8601ErrorMessage, "iso8601String");
}

Expand Down Expand Up @@ -351,20 +386,20 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
minutes = minutes + new Date(Date.UTC(year, month - 1, day)).getTimezoneOffset();
}

//ISO8601 denotes a leap second by any time having a seconds component of exactly 60 seconds.
//ISO8601 denotes a leap second by any time having a seconds component of 60 seconds.
//If that's the case, we need to temporarily subtract a second in order to build a UTC date.
//Then we add it back in after converting to TAI.
var isLeapSecond = seconds === 60;
if (isLeapSecond) {
seconds--;
}

//Even if we successfully parsed the string into it's components, after applying UTC offset or
//Even if we successfully parsed the string into its components, after applying UTC offset or
//special cases like 24:00:00 denoting midnight, we need to normalize the data appropriately.

//milliseconds can never be greater than 1000, so we start with seconds
while (seconds >= 60) {
seconds -= 60;
//milliseconds can never be greater than 1000, and seconds can't be above 60, so we start with minutes
while (minutes >= 60) {
minutes -= 60;
hours++;
}

Expand All @@ -377,15 +412,21 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
while (day > tmp) {
day -= tmp;
month++;

if (month > 12) {
month -= 12;
year++;
}

tmp = (inLeapYear && month === 2) ? daysInLeapFeburary : daysInMonth[month - 1];
}

while (month > 12) {
month -= 12;
year++;
//If UTC offset is at the beginning/end of the day, minutes can be negative.
while (minutes < 0) {
minutes += 60;
hours--;
}

//If UTC offset is at the beginning/end of the day, hours can be negative.
while (hours < 0) {
hours += 24;
day--;
Expand All @@ -395,11 +436,11 @@ function(DeveloperError, binarySearch, TimeConstants, LeapSecond, TimeStandard,
tmp = (inLeapYear && month === 2) ? daysInLeapFeburary : daysInMonth[month - 1];
day += tmp;
month--;
}

while (month < 1) {
month += 12;
year--;
if (month < 1) {
month += 12;
year--;
}
}

//Now create the JulianDate components from the Gregorian date and actually create our instance.
Expand Down
85 changes: 82 additions & 3 deletions Specs/Core/JulianDateSpec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
defineSuite(['Core/JulianDate', 'Core/TimeStandard', 'Core/TimeConstants', 'Core/Math'], function(JulianDate, TimeStandard, TimeConstants, CesiumMath) {
defineSuite(['Core/JulianDate',
'Core/TimeStandard',
'Core/TimeConstants',
'Core/Math'],
function(JulianDate,
TimeStandard,
TimeConstants,
CesiumMath) {
"use strict";
/*global it, xit, expect*/
/*global it, expect*/

// All exact Julian Dates found using NASA's Time Conversion Tool: http://ssd.jpl.nasa.gov/tc.cgi
it("Construct a default date", function() {
// FIXME Default constructing a date uses "now". Unfortunately,
// Default constructing a date uses "now". Unfortunately,
// there's no way to know exactly what that time will be, so we
// give ourselves a 5 second epsilon as a hack to avoid possible
// race conditions. In reality, it might be better to just omit
Expand Down Expand Up @@ -479,6 +486,18 @@ defineSuite(['Core/JulianDate', 'Core/TimeStandard', 'Core/TimeConstants', 'Core
expect(computedDate.equals(expectedDate)).toBeTruthy();
});

it("Construct from an ISO8601 local calendar date with UTC offset that crosses into next year", function() {
var expectedDate = JulianDate.fromDate(new Date(Date.UTC(2008, 11, 31, 23, 0, 0)));
var julianDate = JulianDate.fromIso8601("2009-01-01T01:00:00+02");
expect(julianDate.equals(expectedDate)).toBeTruthy();
});

it("Construct from an ISO8601 local calendar date with UTC offset that crosses into previous year", function() {
var expectedDate = JulianDate.fromDate(new Date(Date.UTC(2009, 0, 1, 1, 0, 0)));
var julianDate = JulianDate.fromIso8601("2008-12-31T23:00:00-02");
expect(julianDate.equals(expectedDate)).toBeTruthy();
});

it("Fails to construct an ISO8601 ordinal date with day less than 1", function() {
expect(function() {
return JulianDate.fromIso8601("2009-000");
Expand Down Expand Up @@ -665,6 +684,66 @@ defineSuite(['Core/JulianDate', 'Core/TimeStandard', 'Core/TimeConstants', 'Core
}).toThrow();
});

it("Fails to construct from an ISO8601 with too many dashes", function() {
expect(function() {
return JulianDate.fromIso8601("2009--01-01");
}).toThrow();
});

it("Fails to construct from an ISO8601 with garbage offset", function() {
expect(function() {
return JulianDate.fromIso8601("2000-12-15T12:59:23ZZ+-050708::1234");
}).toThrow();
});

it("Fails to construct an ISO8601 date with more than one decimal place", function() {
expect(function() {
return JulianDate.fromIso8601("2000-12-15T12:59:22..2");
}).toThrow();
});

it("Fails to construct an ISO8601 calendar date mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("200108-01");
}).toThrow();
});

it("Fails to construct an ISO8601 calendar date mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("2001-0801");
}).toThrow();
});

it("Fails to construct an ISO8601 calendar week mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("2008-W396");
}).toThrow();
});

it("Fails to construct an ISO8601 calendar week mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("2008W39-6");
}).toThrow();
});

it("Fails to construct an ISO8601 date with trailing -", function() {
expect(function() {
return JulianDate.fromIso8601("2001-");
}).toThrow();
});

it("Fails to construct an ISO8601 time mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("2000-12-15T22:0100");
}).toThrow();
});

it("Fails to construct an ISO8601 time mixing basic and extended format", function() {
expect(function() {
return JulianDate.fromIso8601("2000-12-15T2201:00");
}).toThrow();
});

it("getJulianTimeFraction works", function() {
var seconds = 12345.678;
var fraction = seconds / 86400.0;
Expand Down

0 comments on commit d4aed3a

Please sign in to comment.