From c208551a44ddd3a3be204b6cb7afe90631a55d8e Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 08:17:26 +0200 Subject: [PATCH] feat : utilitaires de format et validation de date (#MUI-33) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../malio/date/composables/dateFormat.test.ts | 62 +++++++++++++++++++ .../malio/date/composables/dateFormat.ts | 26 ++++++++ 2 files changed, 88 insertions(+) create mode 100644 app/components/malio/date/composables/dateFormat.test.ts create mode 100644 app/components/malio/date/composables/dateFormat.ts diff --git a/app/components/malio/date/composables/dateFormat.test.ts b/app/components/malio/date/composables/dateFormat.test.ts new file mode 100644 index 0000000..744c0d3 --- /dev/null +++ b/app/components/malio/date/composables/dateFormat.test.ts @@ -0,0 +1,62 @@ +import {describe, expect, it} from 'vitest' +import {formatIsoToDisplay, isDateInRange, isValidIso, parseDisplayToIso} from './dateFormat' + +describe('dateFormat', () => { + describe('isValidIso', () => { + it('accepts a real ISO date', () => { + expect(isValidIso('2026-05-19')).toBe(true) + }) + it('rejects a malformed string', () => { + expect(isValidIso('19/05/2026')).toBe(false) + expect(isValidIso('2026-5-9')).toBe(false) + expect(isValidIso('')).toBe(false) + }) + it('rejects an impossible date', () => { + expect(isValidIso('2026-02-30')).toBe(false) + expect(isValidIso('2026-13-01')).toBe(false) + }) + it('accepts Feb 29 on a leap year and rejects it otherwise', () => { + expect(isValidIso('2024-02-29')).toBe(true) + expect(isValidIso('2026-02-29')).toBe(false) + }) + }) + + describe('formatIsoToDisplay', () => { + it('formats ISO to DD/MM/YYYY', () => { + expect(formatIsoToDisplay('2026-05-19')).toBe('19/05/2026') + }) + it('returns empty string for null or invalid input', () => { + expect(formatIsoToDisplay(null)).toBe('') + expect(formatIsoToDisplay('nope')).toBe('') + }) + }) + + describe('parseDisplayToIso', () => { + it('parses DD/MM/YYYY to ISO', () => { + expect(parseDisplayToIso('19/05/2026')).toBe('2026-05-19') + }) + it('returns null for malformed or impossible input', () => { + expect(parseDisplayToIso('2026-05-19')).toBeNull() + expect(parseDisplayToIso('31/02/2026')).toBeNull() + expect(parseDisplayToIso('')).toBeNull() + }) + }) + + describe('isDateInRange', () => { + it('returns true when no bounds are given', () => { + expect(isDateInRange('2026-05-19')).toBe(true) + }) + it('respects the min bound (inclusive)', () => { + expect(isDateInRange('2026-05-19', '2026-05-19')).toBe(true) + expect(isDateInRange('2026-05-18', '2026-05-19')).toBe(false) + }) + it('respects the max bound (inclusive)', () => { + expect(isDateInRange('2026-05-19', undefined, '2026-05-19')).toBe(true) + expect(isDateInRange('2026-05-20', undefined, '2026-05-19')).toBe(false) + }) + it('respects both bounds', () => { + expect(isDateInRange('2026-05-15', '2026-05-10', '2026-05-20')).toBe(true) + expect(isDateInRange('2026-05-25', '2026-05-10', '2026-05-20')).toBe(false) + }) + }) +}) diff --git a/app/components/malio/date/composables/dateFormat.ts b/app/components/malio/date/composables/dateFormat.ts new file mode 100644 index 0000000..fc0bf1d --- /dev/null +++ b/app/components/malio/date/composables/dateFormat.ts @@ -0,0 +1,26 @@ +export function isValidIso(iso: string): boolean { + if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false + const [y, m, d] = iso.split('-').map(Number) + const date = new Date(y, m - 1, d) + return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d +} + +export function formatIsoToDisplay(iso: string | null): string { + if (!iso || !isValidIso(iso)) return '' + const [y, m, d] = iso.split('-') + return `${d}/${m}/${y}` +} + +export function parseDisplayToIso(display: string): string | null { + const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(display.trim()) + if (!match) return null + const [, dd, mm, yyyy] = match + const iso = `${yyyy}-${mm}-${dd}` + return isValidIso(iso) ? iso : null +} + +export function isDateInRange(iso: string, min?: string, max?: string): boolean { + if (min && iso < min) return false + if (max && iso > max) return false + return true +}