Skip to content

Commit 0a65630

Browse files
ddariboChronosSF
andauthored
Date/time editors: Add support for same input formats as Angular Date Pipe - AmPm (Period), Fractional Seconds parts (#14065)
--------- Co-authored-by: Stamen Stoychev <[email protected]>
1 parent 37578ef commit 0a65630

18 files changed

+373
-133
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes for each version of this project will be documented in this
66
### New Features
77
- `IgxCombo`, `IgxSimpleCombo`:
88
- Introduced abillity for hiding the clear icon button when the custom clear icon template is empty.
9+
- `IgxDateTimeEditor`, `IgxTimePicker`:
10+
- Now accept the following custom `inputFormat` options, as Angular's DatePipe:
11+
- Fractional seconds: S, SS, SSS.
12+
- Period (Am/Pm): a, aa, aaa, aaaa, aaaaa
913

1014
## 17.2.0
1115
### New Features

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

+70-19
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => {
99
describe(`DateTimeUtil Unit tests`, () => {
1010
describe('Date Time Parsing', () => {
1111
it('should correctly parse all date time parts (base)', () => {
12-
const result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss tt');
12+
let result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS a');
1313
const expected = [
1414
{ start: 0, end: 2, type: DatePart.Date, format: 'dd' },
1515
{ start: 2, end: 3, type: DatePart.Literal, format: '/' },
@@ -22,10 +22,16 @@ describe(`DateTimeUtil Unit tests`, () => {
2222
{ start: 14, end: 16, type: DatePart.Minutes, format: 'mm' },
2323
{ start: 16, end: 17, type: DatePart.Literal, format: ':' },
2424
{ start: 17, end: 19, type: DatePart.Seconds, format: 'ss' },
25-
{ start: 19, end: 20, type: DatePart.Literal, format: ' ' },
26-
{ start: 20, end: 22, type: DatePart.AmPm, format: 'tt' }
25+
{ start: 19, end: 20, type: DatePart.Literal, format: ':' },
26+
{ start: 20, end: 23, type: DatePart.FractionalSeconds, format: 'SSS' },
27+
{ start: 23, end: 24, type: DatePart.Literal, format: ' ' },
28+
{ start: 24, end: 26, type: DatePart.AmPm, format: 'aa' }
2729
];
2830
expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
31+
32+
result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS tt');
33+
expected[expected.length - 1] = { start: 24, end: 26, type: DatePart.AmPm, format: 'tt' }
34+
expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
2935
});
3036

3137
it('should correctly parse date parts of with short formats', () => {
@@ -112,27 +118,37 @@ describe(`DateTimeUtil Unit tests`, () => {
112118
expect(result).toEqual(new Date(2020, 9, 31));
113119
});
114120

115-
it('should correctly parse values in h:m:s tt format', () => {
121+
it('should correctly parse values in h:m:s a, aa,.. or h:m:s tt format', () => {
116122
const verifyTime = (val: Date, hours = 0, minutes = 0, seconds = 0, milliseconds = 0) => {
117123
expect(val.getHours()).toEqual(hours);
118124
expect(val.getMinutes()).toEqual(minutes);
119125
expect(val.getSeconds()).toEqual(seconds);
120126
expect(val.getMilliseconds()).toEqual(milliseconds);
121127
};
122128

123-
const parts = DateTimeUtil.parseDateTimeFormat('h:m:s tt');
124-
let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts);
125-
verifyTime(result, 11, 34, 12);
126-
result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts);
127-
verifyTime(result, 16, 12, 15);
128-
result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts);
129-
verifyTime(result, 11, 0, 0);
130-
result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts);
131-
verifyTime(result, 22, 0, 0);
132-
result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts);
133-
verifyTime(result, 12, 0, 0);
134-
result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts);
135-
verifyTime(result, 0, 0, 0);
129+
const runTestsForParts = (parts: DatePartInfo[]) => {
130+
let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts);
131+
verifyTime(result, 11, 34, 12);
132+
result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts);
133+
verifyTime(result, 16, 12, 15);
134+
result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts);
135+
verifyTime(result, 11, 0, 0);
136+
result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts);
137+
verifyTime(result, 22, 0, 0);
138+
result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts);
139+
verifyTime(result, 12, 0, 0);
140+
result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts);
141+
verifyTime(result, 0, 0, 0);
142+
}
143+
144+
const inputFormat = 'h:m:s';
145+
let parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} tt`);
146+
runTestsForParts(parts);
147+
148+
for (let i = 0; i < 5; i++) {
149+
parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} ${'a'.repeat(i + 1)}`);
150+
runTestsForParts(parts);
151+
}
136152
});
137153
});
138154

