diff --git a/projects/natural/src/lib/modules/common/pipes/error-message.pipe.spec.ts b/projects/natural/src/lib/modules/common/pipes/error-message.pipe.spec.ts index 6f373330..25d1636f 100644 --- a/projects/natural/src/lib/modules/common/pipes/error-message.pipe.spec.ts +++ b/projects/natural/src/lib/modules/common/pipes/error-message.pipe.spec.ts @@ -32,6 +32,7 @@ describe('NaturalErrorMessagePipe', () => { [{myValidator: {message: 123}}, ''], [{myValidator: {message: (unit: string) => `my message${unit}`}}, `my message`], [{myValidator: {message: (unit: string) => `my message${unit}`}}, '%', `my message %`], + [{matDatepickerParse: {text: '01.01.'}}, 'Date invalide'], [ { matDatepickerMin: { diff --git a/projects/natural/src/lib/modules/common/pipes/error-message.pipe.ts b/projects/natural/src/lib/modules/common/pipes/error-message.pipe.ts index c9b04f8c..587ea1df 100644 --- a/projects/natural/src/lib/modules/common/pipes/error-message.pipe.ts +++ b/projects/natural/src/lib/modules/common/pipes/error-message.pipe.ts @@ -40,6 +40,7 @@ import {formatIsoDate, formatSwissDate} from '../../../classes/utility'; * - `Validators.required` * - `matDatepickerMin` * - `matDatepickerMax` + * - `matDatepickerParse` * * **Generic**: * @@ -70,6 +71,8 @@ export class NaturalErrorMessagePipe implements PipeTransform { return $localize`Doit être plus grand ou égal à ${errors.min.min}${unit}`; } else if (errors.max) { return $localize`Doit être plus petit ou égal à ${errors.max.max}${unit}`; + } else if (errors.matDatepickerParse) { + return $localize`Date invalide`; } else if (errors.matDatepickerMin) { const min = formatIsoDate(errors.matDatepickerMin.min as Date); const date = new Date(); diff --git a/projects/natural/src/lib/modules/dropdown-components/type-date-range/type-date-range.component.ts b/projects/natural/src/lib/modules/dropdown-components/type-date-range/type-date-range.component.ts index f25ab9e1..5fef2041 100644 --- a/projects/natural/src/lib/modules/dropdown-components/type-date-range/type-date-range.component.ts +++ b/projects/natural/src/lib/modules/dropdown-components/type-date-range/type-date-range.component.ts @@ -139,8 +139,9 @@ export class TypeDateRangeComponent implements DropdownComponent { }; if (condition.between) { - value.from = this.dateAdapter.parse(condition.between.from, null); - value.to = this.dateAdapter.parse(condition.between.to, null); + // A condition that cannot be read parses into an invalid date, which must not reach the form + value.from = this.dateAdapter.getValidDateOrNull(this.dateAdapter.parse(condition.between.from, null)); + value.to = this.dateAdapter.getValidDateOrNull(this.dateAdapter.parse(condition.between.to, null)); } this.form.setValue(value); diff --git a/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.spec.ts b/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.spec.ts index fcd5c502..3fd36378 100644 --- a/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.spec.ts +++ b/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.spec.ts @@ -49,27 +49,48 @@ describe('NaturalSwissParsingDateAdapter', () => { expect(formatIsoDate(adapter.parse(' 2018-01-02 '))).toBe('2018-01-02'); }); + it('should return nothing at all for an empty field', () => { + expect(adapter.parse('')).toBeNull(); + expect(adapter.parse(' ')).toBeNull(); + expect(adapter.parse(null)).toBeNull(); + expect(adapter.parse(undefined)).toBeNull(); + }); + + it('should tell an empty field apart from a date still being typed', () => { + expect(adapter.parse('')).toBeNull(); + + const stillBeingTyped = adapter.parse('01.01.'); + expect(stillBeingTyped).not.toBeNull(); + expect(adapter.isValid(stillBeingTyped!)).toBeFalse(); + }); + it('should reject too much partial Swiss format', () => { - expect(formatIsoDate(adapter.parse('2.1.1'))).toBeNull(); + expect(adapter.isValid(adapter.parse('2.1.1')!)).toBeFalse(); }); it('should reject mixed separators', () => { - expect(adapter.parse('22.11/2018')).toBeNull(); + expect(adapter.isValid(adapter.parse('22.11/2018')!)).toBeFalse(); }); it('should reject no separator at all', () => { - expect(adapter.parse('220905')).toBeNull(); + expect(adapter.isValid(adapter.parse('220905')!)).toBeFalse(); }); - it('should reject invalid date', () => { - expect(adapter.parse('00.01.2000')).toBeNull(); - expect(adapter.parse('01.00.2000')).toBeNull(); - expect(adapter.parse('01.31.2000')).toBeNull(); - expect(adapter.parse('50.01.2000')).toBeNull(); + it('should parse the 29th of February of a leap year', () => { + expect(formatIsoDate(adapter.parse('29.02.2024'))).toBe('2024-02-29'); }); - it('should not parse invalid format', () => { - expect(adapter.parse('')).toBeNull(); - expect(adapter.parse(null)).toBeNull(); + it('should reject a day that does not exist in that month', () => { + expect(adapter.isValid(adapter.parse('29.02.2026')!)).toBeFalse(); + expect(adapter.isValid(adapter.parse('31.02.2026')!)).toBeFalse(); + expect(adapter.isValid(adapter.parse('31.04.2026')!)).toBeFalse(); + expect(formatIsoDate(adapter.parse('30.04.2026'))).toBe('2026-04-30'); + }); + + it('should reject invalid date', () => { + expect(adapter.isValid(adapter.parse('00.01.2000')!)).toBeFalse(); + expect(adapter.isValid(adapter.parse('01.00.2000')!)).toBeFalse(); + expect(adapter.isValid(adapter.parse('01.31.2000')!)).toBeFalse(); + expect(adapter.isValid(adapter.parse('50.01.2000')!)).toBeFalse(); }); }); diff --git a/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.ts b/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.ts index 2910e15e..c8114292 100644 --- a/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.ts +++ b/projects/natural/src/lib/services/swiss-parsing-date-adapter.service.ts @@ -18,42 +18,54 @@ export class NaturalSwissParsingDateAdapter extends NativeDateAdapter { * - 24.12.2018 * - 1.4.18 * - 2018-12-24 + * + * An empty field is `null`, and text that cannot be read is an invalid date. */ public override parse(value: unknown): Date | null { if (typeof value === 'number') { return new Date(value); } - if (typeof value === 'string') { - const trimmed = value.trim(); + if (typeof value !== 'string') { + return null; + } + + const trimmed = value.trim(); + if (!trimmed) { + return null; + } - for (const pattern of patterns) { - const m = trimmed.match(pattern); - if (m?.groups) { - const year = +m.groups.year; - const month = +m.groups.month; - const day = +m.groups.day; + for (const pattern of patterns) { + const m = trimmed.match(pattern); + if (m?.groups) { + const year = +m.groups.year; + const month = +m.groups.month; + const day = +m.groups.day; - return this.createDateIfValid(year, month, day); - } + return this.createDateOrInvalid(year, month, day); } } - return null; + return this.invalid(); } - private createDateIfValid(year: number, month: number, date: number): Date | null { + private createDateOrInvalid(year: number, month: number, date: number): Date { // Assume year 2000 if only two digits if (year < 100) { year += 2000; } month = month - 1; - if (month >= 0 && month <= 11 && date >= 1 && date <= 31) { - return this.createDate(year, month, date); + if (month < 0 || month > 11 || date < 1 || date > 31) { + return this.invalid(); + } + + // A day that does not exist overflows into the next month + if (new Date(year, month, date).getMonth() !== month) { + return this.invalid(); } - return null; + return this.createDate(year, month, date); } public override getFirstDayOfWeek(): number {