Skip to content

Commit 106084e

Browse files
MayaKirovaMKirovaskrustev
authored
fix(date-editor): Use default display format based on locale. (#16237)
* fix(date-editor): Use default display format based on locale. * chore(*): Replace angular's format date with the new core lib one. * chore(*): Use default locale from core when resolving default input format. * chore(*): Pass specific input options to intl formatter in core. * chore(*): Remove unused functions in util. * chore(*): Pass format options based on dataType. * chore(*): Update formats in tests to match Intl. * refactor(localization): Update Intl use to latest changes from localization. --------- Co-authored-by: MKirova <MKirova@DEV-MKIROVA> Co-authored-by: skrustev <[email protected]>
1 parent c376383 commit 106084e

File tree

5 files changed

+46
-249
lines changed

5 files changed

+46
-249
lines changed

projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-t
33
import { DataType } from '../../data-operations/data-util';
44
import { registerLocaleData } from '@angular/common';
55
import localeBg from "@angular/common/locales/bg";
6+
import { BaseFormatter } from '../../core/i18n/formatters/formatter-base';
67

78
const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => {
89
obj[x.type] = x;
@@ -239,7 +240,7 @@ describe(`DateTimeUtil Unit tests`, () => {
239240

240241
it('should properly build input formats based on locale for dateTime data type ', () => {
241242
let result = DateTimeUtil.getDefaultInputFormat('en-US', DataType.DateTime);
242-
expect(result.normalize('NFKC')).toEqual('MM/dd/yyyy, hh:mm:ss tt');
243+
expect(result.normalize('NFKC')).toEqual('MM/dd/yyyy, hh:mm:ss a');
243244

244245
result = DateTimeUtil.getDefaultInputFormat('bg-BG', DataType.DateTime);
245246
expect(result.normalize('NFKC')).toEqual('dd.MM.yyyy г., HH:mm:ss');
@@ -250,7 +251,7 @@ describe(`DateTimeUtil Unit tests`, () => {
250251

251252
it('should properly build input formats based on locale for time data type ', () => {
252253
let result = DateTimeUtil.getDefaultInputFormat('en-US', DataType.Time);
253-
expect(result.normalize('NFKC')).toEqual('hh:mm tt');
254+
expect(result.normalize('NFKC')).toEqual('hh:mm a');
254255

255256
result = DateTimeUtil.getDefaultInputFormat('bg-BG', DataType.Time);
256257
expect(result.normalize('NFKC')).toEqual('HH:mm');
@@ -658,6 +659,7 @@ describe(`DateTimeUtil Unit tests`, () => {
658659
it('should correctly identify formats that would resolve to only numeric parts (and period) for the date/time parts', () => {
659660
// test with locale covering non-ASCII characters as well
660661
const locale = 'bg';
662+
const angularFormatter = new BaseFormatter;
661663

662664
const numericFormats = ['y', 'yy', 'yyy', 'yyyy', 'M', 'MM', 'd', 'dd', 'h', 'hh',
663665
'H', 'HH', 'm', 'mm', 's', 'ss', 'S', 'SS', 'SSS',
@@ -666,47 +668,48 @@ describe(`DateTimeUtil Unit tests`, () => {
666668
'dd/MM/yyyy test hh:mm'
667669
];
668670
numericFormats.forEach(format => {
669-
expect(DateTimeUtil.isFormatNumeric(locale, format)).withContext(`Format: ${format}`).toBeTrue();
671+
expect(DateTimeUtil.isFormatNumeric(locale, format, angularFormatter)).withContext(`Format: ${format}`).toBeTrue();
670672
});
671673

672674
const nonNumericFormats = ['MMM', 'MMMM', 'MMMMM', 'medium', 'long', 'full', 'mediumDate',
673675
'longDate', 'fullDate', 'longTime', 'fullTime', 'dd-MMM-yyyy', 'E', 'EE'];
674676

675677
nonNumericFormats.forEach(format => {
676-
expect(DateTimeUtil.isFormatNumeric(locale, format)).withContext(`Format: ${format}`).toBeFalse();
678+
expect(DateTimeUtil.isFormatNumeric(locale, format, angularFormatter)).withContext(`Format: ${format}`).toBeFalse();
677679
});
678680
});
679681

680682
it('getNumericInputFormat should return formats with date parts that the date-time editors can handle', () => {
681683
let locale = 'en-US';
684+
const angularFormatter = new BaseFormatter;
682685

683686
// returns the equivalent of the predefined numeric formats as date parts
684687
// should be transformed as inputFormats for editing (numeric year, 2-digit parts for the rest)
685-
expect(DateTimeUtil.getNumericInputFormat(locale, 'short')).toBe('MM/dd/yyyy, hh:mm tt');
686-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate')).toBe('MM/dd/yyyy');
687-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime').normalize('NFKD')).toBe('hh:mm tt');
688-
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime').normalize('NFKD')).toBe('hh:mm:ss tt');
688+
expect(DateTimeUtil.getNumericInputFormat(locale, 'short', angularFormatter)).toBe('MM/dd/yyyy, hh:mm tt');
689+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate', angularFormatter)).toBe('MM/dd/yyyy');
690+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime', angularFormatter).normalize('NFKD')).toBe('hh:mm tt');
691+
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime', angularFormatter).normalize('NFKD')).toBe('hh:mm:ss tt');
689692

690693
// handle the predefined formats for different locales
691694
locale = 'bg-BG';
692-
expect(DateTimeUtil.getNumericInputFormat(locale, 'short').normalize('NFKD')).toBe('dd.MM.yyyy г., HH:mm');
693-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate').normalize('NFKD')).toBe('dd.MM.yyyy г.');
694-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime').normalize('NFKD')).toBe('HH:mm');
695-
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime').normalize('NFKD')).toBe('HH:mm:ss');
695+
expect(DateTimeUtil.getNumericInputFormat(locale, 'short', angularFormatter).normalize('NFKD')).toBe('dd.MM.yyyy г., HH:mm');
696+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate', angularFormatter).normalize('NFKD')).toBe('dd.MM.yyyy г.');
697+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime', angularFormatter).normalize('NFKD')).toBe('HH:mm');
698+
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime', angularFormatter).normalize('NFKD')).toBe('HH:mm:ss');
696699

697700
locale = 'ja-JP';
698-
expect(DateTimeUtil.getNumericInputFormat(locale, 'short')).toBe('yyyy/MM/dd HH:mm');
699-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate')).toBe('yyyy/MM/dd');
700-
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime').normalize('NFKD')).toBe('HH:mm');
701-
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime').normalize('NFKD')).toBe('HH:mm:ss');
701+
expect(DateTimeUtil.getNumericInputFormat(locale, 'short', angularFormatter)).toBe('yyyy/MM/dd HH:mm');
702+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate', angularFormatter)).toBe('yyyy/MM/dd');
703+
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime', angularFormatter).normalize('NFKD')).toBe('HH:mm');
704+
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime', angularFormatter).normalize('NFKD')).toBe('HH:mm:ss');
702705

703706
// returns the same format if it is custom and numeric
704-
expect(DateTimeUtil.getNumericInputFormat(locale, 'dd-MM-yyyy')).toBe('dd-MM-yyyy');
705-
expect(DateTimeUtil.getNumericInputFormat(locale, 'dd/M/yyyy hh:mm:ss:SS aa')).toBe('dd/M/yyyy hh:mm:ss:SS aa');
707+
expect(DateTimeUtil.getNumericInputFormat(locale, 'dd-MM-yyyy', angularFormatter)).toBe('dd-MM-yyyy');
708+
expect(DateTimeUtil.getNumericInputFormat(locale, 'dd/M/yyyy hh:mm:ss:SS aa', angularFormatter)).toBe('dd/M/yyyy hh:mm:ss:SS aa');
706709

707710
// returns empty string if predefined and not among the numeric ones
708-
expect(DateTimeUtil.getNumericInputFormat(locale, 'medium')).toBe('');
709-
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumDate')).toBe('');
710-
expect(DateTimeUtil.getNumericInputFormat(locale, 'longTime')).toBe('');
711+
expect(DateTimeUtil.getNumericInputFormat(locale, 'medium', angularFormatter)).toBe('');
712+
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumDate', angularFormatter)).toBe('');
713+
expect(DateTimeUtil.getNumericInputFormat(locale, 'longTime', angularFormatter)).toBe('');
711714
});
712715
});

projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts

Lines changed: 9 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-time-editor.common';
2-
import { formatDate, FormatWidth, getLocaleDateFormat } from '@angular/common';
32
import { ValidationErrors } from '@angular/forms';
43
import { isDate } from '../../core/utils';
54
import { DataType } from '../../data-operations/data-util';
5+
import { getDateFormatter } from 'igniteui-i18n-core';
6+
import { BaseFormatter } from '../../core/i18n/formatters/formatter-base';
67

78
/** @hidden */
89
const enum FormatDesc {
@@ -250,55 +251,7 @@ export abstract class DateTimeUtil {
250251
/** Builds a date-time editor's default input format based on provided locale settings and data type. */
251252
public static getDefaultInputFormat(locale: string, dataType: DataType = DataType.Date): string {
252253
locale = locale || DateTimeUtil.DEFAULT_LOCALE;
253-
if (!Intl || !Intl.DateTimeFormat || !Intl.DateTimeFormat.prototype.formatToParts) {
254-
// TODO: fallback with Intl.format for IE?
255-
return DateTimeUtil.DEFAULT_INPUT_FORMAT;
256-
}
257-
const parts = DateTimeUtil.getDefaultLocaleMask(locale, dataType);
258-
parts.forEach(p => {
259-
if (p.type !== DatePart.Year && p.type !== DateTimeUtil.SEPARATOR && p.type !== DatePart.AmPm) {
260-
p.formatType = FormatDesc.TwoDigits;
261-
}
262-
});
263-
264-
return DateTimeUtil.getMask(parts);
265-
}
266-
267-
/** Tries to format a date using Angular's DatePipe. Fallbacks to `Intl` if no locale settings have been loaded. */
268-
public static formatDate(value: number | Date, format: string, locale: string, timezone?: string): string {
269-
let formattedDate: string;
270-
try {
271-
formattedDate = formatDate(value, format, locale, timezone);
272-
} catch {
273-
DateTimeUtil.logMissingLocaleSettings(locale);
274-
const formatter = new Intl.DateTimeFormat(locale);
275-
formattedDate = formatter.format(value);
276-
}
277-
278-
return formattedDate;
279-
}
280-
281-
/**
282-
* Returns the date format based on a provided locale.
283-
* Supports Angular's DatePipe format options such as `shortDate`, `longDate`.
284-
*/
285-
public static getLocaleDateFormat(locale: string, displayFormat?: string): string {
286-
const formatKeys = Object.keys(FormatWidth) as (keyof FormatWidth)[];
287-
const targetKey = formatKeys.find(k => k.toLowerCase() === displayFormat?.toLowerCase().replace('date', ''));
288-
if (!targetKey) {
289-
// if displayFormat is not shortDate, longDate, etc.
290-
// or if it is not set by the user
291-
return displayFormat;
292-
}
293-
let format: string;
294-
try {
295-
format = getLocaleDateFormat(locale, FormatWidth[targetKey]);
296-
} catch {
297-
DateTimeUtil.logMissingLocaleSettings(locale);
298-
format = DateTimeUtil.getDefaultInputFormat(locale);
299-
}
300-
301-
return format;
254+
return getDateFormatter().getLocaleDateTimeFormat(locale, true, DateTimeUtil.getFormatOptions(dataType));
302255
}
303256

304257
/** Determines if a given character is `d/M/y` or `h/m/s`. */
@@ -529,7 +482,7 @@ export abstract class DateTimeUtil {
529482
return false;
530483
}
531484

532-
public static isFormatNumeric(locale: string, inputFormat: string): boolean {
485+
public static isFormatNumeric(locale: string, inputFormat: string, formatter: BaseFormatter): boolean {
533486
const dateParts = DateTimeUtil.parseDateTimeFormat(inputFormat);
534487
if (predefinedNonNumericFormats.has(inputFormat) || dateParts.every(p => p.type === DatePart.Literal)) {
535488
return false;
@@ -538,7 +491,7 @@ export abstract class DateTimeUtil {
538491
if (dateParts[i].type === DatePart.AmPm || dateParts[i].type === DatePart.Literal) {
539492
continue;
540493
}
541-
const transformedValue = formatDate(new Date(), dateParts[i].format, locale);
494+
const transformedValue = formatter.formatDate(new Date(), dateParts[i].format, locale);
542495
// check if the transformed date/time part contains any kind of letter from any language
543496
if (/\p{L}+/gu.test(transformedValue)) {
544497
return false;
@@ -554,15 +507,15 @@ export abstract class DateTimeUtil {
554507
* for the corresponding numeric date parts
555508
* - otherwise, return an empty string
556509
*/
557-
public static getNumericInputFormat(locale: string, format: string): string {
510+
public static getNumericInputFormat(locale: string, format: string, formatter: BaseFormatter): string {
558511
let resultFormat = '';
559512
if (!format) {
560513
return resultFormat;
561514
}
562515
if (predefinedNumericFormats.has(format)) {
563516
resultFormat = DateTimeUtil.getLocaleInputFormatFromParts(locale, predefinedNumericFormats.get(format));
564517

565-
} else if (DateTimeUtil.isFormatNumeric(locale, format)) {
518+
} else if (DateTimeUtil.isFormatNumeric(locale, format, formatter)) {
566519
resultFormat = format;
567520
}
568521
return resultFormat;
@@ -578,10 +531,8 @@ export abstract class DateTimeUtil {
578531
options[p] = FormatDesc.TwoDigits;
579532
}
580533
});
581-
const formatter = new Intl.DateTimeFormat(locale, options);
582-
const dateStruct = DateTimeUtil.getDateStructFromParts(formatter.formatToParts(new Date()), formatter);
583-
DateTimeUtil.fillDatePartsPositions(dateStruct);
584-
return DateTimeUtil.getMask(dateStruct);
534+
535+
return getDateFormatter().getLocaleDateTimeFormat(locale, true, options);
585536
}
586537

587538
private static addCurrentPart(currentPart: DatePartInfo, dateTimeParts: DatePartInfo[]): void {
@@ -599,70 +550,6 @@ export abstract class DateTimeUtil {
599550
return result;
600551
}
601552

602-
private static getMask(dateStruct: any[]): string {
603-
const mask = [];
604-
for (const part of dateStruct) {
605-
if (part.formatType === FormatDesc.Numeric) {
606-
switch (part.type) {
607-
case DateParts.Day:
608-
mask.push('d');
609-
break;
610-
case DateParts.Month:
611-
mask.push('M');
612-
break;
613-
case DateParts.Year:
614-
mask.push('yyyy');
615-
break;
616-
case DateParts.Hour:
617-
mask.push(part.hour12 ? 'h' : 'H');
618-
break;
619-
case DateParts.Minute:
620-
mask.push('m');
621-
break;
622-
case DateParts.Second:
623-
mask.push('s');
624-
break;
625-
}
626-
} else if (part.formatType === FormatDesc.TwoDigits) {
627-
switch (part.type) {
628-
case DateParts.Day:
629-
mask.push('dd');
630-
break;
631-
case DateParts.Month:
632-
mask.push('MM');
633-
break;
634-
case DateParts.Year:
635-
mask.push('yy');
636-
break;
637-
case DateParts.Hour:
638-
mask.push(part.hour12 ? 'hh' : 'HH');
639-
break;
640-
case DateParts.Minute:
641-
mask.push('mm');
642-
break;
643-
case DateParts.Second:
644-
mask.push('ss');
645-
break;
646-
}
647-
}
648-
649-
if (part.type === DateParts.AmPm) {
650-
mask.push('tt');
651-
}
652-
653-
if (part.type === DateTimeUtil.SEPARATOR) {
654-
mask.push(part.value);
655-
}
656-
}
657-
658-
return mask.join('');
659-
}
660-
661-
private static logMissingLocaleSettings(locale: string): void {
662-
console.warn(`Missing locale data for the locale ${locale}. Please refer to https://angular.io/guide/i18n#i18n-pipes`);
663-
console.warn('Using default browser locale settings.');
664-
}
665-
666553
private static prependValue(value: number, partLength: number, prependChar: string): string {
667554
return (prependChar + value.toString()).slice(-partLength);
668555
}
@@ -752,102 +639,4 @@ export abstract class DateTimeUtil {
752639
return { };
753640
}
754641
}
755-
756-
private static getDefaultLocaleMask(locale: string, dataType: DataType = DataType.Date) {
757-
const options = DateTimeUtil.getFormatOptions(dataType);
758-
const formatter = new Intl.DateTimeFormat(locale, options);
759-
const formatToParts = formatter.formatToParts(new Date());
760-
const dateStruct = DateTimeUtil.getDateStructFromParts(formatToParts, formatter);
761-
DateTimeUtil.fillDatePartsPositions(dateStruct);
762-
return dateStruct;
763-
}
764-
765-
private static getDateStructFromParts(parts: Intl.DateTimeFormatPart[], formatter: Intl.DateTimeFormat): any[] {
766-
const dateStruct = [];
767-
for (const part of parts) {
768-
if (part.type === DateTimeUtil.SEPARATOR) {
769-
dateStruct.push({
770-
type: DateTimeUtil.SEPARATOR,
771-
value: part.value
772-
});
773-
} else {
774-
dateStruct.push({
775-
type: part.type
776-
});
777-
}
778-
}
779-
const formatterOptions = formatter.resolvedOptions();
780-
for (const part of dateStruct) {
781-
switch (part.type) {
782-
case DateParts.Day: {
783-
part.formatType = formatterOptions.day;
784-
break;
785-
}
786-
case DateParts.Month: {
787-
part.formatType = formatterOptions.month;
788-
break;
789-
}
790-
case DateParts.Year: {
791-
part.formatType = formatterOptions.year;
792-
break;
793-
}
794-
case DateParts.Hour: {
795-
part.formatType = formatterOptions.hour;
796-
if (formatterOptions.hour12) {
797-
part.hour12 = true;
798-
}
799-
break;
800-
}
801-
case DateParts.Minute: {
802-
part.formatType = formatterOptions.minute;
803-
break;
804-
}
805-
case DateParts.Second: {
806-
part.formatType = formatterOptions.second;
807-
break;
808-
}
809-
case DateParts.AmPm: {
810-
part.formatType = formatterOptions.dayPeriod;
811-
break;
812-
}
813-
}
814-
}
815-
return dateStruct;
816-
}
817-
818-
private static fillDatePartsPositions(dateArray: any[]): void {
819-
let currentPos = 0;
820-
821-
for (const part of dateArray) {
822-
// Day|Month|Hour|Minute|Second|AmPm part positions
823-
if (part.type === DateParts.Day || part.type === DateParts.Month ||
824-
part.type === DateParts.Hour || part.type === DateParts.Minute || part.type === DateParts.Second ||
825-
part.type === DateParts.AmPm
826-
) {
827-
// Offset 2 positions for number
828-
part.position = [currentPos, currentPos + 2];
829-
currentPos += 2;
830-
} else if (part.type === DateParts.Year) {
831-
// Year part positions
832-
switch (part.formatType) {
833-
case FormatDesc.Numeric: {
834-
// Offset 4 positions for full year
835-
part.position = [currentPos, currentPos + 4];
836-
currentPos += 4;
837-
break;
838-
}
839-
case FormatDesc.TwoDigits: {
840-
// Offset 2 positions for short year
841-
part.position = [currentPos, currentPos + 2];
842-
currentPos += 2;
843-
break;
844-
}
845-
}
846-
} else if (part.type === DateTimeUtil.SEPARATOR) {
847-
// Separator positions
848-
part.position = [currentPos, currentPos + 1];
849-
currentPos++;
850-
}
851-
}
852-
}
853642
}

0 commit comments

Comments
 (0)