Skip to content

Instantly share code, notes, and snippets.

@spatialtime
Created May 21, 2020 19:45
Show Gist options
  • Save spatialtime/f32969c6468ce2fca78385e0b125005b to your computer and use it in GitHub Desktop.
Save spatialtime/f32969c6468ce2fca78385e0b125005b to your computer and use it in GitHub Desktop.
JavaScript code for parsing and formatting ISO 8601 weeks
//------------------------------------------------------------------------------
// 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