diff --git a/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx b/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx index c58397c0a..0fdf76cd5 100644 --- a/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx +++ b/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx @@ -2,8 +2,8 @@ import { Box } from "@mui/material"; import { DatePicker, TimePicker } from "@mui/x-date-pickers"; import React from "react"; import { useController } from "react-hook-form"; +import { zonedTimeToUtc } from "../../../dateTimeUtils.js"; import type { GadgetDateTimeConfig } from "../../../internal/gql/graphql.js"; -import { zonedTimeToUtc } from "../../../utils.js"; import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; export const MUIAutoDateTimePicker = (props: { field: string; value?: Date; onChange?: (value: Date) => void; error?: string }) => { diff --git a/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx b/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx index 9b0bf2554..b726a7d8b 100644 --- a/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx +++ b/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx @@ -3,8 +3,8 @@ import { DatePicker, Icon, InlineStack, Popover, TextField } from "@shopify/pola import { CalendarIcon } from "@shopify/polaris-icons"; import React, { useCallback, useMemo, useState } from "react"; import { useController } from "react-hook-form"; +import { formatShortDateString, isValidDate, utcToZonedTime, zonedTimeToUtc } from "../../../dateTimeUtils.js"; import type { GadgetDateTimeConfig } from "../../../internal/gql/graphql.js"; -import { formatShortDateString, isValidDate, utcToZonedTime, zonedTimeToUtc } from "../../../utils.js"; import { useFieldMetadata } from "../../hooks/useFieldMetadata.js"; import type { DateTimeState } from "./PolarisAutoTimePicker.js"; import PolarisAutoTimePicker from "./PolarisAutoTimePicker.js"; diff --git a/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx b/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx index 480c41d0a..e01f9915d 100644 --- a/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx +++ b/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx @@ -3,7 +3,7 @@ import { Box, Icon, Listbox, Popover, Scrollable, Text, TextField } from "@shopi import { ClockIcon } from "@shopify/polaris-icons"; import React, { useEffect, useState } from "react"; import type { ControllerRenderProps, FieldValues } from "react-hook-form"; -import { zonedTimeToUtc } from "../../../utils.js"; +import { zonedTimeToUtc } from "../../../dateTimeUtils.js"; import { copyTime, getDateFromDateTimeObject, getDateTimeObjectFromDate } from "./PolarisAutoDateTimePicker.js"; const createMarkup = ( diff --git a/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx b/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx index f082bc7ea..c14b59c1e 100644 --- a/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx +++ b/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { formatLongDateTimeString } from "../../../utils.js"; +import { formatLongDateTimeString } from "../../../dateTimeUtils.js"; import { PolarisAutoTableTextCell } from "./PolarisAutoTableTextCell.js"; export const PolarisAutoTableDateTimeCell = (props: { value: Date; includeTime: boolean }) => { diff --git a/packages/react/src/dateTimeUtils.ts b/packages/react/src/dateTimeUtils.ts new file mode 100644 index 000000000..b19d5bbf0 --- /dev/null +++ b/packages/react/src/dateTimeUtils.ts @@ -0,0 +1,375 @@ +/** + * Formats a date object in a "yyyy-MM-dd" format + * Ex: 2000-01-30 + * @param date + * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string + */ +export const formatShortDateString = (date: Date) => { + if (!date) return ""; + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + return `${year}-${month}-${day}`; +}; + +/** + * Formats a date object in a "LLL d, y K:mm a" format + * Ex: Jun 30, 2024 8:00 PM + * @param date + * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string + */ +export const formatLongDateTimeString = (date: Date, includeTime: boolean) => { + if (!date) return ""; + + const dateString = date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + if (includeTime) { + const timeString = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + + return `${dateString} ${timeString}`; + } else return dateString; +}; + +/** + * + * @param date + * @returns + */ +export const isValidDate = (date: unknown) => { + return date instanceof Date && !isNaN(date.getTime()); +}; + +/** + * @name zonedTimeToUtc + * (called "toZonedTime" in the original date-fns-tz repo) + * @category Time Zone Helpers + * @summary Get the UTC date/time from a date representing local time in a given time zone + * @author https://github.com/marnusw + * Taken from https://github.com/marnusw/date-fns-tz + * + * @description + * Returns a date instance with the UTC time of the provided date of which the values + * represented the local time in the time zone specified. In other words, if the input + * date represented local time in time zone, the timestamp of the output date will + * give the equivalent UTC of that local time regardless of the current system time zone. + * + * @param date the date with values representing the local time + * @param timeZone the time zone of this local time, can be an offset or IANA time zone + * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options} + * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate} + * @throws {TypeError} 2 arguments required + * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2 + * + * @example + * // In June 10am in Los Angeles is 5pm UTC + * const result = fromZonedTime(new Date(2014, 5, 25, 10, 0, 0), 'America/Los_Angeles') + * //=> 2014-06-25T17:00:00.000Z + */ +export const zonedTimeToUtc = (date: Date, timeZone: string): Date => { + const utc = newDateUTC( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds() + ).getTime(); + + const offsetMilliseconds = tzParseTimezone(timeZone, new Date(utc)); + + return new Date(utc + offsetMilliseconds); +}; + +/** + * @name utcToZonedTime + * (called "toZonedTime" in the original date-fns-tz repo) + * @category Time Zone Helpers + * @summary Get a date/time representing local time in a given time zone from the UTC date + * @author https://github.com/marnusw + * Taken from https://github.com/marnusw/date-fns-tz + * + * @description + * Returns a date instance with values representing the local time in the time zone + * specified of the UTC time from the date provided. In other words, when the new date + * is formatted it will show the equivalent hours in the target time zone regardless + * of the current system time zone. + * + * @param date the date with the relevant UTC time + * @param timeZone the time zone to get local time for, can be an offset or IANA time zone + * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options} + * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate} + * + * @throws {TypeError} 2 arguments required + * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2 + * + * @example + * // In June 10am UTC is 6am in New York (-04:00) + * const result = toZonedTime('2014-06-25T10:00:00.000Z', 'America/New_York') + * //=> Jun 25 2014 06:00:00 + */ +export const utcToZonedTime = (date: Date, timeZone: string): Date => { + const offsetMilliseconds = tzParseTimezone(timeZone, date, true); + + const d = new Date(date.getTime() - offsetMilliseconds); + + const resultDate = new Date(0); + + resultDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); + + resultDate.setHours(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()); + + return resultDate; +}; + +// Helper functions for utcToZonedTime +const patterns = { + timezone: /([Z+-].*)$/, + timezoneZ: /^(Z)$/, + timezoneHH: /^([+-]\d{2})$/, + timezoneHHMM: /^([+-])(\d{2}):?(\d{2})$/, +}; + +const validIANATimezoneCache: Record = {}; + +const MILLISECONDS_IN_HOUR = 3600000; +const MILLISECONDS_IN_MINUTE = 60000; + +const isValidTimezoneIANAString = (timeZoneString: string) => { + if (validIANATimezoneCache[timeZoneString]) return true; + try { + new Intl.DateTimeFormat(undefined, { timeZone: timeZoneString }); + validIANATimezoneCache[timeZoneString] = true; + return true; + } catch (error) { + return false; + } +}; + +export const tzParseTimezone = (timezoneString: string | undefined, date: Date | number | undefined, isUtcDate?: boolean): number => { + // Empty string + if (!timezoneString) { + return 0; + } + + // Z + let token = patterns.timezoneZ.exec(timezoneString); + if (token) { + return 0; + } + + let hours: number; + let absoluteOffset: number; + + // ±hh + token = patterns.timezoneHH.exec(timezoneString); + if (token) { + hours = parseInt(token[1], 10); + + if (!validateTimezone(hours)) { + return NaN; + } + + return -(hours * MILLISECONDS_IN_HOUR); + } + + // ±hh:mm or ±hhmm + token = patterns.timezoneHHMM.exec(timezoneString); + if (token) { + hours = parseInt(token[2], 10); + const minutes = parseInt(token[3], 10); + + if (!validateTimezone(hours, minutes)) { + return NaN; + } + + absoluteOffset = Math.abs(hours) * MILLISECONDS_IN_HOUR + minutes * MILLISECONDS_IN_MINUTE; + return token[1] === "+" ? -absoluteOffset : absoluteOffset; + } + + const fixOffset = (date: Date, offset: number, timezoneString: string) => { + const localTS = date.getTime(); + + // Our UTC time is just a guess because our offset is just a guess + let utcGuess = localTS - offset; + + // Test whether the zone matches the offset for this ts + const o2 = calcOffset(new Date(utcGuess), timezoneString); + + // If so, offset didn't change, and we're done + if (offset === o2) { + return offset; + } + + // If not, change the ts by the difference in the offset + utcGuess -= o2 - offset; + + // If that gives us the local time we want, we're done + const o3 = calcOffset(new Date(utcGuess), timezoneString); + if (o2 === o3) { + return o2; + } + + // If it's different, we're in a hole time. The offset has changed, but we don't adjust the time + return Math.max(o2, o3); + }; + + // IANA time zone + if (isValidTimezoneIANAString(timezoneString)) { + date = new Date(date || Date.now()); + const utcDate = isUtcDate ? date : toUtcDate(date); + + const offset = calcOffset(utcDate, timezoneString); + + const fixedOffset = isUtcDate ? offset : fixOffset(date, offset, timezoneString); + + return -fixedOffset; + } + + return NaN; +}; + +const validateTimezone = (hours: number, minutes?: number | null) => { + return -23 <= hours && hours <= 23 && (minutes == null || (0 <= minutes && minutes <= 59)); +}; + +const calcOffset = (date: Date, timezoneString: string) => { + const tokens = tzTokenizeDate(date, timezoneString); + + // ms dropped because it's not provided by tzTokenizeDate + const asUTC = newDateUTC(tokens[0], tokens[1] - 1, tokens[2], tokens[3] % 24, tokens[4], tokens[5], 0).getTime(); + + let asTS = date.getTime(); + const over = asTS % 1000; + asTS -= over >= 0 ? over : 1000 + over; + return asUTC - asTS; +}; + +const toUtcDate = (date: Date) => { + return newDateUTC( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds() + ); +}; + +const newDateUTC = ( + fullYear: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + millisecond: number +): Date => { + const utcDate = new Date(0); + utcDate.setUTCFullYear(fullYear, month, day); + utcDate.setUTCHours(hour, minute, second, millisecond); + return utcDate; +}; + +const tzTokenizeDate = (date: Date, timeZone: string): number[] => { + const dtf = getDateTimeFormat(timeZone); + return "formatToParts" in dtf ? partsOffset(dtf, date) : hackyOffset(dtf, date); +}; + +const dtfCache: Record = {}; + +const getDateTimeFormat = (timeZone: string) => { + if (!dtfCache[timeZone]) { + // New browsers use `hourCycle`, IE and Chrome <73 does not support it and uses `hour12` + const testDateFormatted = new Intl.DateTimeFormat("en-US", { + hourCycle: "h23", + timeZone: "America/New_York", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }).format(new Date("2014-06-25T04:00:00.123Z")); + const hourCycleSupported = testDateFormatted === "06/25/2014, 00:00:00" || testDateFormatted === "‎06‎/‎25‎/‎2014‎ ‎00‎:‎00‎:‎00"; + + dtfCache[timeZone] = hourCycleSupported + ? new Intl.DateTimeFormat("en-US", { + hourCycle: "h23", + timeZone: timeZone, + year: "numeric", + month: "numeric", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + : new Intl.DateTimeFormat("en-US", { + hour12: false, + timeZone: timeZone, + year: "numeric", + month: "numeric", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } + return dtfCache[timeZone]; +}; + +const typeToPos: { [type in keyof Intl.DateTimeFormatPartTypesRegistry]?: number } = { + year: 0, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, +}; + +const partsOffset = (dtf: Intl.DateTimeFormat, date: Date) => { + try { + const formatted = dtf.formatToParts(date); + const filled: number[] = []; + for (let i = 0; i < formatted.length; i++) { + const pos = typeToPos[formatted[i].type]; + + if (pos !== undefined) { + filled[pos] = parseInt(formatted[i].value, 10); + } + } + return filled; + } catch (error) { + if (error instanceof RangeError) { + return [NaN]; + } + throw error; + } +}; + +const hackyOffset = (dtf: Intl.DateTimeFormat, date: Date) => { + const formatted = dtf.format(date); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const parsed = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(formatted)!; + // const [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed + // return [fYear, fMonth, fDay, fHour, fMinute, fSecond] + return [ + parseInt(parsed[3], 10), + parseInt(parsed[1], 10), + parseInt(parsed[2], 10), + parseInt(parsed[4], 10), + parseInt(parsed[5], 10), + parseInt(parsed[6], 10), + ]; +}; diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index 523310903..1b2d44be2 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -482,382 +482,6 @@ export const sortByProperty = (arr: T[], property: keyof T, order: SortOrder }); }; -/** - * Formats a date object in a "yyyy-MM-dd" format - * Ex: 2000-01-30 - * @param date - * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string - */ -export const formatShortDateString = (date: Date) => { - if (!date) return ""; - - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - - return `${year}-${month}-${day}`; -}; - -/** - * Formats a date object in a "LLL d, y K:mm a" format - * Ex: Jun 30, 2024 8:00 PM - * @param date - * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string - */ -export const formatLongDateTimeString = (date: Date, includeTime: boolean) => { - if (!date) return ""; - - const dateString = date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - - if (includeTime) { - const timeString = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - - return `${dateString} ${timeString}`; - } else return dateString; -}; - -/** - * - * @param date - * @returns - */ -export const isValidDate = (date: unknown) => { - return date instanceof Date && !isNaN(date.getTime()); -}; - -/** - * @name zonedTimeToUtc - * (called "toZonedTime" in the original date-fns-tz repo) - * @category Time Zone Helpers - * @summary Get the UTC date/time from a date representing local time in a given time zone - * @author https://github.com/marnusw - * Taken from https://github.com/marnusw/date-fns-tz - * - * @description - * Returns a date instance with the UTC time of the provided date of which the values - * represented the local time in the time zone specified. In other words, if the input - * date represented local time in time zone, the timestamp of the output date will - * give the equivalent UTC of that local time regardless of the current system time zone. - * - * @param date the date with values representing the local time - * @param timeZone the time zone of this local time, can be an offset or IANA time zone - * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options} - * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate} - * @throws {TypeError} 2 arguments required - * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2 - * - * @example - * // In June 10am in Los Angeles is 5pm UTC - * const result = fromZonedTime(new Date(2014, 5, 25, 10, 0, 0), 'America/Los_Angeles') - * //=> 2014-06-25T17:00:00.000Z - */ -export const zonedTimeToUtc = (date: Date, timeZone: string): Date => { - const utc = newDateUTC( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - date.getMilliseconds() - ).getTime(); - - const offsetMilliseconds = tzParseTimezone(timeZone, new Date(utc)); - - return new Date(utc + offsetMilliseconds); -}; - -/** - * @name utcToZonedTime - * (called "toZonedTime" in the original date-fns-tz repo) - * @category Time Zone Helpers - * @summary Get a date/time representing local time in a given time zone from the UTC date - * @author https://github.com/marnusw - * Taken from https://github.com/marnusw/date-fns-tz - * - * @description - * Returns a date instance with values representing the local time in the time zone - * specified of the UTC time from the date provided. In other words, when the new date - * is formatted it will show the equivalent hours in the target time zone regardless - * of the current system time zone. - * - * @param date the date with the relevant UTC time - * @param timeZone the time zone to get local time for, can be an offset or IANA time zone - * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options} - * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate} - * - * @throws {TypeError} 2 arguments required - * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2 - * - * @example - * // In June 10am UTC is 6am in New York (-04:00) - * const result = toZonedTime('2014-06-25T10:00:00.000Z', 'America/New_York') - * //=> Jun 25 2014 06:00:00 - */ -export const utcToZonedTime = (date: Date, timeZone: string): Date => { - const offsetMilliseconds = tzParseTimezone(timeZone, date, true); - - const d = new Date(date.getTime() - offsetMilliseconds); - - const resultDate = new Date(0); - - resultDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); - - resultDate.setHours(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()); - - return resultDate; -}; - -// Helper functions for utcToZonedTime -const patterns = { - timezone: /([Z+-].*)$/, - timezoneZ: /^(Z)$/, - timezoneHH: /^([+-]\d{2})$/, - timezoneHHMM: /^([+-])(\d{2}):?(\d{2})$/, -}; - -const validIANATimezoneCache: Record = {}; - -const MILLISECONDS_IN_HOUR = 3600000; -const MILLISECONDS_IN_MINUTE = 60000; - -const isValidTimezoneIANAString = (timeZoneString: string) => { - if (validIANATimezoneCache[timeZoneString]) return true; - try { - new Intl.DateTimeFormat(undefined, { timeZone: timeZoneString }); - validIANATimezoneCache[timeZoneString] = true; - return true; - } catch (error) { - return false; - } -}; - -export const tzParseTimezone = (timezoneString: string | undefined, date: Date | number | undefined, isUtcDate?: boolean): number => { - // Empty string - if (!timezoneString) { - return 0; - } - - // Z - let token = patterns.timezoneZ.exec(timezoneString); - if (token) { - return 0; - } - - let hours: number; - let absoluteOffset: number; - - // ±hh - token = patterns.timezoneHH.exec(timezoneString); - if (token) { - hours = parseInt(token[1], 10); - - if (!validateTimezone(hours)) { - return NaN; - } - - return -(hours * MILLISECONDS_IN_HOUR); - } - - // ±hh:mm or ±hhmm - token = patterns.timezoneHHMM.exec(timezoneString); - if (token) { - hours = parseInt(token[2], 10); - const minutes = parseInt(token[3], 10); - - if (!validateTimezone(hours, minutes)) { - return NaN; - } - - absoluteOffset = Math.abs(hours) * MILLISECONDS_IN_HOUR + minutes * MILLISECONDS_IN_MINUTE; - return token[1] === "+" ? -absoluteOffset : absoluteOffset; - } - - const fixOffset = (date: Date, offset: number, timezoneString: string) => { - const localTS = date.getTime(); - - // Our UTC time is just a guess because our offset is just a guess - let utcGuess = localTS - offset; - - // Test whether the zone matches the offset for this ts - const o2 = calcOffset(new Date(utcGuess), timezoneString); - - // If so, offset didn't change, and we're done - if (offset === o2) { - return offset; - } - - // If not, change the ts by the difference in the offset - utcGuess -= o2 - offset; - - // If that gives us the local time we want, we're done - const o3 = calcOffset(new Date(utcGuess), timezoneString); - if (o2 === o3) { - return o2; - } - - // If it's different, we're in a hole time. The offset has changed, but we don't adjust the time - return Math.max(o2, o3); - }; - - // IANA time zone - if (isValidTimezoneIANAString(timezoneString)) { - date = new Date(date || Date.now()); - const utcDate = isUtcDate ? date : toUtcDate(date); - - const offset = calcOffset(utcDate, timezoneString); - - const fixedOffset = isUtcDate ? offset : fixOffset(date, offset, timezoneString); - - return -fixedOffset; - } - - return NaN; -}; - -const validateTimezone = (hours: number, minutes?: number | null) => { - return -23 <= hours && hours <= 23 && (minutes == null || (0 <= minutes && minutes <= 59)); -}; - -const calcOffset = (date: Date, timezoneString: string) => { - const tokens = tzTokenizeDate(date, timezoneString); - - // ms dropped because it's not provided by tzTokenizeDate - const asUTC = newDateUTC(tokens[0], tokens[1] - 1, tokens[2], tokens[3] % 24, tokens[4], tokens[5], 0).getTime(); - - let asTS = date.getTime(); - const over = asTS % 1000; - asTS -= over >= 0 ? over : 1000 + over; - return asUTC - asTS; -}; - -const toUtcDate = (date: Date) => { - return newDateUTC( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - date.getMilliseconds() - ); -}; - -const newDateUTC = ( - fullYear: number, - month: number, - day: number, - hour: number, - minute: number, - second: number, - millisecond: number -): Date => { - const utcDate = new Date(0); - utcDate.setUTCFullYear(fullYear, month, day); - utcDate.setUTCHours(hour, minute, second, millisecond); - return utcDate; -}; - -const tzTokenizeDate = (date: Date, timeZone: string): number[] => { - const dtf = getDateTimeFormat(timeZone); - return "formatToParts" in dtf ? partsOffset(dtf, date) : hackyOffset(dtf, date); -}; - -const dtfCache: Record = {}; - -const getDateTimeFormat = (timeZone: string) => { - if (!dtfCache[timeZone]) { - // New browsers use `hourCycle`, IE and Chrome <73 does not support it and uses `hour12` - const testDateFormatted = new Intl.DateTimeFormat("en-US", { - hourCycle: "h23", - timeZone: "America/New_York", - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }).format(new Date("2014-06-25T04:00:00.123Z")); - const hourCycleSupported = testDateFormatted === "06/25/2014, 00:00:00" || testDateFormatted === "‎06‎/‎25‎/‎2014‎ ‎00‎:‎00‎:‎00"; - - dtfCache[timeZone] = hourCycleSupported - ? new Intl.DateTimeFormat("en-US", { - hourCycle: "h23", - timeZone: timeZone, - year: "numeric", - month: "numeric", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }) - : new Intl.DateTimeFormat("en-US", { - hour12: false, - timeZone: timeZone, - year: "numeric", - month: "numeric", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - return dtfCache[timeZone]; -}; - -const typeToPos: { [type in keyof Intl.DateTimeFormatPartTypesRegistry]?: number } = { - year: 0, - month: 1, - day: 2, - hour: 3, - minute: 4, - second: 5, -}; - -const partsOffset = (dtf: Intl.DateTimeFormat, date: Date) => { - try { - const formatted = dtf.formatToParts(date); - const filled: number[] = []; - for (let i = 0; i < formatted.length; i++) { - const pos = typeToPos[formatted[i].type]; - - if (pos !== undefined) { - filled[pos] = parseInt(formatted[i].value, 10); - } - } - return filled; - } catch (error) { - if (error instanceof RangeError) { - return [NaN]; - } - throw error; - } -}; - -const hackyOffset = (dtf: Intl.DateTimeFormat, date: Date) => { - const formatted = dtf.format(date); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const parsed = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(formatted)!; - // const [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed - // return [fYear, fMonth, fDay, fHour, fMinute, fSecond] - return [ - parseInt(parsed[3], 10), - parseInt(parsed[1], 10), - parseInt(parsed[2], 10), - parseInt(parsed[4], 10), - parseInt(parsed[5], 10), - parseInt(parsed[6], 10), - ]; -}; - /** * In some cases, we need to exclude the `ref` property from the original object (e.g. input controllers) to prevent from showing up a warning message from React. * This function helps to get the object without the `ref` property.