diff --git a/app/components/malio/date/composables/dateRange.test.ts b/app/components/malio/date/composables/dateRange.test.ts new file mode 100644 index 0000000..d6e5ab7 --- /dev/null +++ b/app/components/malio/date/composables/dateRange.test.ts @@ -0,0 +1,57 @@ +import {describe, expect, it} from 'vitest' +import {dayRangeRole, normalizeRange, resolveRangeBounds} from './dateRange' + +describe('dateRange', () => { + describe('normalizeRange', () => { + it('keeps an already ordered pair', () => { + expect(normalizeRange('2026-05-19', '2026-05-25')).toEqual({start: '2026-05-19', end: '2026-05-25'}) + }) + it('swaps a reversed pair', () => { + expect(normalizeRange('2026-05-25', '2026-05-19')).toEqual({start: '2026-05-19', end: '2026-05-25'}) + }) + it('handles an equal pair', () => { + expect(normalizeRange('2026-05-19', '2026-05-19')).toEqual({start: '2026-05-19', end: '2026-05-19'}) + }) + }) + + describe('resolveRangeBounds', () => { + it('returns null without a start', () => { + expect(resolveRangeBounds(null, null, null)).toBeNull() + }) + it('returns a single-point range when only start is set', () => { + expect(resolveRangeBounds('2026-05-19', null, null)).toEqual({lo: '2026-05-19', hi: '2026-05-19'}) + }) + it('orders start and committed end', () => { + expect(resolveRangeBounds('2026-05-19', '2026-05-25', null)).toEqual({lo: '2026-05-19', hi: '2026-05-25'}) + }) + it('uses preview when end is not set', () => { + expect(resolveRangeBounds('2026-05-19', null, '2026-05-22')).toEqual({lo: '2026-05-19', hi: '2026-05-22'}) + }) + it('inverts when preview is before start', () => { + expect(resolveRangeBounds('2026-05-19', null, '2026-05-10')).toEqual({lo: '2026-05-10', hi: '2026-05-19'}) + }) + it('prioritises committed end over preview', () => { + expect(resolveRangeBounds('2026-05-19', '2026-05-25', '2026-05-30')).toEqual({lo: '2026-05-19', hi: '2026-05-25'}) + }) + }) + + describe('dayRangeRole', () => { + const bounds = {lo: '2026-05-19', hi: '2026-05-25'} + it('returns none without bounds', () => { + expect(dayRangeRole('2026-05-20', null)).toBe('none') + }) + it('returns single when lo === hi and matches', () => { + expect(dayRangeRole('2026-05-19', {lo: '2026-05-19', hi: '2026-05-19'})).toBe('single') + expect(dayRangeRole('2026-05-20', {lo: '2026-05-19', hi: '2026-05-19'})).toBe('none') + }) + it('returns start, end and in-range', () => { + expect(dayRangeRole('2026-05-19', bounds)).toBe('start') + expect(dayRangeRole('2026-05-25', bounds)).toBe('end') + expect(dayRangeRole('2026-05-22', bounds)).toBe('in-range') + }) + it('returns none outside the bounds', () => { + expect(dayRangeRole('2026-05-10', bounds)).toBe('none') + expect(dayRangeRole('2026-05-30', bounds)).toBe('none') + }) + }) +}) diff --git a/app/components/malio/date/composables/dateRange.ts b/app/components/malio/date/composables/dateRange.ts new file mode 100644 index 0000000..5dd62b5 --- /dev/null +++ b/app/components/malio/date/composables/dateRange.ts @@ -0,0 +1,31 @@ +export type DateRangeValue = {start: string; end: string} + +export function normalizeRange(a: string, b: string): DateRangeValue { + return a <= b ? {start: a, end: b} : {start: b, end: a} +} + +export function resolveRangeBounds( + start: string | null, + end: string | null, + preview: string | null, +): {lo: string; hi: string} | null { + if (!start) return null + const other = end ?? preview + if (!other) return {lo: start, hi: start} + return start <= other ? {lo: start, hi: other} : {lo: other, hi: start} +} + +export type DayRangeRole = 'none' | 'single' | 'start' | 'end' | 'in-range' + +export function dayRangeRole( + iso: string, + bounds: {lo: string; hi: string} | null, +): DayRangeRole { + if (!bounds) return 'none' + const {lo, hi} = bounds + if (lo === hi) return iso === lo ? 'single' : 'none' + if (iso === lo) return 'start' + if (iso === hi) return 'end' + if (iso > lo && iso < hi) return 'in-range' + return 'none' +}