@@ -159,7 +175,7 @@ describe(`DateTimeUtil Unit tests`, () => {
159175
{ start: 5, end: 6, type: DatePart.Literal, format: ':' },
160176
{ start: 6, end: 8, type: DatePart.Seconds, format: 'ss' },
161177
{ start: 8, end: 9, type: DatePart.Literal, format: ' ' },
162-
{ start: 9, end: 11, type: DatePart.AmPm, format: 'tt' }
178+
{ start: 9, end: 11, type: DatePart.AmPm, format: 'a' }
163179
];
164180

165181
result = DateTimeUtil.parseValueFromMask(input, dateParts);
@@ -225,6 +241,7 @@ describe(`DateTimeUtil Unit tests`, () => {
225241
expect(DateTimeUtil.isDateOrTimeChar('h')).toBeTrue();
226242
expect(DateTimeUtil.isDateOrTimeChar('m')).toBeTrue();
227243
expect(DateTimeUtil.isDateOrTimeChar('s')).toBeTrue();
244+
expect(DateTimeUtil.isDateOrTimeChar('S')).toBeTrue();
228245
expect(DateTimeUtil.isDateOrTimeChar(':')).toBeFalse();
229246
expect(DateTimeUtil.isDateOrTimeChar('/')).toBeFalse();
230247
expect(DateTimeUtil.isDateOrTimeChar('.')).toBeFalse();
@@ -404,7 +421,35 @@ describe(`DateTimeUtil Unit tests`, () => {
404421
expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 57).getTime());
405422
});
406423

407-
it('should spin AM/PM portion correctly', () => {
424+
it('should spin fractional seconds portion correctly', () => {
425+
// base
426+
let date = new Date(2024, 3, 10, 6, 10, 5, 555);
427+
DateTimeUtil.spinFractionalSeconds(1, date, false);
428+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 556).getTime());
429+
DateTimeUtil.spinFractionalSeconds(-1, date, false);
430+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 555).getTime());
431+
432+
// delta !== 1
433+
DateTimeUtil.spinFractionalSeconds(5, date, false);
434+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 560).getTime());
435+
DateTimeUtil.spinFractionalSeconds(-6, date, false);
436+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 554).getTime());
437+
438+
// without looping over
439+
date = new Date(2024, 3, 10, 6, 10, 5, 999);
440+
DateTimeUtil.spinFractionalSeconds(1, date, false);
441+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 999).getTime());
442+
DateTimeUtil.spinFractionalSeconds(-1000, date, false);
443+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 0).getTime());
444+
445+
// with looping over (seconds are not affected)
446+
DateTimeUtil.spinFractionalSeconds(1001, date, true);
447+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 1).getTime());
448+
DateTimeUtil.spinFractionalSeconds(-5, date, true);
449+
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 996).getTime());
450+
});
451+
452+
it('should spin AM/PM and a/p portion correctly', () => {
408453
const currentDate = new Date(2015, 4, 31, 4, 59, 59);
409454
const newDate = new Date(2015, 4, 31, 4, 59, 59);
410455
// spin from AM to PM
@@ -414,6 +459,12 @@ describe(`DateTimeUtil Unit tests`, () => {
414459
// spin from PM to AM
415460
DateTimeUtil.spinAmPm(currentDate, newDate, 'AM');
416461
expect(currentDate.getHours()).toEqual(4);
462+
463+
DateTimeUtil.spinAmPm(currentDate, newDate, 'p');
464+
expect(currentDate.getHours()).toEqual(16);
465+
466+
DateTimeUtil.spinAmPm(currentDate, newDate, 'a');
467+
expect(currentDate.getHours()).toEqual(4);
417468
});
418469

419470
it('should compare dates correctly', () => {

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

+90-17
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ const enum FormatDesc {
99
TwoDigits = '2-digit'
1010
}
1111

12-
const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T'];
13-
const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y'];
12+
const TIME_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T', 'a'];
13+
const DATE_CHARS = ['d', 'D', 'M', 'y', 'Y'];
14+
15+
/** @hidden */
16+
const enum AmPmValues {
17+
AM = 'AM',
18+
A = 'a',
19+
PM = 'PM',
20+
P = 'p'
21+
}
1422

1523
/** @hidden */
1624
const enum DateParts {
@@ -56,7 +64,8 @@ export abstract class DateTimeUtil {
5664
return null;
5765
}
5866

59-
if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) {
67+
if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59
68+
|| parts[DatePart.Seconds] > 59 || parts[DatePart.FractionalSeconds] > 999) {
6069
return null;
6170
}
6271

@@ -65,8 +74,11 @@ export abstract class DateTimeUtil {
6574
parts[DatePart.Hours] %= 12;
6675
}
6776

68-
if (amPm && DateTimeUtil.getCleanVal(inputData, amPm, promptChar).toLowerCase() === 'pm') {
69-
parts[DatePart.Hours] += 12;
77+
if (amPm) {
78+
const cleanVal = DateTimeUtil.getCleanVal(inputData, amPm, promptChar);
79+
if (DateTimeUtil.isPm(cleanVal)) {
80+
parts[DatePart.Hours] += 12;
81+
}
7082
}
7183

7284
return new Date(
@@ -75,7 +87,8 @@ export abstract class DateTimeUtil {
7587
parts[DatePart.Date] || 1,
7688
parts[DatePart.Hours] || 0,
7789
parts[DatePart.Minutes] || 0,
78-
parts[DatePart.Seconds] || 0
90+
parts[DatePart.Seconds] || 0,
91+
parts[DatePart.FractionalSeconds] || 0
7992
);
8093
}
8194

@@ -86,7 +99,7 @@ export abstract class DateTimeUtil {
8699
const formatArray = Array.from(format);
87100
let currentPart: DatePartInfo = null;
88101
let position = 0;
89-
102+
let lastPartAdded = false;
90103
for (let i = 0; i < formatArray.length; i++, position++) {
91104
const type = DateTimeUtil.determineDatePart(formatArray[i]);
92105
if (currentPart) {
@@ -97,8 +110,15 @@ export abstract class DateTimeUtil {
97110
}
98111
}
99112

113+
if (currentPart.type === DatePart.AmPm && currentPart.format.indexOf('a') !== -1) {
114+
currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart);
115+
}
100116
DateTimeUtil.addCurrentPart(currentPart, dateTimeParts);
117+
lastPartAdded = true;
101118
position = currentPart.end;
119+
if(i === formatArray.length - 1 && currentPart.type !== type) {
120+
lastPartAdded = false;
121+
}
102122
}
103123

104124
currentPart = {
@@ -110,7 +130,10 @@ export abstract class DateTimeUtil {
110130
}
111131

112132
// make sure the last member of a format like H:m:s is not omitted
113-
if (!dateTimeParts.filter(p => p.format.includes(currentPart.format)).length) {
133+
if (!lastPartAdded) {
134+
if (currentPart.type === DatePart.AmPm) {
135+
currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart);
136+
}
114137
DateTimeUtil.addCurrentPart(currentPart, dateTimeParts);
115138
}
116139
// formats like "y" or "yyy" are treated like "yyyy" while editing
@@ -123,6 +146,13 @@ export abstract class DateTimeUtil {
123146
return dateTimeParts;
124147
}
125148

149+
/** Simplifies the AmPm part to as many chars as will be displayed */
150+
private static simplifyAmPmFormat(currentPart: DatePartInfo){
151+
currentPart.format = currentPart.format.length === 5 ? 'a' : 'aa';
152+
currentPart.end = currentPart.start + currentPart.format.length;
153+
return { ...currentPart };
154+
}
155+
126156
public static getPartValue(value: Date, datePartInfo: DatePartInfo, partLength: number): string {
127157
let maskedValue;
128158
const datePart = datePartInfo.type;
@@ -156,8 +186,11 @@ export abstract class DateTimeUtil {
156186
case DatePart.Seconds:
157187
maskedValue = value.getSeconds();
158188
break;
189+
case DatePart.FractionalSeconds:
190+
maskedValue = value.getMilliseconds();
191+
break;
159192
case DatePart.AmPm:
160-
maskedValue = value.getHours() >= 12 ? 'PM' : 'AM';
193+
maskedValue = DateTimeUtil.getAmPmValue(partLength, value.getHours() < 12);
161194
break;
162195
}
163196

@@ -168,6 +201,29 @@ export abstract class DateTimeUtil {
168201
return maskedValue;
169202
}
170203

204+
/** Returns the AmPm part value depending on the part length and a
205+
* conditional expression indicating whether the value is AM or PM.
206+
*/
207+
public static getAmPmValue(partLength: number, isAm: boolean) {
208+
if (isAm) {
209+
return partLength === 1 ? AmPmValues.A : AmPmValues.AM;
210+
} else {
211+
return partLength === 1 ? AmPmValues.P : AmPmValues.PM;
212+
}
213+
}
214+
215+
/** Returns true if a string value indicates an AM period */
216+
public static isAm(value: string) {
217+
value = value.toLowerCase();
218+
return (value === AmPmValues.AM.toLowerCase() || value === AmPmValues.A.toLowerCase());
219+
}
220+
221+
/** Returns true if a string value indicates a PM period */
222+
public static isPm(value: string) {
223+
value = value.toLowerCase();
224+
return (value === AmPmValues.PM.toLowerCase() || value === AmPmValues.P.toLowerCase());
225+
}
226+
171227
/** Builds a date-time editor's default input format based on provided locale settings. */
172228
public static getDefaultInputFormat(locale: string): string {
173229
locale = locale || DateTimeUtil.DEFAULT_LOCALE;
@@ -311,16 +367,28 @@ export abstract class DateTimeUtil {
311367
newDate.setSeconds(seconds);
312368
}
313369

370+
/** Spins the fractional seconds (milliseconds) portion in a date-time editor. */
371+
public static spinFractionalSeconds(delta: number, newDate: Date, spinLoop: boolean) {
372+
const maxMs = 999;
373+
const minMs = 0;
374+
let ms = newDate.getMilliseconds() + delta;
375+
if (ms > maxMs) {
376+
ms = spinLoop ? ms % maxMs - 1 : maxMs;
377+
} else if (ms < minMs) {
378+
ms = spinLoop ? maxMs + (ms % maxMs) + 1 : minMs;
379+
}
380+
381+
newDate.setMilliseconds(ms);
382+
}
383+
314384
/** Spins the AM/PM portion in a date-time editor. */
315385
public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date {
316-
switch (amPmFromMask) {
317-
case 'AM':
318-
newDate = new Date(newDate.setHours(newDate.getHours() + 12));
319-
break;
320-
case 'PM':
321-
newDate = new Date(newDate.setHours(newDate.getHours() - 12));
322-
break;
386+
if(DateTimeUtil.isAm(amPmFromMask)) {
387+
newDate = new Date(newDate.setHours(newDate.getHours() + 12));
388+
} else if(DateTimeUtil.isPm(amPmFromMask)) {
389+
newDate = new Date(newDate.setHours(newDate.getHours() - 12));
323390
}
391+
324392
if (newDate.getDate() !== currentDate.getDate()) {
325393
return currentDate;
326394
}
@@ -517,6 +585,9 @@ export abstract class DateTimeUtil {
517585
part.format = part.format.repeat(2);
518586
}
519587
break;
588+
case DatePart.FractionalSeconds:
589+
part.format = part.format[0].repeat(3);
590+
break;
520591
}
521592
}
522593

@@ -540,8 +611,10 @@ export abstract class DateTimeUtil {
540611
case 'm':
541612
return DatePart.Minutes;
542613
case 's':
543-
case 'S':
544614
return DatePart.Seconds;
615+
case 'S':
616+
return DatePart.FractionalSeconds;
617+
case 'a':
545618
case 't':
546619
case 'T':
547620
return DatePart.AmPm;

Diff for: projects/igniteui-angular/src/lib/directives/date-time-editor/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public datePart: typeof DatePart = DatePart;
6868
```
6969
```html
7070
<igx-input-group>
71-
<input igxInput #timeEditor="igxDateTimeEditor" type="text" [igxDateTimeEditor]="'HH:mm tt'" [(ngModel)]="date">
71+
<input igxInput #timeEditor="igxDateTimeEditor" type="text" [igxDateTimeEditor]="'HH:mm a'" [(ngModel)]="date">
7272
<igx-suffix>
7373
<igx-icon (click)="timeEditor.increment(datePart.Minutes)">keyboard_arrow_up</igx-icon>
7474
<igx-icon (click)="timeEditor.decrement(datePart.Minutes)">keyboard_arrow_down</igx-icon>

Diff for: projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum DatePart {
1414
Hours = 'hours',
1515
Minutes = 'minutes',
1616
Seconds = 'seconds',
17+
FractionalSeconds = 'fractionalSeconds',
1718
AmPm = 'ampm',
1819
Literal = 'literal'
1920
}
@@ -34,4 +35,5 @@ export interface DatePartDeltas {
3435
hours?: number;
3536
minutes?: number;
3637
seconds?: number;
38+
fractionalSeconds?: number;
3739
}

0 commit comments

Comments
 (0)