Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date/time editors: Add support for same input formats as Angular Date Pipe - AmPm (Period), Fractional Seconds parts #14065

Merged
merged 23 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d4291f3
chore(date/time editors): refactor tests for 'a', 'aa',... input format
ddaribo Apr 5, 2024
bd35093
feat(date/time editors): add support for a,aa,... inputFormat for AmPm
ddaribo Apr 5, 2024
9a85e24
chore(time-picker): update formats in dev samples
ddaribo Apr 5, 2024
19fd099
Merge branch 'master' into bpachilova/feat-14011
ddaribo Apr 5, 2024
e130eb4
refactor(date-time-util): fix variable name for date/time chars
ddaribo Apr 5, 2024
6df32e3
fix(date-time-util): remove redundant cases
ddaribo Apr 5, 2024
4ef85dc
Merge branch 'master' into bpachilova/feat-14011
ddaribo Apr 5, 2024
1612051
Merge branch 'master' into bpachilova/feat-14011
ChronosSF Apr 5, 2024
7c8582c
fix(date/time editors): keep 'tt' format as alias to 'a','aa', ..
ddaribo Apr 8, 2024
34ba51d
Merge branch 'master' into bpachilova/feat-14011
ddaribo Apr 8, 2024
5b2d65b
feat(date-time-editor): fractional seconds support
ddaribo Apr 10, 2024
5ab2f4e
feat(time-picker): support fractional seconds for input
ddaribo Apr 12, 2024
b087478
Merge branch 'master' into bpachilova/feat-14011
ChronosSF Apr 16, 2024
63ab334
Merge branch 'master' into bpachilova/feat-14011
ddaribo Apr 22, 2024
63a20e6
Merge branch 'master' into bpachilova/feat-14011
ddaribo Apr 30, 2024
6124934
Merge branch 'master' into bpachilova/feat-14011
ddaribo May 13, 2024
84e196d
Merge branch 'master' into bpachilova/feat-14011
ddaribo May 20, 2024
2a3c61e
refactor(date/time editors): keep 'tt' as default format
ddaribo May 20, 2024
0685a2b
refactor(date/time editors): handle default/inputFormat placeholder s…
ddaribo May 20, 2024
05d4427
refactor(date/time editors): add DateTimeUtil methods for AM/PM value…
ddaribo May 21, 2024
611b9f3
Merge branch 'master' into bpachilova/feat-14011
ddaribo May 21, 2024
a614705
fix(date-time.util): address failing test
ddaribo May 21, 2024
566ff89
chore(*): Update changelog
ddaribo May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes for each version of this project will be documented in this
### New Features
- `IgxCombo`, `IgxSimpleCombo`:
- Introduced abillity for hiding the clear icon button when the custom clear icon template is empty.
- `IgxDateTimeEditor`, `IgxTimePicker`:
- Now accept the following custom `inputFormat` options, as Angular's DatePipe:
- Fractional seconds: S, SS, SSS.
- Period (Am/Pm): a, aa, aaa, aaaa, aaaaa

## 17.2.0
### New Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => {
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');
let result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS a');
const expected = [
{ start: 0, end: 2, type: DatePart.Date, format: 'dd' },
{ start: 2, end: 3, type: DatePart.Literal, format: '/' },
Expand All @@ -22,10 +22,16 @@ describe(`DateTimeUtil Unit tests`, () => {
{ 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' }
{ start: 19, end: 20, type: DatePart.Literal, format: ':' },
{ start: 20, end: 23, type: DatePart.FractionalSeconds, format: 'SSS' },
{ start: 23, end: 24, type: DatePart.Literal, format: ' ' },
{ start: 24, end: 26, type: DatePart.AmPm, format: 'aa' }
];
expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));

result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS tt');
expected[expected.length - 1] = { start: 24, end: 26, type: DatePart.AmPm, format: 'tt' }
expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
});

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

it('should correctly parse values in h:m:s tt format', () => {
it('should correctly parse values in h:m:s a, aa,.. or h:m:s tt format', () => {
const verifyTime = (val: Date, hours = 0, minutes = 0, seconds = 0, milliseconds = 0) => {
expect(val.getHours()).toEqual(hours);
expect(val.getMinutes()).toEqual(minutes);
expect(val.getSeconds()).toEqual(seconds);
expect(val.getMilliseconds()).toEqual(milliseconds);
};

const parts = DateTimeUtil.parseDateTimeFormat('h:m:s tt');
let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts);
verifyTime(result, 11, 34, 12);
result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts);
verifyTime(result, 16, 12, 15);
result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts);
verifyTime(result, 11, 0, 0);
result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts);
verifyTime(result, 22, 0, 0);
result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts);
verifyTime(result, 12, 0, 0);
result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts);
verifyTime(result, 0, 0, 0);
const runTestsForParts = (parts: DatePartInfo[]) => {
let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts);
verifyTime(result, 11, 34, 12);
result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts);
verifyTime(result, 16, 12, 15);
result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts);
verifyTime(result, 11, 0, 0);
result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts);
verifyTime(result, 22, 0, 0);
result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts);
verifyTime(result, 12, 0, 0);
result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts);
verifyTime(result, 0, 0, 0);
}

