UNPKG

@sbh321/qcalendar

Version:

A forked version of Jeff Galbraith's fork of Quasar UI QCalendar

1,328 lines (1,198 loc) 46.2 kB
/*! * @subhambhandari/qcalendar v4.0.0-beta.19 * (c) 2024 Subham Bhandari <bhandarimaiya65@gmail.com> * Released under the MIT License. */ const version = '4.0.0-beta.19'; const PARSE_REGEX = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?([^\d]+(\d{1,2}))?(:(\d{1,2}))?(:(\d{1,2}))?(.(\d{1,3}))?$/; const PARSE_DATE = /^(\d{4})-(\d{1,2})(-(\d{1,2}))/; const PARSE_TIME = /(\d\d?)(:(\d\d?)|)(:(\d\d?)|)/; const DAYS_IN_MONTH = [ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; const DAYS_IN_MONTH_LEAP = [ 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; const DAYS_IN_MONTH_MIN = 28; const DAYS_IN_MONTH_MAX = 31; const MONTH_MAX = 12; const MONTH_MIN = 1; const DAY_MIN = 1; const DAYS_IN_WEEK = 7; const MINUTES_IN_HOUR = 60; const HOURS_IN_DAY = 24; const FIRST_HOUR = 0; const MILLISECONDS_IN_MINUTE = 60000; const MILLISECONDS_IN_HOUR = 3600000; const MILLISECONDS_IN_DAY = 86400000; const MILLISECONDS_IN_WEEK = 604800000; /* eslint-disable no-multi-spaces */ /** * @typedef {Object} Timestamp The Timestamp object * @property {string=} Timestamp.date Date string in format 'YYYY-MM-DD' * @property {string=} Timestamp.time Time string in format 'HH:MM' * @property {number} Timestamp.year The numeric year * @property {number} Timestamp.month The numeric month (Jan = 1, ...) * @property {number} Timestamp.day The numeric day * @property {number} Timestamp.weekday The numeric weekday (Sun = 0, ..., Sat = 6) * @property {number=} Timestamp.hour The numeric hour * @property {number} Timestamp.minute The numeric minute * @property {number=} Timestamp.doy The numeric day of the year (doy) * @property {number=} Timestamp.workweek The numeric workweek * @property {boolean} Timestamp.hasDay True if Timestamp.date is filled in and usable * @property {boolean} Timestamp.hasTime True if Timestamp.time is filled in and usable * @property {boolean=} Timestamp.past True if the Timestamp is in the past * @property {boolean=} Timestamp.current True if Timestamp is current day (now) * @property {boolean=} Timestamp.future True if Timestamp is in the future * @property {boolean=} Timestamp.disabled True if this is a disabled date * @property {boolean=} Timestamp.currentWeekday True if this date corresponds to current weekday */ const Timestamp = { date: '', // YYYY-MM-DD time: '', // HH:MM (optional) year: 0, // YYYY month: 0, // MM (Jan = 1, etc) day: 0, // day of the month weekday: 0, // week day (0=Sunday...6=Saturday) hour: 0, // 24-hr format minute: 0, // mm doy: 0, // day of year workweek: 0, // workweek number hasDay: false, // if this timestamp is supposed to have a date hasTime: false, // if this timestamp is supposed to have a time past: false, // if timestamp is in the past (based on `now` property) current: false, // if timestamp is current date (based on `now` property) future: false, // if timestamp is in the future (based on `now` property) disabled: false, // if timestamp is disabled currentWeekday: false // if this date corresponds to current weekday }; const TimeObject = { hour: 0, // Number minute: 0 // Number }; /* eslint-enable no-multi-spaces */ // returns YYYY-MM-dd format /** * Returns today's date * @returns {string} Date string in the form 'YYYY-MM-DD' */ function today () { const d = new Date(), month = '' + (d.getMonth() + 1), day = '' + d.getDate(), year = d.getFullYear(); return [ year, padNumber(month, 2), padNumber(day, 2) ].join('-') } /** * Returns the start of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the start of the week). * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}. * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the week * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information * @returns {Timestamp} The {@link Timestamp} representing the start of the week */ function getStartOfWeek (timestamp, weekdays, today) { let start = copyTimestamp(timestamp); if (start.day === 1 || start.weekday === 0) { while (!weekdays.includes(start.weekday)) { start = nextDay(start); } } start = findWeekday(start, weekdays[ 0 ], prevDay); start = updateFormatted(start); if (today) { start = updateRelative(start, today, start.hasTime); } return start } /** * Returns the end of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the last of the week). * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}. * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the week * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information * @returns {Timestamp} The {@link Timestamp} representing the end of the week */ function getEndOfWeek (timestamp, weekdays, today) { let end = copyTimestamp(timestamp); // is last day of month? const lastDay = daysInMonth(end.year, end.month); if (lastDay === end.day || end.weekday === 6) { while (!weekdays.includes(end.weekday)) { end = prevDay(end); } } end = findWeekday(end, weekdays[ weekdays.length - 1 ], nextDay); end = updateFormatted(end); if (today) { end = updateRelative(end, today, end.hasTime); } return end } /** * Finds the start of the month based on the passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the month * @returns {Timestamp} A {@link Timestamp} of the start of the month */ function getStartOfMonth (timestamp) { const start = copyTimestamp(timestamp); start.day = DAY_MIN; updateFormatted(start); return start } /** * Finds the end of the month based on the passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the month * @returns {Timestamp} A {@link Timestamp} of the end of the month */ function getEndOfMonth (timestamp) { const end = copyTimestamp(timestamp); end.day = daysInMonth(end.year, end.month); updateFormatted(end); return end } // returns minutes since midnight function parseTime (input) { const type = Object.prototype.toString.call(input); switch (type) { case '[object Number]': // when a number is given, it's minutes since 12:00am return input case '[object String]': { // when a string is given, it's a hh:mm:ss format where seconds are optional, but not used const parts = PARSE_TIME.exec(input); if (!parts) { return false } return parseInt(parts[ 1 ], 10) * 60 + parseInt(parts[ 3 ] || 0, 10) } case '[object Object]': // when an object is given, it must have hour and minute if (typeof input.hour !== 'number' || typeof input.minute !== 'number') { return false } return input.hour * 60 + input.minute } return false } /** * Validates the passed input ('YYY-MM-DD') as a date or ('YYY-MM-DD HH:MM') date time combination * @param {string} input A string in the form 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM' * @returns {boolean} True if parseable */ function validateTimestamp (input) { return !!PARSE_REGEX.exec(input) } /** * Compares two {@link Timestamp}s for exactness * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @returns {boolean} True if the two {@link Timestamp}s are an exact match */ function compareTimestamps (ts1, ts2) { return JSON.stringify(ts1) === JSON.stringify(ts2) } /** * Compares the date of two {@link Timestamp}s that have been updated with relative data * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @returns {boolean} True if the two dates are the same */ function compareDate (ts1, ts2) { return getDate(ts1) === getDate(ts2) } /** * Compares the time of two {@link Timestamp}s that have been updated with relative data * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @returns {boolean} True if the two times are an exact match */ function compareTime (ts1, ts2) { return getTime(ts1) === getTime(ts2) } /** * Compares the date and time of two {@link Timestamp}s that have been updated with relative data * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @returns {boolean} True if the date and time are an exact match */ function compareDateTime (ts1, ts2) { return getDateTime(ts1) === getDateTime(ts2) } /** * Fast low-level parser for a date string ('YYYY-MM-DD'). Does not update formatted or relative date. * Use 'parseTimestamp' for formatted and relative updates * @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used) * @returns {Timestamp} This {@link Timestamp} is minimally filled in. The {@link Timestamp.date} and {@link Timestamp.time} as well as relative data will not be filled in. */ function parsed (input) { const parts = PARSE_REGEX.exec(input); if (!parts) return null return { date: input, time: padNumber(parseInt(parts[ 6 ], 10) || 0, 2) + ':' + padNumber(parseInt(parts[ 8 ], 10) || 0, 2), year: parseInt(parts[ 1 ], 10), month: parseInt(parts[ 2 ], 10), day: parseInt(parts[ 4 ], 10) || 1, hour: !isNaN(parseInt(parts[ 6 ], 10)) ? parseInt(parts[ 6 ], 10) : 0, minute: !isNaN(parseInt(parts[ 8 ], 10)) ? parseInt(parts[ 8 ], 10) : 0, weekday: 0, doy: 0, workweek: 0, hasDay: !!parts[ 4 ], hasTime: true, // there is always time because no time is '00:00', which is valid past: false, current: false, future: false, disabled: false } } /** * High-level parser that converts the passed in string to {@link Timestamp} and uses 'now' to update relative information. * @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used) * @param {Timestamp} now A {@link Timestamp} to use for relative data updates * @returns {Timestamp} The {@link Timestamp.date} will be filled in as well as the {@link Timestamp.time} if a time is supplied and formatted fields (doy, weekday, workweek, etc). If 'now' is supplied, then relative data will also be updated. */ function parseTimestamp (input, now) { let timestamp = parsed(input); if (timestamp === null) return null timestamp = updateFormatted(timestamp); if (now) { updateRelative(timestamp, now, timestamp.hasTime); } return timestamp } /** * Takes a JavaScript Date and returns a {@link Timestamp}. The {@link Timestamp} is not updated with relative information. * @param {date} date JavaScript Date * @param {boolean} utc If set the {@link Timestamp} will parse the Date as UTC * @returns {Timestamp} A minimal {@link Timestamp} without updated or relative updates. */ function parseDate (date, utc = false) { const UTC = !!utc ? 'UTC' : ''; return updateFormatted({ date: padNumber(date[ `get${ UTC }FullYear` ](), 4) + '-' + padNumber(date[ `get${ UTC }Month` ]() + 1, 2) + '-' + padNumber(date[ `get${ UTC }Date` ](), 2), time: padNumber(date[ `get${ UTC }Hours` ]() || 0, 2) + ':' + padNumber(date[ `get${ UTC }Minutes` ]() || 0, 2), year: date[ `get${ UTC }FullYear` ](), month: date[ `get${ UTC }Month` ]() + 1, day: date[ `get${ UTC }Date` ](), hour: date[ `get${ UTC }Hours` ](), minute: date[ `get${ UTC }Minutes` ](), weekday: 0, doy: 0, workweek: 0, hasDay: true, hasTime: true, // Date always has time, even if it is '00:00' past: false, current: false, future: false, disabled: false }) } /** * Converts a {@link Timestamp} into a numeric date identifier based on the passed {@link Timestamp}'s date * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The numeric date identifier */ function getDayIdentifier (timestamp) { return timestamp.year * 100000000 + timestamp.month * 1000000 + timestamp.day * 10000 } /** * Converts a {@link Timestamp} into a numeric time identifier based on the passed {@link Timestamp}'s time * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The numeric time identifier */ function getTimeIdentifier (timestamp) { return timestamp.hour * 100 + timestamp.minute } /** * Converts a {@link Timestamp} into a numeric date and time identifier based on the passed {@link Timestamp}'s date and time * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The numeric date+time identifier */ function getDayTimeIdentifier (timestamp) { return getDayIdentifier(timestamp) + getTimeIdentifier(timestamp) } /** * Returns the difference between two {@link Timestamp}s * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @param {boolean=} strict Optional flag to not to return negative numbers * @returns {number} The difference */ function diffTimestamp (ts1, ts2, strict) { const utc1 = Date.UTC(ts1.year, ts1.month - 1, ts1.day, ts1.hour, ts1.minute); const utc2 = Date.UTC(ts2.year, ts2.month - 1, ts2.day, ts2.hour, ts2.minute); if (strict === true && utc2 < utc1) { // Not negative number // utc2 - utc1 < 0 -> utc2 < utc1 -> NO: utc1 >= utc2 return 0 } return utc2 - utc1 } /** * Updates a {@link Timestamp} with relative data (past, current and future) * @param {Timestamp} timestamp The {@link Timestamp} that needs relative data updated * @param {Timestamp} now {@link Timestamp} that represents the current date (optional time) * @param {boolean=} time Optional flag to include time ('timestamp' and 'now' params should have time values) * @returns {Timestamp} The updated {@link Timestamp} */ function updateRelative (timestamp, now, time = false) { let a = getDayIdentifier(now); let b = getDayIdentifier(timestamp); let current = a === b; if (timestamp.hasTime && time && current) { a = getTimeIdentifier(now); b = getTimeIdentifier(timestamp); current = a === b; } timestamp.past = b < a; timestamp.current = current; timestamp.future = b > a; timestamp.currentWeekday = timestamp.weekday === now.weekday; return timestamp } /** * Sets a Timestamp{@link Timestamp} to number of minutes past midnight (modifies hour and minutes if needed) * @param {Timestamp} timestamp The {@link Timestamp} to modify * @param {number} minutes The number of minutes to set from midnight * @param {Timestamp=} now Optional {@link Timestamp} representing current date and time * @returns {Timestamp} The updated {@link Timestamp} */ function updateMinutes (timestamp, minutes, now) { timestamp.hasTime = true; timestamp.hour = Math.floor(minutes / MINUTES_IN_HOUR); timestamp.minute = minutes % MINUTES_IN_HOUR; timestamp.time = getTime(timestamp); if (now) { updateRelative(timestamp, now, true); } return timestamp } /** * Updates the {@link Timestamp} with the weekday * @param {Timestamp} timestamp The {@link Timestamp} to modify * @returns The modified Timestamp */ function updateWeekday (timestamp) { timestamp.weekday = getWeekday(timestamp); return timestamp } /** * Updates the {@link Timestamp} with the day of the year (doy) * @param {Timestamp} timestamp The {@link Timestamp} to modify * @returns The modified Timestamp */ function updateDayOfYear (timestamp) { timestamp.doy = getDayOfYear(timestamp); return timestamp } /** * Updates the {@link Timestamp} with the workweek * @param {Timestamp} timestamp The {@link Timestamp} to modify * @returns The modified {@link Timestamp} */ function updateWorkWeek (timestamp) { timestamp.workweek = getWorkWeek(timestamp); return timestamp } /** * Updates the passed {@link Timestamp} with disabled, if needed * @param {Timestamp} timestamp The {@link Timestamp} to modify * @param {string} [disabledBefore] In 'YYY-MM-DD' format * @param {string} [disabledAfter] In 'YYY-MM-DD' format * @param {number[]} [disabledWeekdays] An array of numbers representing weekdays [0 = Sun, ..., 6 = Sat] * @param {string[]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range. * @returns The modified {@link Timestamp} */ function updateDisabled (timestamp, disabledBefore, disabledAfter, disabledWeekdays, disabledDays) { const t = getDayIdentifier(timestamp); if (disabledBefore !== undefined) { const before = getDayIdentifier(parsed(disabledBefore)); if (t <= before) { timestamp.disabled = true; } } if (timestamp.disabled !== true && disabledAfter !== undefined) { const after = getDayIdentifier(parsed(disabledAfter)); if (t >= after) { timestamp.disabled = true; } } if (timestamp.disabled !== true && Array.isArray(disabledWeekdays) && disabledWeekdays.length > 0) { for (const weekday in disabledWeekdays) { if (disabledWeekdays[ weekday ] === timestamp.weekday) { timestamp.disabled = true; break } } } if (timestamp.disabled !== true && Array.isArray(disabledDays) && disabledDays.length > 0) { for (const day in disabledDays) { if (Array.isArray(disabledDays[ day ]) && disabledDays[ day ].length === 2) { const start = parsed(disabledDays[ day ][ 0 ]); const end = parsed(disabledDays[ day ][ 1 ]); if (isBetweenDates(timestamp, start, end)) { timestamp.disabled = true; break } } else { const d = getDayIdentifier(parseTimestamp(disabledDays[ day ] + ' 00:00')); if (d === t) { timestamp.disabled = true; break } } } } return timestamp } /** * Updates the passed {@link Timestamp} with formatted data (time string, date string, weekday, day of year and workweek) * @param {Timestamp} timestamp The {@link Timestamp} to modify * @returns The modified {@link Timestamp} */ function updateFormatted (timestamp) { timestamp.hasTime = true; timestamp.time = getTime(timestamp); timestamp.date = getDate(timestamp); timestamp.weekday = getWeekday(timestamp); timestamp.doy = getDayOfYear(timestamp); timestamp.workweek = getWorkWeek(timestamp); return timestamp } /** * Returns day of the year (doy) for the passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The day of the year */ function getDayOfYear (timestamp) { if (timestamp.year === 0) return return (Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day) - Date.UTC(timestamp.year, 0, 0)) / 24 / 60 / 60 / 1000 } /** * Returns workweek for the passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The work week */ function getWorkWeek (timestamp) { if (timestamp.year === 0) { timestamp = parseTimestamp(today()); } const date = makeDate(timestamp); if (isNaN(date)) return 0 // Remove time components of date const weekday = new Date(date.getFullYear(), date.getMonth(), date.getDate()); // Change date to Thursday same week weekday.setDate(weekday.getDate() - ((weekday.getDay() + 6) % 7) + 3); // Take January 4th as it is always in week 1 (see ISO 8601) const firstThursday = new Date(weekday.getFullYear(), 0, 4); // Change date to Thursday same week firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3); // Check if daylight-saving-time-switch occurred and correct for it const ds = weekday.getTimezoneOffset() - firstThursday.getTimezoneOffset(); weekday.setHours(weekday.getHours() - ds); // Number of weeks between target Thursday and first Thursday const weekDiff = (weekday - firstThursday) / (MILLISECONDS_IN_WEEK); return 1 + Math.floor(weekDiff) } /** * Returns weekday for the passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {number} The weekday */ function getWeekday (timestamp) { let weekday = timestamp.weekday; if (timestamp.hasDay) { const floor = Math.floor; const day = timestamp.day; const month = ((timestamp.month + 9) % MONTH_MAX) + 1; const century = floor(timestamp.year / 100); const year = (timestamp.year % 100) - (timestamp.month <= 2 ? 1 : 0); weekday = (((day + floor(2.6 * month - 0.2) - 2 * century + year + floor(year / 4) + floor(century / 4)) % 7) + 7) % 7; } return weekday } /** * Returns if the passed year is a leap year * @param {number} year The year to check (ie: 1999, 2020) * @returns {boolean} True if the year is a leap year */ function isLeapYear (year) { return ((year % 4 === 0) ^ (year % 100 === 0) ^ (year % 400 === 0)) === 1 } /** * Returns the days of the specified month in a year * @param {number} year The year (ie: 1999, 2020) * @param {number} month The month (zero-based) * @returns {number} The number of days in the month (corrected for leap years) */ function daysInMonth (year, month) { return isLeapYear(year) ? DAYS_IN_MONTH_LEAP[ month ] : DAYS_IN_MONTH[ month ] } /** * Makes a copy of the passed in {@link Timestamp} * @param {Timestamp} timestamp The original {@link Timestamp} * @returns {Timestamp} A copy of the original {@link Timestamp} */ function copyTimestamp (timestamp) { return { ...timestamp } } /** * Padds a passed in number to length (converts to a string). Good for converting '5' as '05'. * @param {number} x The number to pad * @param {number} length The length of the required number as a string * @returns {string} The padded number (as a string). (ie: 5 = '05') */ function padNumber (x, length) { let padded = String(x); while (padded.length < length) { padded = '0' + padded; } return padded } /** * Used internally to convert {@link Timestamp} used with 'parsed' or 'parseDate' so the 'date' portion of the {@link Timestamp} is correct. * @param {Timestamp} timestamp The (raw) {@link Timestamp} * @returns {string} A formatted date ('YYYY-MM-DD') */ function getDate (timestamp) { let str = `${ padNumber(timestamp.year, 4) }-${ padNumber(timestamp.month, 2) }`; if (timestamp.hasDay) str += `-${ padNumber(timestamp.day, 2) }`; return str } /** * Used intenally to convert {@link Timestamp} with 'parsed' or 'parseDate' so the 'time' portion of the {@link Timestamp} is correct. * @param {Timestamp} timestamp The (raw) {@link Timestamp} * @returns {string} A formatted time ('hh:mm') */ function getTime (timestamp) { if (!timestamp.hasTime) { return '' } return `${ padNumber(timestamp.hour, 2) }:${ padNumber(timestamp.minute, 2) }` } /** * Returns a formatted string date and time ('YYYY-YY-MM hh:mm') * @param {Timestamp} timestamp The {@link Timestamp} * @returns {string} A formatted date time ('YYYY-MM-DD HH:mm') */ function getDateTime (timestamp) { return getDate(timestamp) + ' ' + (timestamp.hasTime ? getTime(timestamp) : '00:00') } /** * Returns a {@link Timestamp} of next day from passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {Timestamp} The modified {@link Timestamp} as the next day */ function nextDay (timestamp) { ++timestamp.day; timestamp.weekday = (timestamp.weekday + 1) % DAYS_IN_WEEK; if (timestamp.day > DAYS_IN_MONTH_MIN && timestamp.day > daysInMonth(timestamp.year, timestamp.month)) { timestamp.day = DAY_MIN; ++timestamp.month; if (timestamp.month > MONTH_MAX) { timestamp.month = MONTH_MIN; ++timestamp.year; } } return timestamp } /** * Returns a {@link Timestamp} of previous day from passed in {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @returns {Timestamp} The modified {@link Timestamp} as the previous day */ function prevDay (timestamp) { timestamp.day--; timestamp.weekday = (timestamp.weekday + 6) % DAYS_IN_WEEK; if (timestamp.day < DAY_MIN) { timestamp.month--; if (timestamp.month < MONTH_MIN) { timestamp.year--; timestamp.month = MONTH_MAX; } timestamp.day = daysInMonth(timestamp.year, timestamp.month); } return timestamp } /** * An alias for {relativeDays} * @param {Timestamp} timestamp The {@link Timestamp} to modify * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}). * @param {number} [days=1] The number of days to move. * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat]. * @returns The modified {@link Timestamp} */ function moveRelativeDays (timestamp, mover = nextDay, days = 1, allowedWeekdays = [ 0, 1, 2, 3, 4, 5, 6 ]) { return relativeDays(timestamp, mover, days, allowedWeekdays) } /** * Moves the {@link Timestamp} the number of relative days * @param {Timestamp} timestamp The {@link Timestamp} to modify * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}). * @param {number} [days=1] The number of days to move. * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat]. * @returns The modified {@link Timestamp} */ function relativeDays (timestamp, mover = nextDay, days = 1, allowedWeekdays = [ 0, 1, 2, 3, 4, 5, 6 ]) { if (!allowedWeekdays.includes(timestamp.weekday) && timestamp.weekday === 0 && mover === nextDay) { ++days; } while (--days >= 0) { timestamp = mover(timestamp); if (allowedWeekdays.length < 7 && !allowedWeekdays.includes(timestamp.weekday)) { ++days; } } return timestamp } /** * Finds the specified weekday (forward or back) based on the {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to modify * @param {number} weekday The weekday number (Sun = 0, ..., Sat = 6) * @param {function} [mover=nextDay] The function to use ({prevDay} or {nextDay}). * @param {number} [maxDays=6] The number of days to look forward or back. * @returns The modified {@link Timestamp} */ function findWeekday (timestamp, weekday, mover = nextDay, maxDays = 6) { while (timestamp.weekday !== weekday && --maxDays >= 0) timestamp = mover(timestamp); return timestamp } /** * Returns an array of 0's and mostly 1's representing skipped days (used internally) * @param {number[]} weekdays An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat]. * @returns {number[]} An array of 0's and mostly 1's (other numbers may occur previous to skipped days) */ function getWeekdaySkips (weekdays) { const skips = [ 1, 1, 1, 1, 1, 1, 1 ]; const filled = [ 0, 0, 0, 0, 0, 0, 0 ]; for (let i = 0; i < weekdays.length; ++i) { filled[ weekdays[ i ] ] = 1; } for (let k = 0; k < DAYS_IN_WEEK; ++k) { let skip = 1; for (let j = 1; j < DAYS_IN_WEEK; ++j) { const next = (k + j) % DAYS_IN_WEEK; if (filled[ next ]) { break } ++skip; } skips[ k ] = filled[ k ] * skip; } return skips } /** * Creates an array of {@link Timestamp}s based on start and end params * @param {Timestamp} start The starting {@link Timestamp} * @param {Timestamp} end The ending {@link Timestamp} * @param {Timestamp} now The relative day * @param {number[]} weekdaySkips An array representing available and skipped weekdays * @param {string} [disabledBefore] Days before this date are disabled (YYYY-MM-DD) * @param {string} [disabledAfter] Days after this date are disabled (YYYY-MM-DD) * @param {number[]} [disabledWeekdays] An array representing weekdays that are disabled [0 = Sun, ..., 6 = Sat] * @param {string[]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range. * @param {number} [max=42] Max days to do * @param {number} [min=0] Min days to do * @returns {Timestamp[]} The requested array of {@link Timestamp}s */ function createDayList (start, end, now, weekdaySkips, disabledBefore, disabledAfter, disabledWeekdays = [], disabledDays = [], max = 42, min = 0) { const stop = getDayIdentifier(end); const days = []; let current = copyTimestamp(start); let currentIdentifier = 0; let stopped = currentIdentifier === stop; if (stop < getDayIdentifier(start)) { return days } while ((!stopped || days.length < min) && days.length < max) { currentIdentifier = getDayIdentifier(current); stopped = stopped || (currentIdentifier > stop && days.length >= min); if (stopped) { break } if (weekdaySkips[ current.weekday ] === 0) { current = relativeDays(current, nextDay); continue } const day = copyTimestamp(current); updateFormatted(day); updateRelative(day, now); updateDisabled(day, disabledBefore, disabledAfter, disabledWeekdays, disabledDays); days.push(day); current = relativeDays(current, nextDay); } return days } /** * Creates an array of interval {@link Timestamp}s based on params * @param {Timestamp} timestamp The starting {@link Timestamp} * @param {number} first The starting interval time * @param {number} minutes How many minutes between intervals (ie: 60, 30, 15 would be common ones) * @param {number} count The number of intervals needed * @param {Timestamp} now A relative {@link Timestamp} with time * @returns {Timestamp[]} The requested array of interval {@link Timestamp}s */ function createIntervalList (timestamp, first, minutes, count, now) { const intervals = []; for (let i = 0; i < count; ++i) { const mins = (first + i) * minutes; const ts = copyTimestamp(timestamp); intervals.push(updateMinutes(ts, mins, now)); } return intervals } /** * @callback getOptions * @param {Timestamp} timestamp A {@link Timestamp} object * @param {boolean} short True if using short options * @returns {Object} An Intl object representing optioons to be used */ /** * @callback formatter * @param {Timestamp} timestamp The {@link Timestamp} being used * @param {boolean} short If short format is being requested * @returns {string} The localized string of the formatted {@link Timestamp} */ /** * Returns a function that uses Intl.DateTimeFormat formatting * @param {string} locale The locale to use (ie: en-US) * @param {getOptions} cb The function to call for options. This function should return an Intl formatted object. The function is passed (timestamp, short). * @returns {formatter} The function has params (timestamp, short). The short is to use the short options. */ function createNativeLocaleFormatter (locale, cb) { const emptyFormatter = (_t, _s) => ''; /* istanbul ignore next */ if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') { return emptyFormatter } return (timestamp, short) => { try { const intlFormatter = new Intl.DateTimeFormat(locale || undefined, cb(timestamp, short)); return intlFormatter.format(makeDateTime(timestamp)) } catch (e) /* istanbul ignore next */ { /* eslint-disable-next-line */ console.error(`Intl.DateTimeFormat: ${e.message} -> ${getDateTime(timestamp)}`); return emptyFormatter } } } /** * Makes a JavaScript Date from the passed {@link Timestamp} * @param {Timestamp} timestamp The {@link Timestamp} to use * @param {boolean} utc True to get Date object using UTC * @returns {date} A JavaScript Date */ function makeDate (timestamp, utc = true) { if (utc) return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0)) return new Date(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0) } /** * Makes a JavaScript Date from the passed {@link Timestamp} (with time) * @param {Timestamp} timestamp The {@link Timestamp} to use * @param {boolean} utc True to get Date object using UTC * @returns {date} A JavaScript Date */ function makeDateTime (timestamp, utc = true) { if (utc) return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute)) return new Date(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute) } // validate a number IS a number /** * Teting is passed value is a number * @param {(string|number)} input The value to use * @returns {boolean} True if a number (not floating point) */ function validateNumber (input) { return isFinite(parseInt(input, 10)) } /** * Given an array of {@link Timestamp}s, finds the max date (and possible time) * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s * @param {boolean=} useTime Default false; if true, uses time in the comparison as well * @returns The {@link Timestamp} with the highest date (and possibly time) value */ function maxTimestamp (timestamps, useTime = false) { const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier; return timestamps.reduce((prev, cur) => { return Math.max(func(prev), func(cur)) === func(prev) ? prev : cur }) } /** * Given an array of {@link Timestamp}s, finds the min date (and possible time) * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s * @param {boolean=} useTime Default false; if true, uses time in the comparison as well * @returns The {@link Timestamp} with the lowest date (and possibly time) value */ function minTimestamp (timestamps, useTime = false) { const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier; return timestamps.reduce((prev, cur) => { return Math.min(func(prev), func(cur)) === func(prev) ? prev : cur }) } /** * Determines if the passed {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range) * @param {Timestamp} timestamp The {@link Timestamp} for testing * @param {Timestamp} startTimestamp The starting {@link Timestamp} * @param {Timestamp} endTimestamp The ending {@link Timestamp} * @param {boolean=} useTime If true, use time from the {@link Timestamp}s * @returns {boolean} True if {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range) */ function isBetweenDates (timestamp, startTimestamp, endTimestamp, useTime /* = false */) { const cd = getDayIdentifier(timestamp) + (useTime === true ? getTimeIdentifier(timestamp) : 0); const sd = getDayIdentifier(startTimestamp) + (useTime === true ? getTimeIdentifier(startTimestamp) : 0); const ed = getDayIdentifier(endTimestamp) + (useTime === true ? getTimeIdentifier(endTimestamp) : 0); return cd >= sd && cd <= ed } /** * Determine if two ranges of {@link Timestamp}s overlap each other * @param {Timestamp} startTimestamp The starting {@link Timestamp} of first range * @param {Timestamp} endTimestamp The endinging {@link Timestamp} of first range * @param {Timestamp} firstTimestamp The starting {@link Timestamp} of second range * @param {Timestamp} lastTimestamp The ending {@link Timestamp} of second range * @returns {boolean} True if the two ranges overlap each other */ function isOverlappingDates (startTimestamp, endTimestamp, firstTimestamp, lastTimestamp) { const start = getDayIdentifier(startTimestamp); const end = getDayIdentifier(endTimestamp); const first = getDayIdentifier(firstTimestamp); const last = getDayIdentifier(lastTimestamp); return ( (start >= first && start <= last) // overlap left || (end >= first && end <= last) // overlap right || (first >= start && end >= last) // surrounding ) } /** * Add or decrements years, months, days, hours or minutes to a timestamp * @param {Timestamp} timestamp The {@link Timestamp} object * @param {Object} options configuration data * @param {number=} options.year If positive, adds years. If negative, removes years. * @param {number=} options.month If positive, adds months. If negative, removes month. * @param {number=} options.day If positive, adds days. If negative, removes days. * @param {number=} options.hour If positive, adds hours. If negative, removes hours. * @param {number=} options.minute If positive, adds minutes. If negative, removes minutes. * @returns {Timestamp} A modified copy of the passed in {@link Timestamp} */ function addToDate (timestamp, options) { const ts = copyTimestamp(timestamp); let minType; __forEachObject(options, (value, key) => { if (ts[ key ] !== undefined) { ts[ key ] += parseInt(value, 10); const indexType = NORMALIZE_TYPES.indexOf(key); if (indexType !== -1) { if (minType === undefined) { minType = indexType; } else { /* istanbul ignore next */ minType = Math.min(indexType, minType); } } } }); // normalize timestamp if (minType !== undefined) { __normalize(ts, NORMALIZE_TYPES[ minType ]); } updateFormatted(ts); return ts } const NORMALIZE_TYPES = [ 'minute', 'hour', 'day', 'month' ]; // addToDate helper function __forEachObject (obj, cb) { Object.keys(obj).forEach(k => cb(obj[ k ], k)); } // normalize minutes function __normalizeMinute (ts) { if (ts.minute >= MINUTES_IN_HOUR || ts.minute < 0) { const hours = Math.floor(ts.minute / MINUTES_IN_HOUR); ts.minute -= hours * MINUTES_IN_HOUR; ts.hour += hours; __normalizeHour(ts); } return ts } // normalize hours function __normalizeHour (ts) { if (ts.hour >= HOURS_IN_DAY || ts.hour < 0) { const days = Math.floor(ts.hour / HOURS_IN_DAY); ts.hour -= days * HOURS_IN_DAY; ts.day += days; __normalizeDay(ts); } return ts } // normalize days function __normalizeDay (ts) { __normalizeMonth(ts); let dim = daysInMonth(ts.year, ts.month); if (ts.day > dim) { ++ts.month; if (ts.month > MONTH_MAX) { __normalizeMonth(ts); } let days = ts.day - dim; dim = daysInMonth(ts.year, ts.month); do { if (days > dim) { ++ts.month; if (ts.month > MONTH_MAX) { __normalizeMonth(ts); } days -= dim; dim = daysInMonth(ts.year, ts.month); } } while (days > dim) ts.day = days; } else if (ts.day <= 0) { let days = -1 * ts.day; --ts.month; if (ts.month <= 0) { __normalizeMonth(ts); } dim = daysInMonth(ts.year, ts.month); do { if (days > dim) /* istanbul ignore next */ { days -= dim; --ts.month; if (ts.month <= 0) { __normalizeMonth(ts); } dim = daysInMonth(ts.year, ts.month); } } while (days > dim) ts.day = dim - days; } return ts } // normalize months function __normalizeMonth (ts) { if (ts.month > MONTH_MAX) { const years = Math.floor(ts.month / MONTH_MAX); ts.month = ts.month % MONTH_MAX; ts.year += years; } else if (ts.month < MONTH_MIN) { ts.month += MONTH_MAX; --ts.year; } return ts } // normalize all function __normalize (ts, type) { switch (type) { case 'minute': return __normalizeMinute(ts) case 'hour': return __normalizeHour(ts) case 'day': return __normalizeDay(ts) case 'month': return __normalizeMonth(ts) } } /** * Returns number of days between two {@link Timestamp}s * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} * @returns Number of days */ function daysBetween (ts1, ts2) { const diff = diffTimestamp(ts1, ts2, true); return Math.floor(diff / MILLISECONDS_IN_DAY) } /** * Returns number of weeks between two {@link Timestamp}s * @param {Timestamp} ts1 The first {@link Timestamp} * @param {Timestamp} ts2 The second {@link Timestamp} */ function weeksBetween (ts1, ts2) { let t1 = copyTimestamp(ts1); let t2 = copyTimestamp(ts2); t1 = findWeekday(t1, 0); t2 = findWeekday(t2, 6); return Math.ceil(daysBetween(t1, t2) / DAYS_IN_WEEK) } // Known dates - starting week on a monday to conform to browser const weekdayDateMap = { Sun: new Date('2020-01-05T00:00:00.000Z'), Mon: new Date('2020-01-06T00:00:00.000Z'), Tue: new Date('2020-01-07T00:00:00.000Z'), Wed: new Date('2020-01-08T00:00:00.000Z'), Thu: new Date('2020-01-09T00:00:00.000Z'), Fri: new Date('2020-01-10T00:00:00.000Z'), Sat: new Date('2020-01-11T00:00:00.000Z') }; function getWeekdayFormatter () { const emptyFormatter = (_d, _t) => ''; const options = { long: { timeZone: 'UTC', weekday: 'long' }, short: { timeZone: 'UTC', weekday: 'short' }, narrow: { timeZone: 'UTC', weekday: 'narrow' } }; /* istanbul ignore next */ if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') { return emptyFormatter } // type = 'narrow', 'short', 'long' function weekdayFormatter (weekday, type, locale) { try { const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options[ type ] || options[ 'long' ]); return intlFormatter.format(weekdayDateMap[ weekday ]) } catch (e) /* istanbul ignore next */ { /* eslint-disable-next-line */ console.error(`Intl.DateTimeFormat: ${e.message} -> day of week: ${ weekday }`); return emptyFormatter } } return weekdayFormatter } function getWeekdayNames (type, locale) { const shortWeekdays = Object.keys(weekdayDateMap); const weekdayFormatter = getWeekdayFormatter(); return shortWeekdays.map(weekday => weekdayFormatter(weekday, type, locale)) } function getMonthFormatter () { const emptyFormatter = (_m, _t) => ''; const options = { long: { timeZone: 'UTC', month: 'long' }, short: { timeZone: 'UTC', month: 'short' }, narrow: { timeZone: 'UTC', month: 'narrow' } }; /* istanbul ignore next */ if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') { return emptyFormatter } // type = 'narrow', 'short', 'long' function monthFormatter (month, type, locale) { try { const intlFormatter = new Intl.DateTimeFormat(locale || undefined, options[ type ] || options[ 'long' ]); const date = new Date(); date.setDate(1); date.setMonth(month); return intlFormatter.format(date) } catch (e) /* istanbul ignore next */ { /* eslint-disable-next-line */ console.error(`Intl.DateTimeFormat: ${e.message} -> month: ${ month }`); return emptyFormatter } } return monthFormatter } function getMonthNames (type, locale) { const monthFormatter = getMonthFormatter(); return [...Array(12).keys()] .map(month => monthFormatter(month, type, locale)) } function convertToUnit (input, unit = 'px') { if (input == null || input === '') { return undefined } else if (isNaN(input)) { return String(input) } else if (input === 'auto') { return input } else { return `${ Number(input) }${ unit }` } } function indexOf (array, cb) { for (let i = 0; i < array.length; i++) { if (cb(array[ i ], i) === true) { return i } } return -1 } var Plugin = { version, PARSE_REGEX, PARSE_DATE, PARSE_TIME, DAYS_IN_MONTH, DAYS_IN_MONTH_LEAP, DAYS_IN_MONTH_MIN, DAYS_IN_MONTH_MAX, MONTH_MAX, MONTH_MIN, DAY_MIN, DAYS_IN_WEEK, MINUTES_IN_HOUR, HOURS_IN_DAY, FIRST_HOUR, MILLISECONDS_IN_MINUTE, MILLISECONDS_IN_HOUR, MILLISECONDS_IN_DAY, MILLISECONDS_IN_WEEK, Timestamp, TimeObject, today, getStartOfWeek, getEndOfWeek, getStartOfMonth, getEndOfMonth, parseTime, validateTimestamp, parsed, parseTimestamp, parseDate, getDayIdentifier, getTimeIdentifier, getDayTimeIdentifier, diffTimestamp, updateRelative, updateMinutes, updateWeekday, updateDayOfYear, updateWorkWeek, updateDisabled, updateFormatted, getDayOfYear, getWorkWeek, getWeekday, isLeapYear, daysInMonth, copyTimestamp, padNumber, getDate, getTime, getDateTime, nextDay, prevDay, relativeDays, moveRelativeDays, findWeekday, getWeekdaySkips, createDayList, createIntervalList, createNativeLocaleFormatter, makeDate, makeDateTime, validateNumber, maxTimestamp, minTimestamp, isBetweenDates, isOverlappingDates, daysBetween, weeksBetween, addToDate, compareTimestamps, compareDate, compareTime, compareDateTime, getWeekdayFormatter, getWeekdayNames, getMonthFormatter, getMonthNames, // helpers convertToUnit, indexOf }; export { DAYS_IN_MONTH, DAYS_IN_MONTH_LEAP, DAYS_IN_MONTH_MAX, DAYS_IN_MONTH_MIN, DAYS_IN_WEEK, DAY_MIN, FIRST_HOUR, HOURS_IN_DAY, MILLISECONDS_IN_DAY, MILLISECONDS_IN_HOUR, MILLISECONDS_IN_MINUTE, MILLISECONDS_IN_WEEK, MINUTES_IN_HOUR, MONTH_MAX, MONTH_MIN, PARSE_DATE, PARSE_REGEX, PARSE_TIME, TimeObject, Timestamp, addToDate, compareDate, compareDateTime, compareTime, compareTimestamps, convertToUnit, copyTimestamp, createDayList, createIntervalList, createNativeLocaleFormatter, daysBetween, daysInMonth, Plugin as default, diffTimestamp, findWeekday, getDate, getDateTime, getDayIdentifier, getDayOfYear, getDayTimeIdentifier, getEndOfMonth, getEndOfWeek, getMonthFormatter, getMonthNames, getStartOfMonth, getStartOfWeek, getTime, getTimeIdentifier, getWeekday, getWeekdayFormatter, getWeekdayNames, getWeekdaySkips, getWorkWeek, indexOf, isBetweenDates, isLeapYear, isOverlappingDates, makeDate, makeDateTime, maxTimestamp, minTimestamp, moveRelativeDays, nextDay, padNumber, parseDate, parseTime, parseTimestamp, parsed, prevDay, relativeDays, today, updateDayOfYear, updateDisabled, updateFormatted, updateMinutes, updateRelative, updateWeekday, updateWorkWeek, validateNumber, validateTimestamp, version, weeksBetween };