|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
if (timeZone === 'UTC') { |
|
return 0; |
|
} |
|
|
|
|
|
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<string, Intl.DateTimeFormat>(); |
|
|
|
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 { |
|
|
|
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, |
|
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); |
|
|
|
|
|
if (timeZone === 'UTC') { |
|
return epochFromDate(dateTime); |
|
} |
|
|
|
|
|
if (timeZone === getLocalTimeZone() && disambiguation === 'compatible') { |
|
dateTime = toCalendar(dateTime, new GregorianCalendar()); |
|
|
|
|
|
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) { |
|
|
|
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); |
|
|
|
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)); |
|
} |
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
export function fromDate(date: Date, timeZone: string): ZonedDateTime { |
|
return fromAbsolute(date.getTime(), timeZone); |
|
} |
|
|
|
export function fromDateToLocal(date: Date): ZonedDateTime { |
|
return fromDate(date, getLocalTimeZone()); |
|
} |
|
|
|
|
|
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 |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
); |
|
} |
|
|
|
|
|
export function toTime(dateTime: CalendarDateTime | ZonedDateTime): Time { |
|
return new Time(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond); |
|
} |
|
|
|
|
|
export function toCalendar<T extends AnyCalendarDate>(date: T, calendar: Calendar): T { |
|
if (date.calendar.identifier === calendar.identifier) { |
|
return date; |
|
} |
|
|
|
let calendarDate = calendar.fromJulianDay(date.calendar.toJulianDay(date)); |
|
let copy: Mutable<T> = 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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
export function toTimeZone(date: ZonedDateTime, timeZone: string): ZonedDateTime { |
|
let ms = epochFromDate(date) - date.offset; |
|
return toCalendar(fromAbsolute(ms, timeZone), date.calendar); |
|
} |
|
|
|
|
|
export function toLocalTimeZone(date: ZonedDateTime): ZonedDateTime { |
|
return toTimeZone(date, getLocalTimeZone()); |
|
} |
|
|