Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-t
import { DataType } from '../../data-operations/data-util';
import { registerLocaleData } from '@angular/common';
import localeBg from "@angular/common/locales/bg";
import { BaseFormatter } from '../../core/i18n/formatters/formatter-base';

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

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

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

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

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

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

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

nonNumericFormats.forEach(format => {
expect(DateTimeUtil.isFormatNumeric(locale, format)).withContext(`Format: ${format}`).toBeFalse();
expect(DateTimeUtil.isFormatNumeric(locale, format, angularFormatter)).withContext(`Format: ${format}`).toBeFalse();
});
});

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

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

// handle the predefined formats for different locales
locale = 'bg-BG';
expect(DateTimeUtil.getNumericInputFormat(locale, 'short').normalize('NFKD')).toBe('dd.MM.yyyy г., HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate').normalize('NFKD')).toBe('dd.MM.yyyy г.');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime').normalize('NFKD')).toBe('HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime').normalize('NFKD')).toBe('HH:mm:ss');
expect(DateTimeUtil.getNumericInputFormat(locale, 'short', angularFormatter).normalize('NFKD')).toBe('dd.MM.yyyy г., HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate', angularFormatter).normalize('NFKD')).toBe('dd.MM.yyyy г.');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime', angularFormatter).normalize('NFKD')).toBe('HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime', angularFormatter).normalize('NFKD')).toBe('HH:mm:ss');

locale = 'ja-JP';
expect(DateTimeUtil.getNumericInputFormat(locale, 'short')).toBe('yyyy/MM/dd HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate')).toBe('yyyy/MM/dd');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime').normalize('NFKD')).toBe('HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime').normalize('NFKD')).toBe('HH:mm:ss');
expect(DateTimeUtil.getNumericInputFormat(locale, 'short', angularFormatter)).toBe('yyyy/MM/dd HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortDate', angularFormatter)).toBe('yyyy/MM/dd');
expect(DateTimeUtil.getNumericInputFormat(locale, 'shortTime', angularFormatter).normalize('NFKD')).toBe('HH:mm');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumTime', angularFormatter).normalize('NFKD')).toBe('HH:mm:ss');

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

// returns empty string if predefined and not among the numeric ones
expect(DateTimeUtil.getNumericInputFormat(locale, 'medium')).toBe('');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumDate')).toBe('');
expect(DateTimeUtil.getNumericInputFormat(locale, 'longTime')).toBe('');
expect(DateTimeUtil.getNumericInputFormat(locale, 'medium', angularFormatter)).toBe('');
expect(DateTimeUtil.getNumericInputFormat(locale, 'mediumDate', angularFormatter)).toBe('');
expect(DateTimeUtil.getNumericInputFormat(locale, 'longTime', angularFormatter)).toBe('');
});
});
229 changes: 9 additions & 220 deletions projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-time-editor.common';
import { formatDate, FormatWidth, getLocaleDateFormat } from '@angular/common';
import { ValidationErrors } from '@angular/forms';
import { isDate } from '../../core/utils';
import { DataType } from '../../data-operations/data-util';
import { getDateFormatter } from 'igniteui-i18n-core';
import { BaseFormatter } from '../../core/i18n/formatters/formatter-base';

