Last active
May 23, 2020 01:58
-
-
Save spatialtime/36752be8cfb6a697a2780f969c962026 to your computer and use it in GitHub Desktop.
JavaScript and ISO 8601 ordinal dates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//------------------------------------------------------------------------------ | |
// 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