Skip to content

Commit

Permalink
refactor(material-luxon-adapter): implement new methods
Browse files Browse the repository at this point in the history
Implements the new methods in the Luxon adapter.
  • Loading branch information
crisbeto committed Sep 30, 2024
1 parent 0e812f2 commit 70cc558
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 15 deletions.
166 changes: 151 additions & 15 deletions src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [LuxonDateModule],
});

beforeEach(() => {
TestBed.configureTestingModule({imports: [LuxonDateModule]});
adapter = TestBed.inject(DateAdapter);
adapter.setLocale('en-US');
}));
});

it('should get year', () => {
expect(adapter.getYear(DateTime.local(2017, JAN, 1))).toBe(2017);
Expand Down Expand Up @@ -550,19 +547,158 @@ describe('LuxonDateAdapter', () => {
it('should create invalid date', () => {
assertValidDate(adapter, adapter.invalid(), false);
});

it('should get hours', () => {
expect(adapter.getHours(DateTime.local(2024, JAN, 1, 14))).toBe(14);
});

it('should get minutes', () => {
expect(adapter.getMinutes(DateTime.local(2024, JAN, 1, 14, 53))).toBe(53);
});

it('should get seconds', () => {
expect(adapter.getSeconds(DateTime.local(2024, JAN, 1, 14, 53, 42))).toBe(42);
});

it('should set the time of a date', () => {
const target = DateTime.local(2024, JAN, 1, 0, 0, 0);
const result = adapter.setTime(target, 14, 53, 42);
expect(adapter.getHours(result)).toBe(14);
expect(adapter.getMinutes(result)).toBe(53);
expect(adapter.getSeconds(result)).toBe(42);
});

it('should throw when passing in invalid hours to setTime', () => {
expect(() => adapter.setTime(adapter.today(), -1, 0, 0)).toThrowError(
'Invalid hours "-1". Hours value must be between 0 and 23.',
);
expect(() => adapter.setTime(adapter.today(), 51, 0, 0)).toThrowError(
'Invalid hours "51". Hours value must be between 0 and 23.',
);
});

it('should throw when passing in invalid minutes to setTime', () => {
expect(() => adapter.setTime(adapter.today(), 0, -1, 0)).toThrowError(
'Invalid minutes "-1". Minutes value must be between 0 and 59.',
);
expect(() => adapter.setTime(adapter.today(), 0, 65, 0)).toThrowError(
'Invalid minutes "65". Minutes value must be between 0 and 59.',
);
});

it('should throw when passing in invalid seconds to setTime', () => {
expect(() => adapter.setTime(adapter.today(), 0, 0, -1)).toThrowError(
'Invalid seconds "-1". Seconds value must be between 0 and 59.',
);
expect(() => adapter.setTime(adapter.today(), 0, 0, 65)).toThrowError(
'Invalid seconds "65". Seconds value must be between 0 and 59.',
);
});

it('should parse a 24-hour time string', () => {
const result = adapter.parseTime('14:52', 't')!;
expect(result).toBeTruthy();
expect(adapter.isValid(result)).toBe(true);
expect(adapter.getHours(result)).toBe(14);
expect(adapter.getMinutes(result)).toBe(52);
expect(adapter.getSeconds(result)).toBe(0);
});

it('should parse a 12-hour time string', () => {
const result = adapter.parseTime('2:52 PM', 't')!;
expect(result).toBeTruthy();
expect(adapter.isValid(result)).toBe(true);
expect(adapter.getHours(result)).toBe(14);
expect(adapter.getMinutes(result)).toBe(52);
expect(adapter.getSeconds(result)).toBe(0);
});

it('should parse a padded time string', () => {
const result = adapter.parseTime('03:04:05', 'tt')!;
expect(result).toBeTruthy();
expect(adapter.isValid(result)).toBe(true);
expect(adapter.getHours(result)).toBe(3);
expect(adapter.getMinutes(result)).toBe(4);
expect(adapter.getSeconds(result)).toBe(5);
});

it('should parse a time string that uses dot as a separator', () => {
adapter.setLocale('fi-FI');
const result = adapter.parseTime('14.52', 't')!;
expect(result).toBeTruthy();
expect(adapter.isValid(result)).toBe(true);
expect(adapter.getHours(result)).toBe(14);
expect(adapter.getMinutes(result)).toBe(52);
expect(adapter.getSeconds(result)).toBe(0);
});

it('should parse a time string with characters around the time', () => {
adapter.setLocale('bg-BG');
const result = adapter.parseTime('14:52 ч.', 't')!;
expect(result).toBeTruthy();
expect(adapter.isValid(result)).toBe(true);
expect(adapter.getHours(result)).toBe(14);
expect(adapter.getMinutes(result)).toBe(52);
expect(adapter.getSeconds(result)).toBe(0);
});

it('should return an invalid date when parsing invalid time string', () => {
expect(adapter.isValid(adapter.parseTime('abc', 't')!)).toBeFalse();
expect(adapter.isValid(adapter.parseTime(' ', 't')!)).toBeFalse();
expect(adapter.isValid(adapter.parseTime('24:05', 't')!)).toBeFalse();
expect(adapter.isValid(adapter.parseTime('00:61:05', 'tt')!)).toBeFalse();
expect(adapter.isValid(adapter.parseTime('14:52:78', 'tt')!)).toBeFalse();
});

it('should return null when parsing unsupported time values', () => {
expect(adapter.parseTime(true, 't')).toBeNull();
expect(adapter.parseTime(undefined, 't')).toBeNull();
expect(adapter.parseTime('', 't')).toBeNull();
});

it('should compare times', () => {
const base = [2024, JAN, 1] as const;

expect(
adapter.compareTime(DateTime.local(...base, 12, 0, 0), DateTime.local(...base, 13, 0, 0)),
).toBeLessThan(0);
expect(
adapter.compareTime(DateTime.local(...base, 12, 50, 0), DateTime.local(...base, 12, 51, 0)),
).toBeLessThan(0);
expect(
adapter.compareTime(DateTime.local(...base, 1, 2, 3), DateTime.local(...base, 1, 2, 3)),
).toBe(0);
expect(
adapter.compareTime(DateTime.local(...base, 13, 0, 0), DateTime.local(...base, 12, 0, 0)),
).toBeGreaterThan(0);
expect(
adapter.compareTime(DateTime.local(...base, 12, 50, 11), DateTime.local(...base, 12, 50, 10)),
).toBeGreaterThan(0);
expect(
adapter.compareTime(DateTime.local(...base, 13, 0, 0), DateTime.local(...base, 10, 59, 59)),
).toBeGreaterThan(0);
});

it('should add milliseconds to a date', () => {
const amount = 1234567;
const initial = DateTime.local(2024, JAN, 1, 12, 34, 56);
const result = adapter.addMilliseconds(initial, amount);
expect(result).not.toBe(initial);
expect(result.toMillis() - initial.toMillis()).toBe(amount);
});
});

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

beforeEach(waitForAsync(() => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LuxonDateModule],
providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}],
});

