Skip to content

Instantly share code, notes, and snippets.

@spatialtime
Last active May 23, 2020 01:58
Show Gist options
  • Save spatialtime/36752be8cfb6a697a2780f969c962026 to your computer and use it in GitHub Desktop.
Save spatialtime/36752be8cfb6a697a2780f969c962026 to your computer and use it in GitHub Desktop.
JavaScript and ISO 8601 ordinal dates
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const MIN_YEAR =1;
const MAX_YEAR = 9999;
const MIN_MONTH = 1;
const MAX_MONTH = 12;
const MIN_DAY = 1;
const MIN_HOUR = 0;
const MAX_HOUR = 24;
const MIN_MINUTE = 0;
const MAX_MINUTE = 59;
const MIN_SECOND = 0;
const MAX_SECOND = 60;
const MILLISECONDS_PER_DAY = 86400000;
/*
* dateFromISOOrdinalDate parses and validates
* an ISO 8601 ordinal date string and returns a JavaScript Date.
* ISO 8601 ordinal date format: YYYY-ddd
*/
let dateFromISOOrdinalDate = function (isoOrdinalDate) {
const ORDINALDATE_REGEX = /^(\d{4})-(\d{3})$/;
const matches = isoOrdinalDate.match(ORDINALDATE_REGEX);
if (matches === null) {
throw new SyntaxError("Invalid ordinal date string");
}
const year = parseInt(matches[1]);
const doy = parseInt(matches[2]);
const maxDay = daysInYear(year);
if (doy <= 0 || doy > maxDay) {
throw new RangeError(`Day must be >= 1 and <= ${maxDay}.`);
}
return new Date(timeFromYear(year) + (doy-1)*MILLISECONDS_PER_DAY);
}
/*
* formatDateAsISOOrdinalDate formats a date to an
* ISO 8601 ordinal date string.
* Note: month is 1-based (as opposed to JavaScript's 0-based).
* January is month 1...December is month 12.
*/
let formatDateAsISOOrdinalDate = function (year, month, day) {
if (year < MIN_YEAR || year > MAX_YEAR) {
throw new RangeError(`Year must be >= ${MIN_YEAR} and <= ${MAX_YEAR}`);
}
if (month < MIN_MONTH || month > MAX_MONTH) {
throw new RangeError(`Month must be >= ${MIN_MONTH} and <= ${MAX_MONTH}`);
}
const leapYear = isLeapYear(year);
const dayCount = monthDayCount(month, leapYear);
if (day < MIN_DAY || day > dayCount) {
throw new RangeError(`Day must be >= ${MIN_DAY} and <= ${dayCount}`);
}
const doy = monthStartDay(month, leapYear) + day - 1;
return year.toString().padStart(4, '0') + "-" + doy.toString().padStart(3, '0');
}
/*
* formatJSDateAsISOOrdinalDate formats a JavaScript Date instance to an
* ISO 8601 ordinal date string.
* Note: to avoid all sorts of complications on your end, pass in a UTC date.
*/
let formatJSDateAsISOOrdinalDate = function (jsDate) {
return formatDateAsISOOrdinalDate(jsDate.getUTCFullYear(), jsDate.getUTCMonth()+1,
jsDate.getUTCDate());
}
/*
* monthStartDay returns the ordinal day (nth day of year)
* on which a month begins.
*/
function monthStartDay(month, leapYear) {
const leapOffset = leapYear ? 1 : 0;
switch (month) {
case 1:
return 1;
case 2:
return 32;
case 3:
return 60 + leapOffset;
case 4:
return 91 + leapOffset;
case 5:
return 121 + leapOffset;
case 6:
return 152 + leapOffset;
case 7:
return 182 + leapOffset;
case 8:
return 213 + leapOffset;
case 9:
return 244 + leapOffset;
case 10:
return 274 + leapOffset;
case 11:
return 305 + leapOffset;
case 12:
return 335 + leapOffset;
default:
throw new RangeError("month must be >=1 and <= 12");
}
}
/*
* How many days in a year?
*/
function daysInYear(y){
return isLeapYear(y)?366:365;
}
/*
* isLeapYear determines whether a given proleptic Gregorian year is a leap year.
*/
function isLeapYear(year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
}
/*
* monthDayCount returns number of days in a given month.
*/
function monthDayCount(month, leapYear) {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
return leapYear ? 29 : 28;
default:
throw new RangeError(`Month must be >= ${MIN_MONTH} and <= ${MAX_MONTH}`);
}
}
/*
* Given a year, how many days since or before 1970.
*/
function dayFromYear(y){
return (365 * (y - 1970) + Math.floor((y - 1969) / 4) -
Math.floor((y - 1901) / 100) + Math.floor((y - 1601) / 400))
}
/*
* Convert daysFromYear into milleseconds...can feed into new Date()
*/
function timeFromYear(y){
return dayFromYear(y)*MILLISECONDS_PER_DAY
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment