diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index 155a9d2319f..51cd05fbeb5 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -154,7 +154,7 @@ export const isObject = (value: any): boolean => value && value.toString() === ' * @returns true if provided variable is Date * @hidden */ -export const isDate = (value: any): boolean => value instanceof Date; +export const isDate = (value: any): value is Date => value instanceof Date; /** * Checks if the two passed arguments are equal diff --git a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts new file mode 100644 index 00000000000..cb1765ae593 --- /dev/null +++ b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts @@ -0,0 +1,516 @@ +import { DateTimeUtil } from './date-time.util'; +import { DatePart, DatePartInfo } from '../../directives/date-time-editor/date-time-editor.common'; + +const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => { + obj[x.type] = x; + return obj; +}, {}); + +describe(`DateTimeUtil Unit tests`, () => { + describe('Date Time Parsing', () => { + it('should correctly parse all date time parts (base)', () => { + const result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss tt'); + const expected = [ + { start: 0, end: 2, type: DatePart.Date, format: 'dd' }, + { start: 2, end: 3, type: DatePart.Literal, format: '/' }, + { start: 3, end: 5, type: DatePart.Month, format: 'MM' }, + { start: 5, end: 6, type: DatePart.Literal, format: '/' }, + { start: 6, end: 10, type: DatePart.Year, format: 'yyyy' }, + { start: 10, end: 11, type: DatePart.Literal, format: ' ' }, + { start: 11, end: 13, type: DatePart.Hours, format: 'HH' }, + { start: 13, end: 14, type: DatePart.Literal, format: ':' }, + { start: 14, end: 16, type: DatePart.Minutes, format: 'mm' }, + { start: 16, end: 17, type: DatePart.Literal, format: ':' }, + { start: 17, end: 19, type: DatePart.Seconds, format: 'ss' }, + { start: 19, end: 20, type: DatePart.Literal, format: ' ' }, + { start: 20, end: 22, type: DatePart.AmPm, format: 'tt' } + ]; + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + }); + + it('should correctly parse date parts of with short formats', () => { + let result = DateTimeUtil.parseDateTimeFormat('MM/dd/yyyy'); + let resDict = reduceToDictionary(result); + expect(result.length).toEqual(5); + expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); + expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); + expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); + + result = DateTimeUtil.parseDateTimeFormat('M/d/yy'); + resDict = reduceToDictionary(result); + expect(result.length).toEqual(5); + expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); + expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); + expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 8 })); + + result = DateTimeUtil.parseDateTimeFormat('dd.MM.yyyy г.'); + resDict = reduceToDictionary(result); + expect(result.length).toEqual(6); + expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); + expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); + expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); + + // TODO + return; + result = DateTimeUtil.parseDateTimeFormat('dd.MM.yyyyг'); + resDict = reduceToDictionary(result); + expect(result.length).toEqual(6); + expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); + expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); + expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); + expect(result[5]?.format).toEqual('г'); + + result = DateTimeUtil.parseDateTimeFormat('yyyy/MM/d'); + resDict = reduceToDictionary(result); + expect(result.length).toEqual(5); + expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 0, end: 4 })); + expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 5, end: 7 })); + expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 8, end: 10 })); + }); + + it('should correctly parse boundary dates', () => { + const parts = DateTimeUtil.parseDateTimeFormat('MM/dd/yyyy'); + let result = DateTimeUtil.parseValueFromMask('08/31/2020', parts); + expect(result).toEqual(new Date(2020, 7, 31)); + result = DateTimeUtil.parseValueFromMask('09/30/2020', parts); + expect(result).toEqual(new Date(2020, 8, 30)); + result = DateTimeUtil.parseValueFromMask('10/31/2020', parts); + expect(result).toEqual(new Date(2020, 9, 31)); + }); + }); + + it('should correctly parse a date value from input', () => { + let input = '12/04/2012'; + let dateParts = [ + { start: 0, end: 2, type: DatePart.Date, format: 'dd' }, + { start: 2, end: 3, type: DatePart.Literal, format: '/' }, + { start: 3, end: 5, type: DatePart.Month, format: 'MM' }, + { start: 5, end: 6, type: DatePart.Literal, format: '/' }, + { start: 6, end: 10, type: DatePart.Year, format: 'yyyy' }, + { start: 10, end: 11, type: DatePart.Literal, format: ' ' } + ]; + + let expected = new Date(2012, 3, 12); + let result = DateTimeUtil.parseValueFromMask(input, dateParts); + expect(result.getTime()).toEqual(expected.getTime()); + + input = '04:12:23 PM'; + dateParts = [ + { start: 0, end: 2, type: DatePart.Hours, format: 'HH' }, + { start: 2, end: 3, type: DatePart.Literal, format: ':' }, + { start: 3, end: 5, type: DatePart.Minutes, format: 'mm' }, + { start: 5, end: 6, type: DatePart.Literal, format: ':' }, + { start: 6, end: 8, type: DatePart.Seconds, format: 'ss' }, + { start: 8, end: 9, type: DatePart.Literal, format: ' ' }, + { start: 9, end: 11, type: DatePart.AmPm, format: 'tt' } + ]; + + result = DateTimeUtil.parseValueFromMask(input, dateParts); + expect(result.getHours()).toEqual(4); + expect(result.getMinutes()).toEqual(12); + expect(result.getSeconds()).toEqual(23); + + input = '12/10/2012 14:06:03'; + dateParts = [ + { start: 0, end: 2, type: DatePart.Date, format: 'dd' }, + { start: 2, end: 3, type: DatePart.Literal, format: '/' }, + { start: 3, end: 5, type: DatePart.Month, format: 'MM' }, + { start: 5, end: 6, type: DatePart.Literal, format: '/' }, + { start: 6, end: 10, type: DatePart.Year, format: 'yyyy' }, + { start: 10, end: 11, type: DatePart.Literal, format: ' ' }, + { start: 11, end: 13, type: DatePart.Hours, format: 'HH' }, + { start: 13, end: 14, type: DatePart.Literal, format: ':' }, + { start: 14, end: 16, type: DatePart.Minutes, format: 'mm' }, + { start: 16, end: 17, type: DatePart.Literal, format: ':' }, + { start: 17, end: 19, type: DatePart.Seconds, format: 'ss' } + ]; + + expected = new Date(2012, 9, 12, 14, 6, 3); + result = DateTimeUtil.parseValueFromMask(input, dateParts); + + expect(result.getDate()).toEqual(12); + expect(result.getMonth()).toEqual(9); + expect(result.getFullYear()).toEqual(2012); + expect(result.getHours()).toEqual(14); + expect(result.getMinutes()).toEqual(6); + expect(result.getSeconds()).toEqual(3); + }); + + it('should properly build input formats based on locale', () => { + spyOn(DateTimeUtil, 'getDefaultInputFormat').and.callThrough(); + let result = DateTimeUtil.getDefaultInputFormat('en-US'); + expect(result).toEqual('MM/dd/yyyy'); + + result = DateTimeUtil.getDefaultInputFormat('bg-BG'); + expect(result).toEqual('dd.MM.yyyy г.'); + + expect(() => { + result = DateTimeUtil.getDefaultInputFormat(null); + }).not.toThrow(); + expect(result).toEqual('MM/dd/yyyy'); + + expect(() => { + result = DateTimeUtil.getDefaultInputFormat(''); + }).not.toThrow(); + expect(result).toEqual('MM/dd/yyyy'); + + expect(() => { + result = DateTimeUtil.getDefaultInputFormat(undefined); + }).not.toThrow(); + expect(result).toEqual('MM/dd/yyyy'); + }); + + it('should correctly distinguish date from time characters', () => { + expect(DateTimeUtil.isDateOrTimeChar('d')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('M')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('y')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('H')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('h')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('m')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('s')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar(':')).toBeFalse(); + expect(DateTimeUtil.isDateOrTimeChar('/')).toBeFalse(); + expect(DateTimeUtil.isDateOrTimeChar('.')).toBeFalse(); + }); + + it('should spin date portions correctly', () => { + // base + let date = new Date(2015, 4, 20); + DateTimeUtil.spinDate(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 21).getTime()); + DateTimeUtil.spinDate(-1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20).getTime()); + + // delta !== 1 + DateTimeUtil.spinDate(5, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 25).getTime()); + DateTimeUtil.spinDate(-6, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 19).getTime()); + + // without looping over + date = new Date(2015, 4, 31); + DateTimeUtil.spinDate(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 31).getTime()); + DateTimeUtil.spinDate(-50, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 1).getTime()); + + // with looping over + DateTimeUtil.spinDate(31, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 1).getTime()); + DateTimeUtil.spinDate(-5, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 27).getTime()); + }); + + it('should spin month portions correctly', () => { + // base + let date = new Date(2015, 4, 20); + DateTimeUtil.spinMonth(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 5, 20).getTime()); + DateTimeUtil.spinMonth(-1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20).getTime()); + + // delta !== 1 + DateTimeUtil.spinMonth(5, date, false); + expect(date.getTime()).toEqual(new Date(2015, 9, 20).getTime()); + DateTimeUtil.spinMonth(-6, date, false); + expect(date.getTime()).toEqual(new Date(2015, 3, 20).getTime()); + + // without looping over + date = new Date(2015, 11, 31); + DateTimeUtil.spinMonth(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 11, 31).getTime()); + DateTimeUtil.spinMonth(-50, date, false); + expect(date.getTime()).toEqual(new Date(2015, 0, 31).getTime()); + + // with looping over + date = new Date(2015, 11, 1); + DateTimeUtil.spinMonth(2, date, true); + expect(date.getTime()).toEqual(new Date(2015, 1, 1).getTime()); + date = new Date(2015, 0, 1); + DateTimeUtil.spinMonth(-1, date, true); + expect(date.getTime()).toEqual(new Date(2015, 11, 1).getTime()); + + // coerces date portion to be no greater than max date of current month + date = new Date(2020, 2, 31); + DateTimeUtil.spinMonth(-1, date, false); + expect(date.getTime()).toEqual(new Date(2020, 1, 29).getTime()); + DateTimeUtil.spinMonth(1, date, false); + expect(date.getTime()).toEqual(new Date(2020, 2, 29).getTime()); + date = new Date(2020, 4, 31); + DateTimeUtil.spinMonth(1, date, false); + expect(date.getTime()).toEqual(new Date(2020, 5, 30).getTime()); + }); + + it('should spin year portions correctly', () => { + // base + let date = new Date(2015, 4, 20); + DateTimeUtil.spinYear(1, date); + expect(date.getTime()).toEqual(new Date(2016, 4, 20).getTime()); + DateTimeUtil.spinYear(-1, date); + expect(date.getTime()).toEqual(new Date(2015, 4, 20).getTime()); + + // delta !== 1 + DateTimeUtil.spinYear(5, date); + expect(date.getTime()).toEqual(new Date(2020, 4, 20).getTime()); + DateTimeUtil.spinYear(-6, date); + expect(date.getTime()).toEqual(new Date(2014, 4, 20).getTime()); + + // coerces February to be 29 days on a leap year and 28 on a non leap year + date = new Date(2020, 1, 29); + DateTimeUtil.spinYear(1, date); + expect(date.getTime()).toEqual(new Date(2021, 1, 28).getTime()); + DateTimeUtil.spinYear(-1, date); + expect(date.getTime()).toEqual(new Date(2020, 1, 28).getTime()); + }); + + it('should spin hours portion correctly', () => { + // base + let date = new Date(2015, 4, 20, 6); + DateTimeUtil.spinHours(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 7).getTime()); + DateTimeUtil.spinHours(-1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6).getTime()); + + // delta !== 1 + DateTimeUtil.spinHours(5, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 11).getTime()); + DateTimeUtil.spinHours(-6, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 5).getTime()); + + // without looping over + date = new Date(2015, 4, 20, 23); + DateTimeUtil.spinHours(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 23).getTime()); + DateTimeUtil.spinHours(-30, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 0).getTime()); + + // with looping over (date is not affected) + DateTimeUtil.spinHours(25, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 1).getTime()); + DateTimeUtil.spinHours(-2, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 23).getTime()); + }); + + it('should spin minutes portion correctly', () => { + // base + let date = new Date(2015, 4, 20, 6, 10); + DateTimeUtil.spinMinutes(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 11).getTime()); + DateTimeUtil.spinMinutes(-1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 10).getTime()); + + // delta !== 1 + DateTimeUtil.spinMinutes(5, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 15).getTime()); + DateTimeUtil.spinMinutes(-6, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 9).getTime()); + + // without looping over + date = new Date(2015, 4, 20, 12, 59); + DateTimeUtil.spinMinutes(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59).getTime()); + DateTimeUtil.spinMinutes(-70, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 0).getTime()); + + // with looping over (hours are not affected) + DateTimeUtil.spinMinutes(61, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 1).getTime()); + DateTimeUtil.spinMinutes(-5, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 56).getTime()); + }); + + it('should spin seconds portion correctly', () => { + // base + let date = new Date(2015, 4, 20, 6, 10, 5); + DateTimeUtil.spinSeconds(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 10, 6).getTime()); + DateTimeUtil.spinSeconds(-1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 10, 5).getTime()); + + // delta !== 1 + DateTimeUtil.spinSeconds(5, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 10, 10).getTime()); + DateTimeUtil.spinSeconds(-6, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 6, 10, 4).getTime()); + + // without looping over + date = new Date(2015, 4, 20, 12, 59, 59); + DateTimeUtil.spinSeconds(1, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 59).getTime()); + DateTimeUtil.spinSeconds(-70, date, false); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 0).getTime()); + + // with looping over (minutes are not affected) + DateTimeUtil.spinSeconds(62, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 2).getTime()); + DateTimeUtil.spinSeconds(-5, date, true); + expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 57).getTime()); + }); + + it('should spin AM/PM portion correctly', () => { + const currentDate = new Date(2015, 4, 31, 4, 59, 59); + const newDate = new Date(2015, 4, 31, 4, 59, 59); + // spin from AM to PM + DateTimeUtil.spinAmPm(currentDate, newDate, 'PM'); + expect(currentDate.getHours()).toEqual(16); + + // spin from PM to AM + DateTimeUtil.spinAmPm(currentDate, newDate, 'AM'); + expect(currentDate.getHours()).toEqual(4); + }); + + it('should compare dates correctly', () => { + // base + let minValue = new Date(2010, 3, 2); + let maxValue = new Date(2010, 3, 7); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 3), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 1), minValue)).toBeTrue(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 7), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 6), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 8), maxValue)).toBeTrue(); + + // time variations + minValue = new Date(2010, 3, 2, 11, 10, 10); + maxValue = new Date(2010, 3, 2, 15, 15, 15); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 11), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 9), minValue)).toBeTrue(); + + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 11, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 9, 10), minValue)).toBeTrue(); + + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 12, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 10, 10, 10), minValue)).toBeTrue(); + + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 3, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 1, 11, 10, 10), minValue)).toBeTrue(); + + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 4, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 2, 2, 11, 10, 10), minValue)).toBeTrue(); + + expect(DateTimeUtil.lessThanMinValue(new Date(2011, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2010, 3, 2, 11, 10, 10), minValue)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(2009, 3, 2, 11, 10, 10), minValue)).toBeTrue(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 16), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 14), maxValue)).toBeFalse(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 16, 15), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 14, 15), maxValue)).toBeFalse(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 16, 15, 15), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 14, 15, 15), maxValue)).toBeFalse(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 3, 15, 15, 15), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 1, 15, 15, 15), maxValue)).toBeFalse(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 4, 2, 15, 15, 15), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 2, 2, 15, 15, 15), maxValue)).toBeFalse(); + + expect(DateTimeUtil.greaterThanMaxValue(new Date(2011, 3, 2, 15, 15, 15), maxValue)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2010, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2009, 3, 2, 15, 15, 15), maxValue)).toBeFalse(); + + + // date excluded + expect(DateTimeUtil.lessThanMinValue(new Date(2030, 3, 2, 11, 10, 9), minValue, true, false)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2000, 3, 2, 15, 15, 16), minValue, true, false)).toBeTrue(); + + // time excluded + expect(DateTimeUtil.lessThanMinValue(new Date(2009, 3, 2, 11, 10, 10), minValue, false, true)).toBeTrue(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(2011, 3, 2, 15, 15, 15), minValue, true, false)).toBeTrue(); + + // falsy values + expect(DateTimeUtil.lessThanMinValue(new Date(NaN), new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(NaN), new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(NaN), null)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(NaN), null)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(null, new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(null, new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(NaN), undefined)).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(NaN), undefined)).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(undefined, new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(undefined, new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(NaN), new Date())).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(NaN), new Date())).toBeFalse(); + expect(DateTimeUtil.lessThanMinValue(new Date(), new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.greaterThanMaxValue(new Date(), new Date(NaN))).toBeFalse(); + }); + + it('should return ValidationErrors for minValue and maxValue', () => { + let minValue = new Date(2010, 3, 2); + let maxValue = new Date(2010, 3, 7); + + expect(DateTimeUtil.validateMinMax(new Date(2010, 3, 4), minValue, maxValue)).toEqual({}); + expect(DateTimeUtil.validateMinMax(new Date(2010, 2, 7), minValue, maxValue)).toEqual({ minValue: true }); + expect(DateTimeUtil.validateMinMax(new Date(2010, 4, 2), minValue, maxValue)).toEqual({ maxValue: true }); + + minValue = new Date(2010, 3, 2, 10, 10, 10); + maxValue = new Date(2010, 3, 2, 15, 15, 15); + + // TODO: test with time portions as well + return; + expect(DateTimeUtil.validateMinMax(new Date(2010, 3, 2, 11, 11, 11), minValue, maxValue)).toEqual({}); + expect(DateTimeUtil.validateMinMax(new Date(2010, 3, 2, 9, 11, 11), minValue, maxValue)).toEqual({ minValue: true }); + expect(DateTimeUtil.validateMinMax(new Date(2010, 3, 2, 16, 11, 11), minValue, maxValue)).toEqual({ maxValue: true }); + }); + + it('should parse dates correctly with parseDate', () => { + pending('TODO: ISO implementation'); + // // ISO strings and numbers + // expect(DateTimeUtil.parseDate('2012-12-12T12:12:12').getTime()).toEqual(new Date(2012, 11, 12, 12, 12, 12).getTime()); + // expect(DateTimeUtil.parseDate(new Date()).getTime()).toEqual(new Date().getTime()); + // expect(DateTimeUtil.parseDate(new Date().getTime()).getTime()).toEqual(new Date().getTime()); + + // // non ISO strings with mask and no dateTimeParts + // let mask = 'dd/MM/yyyy HH:mm:ss'; + // expect(DateTimeUtil.parseDate('12/12/2012 12:12:12', null, mask) + // .getTime()).toEqual(new Date(2012, 11, 12, 12, 12, 12).getTime()); + // mask = 'MM-dd-yyyy mm:ss'; + // expect(DateTimeUtil.parseDate('06/04/2012 44:12', null, mask).getTime()).toEqual(new Date(2012, 5, 4, 0, 44, 12).getTime()); + // mask = 'yy-dd-MM ss:HHmm'; + // expect(DateTimeUtil.parseDate('12/12/12 12:12:12', null, mask).getTime()).toEqual(new Date(2012, 11, 12, 12, 12, 12).getTime()); + // mask = 'dd///()#yy123/\\\/MM ___ ss(|::HH123456::123123 mm'; + // expect(DateTimeUtil.parseDate('12/12/12 12:12:12', null, mask).getTime()).toEqual(new Date(2012, 11, 12, 12, 12, 12).getTime()); + + // // non ISO strings with dateTimeParts and no mask + // const dateTimeParts = DateTimeUtil.parseDateTimeFormat(mask); + // expect(DateTimeUtil.parseDate('12/12/12 12:12:12', dateTimeParts).getTime()) + // .toEqual(new Date(2012, 11, 12, 12, 12, 12).getTime()); + + // // invalid values + // expect(DateTimeUtil.parseDate(undefined)).toEqual(null); + // expect(DateTimeUtil.parseDate(NaN)).toEqual(null); + // expect(DateTimeUtil.parseDate([])).toEqual(null); + // expect(DateTimeUtil.parseDate({})).toEqual(null); + // expect(DateTimeUtil.parseDate('')).toEqual(null); + // expect(DateTimeUtil.parseDate(new Date(NaN))).toEqual(null); + // expect(DateTimeUtil.parseDate(null)).toBeInstanceOf(Date); + // expect(DateTimeUtil.parseDate(true)).toBeInstanceOf(Date); + // expect(DateTimeUtil.parseDate(false)).toBeInstanceOf(Date); + }); + + it('isValidDate should properly determine if a date is valid or not', () => { + expect(DateTimeUtil.isValidDate(new Date())).toBeTrue(); + expect(DateTimeUtil.isValidDate(new Date(NaN))).toBeFalse(); + expect(DateTimeUtil.isValidDate(new Date().getTime())).toBeFalse(); + expect(DateTimeUtil.isValidDate('')).toBeFalse(); + expect(DateTimeUtil.isValidDate({})).toBeFalse(); + expect(DateTimeUtil.isValidDate([])).toBeFalse(); + expect(DateTimeUtil.isValidDate(null)).toBeFalse(); + expect(DateTimeUtil.isValidDate(undefined)).toBeFalse(); + expect(DateTimeUtil.isValidDate(false)).toBeFalse(); + expect(DateTimeUtil.isValidDate(true)).toBeFalse(); + }); +}); diff --git a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts new file mode 100644 index 00000000000..924372d3846 --- /dev/null +++ b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts @@ -0,0 +1,516 @@ +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, parseDate } from '../../core/utils'; + +/** @hidden */ +const enum FormatDesc { + Numeric = 'numeric', + TwoDigits = '2-digit' +} + +const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T']; +const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y']; + +/** @hidden */ +const enum DateParts { + Day = 'day', + Month = 'month', + Year = 'year' +} + +/** @hidden */ +export abstract class DateTimeUtil { + public static readonly DEFAULT_INPUT_FORMAT = 'MM/dd/yyyy'; + private static readonly SEPARATOR = 'literal'; + private static readonly DEFAULT_LOCALE = 'en'; + + /** + * Parse a Date value from masked string input based on determined date parts + * + * @param inputData masked value to parse + * @param dateTimeParts Date parts array for the mask + */ + public static parseValueFromMask(inputData: string, dateTimeParts: DatePartInfo[], promptChar?: string): Date | null { + const parts: { [key in DatePart]: number } = {} as any; + dateTimeParts.forEach(dp => { + let value = parseInt(DateTimeUtil.getCleanVal(inputData, dp, promptChar), 10); + if (!value) { + value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0; + } + parts[dp.type] = value; + }); + parts[DatePart.Month] -= 1; + + if (parts[DatePart.Month] < 0 || 11 < parts[DatePart.Month]) { + return null; + } + + // TODO: Century threshold + if (parts[DatePart.Year] < 50) { + parts[DatePart.Year] += 2000; + } + + if (parts[DatePart.Date] > DateTimeUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) { + return null; + } + + if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) { + return null; + } + + return new Date( + parts[DatePart.Year] || 2000, + parts[DatePart.Month] || 0, + parts[DatePart.Date] || 1, + parts[DatePart.Hours] || 0, + parts[DatePart.Minutes] || 0, + parts[DatePart.Seconds] || 0 + ); + } + + /** Parse the mask into date/time and literal parts */ + public static parseDateTimeFormat(mask: string, locale?: string): DatePartInfo[] { + const format = mask || DateTimeUtil.getDefaultInputFormat(locale); + const dateTimeParts: DatePartInfo[] = []; + const formatArray = Array.from(format); + let currentPart: DatePartInfo = null; + let position = 0; + + for (let i = 0; i < formatArray.length; i++, position++) { + const type = DateTimeUtil.determineDatePart(formatArray[i]); + if (currentPart) { + if (currentPart.type === type) { + currentPart.format += formatArray[i]; + if (i < formatArray.length - 1) { + continue; + } + } + + DateTimeUtil.ensureLeadingZero(currentPart); + currentPart.end = currentPart.start + currentPart.format.length; + position = currentPart.end; + dateTimeParts.push(currentPart); + } + + currentPart = { + start: position, + end: position + formatArray[i].length, + type, + format: formatArray[i] + }; + } + + return dateTimeParts; + } + + /** Builds a date-time editor's default input format based on provided locale settings. */ + public static getDefaultInputFormat(locale: string): 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); + parts.forEach(p => { + if (p.type !== DatePart.Year && p.type !== DateTimeUtil.SEPARATOR) { + 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; + } + + /** Determines if a given character is `d/M/y` or `h/m/s`. */ + public static isDateOrTimeChar(char: string): boolean { + return DATE_CHARS.indexOf(char) !== -1 || TIME_CHARS.indexOf(char) !== -1; + } + + /** Spins the date portion in a date-time editor. */ + public static spinDate(delta: number, newDate: Date, isSpinLoop: boolean): void { + const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth()); + let date = newDate.getDate() + delta; + if (date > maxDate) { + date = isSpinLoop ? date % maxDate : maxDate; + } else if (date < 1) { + date = isSpinLoop ? maxDate + (date % maxDate) : 1; + } + + newDate.setDate(date); + } + + /** Spins the month portion in a date-time editor. */ + public static spinMonth(delta: number, newDate: Date, isSpinLoop: boolean): void { + const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth() + delta); + if (newDate.getDate() > maxDate) { + newDate.setDate(maxDate); + } + + const maxMonth = 11; + const minMonth = 0; + let month = newDate.getMonth() + delta; + if (month > maxMonth) { + month = isSpinLoop ? (month % maxMonth) - 1 : maxMonth; + } else if (month < minMonth) { + month = isSpinLoop ? maxMonth + (month % maxMonth) + 1 : minMonth; + } + + newDate.setMonth(month); + } + + /** Spins the year portion in a date-time editor. */ + public static spinYear(delta: number, newDate: Date): void { + const maxDate = DateTimeUtil.daysInMonth(newDate.getFullYear() + delta, newDate.getMonth()); + if (newDate.getDate() > maxDate) { + // clip to max to avoid leap year change shifting the entire value + newDate.setDate(maxDate); + } + newDate.setFullYear(newDate.getFullYear() + delta); + } + + /** Spins the hours portion in a date-time editor. */ + public static spinHours(delta: number, newDate: Date, isSpinLoop: boolean): void { + const maxHour = 23; + const minHour = 0; + let hours = newDate.getHours() + delta; + if (hours > maxHour) { + hours = isSpinLoop ? hours % maxHour - 1 : maxHour; + } else if (hours < minHour) { + hours = isSpinLoop ? maxHour + (hours % maxHour) + 1 : minHour; + } + + newDate.setHours(hours); + } + + /** Spins the minutes portion in a date-time editor. */ + public static spinMinutes(delta: number, newDate: Date, isSpinLoop: boolean): void { + const maxMinutes = 59; + const minMinutes = 0; + let minutes = newDate.getMinutes() + delta; + if (minutes > maxMinutes) { + minutes = isSpinLoop ? minutes % maxMinutes - 1 : maxMinutes; + } else if (minutes < minMinutes) { + minutes = isSpinLoop ? maxMinutes + (minutes % maxMinutes) + 1 : minMinutes; + } + + newDate.setMinutes(minutes); + } + + /** Spins the seconds portion in a date-time editor. */ + public static spinSeconds(delta: number, newDate: Date, isSpinLoop: boolean): void { + const maxSeconds = 59; + const minSeconds = 0; + let seconds = newDate.getSeconds() + delta; + if (seconds > maxSeconds) { + seconds = isSpinLoop ? seconds % maxSeconds - 1 : maxSeconds; + } else if (seconds < minSeconds) { + seconds = isSpinLoop ? maxSeconds + (seconds % maxSeconds) + 1 : minSeconds; + } + + newDate.setSeconds(seconds); + } + + /** Spins the AM/PM portion in a date-time editor. */ + public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date { + switch (amPmFromMask) { + case 'AM': + newDate = new Date(newDate.setHours(newDate.getHours() + 12)); + break; + case 'PM': + newDate = new Date(newDate.setHours(newDate.getHours() - 12)); + break; + } + if (newDate.getDate() !== currentDate.getDate()) { + return currentDate; + } + + return newDate; + } + + /** + * Determines whether the provided value is greater than the provided max value. + * + * @param includeTime set to false if you want to exclude time portion of the two dates + * @param includeDate set to false if you want to exclude the date portion of the two dates + * @returns true if provided value is greater than provided maxValue + */ + public static greaterThanMaxValue(value: Date, maxValue: Date, includeTime = true, includeDate = true): boolean { + if (includeTime && includeDate) { + return value.getTime() > maxValue.getTime(); + } + + const _value = new Date(value.getTime()); + const _maxValue = new Date(maxValue.getTime()); + if (!includeTime) { + _value.setHours(0, 0, 0, 0); + _maxValue.setHours(0, 0, 0, 0); + } + if (!includeDate) { + _value.setFullYear(0, 0, 0); + _maxValue.setFullYear(0, 0, 0); + } + + return _value.getTime() > _maxValue.getTime(); + } + + /** + * Determines whether the provided value is less than the provided min value. + * + * @param includeTime set to false if you want to exclude time portion of the two dates + * @param includeDate set to false if you want to exclude the date portion of the two dates + * @returns true if provided value is less than provided minValue + */ + public static lessThanMinValue(value: Date, minValue: Date, includeTime = true, includeDate = true): boolean { + if (includeTime && includeDate) { + return value.getTime() < minValue.getTime(); + } + + const _value = new Date(value.getTime()); + const _minValue = new Date(minValue.getTime()); + if (!includeTime) { + _value.setHours(0, 0, 0, 0); + _minValue.setHours(0, 0, 0, 0); + } + if (!includeDate) { + _value.setFullYear(0, 0, 0); + _minValue.setFullYear(0, 0, 0); + } + + return _value.getTime() < _minValue.getTime(); + } + + /** + * Validates a value within a given min and max value range. + * + * @param value The value to validate + * @param minValue The lowest possible value that `value` can take + * @param maxValue The largest possible value that `value` can take + */ + public static validateMinMax(value: Date, minValue: Date | string, maxValue: Date | string): ValidationErrors | null { + const errors = {}; + const min = parseDate(minValue); + const max = parseDate(maxValue); + if ((min && value && DateTimeUtil.lessThanMinValue(value, min, false)) + || (min && value && DateTimeUtil.lessThanMinValue(value, min, false))) { + Object.assign(errors, { minValue: true }); + } + if ((max && value && DateTimeUtil.greaterThanMaxValue(value, max, false)) + || (max && value && DateTimeUtil.greaterThanMaxValue(value, max, false))) { + Object.assign(errors, { maxValue: true }); + } + + return errors; + } + + /** + * Returns whether the input is valid date + * + * @param value input to check + * @returns true if provided input is a valid date + */ + public static isValidDate(value: any): value is Date { + if (isDate(value)) { + return !isNaN(value.getTime()); + } + + return false; + } + + private static daysInMonth(fullYear: number, month: number): number { + return new Date(fullYear, month + 1, 0).getDate(); + } + + private static trimEmptyPlaceholders(value: string, promptChar?: string): string { + const result = value.replace(new RegExp(promptChar || '_', 'g'), ''); + return result; + } + + private static getMask(dateStruct: any[]): string { + const mask = []; + for (const part of dateStruct) { + switch (part.formatType) { + case FormatDesc.Numeric: { + if (part.type === DateParts.Day) { + mask.push('d'); + } else if (part.type === DateParts.Month) { + mask.push('M'); + } else { + mask.push('yyyy'); + } + break; + } + case FormatDesc.TwoDigits: { + if (part.type === DateParts.Day) { + mask.push('dd'); + } else if (part.type === DateParts.Month) { + mask.push('MM'); + } else { + mask.push('yy'); + } + } + } + + 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 ensureLeadingZero(part: DatePartInfo) { + switch (part.type) { + case DatePart.Date: + case DatePart.Month: + case DatePart.Hours: + case DatePart.Minutes: + case DatePart.Seconds: + if (part.format.length === 1) { + part.format = part.format.repeat(2); + } + break; + } + } + + private static getCleanVal(inputData: string, datePart: DatePartInfo, promptChar?: string): string { + return DateTimeUtil.trimEmptyPlaceholders(inputData.substring(datePart.start, datePart.end), promptChar); + } + + private static determineDatePart(char: string): DatePart { + switch (char) { + case 'd': + case 'D': + return DatePart.Date; + case 'M': + return DatePart.Month; + case 'y': + case 'Y': + return DatePart.Year; + case 'h': + case 'H': + return DatePart.Hours; + case 'm': + return DatePart.Minutes; + case 's': + case 'S': + return DatePart.Seconds; + case 't': + case 'T': + return DatePart.AmPm; + default: + return DatePart.Literal; + } + } + + private static getDefaultLocaleMask(locale: string) { + const dateStruct = []; + const formatter = new Intl.DateTimeFormat(locale); + const formatToParts = formatter.formatToParts(new Date()); + for (const part of formatToParts) { + 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; + } + } + } + DateTimeUtil.fillDatePartsPositions(dateStruct); + return dateStruct; + } + + private static fillDatePartsPositions(dateArray: any[]): void { + let currentPos = 0; + + for (const part of dateArray) { + // Day|Month part positions + if (part.type === DateParts.Day || part.type === DateParts.Month) { + // 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++; + } + } + } +} diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts deleted file mode 100644 index fce6336d14e..00000000000 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts +++ /dev/null @@ -1,968 +0,0 @@ -import { isIE } from '../core/utils'; -import { DatePart, DatePartInfo } from '../directives/date-time-editor/date-time-editor.common'; -import { formatDate, FormatWidth, getLocaleDateFormat } from '@angular/common'; -import { ValidationErrors } from '@angular/forms'; - -/** - * This enum is used to keep the date validation result. - * - * @hidden - */ -export const enum DateState { - Valid = 'valid', - Invalid = 'invalid', -} - -/** @hidden */ -const enum FormatDesc { - Numeric = 'numeric', - TwoDigits = '2-digit' -} - -/** @hidden */ -const enum DateChars { - YearChar = 'y', - MonthChar = 'M', - DayChar = 'd' -} - -const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T']; -const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y']; - -/** @hidden */ -const enum DateParts { - Day = 'day', - Month = 'month', - Year = 'year' -} - - -/** @hidden */ -export abstract class DatePickerUtil { - public static readonly DEFAULT_INPUT_FORMAT = 'MM/dd/yyyy'; - // TODO: this is the def mask for the date-picker, should remove it during refactoring - private static readonly SHORT_DATE_MASK = 'MM/dd/yy'; - private static readonly SEPARATOR = 'literal'; - private static readonly NUMBER_OF_MONTHS = 12; - private static readonly PROMPT_CHAR = '_'; - private static readonly DEFAULT_LOCALE = 'en'; - - /** - * TODO: (in issue #6483) Unit tests and docs for all public methods. - */ - - - - /** - * Parse a Date value from masked string input based on determined date parts - * - * @param inputData masked value to parse - * @param dateTimeParts Date parts array for the mask - */ - public static parseValueFromMask(inputData: string, dateTimeParts: DatePartInfo[], promptChar?: string): Date | null { - const parts: { [key in DatePart]: number } = {} as any; - dateTimeParts.forEach(dp => { - let value = parseInt(DatePickerUtil.getCleanVal(inputData, dp, promptChar), 10); - if (!value) { - value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0; - } - parts[dp.type] = value; - }); - parts[DatePart.Month] -= 1; - - if (parts[DatePart.Month] < 0 || 11 < parts[DatePart.Month]) { - return null; - } - - // TODO: Century threshold - if (parts[DatePart.Year] < 50) { - parts[DatePart.Year] += 2000; - } - - if (parts[DatePart.Date] > DatePickerUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) { - return null; - } - - if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) { - return null; - } - - return new Date( - parts[DatePart.Year] || 2000, - parts[DatePart.Month] || 0, - parts[DatePart.Date] || 1, - parts[DatePart.Hours] || 0, - parts[DatePart.Minutes] || 0, - parts[DatePart.Seconds] || 0 - ); - } - - /** - * Parse the mask into date/time and literal parts - */ - public static parseDateTimeFormat(mask: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): DatePartInfo[] { - const format = mask || DatePickerUtil.getDefaultInputFormat(locale); - const dateTimeParts: DatePartInfo[] = []; - const formatArray = Array.from(format); - let currentPart: DatePartInfo = null; - let position = 0; - - for (let i = 0; i < formatArray.length; i++, position++) { - const type = DatePickerUtil.determineDatePart(formatArray[i]); - if (currentPart) { - if (currentPart.type === type) { - currentPart.format += formatArray[i]; - if (i < formatArray.length - 1) { - continue; - } - } - - DatePickerUtil.ensureLeadingZero(currentPart); - currentPart.end = currentPart.start + currentPart.format.length; - position = currentPart.end; - dateTimeParts.push(currentPart); - } - - currentPart = { - start: position, - end: position + formatArray[i].length, - type, - format: formatArray[i] - }; - } - - return dateTimeParts; - } - - public static getDefaultInputFormat(locale: string): string { - if (!Intl || !Intl.DateTimeFormat || !Intl.DateTimeFormat.prototype.formatToParts) { - // TODO: fallback with Intl.format for IE? - return DatePickerUtil.SHORT_DATE_MASK; - } - const parts = DatePickerUtil.getDefaultLocaleMask(locale); - parts.forEach(p => { - if (p.type !== DatePart.Year && p.type !== DatePickerUtil.SEPARATOR) { - p.formatType = FormatDesc.TwoDigits; - } - }); - - return DatePickerUtil.getMask(parts); - } - - public static formatDate(value: number | Date, format: string, locale: string, timezone?: string): string { - let formattedDate: string; - try { - formattedDate = formatDate(value, format, locale, timezone); - } catch { - DatePickerUtil.logMissingLocaleSettings(locale); - const formatter = new Intl.DateTimeFormat(locale); - formattedDate = formatter.format(value); - } - - return formattedDate; - } - - 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 { - DatePickerUtil.logMissingLocaleSettings(locale); - format = DatePickerUtil.getDefaultInputFormat(locale); - } - - return format; - } - - public static isDateOrTimeChar(char: string): boolean { - return DATE_CHARS.indexOf(char) !== -1 || TIME_CHARS.indexOf(char) !== -1; - } - - public static spinDate(delta: number, newDate: Date, isSpinLoop: boolean): void { - const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth()); - let date = newDate.getDate() + delta; - if (date > maxDate) { - date = isSpinLoop ? date % maxDate : maxDate; - } else if (date < 1) { - date = isSpinLoop ? maxDate + (date % maxDate) : 1; - } - - newDate.setDate(date); - } - - public static spinMonth(delta: number, newDate: Date, isSpinLoop: boolean): void { - const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth() + delta); - if (newDate.getDate() > maxDate) { - newDate.setDate(maxDate); - } - - const maxMonth = 11; - const minMonth = 0; - let month = newDate.getMonth() + delta; - if (month > maxMonth) { - month = isSpinLoop ? (month % maxMonth) - 1 : maxMonth; - } else if (month < minMonth) { - month = isSpinLoop ? maxMonth + (month % maxMonth) + 1 : minMonth; - } - - newDate.setMonth(month); - } - - public static spinYear(delta: number, newDate: Date): void { - const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear() + delta, newDate.getMonth()); - if (newDate.getDate() > maxDate) { - // clip to max to avoid leap year change shifting the entire value - newDate.setDate(maxDate); - } - newDate.setFullYear(newDate.getFullYear() + delta); - } - - public static spinHours(delta: number, newDate: Date, isSpinLoop: boolean): void { - const maxHour = 23; - const minHour = 0; - let hours = newDate.getHours() + delta; - if (hours > maxHour) { - hours = isSpinLoop ? hours % maxHour - 1 : maxHour; - } else if (hours < minHour) { - hours = isSpinLoop ? maxHour + (hours % maxHour) + 1 : minHour; - } - - newDate.setHours(hours); - } - - public static spinMinutes(delta: number, newDate: Date, isSpinLoop: boolean): void { - const maxMinutes = 59; - const minMinutes = 0; - let minutes = newDate.getMinutes() + delta; - if (minutes > maxMinutes) { - minutes = isSpinLoop ? minutes % maxMinutes - 1 : maxMinutes; - } else if (minutes < minMinutes) { - minutes = isSpinLoop ? maxMinutes + (minutes % maxMinutes) + 1 : minMinutes; - } - - newDate.setMinutes(minutes); - } - - public static spinSeconds(delta: number, newDate: Date, isSpinLoop: boolean): void { - const maxSeconds = 59; - const minSeconds = 0; - let seconds = newDate.getSeconds() + delta; - if (seconds > maxSeconds) { - seconds = isSpinLoop ? seconds % maxSeconds - 1 : maxSeconds; - } else if (seconds < minSeconds) { - seconds = isSpinLoop ? maxSeconds + (seconds % maxSeconds) + 1 : minSeconds; - } - - newDate.setSeconds(seconds); - } - - public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date { - switch (amPmFromMask) { - case 'AM': - newDate = new Date(newDate.setHours(newDate.getHours() + 12)); - break; - case 'PM': - newDate = new Date(newDate.setHours(newDate.getHours() - 12)); - break; - } - if (newDate.getDate() !== currentDate.getDate()) { - return currentDate; - } - - return newDate; - } - - /** - * Determines whether the provided value is greater than the provided max value. - * - * @param includeTime set to false if you want to exclude time portion of the two dates - * @param includeDate set to false if you want to exclude the date portion of the two dates - * @returns true if provided value is greater than provided maxValue - */ - public static greaterThanMaxValue(value: Date, maxValue: Date, includeTime = true, includeDate = true): boolean { - // TODO: check if provided dates are valid dates and not Invalid Date - // if maxValue is Invalid Date and value is valid date this will return: - // - false if includeDate is true - // - true if includeDate is false - if (includeTime && includeDate) { - return value.getTime() > maxValue.getTime(); - } - - const _value = new Date(value.getTime()); - const _maxValue = new Date(maxValue.getTime()); - if (!includeTime) { - _value.setHours(0, 0, 0, 0); - _maxValue.setHours(0, 0, 0, 0); - } - if (!includeDate) { - _value.setFullYear(0, 0, 0); - _maxValue.setFullYear(0, 0, 0); - } - - return _value.getTime() > _maxValue.getTime(); - } - - /** - * Determines whether the provided value is less than the provided min value. - * - * @param includeTime set to false if you want to exclude time portion of the two dates - * @param includeDate set to false if you want to exclude the date portion of the two dates - * @returns true if provided value is less than provided minValue - */ - public static lessThanMinValue(value: Date, minValue: Date, includeTime = true, includeDate = true): boolean { - // TODO: check if provided dates are valid dates and not Invalid Date - // if value is Invalid Date and minValue is valid date this will return: - // - false if includeDate is true - // - true if includeDate is false - if (includeTime && includeDate) { - return value.getTime() < minValue.getTime(); - } - - const _value = new Date(value.getTime()); - const _minValue = new Date(minValue.getTime()); - if (!includeTime) { - _value.setHours(0, 0, 0, 0); - _minValue.setHours(0, 0, 0, 0); - } - if (!includeDate) { - _value.setFullYear(0, 0, 0); - _minValue.setFullYear(0, 0, 0); - } - - return _value.getTime() < _minValue.getTime(); - } - - /** - * Validates a value within a given min and max value range. - * - * @param value The value to validate - * @param minValue The lowest possible value that `value` can take - * @param maxValue The largest possible value that `value` can take - */ - public static validateMinMax(value: Date, minValue: Date | string, maxValue: Date | string): ValidationErrors | null { - const errors = {}; - const min = DatePickerUtil.parseDate(minValue); - const max = DatePickerUtil.parseDate(maxValue); - if ((min && value && DatePickerUtil.lessThanMinValue(value, min, false)) - || (min && value && DatePickerUtil.lessThanMinValue(value, min, false))) { - Object.assign(errors, { minValue: true }); - } - if ((max && value && DatePickerUtil.greaterThanMaxValue(value, max, false)) - || (max && value && DatePickerUtil.greaterThanMaxValue(value, max, false))) { - Object.assign(errors, { maxValue: true }); - } - - return errors; - } - - /** - * This method generates date parts structure based on editor mask and locale. - * - * @param maskValue: string - * @param locale: string - * @returns array containing information about date parts - type, position, format - */ - public static parseDateFormat(maskValue: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): any[] { - let dateStruct = []; - if (maskValue === undefined && !isIE()) { - dateStruct = DatePickerUtil.getDefaultLocaleMask(locale); - } else { - const mask = (maskValue) ? maskValue : DatePickerUtil.SHORT_DATE_MASK; - const maskArray = Array.from(mask); - const monthInitPosition = mask.indexOf(DateChars.MonthChar); - const dayInitPosition = mask.indexOf(DateChars.DayChar); - const yearInitPosition = mask.indexOf(DateChars.YearChar); - - if (yearInitPosition !== -1) { - dateStruct.push({ - type: DateParts.Year, - initialPosition: yearInitPosition, - formatType: DatePickerUtil.getYearFormatType(mask) - }); - } - - if (monthInitPosition !== -1) { - dateStruct.push({ - type: DateParts.Month, - initialPosition: monthInitPosition, - formatType: DatePickerUtil.getMonthFormatType(mask) - }); - } - - if (dayInitPosition !== -1) { - dateStruct.push({ - type: DateParts.Day, - initialPosition: dayInitPosition, - formatType: DatePickerUtil.getDayFormatType(mask) - }); - } - - for (let i = 0; i < maskArray.length; i++) { - if (!DatePickerUtil.isDateChar(maskArray[i])) { - dateStruct.push({ - type: DatePickerUtil.SEPARATOR, - initialPosition: i, - value: maskArray[i] - }); - } - } - - dateStruct.sort((a, b) => a.initialPosition - b.initialPosition); - DatePickerUtil.fillDatePartsPositions(dateStruct); - } - return dateStruct; - } - - /** - * This method generates input mask based on date parts. - * - * @param dateStruct array - * @returns input mask - */ - public static getInputMask(dateStruct: any[]): string { - const inputMask = []; - for (const part of dateStruct) { - if (part.type === DatePickerUtil.SEPARATOR) { - inputMask.push(part.value); - } else if (part.type === DateParts.Day || part.type === DateParts.Month) { - inputMask.push('00'); - } else if (part.type === DateParts.Year) { - switch (part.formatType) { - case FormatDesc.Numeric: { - inputMask.push('0000'); - break; - } - case FormatDesc.TwoDigits: { - inputMask.push('00'); - break; - } - } - } - } - return inputMask.join(''); - } - - /** - * This method generates editor mask. - * - * @param dateStruct - * @returns editor mask - */ - public static getMask(dateStruct: any[]): string { - const mask = []; - for (const part of dateStruct) { - switch (part.formatType) { - case FormatDesc.Numeric: { - if (part.type === DateParts.Day) { - mask.push('d'); - } else if (part.type === DateParts.Month) { - mask.push('M'); - } else { - mask.push('yyyy'); - } - break; - } - case FormatDesc.TwoDigits: { - if (part.type === DateParts.Day) { - mask.push('dd'); - } else if (part.type === DateParts.Month) { - mask.push('MM'); - } else { - mask.push('yy'); - } - } - } - - if (part.type === DatePickerUtil.SEPARATOR) { - mask.push(part.value); - } - } - - return mask.join(''); - } - /** - * This method parses an input string base on date parts and returns a date and its validation state. - * - * @param dateFormatParts - * @param prevDateValue - * @param inputValue - * @returns object containing a date and its validation state - */ - public static parseDateArray(dateFormatParts: any[], prevDateValue: Date, inputValue: string): any { - const dayStr = DatePickerUtil.getDayValueFromInput(dateFormatParts, inputValue); - const monthStr = DatePickerUtil.getMonthValueFromInput(dateFormatParts, inputValue); - const yearStr = DatePickerUtil.getYearValueFromInput(dateFormatParts, inputValue); - const yearFormat = DatePickerUtil.getDateFormatPart(dateFormatParts, DateParts.Year).formatType; - const day = (dayStr !== '') ? parseInt(dayStr, 10) : 1; - const month = (monthStr !== '') ? parseInt(monthStr, 10) - 1 : 0; - - let year; - if (yearStr === '') { - year = (yearFormat === FormatDesc.TwoDigits) ? '00' : '2000'; - } else { - year = yearStr; - } - let yearPrefix; - if (prevDateValue) { - const originalYear = prevDateValue.getFullYear().toString(); - if (originalYear.length === 4) { - yearPrefix = originalYear.substring(0, 2); - } - } else { - yearPrefix = '20'; - } - const fullYear = (yearFormat === FormatDesc.TwoDigits) ? yearPrefix.concat(year) : year; - - if ((month < 0) || (month > 11) || isNaN(month)) { - return { state: DateState.Invalid, value: inputValue }; - } - - if ((day < 1) || (day > DatePickerUtil.daysInMonth(fullYear, month)) || isNaN(day)) { - return { state: DateState.Invalid, value: inputValue }; - } - - return { state: DateState.Valid, date: new Date(fullYear, month, day) }; - } - - public static maskToPromptChars(mask: string): string { - const result = mask.replace(/0|L/g, DatePickerUtil.PROMPT_CHAR); - return result; - } - - /** - * This method replaces prompt chars with empty string. - * - * @param value - */ - public static trimEmptyPlaceholders(value: string, promptChar?: string): string { - const result = value.replace(new RegExp(promptChar || '_', 'g'), ''); - return result; - } - - /** - * This method is used for spinning date parts. - * - * @param dateFormatParts - * @param inputValue - * @param position - * @param delta - * @param isSpinLoop - * @return modified text input - */ - public static getModifiedDateInput(dateFormatParts: any[], - inputValue: string, - position: number, - delta: number, - isSpinLoop: boolean): string { - const datePart = DatePickerUtil.getDatePartOnPosition(dateFormatParts, position); - const datePartType = datePart.type; - const datePartFormatType = datePart.formatType; - let newValue; - - const datePartValue = DatePickerUtil.getDateValueFromInput(dateFormatParts, datePartType, inputValue); - newValue = parseInt(datePartValue, 10); - - const minMax = DatePickerUtil.getMinMaxValue(dateFormatParts, datePart, inputValue); - const minValue = minMax.min; - const maxValue = minMax.max; - - if (isNaN(newValue)) { - if (minValue === 'infinite') { - newValue = 2000; - } else { - newValue = minValue; - } - } - let tempValue = newValue; - tempValue += delta; - - // Infinite loop for full years - if (maxValue === 'infinite' && minValue === 'infinite') { - newValue = tempValue; - } - - if (isSpinLoop) { - if (tempValue > maxValue) { - tempValue = minValue; - } - if (tempValue < minValue) { - tempValue = maxValue; - } - newValue = tempValue; - } else { - if (tempValue <= maxValue && tempValue >= minValue) { - newValue = tempValue; - } - } - - const startIdx = datePart.position[0]; - const endIdx = datePart.position[1]; - const start = inputValue.slice(0, startIdx); - const end = inputValue.slice(endIdx, inputValue.length); - const prefix = DatePickerUtil.getNumericFormatPrefix(datePartFormatType); - const changedPart = (newValue < 10) ? `${prefix}${newValue}` : `${newValue}`; - - return `${start}${changedPart}${end}`; - } - - /** - * This method returns date input with prompt chars. - * - * @param dateFormatParts - * @param date - * @param inputValue - * @returns date input including prompt chars - */ - public static addPromptCharsEditMode(dateFormatParts: any[], date: Date, inputValue: string): string { - const dateArray = Array.from(inputValue); - for (const part of dateFormatParts) { - if (part.formatType === FormatDesc.Numeric) { - if ((part.type === DateParts.Day && date.getDate() < 10) - || (part.type === DateParts.Month && date.getMonth() + 1 < 10)) { - dateArray.splice(part.position[0], 0, DatePickerUtil.PROMPT_CHAR); - dateArray.join(''); - } - } - } - return dateArray.join(''); - } - - /** - * This method checks if date input is done. - * - * @param dateFormatParts - * @param input - * @returns input completeness - */ - public static checkForCompleteDateInput(dateFormatParts: any[], input: string): string { - const dayValue = DatePickerUtil.getDayValueFromInput(dateFormatParts, input); - const monthValue = DatePickerUtil.getMonthValueFromInput(dateFormatParts, input); - const yearValue = DatePickerUtil.getYearValueFromInput(dateFormatParts, input); - const dayStr = DatePickerUtil.getDayValueFromInput(dateFormatParts, input, false); - const monthStr = DatePickerUtil.getMonthValueFromInput(dateFormatParts, input, false); - - if (DatePickerUtil.isFullInput(dayValue, dayStr) - && DatePickerUtil.isFullInput(monthValue, monthStr) - && DatePickerUtil.isFullYearInput(dateFormatParts, yearValue)) { - return 'complete'; - } else if (dayValue === '' && monthValue === '' && yearValue === '') { - return 'empty'; - } else if (dayValue === '' || monthValue === '' || yearValue === '') { - return 'partial'; - } - return ''; - } - - public static daysInMonth(fullYear: number, month: number): number { - return new Date(fullYear, month + 1, 0).getDate(); - } - - /** - * Parse provided input to Date. - * - * @param value input to parse - * @returns Date if parse succeed or null - */ - public static parseDate(value: any): Date | null { - if (typeof value === 'number') { - return new Date(value); - } - - // if value is Invalid Date we should return null - if (DatePickerUtil.isDate(value)) { - return DatePickerUtil.isValidDate(value) ? value : null; - } - - return value ? new Date(Date.parse(value)) : null; - } - - /** - * Returns whether provided input is date - * - * @param value input to check - * @returns true if provided input is date - */ - public static isDate(value: any): boolean { - return Object.prototype.toString.call(value) === '[object Date]'; - } - - /** - * Returns whether the input is valid date - * - * @param value input to check - * @returns true if provided input is a valid date - */ - public static isValidDate(value: any): boolean { - if (DatePickerUtil.isDate(value)) { - return !isNaN(value.getTime()); - } - - return false; - } - - 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 ensureLeadingZero(part: DatePartInfo) { - switch (part.type) { - case DatePart.Date: - case DatePart.Month: - case DatePart.Hours: - case DatePart.Minutes: - case DatePart.Seconds: - if (part.format.length === 1) { - part.format = part.format.repeat(2); - } - break; - } - } - - private static getCleanVal(inputData: string, datePart: DatePartInfo, promptChar?: string): string { - return DatePickerUtil.trimEmptyPlaceholders(inputData.substring(datePart.start, datePart.end), promptChar); - } - - private static determineDatePart(char: string): DatePart { - switch (char) { - case 'd': - case 'D': - return DatePart.Date; - case 'M': - return DatePart.Month; - case 'y': - case 'Y': - return DatePart.Year; - case 'h': - case 'H': - return DatePart.Hours; - case 'm': - return DatePart.Minutes; - case 's': - case 'S': - return DatePart.Seconds; - case 't': - case 'T': - return DatePart.AmPm; - default: - return DatePart.Literal; - } - } - - private static getYearFormatType(format: string): string { - switch (format.match(new RegExp(DateChars.YearChar, 'g')).length) { - case 1: { - // y (2020) - return FormatDesc.Numeric; - } - case 4: { - // yyyy (2020) - return FormatDesc.Numeric; - } - case 2: { - // yy (20) - return FormatDesc.TwoDigits; - } - } - } - - private static getMonthFormatType(format: string): string { - switch (format.match(new RegExp(DateChars.MonthChar, 'g')).length) { - case 1: { - // M (8) - return FormatDesc.Numeric; - } - case 2: { - // MM (08) - return FormatDesc.TwoDigits; - } - } - } - - private static getDayFormatType(format: string): string { - switch (format.match(new RegExp(DateChars.DayChar, 'g')).length) { - case 1: { - // d (6) - return FormatDesc.Numeric; - } - case 2: { - // dd (06) - return FormatDesc.TwoDigits; - } - } - } - - private static getDefaultLocaleMask(locale: string) { - const dateStruct = []; - const formatter = new Intl.DateTimeFormat(locale); - const formatToParts = formatter.formatToParts(new Date()); - for (const part of formatToParts) { - if (part.type === DatePickerUtil.SEPARATOR) { - dateStruct.push({ - type: DatePickerUtil.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; - } - } - } - DatePickerUtil.fillDatePartsPositions(dateStruct); - return dateStruct; - } - - private static isDateChar(char: string): boolean { - return (char === DateChars.YearChar || char === DateChars.MonthChar || char === DateChars.DayChar); - } - - private static getNumericFormatPrefix(formatType: string): string { - switch (formatType) { - case FormatDesc.TwoDigits: { - return '0'; - } - case FormatDesc.Numeric: { - return DatePickerUtil.PROMPT_CHAR; - } - } - } - - private static getMinMaxValue(dateFormatParts: any[], datePart, inputValue: string): any { - let maxValue; let minValue; - switch (datePart.type) { - case DateParts.Month: { - minValue = 1; - maxValue = DatePickerUtil.NUMBER_OF_MONTHS; - break; - } - case DateParts.Day: { - minValue = 1; - maxValue = DatePickerUtil.daysInMonth( - DatePickerUtil.getFullYearFromString(DatePickerUtil.getDateFormatPart(dateFormatParts, DateParts.Year), inputValue), - parseInt(DatePickerUtil.getMonthValueFromInput(dateFormatParts, inputValue), 10)); - break; - } - case DateParts.Year: { - if (datePart.formatType === FormatDesc.TwoDigits) { - minValue = 0; - maxValue = 99; - } else { - // Infinite loop - minValue = 'infinite'; - maxValue = 'infinite'; - } - break; - } - } - return { min: minValue, max: maxValue }; - } - - private static getDateValueFromInput(dateFormatParts: any[], type: DateParts, inputValue: string, trim: boolean = true): string { - const partPosition = DatePickerUtil.getDateFormatPart(dateFormatParts, type).position; - const result = inputValue.substring(partPosition[0], partPosition[1]); - return (trim) ? DatePickerUtil.trimEmptyPlaceholders(result) : result; - } - - private static getDayValueFromInput(dateFormatParts: any[], inputValue: string, trim: boolean = true): string { - return DatePickerUtil.getDateValueFromInput(dateFormatParts, DateParts.Day, inputValue, trim); - } - - private static getMonthValueFromInput(dateFormatParts: any[], inputValue: string, trim: boolean = true): string { - return DatePickerUtil.getDateValueFromInput(dateFormatParts, DateParts.Month, inputValue, trim); - } - - private static getYearValueFromInput(dateFormatParts: any[], inputValue: string, trim: boolean = true): string { - return DatePickerUtil.getDateValueFromInput(dateFormatParts, DateParts.Year, inputValue, trim); - } - - private static getDateFormatPart(dateFormatParts: any[], type: DateParts): any { - const result = dateFormatParts.filter((datePart) => (datePart.type === type))[0]; - return result; - } - - private static isFullInput(value: any, input: string): boolean { - return (value !== '' && input.length === 2 && input.charAt(1) !== DatePickerUtil.PROMPT_CHAR); - } - - private static isFullYearInput(dateFormatParts: any[], value: any): boolean { - switch (DatePickerUtil.getDateFormatPart(dateFormatParts, DateParts.Year).formatType) { - case FormatDesc.Numeric: { - return (value !== '' && value.length === 4); - } - case FormatDesc.TwoDigits: { - return (value !== '' && value.length === 2); - } - default: { - return false; - } - } - } - - private static getDatePartOnPosition(dateFormatParts: any[], position: number) { - const result = dateFormatParts.filter((element) => - element.position[0] <= position && position <= element.position[1] && element.type !== DatePickerUtil.SEPARATOR)[0]; - return result; - } - - private static getFullYearFromString(yearPart, inputValue): number { - return parseInt(inputValue.substring(yearPart.position[0], yearPart.position[1]), 10); - } - - private static fillDatePartsPositions(dateArray: any[]): void { - let currentPos = 0; - - for (const part of dateArray) { - // Day|Month part positions - if (part.type === DateParts.Day || part.type === DateParts.Month) { - // 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 === DatePickerUtil.SEPARATOR) { - // Separator positions - part.position = [currentPos, currentPos + 1]; - currentPos++; - } - } - } -} - diff --git a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker-inputs.common.ts b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker-inputs.common.ts index fa4ae48cbb4..f8a0adac1c7 100644 --- a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker-inputs.common.ts +++ b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker-inputs.common.ts @@ -3,7 +3,7 @@ import { NgControl } from '@angular/forms'; import { IgxInputDirective, IgxInputState } from '../input-group/public_api'; import { IgxInputGroupComponent } from '../input-group/input-group.component'; import { IgxInputGroupBase } from '../input-group/input-group.common'; -import { DatePickerUtil } from '../date-picker/date-picker.utils'; +import { DateTimeUtil } from '../date-common/util/date-time.util'; import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/public_api'; /** Represents a range between two dates. */ @@ -24,8 +24,8 @@ export class DateRangePickerFormatPipe implements PipeTransform { return formatter(values); } const { start, end } = values; - const startDate = appliedFormat ? DatePickerUtil.formatDate(start, appliedFormat, locale || 'en') : start?.toLocaleDateString(); - const endDate = appliedFormat ? DatePickerUtil.formatDate(end, appliedFormat, locale || 'en') : end?.toLocaleDateString(); + const startDate = appliedFormat ? DateTimeUtil.formatDate(start, appliedFormat, locale || 'en') : start?.toLocaleDateString(); + const endDate = appliedFormat ? DateTimeUtil.formatDate(end, appliedFormat, locale || 'en') : end?.toLocaleDateString(); let formatted; if (start) { formatted = `${startDate} - `; diff --git a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts index e00577b40d2..20de3c7ce95 100644 --- a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts +++ b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts @@ -17,11 +17,11 @@ import { DateRangeType } from '../core/dates'; import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; import { InteractionMode } from '../core/enums'; import { CurrentResourceStrings } from '../core/i18n/resources'; -import { IBaseCancelableBrowserEventArgs, KEYS } from '../core/utils'; +import { DateTimeUtil } from '../date-common/util/date-time.util'; +import { IBaseCancelableBrowserEventArgs, KEYS, parseDate } from '../core/utils'; import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component'; import { IgxPickerActionsDirective, IgxPickerToggleComponent } from '../date-common/picker-icons.common'; import { PickersBaseDirective } from '../date-common/pickers-base.directive'; -import { DatePickerUtil } from '../date-picker/date-picker.utils'; import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; import { IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState, @@ -333,8 +333,8 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective /** @hidden @internal */ public get appliedFormat(): string { - return DatePickerUtil.getLocaleDateFormat(this.locale, this.displayFormat) - || DatePickerUtil.DEFAULT_INPUT_FORMAT; + return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat) + || DateTimeUtil.DEFAULT_INPUT_FORMAT; } /** @hidden @internal */ @@ -584,16 +584,16 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective } } - const min = DatePickerUtil.parseDate(this.minValue); - const max = DatePickerUtil.parseDate(this.maxValue); - const start = DatePickerUtil.parseDate(value.start); - const end = DatePickerUtil.parseDate(value.end); - if ((min && start && DatePickerUtil.lessThanMinValue(start, min, false)) - || (min && end && DatePickerUtil.lessThanMinValue(end, min, false))) { + const min = parseDate(this.minValue); + const max = parseDate(this.maxValue); + const start = parseDate(value.start); + const end = parseDate(value.end); + if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false)) + || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) { Object.assign(errors, { minValue: true }); } - if ((max && start && DatePickerUtil.greaterThanMaxValue(start, max, false)) - || (max && end && DatePickerUtil.greaterThanMaxValue(end, max, false))) { + if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false)) + || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) { Object.assign(errors, { maxValue: true }); } } @@ -651,8 +651,8 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective /** @hidden @internal */ public ngOnChanges(changes: SimpleChanges): void { - if (changes['locale']) { - this.inputFormat = DatePickerUtil.getDefaultInputFormat(this.locale || 'en') || DatePickerUtil.DEFAULT_INPUT_FORMAT; + if (changes['locale'] && !this.inputFormat) { + this.inputFormat = DateTimeUtil.getDefaultInputFormat(this.locale); } if (changes['displayFormat'] && this.hasProjectedInputs) { this.updateDisplayFormat(); @@ -826,11 +826,11 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective } private parseMinValue(value: string | Date): Date | null { - let minValue: Date = DatePickerUtil.parseDate(value); + let minValue: Date = parseDate(value); if (!minValue && this.hasProjectedInputs) { const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0]; if (start) { - minValue = DatePickerUtil.parseDate(start.dateTimeEditor.minValue); + minValue = parseDate(start.dateTimeEditor.minValue); } } @@ -838,11 +838,11 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective } private parseMaxValue(value: string | Date): Date | null { - let maxValue: Date = DatePickerUtil.parseDate(value); + let maxValue: Date = parseDate(value); if (!maxValue && this.projectedInputs) { const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0]; if (end) { - maxValue = DatePickerUtil.parseDate(end.dateTimeEditor.maxValue); + maxValue = parseDate(end.dateTimeEditor.maxValue); } } @@ -862,7 +862,7 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective const range: Date[] = []; if (this.value?.start && this.value?.end) { - if (DatePickerUtil.greaterThanMaxValue(this.value.start, this.value.end)) { + if (DateTimeUtil.greaterThanMaxValue(this.value.start, this.value.end)) { this.swapEditorDates(); } if (this.valueInRange(this.value, minValue, maxValue)) { @@ -888,10 +888,10 @@ export class IgxDateRangePickerComponent extends PickersBaseDirective } private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean { - if (minValue && DatePickerUtil.lessThanMinValue(value.start, minValue, false)) { + if (minValue && DateTimeUtil.lessThanMinValue(value.start, minValue, false)) { return false; } - if (maxValue && DatePickerUtil.greaterThanMaxValue(value.end, maxValue, false)) { + if (maxValue && DateTimeUtil.greaterThanMaxValue(value.end, maxValue, false)) { return false; } diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts index 237323b99af..610249980cc 100644 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts @@ -11,13 +11,11 @@ import { import { DOCUMENT } from '@angular/common'; import { IgxMaskDirective } from '../mask/mask.directive'; import { MaskParsingService } from '../mask/mask-parsing.service'; -import { KEYS } from '../../core/utils'; -import { - DatePickerUtil -} from '../../date-picker/date-picker.utils'; +import { isDate, KEYS } from '../../core/utils'; import { IgxDateTimeEditorEventArgs, DatePartInfo, DatePart } from './date-time-editor.common'; import { noop } from 'rxjs'; import { DatePartDeltas } from './date-time-editor.common'; +import { DateTimeUtil } from '../../date-common/util/date-time.util'; /** * Date Time Editor provides a functionality to input, edit and format date and time. @@ -151,7 +149,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh if (value) { this._format = value; } - const mask = (this.inputFormat || DatePickerUtil.DEFAULT_INPUT_FORMAT) + const mask = (this.inputFormat || DateTimeUtil.DEFAULT_INPUT_FORMAT) .replace(new RegExp(/(?=[^t])[\w]/, 'g'), '0'); this.mask = mask.indexOf('tt') !== -1 ? mask.replace(new RegExp('tt', 'g'), 'LL') : mask; } @@ -359,15 +357,15 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh return { value: true }; } - const maxValueAsDate = this.isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue); - const minValueAsDate = this.isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue); + const maxValueAsDate = DateTimeUtil.isValidDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue); + const minValueAsDate = DateTimeUtil.isValidDate(this.minValue) ? this.minValue : this.parseDate(this.minValue); if (minValueAsDate - && DatePickerUtil.lessThanMinValue( + && DateTimeUtil.lessThanMinValue( control.value, minValueAsDate, this.hasTimeParts, this.hasDateParts)) { return { minValue: true }; } if (maxValueAsDate - && DatePickerUtil.greaterThanMaxValue( + && DateTimeUtil.greaterThanMaxValue( control.value, maxValueAsDate, this.hasTimeParts, this.hasDateParts)) { return { maxValue: true }; } @@ -399,13 +397,13 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh super.onInputChanged(); if (this.inputIsComplete()) { const parsedDate = this.parseDate(this.inputValue); - if (this.isValidDate(parsedDate)) { + if (DateTimeUtil.isValidDate(parsedDate)) { this.updateValue(parsedDate); } else { const oldValue = this.value && new Date(this.value.getTime()); - const args = { oldValue, newValue: parsedDate, userInput: this.inputValue }; + const args: IgxDateTimeEditorEventArgs = { oldValue, newValue: parsedDate, userInput: this.inputValue }; this.validationFailed.emit(args); - if (args.newValue?.getTime && args.newValue.getTime() !== oldValue.getTime()) { + if (DateTimeUtil.isValidDate(args.newValue)) { this.updateValue(args.newValue); } else { this.updateValue(null); @@ -463,13 +461,13 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh this.inputValue = this.getMaskedValue(); this.setSelectionRange(cursor); } else { - if (!this.value || !this.isValidDate(this.value)) { + if (!this.value || !DateTimeUtil.isValidDate(this.value)) { this.inputValue = ''; return; } const format = this.displayFormat || this.inputFormat; if (format) { - this.inputValue = DatePickerUtil.formatDate(this.value, format.replace('tt', 'aa'), this.locale); + this.inputValue = DateTimeUtil.formatDate(this.value, format.replace('tt', 'aa'), this.locale); } else { // TODO: formatter function? this.inputValue = this.value.toLocaleString(); @@ -477,12 +475,11 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh } } - // TODO: move parseDate to utils - public parseDate(val: string): Date | null { + private parseDate(val: string): Date | null { if (!val) { return null; } - return DatePickerUtil.parseValueFromMask(val, this._inputDateParts, this.promptChar); + return DateTimeUtil.parseValueFromMask(val, this._inputDateParts, this.promptChar); } private getMaskedValue(): string { @@ -504,8 +501,8 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh } private updateInputFormat(): void { - const defPlaceholder = this.inputFormat || DatePickerUtil.getDefaultInputFormat(this.locale); - this._inputDateParts = DatePickerUtil.parseDateTimeFormat(this.inputFormat); + const defPlaceholder = this.inputFormat || DateTimeUtil.getDefaultInputFormat(this.locale); + this._inputDateParts = DateTimeUtil.parseDateTimeFormat(this.inputFormat); this.inputFormat = this._inputDateParts.map(p => p.format).join(''); if (!this.nativeElement.placeholder || this._inputFormat !== this.inputFormat) { this.renderer.setAttribute(this.nativeElement, 'placeholder', defPlaceholder); @@ -515,24 +512,19 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh this._inputFormat = this.inputFormat; } - // TODO: move isDate to utils - private isDate(value: any): value is Date { - return value instanceof Date && typeof value === 'object'; - } - private valueInRange(value: Date): boolean { if (!value) { return false; } - const maxValueAsDate = this.isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue); - const minValueAsDate = this.isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue); + const maxValueAsDate = isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue); + const minValueAsDate = isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue); if (minValueAsDate - && DatePickerUtil.lessThanMinValue( + && DateTimeUtil.lessThanMinValue( value, minValueAsDate, this.hasTimeParts, this.hasDateParts)) { return false; } if (maxValueAsDate - && DatePickerUtil.greaterThanMaxValue( + && DateTimeUtil.greaterThanMaxValue( value, maxValueAsDate, this.hasTimeParts, this.hasDateParts)) { return false; } @@ -541,33 +533,33 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh } private spinValue(datePart: DatePart, delta: number): Date { - if (!this.value || !this.isValidDate(this.value)) { + if (!this.value || !DateTimeUtil.isValidDate(this.value)) { return null; } const newDate = new Date(this.value.getTime()); switch (datePart) { case DatePart.Date: - DatePickerUtil.spinDate(delta, newDate, this.isSpinLoop); + DateTimeUtil.spinDate(delta, newDate, this.isSpinLoop); break; case DatePart.Month: - DatePickerUtil.spinMonth(delta, newDate, this.isSpinLoop); + DateTimeUtil.spinMonth(delta, newDate, this.isSpinLoop); break; case DatePart.Year: - DatePickerUtil.spinYear(delta, newDate); + DateTimeUtil.spinYear(delta, newDate); break; case DatePart.Hours: - DatePickerUtil.spinHours(delta, newDate, this.isSpinLoop); + DateTimeUtil.spinHours(delta, newDate, this.isSpinLoop); break; case DatePart.Minutes: - DatePickerUtil.spinMinutes(delta, newDate, this.isSpinLoop); + DateTimeUtil.spinMinutes(delta, newDate, this.isSpinLoop); break; case DatePart.Seconds: - DatePickerUtil.spinSeconds(delta, newDate, this.isSpinLoop); + DateTimeUtil.spinSeconds(delta, newDate, this.isSpinLoop); break; case DatePart.AmPm: const formatPart = this._inputDateParts.find(dp => dp.type === DatePart.AmPm); const amPmFromMask = this.inputValue.substring(formatPart.start, formatPart.end); - return DatePickerUtil.spinAmPm(newDate, this.value, amPmFromMask); + return DateTimeUtil.spinAmPm(newDate, this.value, amPmFromMask); } return newDate; @@ -586,6 +578,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh this._oldValue = this.value; this.value = newDate; + // TODO: should we emit events here? if (this.value && !this.valueInRange(this.value)) { this.validationFailed.emit({ oldValue: this._oldValue, newValue: this.value, userInput: this.inputValue }); } @@ -672,10 +665,6 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh return this.inputValue.indexOf(this.promptChar) === -1; } - private isValidDate(date: Date): boolean { - return date && date.getTime && !isNaN(date.getTime()); - } - private moveCursor(event: KeyboardEvent): void { const value = (event.target as HTMLInputElement).value; switch (event.key) { diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-parsing.spec.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-parsing.spec.ts deleted file mode 100644 index f25a91a891f..00000000000 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-parsing.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { DatePickerUtil } from '../../date-picker/date-picker.utils'; -import { DatePart, DatePartInfo } from './date-time-editor.common'; - -const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => { - obj[x.type] = x; - return obj; -}, {}); - -describe('Date Time Parsing', () => { - it('should correctly parse all date time parts (base)', () => { - const result = DatePickerUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss tt'); - const expected = [ - { start: 0, end: 2, type: DatePart.Date, format: 'dd' }, - { start: 2, end: 3, type: DatePart.Literal, format: '/' }, - { start: 3, end: 5, type: DatePart.Month, format: 'MM' }, - { start: 5, end: 6, type: DatePart.Literal, format: '/' }, - { start: 6, end: 10, type: DatePart.Year, format: 'yyyy' }, - { start: 10, end: 11, type: DatePart.Literal, format: ' ' }, - { start: 11, end: 13, type: DatePart.Hours, format: 'HH' }, - { start: 13, end: 14, type: DatePart.Literal, format: ':' }, - { start: 14, end: 16, type: DatePart.Minutes, format: 'mm' }, - { start: 16, end: 17, type: DatePart.Literal, format: ':' }, - { start: 17, end: 19, type: DatePart.Seconds, format: 'ss' }, - { start: 19, end: 20, type: DatePart.Literal, format: ' ' }, - { start: 20, end: 22, type: DatePart.AmPm, format: 'tt' } - ]; - expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); - }); - - it('should correctly parse date parts of with short formats', () => { - let result = DatePickerUtil.parseDateTimeFormat('MM/dd/yyyy'); - let resDict = reduceToDictionary(result); - expect(result.length).toEqual(5); - expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); - expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); - expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); - - result = DatePickerUtil.parseDateTimeFormat('M/d/yy'); - resDict = reduceToDictionary(result); - expect(result.length).toEqual(5); - expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); - expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); - expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 8 })); - - result = DatePickerUtil.parseDateTimeFormat('dd.MM.yyyy г.'); - resDict = reduceToDictionary(result); - expect(result.length).toEqual(6); - expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); - expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); - expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); - - return; // TODO - result = DatePickerUtil.parseDateTimeFormat('dd.MM.yyyyг'); - resDict = reduceToDictionary(result); - expect(result.length).toEqual(6); - expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 0, end: 2 })); - expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 3, end: 5 })); - expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 6, end: 10 })); - expect(result[5]?.format).toEqual('г'); - - result = DatePickerUtil.parseDateTimeFormat('yyyy/MM/d'); - resDict = reduceToDictionary(result); - expect(result.length).toEqual(5); - expect(resDict[DatePart.Year]).toEqual(jasmine.objectContaining({ start: 0, end: 4 })); - expect(resDict[DatePart.Month]).toEqual(jasmine.objectContaining({ start: 5, end: 7 })); - expect(resDict[DatePart.Date]).toEqual(jasmine.objectContaining({ start: 8, end: 10 })); - }); - - it('should correctly parse boundary dates', () => { - const parts = DatePickerUtil.parseDateTimeFormat('MM/dd/yyyy'); - let result = DatePickerUtil.parseValueFromMask('08/31/2020', parts); - expect(result).toEqual(new Date(2020, 7, 31)); - result = DatePickerUtil.parseValueFromMask('09/30/2020', parts); - expect(result).toEqual(new Date(2020, 8, 30)); - result = DatePickerUtil.parseValueFromMask('10/31/2020', parts); - expect(result).toEqual(new Date(2020, 9, 31)); - }); -});