adapter = TestBed.inject(DateAdapter);
}));
});

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

beforeEach(waitForAsync(() => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LuxonDateModule],
providers: [{provide: LOCALE_ID, useValue: 'fr-FR'}],
});

adapter = TestBed.inject(DateAdapter);
}));
});

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

beforeEach(waitForAsync(() => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LuxonDateModule],
providers: [
Expand All @@ -603,7 +739,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () =>
});

adapter = TestBed.inject(DateAdapter);
}));
});

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

const calendarExample: CalendarSystem = 'islamic';

beforeEach(waitForAsync(() => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LuxonDateModule],
providers: [
Expand All @@ -649,7 +785,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override for defa
});

adapter = TestBed.inject(DateAdapter);
}));
});

describe(`use ${calendarExample} calendar`, () => {
it(`should create Luxon date in ${calendarExample} calendar`, () => {
Expand Down
55 changes: 55 additions & 0 deletions src/material-luxon-adapter/adapter/luxon-date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,61 @@ export class LuxonDateAdapter extends DateAdapter<LuxonDateTime> {
return LuxonDateTime.invalid('Invalid Luxon DateTime object.');
}

override setTime(
target: LuxonDateTime,
hours: number,
minutes: number,
seconds: number,
): LuxonDateTime {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (hours < 0 || hours > 23) {
throw Error(`Invalid hours "${hours}". Hours value must be between 0 and 23.`);
}

if (minutes < 0 || minutes > 59) {
throw Error(`Invalid minutes "${minutes}". Minutes value must be between 0 and 59.`);
}

if (seconds < 0 || seconds > 59) {
throw Error(`Invalid seconds "${seconds}". Seconds value must be between 0 and 59.`);
}
}

return this.clone(target).set({
hour: hours,
minute: minutes,
second: seconds,
});
}

override getHours(date: LuxonDateTime): number {
return date.hour;
}

override getMinutes(date: LuxonDateTime): number {
return date.minute;
}

override getSeconds(date: LuxonDateTime): number {
return date.second;
}

override parseTime(value: any, parseFormat: string | string[]): LuxonDateTime | null {
const result = this.parse(value, parseFormat);

if ((!result || !this.isValid(result)) && typeof value === 'string') {
// It seems like Luxon doesn't work well cross-browser for strings that have
// additional characters around the time. Try parsing without those characters.
return this.parse(value.replace(/[^0-9:(AM|PM)]/gi, ''), parseFormat) || result;
}

return result;
}

override addMilliseconds(date: LuxonDateTime, amount: number): LuxonDateTime {
return date.reconfigure(this._getOptions()).plus({milliseconds: amount});
}

/** Gets the options that should be used when constructing a new `DateTime` object. */
private _getOptions(): LuxonDateTimeOptions {
return {
Expand Down
3 changes: 3 additions & 0 deletions src/material-luxon-adapter/adapter/luxon-date-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import {MatDateFormats} from '@angular/material/core';
export const MAT_LUXON_DATE_FORMATS: MatDateFormats = {
parse: {
dateInput: 'D',
timeInput: 't',
},
display: {
dateInput: 'D',
timeInput: 't',
monthYearLabel: 'LLL yyyy',
dateA11yLabel: 'DD',
monthYearA11yLabel: 'LLLL yyyy',
timeOptionLabel: 't',
},
};

0 comments on commit 70cc558

Please sign in to comment.