/** @hidden */
const enum FormatDesc {
Expand Down Expand Up @@ -250,55 +251,7 @@ export abstract class DateTimeUtil {
/** Builds a date-time editor's default input format based on provided locale settings and data type. */
public static getDefaultInputFormat(locale: string, dataType: DataType = DataType.Date): string {
locale = locale || DateTimeUtil.DEFAULT_LOCALE;
if (!Intl || !Intl.DateTimeFormat || !Intl.DateTimeFormat.prototype.formatToParts) {
// TODO: fallback with Intl.format for IE?
return DateTimeUtil.DEFAULT_INPUT_FORMAT;
}
const parts = DateTimeUtil.getDefaultLocaleMask(locale, dataType);
parts.forEach(p => {
if (p.type !== DatePart.Year && p.type !== DateTimeUtil.SEPARATOR && p.type !== DatePart.AmPm) {
p.formatType = FormatDesc.TwoDigits;
}
});

return DateTimeUtil.getMask(parts);
}

/** Tries to format a date using Angular's DatePipe. Fallbacks to `Intl` if no locale settings have been loaded. */
public static formatDate(value: number | Date, format: string, locale: string, timezone?: string): string {
let formattedDate: string;
try {
formattedDate = formatDate(value, format, locale, timezone);
} catch {
DateTimeUtil.logMissingLocaleSettings(locale);
const formatter = new Intl.DateTimeFormat(locale);
formattedDate = formatter.format(value);
}

return formattedDate;
}

/**
* Returns the date format based on a provided locale.
* Supports Angular's DatePipe format options such as `shortDate`, `longDate`.
*/
public static getLocaleDateFormat(locale: string, displayFormat?: string): string {
const formatKeys = Object.keys(FormatWidth) as (keyof FormatWidth)[];
const targetKey = formatKeys.find(k => k.toLowerCase() === displayFormat?.toLowerCase().replace('date', ''));
if (!targetKey) {
// if displayFormat is not shortDate, longDate, etc.
// or if it is not set by the user
return displayFormat;
}
let format: string;
try {
format = getLocaleDateFormat(locale, FormatWidth[targetKey]);
} catch {
DateTimeUtil.logMissingLocaleSettings(locale);
format = DateTimeUtil.getDefaultInputFormat(locale);
}

return format;
return getDateFormatter().getLocaleDateTimeFormat(locale, true, DateTimeUtil.getFormatOptions(dataType));
}

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

public static isFormatNumeric(locale: string, inputFormat: string): boolean {
public static isFormatNumeric(locale: string, inputFormat: string, formatter: BaseFormatter): boolean {
const dateParts = DateTimeUtil.parseDateTimeFormat(inputFormat);
if (predefinedNonNumericFormats.has(inputFormat) || dateParts.every(p => p.type === DatePart.Literal)) {
return false;
Expand All @@ -538,7 +491,7 @@ export abstract class DateTimeUtil {
if (dateParts[i].type === DatePart.AmPm || dateParts[i].type === DatePart.Literal) {
continue;
}
const transformedValue = formatDate(new Date(), dateParts[i].format, locale);
const transformedValue = formatter.formatDate(new Date(), dateParts[i].format, locale);
// check if the transformed date/time part contains any kind of letter from any language
if (/\p{L}+/gu.test(transformedValue)) {
return false;
Expand All @@ -554,15 +507,15 @@ export abstract class DateTimeUtil {
* for the corresponding numeric date parts
* - otherwise, return an empty string
*/
public static getNumericInputFormat(locale: string, format: string): string {
public static getNumericInputFormat(locale: string, format: string, formatter: BaseFormatter): string {
let resultFormat = '';
if (!format) {
return resultFormat;
}
if (predefinedNumericFormats.has(format)) {
resultFormat = DateTimeUtil.getLocaleInputFormatFromParts(locale, predefinedNumericFormats.get(format));

} else if (DateTimeUtil.isFormatNumeric(locale, format)) {
} else if (DateTimeUtil.isFormatNumeric(locale, format, formatter)) {
resultFormat = format;
}
return resultFormat;
Expand All @@ -578,10 +531,8 @@ export abstract class DateTimeUtil {
options[p] = FormatDesc.TwoDigits;
}
});
const formatter = new Intl.DateTimeFormat(locale, options);
const dateStruct = DateTimeUtil.getDateStructFromParts(formatter.formatToParts(new Date()), formatter);
DateTimeUtil.fillDatePartsPositions(dateStruct);
return DateTimeUtil.getMask(dateStruct);

return getDateFormatter().getLocaleDateTimeFormat(locale, true, options);
}

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

private static getMask(dateStruct: any[]): string {
const mask = [];
for (const part of dateStruct) {
if (part.formatType === FormatDesc.Numeric) {
switch (part.type) {
case DateParts.Day:
mask.push('d');
break;
case DateParts.Month:
mask.push('M');
break;
case DateParts.Year:
mask.push('yyyy');
break;
case DateParts.Hour:
mask.push(part.hour12 ? 'h' : 'H');
break;
case DateParts.Minute:
mask.push('m');
break;
case DateParts.Second:
mask.push('s');
break;
}
} else if (part.formatType === FormatDesc.TwoDigits) {
switch (part.type) {
case DateParts.Day:
mask.push('dd');
break;
case DateParts.Month:
mask.push('MM');
break;
case DateParts.Year:
mask.push('yy');
break;
case DateParts.Hour:
mask.push(part.hour12 ? 'hh' : 'HH');
break;
case DateParts.Minute:
mask.push('mm');
break;
case DateParts.Second:
mask.push('ss');
break;
}
}

if (part.type === DateParts.AmPm) {
mask.push('tt');
}

if (part.type === DateTimeUtil.SEPARATOR) {
mask.push(part.value);
}
}

return mask.join('');
}

private static logMissingLocaleSettings(locale: string): void {
console.warn(`Missing locale data for the locale ${locale}. Please refer to https://angular.io/guide/i18n#i18n-pipes`);
console.warn('Using default browser locale settings.');
}

private static prependValue(value: number, partLength: number, prependChar: string): string {
return (prependChar + value.toString()).slice(-partLength);
}
Expand Down Expand Up @@ -752,102 +639,4 @@ export abstract class DateTimeUtil {
return { };
}
}

private static getDefaultLocaleMask(locale: string, dataType: DataType = DataType.Date) {
const options = DateTimeUtil.getFormatOptions(dataType);
const formatter = new Intl.DateTimeFormat(locale, options);
const formatToParts = formatter.formatToParts(new Date());
const dateStruct = DateTimeUtil.getDateStructFromParts(formatToParts, formatter);
DateTimeUtil.fillDatePartsPositions(dateStruct);
return dateStruct;
}

private static getDateStructFromParts(parts: Intl.DateTimeFormatPart[], formatter: Intl.DateTimeFormat): any[] {
const dateStruct = [];
for (const part of parts) {
if (part.type === DateTimeUtil.SEPARATOR) {
dateStruct.push({
type: DateTimeUtil.SEPARATOR,
value: part.value
});
} else {
dateStruct.push({
type: part.type
});
}
}
const formatterOptions = formatter.resolvedOptions();
for (const part of dateStruct) {
switch (part.type) {
case DateParts.Day: {
part.formatType = formatterOptions.day;
break;
}
case DateParts.Month: {
part.formatType = formatterOptions.month;
break;
}
case DateParts.Year: {
part.formatType = formatterOptions.year;
break;
}
case DateParts.Hour: {
part.formatType = formatterOptions.hour;
if (formatterOptions.hour12) {
part.hour12 = true;
}
break;
}
case DateParts.Minute: {
part.formatType = formatterOptions.minute;
break;
}
case DateParts.Second: {
part.formatType = formatterOptions.second;
break;
}
case DateParts.AmPm: {
part.formatType = formatterOptions.dayPeriod;
break;
}
}
}
return dateStruct;
}

private static fillDatePartsPositions(dateArray: any[]): void {
let currentPos = 0;

for (const part of dateArray) {
// Day|Month|Hour|Minute|Second|AmPm part positions
if (part.type === DateParts.Day || part.type === DateParts.Month ||
part.type === DateParts.Hour || part.type === DateParts.Minute || part.type === DateParts.Second ||
part.type === DateParts.AmPm
) {
// Offset 2 positions for number
part.position = [currentPos, currentPos + 2];
currentPos += 2;
} else if (part.type === DateParts.Year) {
// Year part positions
switch (part.formatType) {
case FormatDesc.Numeric: {
// Offset 4 positions for full year
part.position = [currentPos, currentPos + 4];
currentPos += 4;
break;
}
case FormatDesc.TwoDigits: {
// Offset 2 positions for short year
part.position = [currentPos, currentPos + 2];
currentPos += 2;
break;
}
}
} else if (part.type === DateTimeUtil.SEPARATOR) {
// Separator positions
part.position = [currentPos, currentPos + 1];
currentPos++;
}
}
}
}
Loading