Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable spellcheck/spell-checker */
import {
afterEach, describe, expect, it,
} from '@jest/globals';
import config from '@ts/core/m_config';

describe('Global config - dateFormats and dateSerializationFormat', () => {
afterEach(() => {
config({ dateFormats: {}, dateSerializationFormat: '' });
});

describe('dateFormats', () => {
it('should default to empty object', () => {
expect(config().dateFormats).toEqual({});
});

it('should accept string format overrides', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });
expect(config().dateFormats.shortdate).toBe('dd/MM/yyyy');
});

it('should accept function format overrides', () => {
const formatter = (date: Date): string => `${date.getFullYear()}`;
config({ dateFormats: { shortdate: formatter } });
expect(config().dateFormats.shortdate).toBe(formatter);
});

it('should accept multiple format overrides', () => {
config({
dateFormats: {
shortdate: 'dd/MM/yyyy',
shorttime: 'HH:mm',
shortdateshorttime: 'dd/MM/yyyy HH:mm',
},
});

expect(config().dateFormats.shortdate).toBe('dd/MM/yyyy');
expect(config().dateFormats.shorttime).toBe('HH:mm');
expect(config().dateFormats.shortdateshorttime).toBe('dd/MM/yyyy HH:mm');
});

it('should be clearable by setting to empty object', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });
config({ dateFormats: {} });
expect(config().dateFormats).toEqual({});
});
});

describe('dateSerializationFormat', () => {
it('should have falsy default value', () => {
expect(config().dateSerializationFormat || undefined).toBeUndefined();
});

it('should accept a format string', () => {
config({ dateSerializationFormat: 'yyyy-MM-dd' });
expect(config().dateSerializationFormat).toBe('yyyy-MM-dd');
});

it('should accept ISO format with time', () => {
config({ dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss' });
expect(config().dateSerializationFormat).toBe('yyyy-MM-ddTHH:mm:ss');
});

it('should be clearable by setting to empty string', () => {
config({ dateSerializationFormat: 'yyyy-MM-dd' });
config({ dateSerializationFormat: '' });
expect(config().dateSerializationFormat).toBe('');
});
});

describe('coexistence with other config options', () => {
it('should not affect existing config options', () => {
const originalRtl = config().rtlEnabled;
const originalCurrency = config().defaultCurrency;

config({
dateFormats: { shortdate: 'dd/MM/yyyy' },
dateSerializationFormat: 'yyyy-MM-dd',
});

expect(config().rtlEnabled).toBe(originalRtl);
expect(config().defaultCurrency).toBe(originalCurrency);
});

it('should persist alongside other config changes', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });
config({ editorStylingMode: 'outlined' });

expect(config().dateFormats.shortdate).toBe('dd/MM/yyyy');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/* eslint-disable spellcheck/spell-checker */
import {
afterEach, describe, expect, it,
} from '@jest/globals';
import dateLocalization from '@ts/core/localization/date';
import config from '@ts/core/m_config';

describe('Global dateFormats config', () => {
afterEach(() => {
config({ dateFormats: {}, dateSerializationFormat: undefined });
});

describe('format()', () => {
const testDate = new Date(2024, 0, 5, 14, 30, 45);

it('should use default format when no global override is set', () => {
const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBeDefined();
expect(typeof result).toBe('string');
});

it('should apply global string override for shortdate', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBe('05/01/2024');
});

it('should apply global string override for shorttime', () => {
config({ dateFormats: { shorttime: 'HH:mm' } });

const result = dateLocalization.format(testDate, 'shorttime');
expect(result).toBe('14:30');
});

it('should apply global string override for shortdateshorttime', () => {
config({ dateFormats: { shortdateshorttime: 'dd.MM.yyyy HH:mm' } });

const result = dateLocalization.format(testDate, 'shortdateshorttime');
expect(result).toBe('05.01.2024 14:30');
});

it('should apply global string override for longdate', () => {
config({ dateFormats: { longdate: 'dd MMMM yyyy' } });

const result = dateLocalization.format(testDate, 'longdate');
expect(result).toBe('05 January 2024');
});

it('should apply global function override', () => {
const customFormatter = (date: Date): string => `${date.getFullYear()}-custom`;
config({ dateFormats: { shortdate: customFormatter } });

const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBe('2024-custom');
});

it('should support case-insensitive format key lookup', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBe('05/01/2024');
});

it('should support camelCase format keys', () => {
config({ dateFormats: { shortDate: 'dd/MM/yyyy' } });

const result = dateLocalization.format(testDate, 'shortDate');
expect(result).toBe('05/01/2024');
});

it('should not affect non-overridden formats', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const result = dateLocalization.format(testDate, 'day');
expect(result).toBe('5');
});

it('should not affect explicit LDML pattern strings', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const result = dateLocalization.format(testDate, 'yyyy-MM-dd');
expect(result).toBe('2024-01-05');
});

it('should handle empty dateFormats config', () => {
config({ dateFormats: {} });

const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBeDefined();
expect(typeof result).toBe('string');
});

it('should handle multiple format overrides simultaneously', () => {
config({
dateFormats: {
shortdate: 'dd.MM.yyyy',
shorttime: 'HH:mm',
shortdateshorttime: 'dd.MM.yyyy, HH:mm',
},
});

expect(dateLocalization.format(testDate, 'shortdate')).toBe('05.01.2024');
expect(dateLocalization.format(testDate, 'shorttime')).toBe('14:30');
expect(dateLocalization.format(testDate, 'shortdateshorttime')).toBe('05.01.2024, 14:30');
});

it('should allow overriding format back to empty (restoring defaults)', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });
expect(dateLocalization.format(testDate, 'shortdate')).toBe('05/01/2024');

config({ dateFormats: {} });
const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBeDefined();
expect(result).not.toBe('05/01/2024');
});
});

