ticktock-es
Version:
A Date utility library
481 lines (405 loc) • 17.5 kB
JavaScript
import dateFormat from "./dateFormatter.js";
import dateDiffFactory from "./Factories/dateDiffFactory.js";
import dateAddFactory from "./Factories/dateAddFactory.js";
import xDate from "../index.js";
import {
isNumeric,
localeInfoValidator,
localeMonthnames,
localeWeekdays,
localLocaleInfo,
retrieveFormattingFormats,
setLocaleInfo,
} from "./genericHelpers.js";
const dateDiff = dateDiffFactory();
const weekdays = weekdayFactory();
const add2Date = dateAddFactory();
export {
add2Date, addParts2Date, cloneInstance, compareDates, daysInMonth, daysUntil,
DSTActive, firstWeekday, format, fullMonth, getAggregatedInfo, getDateValues,
getDowNumber, getDTValues, getFullDate, getISO8601Weeknr, getNames, getQuarter,
getTime, getTimeValues, getWeeksInYear, hasDST, localeInfoValidator, localeMonthnames,
localeWeekdays, localLocaleInfo, nextOrPrevious, offset2Number, offsetFrom, pad0,
relocate, removeTime, revalue, setDateParts, setLocaleInfo, setProxy, setTimeParts,
timezoneAwareDifferenceTo,toJSDateString,toLocalString, dateFormat
};
function addParts2Date(instance, ...parts2Add) {
add2Date(instance, ...parts2Add);
return instance;
}
function compareDates(instance, {start, end, future, past, include = {start: false, end: false}} = {}) {
const instnc = instance.clone.UTC;
start = start?.value || start?.constructor === Date ? xDate(start?.value || start).UTC : xDate.now.UTC;
end = end && end?.value || end?.constructor === Date ? xDate(end?.value || end).UTC : xDate.now.UTC;
instnc.milliseconds = 0;
start.milliseconds = 0;
end.milliseconds = 0;
return future ? start > end : past ? start < end :
(include.start ? +instnc >= +start : +instnc > +start) && (include.end ? +instnc <= +end : +instnc < +end);
}
function format(instance, {zoneTime = false, formatStr, moreOptions} = {}) {
moreOptions = zoneTime
? instance.localeInfo.formatOptions + (moreOptions ? `,${moreOptions}` : '')
: localLocaleInfo.formatOptions + (moreOptions ? `,${moreOptions}` : '');
if (!zoneTime) {
return formatLocal(instance, formatStr, moreOptions);
}
if (!instance.localeInfo) {
instance.localeInfo = localLocaleInfo;
}
return dateFormat(instance, formatStr, moreOptions);
}
function formatLocal(instance, formatStr, options) {
const localized = instance.clone.relocate(localLocaleInfo);
options = (options || ``).startsWith(`+`)
? `${localized.localeInfo.formatOptions},${options.slice(1)}`
: options || localized.localeInfo.formatOptions;
return dateFormat(localized, formatStr, options);
}
function daysUntil(instance, nextDate) {
const diff = dateDiff({start: instance, end: nextDate || instance});
return parseInt(`${diff.sign}${diff.diffInDays}`);
}
function getNames(instance, forLocale = false) {
const { locale, timeZone, } = forLocale ? instance.localeInfo : localLocaleInfo;
const formatOptions = retrieveFormattingFormats(locale, timeZone);
const monthAndDay = instance.format(`MM|WD`, formatOptions).split(`|`);
return {
locale,
timeZone,
monthName: monthAndDay[0],
dayName: monthAndDay[1],
dayNames: localeWeekdays(locale),
monthNames: localeMonthnames(locale),
};
}
function getTime(instance, inUserTimezone = false) {
const [hours, minutes, seconds, milliseconds] = getTimeValues(instance, inUserTimezone);
const values4Timezone = !inUserTimezone ? instance.timeZone : localLocaleInfo.timeZone
const returnValue = { values4Timezone, hours, minutes, seconds, milliseconds };
return Object.freeze(returnValue);
}
function getTimeValues(instance, inUserTimeZone = false) {
const tzOpt = !inUserTimeZone ? `tz:${instance.timeZone}` : `tz:${localLocaleInfo.timeZone}`;
const opts = `l:en-CA,${tzOpt},hrc:23,ts:medium`;
return instance.format("", opts)
.split(/:/)
.map(Number)
.concat(instance.getMilliseconds());
}
function getFullDate(instance, inUserTimeZone) {
const tzOpt = !inUserTimeZone ? `,tz:${instance.timeZone}` : `tz:${localLocaleInfo.timeZone}`;
let [year, month, date] = instance.format(`yyyy-mm-dd`, tzOpt).split(/-/) .map(Number);
month -= 1;
const values4Timezone = !inUserTimeZone ? instance.timeZone : localLocaleInfo.timeZone;
return Object.freeze({ values4Timezone, year, month, date, });
}
function getDateValues(instance, inUserTimezone = true) {
if (inUserTimezone) {
return [instance.getFullYear(), instance.getMonth(), instance.getDate()];
}
const values = instance.format("yyyy-m-d", instance.localeInfo.formatOptions).split(/-/).map(Number);
values[1] -= 1;
return values;
}
function firstWeekday(instance, {sunday = false} = {}) {
return nextOrPrevious(instance, { day: sunday ? `sunday` : `monday` }) ;
}
function zoneDiff(d1, d2) {
let gmt1 = d1.toString().match(/GMT([+-])\d+/)?.[0]?.slice(3) ?? `+0000`;
let gmt2 = d2.toString().match(/GMT([+-])\d+/)?.[0]?.slice(3) ?? `+0000`;
gmt1 = offset2Number(gmt1.slice(0, 3) + `:` + gmt1.slice(-2), true);
gmt2 = offset2Number(gmt2.slice(0, 3) + `:` + gmt2.slice(-2), true);
return [-gmt1[0] + gmt2[0], -gmt1[1] + gmt2[1]].map(v => gmt1[0] < 0 ? -v : v);
}
function timezoneAwareDifferenceTo({start, end} = {}) {
if (!end) {
end = start.clone;
}
if (!end?.clone) {
end = xDate(end, {timeZone: start.timeZone});
}
start = xDate(DTInTimezone(start, start.timeZone), {timeZone: start.timeZone});
end = xDate(DTInTimezone(end, end.timeZone), {timeZone: end.timeZone});
const diff = dateDiff({start, end, diffs: {timeZoneStart: start.timeZone, timeZoneEnd: end.timeZone}});
const diffZones = zoneDiff(end, start);
const aheadBehind = diffZones[0] > 0 ? `ahead of` : `behind`;
const [hr, mi] = diffZones.map(v => Math.abs(v));
const [hours, minutes] = [
`${hr} ${maybePlural(hr, `hour`)}`,
`${mi} ${maybePlural(mi, `minute`)}` ];
diff.timeZonesOffsetDifference = diff.sign.length < 1 || hr + mi === 0
? `Offsets of ${start.timeZone} and ${end.timeZone} are equal`
: `${start.timeZone} is ${hours}${mi > 0 ? ` and ${minutes}` : ``} ${aheadBehind} ${end.timeZone}`;
return diff;
}
function flipSign(sign) {
return sign === `+` ? `-` : `+`;
}
function offsetFrom(instance, from) {
const isUTC = String(from).toLowerCase() === `utc` || from.timeZone === `UTC`;
from = isUTC
? instance.clone.relocate({timeZone: `UTC`})
: xDate(instance.value, { timeZone: from.timeZone || localLocaleInfo.timeZone });
const diff = timezoneAwareDifferenceTo({start: instance.clone, end: from});
const sign = !isUTC ? diff.sign : flipSign(diff.sign);
const offset = `${sign}${pad0(diff.hours)}:${pad0(diff.minutes)}`;
return {
fromTZ: instance.timeZone,
toTZ: from.timeZone,
offset,
offsetText: `${from.timeZone} ` + timeDiffenceInWords(offset) + ` ${instance.timeZone}`
};
}
function pad0(number2Pad, n = 2) {
return `${number2Pad}`.padStart(n, `0`);
}
function maybePlural(value, word) {
return `${word}${value > 1 || value === 0 ? `s` : ``}`;
}
function timeDiffenceInWords(diffInfo) {
if (/00:00/.test(diffInfo)) { return `no time diffence to`; }
const hoursAndMinutes = diffInfo.slice(1).split(`:`).map(Number);
const [hours, minutes] = hoursAndMinutes;
const later = diffInfo.at(0) === `+`;
return minutes > 0
? `${hours} ${maybePlural(hours, `hour`)} and ${minutes} ${
maybePlural(minutes, `minute`)} ${later ? `ahead of`: `behind`}`
: `${hours} ${maybePlural(hours, `hour`)} ${later ? `ahead of`: `behind`}`;
}
function toFormattedJSDateString(instance, formatString, formatOptions) {
return instance.clone.format(formatString, formatOptions || instance.localeInfo.formatOptions);
}
function toJSDateString(instance, {withFormat, withFormatOptions, local=false} = {}) {
if (withFormat) {
return local
? toFormattedJSDateString(instance, withFormat, $D.localeInformation.formatOptions)
: toFormattedJSDateString(instance, withFormat, withFormatOptions);
}
const instanceEN = instance.clone.relocate({locale: `en`});
const fmtOpts = local ? localLocaleInfo.formatOptions : instanceEN.localeInfo.formatOptions;
const gmtString = instanceEN.format(`tz`, fmtOpts + `,tzn:longOffset`).replace(`:`, ``);
const formatString = `wd M dd yyyy hh:mmi:ss ${gmtString} (tz)`;
return instanceEN.format(formatString, fmtOpts + `,tzn:long, hrc:23`);
}
function getDowNumber(instance, remote = false) {
const dayFormat = Intl.DateTimeFormat(`en`, {
timeZone: remote ? instance.timeZone : localLocaleInfo.timeZone,
weekday: "short",
});
return weekdays(dayFormat.format(instance));
}
function getAggregatedInfo(instance) {
const userZone = localLocaleInfo;
const remoteZone = instance.localeInfo;
const localInstance = instance.clone.relocate({locale: userZone.locale, timeZone: userZone.timeZone});
const timeDifferenceUserLocal2Remote = instance.offsetFrom(localInstance); //.offset
const timeDifferenceRemote2UserLocal = localInstance.offsetFrom(instance);//.offset;
const local = userZone;
const remote = remoteZone;
const pmRemote = instance.format(`hh:mmi:ss dp`, `hrc:12,tz:${instance.timeZone}`);
const pmLocal = localInstance.format(`hh:mmi:ss dp`, `hrc:12,tz:${localInstance.timeZone}`);
const userData = {
note: "'user' are values for your locale/timeZone, 'remote' (if applicable) idem for the instance",
locales: {
user: {locale: local.locale, timeZone: local.timeZone},
},
dateTime: {
user: {
...instance.dateTime,
monthName: localInstance.monthName,
weekdayNr: localInstance.day,
weekdayName: localInstance.dayName,
dayPeriodTime: pmLocal,
hasDST: localInstance.hasDST,
DSTActive: localInstance.DSTActive,
offsetFromRemote: timeDifferenceUserLocal2Remote.offset,
string: localInstance.toString()
},
},
offset: {
fromUTC: instance.UTC.offsetFrom(instance).offsetText,
}
};
if (remoteZone.timeZone !== userZone.timeZone) {
userData.locales.remote = {locale: remote.locale, timeZone: remote.timeZone };
userData.dateTime.remote = {
...instance.zoneDateTime,
monthName: instance.zoneNames.monthName,
weekdayNr: getDowNumber(instance, true),
weekdayName: instance.zoneNames.dayName,
dayPeriodTime: pmRemote,
hasDST: instance.hasDST,
DSTActive: instance.DSTActive,
offsetFromUser: timeDifferenceRemote2UserLocal.offset,
string: instance.toString(),
};
userData.offset.fromUserTime = timeDifferenceRemote2UserLocal.offsetText;
}
return userData;
}
function getDTValues(instance, local = true) {
if (local) {
return [
instance.getFullYear(),
instance.getMonth(),
instance.getDate(),
instance.getHours(),
instance.getMinutes(),
instance.getSeconds(),
instance.getMilliseconds()
];
}
const numbers = instance.format("yyyy-m-d-hh-mmi-ss", `${instance.localeInfo.formatOptions},hrc23:true`)
.split(/-/)
.map(Number)
.concat(instance.getMilliseconds());
numbers[1] -= 1;
return numbers;
}
function daysInMonth(instance) {
return new Date(instance.year, instance.month + 1, 0, 0, 0, 0).getDate();
}
function fullMonth(instance, forLocale) {
forLocale = localeInfoValidator({locale: forLocale}).locale;
const firstDay = instance.clone.relocate({locale:forLocale});
firstDay.date = { date: 1 };
return [firstDay].concat([...Array(daysInMonth(firstDay)-1)].map( (v, i) => firstDay.clone.add(`${i+1} days`) ));
}
function nextOrPrevious(instance, {day, next = false} = {}) {
let dayNr = weekdays(day?.toLowerCase());
const cloned = xDate(new Date(...instance.dateTimeValues), instance.localeInfo);
if (dayNr < 0) {
console.error(`[TickTock instance].next/previous invalid day value ${day}`);
return cloned;
}
let addTerm = next ? 1 : -1 ;
return findDayRecursive(cloned, dayNr, addTerm);
}
function findDayRecursive(cloned, dayNr, addTerm) {
return add2Date(cloned, `${addTerm} day`).getDay() < dayNr
? findDayRecursive(cloned, dayNr, addTerm)
: cloned;
}
function toLocalString(instance, {dateOnly = false, timeOnly = false} = {}) {
if (!instance.localeInfo) {
instance.localeInfo = setLocaleInfo();
}
const {locale, timeZone} = instance.localeInfo;
return dateOnly
? new Date(instance).toLocaleDateString(locale, {timeZone})
: timeOnly
? new Date(instance).toLocaleTimeString(locale, {timeZone})
: new Date(instance).toLocaleString(locale, {timeZone});
}
function DTInTimezone(date, timeZoneID) {
const timeZoneInfo = {timeZone: timeZoneID, hourCycle: `h23`};
return new Date(new Date(date).toLocaleString(`en`, timeZoneInfo));
}
function setDateParts(instance, {year, month, date} = {}) {
if (isNumeric(year)) { instance.setFullYear(parseInt(year)); }
if (isNumeric(date)) { instance.setDate(parseInt(date)); }
if (isNumeric(month)) { instance.setMonth(parseInt(month)); }
return instance;
}
function setTimeParts(instance, {hours, minutes, seconds, milliseconds} = {}) {
if (isNumeric(hours)) { instance.setHours(parseInt(hours)); }
if (isNumeric(minutes)) { instance.setMinutes(parseInt(minutes)); }
if (isNumeric(seconds)) { instance.setSeconds(parseInt(seconds)) }
if (isNumeric(milliseconds)) { instance.setMilliseconds(parseInt(milliseconds)); }
return instance;
}
function isDateOrInstance(maybeDate) {
return maybeDate?.constructor === Date || maybeDate?.value;
}
function cloneInstance(instance, date) {
if (isDateOrInstance(date)) {
return xDate(date?.value || date, instance.localeInfo);
}
return xDate.from(...instance.dateTimeValues).relocate(instance.localeInfo);
}
function weekdayFactory() {
const dow = {
short: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
long: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
};
return function(day) {
day = `${day}`.toLowerCase();
let dayNr = dow.short.indexOf(day);
return dayNr < 0 ? dow.long.indexOf(day) : dayNr;
};
}
function setProxy(proxy) {
return proxy;
}
function offset2Number(offsetString, withSign = false) {
if (withSign) {
let values = offsetString.slice(1).split(/[-:]/).map(Number);
const minus = offsetString.slice(0, 1) === `-`;
values = values.map(v => minus ? -v : v);
return values;
}
return Number(offsetString.split(/[+-]/).slice(-1)[0].replace(/:/, ``));
}
function removeTime(instance) {
instance.time = {hours: 0, minutes: 0, seconds: 0, milliseconds: 0};
return instance;
}
function getTimezoneName(dt, timeZone) {
return Intl.DateTimeFormat(`en-CA`, {timeZone, timeZoneName: `long`})
.format(dt).split(/,/)[1].trim();
}
function hasDST(instance, timeZone) {
timeZone = timeZone || instance?.timeZone || localLocaleInfo.timeZone;
instance = instance?.value
? instance : instance?.constructor === Date
? xDate(instance, {timeZone}) : xDate({timeZone});
const year = instance.year || instance.getFullYear();
const janTZN = getTimezoneName(new Date(year, 0, 1), timeZone);
const midYrTZN = getTimezoneName(new Date(year, 5, 1), timeZone);
return janTZN !== midYrTZN;
}
function DSTActive(instance, timeZone) {
timeZone = timeZone || instance?.timeZone || localLocaleInfo.timeZone;
instance = instance?.hasDST
? instance : instance?.constructor === Date
? xDate(instance, {timeZone}) : xDate({timeZone});
return instance.hasDST ? !/standard/i.test(instance.toString()) : false;
}
function relocate(instance, {locale, timeZone, l, tz} = {}) {
instance.localeInfo = localeInfoValidator({
locale: l || locale || instance.l || instance.locale,
timeZone: tz || timeZone || instance.tz || instance.timeZone,
});
return instance;
}
function revalue(instance, date) {
if (!isDateOrInstance(date)) { return instance; }
instance = xDate(date.value || date, date.localeInfo || instance.localeInfo);
return instance;
}
function getWeeksInYear(year, d) {
const currentWeek = getISO8601Weeknr(new Date(year, 11, d || 31));
return currentWeek === 1 ? getWeeksInYear(year, (d || 31) - 1) : currentWeek;
}
function getQuarter(instance, numeric) {
const currentMonth = instance.month;
switch(true) {
case currentMonth < 3: return numeric ? 1 : `First`;
case currentMonth < 6: return numeric ? 2 : `Second`;
case currentMonth < 9: return numeric ? 3 : `Third`;
case currentMonth < 12: return numeric ? 4 : `Fourth`;
default: return `unknown`;
}
}
function getISO8601Weeknr(date) {
const clone = new Date(date);
const dayn = (clone.getDay() + 6) % 7;
clone.setDate(clone.getDate() - dayn + 3);
const firstThursday = clone.valueOf();
clone.setMonth(0, 1);
if (clone.getDay() !== 4) {
clone.setMonth(0, 1 + ((4 - clone.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - clone) / 604800000);
}