Skip to content

Commit 70cc558

Browse files
committed
refactor(material-luxon-adapter): implement new methods
Implements the new methods in the Luxon adapter.
1 parent 0e812f2 commit 70cc558

File tree

3 files changed

+209
-15
lines changed

3 files changed

+209
-15
lines changed

src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {LOCALE_ID} from '@angular/core';
10-
import {TestBed, waitForAsync} from '@angular/core/testing';
10+
import {TestBed} from '@angular/core/testing';
1111
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core';
1212
import {CalendarSystem, DateTime, FixedOffsetZone, Settings} from 'luxon';
1313
import {LuxonDateModule} from './index';
@@ -21,14 +21,11 @@ const JAN = 1,
2121
describe('LuxonDateAdapter', () => {
2222
let adapter: DateAdapter<DateTime>;
2323

24-
beforeEach(waitForAsync(() => {
25-
TestBed.configureTestingModule({
26-
imports: [LuxonDateModule],
27-
});
28-
24+
beforeEach(() => {
25+
TestBed.configureTestingModule({imports: [LuxonDateModule]});
2926
adapter = TestBed.inject(DateAdapter);
3027
adapter.setLocale('en-US');
31-
}));
28+
});
3229

3330
it('should get year', () => {
3431
expect(adapter.getYear(DateTime.local(2017, JAN, 1))).toBe(2017);
@@ -550,19 +547,158 @@ describe('LuxonDateAdapter', () => {
550547
it('should create invalid date', () => {
551548
assertValidDate(adapter, adapter.invalid(), false);
552549
});
550+
551+
it('should get hours', () => {
552+
expect(adapter.getHours(DateTime.local(2024, JAN, 1, 14))).toBe(14);
553+
});
554+
555+
it('should get minutes', () => {
556+
expect(adapter.getMinutes(DateTime.local(2024, JAN, 1, 14, 53))).toBe(53);
557+
});
558+
559+
it('should get seconds', () => {
560+
expect(adapter.getSeconds(DateTime.local(2024, JAN, 1, 14, 53, 42))).toBe(42);
561+
});
562+
563+
it('should set the time of a date', () => {
564+
const target = DateTime.local(2024, JAN, 1, 0, 0, 0);
565+
const result = adapter.setTime(target, 14, 53, 42);
566+
expect(adapter.getHours(result)).toBe(14);
567+
expect(adapter.getMinutes(result)).toBe(53);
568+
expect(adapter.getSeconds(result)).toBe(42);
569+
});
570+
571+
it('should throw when passing in invalid hours to setTime', () => {
572+
expect(() => adapter.setTime(adapter.today(), -1, 0, 0)).toThrowError(
573+
'Invalid hours "-1". Hours value must be between 0 and 23.',
574+
);
575+
expect(() => adapter.setTime(adapter.today(), 51, 0, 0)).toThrowError(
576+
'Invalid hours "51". Hours value must be between 0 and 23.',
577+
);
578+
});
579+
580+
it('should throw when passing in invalid minutes to setTime', () => {
581+
expect(() => adapter.setTime(adapter.today(), 0, -1, 0)).toThrowError(
582+
'Invalid minutes "-1". Minutes value must be between 0 and 59.',
583+
);
584+
expect(() => adapter.setTime(adapter.today(), 0, 65, 0)).toThrowError(
585+
'Invalid minutes "65". Minutes value must be between 0 and 59.',
586+
);
587+
});
588+
589+
it('should throw when passing in invalid seconds to setTime', () => {
590+
expect(() => adapter.setTime(adapter.today(), 0, 0, -1)).toThrowError(
591+
'Invalid seconds "-1". Seconds value must be between 0 and 59.',
592+
);
593+
expect(() => adapter.setTime(adapter.today(), 0, 0, 65)).toThrowError(
594+
'Invalid seconds "65". Seconds value must be between 0 and 59.',
595+
);
596+
});
597+
598+
it('should parse a 24-hour time string', () => {
599+
const result = adapter.parseTime('14:52', 't')!;
600+
expect(result).toBeTruthy();
601+
expect(adapter.isValid(result)).toBe(true);
602+
expect(adapter.getHours(result)).toBe(14);
603+
expect(adapter.getMinutes(result)).toBe(52);
604+
expect(adapter.getSeconds(result)).toBe(0);
605+
});
606+
607+
it('should parse a 12-hour time string', () => {
608+
const result = adapter.parseTime('2:52 PM', 't')!;
609+
expect(result).toBeTruthy();
610+
expect(adapter.isValid(result)).toBe(true);
611+
expect(adapter.getHours(result)).toBe(14);
612+
expect(adapter.getMinutes(result)).toBe(52);
613+
expect(adapter.getSeconds(result)).toBe(0);
614+
});
615+
616+
it('should parse a padded time string', () => {
617+
const result = adapter.parseTime('03:04:05', 'tt')!;
618+
expect(result).toBeTruthy();
619+
expect(adapter.isValid(result)).toBe(true);
620+
expect(adapter.getHours(result)).toBe(3);
621+
expect(adapter.getMinutes(result)).toBe(4);
622+
expect(adapter.getSeconds(result)).toBe(5);
623+
});
624+
625+
it('should parse a time string that uses dot as a separator', () => {
626+
adapter.setLocale('fi-FI');
627+
const result = adapter.parseTime('14.52', 't')!;
628+
expect(result).toBeTruthy();
629+
expect(adapter.isValid(result)).toBe(true);
630+
expect(adapter.getHours(result)).toBe(14);
631+
expect(adapter.getMinutes(result)).toBe(52);
632+
expect(adapter.getSeconds(result)).toBe(0);
633+
});
634+
635+
it('should parse a time string with characters around the time', () => {
636+
adapter.setLocale('bg-BG');
637+
const result = adapter.parseTime('14:52 ч.', 't')!;
638+
expect(result).toBeTruthy();
639+
expect(adapter.isValid(result)).toBe(true);
640+
expect(adapter.getHours(result)).toBe(14);
641+
expect(adapter.getMinutes(result)).toBe(52);
642+
expect(adapter.getSeconds(result)).toBe(0);
643+
});
644+
645+
it('should return an invalid date when parsing invalid time string', () => {
646+
expect(adapter.isValid(adapter.parseTime('abc', 't')!)).toBeFalse();
647+
expect(adapter.isValid(adapter.parseTime(' ', 't')!)).toBeFalse();
648+
expect(adapter.isValid(adapter.parseTime('24:05', 't')!)).toBeFalse();
649+
expect(adapter.isValid(adapter.parseTime('00:61:05', 'tt')!)).toBeFalse();
650+
expect(adapter.isValid(adapter.parseTime('14:52:78', 'tt')!)).toBeFalse();
651+
});
652+
653+
it('should return null when parsing unsupported time values', () => {
654+
expect(adapter.parseTime(true, 't')).toBeNull();
655+
expect(adapter.parseTime(undefined, 't')).toBeNull();
656+
expect(adapter.parseTime('', 't')).toBeNull();
657+
});
658+
659+
it('should compare times', () => {
660+
const base = [2024, JAN, 1] as const;
661+
662+
expect(
663+
adapter.compareTime(DateTime.local(...base, 12, 0, 0), DateTime.local(...base, 13, 0, 0)),
664+
).toBeLessThan(0);
665+
expect(
666+
adapter.compareTime(DateTime.local(...base, 12, 50, 0), DateTime.local(...base, 12, 51, 0)),
667+
).toBeLessThan(0);
668+
expect(
669+
adapter.compareTime(DateTime.local(...base, 1, 2, 3), DateTime.local(...base, 1, 2, 3)),
670+
).toBe(0);
671+
expect(
672+
adapter.compareTime(DateTime.local(...base, 13, 0, 0), DateTime.local(...base, 12, 0, 0)),
673+
).toBeGreaterThan(0);
674+
expect(
675+
adapter.compareTime(DateTime.local(...base, 12, 50, 11), DateTime.local(...base, 12, 50, 10)),
676+
).toBeGreaterThan(0);
677+
expect(
678+
adapter.compareTime(DateTime.local(...base, 13, 0, 0), DateTime.local(...base, 10, 59, 59)),
679+
).toBeGreaterThan(0);
680+
});
681+
682+
it('should add milliseconds to a date', () => {
683+
const amount = 1234567;
684+
const initial = DateTime.local(2024, JAN, 1, 12, 34, 56);
685+
const result = adapter.addMilliseconds(initial, amount);
686+
expect(result).not.toBe(initial);
687+
expect(result.toMillis() - initial.toMillis()).toBe(amount);
688+
});
553689
});
554690

555691
describe('LuxonDateAdapter with MAT_DATE_LOCALE override', () => {
556692
let adapter: DateAdapter<DateTime>;
557693

558-
beforeEach(waitForAsync(() => {
694+
beforeEach(() => {
559695
TestBed.configureTestingModule({
560696
imports: [LuxonDateModule],
561697
providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}],
562698
});
563699

564700
adapter = TestBed.inject(DateAdapter);
565-
}));
701+
});
566702

567703
it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => {
568704
const date = adapter.format(DateTime.local(2017, JAN, 2), 'DD');
@@ -573,14 +709,14 @@ describe('LuxonDateAdapter with MAT_DATE_LOCALE override', () => {
573709
describe('LuxonDateAdapter with LOCALE_ID override', () => {
574710
let adapter: DateAdapter<DateTime>;
575711

576-
beforeEach(waitForAsync(() => {
712+
beforeEach(() => {
577713
TestBed.configureTestingModule({
578714
imports: [LuxonDateModule],
579715
providers: [{provide: LOCALE_ID, useValue: 'fr-FR'}],
580716
});
581717

582718
adapter = TestBed.inject(DateAdapter);
583-
}));
719+
});
584720

585721
it('should take the default locale id from the LOCALE_ID injection token', () => {
586722
const date = adapter.format(DateTime.local(2017, JAN, 2), 'DD');
@@ -591,7 +727,7 @@ describe('LuxonDateAdapter with LOCALE_ID override', () => {
591727
describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => {
592728
let adapter: DateAdapter<DateTime>;
593729

594-
beforeEach(waitForAsync(() => {
730+
beforeEach(() => {
595731
TestBed.configureTestingModule({
596732
imports: [LuxonDateModule],
597733
providers: [
@@ -603,7 +739,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () =>
603739
});
604740

605741
adapter = TestBed.inject(DateAdapter);
606-
}));
742+
});
607743

608744
describe('use UTC', () => {
609745
it('should create Luxon date in UTC', () => {
@@ -637,7 +773,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override for defa
637773

638774
const calendarExample: CalendarSystem = 'islamic';
639775

640-
beforeEach(waitForAsync(() => {
776+
beforeEach(() => {
641777
TestBed.configureTestingModule({
642778
imports: [LuxonDateModule],
643779
providers: [
@@ -649,7 +785,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override for defa
649785
});
650786

651787
adapter = TestBed.inject(DateAdapter);
652-
}));
788+
});
653789

654790
describe(`use ${calendarExample} calendar`, () => {
655791
it(`should create Luxon date in ${calendarExample} calendar`, () => {

src/material-luxon-adapter/adapter/luxon-date-adapter.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,61 @@ export class LuxonDateAdapter extends DateAdapter<LuxonDateTime> {
272272
return LuxonDateTime.invalid('Invalid Luxon DateTime object.');
273273
}
274274

275+
override setTime(
276+
target: LuxonDateTime,
277+
hours: number,
278+
minutes: number,
279+
seconds: number,
280+
): LuxonDateTime {
281+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
282+
if (hours < 0 || hours > 23) {
283+
throw Error(`Invalid hours "${hours}". Hours value must be between 0 and 23.`);
284+
}
285+
286+
if (minutes < 0 || minutes > 59) {
287+
throw Error(`Invalid minutes "${minutes}". Minutes value must be between 0 and 59.`);
288+
}
289+
290+
if (seconds < 0 || seconds > 59) {
291+
throw Error(`Invalid seconds "${seconds}". Seconds value must be between 0 and 59.`);
292+
}
293+
}
294+
295+
return this.clone(target).set({
296+
hour: hours,
297+
minute: minutes,
298+
second: seconds,
299+
});
300+
}
301+
302+
override getHours(date: LuxonDateTime): number {
303+
return date.hour;
304+
}
305+
306+
override getMinutes(date: LuxonDateTime): number {
307+
return date.minute;
308+
}
309+
310+
override getSeconds(date: LuxonDateTime): number {
311+
return date.second;
312+
}
313+
314+
override parseTime(value: any, parseFormat: string | string[]): LuxonDateTime | null {
315+
const result = this.parse(value, parseFormat);
316+
317+
if ((!result || !this.isValid(result)) && typeof value === 'string') {
318+
// It seems like Luxon doesn't work well cross-browser for strings that have
319+
// additional characters around the time. Try parsing without those characters.
320+
return this.parse(value.replace(/[^0-9:(AM|PM)]/gi, ''), parseFormat) || result;
321+
}
322+
323+
return result;
324+
}
325+
326+
override addMilliseconds(date: LuxonDateTime, amount: number): LuxonDateTime {
327+
return date.reconfigure(this._getOptions()).plus({milliseconds: amount});
328+
}
329+
275330
/** Gets the options that should be used when constructing a new `DateTime` object. */
276331
private _getOptions(): LuxonDateTimeOptions {
277332
return {

src/material-luxon-adapter/adapter/luxon-date-formats.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {MatDateFormats} from '@angular/material/core';
1111
export const MAT_LUXON_DATE_FORMATS: MatDateFormats = {
1212
parse: {
1313
dateInput: 'D',
14+
timeInput: 't',
1415
},
1516
display: {
1617
dateInput: 'D',
18+
timeInput: 't',
1719
monthYearLabel: 'LLL yyyy',
1820
dateA11yLabel: 'DD',
1921
monthYearA11yLabel: 'LLLL yyyy',
22+
timeOptionLabel: 't',
2023
},
2124
};

0 commit comments

Comments
 (0)