You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/lib/util/dates/queries.ts

534 lines
20 KiB

/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import type {AnyCalendarDate, AnyTime, Calendar} from './types';
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate';
import {fromAbsolute, toAbsolute, toCalendar, toCalendarDate} from './conversion';
import {weekStartData} from './weekStartData';
export type DateValue = CalendarDate | CalendarDateTime | ZonedDateTime;
/** Returns whether the given dates occur on the same day, regardless of the time or calendar system. */
export function isSameDay(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
return a.era === b.era && a.year === b.year && a.month === b.month && a.day === b.day;
}
/** Returns whether the given dates occur in the same month, using the calendar system of the first date. */
export function isSameMonth(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
// In the Japanese calendar, months can span multiple eras/years, so only compare the first of the month.
a = startOfMonth(a);
b = startOfMonth(b);
return a.era === b.era && a.year === b.year && a.month === b.month;
}
/** Returns whether the given dates occur in the same year, using the calendar system of the first date. */
export function isSameYear(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
a = startOfYear(a);
b = startOfYear(b);
return a.era === b.era && a.year === b.year;
}
/** Returns whether the given dates occur on the same day, and are of the same calendar system. */
export function isEqualDay(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameDay(a, b);
}
/** Returns whether the given dates occur in the same month, and are of the same calendar system. */
export function isEqualMonth(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameMonth(a, b);
}
/** Returns whether the given dates occur in the same year, and are of the same calendar system. */
export function isEqualYear(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameYear(a, b);
}
/** Returns whether two calendars are the same. */
export function isEqualCalendar(a: Calendar, b: Calendar): boolean {
return a.isEqual?.(b) ?? b.isEqual?.(a) ?? a.identifier === b.identifier;
}
/** Returns whether the date is today in the given time zone. */
export function isToday(date: DateValue, timeZone: string): boolean {
return isSameDay(date, today(timeZone));
}
const DAY_MAP = {
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6
};
type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';
/**
* Returns the day of week for the given date and locale. Days are numbered from zero to six,
* where zero is the first day of the week in the given locale. For example, in the United States,
* the first day of the week is Sunday, but in France it is Monday.
*/
export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let julian = date.calendar.toJulianDay(date);
// If julian is negative, then julian % 7 will be negative, so we adjust
// accordingly. Julian day 0 is Monday.
let weekStart = firstDayOfWeek ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale);
let dayOfWeek = Math.ceil(julian + 1 - weekStart) % 7;
if (dayOfWeek < 0) {
dayOfWeek += 7;
}
return dayOfWeek;
}
/** Returns the current time in the given time zone. */
export function now(timeZone: string): ZonedDateTime {
return fromAbsolute(Date.now(), timeZone);
}
/** Returns today's date in the given time zone. */
export function today(timeZone: string): CalendarDate {
return toCalendarDate(now(timeZone));
}
export function compareDate(a: AnyCalendarDate, b: AnyCalendarDate): number {
return a.calendar.toJulianDay(a) - b.calendar.toJulianDay(b);
}
export function compareTime(a: AnyTime, b: AnyTime): number {
return timeToMs(a) - timeToMs(b);
}
function timeToMs(a: AnyTime): number {
return a.hour * 60 * 60 * 1000 + a.minute * 60 * 1000 + a.second * 1000 + a.millisecond;
}
/**
* Returns the number of hours in the given date and time zone.
* Usually this is 24, but it could be 23 or 25 if the date is on a daylight saving transition.
*/
export function getHoursInDay(a: CalendarDate, timeZone: string): number {
let ms = toAbsolute(a, timeZone);
let tomorrow = a.add({days: 1});
let tomorrowMs = toAbsolute(tomorrow, timeZone);
return (tomorrowMs - ms) / 3600000;
}
let localTimeZone: string | null = null;
let localTimeZoneOverride = false;
/** Returns the time zone identifier for the current user. */
export function getLocalTimeZone(): string {
if (localTimeZone == null) {
localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
}
return localTimeZone!;
}
/** Sets the time zone identifier for the current user. */
export function setLocalTimeZone(timeZone: string): void {
localTimeZoneOverride = true;
localTimeZone = timeZone;
}
/** Resets the time zone identifier for the current user. */
export function resetLocalTimeZone(): void {
localTimeZoneOverride = false;
localTimeZone = null;
}
/** Returns whether the local time zone has been explicitly overridden via `setLocalTimeZone`. */
export function isLocalTimeZoneOverridden(): boolean {
return localTimeZoneOverride;
}
/** Returns the first date of the month for the given date. */
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;
export function startOfMonth(date: CalendarDate): CalendarDate;
export function startOfMonth(date: DateValue): DateValue;
export function startOfMonth(date: DateValue): DateValue {
// Use `subtract` instead of `set` so we don't get constrained in an era.
return date.subtract({days: date.day - 1});
}
/** Returns the last date of the month for the given date. */
export function endOfMonth(date: ZonedDateTime): ZonedDateTime;
export function endOfMonth(date: CalendarDateTime): CalendarDateTime;
export function endOfMonth(date: CalendarDate): CalendarDate;
export function endOfMonth(date: DateValue): DateValue;
export function endOfMonth(date: DateValue): DateValue {
return date.add({days: date.calendar.getDaysInMonth(date) - date.day});
}
/** Returns the first day of the year for the given date. */
export function startOfYear(date: ZonedDateTime): ZonedDateTime;
export function startOfYear(date: CalendarDateTime): CalendarDateTime;
export function startOfYear(date: CalendarDate): CalendarDate;
export function startOfYear(date: DateValue): DateValue;
export function startOfYear(date: DateValue): DateValue {
return startOfMonth(date.subtract({months: date.month - 1}));
}
/** Returns the last day of the year for the given date. */
export function endOfYear(date: ZonedDateTime): ZonedDateTime;
export function endOfYear(date: CalendarDateTime): CalendarDateTime;
export function endOfYear(date: CalendarDate): CalendarDate;
export function endOfYear(date: DateValue): DateValue;
export function endOfYear(date: DateValue): DateValue {
return endOfMonth(date.add({months: date.calendar.getMonthsInYear(date) - date.month}));
}
export function getMinimumMonthInYear(date: AnyCalendarDate): number {
if (date.calendar.getMinimumMonthInYear) {
return date.calendar.getMinimumMonthInYear(date);
}
return 1;
}
export function getMinimumDayInMonth(date: AnyCalendarDate): number {
if (date.calendar.getMinimumDayInMonth) {
return date.calendar.getMinimumDayInMonth(date);
}
return 1;
}
/** Returns the first date of the week for the given date and locale. */
export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function startOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek);
return date.subtract({days: dayOfWeek});
}
/** Returns the last date of the week for the given date and locale. */
export function endOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function endOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function endOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
return startOfWeek(date, locale, firstDayOfWeek).add({days: 6});
}
const cachedRegions = new Map<string, string>();
const cachedWeekInfo = new Map<string, {firstDay: number}>();
function getRegion(locale: string): string | undefined {
// If the Intl.Locale API is available, use it to get the region for the locale.
// @ts-ignore
if (Intl.Locale) {
// Constructing an Intl.Locale is expensive, so cache the result.
let region = cachedRegions.get(locale);
if (!region) {
// @ts-ignore
region = new Intl.Locale(locale).maximize().region;
if (region) {
cachedRegions.set(locale, region);
}
}
return region;
}
// If not, just try splitting the string.
// If the second part of the locale string is 'u',
// then this is a unicode extension, so ignore it.
// Otherwise, it should be the region.
let part = locale.split('-')[1];
return part === 'u' ? undefined : part;
}
function getWeekStart(locale: string): number {
// TODO: use Intl.Locale for this once browsers support the weekInfo property
// https://github.com/tc39/proposal-intl-locale-info
let weekInfo = cachedWeekInfo.get(locale);
if (!weekInfo) {
if (Intl.Locale) {
// @ts-ignore
let localeInst = new Intl.Locale(locale);
if ('getWeekInfo' in localeInst) {
// @ts-expect-error
weekInfo = localeInst.getWeekInfo();
if (weekInfo) {
cachedWeekInfo.set(locale, weekInfo);
return weekInfo.firstDay;
}
}
}
let region = getRegion(locale);
if (locale.includes('-fw-')) {
// pull the value for the attribute fw from strings such as en-US-u-ca-iso8601-fw-tue or en-US-u-ca-iso8601-fw-mon-nu-thai
// where the fw attribute could be followed by another unicode locale extension or not
let day = locale.split('-fw-')[1].split('-')[0];
if (day === 'mon') {
weekInfo = {firstDay: 1};
} else if (day === 'tue') {
weekInfo = {firstDay: 2};
} else if (day === 'wed') {
weekInfo = {firstDay: 3};
} else if (day === 'thu') {
weekInfo = {firstDay: 4};
} else if (day === 'fri') {
weekInfo = {firstDay: 5};
} else if (day === 'sat') {
weekInfo = {firstDay: 6};
} else {
weekInfo = {firstDay: 0};
}
} else if (locale.includes('-ca-iso8601')) {
weekInfo = {firstDay: 1};
} else {
weekInfo = {firstDay: region ? weekStartData[region as keyof typeof weekStartData] || 0 : 0};
}
cachedWeekInfo.set(locale, weekInfo);
}
return weekInfo.firstDay;
}
/** Returns the number of weeks in the given month and locale. */
export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let days = date.calendar.getDaysInMonth(date);
return Math.ceil((getDayOfWeek(startOfMonth(date), locale, firstDayOfWeek) + days) / 7);
}
/** Returns the lesser of the two provider dates. */
export function minDate<A extends DateValue, B extends DateValue>(a?: A | null, b?: B | null): A | B | null | undefined {
if (a && b) {
return a.compare(b) <= 0 ? a : b;
}
return a || b;
}
/** Returns the greater of the two provider dates. */
export function maxDate<A extends DateValue, B extends DateValue>(a?: A | null, b?: B | null): A | B | null | undefined {
if (a && b) {
return a.compare(b) >= 0 ? a : b;
}
return a || b;
}
const WEEKEND_DATA = {
AF: [4, 5],
AE: [5, 6],
BH: [5, 6],
DZ: [5, 6],
EG: [5, 6],
IL: [5, 6],
IQ: [5, 6],
IR: [5, 5],
JO: [5, 6],
KW: [5, 6],
LY: [5, 6],
OM: [5, 6],
QA: [5, 6],
SA: [5, 6],
SD: [5, 6],
SY: [5, 6],
YE: [5, 6]
};
/** Returns whether the given date is on a weekend in the given locale. */
export function isWeekend(date: DateValue, locale: string): boolean {
let julian = date.calendar.toJulianDay(date);
// If julian is negative, then julian % 7 will be negative, so we adjust
// accordingly. Julian day 0 is Monday.
let dayOfWeek = Math.ceil(julian + 1) % 7;
if (dayOfWeek < 0) {
dayOfWeek += 7;
}
let region = getRegion(locale);
// Use Intl.Locale for this once weekInfo is supported.
// https://github.com/tc39/proposal-intl-locale-info
let [start, end] = (region ? WEEKEND_DATA[region as keyof typeof WEEKEND_DATA] : undefined) || [6, 0];
return dayOfWeek === start || dayOfWeek === end;
}
/** Returns whether the given date is on a weekday in the given locale. */
export function isWeekday(date: DateValue, locale: string): boolean {
return !isWeekend(date, locale);
}
// ─── Domain types ─────────────────────────────────────────────────────────────
/** Union of all date+time value types. */
export type TimeValue = Time | CalendarDateTime | ZonedDateTime;
/** A date range with optional start and end. */
export type DateRange = {
start: DateValue | undefined;
end: DateValue | undefined;
};
/** A time range with optional start and end. */
export type TimeRange<T extends TimeValue = Time> = {
start: T | undefined;
end: T | undefined;
};
/** A predicate that returns true if the given date matches some criteria. */
export type DateMatcher = (date: DateValue) => boolean;
/**
* A function that returns validation errors for a date, or nothing if valid.
*/
export type DateValidator = (date: DateValue) => string[] | string | void;
/**
* A function that returns validation errors for a date range, or nothing if valid.
*/
export type DateRangeValidator = (range: { start: DateValue; end: DateValue }) => string[] | string | void;
/** A function that returns validation errors for a time value, or nothing if valid. */
export type TimeValidator<T extends TimeValue> = (time: T) => string[] | string | void;
/** A function that returns validation errors for a time range, or nothing if valid. */
export type TimeRangeValidator<T extends TimeValue = Time> = (range: { start: T; end: T }) => string[] | string | void;
/** Callback fired when a date value is determined to be invalid. */
export type DateOnInvalid = (reason: 'min' | 'max' | 'custom', msg?: string | string[]) => void;
/** Callback fired when a time value is determined to be invalid. */
export type TimeOnInvalid = (reason: 'min' | 'max' | 'custom', msg?: string | string[]) => void;
/**
* Represents a single month in a calendar grid.
* - `value` — the DateValue that identifies this month.
* - `weeks` — rows of dates (T[][]) for rendering a table grid.
* - `dates` — flat array of all dates shown in the grid.
*/
export type Month<T> = {
value: DateValue;
weeks: T[][];
dates: T[];
};
// ─── Comparison helpers ───────────────────────────────────────────────────────
function isBeforeOrSame(dateToCompare: DateValue, referenceDate: DateValue): boolean {
return dateToCompare.compare(referenceDate) <= 0;
}
function isAfterOrSame(dateToCompare: DateValue, referenceDate: DateValue): boolean {
return dateToCompare.compare(referenceDate) >= 0;
}
/** Returns whether `dateToCompare` is strictly before `referenceDate`. */
export function isBefore(dateToCompare: DateValue, referenceDate: DateValue): boolean {
return dateToCompare.compare(referenceDate) < 0;
}
/** Returns whether `dateToCompare` is strictly after `referenceDate`. */
export function isAfter(dateToCompare: DateValue, referenceDate: DateValue): boolean {
return dateToCompare.compare(referenceDate) > 0;
}
/** Returns whether `date` is inclusively between `start` and `end`. */
export function isBetweenInclusive(date: DateValue, start: DateValue, end: DateValue): boolean {
return isAfterOrSame(date, start) && isBeforeOrSame(date, end);
}
/** Type guard — returns true if `dateValue` is a `ZonedDateTime`. */
export function isZonedDateTime(dateValue: DateValue | TimeValue): dateValue is ZonedDateTime {
return dateValue instanceof ZonedDateTime;
}
/** Returns true if `dateValue` contains time components. */
export function hasTime(dateValue: DateValue): dateValue is CalendarDateTime | ZonedDateTime {
return dateValue instanceof CalendarDateTime || dateValue instanceof ZonedDateTime;
}
/**
* Returns the number of days in the month of the given date.
* Accepts a native `Date` or a `DateValue`.
*/
export function getDaysInMonth(date: Date | DateValue): number {
if (date instanceof Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
return new Date(year, month, 0).getDate();
}
return date.set({ day: 100 }).day;
}
/** Returns the first day of the week that contains `date`. */
export function getLastFirstDayOfWeek<T extends DateValue = DateValue>(
date: T,
firstDayOfWeek: number,
locale: string
): T {
const day = getDayOfWeek(date, locale);
if (firstDayOfWeek > day) return date.subtract({ days: day + 7 - firstDayOfWeek }) as T;
if (firstDayOfWeek === day) return date as T;
return date.subtract({ days: day - firstDayOfWeek }) as T;
}
/** Returns the last day of the week that contains `date`. */
export function getNextLastDayOfWeek<T extends DateValue = DateValue>(
date: T,
firstDayOfWeek: number,
locale: string
): T {
const day = getDayOfWeek(date, locale);
const lastDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
if (day === lastDayOfWeek) return date as T;
if (day > lastDayOfWeek) return date.add({ days: 7 - day + lastDayOfWeek }) as T;
return date.add({ days: lastDayOfWeek - day }) as T;
}
/**
* Returns true if all days between `start` and `end` pass the provided
* `isUnavailable` and `isDisabled` matchers (or if no matchers are given).
*/
export function areAllDaysBetweenValid(
start: DateValue,
end: DateValue,
isUnavailable: DateMatcher | undefined,
isDisabled: DateMatcher | undefined
): boolean {
if (isUnavailable === undefined && isDisabled === undefined) return true;
let dCurrent = start.add({ days: 1 });
if (isDisabled?.(dCurrent) || isUnavailable?.(dCurrent)) return false;
const dEnd = end;
while (dCurrent.compare(dEnd) < 0) {
dCurrent = dCurrent.add({ days: 1 });
if (isDisabled?.(dCurrent) || isUnavailable?.(dCurrent)) return false;
}
return true;
}
// ─── Time comparison helpers ──────────────────────────────────────────────────
/** Returns whether `timeToCompare` is strictly before `referenceTime`. */
export function isTimeBefore(timeToCompare: Time, referenceTime: Time): boolean {
return timeToCompare.compare(referenceTime) < 0;
}
/** Returns whether `timeToCompare` is strictly after `referenceTime`. */
export function isTimeAfter(timeToCompare: Time, referenceTime: Time): boolean {
return timeToCompare.compare(referenceTime) > 0;
}

Powered by TurnKey Linux.