Created
May 21, 2020 19:45
-
-
Save spatialtime/f32969c6468ce2fca78385e0b125005b to your computer and use it in GitHub Desktop.
JavaScript code for parsing and formatting ISO 8601 weeks
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 | |
/* | |
* dateFromISOWeek creates a JavaScript date from a string | |
* that adheres to either of ISO 8601'S ISO week formats: | |
* [a] short: YYYY-Www, [b] long: YYYY-Www-d | |
*/ | |
let dateFromISOWeek = function (isoWeek) { | |
const ISOWEEK_REGEX = /^(\d{4})-W([0-5]\d)(?:-([1-7]))?$/; | |
let matches = isoWeek.match(ISOWEEK_REGEX); | |
if (matches === null) { | |
throw new SyntaxError("Invalid ISO week syntax"); | |
} | |
let year = parseInt(matches[1]); | |
if (year < MIN_YEAR || year > MAX_YEAR) { | |
throw new RangeError(`Year must be >= ${MIN_YEAR} and <= ${MAX_YEAR}`); | |
} | |
let week = parseInt(matches[2]) | |
if (week <= 0 || week > weeksInISOYear(year)) { | |
throw new RangeError(`Week must be > 0 and <= the ISO week count for the year in question.`); | |
} | |
let day | |
if (matches[3]) { | |
day = parseInt(matches[3]); | |
} else { | |
day = 1 | |
} | |
let ordinalDate = ordinalDateFromWeekDate(year, week, day); | |
let time = timeFromYear(year) + (ordinalDate -1)*MILLISECONDS_PER_DAY; | |
return new Date(time) | |
} | |
/* | |
* formatDateAsISOWeek formats a date to ISO 8601 week format specification. | |
* By default, it returns the long form, YYYY-Www-d, | |
* but will return short form, YYYY-Www, if wantShortForm is set to true. | |
*/ | |
let formatDateAsISOWeek = function (year, month, day, wantShortForm) { | |
// validate parameters | |
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, isLeapYear(year)) + day - 1; | |
const dow = weekday(year, month,day)+1 | |
let woy = Math.floor((doy-dow+10)/7) | |
if(woy ===0){ | |
--year; | |
woy = weeksInISOYear(year); | |
} else if (woy===53){ | |
if(weeksInISOYear(year) === 52){ | |
++year; | |
woy = 1; | |
} | |
} | |
const woyString = woy.toString().padStart(2,"0"); | |
if (wantShortForm) { | |
return `${year}-W${woyString}`; | |
} else { | |
return `${year}-W${woyString}-${dow}`; | |
} | |
} | |
/* | |
* formatJSDateAsISOWeek formats a JavaScript Date instance | |
* to ISO 8601 week format specification. | |
* By default, it returns the long form, YYYY-Www-d, | |
* but will return short form, YYYY-Www, if wantShortForm is set to true. | |
* Note: to avoid all sorts of complications on your end, pass in a UTC date. | |
*/ | |
let formatJSDateAsISOWeek = function (jsDate, wantShortForm) { | |
return formatDateAsISOWeek(jsDate.getUTCFullYear(), jsDate.getUTCMonth()+1, | |
jsDate.getUTCDate(), wantShortForm); | |
} | |
/* | |
* weeksInISOYear returns number of weeks in a given proleptic Gregorian year. | |
*/ | |
function weeksInISOYear(gregYear) { | |
function calcP(y) { | |
return y + Math.floor(y / 4) - Math.floor(y / 100) + | |
Math.floor(y / 400); | |
} | |
if ((calcP(gregYear) % 7 === 4) || | |
(calcP(gregYear - 1) % 7 === 3)) { | |
return 53; | |
} else { | |
return 52; | |
} | |
} | |
/* | |
* Zeller's congruence. Computes day of week for any day. | |
* Returns integer representing day of week (Monday=0...Sunday=6). | |
*/ | |
function weekday(year, month, day) { | |
if (month === 1) { | |
month = 13; | |
year--; | |
} else if (month === 2) { | |
month = 14; | |
year--; | |
} | |
let dow = Math.floor(day + (13 * (month + 1) / 5) + year + | |
Math.floor(year / 4) - Math.floor(year / 100) + | |
Math.floor(year / 400)) % 7; | |
return (7 + (dow - 2)) % 7; | |
} | |
/* | |
* 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"); | |
} | |
} | |
/* | |
* 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 weekdate, compute the ordinal date. | |
*/ | |
function ordinalDateFromWeekDate(year, woy,dow){ | |
return (woy*7)+dow - (weekday(year,1,4)+1+3); | |
} | |
/* | |
* 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