Skip to content

Commit b5bcacb

Browse files
committed
add dateTimePreset, tests and more minor changes
1 parent f0033b2 commit b5bcacb

10 files changed

Lines changed: 821 additions & 13 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it,
3+
} from '@jest/globals';
4+
import dateLocalization from '@js/common/core/localization/date';
5+
import config from '@js/core/config';
6+
7+
const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const;
8+
9+
const saveAndRestore = (): { save: () => void; restore: () => void } => {
10+
let savedValues: Record<string, unknown> = {};
11+
12+
return {
13+
save() {
14+
const currentConfig = config();
15+
16+
savedValues = {};
17+
GLOBAL_FORMAT_KEYS.forEach((key) => {
18+
savedValues[key] = currentConfig[key];
19+
});
20+
},
21+
restore() {
22+
const currentConfig = config();
23+
24+
GLOBAL_FORMAT_KEYS.forEach((key) => {
25+
if (savedValues[key] === undefined) {
26+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
27+
delete currentConfig[key];
28+
} else {
29+
currentConfig[key] = savedValues[key];
30+
}
31+
});
32+
},
33+
};
34+
};
35+
36+
describe('date localization - dateTimeFormatPresets', () => {
37+
const { save, restore } = saveAndRestore();
38+
39+
beforeEach(() => { save(); });
40+
afterEach(() => { restore(); });
41+
42+
describe('string preset override', () => {
43+
it('should override shortDate with custom LDML pattern', () => {
44+
config({
45+
...config(),
46+
dateTimeFormatPresets: {
47+
shortDate: 'dd/MM/yyyy',
48+
},
49+
});
50+
51+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
52+
53+
expect(result).toBe('02/01/2020');
54+
});
55+
56+
it('should override shortTime with custom LDML pattern', () => {
57+
config({
58+
...config(),
59+
dateTimeFormatPresets: {
60+
shortTime: 'HH:mm:ss',
61+
},
62+
});
63+
64+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5, 30), 'shortTime');
65+
66+
expect(result).toBe('14:05:30');
67+
});
68+
69+
it('should override longDate with custom LDML pattern', () => {
70+
config({
71+
...config(),
72+
dateTimeFormatPresets: {
73+
longDate: 'dd MMMM yyyy',
74+
},
75+
});
76+
77+
const result = dateLocalization.format(new Date(2020, 0, 2), 'longDate');
78+
79+
expect(result).toBe('02 January 2020');
80+
});
81+
82+
it('should override shortDateShortTime with custom LDML pattern', () => {
83+
config({
84+
...config(),
85+
dateTimeFormatPresets: {
86+
shortDateShortTime: 'dd/MM/yyyy HH:mm',
87+
},
88+
});
89+
90+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortDateShortTime');
91+
92+
expect(result).toBe('02/01/2020 14:05');
93+
});
94+
});
95+
96+
describe('function preset override', () => {
97+
it('should use function override for shortDate', () => {
98+
config({
99+
...config(),
100+
dateTimeFormatPresets: {
101+
shortDate: (d: Date) => `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`,
102+
},
103+
});
104+
105+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
106+
107+
expect(result).toBe('2-1-2020');
108+
});
109+
110+
it('should use function override for shortTime', () => {
111+
config({
112+
...config(),
113+
dateTimeFormatPresets: {
114+
shortTime: (d: Date) => `${d.getHours()}h${String(d.getMinutes()).padStart(2, '0')}`,
115+
},
116+
});
117+
118+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortTime');
119+
120+
expect(result).toBe('14h05');
121+
});
122+
});
123+
124+
describe('case insensitivity', () => {
125+
it('should apply override regardless of case in format name', () => {
126+
config({
127+
...config(),
128+
dateTimeFormatPresets: {
129+
shortDate: 'dd/MM/yyyy',
130+
},
131+
});
132+
133+
const date = new Date(2020, 0, 2);
134+
135+
expect(dateLocalization.format(date, 'shortdate')).toBe('02/01/2020');
136+
expect(dateLocalization.format(date, 'SHORTDATE')).toBe('02/01/2020');
137+
expect(dateLocalization.format(date, 'ShortDate')).toBe('02/01/2020');
138+
});
139+
});
140+
141+
describe('locale map in preset', () => {
142+
it('should resolve preset with default locale', () => {
143+
config({
144+
...config(),
145+
dateTimeFormatPresets: {
146+
shortDate: {
147+
default: 'dd/MM/yyyy',
148+
'de-DE': 'dd.MM.yyyy',
149+
},
150+
},
151+
});
152+
153+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
154+
155+
expect(result).toBe('02/01/2020');
156+
});
157+
});
158+
159+
describe('no override', () => {
160+
it('should use built-in format when no preset override is configured', () => {
161+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
162+
163+
// Built-in Intl format for en locale
164+
expect(result).toBeTruthy();
165+
expect(typeof result).toBe('string');
166+
});
167+
168+
it('should leave non-preset string formats unaffected', () => {
169+
config({
170+
...config(),
171+
dateTimeFormatPresets: {
172+
shortDate: 'dd/MM/yyyy',
173+
},
174+
});
175+
176+
const result = dateLocalization.format(new Date(2020, 0, 2), 'yyyy-MM-dd');
177+
178+
// LDML pattern should be used directly, not affected by preset overrides
179+
expect(result).toBe('2020-01-02');
180+
});
181+
182+
it('should leave FormatObject formats unaffected', () => {
183+
config({
184+
...config(),
185+
dateTimeFormatPresets: {
186+
shortDate: 'dd/MM/yyyy',
187+
},
188+
});
189+
190+
const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`;
191+
const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter });
192+
193+
expect(result).toBe('custom:2020');
194+
});
195+
196+
it('should not affect formatting when dateTimeFormatPresets is empty', () => {
197+
config({
198+
...config(),
199+
dateTimeFormatPresets: {},
200+
});
201+
202+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
203+
204+
expect(result).toBeTruthy();
205+
expect(typeof result).toBe('string');
206+
});
207+
});
208+
209+
describe('unknown preset key', () => {
210+
it('should safely ignore unknown preset keys', () => {
211+
config({
212+
...config(),
213+
dateTimeFormatPresets: {
214+
unknownFormat: 'dd/MM/yyyy',
215+
},
216+
});
217+
218+
// Known presets should still work normally
219+
const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
220+
221+
expect(result).toBeTruthy();
222+
expect(typeof result).toBe('string');
223+
});
224+
});
225+
226+
describe('preset override aliases another preset', () => {
227+
it('should support aliasing one preset to another', () => {
228+
config({
229+
...config(),
230+
dateTimeFormatPresets: {
231+
shortDate: 'longDate',
232+
},
233+
});
234+
235+
const dateLong = dateLocalization.format(new Date(2020, 0, 2), 'longDate');
236+
const dateShort = dateLocalization.format(new Date(2020, 0, 2), 'shortDate');
237+
238+
// shortDate should now format like longDate
239+
expect(dateShort).toBe(dateLong);
240+
});
241+
});
242+
});
243+
244+
describe('date localization - global *Format precedence', () => {
245+
const { save, restore } = saveAndRestore();
246+
247+
beforeEach(() => { save(); });
248+
afterEach(() => { restore(); });
249+
250+
it('should apply dateFormat for direct calls with the resolved format', () => {
251+
config({
252+
...config(),
253+
dateFormat: 'dd/MM/yyyy',
254+
});
255+
256+
const result = dateLocalization.format(new Date(2020, 0, 2), config().dateFormat);
257+
258+
expect(result).toBe('02/01/2020');
259+
});
260+
261+
it('should apply dateTimeFormat for direct calls with the resolved format', () => {
262+
config({
263+
...config(),
264+
dateTimeFormat: 'dd/MM/yyyy, HH:mm',
265+
});
266+
267+
const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat);
268+
269+
expect(result).toBe('02/01/2020, 14:05');
270+
});
271+
});

packages/devextreme/js/__internal/core/localization/date.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml
99
import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser';
1010
import numberLocalization from '@ts/core/localization/number';
1111
import errors from '@ts/core/m_errors';
12+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
1213
import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector';
1314
import { each } from '@ts/core/utils/m_iterator';
1415
import { isString } from '@ts/core/utils/m_type';
@@ -67,6 +68,31 @@ const dateLocalization = dependencyInjector({
6768
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
6869
return this._getPatternByFormat(pattern) || pattern;
6970
},
71+
_resolveStringFormat(
72+
format: string,
73+
date: Date,
74+
): string | undefined {
75+
const presetOverride = resolvePresetOverride(format);
76+
77+
if (presetOverride === undefined) {
78+
return undefined;
79+
}
80+
if (typeof presetOverride === 'function') {
81+
return (presetOverride as DateFormatter)(date);
82+
}
83+
if (isString(presetOverride)) {
84+
const pattern = FORMATS_TO_PATTERN_MAP[
85+
(presetOverride as string).toLowerCase()
86+
] || presetOverride as string;
87+
88+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
89+
return numberLocalization.convertDigits(
90+
getLDMLDateFormatter(pattern, this)(date),
91+
);
92+
}
93+
94+
return undefined;
95+
},
7096
formatUsesMonthName(format: string): boolean {
7197
return this._expandPattern(format).indexOf('MMMM') !== -1;
7298
},
@@ -139,6 +165,13 @@ const dateLocalization = dependencyInjector({
139165
// eslint-disable-next-line no-param-reassign
140166
format = (format as FormatObject).type ?? format;
141167
if (isString(format)) {
168+
const resolvedFormat = this._resolveStringFormat(format as string, date);
169+
170+
if (resolvedFormat !== undefined) {
171+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
172+
return resolvedFormat;
173+
}
174+
142175
// eslint-disable-next-line no-param-reassign
143176
format = (FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] || format) as string;
144177

packages/devextreme/js/__internal/core/localization/globalize/date.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'globalize/date';
66
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
77
import type { DateFormatter, DateParser, Format } from '@ts/core/localization/date';
88
import dateLocalization from '@ts/core/localization/date';
9+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
910
import * as iteratorUtils from '@ts/core/utils/m_iterator';
1011
import { isObject } from '@ts/core/utils/m_type';
1112
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -186,6 +187,23 @@ if (Globalize?.formatDate) {
186187
format = (format as FormatObject).type ?? format;
187188

188189
if (typeof format === 'string') {
190+
const presetOverride = resolvePresetOverride(format);
191+
192+
if (presetOverride !== undefined) {
193+
if (typeof presetOverride === 'function') {
194+
return (presetOverride as DateFormatter)(date);
195+
}
196+
if (typeof presetOverride === 'string') {
197+
// eslint-disable-next-line no-param-reassign
198+
format = presetOverride;
199+
} else if (isObject(presetOverride) && this._isAcceptableFormat(presetOverride)) {
200+
formatter = Globalize.dateFormatter(presetOverride);
201+
202+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
203+
return this.removeRtlMarks(formatter(date));
204+
}
205+
}
206+
189207
formatCacheKey = `${Globalize.locale().locale}:${format}`;
190208
formatter = formattersCache[formatCacheKey];
191209
if (!formatter) {

packages/devextreme/js/__internal/core/localization/intl/date.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { Format as LocalizationFormat, FormatObject } from '@js/localization';
33
import localizationCoreUtils from '@ts/core/localization/core';
44
import type { DateFormatter, Format } from '@ts/core/localization/date';
5+
import { resolvePresetOverride } from '@ts/core/m_global_format_config';
56
import { extend } from '@ts/core/utils/m_extend';
67

78
interface DateArgs {
@@ -237,6 +238,19 @@ export default {
237238
// eslint-disable-next-line no-param-reassign
238239
format = (format as FormatObject).type ?? format;
239240
}
241+
242+
if (typeof format === 'string') {
243+
const presetOverride = resolvePresetOverride(format);
244+
245+
if (presetOverride !== undefined) {
246+
if (typeof presetOverride === 'function') {
247+
return (presetOverride as DateFormatter)(date);
248+
}
249+
// eslint-disable-next-line no-param-reassign
250+
format = presetOverride as LocalizationFormat;
251+
}
252+
}
253+
240254
const intlFormat = getIntlFormat(format);
241255

242256
if (intlFormat) {

0 commit comments

Comments
 (0)