mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
377 lines
14 KiB
JavaScript
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
|
|
}
|
|
// ====================================================================
|
|
}
|