diff --git a/.changeset/slimy-bags-beg.md b/.changeset/slimy-bags-beg.md new file mode 100644 index 0000000..c447665 --- /dev/null +++ b/.changeset/slimy-bags-beg.md @@ -0,0 +1,5 @@ +--- +'@layerstack/utils': patch +--- + +feat(format): Support passing config option to easily set `type` and additional `options` as single object (ex. `format(number, { type: 'currency', options: { notation: 'compact' } })`) diff --git a/.changeset/two-doodles-return.md b/.changeset/two-doodles-return.md new file mode 100644 index 0000000..6e159f0 --- /dev/null +++ b/.changeset/two-doodles-return.md @@ -0,0 +1,5 @@ +--- +'@layerstack/utils': patch +--- + +feat(format): Support passing PeriodTypeCode strings to easily format dates (ex. `format(date, 'day')`) diff --git a/packages/utils/src/lib/array.test.ts b/packages/utils/src/lib/array.test.ts index da498ba..0a42369 100644 --- a/packages/utils/src/lib/array.test.ts +++ b/packages/utils/src/lib/array.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { greatestAbs, sumObjects } from './array.js'; -import { testDate } from './date.test.js'; +import { testDateStr } from './date.test.js'; describe('sumObjects', () => { it('Sum array of objects ', () => { @@ -11,7 +11,7 @@ describe('sumObjects', () => { { one: null, two: null, three: null, four: null }, { one: NaN, two: NaN, three: NaN, four: NaN }, { one: 'NaN', two: 'NaN', three: 'NaN', four: 'NaN' }, - { one: '3', two: 6, four: '4', startDate: new Date(testDate) }, + { one: '3', two: 6, four: '4', startDate: new Date(testDateStr) }, ]; const actual = sumObjects(values); @@ -21,7 +21,7 @@ describe('sumObjects', () => { three: 9, four: 4, extra: 0, - startDate: +new Date(testDate), + startDate: +new Date(testDateStr), }; expect(actual).toEqual(expected); diff --git a/packages/utils/src/lib/date.test.ts b/packages/utils/src/lib/date.test.ts index c2f6620..2c5cfd7 100644 --- a/packages/utils/src/lib/date.test.ts +++ b/packages/utils/src/lib/date.test.ts @@ -13,18 +13,20 @@ import { replaceDayOfWeek, isStringDate, } from './date.js'; -import { formatWithLocale } from './format.js'; -import { createLocaleSettings, defaultLocale } from './locale.js'; +import { createLocaleSettings, defaultLocale, LocaleSettings } from './locale.js'; import { PeriodType, type FormatDateOptions, DayOfWeek, type CustomIntlDateTimeFormatOptions, DateToken, + PeriodTypeCode, } from './date_types.js'; import { getWeekStartsOnFromIntl } from './dateInternal.js'; +import { parseISO } from 'date-fns'; -export const testDate = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) +export const testDateStr = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) +export const testDate = parseISO('2023-11-21'); // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) const dt_2M_2d = new Date(2023, 10, 21); const dt_2M_1d = new Date(2023, 10, 7); const dt_1M_1d = new Date(2023, 2, 7); @@ -56,274 +58,707 @@ describe('formatDate()', () => { // @ts-expect-error expect(formatDate('invalid date')).equal(''); }); +}); - describe('should format date for PeriodType.Day', () => { - const localDate = new Date(2023, 10, 21); - const cases = [ - ['short', defaultLocale, '11/21'], - ['short', fr, '21/11'], - ['default', defaultLocale, '11/21/2023'], - ['default', fr, '21/11/2023'], - ['long', defaultLocale, 'Nov 21, 2023'], - ['long', fr, '21 nov. 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, localDate, PeriodType.Day, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date string for PeriodType.Day', () => { - const cases = [ - ['short', defaultLocale, '11/21'], - ['short', fr, '21/11'], - ['default', defaultLocale, '11/21/2023'], - ['default', fr, '21/11/2023'], - ['long', defaultLocale, 'Nov 21, 2023'], - ['long', fr, '21 nov. 2023'], - ] as const; - - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Day, { variant })).equal( - expected - ); - }); - } - }); - - describe('should format date string for DayTime, TimeOnly', () => { - const cases: [Date, PeriodType, FormatDateOptions, string[]][] = [ +describe('formatDateWithLocale()', () => { + describe('PeriodType', () => { + const cases: [PeriodType, FormatDateOptions, Date, [LocaleSettings, string][]][] = [ + // PeriodType.Day [ - dt_1M_1d_time_pm, - PeriodType.DayTime, + PeriodType.Day, { variant: 'short' }, - ['3/7/2023, 2:02 PM', '07/03/2023 14:02'], + testDate, + [ + [defaultLocale, '11/21'], + [fr, '21/11'], + ], ], [ + PeriodType.Day, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/21/2023'], + [fr, '21/11/2023'], + ], + ], + [ + PeriodType.Day, + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'Nov 21, 2023'], + [fr, '21 nov. 2023'], + ], + ], + // PeriodType.DayTime + [ + PeriodType.DayTime, + { variant: 'short' }, dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 2:02 PM'], + [fr, '07/03/2023 14:02'], + ], + ], + [ PeriodType.DayTime, { variant: 'default' }, - ['3/7/2023, 02:02 PM', '07/03/2023 14:02'], + dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 02:02 PM'], + [fr, '07/03/2023 14:02'], + ], ], [ - dt_1M_1d_time_pm, PeriodType.DayTime, { variant: 'long' }, - ['3/7/2023, 02:02:03 PM', '07/03/2023 14:02:03'], + dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 02:02:03 PM'], + [fr, '07/03/2023 14:02:03'], + ], ], - [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'short' }, ['2:02 PM', '14:02']], - [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'default' }, ['02:02:03 PM', '14:02:03']], + // PeriodType.TimeOnly [ + PeriodType.TimeOnly, + { variant: 'short' }, dt_1M_1d_time_pm, + [ + [defaultLocale, '2:02 PM'], + [fr, '14:02'], + ], + ], + [ + PeriodType.TimeOnly, + { variant: 'default' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '02:02:03 PM'], + [fr, '14:02:03'], + ], + ], + [ PeriodType.TimeOnly, { variant: 'long' }, - ['02:02:03.004 PM', '14:02:03,004'], + dt_1M_1d_time_pm, + [ + [defaultLocale, '02:02:03.004 PM'], + [fr, '14:02:03,004'], + ], + ], + // PeriodType.WeekSun / Mon + [ + PeriodType.WeekSun, + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/19 - 11/25'], + [fr, '19/11 - 25/11'], + ], + ], + [ + PeriodType.WeekSun, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '19/11/2023 - 25/11/2023'], + ], + ], + [ + PeriodType.WeekSun, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '19/11/2023 - 25/11/2023'], + ], + ], + [ + PeriodType.WeekMon, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/20/2023 - 11/26/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + // PeriodType.Week + [ + PeriodType.Week, + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/19 - 11/25'], + [fr, '20/11 - 26/11'], + ], + ], + [ + PeriodType.Week, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + [ + PeriodType.Week, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + // PeriodType.Month + [ + PeriodType.Month, + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Nov'], + [fr, 'nov.'], + ], + ], + [ + PeriodType.Month, + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'November'], + [fr, 'novembre'], + ], + ], + [ + PeriodType.Month, + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], + ], + // PeriodType.MonthYear + [ + PeriodType.MonthYear, + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Nov 23'], + [fr, 'nov. 23'], + ], + ], + [ + PeriodType.MonthYear, + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], + ], + [ + PeriodType.MonthYear, + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], ], - ]; - - for (const c of cases) { - const [date, periodType, options, [expected_default, expected_fr]] = c; - it(c.toString(), () => { - expect(formatWithLocale(defaultLocale, date, periodType, options)).equal(expected_default); - }); - - it(c.toString() + 'fr', () => { - expect(formatWithLocale(fr, date, periodType, options)).equal(expected_fr); - }); - } - }); - - describe('should format date for PeriodType.WeekSun / Mon no mather the locale', () => { - const cases = [ - [PeriodType.WeekSun, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.WeekSun, 'short', fr, '19/11 - 25/11'], - [PeriodType.WeekSun, 'default', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.WeekSun, 'default', fr, '19/11/2023 - 25/11/2023'], - [PeriodType.WeekSun, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.WeekSun, 'long', fr, '19/11/2023 - 25/11/2023'], - [PeriodType.WeekMon, 'long', defaultLocale, '11/20/2023 - 11/26/2023'], - [PeriodType.WeekMon, 'long', fr, '20/11/2023 - 26/11/2023'], - ] as const; - - for (const c of cases) { - const [periodType, variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, periodType, { variant })).equal(expected); - }); - } - }); - - describe('should format date for PeriodType.Week with the good weekstarton locale', () => { - const cases = [ - [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.Week, 'short', fr, '20/11 - 26/11'], - [PeriodType.Week, 'default', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'default', fr, '20/11/2023 - 26/11/2023'], - [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], - - [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], - [PeriodType.Week, 'short', fr, '20/11 - 26/11'], - [PeriodType.Week, 'default', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'default', fr, '20/11/2023 - 26/11/2023'], - [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], - ] as const; - for (const c of cases) { - const [periodType, variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, periodType, { variant })).equal(expected); - }); - } - }); + // PeriodType.Quarter + [ + PeriodType.Quarter, + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Oct - Dec 23'], + [fr, 'oct. - déc. 23'], + ], + ], + [ + PeriodType.Quarter, + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'October - December 2023'], + [fr, 'octobre - décembre 2023'], + ], + ], + [ + PeriodType.Quarter, + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'October 2023 - December 2023'], + [fr, 'octobre 2023 - décembre 2023'], + ], + ], - describe('should format date for PeriodType.Month', () => { - const cases = [ - ['short', defaultLocale, 'Nov'], - ['short', fr, 'nov.'], - ['default', defaultLocale, 'November'], - ['default', fr, 'novembre'], - ['long', defaultLocale, 'November 2023'], - ['long', fr, 'novembre 2023'], - ] as const; + // PeriodType.CalendarYear + [ + PeriodType.CalendarYear, + { variant: 'short' }, + testDate, + [ + [defaultLocale, '23'], + [fr, '23'], + ], + ], + [ + PeriodType.CalendarYear, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '2023'], + [fr, '2023'], + ], + ], + [ + PeriodType.CalendarYear, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '2023'], + [fr, '2023'], + ], + ], - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Month, { variant })).equal( - expected - ); - }); - } - }); + // PeriodType.FiscalYearOctober + [ + PeriodType.FiscalYearOctober, + { variant: 'short' }, + testDate, + [ + [defaultLocale, '24'], + [fr, '24'], + ], + ], + [ + PeriodType.FiscalYearOctober, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '2024'], + [fr, '2024'], + ], + ], + [ + PeriodType.FiscalYearOctober, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '2024'], + [fr, '2024'], + ], + ], - describe('should format date for PeriodType.MonthYear', () => { - const cases = [ - ['short', defaultLocale, 'Nov 23'], - ['short', fr, 'nov. 23'], - ['default', defaultLocale, 'November 2023'], - ['default', fr, 'novembre 2023'], - ['long', defaultLocale, 'November 2023'], - ['long', fr, 'novembre 2023'], + // PeriodType.BiWeek1Sun + [ + PeriodType.BiWeek1Sun, + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/12 - 11/25'], + [fr, '12/11 - 25/11'], + ], + ], + [ + PeriodType.BiWeek1Sun, + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/12/2023 - 11/25/2023'], + [fr, '12/11/2023 - 25/11/2023'], + ], + ], + [ + PeriodType.BiWeek1Sun, + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/12/2023 - 11/25/2023'], + [fr, '12/11/2023 - 25/11/2023'], + ], + ], ] as const; for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.MonthYear, { variant })).equal( - expected - ); + const [periodType, options, date, locales] = c; + describe(PeriodType[periodType], () => { + describe(`options: ${JSON.stringify(options)}`, () => { + for (const [locale, expected] of locales) { + it(`locale: ${locale.locale}`, () => { + expect(formatDateWithLocale(locale, date, periodType, options)).equal(expected); + }); + } + }); }); } }); - describe('should format date for PeriodType.Quarter', () => { - const cases = [ - ['short', defaultLocale, 'Oct - Dec 23'], - ['short', fr, 'oct. - déc. 23'], - ['default', defaultLocale, 'October - December 2023'], - ['default', fr, 'octobre - décembre 2023'], - ['long', defaultLocale, 'October 2023 - December 2023'], - ['long', fr, 'octobre 2023 - décembre 2023'], - ] as const; + describe('PeriodTypeCode string', () => { + const cases: [PeriodTypeCode, FormatDateOptions, Date, [LocaleSettings, string][]][] = [ + // 'day' + [ + 'day', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/21'], + [fr, '21/11'], + ], + ], + [ + 'day', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/21/2023'], + [fr, '21/11/2023'], + ], + ], + [ + 'day', + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'Nov 21, 2023'], + [fr, '21 nov. 2023'], + ], + ], + // 'daytime' + [ + 'daytime', + { variant: 'short' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 2:02 PM'], + [fr, '07/03/2023 14:02'], + ], + ], + [ + 'daytime', + { variant: 'default' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 02:02 PM'], + [fr, '07/03/2023 14:02'], + ], + ], + [ + 'daytime', + { variant: 'long' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '3/7/2023, 02:02:03 PM'], + [fr, '07/03/2023 14:02:03'], + ], + ], + // 'time' + [ + 'time', + { variant: 'short' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '2:02 PM'], + [fr, '14:02'], + ], + ], + [ + 'time', + { variant: 'default' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '02:02:03 PM'], + [fr, '14:02:03'], + ], + ], + [ + 'time', + { variant: 'long' }, + dt_1M_1d_time_pm, + [ + [defaultLocale, '02:02:03.004 PM'], + [fr, '14:02:03,004'], + ], + ], + // 'week-sun' + [ + 'week-sun', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/19 - 11/25'], + [fr, '19/11 - 25/11'], + ], + ], + [ + 'week-sun', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '19/11/2023 - 25/11/2023'], + ], + ], + [ + 'week-sun', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '19/11/2023 - 25/11/2023'], + ], + ], + // 'week-mon' + [ + 'week-mon', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/20/2023 - 11/26/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + // 'week' + [ + 'week', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/19 - 11/25'], + [fr, '20/11 - 26/11'], + ], + ], + [ + 'week', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + [ + 'week', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/19/2023 - 11/25/2023'], + [fr, '20/11/2023 - 26/11/2023'], + ], + ], + // 'month' + [ + 'month', + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Nov'], + [fr, 'nov.'], + ], + ], + [ + 'month', + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'November'], + [fr, 'novembre'], + ], + ], + [ + 'month', + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], + ], + // 'month-year' + [ + 'month-year', + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Nov 23'], + [fr, 'nov. 23'], + ], + ], + [ + 'month-year', + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], + ], + [ + 'month-year', + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'November 2023'], + [fr, 'novembre 2023'], + ], + ], - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.Quarter, { variant })).equal( - expected - ); - }); - } - }); + // 'quarter' + [ + 'quarter', + { variant: 'short' }, + testDate, + [ + [defaultLocale, 'Oct - Dec 23'], + [fr, 'oct. - déc. 23'], + ], + ], + [ + 'quarter', + { variant: 'default' }, + testDate, + [ + [defaultLocale, 'October - December 2023'], + [fr, 'octobre - décembre 2023'], + ], + ], + [ + 'quarter', + { variant: 'long' }, + testDate, + [ + [defaultLocale, 'October 2023 - December 2023'], + [fr, 'octobre 2023 - décembre 2023'], + ], + ], - describe('should format date for PeriodType.CalendarYear', () => { - const cases = [ - ['short', defaultLocale, '23'], - ['short', fr, '23'], - ['default', defaultLocale, '2023'], - ['default', fr, '2023'], - ['long', defaultLocale, '2023'], - ['long', fr, '2023'], - ] as const; + // 'year' + [ + 'year', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '23'], + [fr, '23'], + ], + ], + [ + 'year', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '2023'], + [fr, '2023'], + ], + ], + [ + 'year', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '2023'], + [fr, '2023'], + ], + ], - for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.CalendarYear, { variant })).equal( - expected - ); - }); - } - }); + // 'fiscal-year-october' + [ + 'fiscal-year-october', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '24'], + [fr, '24'], + ], + ], + [ + 'fiscal-year-october', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '2024'], + [fr, '2024'], + ], + ], + [ + 'fiscal-year-october', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '2024'], + [fr, '2024'], + ], + ], - describe('should format date for PeriodType.FiscalYearOctober', () => { - const cases = [ - ['short', defaultLocale, '24'], - ['short', fr, '24'], - ['default', defaultLocale, '2024'], - ['default', fr, '2024'], - ['long', defaultLocale, '2024'], - ['long', fr, '2024'], - ] as const; + // 'biweek1-sun' + [ + 'biweek1-sun', + { variant: 'short' }, + testDate, + [ + [defaultLocale, '11/12 - 11/25'], + [fr, '12/11 - 25/11'], + ], + ], + [ + 'biweek1-sun', + { variant: 'default' }, + testDate, + [ + [defaultLocale, '11/12/2023 - 11/25/2023'], + [fr, '12/11/2023 - 25/11/2023'], + ], + ], + [ + 'biweek1-sun', + { variant: 'long' }, + testDate, + [ + [defaultLocale, '11/12/2023 - 11/25/2023'], + [fr, '12/11/2023 - 25/11/2023'], + ], + ], + ]; for (const c of cases) { - const [variant, locales, expected] = c; - it(c.toString(), () => { - expect( - formatDateWithLocale(locales, testDate, PeriodType.FiscalYearOctober, { variant }) - ).equal(expected); + const [periodTypeCode, options, date, locales] = c; + describe(periodTypeCode, () => { + describe(`options: ${JSON.stringify(options)}`, () => { + for (const [locale, expected] of locales) { + it(`locale: ${locale.locale}`, () => { + expect(formatDateWithLocale(locale, date, periodTypeCode, options)).equal(expected); + }); + } + }); }); } }); - describe('should format date for PeriodType.BiWeek1Sun', () => { + describe('should format date string for PeriodType.Day', () => { const cases = [ - ['short', defaultLocale, '11/12 - 11/25'], - ['short', fr, '12/11 - 25/11'], - ['default', defaultLocale, '11/12/2023 - 11/25/2023'], - ['default', fr, '12/11/2023 - 25/11/2023'], - ['long', defaultLocale, '11/12/2023 - 11/25/2023'], - ['long', fr, '12/11/2023 - 25/11/2023'], + ['short', defaultLocale, '11/21'], + ['short', fr, '21/11'], + ['default', defaultLocale, '11/21/2023'], + ['default', fr, '21/11/2023'], + ['long', defaultLocale, 'Nov 21, 2023'], + ['long', fr, '21 nov. 2023'], ] as const; for (const c of cases) { - const [variant, locales, expected] = c; + const [variant, locale, expected] = c; it(c.toString(), () => { - expect(formatDateWithLocale(locales, testDate, PeriodType.BiWeek1Sun, { variant })).equal( + expect(formatDateWithLocale(locale, testDateStr, PeriodType.Day, { variant })).equal( expected ); }); } }); - - describe('should format date for PeriodType.undefined', () => { - const expected = '2023-11-21T00:00:00-04:00'; - const cases = [ - ['short', defaultLocale], - ['short', fr], - ['default', defaultLocale], - ['default', fr], - ['long', defaultLocale], - ['long', fr], - ] as const; - - for (const c of cases) { - const [variant, locales] = c; - it(c.toString(), () => { - // @ts-expect-error - expect(formatDateWithLocale(locales, testDate, undefined, { variant })).equal(expected); - }); - } - }); }); describe('formatIntl() tokens', () => { @@ -430,7 +865,7 @@ describe('localToUtcDate()', () => { describe('getMonthDaysByWeek()', () => { it('default starting Week: Sunday', () => { - const dates = getMonthDaysByWeek(new Date(testDate)); + const dates = getMonthDaysByWeek(testDate); expect(dates).toMatchInlineSnapshot(` [ [ @@ -483,7 +918,7 @@ describe('getMonthDaysByWeek()', () => { }); it('Starting Week: Monday', () => { - const dates = getMonthDaysByWeek(new Date(testDate), 1); + const dates = getMonthDaysByWeek(testDate, 1); expect(dates).toMatchInlineSnapshot(` [ [ diff --git a/packages/utils/src/lib/date.ts b/packages/utils/src/lib/date.ts index d42c68f..97bfbd2 100644 --- a/packages/utils/src/lib/date.ts +++ b/packages/utils/src/lib/date.ts @@ -31,7 +31,7 @@ import { } from 'date-fns'; import { hasKeyOf } from './typeGuards.js'; -import { assertNever, entries, ValueOf } from './typeHelpers.js'; +import { assertNever, entries } from './typeHelpers.js'; import { chunk } from './array.js'; import { PeriodType, @@ -41,6 +41,8 @@ import { type CustomIntlDateTimeFormatOptions, type FormatDateOptions, type DateFormatVariantPreset, + periodTypeMappings, + PeriodTypeCode, } from './date_types.js'; import { defaultLocale, type LocaleSettings } from './locale.js'; @@ -142,48 +144,6 @@ export function getPeriodTypeNameWithLocale(settings: LocaleSettings, periodType } } -const periodTypeMappings = { - [PeriodType.Custom]: 'custom', - [PeriodType.Day]: 'day', - [PeriodType.DayTime]: 'daytime', - [PeriodType.TimeOnly]: 'time', - - [PeriodType.WeekSun]: 'week-sun', - [PeriodType.WeekMon]: 'week-mon', - [PeriodType.WeekTue]: 'week-tue', - [PeriodType.WeekWed]: 'week-wed', - [PeriodType.WeekThu]: 'week-thu', - [PeriodType.WeekFri]: 'week-fri', - [PeriodType.WeekSat]: 'week-sat', - [PeriodType.Week]: 'week', - - [PeriodType.Month]: 'month', - [PeriodType.MonthYear]: 'month-year', - [PeriodType.Quarter]: 'quarter', - [PeriodType.CalendarYear]: 'year', - [PeriodType.FiscalYearOctober]: 'fiscal-year-october', - - [PeriodType.BiWeek1Sun]: 'biweek1-sun', - [PeriodType.BiWeek1Mon]: 'biweek1-mon', - [PeriodType.BiWeek1Tue]: 'biweek1-tue', - [PeriodType.BiWeek1Wed]: 'biweek1-wed', - [PeriodType.BiWeek1Thu]: 'biweek1-thu', - [PeriodType.BiWeek1Fri]: 'biweek1-fri', - [PeriodType.BiWeek1Sat]: 'biweek1-sat', - [PeriodType.BiWeek1]: 'biweek1', - - [PeriodType.BiWeek2Sun]: 'biweek2-sun', - [PeriodType.BiWeek2Mon]: 'biweek2-mon', - [PeriodType.BiWeek2Tue]: 'biweek2-tue', - [PeriodType.BiWeek2Wed]: 'biweek2-wed', - [PeriodType.BiWeek2Thu]: 'biweek2-thu', - [PeriodType.BiWeek2Fri]: 'biweek2-fri', - [PeriodType.BiWeek2Sat]: 'biweek2-sat', - [PeriodType.BiWeek2]: 'biweek2', -} as const; - -export type PeriodTypeCode = ValueOf; - export function getPeriodTypeCode(periodType: PeriodType): PeriodTypeCode { return periodTypeMappings[periodType]; } @@ -700,7 +660,7 @@ export function updatePeriodTypeWithWeekStartsOn( export function formatDateWithLocale( settings: LocaleSettings, date: Date | string | null | undefined, - periodType: PeriodType, + periodType: PeriodType | PeriodTypeCode, options: FormatDateOptions = {} ): string { if (typeof date === 'string') { @@ -717,6 +677,11 @@ export function formatDateWithLocale( const { day, dayTime, timeOnly, week, month, monthsYear, year } = settings.formats.dates.presets; + periodType = + typeof periodType === 'string' + ? getPeriodTypeByCode(periodType) + : ((periodType ?? PeriodType.Day) as PeriodType); + periodType = updatePeriodTypeWithWeekStartsOn(weekStartsOn, periodType) ?? periodType; /** Resolve a preset given the chosen variant */ diff --git a/packages/utils/src/lib/date_types.ts b/packages/utils/src/lib/date_types.ts index f10cfce..6dd1a15 100644 --- a/packages/utils/src/lib/date_types.ts +++ b/packages/utils/src/lib/date_types.ts @@ -1,4 +1,5 @@ import type { DateRange } from './dateRange.js'; +import { ValueOf } from './typeHelpers.js'; export type SelectedDate = Date | Date[] | DateRange | null | undefined; @@ -51,6 +52,48 @@ export enum PeriodType { BiWeek2Sat = 87, } +export const periodTypeMappings = { + [PeriodType.Custom]: 'custom', + [PeriodType.Day]: 'day', + [PeriodType.DayTime]: 'daytime', + [PeriodType.TimeOnly]: 'time', + + [PeriodType.WeekSun]: 'week-sun', + [PeriodType.WeekMon]: 'week-mon', + [PeriodType.WeekTue]: 'week-tue', + [PeriodType.WeekWed]: 'week-wed', + [PeriodType.WeekThu]: 'week-thu', + [PeriodType.WeekFri]: 'week-fri', + [PeriodType.WeekSat]: 'week-sat', + [PeriodType.Week]: 'week', + + [PeriodType.Month]: 'month', + [PeriodType.MonthYear]: 'month-year', + [PeriodType.Quarter]: 'quarter', + [PeriodType.CalendarYear]: 'year', + [PeriodType.FiscalYearOctober]: 'fiscal-year-october', + + [PeriodType.BiWeek1Sun]: 'biweek1-sun', + [PeriodType.BiWeek1Mon]: 'biweek1-mon', + [PeriodType.BiWeek1Tue]: 'biweek1-tue', + [PeriodType.BiWeek1Wed]: 'biweek1-wed', + [PeriodType.BiWeek1Thu]: 'biweek1-thu', + [PeriodType.BiWeek1Fri]: 'biweek1-fri', + [PeriodType.BiWeek1Sat]: 'biweek1-sat', + [PeriodType.BiWeek1]: 'biweek1', + + [PeriodType.BiWeek2Sun]: 'biweek2-sun', + [PeriodType.BiWeek2Mon]: 'biweek2-mon', + [PeriodType.BiWeek2Tue]: 'biweek2-tue', + [PeriodType.BiWeek2Wed]: 'biweek2-wed', + [PeriodType.BiWeek2Thu]: 'biweek2-thu', + [PeriodType.BiWeek2Fri]: 'biweek2-fri', + [PeriodType.BiWeek2Sat]: 'biweek2-sat', + [PeriodType.BiWeek2]: 'biweek2', +} as const; + +export type PeriodTypeCode = ValueOf; + export enum DayOfWeek { Sunday = 0, Monday = 1, diff --git a/packages/utils/src/lib/duration.test.ts b/packages/utils/src/lib/duration.test.ts index e2f8d77..529218d 100644 --- a/packages/utils/src/lib/duration.test.ts +++ b/packages/utils/src/lib/duration.test.ts @@ -1,8 +1,6 @@ import { describe, it, expect } from 'vitest'; import { Duration, DurationUnits } from './duration.js'; -import { PeriodType } from './date_types.js'; -import { testDate } from './date.test.js'; import { subDays } from 'date-fns'; describe('Duration', () => { diff --git a/packages/utils/src/lib/format.test.ts b/packages/utils/src/lib/format.test.ts index 7017ac2..a7f6ce0 100644 --- a/packages/utils/src/lib/format.test.ts +++ b/packages/utils/src/lib/format.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { format } from './format.js'; import { PeriodType } from './date_types.js'; -import { testDate } from './date.test.js'; +import { testDateStr } from './date.test.js'; import { parseISO } from 'date-fns'; describe('format()', () => { @@ -16,47 +16,106 @@ describe('format()', () => { expect(actual).equal(''); }); - it('returns value as string for style "none"', () => { - const actual = format(1234.5678, 'none'); - expect(actual).equal('1234.5678'); - }); + describe('formats number', () => { + // See `number.test.ts` for more number tests + it('returns original value as string for style "none"', () => { + const actual = format(1234.5678, 'none'); + expect(actual).equal('1234.5678'); + }); - // See `number.test.ts` for more number tests - it('formats number with number format (integer)', () => { - const actual = format(1234.5678, 'integer'); - expect(actual).equal('1,235'); - }); + it('formats with "integer" type', () => { + const actual = format(1234.5678, 'integer'); + expect(actual).equal('1,235'); + }); - // See `date.test.ts` for more date tests - it('formats date with PeriodType format (date)', () => { - const actual = format(testDate, PeriodType.Day); - expect(actual).equal('11/21/2023'); - }); + it('formats with "integer" config with default options', () => { + const actual = format(1234.5678, { type: 'integer' }); + expect(actual).equal('1,235'); + }); - it('formats number with custom function', () => { - const actual = format(1234.5678, (value) => Math.round(value).toString()); - expect(actual).equal('1235'); - }); + it('formats with "integer" config with extra options', () => { + const actual = format(1234.5678, { + type: 'integer', + options: { maximumSignificantDigits: 2 }, + }); + expect(actual).equal('1,200'); + }); - // Default format based on value type - it('format based on value type (integer)', () => { - const actual = format(1234); - expect(actual).equal('1,234'); - }); - it('format based on value type (decimal)', () => { - const actual = format(1234.5678); - expect(actual).equal('1,234.57'); - }); - it('format based on value type (date string)', () => { - const actual = format(testDate); - expect(actual).equal('11/21/2023'); + it('formats with "decimal" config with default options', () => { + const actual = format(1234.5678, { type: 'decimal' }); + expect(actual).equal('1,234.57'); + }); + + it('formats with "decimal" config with extra options', () => { + const actual = format(1234.5678, { type: 'decimal', options: { fractionDigits: 3 } }); + expect(actual).equal('1,234.568'); + }); + + it('formats with "currency" config with default options', () => { + const actual = format(1234.5678, { type: 'currency' }); + expect(actual).equal('$1,234.57'); + }); + + it('formats with "currency" config with extra options (currency)', () => { + const actual = format(1234.5678, { type: 'currency', options: { currency: 'EUR' } }); + expect(actual).equal('€1,234.57'); + }); + + it('formats with "currency" config with extra options (compact notation)', () => { + const actual = format(1234.5678, { + type: 'currency', + options: { notation: 'compact' }, + }); + expect(actual).equal('$1.23K'); + }); + + it('formats with "currency" config with extra options (compact notation with short display)', () => { + const actual = format(1234.5678, { + type: 'currency', + options: { notation: 'compact', maximumSignificantDigits: 2 }, + }); + expect(actual).equal('$1.2K'); + }); + + it('formats with custom function', () => { + const actual = format(1234.5678, (value) => Math.round(value).toString()); + expect(actual).equal('1235'); + }); }); - it('format based on value type (date)', () => { - const actual = format(parseISO(testDate)); - expect(actual).equal('11/21/2023'); + + describe('formats date', () => { + // See `date.test.ts` for more date tests + it('formats date with PeriodType (date)', () => { + const actual = format(testDateStr, PeriodType.Day); + expect(actual).equal('11/21/2023'); + }); + + it('formats date with period type code (date)', () => { + const actual = format(testDateStr, 'day'); + expect(actual).equal('11/21/2023'); + }); }); - it('format based on value type (string)', () => { - const actual = format('hello'); - expect(actual).equal('hello'); + + describe('format based on value type', () => { + it('format based on value type (integer)', () => { + const actual = format(1234); + expect(actual).equal('1,234'); + }); + it('format based on value type (decimal)', () => { + const actual = format(1234.5678); + expect(actual).equal('1,234.57'); + }); + it('format based on value type (date string)', () => { + const actual = format(testDateStr); + expect(actual).equal('11/21/2023'); + }); + it('format based on value type (date)', () => { + const actual = format(parseISO(testDateStr)); + expect(actual).equal('11/21/2023'); + }); + it('format based on value type (string)', () => { + const actual = format('hello'); + expect(actual).equal('hello'); + }); }); }); diff --git a/packages/utils/src/lib/format.ts b/packages/utils/src/lib/format.ts index e3869cd..f154d33 100644 --- a/packages/utils/src/lib/format.ts +++ b/packages/utils/src/lib/format.ts @@ -3,60 +3,144 @@ import { getPeriodTypeNameWithLocale, getDayOfWeekName, isStringDate, + periodTypeMappings, } from './date.js'; import { formatNumberWithLocale } from './number.js'; import type { FormatNumberOptions, FormatNumberStyle } from './number.js'; import { defaultLocale, type LocaleSettings } from './locale.js'; -import { PeriodType, type FormatDateOptions, DayOfWeek } from './date_types.js'; +import { + PeriodType, + type FormatDateOptions, + DayOfWeek, + type PeriodTypeCode, +} from './date_types.js'; -export type FormatType = FormatNumberStyle | PeriodType | CustomFormatter; export type CustomFormatter = (value: any) => string; +export type FormatType = FormatNumberStyle | PeriodType | PeriodTypeCode | CustomFormatter; +export type FormatConfig = { + type: FormatType; + options?: FormatType extends FormatNumberStyle + ? FormatNumberOptions + : FormatType extends PeriodType | PeriodTypeCode + ? FormatDateOptions + : never; +}; + // re-export for convenience -export type { FormatNumberStyle, PeriodType }; +export type { FormatNumberStyle, PeriodType, PeriodTypeCode }; /** * Generic format which can handle Dates, Numbers, or custom format function */ export function format(value: null | undefined, format?: FormatType): string; +export function format(value: null | undefined, config: { type: FormatType }): string; export function format( value: number, format?: FormatNumberStyle | CustomFormatter, options?: FormatNumberOptions ): string; +export function format( + value: number, + config: { type: FormatNumberStyle | CustomFormatter; options?: FormatNumberOptions } +): string; export function format( value: string | Date, - format?: PeriodType | CustomFormatter, + format?: PeriodType | PeriodTypeCode | CustomFormatter, options?: FormatDateOptions ): string; +export function format( + value: string | Date, + config: { type: PeriodType | PeriodTypeCode | CustomFormatter; options?: FormatDateOptions } +): string; export function format( value: any, - format?: FormatType, + formatOrConfig?: + | FormatType + | { type: FormatType; options?: FormatNumberOptions | FormatDateOptions }, options?: FormatNumberOptions | FormatDateOptions ): any { - return formatWithLocale(defaultLocale, value, format, options); + if (formatOrConfig && typeof formatOrConfig === 'object' && 'type' in formatOrConfig) { + return formatWithLocale(defaultLocale, value, formatOrConfig.type, formatOrConfig.options); + } + return formatWithLocale(defaultLocale, value, formatOrConfig as FormatType, options); } +// null | undefined export function formatWithLocale( settings: LocaleSettings, - value: any, + value: null | undefined, format?: FormatType, options?: FormatNumberOptions | FormatDateOptions +): string; +export function formatWithLocale( + settings: LocaleSettings, + value: null | undefined, + config: FormatConfig +): string; + +// number +export function formatWithLocale( + settings: LocaleSettings, + value: number, + format?: FormatNumberStyle | CustomFormatter, + options?: FormatNumberOptions +): string; +export function formatWithLocale( + settings: LocaleSettings, + value: number, + config: FormatConfig +): string; + +// Date +export function formatWithLocale( + settings: LocaleSettings, + value: string | Date, + format?: PeriodType | PeriodTypeCode | CustomFormatter, + options?: FormatDateOptions +): string; +export function formatWithLocale( + settings: LocaleSettings, + value: string | Date, + config: FormatConfig +): string; + +export function formatWithLocale( + settings: LocaleSettings, + value: any, + formatOrConfig?: FormatType | FormatConfig, + options?: FormatNumberOptions | FormatDateOptions ) { + const format = + formatOrConfig && typeof formatOrConfig === 'object' && 'type' in formatOrConfig + ? formatOrConfig.type + : (formatOrConfig as FormatType); + + const formatOptions = + formatOrConfig && typeof formatOrConfig === 'object' && 'type' in formatOrConfig + ? formatOrConfig.options + : options; + if (typeof format === 'function') { return format(value); - } else if (value instanceof Date || isStringDate(value) || (format && format in PeriodType)) { + } else if ( + value instanceof Date || + isStringDate(value) || + (format && + (format in PeriodType || + Object.values(periodTypeMappings).includes(format as PeriodTypeCode))) + ) { return formatDateWithLocale( settings, value, - (format ?? PeriodType.Day) as PeriodType, - options as FormatDateOptions + format as PeriodType | PeriodTypeCode, + formatOptions as FormatDateOptions ); } else if (typeof value === 'number') { return formatNumberWithLocale( settings, value, format as FormatNumberStyle, - options as FormatNumberOptions + formatOptions as FormatNumberOptions ); } else if (typeof value === 'string') { // Keep original value if already string @@ -76,7 +160,7 @@ export type FormatFunction = (( ) => string) & (( value: Date | string | null | undefined, - period: PeriodType, + period: PeriodType | PeriodTypeCode, options?: FormatDateOptions ) => string); @@ -91,7 +175,7 @@ export type FormatFunctions = FormatFunction & FormatFunctionProperties; export function buildFormatters(settings: LocaleSettings): FormatFunctions { const mainFormat = ( value: any, - style: FormatNumberStyle | PeriodType, + style: FormatNumberStyle | PeriodType | PeriodTypeCode, options?: FormatNumberOptions | FormatDateOptions ) => formatWithLocale(settings, value, style, options); diff --git a/sites/docs/src/routes/docs/utils/format/+page.svelte b/sites/docs/src/routes/docs/utils/format/+page.svelte index 7ee5656..15a8fcd 100644 --- a/sites/docs/src/routes/docs/utils/format/+page.svelte +++ b/sites/docs/src/routes/docs/utils/format/+page.svelte @@ -91,7 +91,7 @@

Numbers

-

Number Formats (default settings)

+

format (default settings)

{format(1234.56, 'integer')}
@@ -104,7 +104,7 @@
{format(0.5678, 'percent')}
-

number formats (additional options)

+

format (additional options)

You can customize numbers with the 3rd arg that is an enhanced `Intl.NumberFormatOptions` type. @@ -123,6 +123,26 @@
{format(0.5678, 'percent', { fractionDigits: 1 })}
+

config

+ + You can customize numbers with a config option. + + +
{format(1234.56, { type: 'integer', options: { maximumSignificantDigits: 2 } })}
+
{format(1234.56, { type: 'decimal', options: { maximumSignificantDigits: 5 } })}
+
{format(1234.56, { type: 'currency', options: { currency: 'EUR' } })}
+
+ {format(123_456_789.99, { + type: 'currency', + options: { notation: 'compact', maximumSignificantDigits: 3 }, + })} +
+
{format(0.5678, { type: 'percent', options: { signDisplay: 'always' } })}
+
{format(0.5678, { type: 'percentRound', options: { signDisplay: 'always' } })}
+
{format(1_234_567, { type: 'metric', options: { minimumSignificantDigits: 12 } })}
+
{format(0.5678, { type: 'percent', options: { fractionDigits: 1 } })}
+
+

Dates

Custom format