const inputFormat = 'h:m:s';
let parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} tt`);
runTestsForParts(parts);

for (let i = 0; i < 5; i++) {
parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} ${'a'.repeat(i + 1)}`);
runTestsForParts(parts);
}
});
});

Expand All @@ -159,7 +175,7 @@ describe(`DateTimeUtil Unit tests`, () => {
{ 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' }
{ start: 9, end: 11, type: DatePart.AmPm, format: 'a' }
];

result = DateTimeUtil.parseValueFromMask(input, dateParts);
Expand Down Expand Up @@ -225,6 +241,7 @@ describe(`DateTimeUtil Unit tests`, () => {
expect(DateTimeUtil.isDateOrTimeChar('h')).toBeTrue();
expect(DateTimeUtil.isDateOrTimeChar('m')).toBeTrue();
expect(DateTimeUtil.isDateOrTimeChar('s')).toBeTrue();
expect(DateTimeUtil.isDateOrTimeChar('S')).toBeTrue();
expect(DateTimeUtil.isDateOrTimeChar(':')).toBeFalse();
expect(DateTimeUtil.isDateOrTimeChar('/')).toBeFalse();
expect(DateTimeUtil.isDateOrTimeChar('.')).toBeFalse();
Expand Down Expand Up @@ -404,7 +421,35 @@ describe(`DateTimeUtil Unit tests`, () => {
expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 57).getTime());
});

it('should spin AM/PM portion correctly', () => {
it('should spin fractional seconds portion correctly', () => {
// base
let date = new Date(2024, 3, 10, 6, 10, 5, 555);
DateTimeUtil.spinFractionalSeconds(1, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 556).getTime());
DateTimeUtil.spinFractionalSeconds(-1, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 555).getTime());

// delta !== 1
DateTimeUtil.spinFractionalSeconds(5, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 560).getTime());
DateTimeUtil.spinFractionalSeconds(-6, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 554).getTime());

// without looping over
date = new Date(2024, 3, 10, 6, 10, 5, 999);
DateTimeUtil.spinFractionalSeconds(1, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 999).getTime());
DateTimeUtil.spinFractionalSeconds(-1000, date, false);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 0).getTime());

// with looping over (seconds are not affected)
DateTimeUtil.spinFractionalSeconds(1001, date, true);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 1).getTime());
DateTimeUtil.spinFractionalSeconds(-5, date, true);
expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 996).getTime());
});

