/* * 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. */ // Portions of the code in this file are based on code from the TC39 Temporal proposal. // Original licensing can be found in the NOTICE file in the root directory of this source tree. import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, DateFields, Disambiguation, TimeFields} from './types'; import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; import {constrain} from './manipulation'; import {getExtendedYear, GregorianCalendar} from './calendars/GregorianCalendar'; import {getLocalTimeZone} from './queries'; import {Mutable} from './utils'; export function epochFromDate(date: AnyDateTime) { date = toCalendar(date, new GregorianCalendar()); let year = getExtendedYear(date.era, date.year); return epochFromParts(year, date.month, date.day, date.hour, date.minute, date.second, date.millisecond); } function epochFromParts(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number) { // Note: Date.UTC() interprets one and two-digit years as being in the // 20th century, so don't use it let date = new Date(); date.setUTCHours(hour, minute, second, millisecond); date.setUTCFullYear(year, month - 1, day); return date.getTime(); } export function getTimeZoneOffset(ms: number, timeZone: string) { // Fast path for UTC. if (timeZone === 'UTC') { return 0; } // Fast path: for local timezone after 1970, use native Date. if (ms > 0 && timeZone === getLocalTimeZone()) { return new Date(ms).getTimezoneOffset() * -60 * 1000; } let {year, month, day, hour, minute, second} = getTimeZoneParts(ms, timeZone); let utc = epochFromParts(year, month, day, hour, minute, second, 0); return utc - Math.floor(ms / 1000) * 1000; } const formattersByTimeZone = new Map(); function getTimeZoneParts(ms: number, timeZone: string) { let formatter = formattersByTimeZone.get(timeZone); if (!formatter) { formatter = new Intl.DateTimeFormat('en-US', { timeZone, hour12: false, era: 'short', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }); formattersByTimeZone.set(timeZone, formatter); } let parts = formatter.formatToParts(new Date(ms)); let namedParts: {[name: string]: string} = {}; for (let part of parts) { if (part.type !== 'literal') { namedParts[part.type] = part.value; } } return { // Firefox returns B instead of BC... https://bugzilla.mozilla.org/show_bug.cgi?id=1752253 year: namedParts.era === 'BC' || namedParts.era === 'B' ? -namedParts.year + 1 : +namedParts.year, month: +namedParts.month, day: +namedParts.day, hour: namedParts.hour === '24' ? 0 : +namedParts.hour, // bugs.chromium.org/p/chromium/issues/detail?id=1045791 minute: +namedParts.minute, second: +namedParts.second }; } const DAYMILLIS = 86400000; export function possibleAbsolutes(date: CalendarDateTime, timeZone: string): number[] { let ms = epochFromDate(date); let earlier = ms - getTimeZoneOffset(ms - DAYMILLIS, timeZone); let later = ms - getTimeZoneOffset(ms + DAYMILLIS, timeZone); return getValidWallTimes(date, timeZone, earlier, later); } function getValidWallTimes(date: CalendarDateTime, timeZone: string, earlier: number, later: number): number[] { let found = earlier === later ? [earlier] : [earlier, later]; return found.filter(absolute => isValidWallTime(date, timeZone, absolute)); } function isValidWallTime(date: CalendarDateTime, timeZone: string, absolute: number) { let parts = getTimeZoneParts(absolute, timeZone); return date.year === parts.year && date.month === parts.month && date.day === parts.day && date.hour === parts.hour && date.minute === parts.minute && date.second === parts.second; } export function toAbsolute(date: CalendarDate | CalendarDateTime, timeZone: string, disambiguation: Disambiguation = 'compatible'): number { let dateTime = toCalendarDateTime(date); // Fast path: if the time zone is UTC, use native Date. if (timeZone === 'UTC') { return epochFromDate(dateTime); } // Fast path: if the time zone is the local timezone and disambiguation is compatible, use native Date. if (timeZone === getLocalTimeZone() && disambiguation === 'compatible') { dateTime = toCalendar(dateTime, new GregorianCalendar()); // Don't use Date constructor here because two-digit years are interpreted in the 20th century. let date = new Date(); let year = getExtendedYear(dateTime.era, dateTime.year); date.setFullYear(year, dateTime.month - 1, dateTime.day); date.setHours(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond); return date.getTime(); } let ms = epochFromDate(dateTime); let offsetBefore = getTimeZoneOffset(ms - DAYMILLIS, timeZone); let offsetAfter = getTimeZoneOffset(ms + DAYMILLIS, timeZone); let valid = getValidWallTimes(dateTime, timeZone, ms - offsetBefore, ms - offsetAfter); if (valid.length === 1) { return valid[0]; } if (valid.length > 1) { switch (disambiguation) { // 'compatible' means 'earlier' for "fall back" transitions case 'compatible': case 'earlier': return valid[0]; case 'later': return valid[valid.length - 1]; case 'reject': throw new RangeError('Multiple possible absolute times found'); } } switch (disambiguation) { case 'earlier': return Math.min(ms - offsetBefore, ms - offsetAfter); // 'compatible' means 'later' for "spring forward" transitions case 'compatible': case 'later': return Math.max(ms - offsetBefore, ms - offsetAfter); case 'reject': throw new RangeError('No such absolute time found'); } } export function toDate(dateTime: CalendarDate | CalendarDateTime, timeZone: string, disambiguation: Disambiguation = 'compatible'): Date { return new Date(toAbsolute(dateTime, timeZone, disambiguation)); } /** * Takes a Unix epoch (milliseconds since 1970) and converts it to the provided time zone. */ export function fromAbsolute(ms: number, timeZone: string): ZonedDateTime { let offset = getTimeZoneOffset(ms, timeZone); let date = new Date(ms + offset); let year = date.getUTCFullYear(); let month = date.getUTCMonth() + 1; let day = date.getUTCDate(); let hour = date.getUTCHours(); let minute = date.getUTCMinutes(); let second = date.getUTCSeconds(); let millisecond = date.getUTCMilliseconds(); return new ZonedDateTime(year, month, day, timeZone, offset, hour, minute, second, millisecond); } /** * Takes a `Date` object and converts it to the provided time zone. */ export function fromDate(date: Date, timeZone: string): ZonedDateTime { return fromAbsolute(date.getTime(), timeZone); } export function fromDateToLocal(date: Date): ZonedDateTime { return fromDate(date, getLocalTimeZone()); } /** Converts a value with date components such as a `CalendarDateTime` or `ZonedDateTime` into a `CalendarDate`. */ export function toCalendarDate(dateTime: AnyCalendarDate): CalendarDate { return new CalendarDate(dateTime.calendar, dateTime.era, dateTime.year, dateTime.month, dateTime.day); } export function toDateFields(date: AnyCalendarDate): DateFields { return { era: date.era, year: date.year, month: date.month, day: date.day }; } export function toTimeFields(date: AnyTime): TimeFields { return { hour: date.hour, minute: date.minute, second: date.second, millisecond: date.millisecond }; } /** * Converts a date value to a `CalendarDateTime`. An optional `Time` value can be passed to set the time * of the resulting value, otherwise it will default to midnight. */ export function toCalendarDateTime(date: CalendarDate | CalendarDateTime | ZonedDateTime, time?: AnyTime): CalendarDateTime { let hour = 0, minute = 0, second = 0, millisecond = 0; if ('timeZone' in date) { ({hour, minute, second, millisecond} = date); } else if ('hour' in date && !time) { return date; } if (time) { ({hour, minute, second, millisecond} = time); } return new CalendarDateTime( date.calendar, date.era, date.year, date.month, date.day, hour, minute, second, millisecond ); } /** Extracts the time components from a value containing a date and time. */ export function toTime(dateTime: CalendarDateTime | ZonedDateTime): Time { return new Time(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond); } /** Converts a date from one calendar system to another. */ export function toCalendar(date: T, calendar: Calendar): T { if (date.calendar.identifier === calendar.identifier) { return date; } let calendarDate = calendar.fromJulianDay(date.calendar.toJulianDay(date)); let copy: Mutable = date.copy(); copy.calendar = calendar; copy.era = calendarDate.era; copy.year = calendarDate.year; copy.month = calendarDate.month; copy.day = calendarDate.day; constrain(copy); return copy; } /** * Converts a date value to a `ZonedDateTime` in the provided time zone. The `disambiguation` option can be set * to control how values that fall on daylight saving time changes are interpreted. */ export function toZoned(date: CalendarDate | CalendarDateTime | ZonedDateTime, timeZone: string, disambiguation?: Disambiguation): ZonedDateTime { if (date instanceof ZonedDateTime) { if (date.timeZone === timeZone) { return date; } return toTimeZone(date, timeZone); } let ms = toAbsolute(date, timeZone, disambiguation); return fromAbsolute(ms, timeZone); } export function zonedToDate(date: ZonedDateTime) { let ms = epochFromDate(date) - date.offset; return new Date(ms); } /** Converts a `ZonedDateTime` from one time zone to another. */ export function toTimeZone(date: ZonedDateTime, timeZone: string): ZonedDateTime { let ms = epochFromDate(date) - date.offset; return toCalendar(fromAbsolute(ms, timeZone), date.calendar); } /** Converts the given `ZonedDateTime` into the user's local time zone. */ export function toLocalTimeZone(date: ZonedDateTime): ZonedDateTime { return toTimeZone(date, getLocalTimeZone()); }