import { DateTime } from 'luxon' import { getUserLocale, getTimezone } from 'utils/user-locale' export type DefaultOpts = { now?: DateTime locale?: string language?: string timezone?: string } type DateLike = string | number | Date export const getNow = (): DateTime => DateTime.now() export const getTz = (): string => getTimezone() const parseDateLike = ( dateLike: DateLike, { locale = getUserLocale(), timezone = getTz() }: DefaultOpts = {} ): DateTime => { if (dateLike instanceof Date) { const dateTime = DateTime.fromJSDate(dateLike, { zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateLike}`) } return dateTime } if (typeof dateLike === 'number') { const dateTime = DateTime.fromMillis(dateLike, { zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateLike}`) } return dateTime } const dateTime = DateTime.fromISO(dateLike, { locale, zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateLike}`) } return dateTime } export const getDateTimeFromISO = ( dateLike: DateLike, options: DefaultOpts = {} ): DateTime => parseDateLike(dateLike, options) export const getDateTimeStringFromISODateOnly = ( dateLike: DateLike, { locale = getUserLocale() } = {} ): string => parseDateLike(dateLike, { locale, timezone: 'UTC' }).startOf('day').toISO({ suppressMilliseconds: true, includeOffset: false, }) export const fromJSDateToJSDateToString = ( date: Date, { timezone = getTz() }: DefaultOpts = {} ): string => { const dateTime = DateTime.fromJSDate(date, { zone: timezone, }).set({ millisecond: 0, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${date}`) } return dateTime.toISO({ suppressMilliseconds: true, }) } // Comparison helpers export const checkNonISOFormats = ( date: string, { locale = getUserLocale(), timezone = getTz() }: DefaultOpts = {} ): string => { // We allow makers to manually type dates into Builder when configuring filters // Therefore, we need to support a few extra non-ISO formats which don't parse // when the app is running under Hermes // Order is important here const extraFormats = ['MM-dd-yyyy', 'MM/dd/yyyy', 'M/d/yyyy', 'M/d/yy'] for (const format of extraFormats) { try { const attempt = DateTime.fromFormat(date, format, { locale, zone: timezone, }) if (attempt.isValid) { return attempt.toISO() } } catch { /* no-op */ } } throw new Error(`Invalid date string: ${date}`) } export const getMostSuitableDateFromInput = ( date: string, { locale = getUserLocale(), timezone = getTz() }: DefaultOpts = {} ): string => { try { const attempt1 = DateTime.fromJSDate(new Date(date), { zone: timezone, }) if (attempt1.isValid) { return attempt1.toISO() } } catch { /* no-op */ } // NOTE: hard to reach as a test-case // This is due to differences between Browser/NodeJS and Hermes try { const attempt2 = DateTime.fromISO(date, { locale, zone: timezone, }) if (attempt2.isValid) { return attempt2.toISO() } } catch { /* no-op */ } // NOTE: hard to reach as a test-case, within this function // This is due to differences between Browser/NodeJS and Hermes return checkNonISOFormats(date, { locale, timezone }) } // Date-picker helpers // used by Web Date Picker export const fromDateOnlyStringToDateString = ( dateString: string, { locale = getUserLocale(), timezone = getTz() }: DefaultOpts = {} ): string => { if (typeof dateString !== 'string') { throw new Error(`Invalid date string: ${dateString}`) } const dateTime = DateTime.fromFormat(dateString, 'yyyy-MM-dd', { locale, zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateString}`) } return dateTime.toISO() } // used by Web and Native Date Picker export const fromJSDateToJSDateStartOfDay = ( date: Date, { timezone = getTz() }: DefaultOpts = {} ): Date => { const dateTime = DateTime.fromJSDate(date, { zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${date}`) } return dateTime.startOf('day').toJSDate() } // used by Web and Native Date Picker export const fromJSDateToStringStartOfDay = ( date: Date, { now = getNow(), locale = getUserLocale(), timezone = getTz(), }: DefaultOpts = {} ): string => { const dateTime = DateTime.fromJSDate( fromJSDateToJSDateStartOfDay(date, { now, locale, timezone, }), { zone: timezone, } ) as DateTime return dateTime.toISO() } // used by Web and Native Date Picker export const fromJSDateToJSDateStartOfMinute = ( date: Date, { timezone = getTz() }: DefaultOpts = {} ): Date => { const dateTime = DateTime.fromJSDate(date, { zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${date}`) } return dateTime.startOf('minute').toJSDate() } export const toJSDate = (date: string): Date => { if (isDateFormatYYYYMMDD(date)) { const dateElements = date.split('-') return new Date( parseInt(dateElements[0]), parseInt(dateElements[1]) - 1, parseInt(dateElements[2]) ) } return new Date(date) } // used by Web and Native Date Picker export const fromJSDateToStringStartOfMinute = ( date: Date, { now = getNow(), locale = getUserLocale(), timezone = getTz(), }: DefaultOpts = {} ): string => { const dateTime = DateTime.fromJSDate( fromJSDateToJSDateStartOfMinute(date, { now, locale, timezone, }), { zone: timezone, } ) as DateTime return dateTime.toISO() } // used by Web Date Picker export const getDateStringFromISO = ( dateString: string, { locale = getUserLocale(), timezone = getTz() } = {} ): string => { const dateTime = DateTime.fromISO(dateString, { locale, zone: timezone, }) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateString}`) } return dateTime.toISODate() } // used by Native Date Picker export const fromJSDateToLocaleString = ( date: Date, datepickerVariation: 'datetime' | 'date' | 'date-text' = 'datetime', { locale = getUserLocale(), timezone = getTz() }: DefaultOpts = {} ): string => { const dateTime = DateTime.fromJSDate(date, { zone: timezone, }).setLocale(locale) if (!dateTime.isValid) { throw new Error(`Invalid date string: ${date}`) } switch (datepickerVariation) { case 'datetime': return dateTime.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY) case 'date': return dateTime.toLocaleString(DateTime.DATE_HUGE) case 'date-text': return dateTime.toLocaleString(DateTime.DATE_SHORT) default: throw new Error(`Invalid datepicker variation: ${datepickerVariation}`) } } export const isDateFormatYYYYMMDD = (dateString: string): boolean => { try { const dateTime = DateTime.fromFormat(dateString, 'yyyy-MM-dd') if (!dateTime.isValid) { throw new Error(`Invalid date string: ${dateString}`) } return true } catch { return false } }