v-calendar
Version:
A calendar and date picker plugin for Vue.js.
997 lines (926 loc) • 24 kB
text/typescript
import {
pad,
isNumber,
isString,
isDate,
isArray,
arrayHasItems,
isFunction,
isObject,
} from '../helpers';
import toFnsDate from 'date-fns-tz/toDate';
import getWeeksInMonth from 'date-fns/getWeeksInMonth';
import getWeek from 'date-fns/getWeek';
import getISOWeek from 'date-fns/getISOWeek';
import addDays from 'date-fns/addDays';
import addMonths from 'date-fns/addMonths';
import addYears from 'date-fns/addYears';
import { type LocaleConfig, default as Locale } from '../locale';
export { addDays, addMonths, addYears };
export { DateRepeat } from './repeat';
// #region Types
type DayNameLength = 'narrow' | 'short' | 'long';
type MonthNameLength = 'short' | 'long';
export type DayInMonth =
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 18
| 20
| 21
| 22
| 23
| 24
| 25
| 26
| 27
| 28
| 29
| 30
| 31
| -1
| -2
| -3
| -4
| -5
| -6
| -7
| -8
| -9
| -10
| -11
| -12
| -13
| -14
| -15
| -16
| -17
| -18
| -18
| -20
| -21
| -22
| -23
| -24
| -25
| -26
| -27
| -28
| -29
| -30
| -31;
export type DayOfWeek = 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type WeekInMonth = 1 | 2 | 3 | 4 | 5 | 6;
export type WeekInMonthFromEnd = -6 | -5 | -4 | -3 | -2 | -1;
export type OrdinalWeekInMonth = -5 | -4 | -3 | -2 | -1 | 1 | 2 | 3 | 4 | 5;
export type MonthInYear = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type StartOfWeek = 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type WeekStartsOn = 0 | 1 | 2 | 3 | 4 | 5 | 6;
export type DateSource = Date | string | number;
export type TimeNames = Partial<Record<Intl.RelativeTimeFormatUnit, string>>;
// #endregion Types
export function isDayInMonth(dayInMonth: unknown): dayInMonth is DayInMonth {
if (!isNumber(dayInMonth)) return false;
return dayInMonth >= 1 && dayInMonth <= 31;
}
export function isDayOfWeek(dayOfWeek: unknown): dayOfWeek is DayOfWeek {
if (!isNumber(dayOfWeek)) return false;
return dayOfWeek >= 1 && dayOfWeek <= 7;
}
export function isWeekInMonth(
weekInMonth: unknown,
): weekInMonth is WeekInMonth {
if (!isNumber(weekInMonth)) return false;
return (
(weekInMonth >= -6 && weekInMonth <= -1) ||
(weekInMonth >= 1 && weekInMonth <= 6)
);
}
export function isMonthInYear(
monthInYear: unknown,
): monthInYear is MonthInYear {
if (!isNumber(monthInYear)) return false;
return monthInYear >= 1 && monthInYear <= 12;
}
export function isOrdinalWeekInMonth(
weekInMonth: unknown,
): weekInMonth is OrdinalWeekInMonth {
if (!isNumber(weekInMonth)) return false;
if (weekInMonth < -5 || weekInMonth > 5 || weekInMonth === 0) return false;
return true;
}
interface NumberRuleConfig {
min?: number;
max?: number;
interval?: number;
}
type DatePartsRuleFunction = (part: number, parts: TimeParts) => boolean;
type DatePartsRule =
| number
| Array<number>
| NumberRuleConfig
| DatePartsRuleFunction;
export interface DatePartsRules {
hours?: DatePartsRule;
minutes?: DatePartsRule;
seconds?: DatePartsRule;
milliseconds?: DatePartsRule;
}
export interface DatePartOption {
value: number;
label: string;
disabled?: boolean;
}
export interface FormatParseOptions {
locale?: Locale | LocaleConfig | string;
timezone?: string;
}
export interface DateOptions {
type: string;
fillDate: DateSource;
mask: string;
patch: DatePatch;
rules: DatePartsRules;
}
export interface PageAddress {
day?: number;
week?: number;
month: number;
year: number;
}
export interface TimeParts {
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
export interface SimpleDateParts {
year: number;
month: number;
day: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
export interface DayParts {
dayIndex: number;
day: number;
dayFromEnd: number;
weekday: number;
weekdayOrdinal: number;
weekdayOrdinalFromEnd: number;
week: number;
weekFromEnd: number;
weeknumber: number;
month: number;
year: number;
date: Date;
}
export interface DateParts extends DayParts {
milliseconds: number;
seconds: number;
minutes: number;
hours: number;
time: number;
dateTime: number;
isValid: boolean;
timezoneOffset: number;
isPm?: boolean;
}
export interface MonthParts {
firstDayOfWeek: DayOfWeek;
firstDayOfMonth: Date;
inLeapYear: boolean;
firstWeekday: number;
numDays: number;
numWeeks: number;
month: number;
year: number;
weeknumbers: number[];
isoWeeknumbers: number[];
}
export type DatePatch = 'dateTime' | 'date' | 'time';
export const DatePatchKeys: Record<DatePatch, (keyof SimpleDateParts)[]> = {
dateTime: [
'year',
'month',
'day',
'hours',
'minutes',
'seconds',
'milliseconds',
],
date: ['year', 'month', 'day'],
time: ['hours', 'minutes', 'seconds', 'milliseconds'],
};
export const daysInWeek = 7;
export const weeksInMonth = 6;
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = MS_PER_SECOND * 60;
export const MS_PER_HOUR = MS_PER_MINUTE * 60;
export const MS_PER_DAY = MS_PER_HOUR * 24;
const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const maskMacros = ['L', 'iso'];
type DatePartsRange = Readonly<[number, number, number]>;
type TimePartsKey = 'milliseconds' | 'seconds' | 'minutes' | 'hours';
const DATE_PART_RANGES: Record<TimePartsKey, DatePartsRange> = {
milliseconds: [0, 999, 3],
seconds: [0, 59, 2],
minutes: [0, 59, 2],
hours: [0, 23, 2],
} as const;
// #region Format constants
const token =
/d{1,2}|W{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|Z{1,4}|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g;
const literal = /\[([^]*?)\]/gm;
const formatFlags: any = {
D(d: DateParts) {
return d.day;
},
DD(d: DateParts) {
return pad(d.day, 2);
},
// Do(d: DateParts, l: Locale) {
// return l.DoFn(d.day);
// },
d(d: DateParts) {
return d.weekday - 1;
},
dd(d: DateParts) {
return pad(d.weekday - 1, 2);
},
W(d: DateParts, l: Locale) {
return l.dayNamesNarrow[d.weekday - 1];
},
WW(d: DateParts, l: Locale) {
return l.dayNamesShorter[d.weekday - 1];
},
WWW(d: DateParts, l: Locale) {
return l.dayNamesShort[d.weekday - 1];
},
WWWW(d: DateParts, l: Locale) {
return l.dayNames[d.weekday - 1];
},
M(d: DateParts) {
return d.month;
},
MM(d: DateParts) {
return pad(d.month, 2);
},
MMM(d: DateParts, l: Locale) {
return l.monthNamesShort[d.month - 1];
},
MMMM(d: DateParts, l: Locale) {
return l.monthNames[d.month - 1];
},
YY(d: DateParts) {
return String(d.year).substr(2);
},
YYYY(d: DateParts) {
return pad(d.year, 4);
},
h(d: DateParts) {
return d.hours % 12 || 12;
},
hh(d: DateParts) {
return pad(d.hours % 12 || 12, 2);
},
H(d: DateParts) {
return d.hours;
},
HH(d: DateParts) {
return pad(d.hours, 2);
},
m(d: DateParts) {
return d.minutes;
},
mm(d: DateParts) {
return pad(d.minutes, 2);
},
s(d: DateParts) {
return d.seconds;
},
ss(d: DateParts) {
return pad(d.seconds, 2);
},
S(d: DateParts) {
return Math.round(d.milliseconds / 100);
},
SS(d: DateParts) {
return pad(Math.round(d.milliseconds / 10), 2);
},
SSS(d: DateParts) {
return pad(d.milliseconds, 3);
},
a(d: DateParts, l: Locale) {
return d.hours < 12 ? l.amPm[0] : l.amPm[1];
},
A(d: DateParts, l: Locale) {
return d.hours < 12 ? l.amPm[0].toUpperCase() : l.amPm[1].toUpperCase();
},
Z() {
return 'Z';
},
ZZ(d: DateParts) {
const o = d.timezoneOffset;
return `${o > 0 ? '-' : '+'}${pad(Math.floor(Math.abs(o) / 60), 2)}`;
},
ZZZ(d: DateParts) {
const o = d.timezoneOffset;
return `${o > 0 ? '-' : '+'}${pad(
Math.floor(Math.abs(o) / 60) * 100 + (Math.abs(o) % 60),
4,
)}`;
},
ZZZZ(d: DateParts) {
const o = d.timezoneOffset;
return `${o > 0 ? '-' : '+'}${pad(Math.floor(Math.abs(o) / 60), 2)}:${pad(
Math.abs(o) % 60,
2,
)}`;
},
};
// #endregion Format constants
// #region Parse constants
const twoDigits = /\d\d?/;
const threeDigits = /\d{3}/;
const fourDigits = /\d{4}/;
const word =
/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const monthUpdate = (arrName: string) => (d: DateParts, v: string, l: any) => {
const index = l[arrName].indexOf(
v.charAt(0).toUpperCase() + v.substr(1).toLowerCase(),
);
if (~index) {
d.month = index;
}
};
const parseFlags: any = {
D: [
twoDigits,
(d: DateParts, v: number) => {
d.day = v;
},
],
Do: [
new RegExp(twoDigits.source + word.source),
(d: DateParts, v: string) => {
d.day = parseInt(v, 10);
},
],
d: [twoDigits, noop],
W: [word, noop],
M: [
twoDigits,
(d: DateParts, v: number) => {
d.month = v - 1;
},
],
MMM: [word, monthUpdate('monthNamesShort')],
MMMM: [word, monthUpdate('monthNames')],
YY: [
twoDigits,
(d: DateParts, v: number) => {
const da = new Date();
const cent = +da.getFullYear().toString().substr(0, 2);
d.year = +`${v > 68 ? cent - 1 : cent}${v}`;
},
],
YYYY: [
fourDigits,
(d: DateParts, v: number) => {
d.year = v;
},
],
S: [
/\d/,
(d: DateParts, v: number) => {
d.milliseconds = v * 100;
},
],
SS: [
/\d{2}/,
(d: DateParts, v: number) => {
d.milliseconds = v * 10;
},
],
SSS: [
threeDigits,
(d: DateParts, v: number) => {
d.milliseconds = v;
},
],
h: [
twoDigits,
(d: DateParts, v: number) => {
d.hours = v;
},
],
m: [
twoDigits,
(d: DateParts, v: number) => {
d.minutes = v;
},
],
s: [
twoDigits,
(d: DateParts, v: number) => {
d.seconds = v;
},
],
a: [
word,
(d: DateParts, v: string, l: Locale) => {
const val = v.toLowerCase();
if (val === l.amPm[0]) {
d.isPm = false;
} else if (val === l.amPm[1]) {
d.isPm = true;
}
},
],
Z: [
/[^\s]*?[+-]\d\d:?\d\d|[^\s]*?Z?/,
(d: DateParts, v: string) => {
if (v === 'Z') v = '+00:00';
const parts = `${v}`.match(/([+-]|\d\d)/gi);
if (parts) {
const minutes = +parts[1] * 60 + parseInt(parts[2], 10);
d.timezoneOffset = parts[0] === '+' ? minutes : -minutes;
}
},
],
};
parseFlags.DD = parseFlags.D;
parseFlags.dd = parseFlags.d;
parseFlags.WWWW = parseFlags.WWW = parseFlags.WW = parseFlags.W;
parseFlags.MM = parseFlags.M;
parseFlags.mm = parseFlags.m;
parseFlags.hh = parseFlags.H = parseFlags.HH = parseFlags.h;
parseFlags.ss = parseFlags.s;
parseFlags.A = parseFlags.a;
parseFlags.ZZZZ = parseFlags.ZZZ = parseFlags.ZZ = parseFlags.Z;
// #endregion Parse constants
function normalizeMasks(masks: string | string[], locale: Locale): string[] {
return (
((arrayHasItems(masks) && masks) || [
(isString(masks) && masks) || 'YYYY-MM-DD',
]) as string[]
).map(m =>
maskMacros.reduce(
(prev, curr) => prev.replace(curr, locale.masks[curr] || ''),
m,
),
);
}
export function isDateParts(parts: unknown): parts is Partial<DateParts> {
return (
isObject(parts) && 'year' in parts && 'month' in parts && 'day' in parts
);
}
export function isDateSource(date: unknown): date is DateSource {
if (date == null) return false;
return isString(date) || isNumber(date) || isDate(date);
}
export function roundDate(dateMs: number, snapMs = 0) {
if (snapMs > 0) return new Date(Math.round(dateMs / snapMs) * snapMs);
return new Date(dateMs);
}
export function startOfWeek(date: Date, firstDayOfWeek: DayOfWeek = 1) {
const day = date.getDay() + 1;
const daysToAdd =
day >= firstDayOfWeek
? firstDayOfWeek - day
: -(7 - (firstDayOfWeek - day));
return addDays(date, daysToAdd);
}
export function getStartOfWeek(date: Date, firstDayOfWeek: DayOfWeek = 1) {
const day = date.getDay() + 1;
const daysToAdd =
day >= firstDayOfWeek
? firstDayOfWeek - day
: -(7 - (firstDayOfWeek - day));
return addDays(date, daysToAdd);
}
export function getDayIndex(year: number, month: number, day: number) {
const utcDate = Date.UTC(year, month - 1, day);
return diffInDays(new Date(0), new Date(utcDate));
}
export function diffInDays(d1: Date, d2: Date) {
return Math.round((d2.getTime() - d1.getTime()) / MS_PER_DAY);
}
export function diffInWeeks(d1: Date, d2: Date) {
return Math.ceil(diffInDays(startOfWeek(d1), startOfWeek(d2)) / 7);
}
export function diffInYears(d1: Date, d2: Date) {
return d2.getUTCFullYear() - d1.getUTCFullYear();
}
export function diffInMonths(d1: Date, d2: Date) {
return diffInYears(d1, d2) * 12 + (d2.getMonth() - d1.getMonth());
}
export function getDateFromParts(
parts: Partial<SimpleDateParts>,
timezone = '',
) {
const d = new Date();
const {
year = d.getFullYear(),
month = d.getMonth() + 1,
day = d.getDate(),
hours: hrs = 0,
minutes: min = 0,
seconds: sec = 0,
milliseconds: ms = 0,
} = parts;
if (timezone) {
const dateString = `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}T${pad(
hrs,
2,
)}:${pad(min, 2)}:${pad(sec, 2)}.${pad(ms, 3)}`;
return toFnsDate(dateString, { timeZone: timezone });
}
return new Date(year, month - 1, day, hrs, min, sec, ms);
}
export function getTimezoneOffset(
parts: Partial<SimpleDateParts>,
timezone = '',
) {
const {
year: y = 0,
month: m = 0,
day: d = 0,
hours: hrs = 0,
minutes: min = 0,
seconds: sec = 0,
milliseconds: ms = 0,
} = parts;
let date;
const utcDate = new Date(Date.UTC(y, m - 1, d, hrs, min, sec, ms));
if (timezone) {
const dateString = `${pad(y, 4)}-${pad(m, 2)}-${pad(d, 2)}T${pad(
hrs,
2,
)}:${pad(min, 2)}:${pad(sec, 2)}.${pad(ms, 3)}`;
date = toFnsDate(dateString, { timeZone: timezone });
} else {
date = new Date(y, m - 1, d, hrs, min, sec, ms);
}
return (date.getTime() - utcDate.getTime()) / 60000;
}
export function getDateParts(date: Date, locale: Locale): DateParts {
let tzDate = new Date(date.getTime());
if (locale.timezone) {
tzDate = new Date(
date.toLocaleString('en-US', { timeZone: locale.timezone }),
);
tzDate.setMilliseconds(date.getMilliseconds());
}
const milliseconds = tzDate.getMilliseconds();
const seconds = tzDate.getSeconds();
const minutes = tzDate.getMinutes();
const hours = tzDate.getHours();
const time =
milliseconds +
seconds * MS_PER_SECOND +
minutes * MS_PER_MINUTE +
hours * MS_PER_HOUR;
const month = <MonthInYear>(tzDate.getMonth() + 1);
const year = tzDate.getFullYear();
const monthParts = locale.getMonthParts(month, year);
const day = <DayInMonth>tzDate.getDate();
const dayFromEnd = monthParts.numDays - day + 1;
const weekday = tzDate.getDay() + 1;
const weekdayOrdinal = Math.floor((day - 1) / 7 + 1);
const weekdayOrdinalFromEnd = Math.floor((monthParts.numDays - day) / 7 + 1);
const week = Math.ceil(
(day + Math.abs(monthParts.firstWeekday - monthParts.firstDayOfWeek)) / 7,
);
const weekFromEnd = monthParts.numWeeks - week + 1;
const weeknumber = monthParts.weeknumbers[week];
const dayIndex = getDayIndex(year, month, day);
const parts: DateParts = {
milliseconds,
seconds,
minutes,
hours,
time,
day,
dayFromEnd,
weekday,
weekdayOrdinal,
weekdayOrdinalFromEnd,
week,
weekFromEnd,
weeknumber,
month,
year,
date: tzDate,
dateTime: tzDate.getTime(),
dayIndex,
timezoneOffset: 0,
isValid: true,
};
return parts;
}
export function getMonthPartsKey(
month: number,
year: number,
firstDayOfWeek: DayOfWeek,
) {
return `${year}-${month}-${firstDayOfWeek}`;
}
export function getMonthParts(
month: number,
year: number,
firstDayOfWeek: DayOfWeek,
) {
const inLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
const firstDayOfMonth = new Date(year, month - 1, 1);
const firstWeekday = firstDayOfMonth.getDay() + 1;
const numDays = month === 2 && inLeapYear ? 29 : daysInMonths[month - 1];
const weekStartsOn: WeekStartsOn = (firstDayOfWeek - 1) as WeekStartsOn;
const numWeeks = getWeeksInMonth(firstDayOfMonth, {
weekStartsOn,
});
const weeknumbers = [];
const isoWeeknumbers = [];
for (let i = 0; i < numWeeks; i++) {
const date = addDays(firstDayOfMonth, i * 7);
weeknumbers.push(getWeek(date, { weekStartsOn }));
isoWeeknumbers.push(getISOWeek(date));
}
return {
firstDayOfWeek,
firstDayOfMonth,
inLeapYear,
firstWeekday,
numDays,
numWeeks,
month,
year,
weeknumbers,
isoWeeknumbers,
};
}
export function getWeekdayDates() {
const dates = [];
const year = 2020;
const month = 1;
const day = 5;
for (let i = 0; i < daysInWeek; i++) {
dates.push(
getDateFromParts({
year,
month,
day: day + i,
hours: 12,
}),
);
}
return dates;
}
export function getDayNames(
length: DayNameLength,
localeId: string | undefined = undefined,
) {
const dtf = new Intl.DateTimeFormat(localeId, {
weekday: length,
});
return getWeekdayDates().map(d => dtf.format(d));
}
export function getHourDates() {
const dates = [];
for (let i = 0; i <= 24; i++) {
dates.push(new Date(2000, 0, 1, i));
}
return dates;
}
export function getRelativeTimeNames(localeId = undefined): TimeNames {
const units: Intl.RelativeTimeFormatUnit[] = [
'second',
'minute',
'hour',
'day',
'week',
'month',
'quarter',
'year',
];
const rtf = new Intl.RelativeTimeFormat(localeId);
return units.reduce<TimeNames>((names, unit) => {
const parts = rtf.formatToParts(100, unit);
// @ts-ignore
names[unit] = parts[1].unit;
return names;
}, {});
}
export function getMonthDates() {
const dates = [];
for (let i = 0; i < 12; i++) {
dates.push(new Date(2000, i, 15));
}
return dates;
}
export function getMonthNames(length: MonthNameLength, localeId = undefined) {
const dtf = new Intl.DateTimeFormat(localeId, {
month: length,
timeZone: 'UTC',
});
return getMonthDates().map(d => dtf.format(d));
}
export function datePartIsValid(
part: number,
rule: DatePartsRule,
parts: DateParts,
): boolean {
if (isNumber(rule)) return rule === part;
if (isArray(rule)) return (rule as number[]).includes(part);
if (isFunction(rule)) return rule(part, parts);
if (rule.min != null && rule.min > part) return false;
if (rule.max != null && rule.max < part) return false;
if (rule.interval != null && part % rule.interval !== 0) return false;
return true;
}
export function getDatePartOptions(
parts: DateParts,
range: DatePartsRange,
rule: DatePartsRule | undefined,
) {
const options: DatePartOption[] = [];
const [min, max, padding] = range;
for (let i = min; i <= max; i++) {
if (rule == null || datePartIsValid(i, rule, parts)) {
options.push({
value: i,
label: pad(i, padding),
});
}
}
return options;
}
export function getDatePartsOptions(parts: DateParts, rules: DatePartsRules) {
return {
milliseconds: getDatePartOptions(
parts,
DATE_PART_RANGES.milliseconds,
rules.milliseconds,
),
seconds: getDatePartOptions(parts, DATE_PART_RANGES.seconds, rules.seconds),
minutes: getDatePartOptions(parts, DATE_PART_RANGES.minutes, rules.minutes),
hours: getDatePartOptions(parts, DATE_PART_RANGES.hours, rules.hours),
};
}
export function getNearestDatePart(
parts: DateParts,
range: DatePartsRange,
value: number,
rule: DatePartsRule,
) {
const options = getDatePartOptions(parts, range, rule);
const result = options.reduce((prev, opt) => {
if (opt.disabled) return prev;
if (isNaN(prev)) return opt.value;
const diffPrev = Math.abs(prev - value);
const diffCurr = Math.abs(opt.value - value);
return diffCurr < diffPrev ? opt.value : prev;
}, NaN);
return isNaN(result) ? value : result;
}
export function applyRulesForDateParts(
dateParts: DateParts,
rules: DatePartsRules,
) {
const result = <DateParts>{ ...dateParts };
Object.entries(rules).forEach(([key, rule]) => {
const range = DATE_PART_RANGES[key as TimePartsKey];
const value = dateParts[key as TimePartsKey];
result[key as TimePartsKey] = getNearestDatePart(
dateParts,
range,
value,
rule,
);
});
return result;
}
export function parseDate(
dateString: string,
mask: string | string[],
locale: Locale,
) {
const masks = normalizeMasks(mask, locale);
return (
masks
.map(m => {
if (typeof m !== 'string') {
throw new Error('Invalid mask');
}
// Reset string value
let str = dateString;
// Avoid regular expression denial of service, fail early for really long strings
// https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS
if (str.length > 1000) {
return false;
}
let isValid = true;
const dp: Partial<DateParts> = {};
m.replace(token, $0 => {
if (parseFlags[$0]) {
const info = parseFlags[$0];
const index = str.search(info[0]);
if (!~index) {
isValid = false;
} else {
str.replace(info[0], result => {
info[1](dp, result, locale);
str = str.substr(index + result.length);
return result;
});
}
}
return parseFlags[$0] ? '' : $0.slice(1, $0.length - 1);
});
if (!isValid) {
return false;
}
const today = new Date();
if (dp.hours != null) {
if (dp.isPm === true && +dp.hours !== 12) {
dp.hours = +dp.hours + 12;
} else if (dp.isPm === false && +dp.hours === 12) {
dp.hours = 0;
}
}
let date;
if (dp.timezoneOffset != null) {
dp.minutes = +(dp.minutes || 0) - +dp.timezoneOffset;
date = new Date(
Date.UTC(
dp.year || today.getFullYear(),
dp.month || 0,
dp.day || 1,
dp.hours || 0,
dp.minutes || 0,
dp.seconds || 0,
dp.milliseconds || 0,
),
);
} else {
date = locale.getDateFromParts({
year: dp.year || today.getFullYear(),
month: (dp.month || 0) + 1,
day: dp.day || 1,
hours: dp.hours || 0,
minutes: dp.minutes || 0,
seconds: dp.seconds || 0,
milliseconds: dp.milliseconds || 0,
});
}
return date;
})
.find(d => d) || new Date(dateString)
);
}
export function formatDate(
date: Date,
masks: string | string[],
locale: Locale,
) {
if (date == null) return '';
let mask = normalizeMasks(masks, locale)[0];
// Convert timezone to utc if needed
if (/Z$/.test(mask)) locale.timezone = 'utc';
const literals: string[] = [];
// Make literals inactive by replacing them with ??
mask = mask.replace(literal, ($0, $1: string) => {
literals.push($1);
return '??';
});
const dateParts = locale.getDateParts(date);
// Apply formatting rules
mask = mask.replace(token, $0 =>
$0 in formatFlags
? formatFlags[$0](dateParts, locale)
: $0.slice(1, $0.length - 1),
);
// Inline literal values back into the formatted value
return mask.replace(/\?\?/g, () => literals.shift()!);
}