Skip to content

Commit 78ed8b8

Browse files
committed
refactor(material/core): add methods to date adapter
Expands the date adapter to include some methods that will be used in the future.
1 parent 8e17112 commit 78ed8b8

File tree

6 files changed

+431
-20
lines changed

6 files changed

+431
-20
lines changed

src/material/core/datetime/date-adapter.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export function MAT_DATE_LOCALE_FACTORY(): {} {
2020
return inject(LOCALE_ID);
2121
}
2222

23+
const NOT_IMPLEMENTED = 'Method not implemented';
24+
2325
/** Adapts type `D` to be usable as a date by cdk-based components that work with dates. */
2426
export abstract class DateAdapter<D, L = any> {
2527
/** The locale to use for all dates. */
@@ -195,6 +197,60 @@ export abstract class DateAdapter<D, L = any> {
195197
*/
196198
abstract invalid(): D;
197199

200+
/**
201+
* Sets the time of one date to the time of another.
202+
* @param target Date whose time will be set.
203+
* @param hours New hours to set on the date object.
204+
* @param minutes New minutes to set on the date object.
205+
* @param seconds New seconds to set on the date object.
206+
*/
207+
setTime(target: D, hours: number, minutes: number, seconds: number): D {
208+
throw new Error(NOT_IMPLEMENTED);
209+
}
210+
211+
/**
212+
* Gets the hours component of the given date.
213+
* @param date The date to extract the hours from.
214+
*/
215+
getHours(date: D): number {
216+
throw new Error(NOT_IMPLEMENTED);
217+
}
218+
219+
/**
220+
* Gets the minutes component of the given date.
221+
* @param date The date to extract the minutes from.
222+
*/
223+
getMinutes(date: D): number {
224+
throw new Error(NOT_IMPLEMENTED);
225+
}
226+
227+
/**
228+
* Gets the seconds component of the given date.
229+
* @param date The date to extract the seconds from.
230+
*/
231+
getSeconds(date: D): number {
232+
throw new Error(NOT_IMPLEMENTED);
233+
}
234+
235+
/**
236+
* Parses a date with a specific time from a user-provided value.
237+
* @param value The value to parse.
238+
* @param parseFormat The expected format of the value being parsed
239+
* (type is implementation-dependent).
240+
*/
241+
parseTime(value: any, parseFormat: any): D | null {
242+
throw new Error(NOT_IMPLEMENTED);
243+
}
244+
245+
/**
246+
* Adds an amount of milliseconds to the specified date.
247+
* @param date Date to which to add the milliseconds.
248+
* @param amount Amount of milliseconds to add to the date.
249+
*/
250+
addMilliseconds(date: D, amount: number): D {
251+
throw new Error(NOT_IMPLEMENTED);
252+
}
253+
198254
/**
199255
* Given a potential date object, returns that same date object if it is
200256
* a valid date, or `null` if it's not a valid date.
@@ -248,6 +304,21 @@ export abstract class DateAdapter<D, L = any> {
248304
);
249305
}
250306

307+
/**
308+
* Compares the time values of two dates.
309+
* @param first First date to compare.
310+
* @param second Second date to compare.
311+
* @returns 0 if the times are equal, a number less than 0 if the first time is earlier,
312+
* a number greater than 0 if the first time is later.
313+
*/
314+
compareTime(first: D, second: D): number {
315+
return (
316+
this.getHours(first) - this.getHours(second) ||
317+
this.getMinutes(first) - this.getMinutes(second) ||
318+
this.getSeconds(first) - this.getSeconds(second)
319+
);
320+
}
321+
251322
/**
252323
* Checks if two dates are equal.
253324
* @param first The first date to check.
@@ -267,6 +338,25 @@ export abstract class DateAdapter<D, L = any> {
267338
return first == second;
268339
}
269340

341+
/**
342+
* Checks if the times of two dates are equal.
343+
* @param first The first date to check.
344+
* @param second The second date to check.
345+
* @returns Whether the times of the two dates are equal.
346+
* Null dates are considered equal to other null dates.
347+
*/
348+
sameTime(first: D | null, second: D | null): boolean {
349+
if (first && second) {
350+
const firstValid = this.isValid(first);
351+
const secondValid = this.isValid(second);
352+
if (firstValid && secondValid) {
353+
return !this.compareTime(first, second);
354+
}
355+
return firstValid == secondValid;
356+
}
357+
return first == second;
358+
}
359+
270360
/**
271361
* Clamp the given date between min and max dates.
272362
* @param date The date to clamp.

src/material/core/datetime/date-formats.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import {InjectionToken} from '@angular/core';
1111
export type MatDateFormats = {
1212
parse: {
1313
dateInput: any;
14+
timeInput?: any;
1415
};
1516
display: {
1617
dateInput: any;
1718
monthLabel?: any;
1819
monthYearLabel: any;
1920
dateA11yLabel: any;
2021
monthYearA11yLabel: any;
22+
timeInput?: any;
23+
timeOptionLabel?: any;
2124
};
2225
};
2326

src/material/core/datetime/native-date-adapter.spec.ts

Lines changed: 184 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
import {LOCALE_ID} from '@angular/core';
2-
import {waitForAsync, inject, TestBed} from '@angular/core/testing';
2+
import {TestBed} from '@angular/core/testing';
3+
import {Platform} from '@angular/cdk/platform';
34
import {DEC, FEB, JAN, MAR} from '../../testing';
45
import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from './index';
56

67
describe('NativeDateAdapter', () => {
78
let adapter: NativeDateAdapter;
89
let assertValidDate: (d: Date | null, valid: boolean) => void;
10+
let platform: Platform;
911

10-
beforeEach(waitForAsync(() => {
11-
TestBed.configureTestingModule({
12-
imports: [NativeDateModule],
13-
});
14-
}));
15-
16-
beforeEach(inject([DateAdapter], (dateAdapter: NativeDateAdapter) => {
17-
adapter = dateAdapter;
12+
beforeEach(() => {
13+
TestBed.configureTestingModule({imports: [NativeDateModule]});
14+
adapter = TestBed.inject(DateAdapter) as NativeDateAdapter;
15+
platform = TestBed.inject(Platform);
1816

1917
assertValidDate = (d: Date | null, valid: boolean) => {
2018
expect(adapter.isDateInstance(d))
@@ -27,7 +25,7 @@ describe('NativeDateAdapter', () => {
2725
)
2826
.toBe(valid);
2927
};
30-
}));
28+
});
3129

3230
it('should get year', () => {
3331
expect(adapter.getYear(new Date(2017, JAN, 1))).toBe(2017);
@@ -464,21 +462,189 @@ describe('NativeDateAdapter', () => {
464462
it('should not throw when attempting to format a date with a year greater than 9999', () => {
465463
expect(() => adapter.format(new Date(10000, 1, 1), {})).not.toThrow();
466464
});
465+
466+
it('should get hours', () => {
467+
expect(adapter.getHours(new Date(2024, JAN, 1, 14))).toBe(14);
468+
});
469+
470+
it('should get minutes', () => {
471+
expect(adapter.getMinutes(new Date(2024, JAN, 1, 14, 53))).toBe(53);
472+
});
473+
474+
it('should get seconds', () => {
475+
expect(adapter.getSeconds(new Date(2024, JAN, 1, 14, 53, 42))).toBe(42);
476+
});
477+
478+
it('should set the time of a date', () => {
479+
const target = new Date(2024, JAN, 1, 0, 0, 0);
480+
const result = adapter.setTime(target, 14, 53, 42);
481+
expect(adapter.getHours(result)).toBe(14);
482+
expect(adapter.getMinutes(result)).toBe(53);
483+
expect(adapter.getSeconds(result)).toBe(42);
484+
});
485+
486+
it('should throw when passing in invalid hours to setTime', () => {
487+
expect(() => adapter.setTime(adapter.today(), -1, 0, 0)).toThrowError(
488+
'Invalid hours "-1". Hours value must be between 0 and 23.',
489+
);
490+
expect(() => adapter.setTime(adapter.today(), 51, 0, 0)).toThrowError(
491+
'Invalid hours "51". Hours value must be between 0 and 23.',
492+
);
493+
});
494+
495+
it('should throw when passing in invalid minutes to setTime', () => {
496+
expect(() => adapter.setTime(adapter.today(), 0, -1, 0)).toThrowError(
497+
'Invalid minutes "-1". Minutes value must be between 0 and 59.',
498+
);
499+
expect(() => adapter.setTime(adapter.today(), 0, 65, 0)).toThrowError(
500+
'Invalid minutes "65". Minutes value must be between 0 and 59.',
501+
);
502+
});
503+
504+
it('should throw when passing in invalid seconds to setTime', () => {
505+
expect(() => adapter.setTime(adapter.today(), 0, 0, -1)).toThrowError(
506+
'Invalid seconds "-1". Seconds value must be between 0 and 59.',
507+
);
508+
expect(() => adapter.setTime(adapter.today(), 0, 0, 65)).toThrowError(
509+
'Invalid seconds "65". Seconds value must be between 0 and 59.',
510+
);
511+
});
512+
513+
it('should parse a 24-hour time string', () => {
514+
const result = adapter.parseTime('14:52')!;
515+
expect(result).toBeTruthy();
516+
expect(adapter.isValid(result)).toBe(true);
517+
expect(adapter.getHours(result)).toBe(14);
518+
expect(adapter.getMinutes(result)).toBe(52);
519+
expect(adapter.getSeconds(result)).toBe(0);
520+
});
521+
522+
it('should parse a 12-hour time string', () => {
523+
const result = adapter.parseTime('2:52 PM')!;
524+
expect(result).toBeTruthy();
525+
expect(adapter.isValid(result)).toBe(true);
526+
expect(adapter.getHours(result)).toBe(14);
527+
expect(adapter.getMinutes(result)).toBe(52);
528+
expect(adapter.getSeconds(result)).toBe(0);
529+
});
530+
531+
it('should parse a 12-hour time string with seconds', () => {
532+
const result = adapter.parseTime('2:52:46 PM')!;
533+
expect(result).toBeTruthy();
534+
expect(adapter.isValid(result)).toBe(true);
535+
expect(adapter.getHours(result)).toBe(14);
536+
expect(adapter.getMinutes(result)).toBe(52);
537+
expect(adapter.getSeconds(result)).toBe(46);
538+
});
539+
540+
it('should parse a padded 12-hour time string', () => {
541+
const result = adapter.parseTime('02:52 PM')!;
542+
expect(result).toBeTruthy();
543+
expect(adapter.isValid(result)).toBe(true);
544+
expect(adapter.getHours(result)).toBe(14);
545+
expect(adapter.getMinutes(result)).toBe(52);
546+
expect(adapter.getSeconds(result)).toBe(0);
547+
});
548+
549+
it('should parse a padded time string', () => {
550+
const result = adapter.parseTime('03:04:05')!;
551+
expect(result).toBeTruthy();
552+
expect(adapter.isValid(result)).toBe(true);
553+
expect(adapter.getHours(result)).toBe(3);
554+
expect(adapter.getMinutes(result)).toBe(4);
555+
expect(adapter.getSeconds(result)).toBe(5);
556+
});
557+
558+
it('should parse a time string that uses dot as a separator', () => {
559+
const result = adapter.parseTime('14.52')!;
560+
expect(result).toBeTruthy();
561+
expect(adapter.isValid(result)).toBe(true);
562+
expect(adapter.getHours(result)).toBe(14);
563+
expect(adapter.getMinutes(result)).toBe(52);
564+
expect(adapter.getSeconds(result)).toBe(0);
565+
});
566+
567+
it('should parse a time string with characters around the time', () => {
568+
const result = adapter.parseTime('14:52 ч.')!;
569+
expect(result).toBeTruthy();
570+
expect(adapter.isValid(result)).toBe(true);
571+
expect(adapter.getHours(result)).toBe(14);
572+
expect(adapter.getMinutes(result)).toBe(52);
573+
expect(adapter.getSeconds(result)).toBe(0);
574+
});
575+
576+
it('should parse a 12-hour time string using a dot separator', () => {
577+
const result = adapter.parseTime('2.52.46 PM')!;
578+
expect(result).toBeTruthy();
579+
expect(adapter.isValid(result)).toBe(true);
580+
expect(adapter.getHours(result)).toBe(14);
581+
expect(adapter.getMinutes(result)).toBe(52);
582+
expect(adapter.getSeconds(result)).toBe(46);
583+
});
584+
585+
it('should return an invalid date when parsing invalid time string', () => {
586+
expect(adapter.isValid(adapter.parseTime('abc')!)).toBe(false);
587+
expect(adapter.isValid(adapter.parseTime('123')!)).toBe(false);
588+
expect(adapter.isValid(adapter.parseTime('14:52 PM')!)).toBe(false);
589+
expect(adapter.isValid(adapter.parseTime('24:05')!)).toBe(false);
590+
591+
// Firefox is a bit more forgiving of invalid times than other browsers.
592+
// E.g. these just roll over instead of producing an invalid object.
593+
if (!platform.FIREFOX) {
594+
expect(adapter.isValid(adapter.parseTime('00:61:05')!)).toBe(false);
595+
expect(adapter.isValid(adapter.parseTime('14:52:78')!)).toBe(false);
596+
}
597+
});
598+
599+
it('should return null when parsing unsupported time values', () => {
600+
expect(adapter.parseTime(321)).toBeNull();
601+
expect(adapter.parseTime('')).toBeNull();
602+
expect(adapter.parseTime(' ')).toBeNull();
603+
expect(adapter.parseTime(true)).toBeNull();
604+
expect(adapter.parseTime(undefined)).toBeNull();
605+
});
606+
607+
it('should compare times', () => {
608+
const base = [2024, JAN, 1] as const;
609+
610+
expect(
611+
adapter.compareTime(new Date(...base, 12, 0, 0), new Date(...base, 13, 0, 0)),
612+
).toBeLessThan(0);
613+
expect(
614+
adapter.compareTime(new Date(...base, 12, 50, 0), new Date(...base, 12, 51, 0)),
615+
).toBeLessThan(0);
616+
expect(adapter.compareTime(new Date(...base, 1, 2, 3), new Date(...base, 1, 2, 3))).toBe(0);
617+
expect(
618+
adapter.compareTime(new Date(...base, 13, 0, 0), new Date(...base, 12, 0, 0)),
619+
).toBeGreaterThan(0);
620+
expect(
621+
adapter.compareTime(new Date(...base, 12, 50, 11), new Date(...base, 12, 50, 10)),
622+
).toBeGreaterThan(0);
623+
expect(
624+
adapter.compareTime(new Date(...base, 13, 0, 0), new Date(...base, 10, 59, 59)),
625+
).toBeGreaterThan(0);
626+
});
627+
628+
it('should add milliseconds to a date', () => {
629+
const amount = 1234567;
630+
const initial = new Date(2024, JAN, 1, 12, 34, 56);
631+
const result = adapter.addMilliseconds(initial, amount);
632+
expect(result).not.toBe(initial);
633+
expect(result.getTime() - initial.getTime()).toBe(amount);
634+
});
467635
});
468636

469637
describe('NativeDateAdapter with MAT_DATE_LOCALE override', () => {
470638
let adapter: NativeDateAdapter;
471639

472-
beforeEach(waitForAsync(() => {
640+
beforeEach(() => {
473641
TestBed.configureTestingModule({
474642
imports: [NativeDateModule],
475643
providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}],
476644
});
477-
}));
478645

479-
beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => {
480-
adapter = d;
481-
}));
646+
adapter = TestBed.inject(DateAdapter) as NativeDateAdapter;
647+
});
482648

483649
it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => {
484650
const expectedValue = ['søndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag'];
@@ -489,16 +655,14 @@ describe('NativeDateAdapter with MAT_DATE_LOCALE override', () => {
489655
describe('NativeDateAdapter with LOCALE_ID override', () => {
490656
let adapter: NativeDateAdapter;
491657

492-
beforeEach(waitForAsync(() => {
658+
beforeEach(() => {
493659
TestBed.configureTestingModule({
494660
imports: [NativeDateModule],
495661
providers: [{provide: LOCALE_ID, useValue: 'da-DK'}],
496662
});
497-
}));
498663

499-
beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => {
500-
adapter = d;
501-
}));
664+
adapter = TestBed.inject(DateAdapter) as NativeDateAdapter;
665+
});
502666

503667
it('should cascade locale id from the LOCALE_ID injection token to MAT_DATE_LOCALE', () => {
504668
const expectedValue = ['søndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag'];

0 commit comments

Comments
 (0)