Files
jandl-taskmanager-app/app/src/plugins/app@vendor/vue-cal/utils/date.js
T
2022-11-28 12:40:23 +01:00

377 lines
14 KiB
JavaScript

/**
* Date Utils & prototypes.
*
* Waiting for VS Code to support JavaScript private fields.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
* Meantime keep `_` for private.
*/
let now, todayDate, todayF, self
let _dateObject = {}
let _timeObject = {}
export default class DateUtils {
texts = {}
constructor (texts, noPrototypes = false) {
self = this // For use in Date prototypes.
this._texts = texts
// Add prototypes ASAP - only once.
if (!noPrototypes && Date && !Date.prototype.addDays) this._initDatePrototypes()
// @todo: This would be nicer, but how to set Date.noPrototypes ASAP only if user wants?
// if (Date.noPrototypes) delete Date.noPrototypes
// else this._initDatePrototypes()
}
_initDatePrototypes () {
/* eslint-disable no-extend-native */
Date.prototype.addDays = function (days) { return self.addDays(this, days) }
Date.prototype.subtractDays = function (days) { return self.subtractDays(this, days) }
Date.prototype.addHours = function (hours) { return self.addHours(this, hours) }
Date.prototype.subtractHours = function (hours) { return self.subtractHours(this, hours) }
Date.prototype.addMinutes = function (minutes) { return self.addMinutes(this, minutes) }
Date.prototype.subtractMinutes = function (minutes) { return self.subtractMinutes(this, minutes) }
Date.prototype.getWeek = function () { return self.getWeek(this) }
Date.prototype.isToday = function () { return self.isToday(this) }
Date.prototype.isLeapYear = function () { return self.isLeapYear(this) }
Date.prototype.format = function (format = 'YYYY-MM-DD') { return self.formatDate(this, format) }
Date.prototype.formatTime = function (format = 'HH:mm') { return self.formatTime(this, format) }
/* eslint-enable no-extend-native */
}
removePrototypes () {
delete Date.prototype.addDays
delete Date.prototype.subtractDays
delete Date.prototype.addHours
delete Date.prototype.subtractHours
delete Date.prototype.addMinutes
delete Date.prototype.subtractMinutes
delete Date.prototype.getWeek
delete Date.prototype.isToday
delete Date.prototype.isLeapYear
delete Date.prototype.format
delete Date.prototype.formatTime
}
updateTexts (texts) {
this._texts = texts
}
// Cache Today's date (to a maximum) for better isToday() performances. Formatted without leading 0.
// We still need to update Today's date when Today changes without page refresh.
_todayFormatted () {
if (todayDate !== (new Date()).getDate()) {
now = new Date()
todayDate = now.getDate()
todayF = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`
}
return todayF
}
// UTILITIES.
// ====================================================================
addDays (date, days) {
const d = new Date(date.valueOf())
d.setDate(d.getDate() + days)
return d
}
subtractDays (date, days) {
const d = new Date(date.valueOf())
d.setDate(d.getDate() - days)
return d
}
addHours (date, hours) {
const d = new Date(date.valueOf())
d.setHours(d.getHours() + hours)
return d
}
subtractHours (date, hours) {
const d = new Date(date.valueOf())
d.setHours(d.getHours() - hours)
return d
}
addMinutes (date, minutes) {
const d = new Date(date.valueOf())
d.setMinutes(d.getMinutes() + minutes)
return d
}
subtractMinutes (date, minutes) {
const d = new Date(date.valueOf())
d.setMinutes(d.getMinutes() - minutes)
return d
}
getWeek (date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
const dayNum = d.getUTCDay() || 7
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
}
isToday (date) {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` === this._todayFormatted()
}
isLeapYear (date) {
const year = date.getFullYear()
return !(year % 400) || (year % 100 && !(year % 4))
}
// Returns today if it's FirstDayOfWeek (Monday or Sunday) or previous FirstDayOfWeek otherwise.
getPreviousFirstDayOfWeek (date = null, weekStartsOnSunday) {
const prevFirstDayOfWeek = (date && new Date(date.valueOf())) || new Date()
const dayModifier = weekStartsOnSunday ? 7 : 6
prevFirstDayOfWeek.setDate(prevFirstDayOfWeek.getDate() - (prevFirstDayOfWeek.getDay() + dayModifier) % 7)
return prevFirstDayOfWeek
}
/**
* Converts a string to a Javascript Date object. If a Date object is passed, return it as is.
*
* @param {String | Date} date the string to convert to Date.
* @return {Date} the equivalent Javascript Date object.
*/
stringToDate (date) {
if (date instanceof Date) return date
// Regexp way is less performant: https://jsperf.com/string-to-date-regexp-vs-new-date
// const [, y, m, d, h = 0, min = 0] = date.match(/(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}))?/)
// return new Date(y, parseInt(m) - 1, d, h, min)
if (date.length === 10) date += ' 00:00'
return new Date(date.replace(/-/g, '/')) // replace '-' with '/' for Safari.
}
/**
* Simply takes a Date and returns the associated time in minutes (sum of hours + minutes).
*
* @param {Date} date the JavaScript Date to extract minutes from.
* @return {Number} the number of minutes (total of hours plus minutes).
*/
dateToMinutes = date => date.getHours() * 60 + date.getMinutes()
/**
* Count the number of days this date range spans onto.
* E.g. countDays(2019-11-02 18:00, 2019-11-03 02:00) = 2
*
* @param {String | Date} start the start date
* @param {String | Date} end the end date
* @return {Integer} The number of days this date range involves
*/
countDays (start, end) {
// replace '-' with '/' for Safari.
if (typeof start === 'string') start = start.replace(/-/g, '/')
if (typeof end === 'string') end = end.replace(/-/g, '/')
// Set start & end at midnight then compare the delta. Don't modify the original dates.
start = (new Date(start)).setHours(0, 0, 0, 0)
// Set end at midnight plus 1 sec, so Math.ceil will round it up to a full day.
end = (new Date(end)).setHours(0, 0, 1, 0)
// Remove the potential daylight saving delta.
const timezoneDiffMs = (new Date(end).getTimezoneOffset() - new Date(start).getTimezoneOffset()) * 60 * 1000
return Math.ceil((end - start - timezoneDiffMs) / (24 * 3600 * 1000))
}
/**
* Take 2 dates and check if within the same time step (useful in overlapping events).
*
* @return {Boolean} `true` if their time is included in the same time step,
* this means these 2 dates are very close.
*/
datesInSameTimeStep (date1, date2, timeStep) {
return Math.abs(date1.getTime() - date2.getTime()) <= timeStep * 60 * 1000
}
// ====================================================================
// FORMATTERS.
// ====================================================================
/**
* Formats a date/time to the given format and returns the formatted string.
*
* @param {Date} date a JavaScript Date object to format.
* @param {String} format the wanted format.
* @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts.
* This becomes useful when showing multiple instances with different languages,
* like in the documentation page.
* @return {String} the formatted date.
*/
formatDate (date, format = 'YYYY-MM-DD', texts = null) {
if (!texts) texts = this._texts
if (!format) format = 'YYYY-MM-DD' // Allows passing null for default format.
if (format === 'YYYY-MM-DD') return this.formatDateLite(date)
// Reinit the date and time object on each function call.
_dateObject = {}
_timeObject = {}
// Each keyword is a function to load the dateObject or timeObject on demand: no wasted resource.
const dateObj = {
YYYY: () => this._hydrateDateObject(date, texts).YYYY,
YY: () => this._hydrateDateObject(date, texts).YY(),
M: () => this._hydrateDateObject(date, texts).M,
MM: () => this._hydrateDateObject(date, texts).MM(),
MMM: () => this._hydrateDateObject(date, texts).MMM(),
MMMM: () => this._hydrateDateObject(date, texts).MMMM(),
MMMMG: () => this._hydrateDateObject(date, texts).MMMMG(),
D: () => this._hydrateDateObject(date, texts).D,
DD: () => this._hydrateDateObject(date, texts).DD(),
S: () => this._hydrateDateObject(date, texts).S(),
d: () => this._hydrateDateObject(date, texts).d,
dd: () => this._hydrateDateObject(date, texts).dd(),
ddd: () => this._hydrateDateObject(date, texts).ddd(),
dddd: () => this._hydrateDateObject(date, texts).dddd(),
HH: () => this._hydrateTimeObject(date, texts).HH,
H: () => this._hydrateTimeObject(date, texts).H,
hh: () => this._hydrateTimeObject(date, texts).hh,
h: () => this._hydrateTimeObject(date, texts).h,
am: () => this._hydrateTimeObject(date, texts).am,
AM: () => this._hydrateTimeObject(date, texts).AM,
mm: () => this._hydrateTimeObject(date, texts).mm,
m: () => this._hydrateTimeObject(date, texts).m
}
return format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => {
const result = dateObj[contents.replace(/\{|\}/g, '')]
return result !== undefined ? result() : contents
})
}
// More performant function to convert a Date to `YYYY-MM-DD` formatted string only.
formatDateLite (date) {
const m = date.getMonth() + 1
const d = date.getDate()
return `${date.getFullYear()}-${m < 10 ? '0' : ''}${m}-${d < 10 ? '0' : ''}${d}`
}
/**
* Formats a time (from Date or number of mins) to the given format and returns the formatted string.
*
* @param {Date | Number} date a JavaScript Date object or a time in minutes.
* @param {String} format the wanted format.
* @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts.
* This becomes useful when showing multiple instances with different languages,
* like in the documentation page.
* @param {Boolean} round if time is 23:59:59, rounds up to 24:00 for formatting only.
* @return {String} the formatted time.
*/
formatTime (date, format = 'HH:mm', texts = null, round = false) {
let shouldRound = false
if (round) {
const [h, m, s] = [date.getHours(), date.getMinutes(), date.getSeconds()]
if ((h + m + s) === (23 + 59 + 59)) shouldRound = true
}
if (date instanceof Date && format === 'HH:mm') return shouldRound ? '24:00' : this.formatTimeLite(date)
_timeObject = {} // Reinit the time object on each function call.
if (!texts) texts = this._texts
const timeObj = this._hydrateTimeObject(date, texts)
const formatted = format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => {
const result = timeObj[contents.replace(/\{|\}/g, '')]
return result !== undefined ? result : contents
})
// Round 23:59:59 to 24:00. For 12-hour format there is nothing to replace: as both are 12am.
// Also don't return `24:00` straight away as the user format may be different.
return shouldRound ? formatted.replace('23:59', '24:00') : formatted
}
/**
* Formats a time to 'HH:mm' from a Date and returns the formatted string.
*
* @param {Date} date a JavaScript Date object to format.
* @return {String} the formatted time.
*/
formatTimeLite (date) {
const h = date.getHours()
const m = date.getMinutes()
return `${(h < 10 ? '0' : '') + h}:${(m < 10 ? '0' : '') + m}`
}
_nth (d) {
if (d > 3 && d < 21) return 'th'
switch (d % 10) {
case 1: return 'st'
case 2: return 'nd'
case 3: return 'rd'
default: return 'th'
}
}
_hydrateDateObject (date, texts) {
if (_dateObject.D) return _dateObject
const YYYY = date.getFullYear()
const M = date.getMonth() + 1
const D = date.getDate()
const day = date.getDay() // Day of the week.
const dayNumber = (day - 1 + 7) % 7 // Day of the week. 0 to 6 with 6 = Sunday.
// Some of this props are functions, to only calculate on demand.
_dateObject = {
// Year.
YYYY, // 2019.
YY: () => YYYY.toString().substring(2), // 19.
// Month.
M, // 1 to 12.
MM: () => (M < 10 ? '0' : '') + M, // 01 to 12.
MMM: () => texts.months[M - 1].substring(0, 3), // Jan to Dec.
MMMM: () => texts.months[M - 1], // January to December.
MMMMG: () => (texts.monthsGenitive || texts.months)[M - 1], // January to December in genitive form (Greek...)
// Day.
D, // 1 to 31.
DD: () => (D < 10 ? '0' : '') + D, // 01 to 31.
S: () => this._nth(D), // st, nd, rd, th.
// Day of the week.
d: dayNumber + 1, // 1 to 7 with 7 = Sunday.
dd: () => texts.weekDays[dayNumber][0], // M to S.
ddd: () => texts.weekDays[dayNumber].substr(0, 3), // Mon to Sun.
dddd: () => texts.weekDays[dayNumber] // Monday to Sunday.
}
return _dateObject
}
_hydrateTimeObject (date, texts) {
if (_timeObject.am) return _timeObject
let H, m
if (date instanceof Date) {
H = date.getHours()
m = date.getMinutes()
}
else {
H = Math.floor(date / 60)
m = Math.floor(date % 60)
}
const h = H % 12 ? H % 12 : 12
const am = (texts || { am: 'am', pm: 'pm' })[H === 24 || H < 12 ? 'am' : 'pm']
_timeObject = {
H,
h,
HH: (H < 10 ? '0' : '') + H,
hh: (h < 10 ? '0' : '') + h,
am,
AM: am.toUpperCase(),
m,
mm: (m < 10 ? '0' : '') + m
}
return _timeObject
}
// ====================================================================
}