diff --git a/.changeset/light-ways-dress.md b/.changeset/light-ways-dress.md
new file mode 100644
index 000000000..1e024620a
--- /dev/null
+++ b/.changeset/light-ways-dress.md
@@ -0,0 +1,5 @@
+---
+'svelte-ux': minor
+---
+
+BREAKING: removed `dateDisplay()` in favor of `format()`
diff --git a/.changeset/ninety-carpets-cross.md b/.changeset/ninety-carpets-cross.md
new file mode 100644
index 000000000..04b6556c5
--- /dev/null
+++ b/.changeset/ninety-carpets-cross.md
@@ -0,0 +1,5 @@
+---
+'svelte-ux': patch
+---
+
+add locale management of date leveraging intl
diff --git a/.gitignore b/.gitignore
index d3f118eb4..5535d8867 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,6 @@ coverage/
.idea/
.svelte-kit/
.env
+.DS_Store
test-*
\ No newline at end of file
diff --git a/packages/svelte-ux/package.json b/packages/svelte-ux/package.json
index 71504b720..7681685e0 100644
--- a/packages/svelte-ux/package.json
+++ b/packages/svelte-ux/package.json
@@ -13,7 +13,7 @@
"prepublishOnly": "svelte-package",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
- "test:unit": "vitest",
+ "test:unit": "TZ=UTC+4 vitest --coverage",
"lint": "prettier --ignore-path ../../.gitignore --check .",
"format": "prettier --ignore-path ../../.gitignore --write .",
"typedoc": "typedoc",
@@ -29,6 +29,7 @@
"@types/lodash-es": "^4.17.11",
"@types/marked": "^6.0.0",
"@types/prismjs": "^1.26.3",
+ "@vitest/coverage-v8": "^0.34.6",
"autoprefixer": "^10.4.16",
"execa": "^8.0.1",
"marked": "^10.0.0",
@@ -56,8 +57,7 @@
"clsx": "^2.0.0",
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
- "d3-time": "^3.1.0",
- "date-fns": "^2.30.0",
+ "date-fns": "^3.0.5",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"posthog-js": "^1.95.1",
diff --git a/packages/svelte-ux/src/lib/components/DateButton.svelte b/packages/svelte-ux/src/lib/components/DateButton.svelte
index 7a8b35aa5..2ec7be7cc 100644
--- a/packages/svelte-ux/src/lib/components/DateButton.svelte
+++ b/packages/svelte-ux/src/lib/components/DateButton.svelte
@@ -1,12 +1,13 @@
{#if value?.from}
- {dateDisplay(value.from, { periodType, format, variant, utc })}
+ {format_ux(value.from, getPeriodType(value), { variant: 'long' })}
{:else}
{/if}
{#if value?.to && showToValue}
-
- {dateDisplay(value.to, { periodType, format, variant, utc })}
+ {format_ux(value.to, getPeriodType(value), { variant: 'long' })}
{/if}
diff --git a/packages/svelte-ux/src/lib/components/DateRangeField.svelte b/packages/svelte-ux/src/lib/components/DateRangeField.svelte
index 573c9c0bb..0c8306442 100644
--- a/packages/svelte-ux/src/lib/components/DateRangeField.svelte
+++ b/packages/svelte-ux/src/lib/components/DateRangeField.svelte
@@ -53,7 +53,6 @@
export let icon: string | null = null;
let open: boolean = false;
- let format: string = undefined;
let currentValue = value;
@@ -104,7 +103,7 @@
on:click={() => (open = true)}
{id}
>
-
+
diff --git a/packages/svelte-ux/src/lib/components/Month.svelte b/packages/svelte-ux/src/lib/components/Month.svelte
index 1348874f9..3175243ae 100644
--- a/packages/svelte-ux/src/lib/components/Month.svelte
+++ b/packages/svelte-ux/src/lib/components/Month.svelte
@@ -6,7 +6,6 @@
endOfDay as endOfDayFunc,
startOfMonth as startOfMonthFunc,
endOfMonth as endOfMonthFunc,
- format,
addMonths,
isSameDay,
isWithinInterval,
@@ -16,10 +15,12 @@
import { getMonthDaysByWeek, PeriodType } from '../utils/date';
import type { SelectedDate } from '../utils/date';
+ import { format } from '../utils';
import { hasKeyOf } from '../types/typeGuards';
import Button from './Button.svelte';
import DateButton from './DateButton.svelte';
+ import { getSettings } from '.';
export let selected: SelectedDate | undefined = undefined;
@@ -33,7 +34,7 @@
startOfMonthFunc(new Date());
$: endOfMonth = endOfMonthFunc(startOfMonth);
- $: monthDaysByWeek = getMonthDaysByWeek(startOfMonth);
+ $: monthDaysByWeek = getMonthDaysByWeek(startOfMonth, getSettings().formats?.dates?.weekStartsOn);
/**
* Hide controls and date. Useful to control externally
@@ -54,15 +55,15 @@
return disabledDays instanceof Function
? disabledDays(date)
: disabledDays instanceof Date
- ? isSameDay(date, disabledDays)
- : disabledDays instanceof Array
- ? disabledDays.some((d) => isSameDay(date, d))
- : disabledDays instanceof Object
- ? isWithinInterval(date, {
- start: startOfDayFunc(disabledDays.from),
- end: endOfDayFunc(disabledDays.to || disabledDays.from),
- })
- : false;
+ ? isSameDay(date, disabledDays)
+ : disabledDays instanceof Array
+ ? disabledDays.some((d) => isSameDay(date, d))
+ : disabledDays instanceof Object
+ ? isWithinInterval(date, {
+ start: startOfDayFunc(disabledDays.from),
+ end: endOfDayFunc(disabledDays.to || disabledDays.from),
+ })
+ : false;
};
$: isDayHidden = (day: Date) => {
@@ -91,7 +92,7 @@
/>
- {format(startOfMonth, 'MMMM yyyy')}
+ {format(startOfMonth, PeriodType.MonthYear)}
{#each monthDaysByWeek[0] ?? [] as day (day.getDate())}
- {format(day, 'eee')[0]}
+ {format(day, PeriodType.Day, { custom: 'eee' })}
{/each}
diff --git a/packages/svelte-ux/src/lib/components/index.ts b/packages/svelte-ux/src/lib/components/index.ts
index 2db586d8c..c1fbbdb81 100644
--- a/packages/svelte-ux/src/lib/components/index.ts
+++ b/packages/svelte-ux/src/lib/components/index.ts
@@ -92,5 +92,5 @@ export { default as TreeList } from './TreeList.svelte';
export { default as TweenedValue } from './TweenedValue.svelte';
export { default as ViewportCenter } from './ViewportCenter.svelte';
export { default as YearList } from './YearList.svelte';
-export { settings, getFormatNumberOptions, getSettings } from './settings';
+export { settings, getFormatNumber, getSettings, getDictionary } from './settings';
export { getTheme, getComponentTheme } from './theme';
diff --git a/packages/svelte-ux/src/lib/components/settings.ts b/packages/svelte-ux/src/lib/components/settings.ts
index fd943e754..c6d862db4 100644
--- a/packages/svelte-ux/src/lib/components/settings.ts
+++ b/packages/svelte-ux/src/lib/components/settings.ts
@@ -2,6 +2,15 @@ import type { FormatNumberOptions, FormatNumberStyle } from '$lib/utils/number';
import { getContext, setContext } from 'svelte';
import type { Theme } from './theme';
import type { Prettify } from '$lib/types/typeHelpers';
+import {
+ type FormatDateOptions,
+ DayOfWeek,
+ type DateFormatVariant,
+ type CustomIntlDateTimeFormatOptions,
+ type OrdinalSuffixes,
+ DateToken,
+} from '$lib/utils/date';
+import type { DictionaryMessages, DictionaryMessagesOptions } from '$lib/utils/dictionary';
type ExcludeNone = T extends 'none' ? never : T;
export type Settings = {
@@ -13,7 +22,9 @@ export type Settings = {
[key in ExcludeNone]?: FormatNumberOptions;
}
>;
+ dates?: FormatDateOptions;
};
+ dictionary?: DictionaryMessagesOptions;
theme?: Theme;
};
@@ -32,7 +43,7 @@ export function getSettings() {
}
}
-export function getFormatNumberOptions(style?: FormatNumberStyle) {
+export function getFormatNumber(style?: FormatNumberStyle) {
let toRet = {
locales: 'en',
currency: 'USD',
@@ -49,3 +60,226 @@ export function getFormatNumberOptions(style?: FormatNumberStyle) {
return toRet;
}
+
+export function getFormatDate(options?: FormatDateOptions) {
+ // if custom is set && variant is not set, let's put custom as variant
+ const variant: FormatDateOptions['variant'] =
+ options?.custom && options?.variant === undefined ? 'custom' : options?.variant ?? 'default';
+
+ const settings = getSettings();
+
+ const baseParsing = options?.baseParsing ?? settings.formats?.dates?.baseParsing ?? 'MM/dd/yyyy';
+
+ const custom = options?.custom ?? '';
+
+ let toRet: {
+ locales: string;
+ baseParsing: string;
+ weekStartsOn: DayOfWeek;
+ variant: DateFormatVariant;
+ custom: CustomIntlDateTimeFormatOptions;
+ presets: {
+ day: Record;
+ dayTime: Record;
+ timeOnly: Record;
+ week: Record;
+ month: Record;
+ monthYear: Record;
+ year: Record;
+ };
+ ordinalSuffixes: Record;
+ dictionaryDate: DictionaryMessages['Date'];
+ } = {
+ locales: options?.locales ?? settings.formats?.dates?.locales ?? 'en',
+ baseParsing,
+ weekStartsOn:
+ options?.weekStartsOn ?? settings.formats?.dates?.weekStartsOn ?? DayOfWeek.Sunday,
+ variant,
+ custom,
+
+ // keep always the en fallback
+ ordinalSuffixes: {
+ en: {
+ one: 'st',
+ two: 'nd',
+ few: 'rd',
+ other: 'th',
+ },
+ ...settings.formats?.dates?.ordinalSuffixes,
+ ...options?.ordinalSuffixes,
+ },
+
+ presets: {
+ day: {
+ short: options?.presets?.day?.short ??
+ settings.formats?.dates?.presets?.day?.short ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ ],
+ default: options?.presets?.day?.default ??
+ settings.formats?.dates?.presets?.day?.default ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ ],
+ long: options?.presets?.day?.long ??
+ settings.formats?.dates?.presets?.day?.long ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_short,
+ DateToken.Year_numeric,
+ ],
+ custom,
+ },
+ dayTime: {
+ short: options?.presets?.dayTime?.short ??
+ settings.formats?.dates?.presets?.dayTime?.short ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ DateToken.Hour_numeric,
+ DateToken.Minute_numeric,
+ ],
+ default: options?.presets?.dayTime?.default ??
+ settings.formats?.dates?.presets?.dayTime?.default ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ DateToken.Hour_2Digit,
+ DateToken.Minute_2Digit,
+ ],
+ long: options?.presets?.dayTime?.long ??
+ settings.formats?.dates?.presets?.dayTime?.long ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ DateToken.Hour_2Digit,
+ DateToken.Minute_2Digit,
+ DateToken.Second_2Digit,
+ ],
+ custom,
+ },
+
+ timeOnly: {
+ short: options?.presets?.timeOnly?.short ??
+ settings.formats?.dates?.presets?.timeOnly?.short ?? [
+ DateToken.Hour_numeric,
+ DateToken.Minute_numeric,
+ ],
+ default: options?.presets?.timeOnly?.default ??
+ settings.formats?.dates?.presets?.timeOnly?.default ?? [
+ DateToken.Hour_2Digit,
+ DateToken.Minute_2Digit,
+ DateToken.Second_2Digit,
+ ],
+ long: options?.presets?.timeOnly?.long ??
+ settings.formats?.dates?.presets?.timeOnly?.long ?? [
+ DateToken.Hour_2Digit,
+ DateToken.Minute_2Digit,
+ DateToken.Second_2Digit,
+ DateToken.MiliSecond_3,
+ ],
+ custom,
+ },
+
+ week: {
+ short: options?.presets?.week?.short ??
+ settings.formats?.dates?.presets?.week?.short ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ ],
+ default: options?.presets?.week?.default ??
+ settings.formats?.dates?.presets?.week?.default ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ ],
+ long: options?.presets?.week?.long ??
+ settings.formats?.dates?.presets?.week?.long ?? [
+ DateToken.DayOfMonth_numeric,
+ DateToken.Month_numeric,
+ DateToken.Year_numeric,
+ ],
+ custom,
+ },
+ month: {
+ short:
+ options?.presets?.month?.short ??
+ settings.formats?.dates?.presets?.month?.short ??
+ DateToken.Month_short,
+ default:
+ options?.presets?.month?.default ??
+ settings.formats?.dates?.presets?.month?.default ??
+ DateToken.Month_short,
+ long:
+ options?.presets?.month?.long ??
+ settings.formats?.dates?.presets?.month?.long ??
+ DateToken.Month_long,
+ custom,
+ },
+ monthYear: {
+ short: options?.presets?.monthsYear?.short ??
+ settings.formats?.dates?.presets?.monthsYear?.short ?? [
+ DateToken.Month_short,
+ DateToken.Year_2Digit,
+ ],
+ default: options?.presets?.monthsYear?.default ??
+ settings.formats?.dates?.presets?.monthsYear?.default ?? [
+ DateToken.Month_long,
+ DateToken.Year_numeric,
+ ],
+ long: options?.presets?.monthsYear?.long ??
+ settings.formats?.dates?.presets?.monthsYear?.long ?? [
+ DateToken.Month_long,
+ DateToken.Year_numeric,
+ ],
+ custom,
+ },
+ year: {
+ short:
+ options?.presets?.year?.short ??
+ settings.formats?.dates?.presets?.year?.short ??
+ DateToken.Year_2Digit,
+ default:
+ options?.presets?.year?.default ??
+ settings.formats?.dates?.presets?.year?.default ??
+ DateToken.Year_numeric,
+ long:
+ options?.presets?.year?.long ??
+ settings.formats?.dates?.presets?.year?.long ??
+ DateToken.Year_numeric,
+ custom,
+ },
+ },
+
+ // dico
+ dictionaryDate: getDictionary().Date,
+ };
+
+ return toRet;
+}
+
+export function getDictionary(options?: DictionaryMessagesOptions) {
+ // if custom is set && variant is not set, let's put custom as variant
+ const settings = getSettings();
+
+ let toRet: DictionaryMessages = {
+ Ok: options?.Ok ?? settings.dictionary?.Ok ?? 'Ok',
+ Cancel: options?.Cancel ?? settings.dictionary?.Cancel ?? 'Cancel',
+
+ Date: {
+ Day: options?.Date?.Day ?? settings.dictionary?.Date?.Day ?? 'Day',
+ Week: options?.Date?.Week ?? settings.dictionary?.Date?.Week ?? 'Week',
+ BiWeek: options?.Date?.BiWeek ?? settings.dictionary?.Date?.BiWeek ?? 'Bi-Week',
+ Month: options?.Date?.Month ?? settings.dictionary?.Date?.Month ?? 'Month',
+ Quarter: options?.Date?.Quarter ?? settings.dictionary?.Date?.Quarter ?? 'Quarter',
+ CalendarYear:
+ options?.Date?.CalendarYear ?? settings.dictionary?.Date?.CalendarYear ?? 'Calendar Year',
+ FiscalYearOct:
+ options?.Date?.FiscalYearOct ??
+ settings.dictionary?.Date?.FiscalYearOct ??
+ 'Fiscal Year (Oct)',
+ },
+ };
+
+ return toRet;
+}
diff --git a/packages/svelte-ux/src/lib/index.ts b/packages/svelte-ux/src/lib/index.ts
index 180dd4045..3e5b71788 100644
--- a/packages/svelte-ux/src/lib/index.ts
+++ b/packages/svelte-ux/src/lib/index.ts
@@ -2,4 +2,5 @@ export * from './actions';
export * from './components';
export * from './stores';
export * from './types';
+// TODO: Conflic Duration Component & Type
export * from './utils';
diff --git a/packages/svelte-ux/src/lib/utils/date.test.ts b/packages/svelte-ux/src/lib/utils/date.test.ts
new file mode 100644
index 000000000..09e8447d1
--- /dev/null
+++ b/packages/svelte-ux/src/lib/utils/date.test.ts
@@ -0,0 +1,483 @@
+import { describe, it, expect } from 'vitest';
+import {
+ PeriodType,
+ formatDate,
+ getMonthDaysByWeek,
+ localToUtcDate,
+ utcToLocalDate,
+ DayOfWeek,
+ formatIntl,
+ type CustomIntlDateTimeFormatOptions,
+ type FormatDateOptions,
+ DateToken,
+} from './date';
+import { format } from '.';
+
+const DATE = '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);
+
+const dt_1M_1d_time_pm = new Date(2023, 2, 7, 14, 2, 3, 4);
+const dt_1M_1d_time_am = new Date(2023, 2, 7, 1, 2, 3, 4);
+
+const fr: FormatDateOptions = {
+ locales: 'fr',
+ ordinalSuffixes: {
+ fr: {
+ one: 'er',
+ two: '',
+ few: '',
+ other: '',
+ },
+ },
+};
+
+describe('formatDate()', () => {
+ it('should return empty string for null or undefined date', () => {
+ expect(formatDate(null)).equal('');
+ expect(formatDate(undefined)).equal('');
+ });
+
+ it('should return empty string for invalid date', () => {
+ expect(formatDate('invalid date')).equal('');
+ });
+
+ describe('should format date for PeriodType.Day', () => {
+ const localDate = new Date(2023, 10, 21);
+ const combi = [
+ ['short', undefined, '11/21'],
+ ['short', 'fr', '21/11'],
+ ['long', undefined, 'Nov 21, 2023'],
+ ['long', 'fr', '21 nov. 2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(localDate, PeriodType.Day, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date string for PeriodType.Day', () => {
+ const combi = [
+ ['short', undefined, '11/21'],
+ ['short', 'fr', '21/11'],
+ ['long', undefined, 'Nov 21, 2023'],
+ ['long', 'fr', '21 nov. 2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.Day, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date string for DayTime, TimeOnly', () => {
+ const combi: [Date, PeriodType, FormatDateOptions, string[]][] = [
+ [
+ dt_1M_1d_time_pm,
+ PeriodType.DayTime,
+ { variant: 'short' },
+ ['3/7/2023, 2:02 PM', '07/03/2023 14:02'],
+ ],
+ [
+ dt_1M_1d_time_pm,
+ PeriodType.DayTime,
+ { variant: 'default' },
+ ['3/7/2023, 02:02 PM', '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, 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']],
+ [
+ dt_1M_1d_time_pm,
+ PeriodType.TimeOnly,
+ { variant: 'long' },
+ ['02:02:03.004 PM', '14:02:03,004'],
+ ],
+ ];
+
+ for (const c of combi) {
+ const [date, periodType, options, [expected_default, expected_fr]] = c;
+ it(c.toString(), () => {
+ expect(format(date, periodType, options)).equal(expected_default);
+ });
+
+ it(c.toString() + 'fr', () => {
+ expect(format(date, periodType, { ...options, ...fr })).equal(expected_fr);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.WeekSun / Mon', () => {
+ const combi = [
+ [PeriodType.WeekSun, 'short', undefined, '11/19 - 11/25'],
+ [PeriodType.WeekSun, 'short', 'fr', '19/11 - 25/11'],
+ [PeriodType.WeekSun, 'long', undefined, '11/19/2023 - 11/25/2023'],
+ [PeriodType.WeekSun, 'long', 'fr', '19/11/2023 - 25/11/2023'],
+ [PeriodType.WeekMon, 'long', undefined, '11/20/2023 - 11/26/2023'],
+ [PeriodType.WeekMon, 'long', 'fr', '20/11/2023 - 26/11/2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [periodType, variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, periodType, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.Week', () => {
+ const combi = [
+ [PeriodType.Week, 'short', undefined, DayOfWeek.Sunday, '11/19 - 11/25'],
+ [PeriodType.Week, 'short', 'fr', DayOfWeek.Sunday, '19/11 - 25/11'],
+ [PeriodType.Week, 'long', undefined, DayOfWeek.Sunday, '11/19/2023 - 11/25/2023'],
+ [PeriodType.Week, 'long', 'fr', DayOfWeek.Sunday, '19/11/2023 - 25/11/2023'],
+
+ [PeriodType.Week, 'short', undefined, DayOfWeek.Monday, '11/20 - 11/26'],
+ [PeriodType.Week, 'short', 'fr', DayOfWeek.Monday, '20/11 - 26/11'],
+ [PeriodType.Week, 'long', undefined, DayOfWeek.Monday, '11/20/2023 - 11/26/2023'],
+ [PeriodType.Week, 'long', 'fr', DayOfWeek.Monday, '20/11/2023 - 26/11/2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [periodType, variant, locales, weekStartsOn, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, periodType, { variant, locales, weekStartsOn })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.Month', () => {
+ const combi = [
+ ['short', undefined, 'Nov'],
+ ['short', 'fr', 'nov.'],
+ ['long', undefined, 'November'],
+ ['long', 'fr', 'novembre'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.Month, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.MonthYear', () => {
+ const combi = [
+ ['short', undefined, 'Nov 23'],
+ ['short', 'fr', 'nov. 23'],
+ ['long', undefined, 'November 2023'],
+ ['long', 'fr', 'novembre 2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.MonthYear, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.Quarter', () => {
+ const combi = [
+ ['short', undefined, 'Oct - Dec 23'],
+ ['short', 'fr', 'oct. - déc. 23'],
+ ['long', undefined, 'October - December 2023'],
+ ['long', 'fr', 'octobre - décembre 2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.Quarter, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.CalendarYear', () => {
+ const combi = [
+ ['short', undefined, '23'],
+ ['short', 'fr', '23'],
+ ['long', undefined, '2023'],
+ ['long', 'fr', '2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.CalendarYear, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.FiscalYearOctober', () => {
+ const combi = [
+ ['short', undefined, '24'],
+ ['short', 'fr', '24'],
+ ['long', undefined, '2024'],
+ ['long', 'fr', '2024'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.FiscalYearOctober, { variant, locales })).equal(
+ expected
+ );
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.BiWeek1Sun', () => {
+ const combi = [
+ ['short', undefined, '11/12 - 11/25'],
+ ['short', 'fr', '12/11 - 25/11'],
+ ['long', undefined, '11/12/2023 - 11/25/2023'],
+ ['long', 'fr', '12/11/2023 - 25/11/2023'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales, expected] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, PeriodType.BiWeek1Sun, { variant, locales })).equal(expected);
+ });
+ }
+ });
+
+ describe('should format date for PeriodType.undefined', () => {
+ const expected = '2023-11-21T00:00:00-04:00';
+ const combi = [
+ ['short', undefined],
+ ['short', 'fr'],
+ ['long', undefined],
+ ['long', 'fr'],
+ ] as const;
+
+ for (const c of combi) {
+ const [variant, locales] = c;
+ it(c.toString(), () => {
+ expect(formatDate(DATE, undefined, { variant, locales })).equal(expected);
+ });
+ }
+ });
+});
+
+describe('formatIntl() tokens', () => {
+ const combi: [Date, CustomIntlDateTimeFormatOptions, string[]][] = [
+ [dt_1M_1d, 'MM/dd/yyyy', ['03/07/2023', '07/03/2023']],
+ [dt_2M_2d, 'M/d/yyyy', ['11/21/2023', '21/11/2023']],
+ [dt_2M_1d, 'M/d/yyyy', ['11/7/2023', '07/11/2023']],
+ [dt_2M_1d, 'M/dd/yyyy', ['11/07/2023', '07/11/2023']],
+ [dt_1M_1d, 'M/d/yyyy', ['3/7/2023', '07/03/2023']],
+ [dt_1M_1d, 'MM/d/yyyy', ['03/7/2023', '7/03/2023']],
+ [dt_2M_2d, 'M/d', ['11/21', '21/11']],
+ [dt_2M_2d, 'MMM d, yyyy', ['Nov 21, 2023', '21 nov. 2023']],
+ [dt_2M_1d, 'MMM d, yyyy', ['Nov 7, 2023', '7 nov. 2023']],
+ [dt_2M_1d, 'MMM do, yyyy', ['Nov 7th, 2023', '7 nov. 2023']],
+ [dt_2M_2d, 'MMM', ['Nov', 'nov.']],
+ [dt_2M_2d, 'MMMM', ['November', 'novembre']],
+ [dt_2M_2d, 'MMM yy', ['Nov 23', 'nov. 23']],
+ [dt_2M_2d, 'MMMM yyyy', ['November 2023', 'novembre 2023']],
+ [dt_2M_2d, 'yy', ['23', '23']],
+ [dt_2M_2d, 'yyyy', ['2023', '2023']],
+ [dt_2M_2d, { dateStyle: 'full' }, ['Tuesday, November 21, 2023', 'mardi 21 novembre 2023']],
+ [dt_2M_2d, { dateStyle: 'long' }, ['November 21, 2023', '21 novembre 2023']],
+ [dt_2M_2d, { dateStyle: 'medium' }, ['Nov 21, 2023', '21 nov. 2023']],
+ [dt_2M_2d, { dateStyle: 'medium', withOrdinal: true }, ['Nov 21st, 2023', '21 nov. 2023']],
+ [dt_2M_2d, { dateStyle: 'short' }, ['11/21/23', '21/11/2023']],
+ [dt_1M_1d, { dateStyle: 'short' }, ['3/7/23', '07/03/2023']],
+
+ // time
+ [dt_1M_1d_time_pm, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['2:02 PM', '14:02']],
+ [dt_1M_1d_time_am, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['1:02 AM', '01:02']],
+ [
+ dt_1M_1d_time_am,
+ [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Hour_wAMPM],
+ ['1:02 AM', '1:02 AM'],
+ ],
+ [
+ dt_1M_1d_time_am,
+ [DateToken.Hour_2Digit, DateToken.Minute_2Digit, DateToken.Hour_woAMPM],
+ ['01:02', '01:02'],
+ ],
+ [
+ dt_1M_1d_time_am,
+ [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Second_numeric],
+ ['1:02:03 AM', '01:02:03'],
+ ],
+ [
+ dt_1M_1d_time_am,
+ [
+ DateToken.Hour_numeric,
+ DateToken.Minute_numeric,
+ DateToken.Second_numeric,
+ DateToken.MiliSecond_3,
+ ],
+ ['1:02:03.004 AM', '01:02:03,004'],
+ ],
+ ];
+
+ for (const c of combi) {
+ const [date, tokens, [expected_default, expected_fr]] = c;
+ it(c.toString(), () => {
+ expect(formatIntl(date, tokens)).equal(expected_default);
+ });
+
+ it(c.toString() + 'fr', () => {
+ expect(formatIntl(date, tokens, fr)).equal(expected_fr);
+ });
+ }
+});
+
+describe('utcToLocalDate()', () => {
+ it('in with offset -00 => local', () => {
+ const utcDate = '2023-11-21T00:00:00-00:00';
+ const localDate = utcToLocalDate(utcDate);
+ expect(localDate.toISOString()).equal('2023-11-21T04:00:00.000Z');
+ });
+
+ it('in without offset, the utc is already +4, to local: another +4', () => {
+ const utcDate = '2023-11-21T00:00:00';
+ const localDate = utcToLocalDate(utcDate);
+ expect(localDate.toISOString()).equal('2023-11-21T08:00:00.000Z');
+ });
+});
+
+describe('localToUtcDate()', () => {
+ it('in with offset -04 => UTC', () => {
+ const localDate = '2023-11-21T00:00:00-04:00';
+ const utcDate = localToUtcDate(localDate);
+ expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z');
+ });
+
+ it('in with offset -00 => UTC', () => {
+ const localDate = '2023-11-21T04:00:00-00:00';
+ const utcDate = localToUtcDate(localDate);
+ expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z');
+ });
+
+ it('in without offset == UTC', () => {
+ const localDate = '2023-11-21T04:00:00';
+ const utcDate = localToUtcDate(localDate);
+ expect(utcDate.toISOString()).equal('2023-11-21T04:00:00.000Z');
+ });
+});
+
+describe('getMonthDaysByWeek()', () => {
+ it('default starting Week: Sunday', () => {
+ const dates = getMonthDaysByWeek(new Date(DATE));
+ expect(dates).toMatchInlineSnapshot(`
+ [
+ [
+ 2023-10-29T04:00:00.000Z,
+ 2023-10-30T04:00:00.000Z,
+ 2023-10-31T04:00:00.000Z,
+ 2023-11-01T04:00:00.000Z,
+ 2023-11-02T04:00:00.000Z,
+ 2023-11-03T04:00:00.000Z,
+ 2023-11-04T04:00:00.000Z,
+ ],
+ [
+ 2023-11-05T04:00:00.000Z,
+ 2023-11-06T04:00:00.000Z,
+ 2023-11-07T04:00:00.000Z,
+ 2023-11-08T04:00:00.000Z,
+ 2023-11-09T04:00:00.000Z,
+ 2023-11-10T04:00:00.000Z,
+ 2023-11-11T04:00:00.000Z,
+ ],
+ [
+ 2023-11-12T04:00:00.000Z,
+ 2023-11-13T04:00:00.000Z,
+ 2023-11-14T04:00:00.000Z,
+ 2023-11-15T04:00:00.000Z,
+ 2023-11-16T04:00:00.000Z,
+ 2023-11-17T04:00:00.000Z,
+ 2023-11-18T04:00:00.000Z,
+ ],
+ [
+ 2023-11-19T04:00:00.000Z,
+ 2023-11-20T04:00:00.000Z,
+ 2023-11-21T04:00:00.000Z,
+ 2023-11-22T04:00:00.000Z,
+ 2023-11-23T04:00:00.000Z,
+ 2023-11-24T04:00:00.000Z,
+ 2023-11-25T04:00:00.000Z,
+ ],
+ [
+ 2023-11-26T04:00:00.000Z,
+ 2023-11-27T04:00:00.000Z,
+ 2023-11-28T04:00:00.000Z,
+ 2023-11-29T04:00:00.000Z,
+ 2023-11-30T04:00:00.000Z,
+ 2023-12-01T04:00:00.000Z,
+ 2023-12-02T04:00:00.000Z,
+ ],
+ ]
+ `);
+ });
+
+ it('Starting Week: Monday', () => {
+ const dates = getMonthDaysByWeek(new Date(DATE), 1);
+ expect(dates).toMatchInlineSnapshot(`
+ [
+ [
+ 2023-10-30T04:00:00.000Z,
+ 2023-10-31T04:00:00.000Z,
+ 2023-11-01T04:00:00.000Z,
+ 2023-11-02T04:00:00.000Z,
+ 2023-11-03T04:00:00.000Z,
+ 2023-11-04T04:00:00.000Z,
+ 2023-11-05T04:00:00.000Z,
+ ],
+ [
+ 2023-11-06T04:00:00.000Z,
+ 2023-11-07T04:00:00.000Z,
+ 2023-11-08T04:00:00.000Z,
+ 2023-11-09T04:00:00.000Z,
+ 2023-11-10T04:00:00.000Z,
+ 2023-11-11T04:00:00.000Z,
+ 2023-11-12T04:00:00.000Z,
+ ],
+ [
+ 2023-11-13T04:00:00.000Z,
+ 2023-11-14T04:00:00.000Z,
+ 2023-11-15T04:00:00.000Z,
+ 2023-11-16T04:00:00.000Z,
+ 2023-11-17T04:00:00.000Z,
+ 2023-11-18T04:00:00.000Z,
+ 2023-11-19T04:00:00.000Z,
+ ],
+ [
+ 2023-11-20T04:00:00.000Z,
+ 2023-11-21T04:00:00.000Z,
+ 2023-11-22T04:00:00.000Z,
+ 2023-11-23T04:00:00.000Z,
+ 2023-11-24T04:00:00.000Z,
+ 2023-11-25T04:00:00.000Z,
+ 2023-11-26T04:00:00.000Z,
+ ],
+ [
+ 2023-11-27T04:00:00.000Z,
+ 2023-11-28T04:00:00.000Z,
+ 2023-11-29T04:00:00.000Z,
+ 2023-11-30T04:00:00.000Z,
+ 2023-12-01T04:00:00.000Z,
+ 2023-12-02T04:00:00.000Z,
+ 2023-12-03T04:00:00.000Z,
+ ],
+ ]
+ `);
+ });
+});
diff --git a/packages/svelte-ux/src/lib/utils/date.ts b/packages/svelte-ux/src/lib/utils/date.ts
index 581354e18..37277dc92 100644
--- a/packages/svelte-ux/src/lib/utils/date.ts
+++ b/packages/svelte-ux/src/lib/utils/date.ts
@@ -1,5 +1,4 @@
import {
- format,
startOfDay,
endOfDay,
startOfWeek,
@@ -31,11 +30,10 @@ import {
formatISO,
} from 'date-fns';
-import { timeDays } from 'd3-time';
-
import { hasKeyOf } from '../types/typeGuards';
import { chunk } from './array';
import type { DateRange } from './dateRange';
+import { getFormatDate } from '$lib/components/settings';
export type SelectedDate = Date | Date[] | DateRange | null;
@@ -46,7 +44,11 @@ export type Period = {
};
export enum PeriodType {
+ Custom = 1,
+
Day = 10,
+ DayTime = 11,
+ TimeOnly = 15,
WeekSun = 20,
WeekMon = 21,
@@ -55,8 +57,10 @@ export enum PeriodType {
WeekThu = 24,
WeekFri = 25,
WeekSat = 26,
+ Week = 27, // will be replaced by WeekSun, WeekMon, etc depending on weekStartsOn
Month = 30,
+ MonthYear = 31,
Quarter = 40,
CalendarYear = 50,
FiscalYearOctober = 60,
@@ -68,6 +72,7 @@ export enum PeriodType {
BiWeek1Thu = 74,
BiWeek1Fri = 75,
BiWeek1Sat = 76,
+ BiWeek1 = 77, // will be replaced by BiWeek1Sun, BiWeek1Mon, etc depending on weekStartsOn
BiWeek2Sun = 80,
BiWeek2Mon = 81,
@@ -76,76 +81,89 @@ export enum PeriodType {
BiWeek2Thu = 84,
BiWeek2Fri = 85,
BiWeek2Sat = 86,
+ BiWeek2 = 87, // will be replaced by BiWeek2Sun, BiWeek2Mon, etc depending on weekStartsOn
}
export enum DayOfWeek {
- SUN,
- MON,
- TUE,
- WED,
- THU,
- FRI,
- SAT,
+ Sunday = 0,
+ Monday = 1,
+ Tuesday = 2,
+ Wednesday = 3,
+ Thursday = 4,
+ Friday = 5,
+ Saturday = 6,
}
-export function getPeriodTypeName(periodType: PeriodType) {
+export function getDayOfWeekName(weekStartsOn: DayOfWeek, locales: string) {
+ // Create a date object for a specific day (0 = Sunday, 1 = Monday, etc.)
+ // And "7 of Jan 2024" is a Sunday
+ const date = new Date(2024, 0, 7 + weekStartsOn);
+ const formatter = new Intl.DateTimeFormat(locales, { weekday: 'short' });
+ return formatter.format(date);
+}
+
+export function getPeriodTypeName(periodType: PeriodType, options: FormatDateOptions = {}) {
+ const { locales, dictionaryDate: dico } = getFormatDate(options);
+
switch (periodType) {
case PeriodType.Day:
- return 'Day';
+ return dico.Day;
case PeriodType.WeekSun:
- return 'Week (Sun)';
+ return `${dico.Week} (${getDayOfWeekName(DayOfWeek.Sunday, locales)})`;
case PeriodType.WeekMon:
- return 'Week (Mon)';
+ return `${dico.Week} (${getDayOfWeekName(1, locales)})`;
case PeriodType.WeekTue:
- return 'Week (Tue)';
+ return `${dico.Week} (${getDayOfWeekName(2, locales)})`;
case PeriodType.WeekWed:
- return 'Week (Wed)';
+ return `${dico.Week} (${getDayOfWeekName(3, locales)})`;
case PeriodType.WeekThu:
- return 'Week (Thu)';
+ return `${dico.Week} (${getDayOfWeekName(4, locales)})`;
case PeriodType.WeekFri:
- return 'Week (Fri)';
+ return `${dico.Week} (${getDayOfWeekName(5, locales)})`;
case PeriodType.WeekSat:
- return 'Week (Sat)';
+ return `${dico.Week} (${getDayOfWeekName(6, locales)})`;
case PeriodType.Month:
- return 'Month';
+ return dico.Month;
+ case PeriodType.MonthYear:
+ return dico.Month;
case PeriodType.Quarter:
- return 'Quarter';
+ return dico.Quarter;
case PeriodType.CalendarYear:
- return 'Calendar Year';
+ return dico.CalendarYear;
case PeriodType.FiscalYearOctober:
- return 'Fiscal Year (Oct)';
+ return dico.FiscalYearOct;
case PeriodType.BiWeek1Sun:
- return 'Bi-Week (Sun)';
+ return `${dico.BiWeek} (${getDayOfWeekName(0, locales)})`;
case PeriodType.BiWeek1Mon:
- return 'Bi-Week (Mon)';
+ return `${dico.BiWeek} (${getDayOfWeekName(1, locales)})`;
case PeriodType.BiWeek1Tue:
- return 'Bi-Week (Tue)';
+ return `${dico.BiWeek} (${getDayOfWeekName(2, locales)})`;
case PeriodType.BiWeek1Wed:
- return 'Bi-Week (Wed)';
+ return `${dico.BiWeek} (${getDayOfWeekName(3, locales)})`;
case PeriodType.BiWeek1Thu:
- return 'Bi-Week (Thu)';
+ return `${dico.BiWeek} (${getDayOfWeekName(4, locales)})`;
case PeriodType.BiWeek1Fri:
- return 'Bi-Week (Fri)';
+ return `${dico.BiWeek} (${getDayOfWeekName(5, locales)})`;
case PeriodType.BiWeek1Sat:
- return 'Bi-Week (Sat)';
+ return `${dico.BiWeek} (${getDayOfWeekName(6, locales)})`;
case PeriodType.BiWeek2Sun:
- return 'Bi-Week 2 (Sun)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(0, locales)})`;
case PeriodType.BiWeek2Mon:
- return 'Bi-Week 2 (Mon)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(1, locales)})`;
case PeriodType.BiWeek2Tue:
- return 'Bi-Week 2 (Tue)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(2, locales)})`;
case PeriodType.BiWeek2Wed:
- return 'Bi-Week 2 (Wed)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(3, locales)})`;
case PeriodType.BiWeek2Thu:
- return 'Bi-Week 2 (Thu)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(4, locales)})`;
case PeriodType.BiWeek2Fri:
- return 'Bi-Week 2 (Fri)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(5, locales)})`;
case PeriodType.BiWeek2Sat:
- return 'Bi-Week 2 (Sat)';
+ return `${dico.BiWeek} 2 (${getDayOfWeekName(6, locales)})`;
default:
return 'Unknown';
@@ -292,12 +310,22 @@ export function getMonths(year = new Date().getFullYear()) {
return Array.from({ length: 12 }, (_, i) => new Date(year, i, 1));
}
-export function getMonthDaysByWeek(startOfMonth: Date): Date[][] {
- const startOfFirstWeek = startOfWeek(startOfMonth);
- const endOfLastWeek = endOfWeek(endOfMonth(startOfMonth));
- const monthDaysByWeek = chunk(timeDays(startOfFirstWeek, endOfLastWeek), 7);
+export function getMonthDaysByWeek(
+ dateInTheMonth: Date,
+ weekStartsOn: DayOfWeek = DayOfWeek.Sunday
+): Date[][] {
+ const startOfFirstWeek = startOfWeek(startOfMonth(dateInTheMonth), { weekStartsOn });
+ const endOfLastWeek = endOfWeek(endOfMonth(dateInTheMonth), { weekStartsOn });
- return monthDaysByWeek;
+ const list = [];
+
+ let valueToAdd = startOfFirstWeek;
+ while (valueToAdd <= endOfLastWeek) {
+ list.push(valueToAdd);
+ valueToAdd = addDays(valueToAdd, 1);
+ }
+
+ return chunk(list, 7) as Date[][];
}
export function getMinSelectedDate(date: SelectedDate | null | undefined) {
@@ -549,11 +577,223 @@ export function formatISODate(
return formatISO(date, { representation });
}
+export enum DateToken {
+ /** `1982, 1986, 2024` */
+ Year_numeric = 'yyy',
+ /** `82, 86, 24` */
+ Year_2Digit = 'yy',
+
+ /** `January, February, ..., December` */
+ Month_long = 'MMMM',
+ /** `Jan, Feb, ..., Dec` */
+ Month_short = 'MMM',
+ /** `01, 02, ..., 12` */
+ Month_2Digit = 'MM',
+ /** `1, 2, ..., 12` */
+ Month_numeric = 'M',
+
+ /** `1, 2, ..., 11, 12` */
+ Hour_numeric = 'h',
+ /** `01, 02, ..., 11, 12` */
+ Hour_2Digit = 'hh',
+ /** You should probably not use this. Force with AM/PM (and the good locale), not specifying this will automatically take the good local */
+ Hour_wAMPM = 'a',
+ /** You should probably not use this. Force without AM/PM (and the good locale), not specifying this will automatically take the good local */
+ Hour_woAMPM = 'aaaaaa',
+
+ /** `0, 1, ..., 59` */
+ Minute_numeric = 'm',
+ /** `00, 01, ..., 59` */
+ Minute_2Digit = 'mm',
+
+ /** `0, 1, ..., 59` */
+ Second_numeric = 's',
+ /** `00, 01, ..., 59` */
+ Second_2Digit = 'ss',
+
+ /** `000, 001, ..., 999` */
+ MiliSecond_3 = 'SSS',
+
+ /** Minimize digit: `1, 2, 11, ...` */
+ DayOfMonth_numeric = 'd',
+ /** `01, 02, 11, ...` */
+ DayOfMonth_2Digit = 'dd',
+ /** `1st, 2nd, 11th, ...` You can have your local ordinal by passing `ordinalSuffixes` in options / settings */
+ DayOfMonth_withOrdinal = 'do',
+
+ /** `M, T, W, T, F, S, S` */
+ DayOfWeek_narrow = 'eeeee',
+ /** `Monday, Tuesday, ..., Sunday` */
+ DayOfWeek_long = 'eeee',
+ /** `Mon, Tue, Wed, ..., Sun` */
+ DayOfWeek_short = 'eee',
+}
+
+export function formatIntl(
+ dt: Date,
+ tokens_or_intlOptions: CustomIntlDateTimeFormatOptions,
+ options: FormatDateOptions = {}
+) {
+ const { locales, ordinalSuffixes } = getFormatDate(options);
+
+ function formatIntlOrdinal(formatter: Intl.DateTimeFormat, with_ordinal = false) {
+ if (with_ordinal) {
+ const suffixes = ordinalSuffixes[locales] ?? ordinalSuffixes['en'];
+ const rules = new Intl.PluralRules(locales, { type: 'ordinal' });
+
+ const splited = formatter.formatToParts(dt);
+ return splited
+ .map((c) => {
+ if (c.type === 'day') {
+ const ordinal = rules.select(parseInt(c.value, 10));
+ const suffix = suffixes[ordinal];
+ return `${c.value}${suffix}`;
+ }
+ return c.value;
+ })
+ .join('');
+ }
+
+ return formatter.format(dt);
+ }
+
+ if (typeof tokens_or_intlOptions !== 'string' && !Array.isArray(tokens_or_intlOptions)) {
+ return formatIntlOrdinal(
+ new Intl.DateTimeFormat(locales, tokens_or_intlOptions),
+ tokens_or_intlOptions.withOrdinal
+ );
+ }
+
+ const tokens = Array.isArray(tokens_or_intlOptions)
+ ? tokens_or_intlOptions.join('')
+ : tokens_or_intlOptions;
+
+ // Order of includes check is important! (longest first)
+ const formatter = new Intl.DateTimeFormat(locales, {
+ year: tokens.includes(DateToken.Year_numeric)
+ ? 'numeric'
+ : tokens.includes(DateToken.Year_2Digit)
+ ? '2-digit'
+ : undefined,
+
+ month: tokens.includes(DateToken.Month_long)
+ ? 'long'
+ : tokens.includes(DateToken.Month_short)
+ ? 'short'
+ : tokens.includes(DateToken.Month_2Digit)
+ ? '2-digit'
+ : tokens.includes(DateToken.Month_numeric)
+ ? 'numeric'
+ : undefined,
+
+ day: tokens.includes(DateToken.DayOfMonth_2Digit)
+ ? '2-digit'
+ : tokens.includes(DateToken.DayOfMonth_numeric)
+ ? 'numeric'
+ : undefined,
+
+ hour: tokens.includes(DateToken.Hour_2Digit)
+ ? '2-digit'
+ : tokens.includes(DateToken.Hour_numeric)
+ ? 'numeric'
+ : undefined,
+ hour12: tokens.includes(DateToken.Hour_woAMPM)
+ ? false
+ : tokens.includes(DateToken.Hour_wAMPM)
+ ? true
+ : undefined,
+
+ minute: tokens.includes(DateToken.Minute_2Digit)
+ ? '2-digit'
+ : tokens.includes(DateToken.Minute_numeric)
+ ? 'numeric'
+ : undefined,
+
+ second: tokens.includes(DateToken.Second_2Digit)
+ ? '2-digit'
+ : tokens.includes(DateToken.Second_numeric)
+ ? 'numeric'
+ : undefined,
+
+ fractionalSecondDigits: tokens.includes(DateToken.MiliSecond_3) ? 3 : undefined,
+
+ weekday: tokens.includes(DateToken.DayOfWeek_narrow)
+ ? 'narrow'
+ : tokens.includes(DateToken.DayOfWeek_long)
+ ? 'long'
+ : tokens.includes(DateToken.DayOfWeek_short)
+ ? 'short'
+ : undefined,
+ });
+
+ return formatIntlOrdinal(formatter, tokens.includes(DateToken.DayOfMonth_withOrdinal));
+}
+
+function range(
+ date: Date,
+ weekStartsOn: DayOfWeek,
+ options: FormatDateOptions,
+ formats: Record,
+ variant: DateFormatVariant,
+ biWeek: undefined | 1 | 2 = undefined // undefined means that it's not a bi-week
+) {
+ const start =
+ biWeek === undefined
+ ? startOfWeek(date, { weekStartsOn })
+ : startOfBiWeek(date, biWeek, weekStartsOn);
+ const end =
+ biWeek === undefined
+ ? endOfWeek(date, { weekStartsOn })
+ : endOfBiWeek(date, biWeek, weekStartsOn);
+
+ const formatToUse = formats[variant];
+
+ return formatIntl(start, formatToUse, options) + ' - ' + formatIntl(end, formatToUse, options);
+}
+
+export type OrdinalSuffixes = {
+ one: string;
+ two: string;
+ few: string;
+ other: string;
+ zero?: string;
+ many?: string;
+};
+export type DateFormatVariant = 'short' | 'default' | 'long' | 'custom';
+type DateFormatVariantPreset = {
+ short?: CustomIntlDateTimeFormatOptions;
+ default?: CustomIntlDateTimeFormatOptions;
+ long?: CustomIntlDateTimeFormatOptions;
+};
+export type CustomIntlDateTimeFormatOptions =
+ | string
+ | string[]
+ | (Intl.DateTimeFormatOptions & { withOrdinal?: boolean });
+export type FormatDateOptions = {
+ locales?: string | undefined;
+ baseParsing?: string;
+ weekStartsOn?: DayOfWeek;
+ variant?: DateFormatVariant;
+ custom?: CustomIntlDateTimeFormatOptions;
+ presets?: {
+ day?: DateFormatVariantPreset;
+ dayTime?: DateFormatVariantPreset;
+ timeOnly?: DateFormatVariantPreset;
+ week?: DateFormatVariantPreset;
+ month?: DateFormatVariantPreset;
+ monthsYear?: DateFormatVariantPreset;
+ year?: DateFormatVariantPreset;
+ };
+ ordinalSuffixes?: Record;
+};
+
export function formatDate(
date: Date | string | null | undefined,
periodType?: PeriodType | null | undefined,
- variant?: 'short' | 'long' // TODO: Support x-long, etc (maybe call it sm, md, lg, xl, etc)
-) {
+ options: FormatDateOptions = {}
+): string {
+ periodType = periodType ?? undefined;
+
if (typeof date === 'string') {
date = parseISO(date);
}
@@ -564,49 +804,118 @@ export function formatDate(
return '';
}
+ const { variant, weekStartsOn, custom, presets } = getFormatDate(options);
+ const { day, dayTime, timeOnly, week, month, monthYear, year } = presets;
+
+ if (periodType === PeriodType.Week) {
+ periodType = [
+ PeriodType.WeekSun,
+ PeriodType.WeekMon,
+ PeriodType.WeekTue,
+ PeriodType.WeekWed,
+ PeriodType.WeekThu,
+ PeriodType.WeekFri,
+ PeriodType.WeekSat,
+ ][weekStartsOn];
+ } else if (periodType === PeriodType.BiWeek1) {
+ periodType = [
+ PeriodType.BiWeek1Sun,
+ PeriodType.BiWeek1Mon,
+ PeriodType.BiWeek1Tue,
+ PeriodType.BiWeek1Wed,
+ PeriodType.BiWeek1Thu,
+ PeriodType.BiWeek1Fri,
+ PeriodType.BiWeek1Sat,
+ ][weekStartsOn];
+ } else if (periodType === PeriodType.BiWeek2) {
+ periodType = [
+ PeriodType.BiWeek2Sun,
+ PeriodType.BiWeek2Mon,
+ PeriodType.BiWeek2Tue,
+ PeriodType.BiWeek2Wed,
+ PeriodType.BiWeek2Thu,
+ PeriodType.BiWeek2Fri,
+ PeriodType.BiWeek2Sat,
+ ][weekStartsOn];
+ }
+
switch (periodType) {
+ case PeriodType.Custom:
+ return formatIntl(date, custom, options);
+
case PeriodType.Day:
- return variant === 'short' ? format(date, 'M/d') : format(date, 'MMM d, yyyy');
+ return formatIntl(date, day[variant], options);
+
+ case PeriodType.DayTime:
+ return formatIntl(date, dayTime[variant], options);
+
+ case PeriodType.TimeOnly:
+ return formatIntl(date, timeOnly[variant], options);
case PeriodType.WeekSun:
+ return range(date, 0, options, week, variant);
case PeriodType.WeekMon:
+ return range(date, 1, options, week, variant);
case PeriodType.WeekTue:
+ return range(date, 2, options, week, variant);
case PeriodType.WeekWed:
+ return range(date, 3, options, week, variant);
case PeriodType.WeekThu:
+ return range(date, 4, options, week, variant);
case PeriodType.WeekFri:
+ return range(date, 5, options, week, variant);
case PeriodType.WeekSat:
- return variant === 'short'
- ? format(date, 'M/d') + ' - ' + format(addDays(date, 6), 'M/d')
- : format(date, 'M/d/yyyy') + ' - ' + format(addDays(date, 6), 'M/d/yyyy');
+ return range(date, 6, options, week, variant);
case PeriodType.Month:
- return variant === 'short' ? format(date, 'MMM yyyy') : format(date, 'MMMM yyyy');
+ return formatIntl(date, month[variant], options);
+
+ case PeriodType.MonthYear:
+ return formatIntl(date, monthYear[variant], options);
+
case PeriodType.Quarter:
- return variant === 'short'
- ? format(date, 'MMM') + ' - ' + format(addMonths(date, 2), 'MMM yyyy')
- : format(date, 'MMMM') + ' - ' + format(addMonths(date, 2), 'MMMM yyyy');
+ return [
+ formatIntl(startOfQuarter(date), month[variant], options),
+ formatIntl(endOfQuarter(date), monthYear[variant], options),
+ ].join(' - ');
+
case PeriodType.CalendarYear:
- return variant === 'short' ? format(date, 'yy') : format(date, 'yyyy');
+ return formatIntl(date, year[variant], options);
+
case PeriodType.FiscalYearOctober:
- return variant === 'short' ? `${getFiscalYear(date)}`.substring(2) : `${getFiscalYear(date)}`;
+ const fDate = new Date(getFiscalYear(date), 0, 1);
+ return formatIntl(fDate, year[variant], options);
case PeriodType.BiWeek1Sun:
+ return range(date, 0, options, week, variant, 1);
case PeriodType.BiWeek1Mon:
+ return range(date, 1, options, week, variant, 1);
case PeriodType.BiWeek1Tue:
+ return range(date, 2, options, week, variant, 1);
case PeriodType.BiWeek1Wed:
+ return range(date, 3, options, week, variant, 1);
case PeriodType.BiWeek1Thu:
+ return range(date, 4, options, week, variant, 1);
case PeriodType.BiWeek1Fri:
+ return range(date, 5, options, week, variant, 1);
case PeriodType.BiWeek1Sat:
+ return range(date, 6, options, week, variant, 1);
+
case PeriodType.BiWeek2Sun:
+ return range(date, 0, options, week, variant, 2);
case PeriodType.BiWeek2Mon:
+ return range(date, 1, options, week, variant, 2);
case PeriodType.BiWeek2Tue:
+ return range(date, 2, options, week, variant, 2);
case PeriodType.BiWeek2Wed:
+ return range(date, 3, options, week, variant, 2);
case PeriodType.BiWeek2Thu:
+ return range(date, 4, options, week, variant, 2);
case PeriodType.BiWeek2Fri:
+ return range(date, 5, options, week, variant, 2);
case PeriodType.BiWeek2Sat:
- return variant === 'short'
- ? format(date, 'M/d') + ' - ' + format(addDays(date, 13), 'M/d')
- : format(date, 'M/d/yyyy') + ' - ' + format(addDays(date, 13), 'M/d/yyyy');
+ return range(date, 6, options, week, variant, 2);
+
default:
return formatISO(date);
}
@@ -620,9 +929,6 @@ export function utcToLocalDate(date: Date | string | null | undefined) {
// https://github.com/date-fns/date-fns/issues/376#issuecomment-454163253
// return new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
-
- // This approach seems to work more reliably with dates before 11/18/1883 @ 12:00
- // https://github.com/d3/d3-time/issues/29#issuecomment-396415951
const d = new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
diff --git a/packages/svelte-ux/src/lib/utils/dateDisplay.ts b/packages/svelte-ux/src/lib/utils/dateDisplay.ts
deleted file mode 100644
index 6ca714f8b..000000000
--- a/packages/svelte-ux/src/lib/utils/dateDisplay.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { format as dateFormat } from 'date-fns';
-import { formatDate, utcToLocalDate, PeriodType } from './date';
-
-export type DateDisplayOptions = {
- periodType?: PeriodType | null;
- variant?: Parameters[2];
- format?: string;
- utc?: boolean;
-};
-
-export function dateDisplay(
- value: Date | string | number | null | undefined,
- options?: DateDisplayOptions
-) {
- let date = value != null ? (value instanceof Date ? value : new Date(value)) : null;
-
- // Offset for UTC
- if (options?.utc) {
- date = utcToLocalDate(date);
- }
-
- let formattedDate = '';
- if (date) {
- if (options?.format) {
- formattedDate = dateFormat(date, options?.format);
- } else if (options?.periodType) {
- formattedDate = formatDate(date, options?.periodType, options?.variant);
- } else {
- formattedDate = date.toLocaleString();
- }
- }
-
- return formattedDate;
-}
diff --git a/packages/svelte-ux/src/lib/utils/dictionary.ts b/packages/svelte-ux/src/lib/utils/dictionary.ts
new file mode 100644
index 000000000..ad59883e0
--- /dev/null
+++ b/packages/svelte-ux/src/lib/utils/dictionary.ts
@@ -0,0 +1,20 @@
+export type DictionaryMessagesOptions = {
+ Ok?: string;
+ Cancel?: string;
+
+ Date?: {
+ Day?: string;
+ Week?: string;
+ BiWeek?: string;
+ Month?: string;
+ Quarter?: string;
+ CalendarYear?: string;
+ FiscalYearOct?: string;
+ };
+};
+
+type DeepRequired = Required<{
+ [K in keyof T]: T[K] extends Required ? Required : DeepRequired;
+}>;
+
+export type DictionaryMessages = DeepRequired;
diff --git a/packages/svelte-ux/src/lib/utils/format.ts b/packages/svelte-ux/src/lib/utils/format.ts
index e066c986c..6a2c2a157 100644
--- a/packages/svelte-ux/src/lib/utils/format.ts
+++ b/packages/svelte-ux/src/lib/utils/format.ts
@@ -1,6 +1,6 @@
import { isFunction } from 'lodash-es';
-import { formatDate, PeriodType } from './date';
+import { formatDate, PeriodType, type FormatDateOptions } from './date';
import { formatNumber } from './number';
import type { FormatNumberOptions, FormatNumberStyle } from './number';
@@ -14,15 +14,19 @@ export type FormatType =
*/
export function format(
value: null | undefined,
- format?: FormatNumberStyle,
- extraFuncArgs?: FormatNumberOptions
+ format?: FormatNumberStyle | PeriodType,
+ extraFuncArgs?: FormatNumberOptions | FormatDateOptions
): string;
export function format(
value: number,
format?: FormatNumberStyle,
extraFuncArgs?: FormatNumberOptions
): string;
-export function format(value: string | Date, format?: PeriodType, ...extraFuncArgs: any[]): string;
+export function format(
+ value: string | Date,
+ format?: PeriodType,
+ extraFuncArgs?: FormatDateOptions
+): string;
export function format(value: any, format?: FormatType, ...extraFuncArgs: any[]): any {
let formattedValue = value ?? ''; // Do not render `null`
@@ -30,7 +34,11 @@ export function format(value: any, format?: FormatType, ...extraFuncArgs: any[])
if (isFunction(format)) {
formattedValue = format(value, ...extraFuncArgs);
} else if (format in PeriodType) {
- formattedValue = formatDate(value, format as PeriodType, ...extraFuncArgs);
+ formattedValue = formatDate(
+ value,
+ format as PeriodType,
+ extraFuncArgs.length > 0 ? extraFuncArgs[0] : undefined
+ );
} else if (typeof value === 'number') {
formattedValue = formatNumber(value, {
style: format,
diff --git a/packages/svelte-ux/src/lib/utils/index.ts b/packages/svelte-ux/src/lib/utils/index.ts
index 991bf1d6a..6429b6577 100644
--- a/packages/svelte-ux/src/lib/utils/index.ts
+++ b/packages/svelte-ux/src/lib/utils/index.ts
@@ -1,6 +1,5 @@
// top-level exports
-export { formatDate, PeriodType } from './date';
-export * from './dateDisplay';
+export { formatDate, PeriodType, DayOfWeek, DateToken } from './date';
export * from './duration';
export * from './file';
export * from './format';
diff --git a/packages/svelte-ux/src/lib/utils/number.ts b/packages/svelte-ux/src/lib/utils/number.ts
index e2be0ec1d..a91b026f1 100644
--- a/packages/svelte-ux/src/lib/utils/number.ts
+++ b/packages/svelte-ux/src/lib/utils/number.ts
@@ -1,4 +1,4 @@
-import { getFormatNumberOptions } from '$lib/components/settings';
+import { getFormatNumber } from '$lib/components/settings';
export type FormatNumberStyle =
| 'decimal' // from Intl.NumberFormat options.style NumberFormatOptions
@@ -32,7 +32,7 @@ export function formatNumber(number: number | null | undefined, options: FormatN
return `${number}`;
}
- const defaults = getFormatNumberOptions(options.style);
+ const defaults = getFormatNumber(options.style);
const formatter = Intl.NumberFormat(options.locales ?? defaults.locales ?? undefined, {
// Let's always starts with all defaults
diff --git a/packages/svelte-ux/src/routes/+layout.svelte b/packages/svelte-ux/src/routes/+layout.svelte
index 4c102bbb6..40b717317 100644
--- a/packages/svelte-ux/src/routes/+layout.svelte
+++ b/packages/svelte-ux/src/routes/+layout.svelte
@@ -17,6 +17,7 @@
import { settings } from '$lib';
import type { PageData } from './$types';
+ import { DateToken } from '$lib/utils/date';
export let data: PageData;
@@ -26,6 +27,7 @@
$: title = data.pr_id ? `🚧 (pr:${data.pr_id}) - ${baseTitle}` : baseTitle;
settings({
+ // Usefull to test different locales with the docs
// formats: {
// numbers: {
// defaults: {
@@ -33,6 +35,32 @@
// currency: 'EUR',
// },
// },
+ // dates: {
+ // locales: 'fr',
+ // weekStartsOn: 1,
+ // presets: {
+ // days: {
+ // long: { dateStyle: 'full' },
+ // },
+ // months: {
+ // default: [DateToken.Month_long],
+ // },
+ // },
+ // ordinalSuffixes: {
+ // fr: {
+ // one: 'er',
+ // two: '',
+ // few: '',
+ // other: '',
+ // },
+ // },
+ // },
+ // },
+ // dictionary: {
+ // Cancel: 'Annuler',
+ // Date: {
+ // Day: 'Jour',
+ // },
// },
// theme: {
// AppBar: 'bg-red-500 text-white shadow-md',
diff --git a/packages/svelte-ux/src/routes/+page.md b/packages/svelte-ux/src/routes/+page.md
index a433e7ef9..2fc78b552 100644
--- a/packages/svelte-ux/src/routes/+page.md
+++ b/packages/svelte-ux/src/routes/+page.md
@@ -110,12 +110,4 @@ Using `components`, `actions`, or `stores` is as simple as importing from `svelt
Click here
```
-Currently, `utils` are not exposed as top-level exports to not polute the namespace, although this may change in the future. For now, you can import them using the full path.
-
-```js
-import { dateDisplay } from 'svelte-ux/utils/dateDisplay';
-```
-
-See each component page for detailed usage examples.
-
diff --git a/packages/svelte-ux/src/routes/_NavMenu.svelte b/packages/svelte-ux/src/routes/_NavMenu.svelte
index 8104a66d6..1636ffa41 100644
--- a/packages/svelte-ux/src/routes/_NavMenu.svelte
+++ b/packages/svelte-ux/src/routes/_NavMenu.svelte
@@ -52,7 +52,6 @@
],
Feedback: ['Badge', 'Progress', 'ProgressCircle'],
Date: [
- 'dateDisplay',
'DateField',
'DatePickerField',
'DateRange',
diff --git a/packages/svelte-ux/src/routes/customization/+page.md b/packages/svelte-ux/src/routes/customization/+page.md
index 4cb1e15f9..c20c68d58 100644
--- a/packages/svelte-ux/src/routes/customization/+page.md
+++ b/packages/svelte-ux/src/routes/customization/+page.md
@@ -80,6 +80,39 @@ settings({
fractionDigits: 4,
},
},
+
+ dates: {
+ // This is the default, but you can override it here for your app
+ locales: 'en',
+ weekStartsOn: DayOfWeek.Sunday,
+
+ presets: {
+ day: {
+ long: { dateStyle: 'full' },
+ },
+ month: {
+ default: [DateToken.Month_long],
+ },
+ },
+
+ ordinalSuffixes: {
+ en: {
+ one: 'st',
+ two: 'nd',
+ few: 'rd',
+ },
+ },
+
+ dico: {
+ Day: 'Day',
+ Week: 'Week',
+ BiWeek: 'Bi-Week',
+ Month: 'Month',
+ Quarter: 'Quarter',
+ CalendarYear: 'Calendar Year',
+ FiscalYearOct: 'Fiscal Year (Oct)',
+ },
+ },
},
});
```
diff --git a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte b/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte
deleted file mode 100644
index f151cfe41..000000000
--- a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-Examples
-
-No format
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'))}
-
-
-Custom format
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), { format: 'EEE, MMMM do' })}
-
-
-PeriodType Day w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Day,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Day,
- variant: 'short',
- })}
-
-
-PeriodType WeekSun w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.WeekSun,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.WeekSun,
- variant: 'short',
- })}
-
-
-PeriodType BiWeek1Sun w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.BiWeek1Sun,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.BiWeek1Sun,
- variant: 'short',
- })}
-
-
-PeriodType Month w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Month,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Month,
- variant: 'short',
- })}
-
-
-PeriodType Quarter w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Quarter,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.Quarter,
- variant: 'short',
- })}
-
-
-PeriodType CalendarYear w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.CalendarYear,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.CalendarYear,
- variant: 'short',
- })}
-
-
-PeriodType FiscalYearOctober w/ long (default)
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.FiscalYearOctober,
- })}
-
-
-short
-
-
- {dateDisplay(new Date('1982-03-30T00:00:00'), {
- periodType: PeriodType.FiscalYearOctober,
- variant: 'short',
- })}
-
diff --git a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts b/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts
deleted file mode 100644
index 6d4abe1dd..000000000
--- a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import source from '$lib/utils/dateDisplay.ts?raw';
-import pageSource from './+page.svelte?raw';
-
-export async function load() {
- return {
- meta: {
- source,
- pageSource,
- features: [
- 'Pass `periodType` along with `variant` for quick formatting',
- 'Pass `format` for greater control ',
- 'By default, will be formatted using `date.toLocaleString()`',
- ],
- },
- };
-}
diff --git a/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte b/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte
index e1839e59e..d5379abe3 100644
--- a/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte
+++ b/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte
@@ -1,30 +1,37 @@
Usage
-Examples
-
-Playground
+Playgrounds
+Playground numbers
-
+
+
+ ({ label: value, value }))}
+ />
- ({ label: value, value }))}
- />
-
{format(value, style, { locales, currency })}
+Playground dates
+
+
+
+
+ ({ label: value, value }))}
+ />
+
+
+
+ {format(myDate, PeriodType.Day, { locales })}
+
+
+Numbers
+
number formats (defaut settings)
@@ -83,13 +102,323 @@
{format(0.5678, 'percent', { locales: 'fr', fractionDigits: 1 })}
-Period formats
+Dates
+
+Custom format
+
+
+
+
With random string
+
+ {format(myDate, PeriodType.Custom, {
+ custom: 'eee, MMMM do',
+ })}
+
+
+
+
With descriptive tokens
+
+ {format(myDate, PeriodType.Custom, {
+ custom: [DateToken.DayOfWeek_short, DateToken.Month_long, DateToken.DayOfMonth_withOrdinal],
+ })}
+
+
+
+
With full intl
+
+ {format(myDate, PeriodType.Custom, {
+ custom: { weekday: 'short', month: 'long', day: 'numeric', withOrdinal: true },
+ })}
+
+
+
+
+PeriodType Day
+
+
+
+
short
+
+ {format(myDate, PeriodType.Day, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.Day, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.Day, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType DayTime
+
+
+
+
short
+
+ {format(myDate, PeriodType.DayTime, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.DayTime, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.DayTime, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType TimeOnly
+
+
+
+
short
+
+ {format(myDate, PeriodType.TimeOnly, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.TimeOnly, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.TimeOnly, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType Week
+
+ It will take your default weekStartsOn
+ settings , if you want to be
+ specific, you can also use
+ PeriodType.WeekSun
+
+
+
+
short
+
+ {format(myDate, PeriodType.Week, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.Week, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.Week, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType BiWeek1
+
+ It will take your default weekStartsOn
+ settings , if you want to be
+ specific, you can also use
+ PeriodType.BiWeek1Sun
+
+
+
+
short
+
+ {format(myDate, PeriodType.BiWeek1, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.BiWeek1, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.BiWeek1, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType Month
+
+
+
+
short
+
+ {format(myDate, PeriodType.Month, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.Month, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.Month, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType Quarter
+
+
+
+
short
+
+ {format(myDate, PeriodType.Quarter, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.Quarter, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.Quarter, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType CalendarYear
+
+
+
+
short
+
+ {format(myDate, PeriodType.CalendarYear, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.CalendarYear, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.CalendarYear, {
+ variant: 'long',
+ })}
+
+
+
+
+PeriodType FiscalYearOctober
+
+
+
+
short
+
+ {format(myDate, PeriodType.FiscalYearOctober, {
+ variant: 'short',
+ })}
+
+
+
+
default
+
+ {format(myDate, PeriodType.FiscalYearOctober, {
+ // variant: 'default',
+ })}
+
+
+
+
long
+
+ {format(myDate, PeriodType.FiscalYearOctober, {
+ variant: 'long',
+ })}
+
+
+
+
+Date / Period formats (local settings)
+
+
+ You can customize numbers with the 3rd arg. You can pass for example locales like fr ,
+ de , ... You can also to that globally in the
+ Settings .
+
- {format(date, PeriodType.Day)}
- {format(date, PeriodType.Month)}
- {format(date, PeriodType.CalendarYear)}
- {format(date, PeriodType.Day, 'short')}
- {format(date, PeriodType.Month, 'short')}
- {format(date, PeriodType.CalendarYear, 'short')}
+ {format(myDate, PeriodType.Day, { locales: 'fr' })}
+ {format(myDate, PeriodType.Month, { locales: 'fr' })}
+ {format(myDate, PeriodType.CalendarYear, { locales: 'fr' })}
+ {format(myDate, PeriodType.Day, { variant: 'short', locales: 'fr' })}
+ {format(myDate, PeriodType.Month, { variant: 'short', locales: 'fr' })}
+
+ {format(myDate, PeriodType.CalendarYear, { variant: 'short', locales: 'fr' })}
+
diff --git a/packages/svelte-ux/src/routes/docs/utils/format/+page.ts b/packages/svelte-ux/src/routes/docs/utils/format/+page.ts
index 74db8cf11..7c3cef134 100644
--- a/packages/svelte-ux/src/routes/docs/utils/format/+page.ts
+++ b/packages/svelte-ux/src/routes/docs/utils/format/+page.ts
@@ -6,7 +6,12 @@ export async function load() {
meta: {
source,
pageSource,
- description: 'Easily format numbers and dates to a variety of formats',
+ description: 'Easily format numbers and dates to a variety of formats and locales',
+ features: [
+ 'Number: Pass `style` for quick formatting',
+ 'Date: Pass `periodType` along with `variant` for quick formatting',
+ 'Date: Pass `custom` for greater control',
+ ],
},
};
}
diff --git a/packages/svelte-ux/vite.config.js b/packages/svelte-ux/vite.config.js
index 97ce538e7..578e7fdd5 100644
--- a/packages/svelte-ux/vite.config.js
+++ b/packages/svelte-ux/vite.config.js
@@ -6,5 +6,8 @@ export default defineConfig({
plugins: [sveltekit(), sveld()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
+ coverage: {
+ reporter: ['html'],
+ },
},
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 59820acc7..82d550621 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -66,12 +66,9 @@ importers:
d3-scale:
specifier: ^4.0.2
version: 4.0.2
- d3-time:
- specifier: ^3.1.0
- version: 3.1.0
date-fns:
- specifier: ^2.30.0
- version: 2.30.0
+ specifier: ^3.0.5
+ version: 3.0.5
immer:
specifier: ^10.0.3
version: 10.0.3
@@ -80,7 +77,7 @@ importers:
version: 4.17.21
posthog-js:
specifier: ^1.95.1
- version: 1.95.1
+ version: 1.96.1
prism-svelte:
specifier: ^0.5.0
version: 0.5.0
@@ -127,6 +124,9 @@ importers:
'@types/prismjs':
specifier: ^1.26.3
version: 1.26.3
+ '@vitest/coverage-v8':
+ specifier: ^0.34.6
+ version: 0.34.6(vitest@0.33.0)
autoprefixer:
specifier: ^10.4.16
version: 10.4.16(postcss@8.4.31)
@@ -229,6 +229,10 @@ packages:
dependencies:
regenerator-runtime: 0.14.0
+ /@bcoe/v8-coverage@0.2.3:
+ resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+ dev: true
+
/@changesets/apply-release-plan@6.1.4:
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
dependencies:
@@ -689,6 +693,11 @@ packages:
wrap-ansi-cjs: /wrap-ansi@7.0.0
dev: true
+ /@istanbuljs/schema@0.1.3:
+ resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ engines: {node: '>=8'}
+ dev: true
+
/@jest/schemas@29.6.3:
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -984,6 +993,10 @@ packages:
dependencies:
ci-info: 3.9.0
+ /@types/istanbul-lib-coverage@2.0.6:
+ resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
+ dev: true
+
/@types/lodash-es@4.17.11:
resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==}
dependencies:
@@ -1040,6 +1053,27 @@ packages:
/@types/unist@3.0.2:
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
+ /@vitest/coverage-v8@0.34.6(vitest@0.33.0):
+ resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==}
+ peerDependencies:
+ vitest: '>=0.32.0 <1'
+ dependencies:
+ '@ampproject/remapping': 2.2.1
+ '@bcoe/v8-coverage': 0.2.3
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 4.0.1
+ istanbul-reports: 3.1.6
+ magic-string: 0.30.5
+ picocolors: 1.0.0
+ std-env: 3.4.3
+ test-exclude: 6.0.0
+ v8-to-istanbul: 9.1.3
+ vitest: 0.33.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@vitest/expect@0.33.0:
resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==}
dependencies:
@@ -1450,6 +1484,10 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
+ /convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ dev: true
+
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
@@ -1556,11 +1594,8 @@ packages:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
dev: true
- /date-fns@2.30.0:
- resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
- engines: {node: '>=0.11'}
- dependencies:
- '@babel/runtime': 7.23.2
+ /date-fns@3.0.5:
+ resolution: {integrity: sha512-Q4Tq5c5s/Zl/zbgdWf6pejn9ru7UwdIlLfvEEg1hVsQNQ7LKt76qIduagIT9OPK7+JCv1mAKherdU6bOqGYDnw==}
dev: false
/debug@4.3.4:
@@ -2118,6 +2153,10 @@ packages:
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+ /html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ dev: true
+
/human-id@1.0.2:
resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==}
@@ -2335,6 +2374,39 @@ packages:
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+ dev: true
+
+ /istanbul-lib-source-maps@4.0.1:
+ resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
+ engines: {node: '>=10'}
+ dependencies:
+ debug: 4.3.4
+ istanbul-lib-coverage: 3.2.2
+ source-map: 0.6.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /istanbul-reports@3.1.6:
+ resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==}
+ engines: {node: '>=8'}
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+ dev: true
+
/jackspeak@2.3.6:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
@@ -2490,6 +2562,13 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
+ /make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+ dependencies:
+ semver: 7.5.4
+ dev: true
+
/map-obj@1.0.1:
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
engines: {node: '>=0.10.0'}
@@ -2969,8 +3048,8 @@ packages:
source-map-js: 1.0.2
dev: true
- /posthog-js@1.95.1:
- resolution: {integrity: sha512-79HPLoBqENBEEGFhn+hueKliYH66Qbu4WcRTEd8WaqtvqHrK9qAQkcrShZNkg1V5vM4kHp0iMIkJYBXg1sq06Q==}
+ /posthog-js@1.96.1:
+ resolution: {integrity: sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA==}
dependencies:
fflate: 0.4.8
dev: false
@@ -3357,6 +3436,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/spawndamnit@2.0.0:
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
dependencies:
@@ -3682,6 +3766,15 @@ packages:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'}
+ /test-exclude@6.0.0:
+ resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ engines: {node: '>=8'}
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 7.2.3
+ minimatch: 3.1.2
+ dev: true
+
/thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -3914,6 +4007,15 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /v8-to-istanbul@9.1.3:
+ resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==}
+ engines: {node: '>=10.12.0'}
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.20
+ '@types/istanbul-lib-coverage': 2.0.6
+ convert-source-map: 2.0.0
+ dev: true
+
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies: