From 859103bbe2daebc7fef17c0b29cad319fb79b8e5 Mon Sep 17 00:00:00 2001 From: ghzhost Date: Wed, 2 Sep 2026 15:30:16 +0000 Subject: [PATCH] fix(utils): consolidate date and datetime formatting helpers (#28) - Update lib/utils.ts date helpers to validate parsed dates against isNaN - Make utils/format.ts delegate formatDate and formatDateTime to lib/utils.ts - Support Date instances and string inputs consistently with graceful unparseable fallbacks - Add test coverage for formatDate and formatDateTime across strings and Date objects --- src/lib/utils.ts | 32 +++++++++++++++++++++----------- src/utils/format.test.ts | 31 ++++++++++++++++++++++++++++++- src/utils/format.ts | 22 +++++++++++++++++++--- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a6abda0..3164528 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -30,7 +30,9 @@ function toClassArray(val: ClassValue): string[] { export function formatDate(iso: string, opts?: Intl.DateTimeFormatOptions): string { try { - return new Date(iso).toLocaleDateString('en-US', { + const d = new Date(iso) + if (isNaN(d.getTime())) return iso + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', @@ -43,7 +45,9 @@ export function formatDate(iso: string, opts?: Intl.DateTimeFormatOptions): stri export function formatDateTime(iso: string): string { try { - return new Date(iso).toLocaleString('en-US', { + const d = new Date(iso) + if (isNaN(d.getTime())) return iso + return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', @@ -56,15 +60,21 @@ export function formatDateTime(iso: string): string { } export function timeAgo(iso: string): string { - const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000) - if (seconds < 60) return 'just now' - const minutes = Math.floor(seconds / 60) - if (minutes < 60) return `${minutes}m ago` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h ago` - const days = Math.floor(hours / 24) - if (days < 30) return `${days}d ago` - return formatDate(iso) + try { + const d = new Date(iso) + if (isNaN(d.getTime())) return iso + const seconds = Math.floor((Date.now() - d.getTime()) / 1000) + if (seconds < 60) return 'just now' + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 30) return `${days}d ago` + return formatDate(iso) + } catch { + return iso + } } // ─── Clipboard ──────────────────────────────────────────────────────────────── diff --git a/src/utils/format.test.ts b/src/utils/format.test.ts index 1c8d40f..44e0d09 100644 --- a/src/utils/format.test.ts +++ b/src/utils/format.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { formatAmount, formatCurrency } from './format' +import { formatAmount, formatCurrency, formatDate, formatDateTime } from './format' describe('formatAmount', () => { it('formats with 2 decimal places', () => expect(formatAmount(1234.5)).toBe('1,234.50')) @@ -9,3 +9,32 @@ describe('formatAmount', () => { describe('formatCurrency', () => { it('formats USD', () => expect(formatCurrency(10)).toContain('10.00')) }) + +describe('formatDate and formatDateTime consolidation (#28)', () => { + it('formats valid ISO date string without time', () => { + const res = formatDate('2026-07-15T12:00:00Z') + expect(res).toContain('Jul') + expect(res).toContain('15') + expect(res).toContain('2026') + }) + + it('formats Date instance', () => { + const d = new Date('2026-07-15T12:00:00Z') + const res = formatDate(d) + expect(res).toContain('Jul') + expect(res).toContain('15') + expect(res).toContain('2026') + }) + + it('formats valid ISO date string with time using formatDateTime', () => { + const res = formatDateTime('2026-07-15T12:00:00Z') + expect(res).toContain('Jul') + expect(res).toContain('15') + expect(res).toContain('2026') + }) + + it('gracefully returns unparseable string on invalid date input', () => { + expect(formatDate('not-a-date')).toBe('not-a-date') + expect(formatDateTime('invalid-date')).toBe('invalid-date') + }) +}) diff --git a/src/utils/format.ts b/src/utils/format.ts index 5e98eca..29e7d0b 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,3 +1,19 @@ -export const formatAmount = (n: number, d = 2) => new Intl.NumberFormat('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n) -export const formatDate = (d: Date | string) => new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(d)) -export const formatCurrency = (n: number, c = 'USD') => new Intl.NumberFormat('en-US', { style: 'currency', currency: c }).format(n) +import { + formatDate as libFormatDate, + formatDateTime as libFormatDateTime, +} from '@/lib/utils' + +export const formatAmount = (n: number, d = 2) => + new Intl.NumberFormat('en-US', { + minimumFractionDigits: d, + maximumFractionDigits: d, + }).format(n) + +export const formatDate = (d: Date | string, opts?: Intl.DateTimeFormatOptions) => + libFormatDate(typeof d === 'string' ? d : d.toISOString(), opts) + +export const formatDateTime = (d: Date | string) => + libFormatDateTime(typeof d === 'string' ? d : d.toISOString()) + +export const formatCurrency = (n: number, c = 'USD') => + new Intl.NumberFormat('en-US', { style: 'currency', currency: c }).format(n)