it('should spin AM/PM and a/p 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
Expand All @@ -414,6 +459,12 @@ describe(`DateTimeUtil Unit tests`, () => {
// spin from PM to AM
DateTimeUtil.spinAmPm(currentDate, newDate, 'AM');
expect(currentDate.getHours()).toEqual(4);

DateTimeUtil.spinAmPm(currentDate, newDate, 'p');
expect(currentDate.getHours()).toEqual(16);

DateTimeUtil.spinAmPm(currentDate, newDate, 'a');
expect(currentDate.getHours()).toEqual(4);
});

it('should compare dates correctly', () => {
Expand Down
107 changes: 90 additions & 17 deletions projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ const enum FormatDesc {
TwoDigits = '2-digit'
}

const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T'];
const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y'];
const TIME_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T', 'a'];
const DATE_CHARS = ['d', 'D', 'M', 'y', 'Y'];

/** @hidden */
const enum AmPmValues {
AM = 'AM',
A = 'a',
PM = 'PM',
P = 'p'
}

/** @hidden */
const enum DateParts {
Expand Down Expand Up @@ -56,7 +64,8 @@ export abstract class DateTimeUtil {
return null;
}

if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) {
if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59
|| parts[DatePart.Seconds] > 59 || parts[DatePart.FractionalSeconds] > 999) {
return null;
}

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

if (amPm && DateTimeUtil.getCleanVal(inputData, amPm, promptChar).toLowerCase() === 'pm') {
parts[DatePart.Hours] += 12;
if (amPm) {
const cleanVal = DateTimeUtil.getCleanVal(inputData, amPm, promptChar);
if (DateTimeUtil.isPm(cleanVal)) {
parts[DatePart.Hours] += 12;
}
}

return new Date(
Expand All @@ -75,7 +87,8 @@ export abstract class DateTimeUtil {
parts[DatePart.Date] || 1,
parts[DatePart.Hours] || 0,
parts[DatePart.Minutes] || 0,
parts[DatePart.Seconds] || 0
parts[DatePart.Seconds] || 0,
parts[DatePart.FractionalSeconds] || 0
);
}

Expand All @@ -86,7 +99,7 @@ export abstract class DateTimeUtil {
const formatArray = Array.from(format);
let currentPart: DatePartInfo = null;
let position = 0;

let lastPartAdded = false;
for (let i = 0; i < formatArray.length; i++, position++) {
const type = DateTimeUtil.determineDatePart(formatArray[i]);
if (currentPart) {
Expand All @@ -97,8 +110,15 @@ export abstract class DateTimeUtil {
}
}

if (currentPart.type === DatePart.AmPm && currentPart.format.indexOf('a') !== -1) {
currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart);
}
DateTimeUtil.addCurrentPart(currentPart, dateTimeParts);
lastPartAdded = true;
position = currentPart.end;
if(i === formatArray.length - 1 && currentPart.type !== type) {
lastPartAdded = false;
}
}

currentPart = {
Expand All @@ -110,7 +130,10 @@ export abstract class DateTimeUtil {
}

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

/** Simplifies the AmPm part to as many chars as will be displayed */
private static simplifyAmPmFormat(currentPart: DatePartInfo){
currentPart.format = currentPart.format.length === 5 ? 'a' : 'aa';
currentPart.end = currentPart.start + currentPart.format.length;
return { ...currentPart };
}

public static getPartValue(value: Date, datePartInfo: DatePartInfo, partLength: number): string {
let maskedValue;
const datePart = datePartInfo.type;
Expand Down Expand Up @@ -156,8 +186,11 @@ export abstract class DateTimeUtil {
case DatePart.Seconds:
maskedValue = value.getSeconds();
break;
case DatePart.FractionalSeconds:
maskedValue = value.getMilliseconds();
break;
case DatePart.AmPm:
maskedValue = value.getHours() >= 12 ? 'PM' : 'AM';
maskedValue = DateTimeUtil.getAmPmValue(partLength, value.getHours() < 12);
break;
}

Expand All @@ -168,6 +201,29 @@ export abstract class DateTimeUtil {
return maskedValue;
}

/** Returns the AmPm part value depending on the part length and a
* conditional expression indicating whether the value is AM or PM.
*/
public static getAmPmValue(partLength: number, isAm: boolean) {
if (isAm) {
return partLength === 1 ? AmPmValues.A : AmPmValues.AM;
} else {
return partLength === 1 ? AmPmValues.P : AmPmValues.PM;
}
}

/** Returns true if a string value indicates an AM period */
public static isAm(value: string) {
value = value.toLowerCase();
return (value === AmPmValues.AM.toLowerCase() || value === AmPmValues.A.toLowerCase());
}

/** Returns true if a string value indicates a PM period */
public static isPm(value: string) {
value = value.toLowerCase();
return (value === AmPmValues.PM.toLowerCase() || value === AmPmValues.P.toLowerCase());
}

/** 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;
Expand Down Expand Up @@ -311,16 +367,28 @@ export abstract class DateTimeUtil {
newDate.setSeconds(seconds);
}

/** Spins the fractional seconds (milliseconds) portion in a date-time editor. */
public static spinFractionalSeconds(delta: number, newDate: Date, spinLoop: boolean) {
const maxMs = 999;
const minMs = 0;
let ms = newDate.getMilliseconds() + delta;
if (ms > maxMs) {
ms = spinLoop ? ms % maxMs - 1 : maxMs;
} else if (ms < minMs) {
ms = spinLoop ? maxMs + (ms % maxMs) + 1 : minMs;
}

newDate.setMilliseconds(ms);
}

/** 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(DateTimeUtil.isAm(amPmFromMask)) {
newDate = new Date(newDate.setHours(newDate.getHours() + 12));
} else if(DateTimeUtil.isPm(amPmFromMask)) {
newDate = new Date(newDate.setHours(newDate.getHours() - 12));
}

if (newDate.getDate() !== currentDate.getDate()) {
return currentDate;
}
Expand Down Expand Up @@ -517,6 +585,9 @@ export abstract class DateTimeUtil {
part.format = part.format.repeat(2);
}
break;
case DatePart.FractionalSeconds:
part.format = part.format[0].repeat(3);
break;
}
}

Expand All @@ -540,8 +611,10 @@ export abstract class DateTimeUtil {
case 'm':
return DatePart.Minutes;
case 's':
case 'S':
return DatePart.Seconds;
case 'S':
return DatePart.FractionalSeconds;
case 'a':
case 't':
case 'T':
return DatePart.AmPm;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public datePart: typeof DatePart = DatePart;
```
```html
<igx-input-group>
<input igxInput #timeEditor="igxDateTimeEditor" type="text" [igxDateTimeEditor]="'HH:mm tt'" [(ngModel)]="date">
<input igxInput #timeEditor="igxDateTimeEditor" type="text" [igxDateTimeEditor]="'HH:mm a'" [(ngModel)]="date">
<igx-suffix>
<igx-icon (click)="timeEditor.increment(datePart.Minutes)">keyboard_arrow_up</igx-icon>
<igx-icon (click)="timeEditor.decrement(datePart.Minutes)">keyboard_arrow_down</igx-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum DatePart {
Hours = 'hours',
Minutes = 'minutes',
Seconds = 'seconds',
FractionalSeconds = 'fractionalSeconds',
AmPm = 'ampm',
Literal = 'literal'
}
Expand All @@ -34,4 +35,5 @@ export interface DatePartDeltas {
hours?: number;
minutes?: number;
seconds?: number;
fractionalSeconds?: number;
}
Loading
Loading