describe('_getPatternByFormat()', () => {
it('should return global override pattern when set', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const pattern = dateLocalization._getPatternByFormat('shortdate');
expect(pattern).toBe('dd/MM/yyyy');
});

it('should return default pattern when no override is set', () => {
const pattern = dateLocalization._getPatternByFormat('shortdate');
expect(pattern).toBeDefined();
});

it('should not return function overrides as patterns', () => {
const customFormatter = (date: Date): string => `${date.getFullYear()}`;
config({ dateFormats: { shortdate: customFormatter } });

const pattern = dateLocalization._getPatternByFormat('shortdate');
expect(typeof pattern).toBe('string');
});
});

describe('parse()', () => {
it('should parse dates using global format override', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const parsed = dateLocalization.parse('05/01/2024', 'shortdate') as Date;
expect(parsed).toBeInstanceOf(Date);
expect(parsed.getFullYear()).toBe(2024);
expect(parsed.getMonth()).toBe(0);
expect(parsed.getDate()).toBe(5);
});

it('should parse dates with overridden shortdateshorttime', () => {
config({ dateFormats: { shortdateshorttime: 'dd.MM.yyyy HH:mm' } });

const parsed = dateLocalization.parse('05.01.2024 14:30', 'shortdateshorttime') as Date;
expect(parsed).toBeInstanceOf(Date);
expect(parsed.getFullYear()).toBe(2024);
expect(parsed.getMonth()).toBe(0);
expect(parsed.getDate()).toBe(5);
expect(parsed.getHours()).toBe(14);
expect(parsed.getMinutes()).toBe(30);
});

it('should parse with default format when no override is set', () => {
const parsed = dateLocalization.parse('1/5/2024', 'shortdate');
expect(parsed).toBeInstanceOf(Date);
});
});

describe('getFormatParts()', () => {
it('should return parts based on global override pattern', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const parts = dateLocalization.getFormatParts('shortdate');
expect(parts).toContain('day');
expect(parts).toContain('month');
expect(parts).toContain('year');
});
});

describe('edge cases', () => {
it('should handle undefined dateFormats gracefully', () => {
config({ dateFormats: undefined });

const testDate = new Date(2024, 0, 5);
const result = dateLocalization.format(testDate, 'shortdate');
expect(result).toBeDefined();
});

it('should handle format with FormatObject type having global override', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const testDate = new Date(2024, 0, 5);
const result = dateLocalization.format(testDate, { type: 'shortdate' });
expect(result).toBe('05/01/2024');
});

it('should not interfere with custom formatter in FormatObject', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const testDate = new Date(2024, 0, 5);
const customFormatter = (date: Date): string => `custom:${date.getFullYear()}`;
const result = dateLocalization.format(testDate, { formatter: customFormatter });
expect(result).toBe('custom:2024');
});

it('should not interfere with function format argument', () => {
config({ dateFormats: { shortdate: 'dd/MM/yyyy' } });

const testDate = new Date(2024, 0, 5);
const result = dateLocalization.format(testDate, (date: Date) => `fn:${date.getFullYear()}`);
expect(result).toBe('fn:2024');
});
});
});
38 changes: 34 additions & 4 deletions packages/devextreme/js/__internal/core/localization/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getFormat as getLDMLDateFormat } from '@ts/core/localization/ldml/date.
import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml/date.formatter';
import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser';
import numberLocalization from '@ts/core/localization/number';
import config from '@ts/core/m_config';
import errors from '@ts/core/m_errors';
import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector';
import { each } from '@ts/core/utils/m_iterator';
Expand Down Expand Up @@ -55,11 +56,27 @@ const possiblePartPatterns = {
milliseconds: ['S', 'SS', 'SSS'],
};

const getGlobalDateFormatOverride = (
format: string,
): string | ((value: Date) => string) | undefined => {
const globalDateFormats = config().dateFormats;
if (!globalDateFormats) {
return undefined;
}
const lowerFormat = format.toLowerCase();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return globalDateFormats[format] ?? globalDateFormats[lowerFormat];
};

const dateLocalization = dependencyInjector({
engine(): string {
return 'base';
},
_getPatternByFormat(format: string): string | undefined {
const globalOverride = getGlobalDateFormatOverride(format);
if (typeof globalOverride === 'string') {
return globalOverride;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return FORMATS_TO_PATTERN_MAP[format.toLowerCase()];
},
Expand Down Expand Up @@ -139,8 +156,14 @@ const dateLocalization = dependencyInjector({
// eslint-disable-next-line no-param-reassign
format = (format as FormatObject).type ?? format;
if (isString(format)) {
const globalOverride = getGlobalDateFormatOverride(format as string);
if (typeof globalOverride === 'function') {
return globalOverride(date);
}
// eslint-disable-next-line no-param-reassign
format = (FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] || format) as string;
format = (
globalOverride ?? FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] ?? format
) as string;

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return numberLocalization.convertDigits(getLDMLDateFormatter(format, this)(date));
Expand Down Expand Up @@ -176,9 +199,16 @@ const dateLocalization = dependencyInjector({
return (format.parser as DateParser)(text);
}

if (typeof format === 'string' && !FORMATS_TO_PATTERN_MAP[format.toLowerCase()]) {
ldmlFormat = format;
} else {
if (typeof format === 'string') {
const globalOverride = getGlobalDateFormatOverride(format);
if (typeof globalOverride === 'string') {
ldmlFormat = globalOverride;
} else if (!FORMATS_TO_PATTERN_MAP[format.toLowerCase()] && !globalOverride) {
ldmlFormat = format;
}
}

if (!ldmlFormat) {
formatter = (value: Date): string => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const text: string = that.format(value, format);
Expand Down
Loading
Loading