From 03067eda846be60c1c491c1e29012243e453b6ed Mon Sep 17 00:00:00 2001 From: joshua Date: Mon, 26 May 2025 08:49:43 +0100 Subject: [PATCH 1/4] feat: format time util --- src/format-time.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/format-time.ts diff --git a/src/format-time.ts b/src/format-time.ts new file mode 100644 index 0000000..79244b2 --- /dev/null +++ b/src/format-time.ts @@ -0,0 +1,80 @@ +/** + * Valid date input types for the formatTime function + */ +type DateInput = Date | string | number; + +/** + * Formats a timestamp into a human-readable relative time string + * @param date - The date to format. Can be a Date object, ISO string, or timestamp + * @param now - The reference date to compare against (defaults to current time) + * @returns Formatted time string (e.g., "2m ago", "1h ago", "04-14", "04-14-2023") + * + * @example + * ```typescript + * // Basic usage + * formatTime(new Date(Date.now() - 5 * 60 * 1000)); // "5m ago" + * formatTime(new Date(Date.now() - 2 * 60 * 60 * 1000)); // "2h ago" + * + * // With custom reference date + * const someDate = new Date('2023-04-14'); + * const referenceDate = new Date('2023-04-15'); + * formatTime(someDate, referenceDate); // "1d ago" + * + * // Date formats for older dates + * formatTime(new Date('2024-04-14')); // "04-14" (if current year is 2024) + * formatTime(new Date('2023-04-14')); // "04-14-2023" (if current year is 2024) + * ``` + */ +function formatTime(date: DateInput, now: DateInput = new Date()): string { + // Convert inputs to Date objects + const targetDate = new Date(date); + const currentDate = new Date(now); + + // Validate dates + if (isNaN(targetDate.getTime()) || isNaN(currentDate.getTime())) { + throw new Error('Invalid date provided'); + } + + const diffMs = currentDate.getTime() - targetDate.getTime(); + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + + // Less than 1 minute ago + if (diffSeconds < 60) { + return diffSeconds <= 1 ? '1s ago' : `${diffSeconds}s ago`; + } + + // Less than 1 hour ago + if (diffMinutes < 60) { + return `${diffMinutes}m ago`; + } + + // Less than 1 day ago + if (diffHours < 24) { + return `${diffHours}h ago`; + } + + // Less than 7 days ago + if (diffDays < 7) { + return `${diffDays}d ago`; + } + + // 7 days or more - use date format + const month = String(targetDate.getMonth() + 1).padStart(2, '0'); + const day = String(targetDate.getDate()).padStart(2, '0'); + const year = targetDate.getFullYear(); + const currentYear = currentDate.getFullYear(); + + // Same year - just month-day + if (year === currentYear) { + return `${month}-${day}`; + } + + // Different year - include year + return `${month}-${day}-${year}`; +} + +export default formatTime; +export { formatTime, type DateInput }; From f9ffc2afc787e653376790b45427122bf2f9e9ea Mon Sep 17 00:00:00 2001 From: joshua Date: Mon, 26 May 2025 08:54:30 +0100 Subject: [PATCH 2/4] test: is-odd --- test/is-odd.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/is-odd.test.ts diff --git a/test/is-odd.test.ts b/test/is-odd.test.ts new file mode 100644 index 0000000..7754017 --- /dev/null +++ b/test/is-odd.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { isOdd } from '../src'; + +describe('isOdd', () => { + it('returns true for odd numbers', () => { + expect(isOdd(1)).toBe(true); + expect(isOdd(-3)).toBe(true); + }); + + it('returns false for even numbers', () => { + expect(isOdd(2)).toBe(false); + }); + + it('throws error for non-integers', () => { + expect(() => isOdd(1.5)).toThrow(); + }); +}); From 48916123aa7ca1a8911317ce6b1a8b573b51ecb6 Mon Sep 17 00:00:00 2001 From: joshua Date: Mon, 26 May 2025 08:54:57 +0100 Subject: [PATCH 3/4] test: format-time --- test/format-time.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/format-time.test.ts diff --git a/test/format-time.test.ts b/test/format-time.test.ts new file mode 100644 index 0000000..e69de29 From 67c2b01b404a9246ca1182b640cdee2004657958 Mon Sep 17 00:00:00 2001 From: joshua Date: Mon, 26 May 2025 09:04:34 +0100 Subject: [PATCH 4/4] feat: add time format util and test --- package-lock.json | 4 +- src/index.ts | 15 +-- test/format-time.test.ts | 202 +++++++++++++++++++++++++++++++++++++++ test/index.test.ts | 18 +--- 4 files changed, 213 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcacff1..0bd7d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@mindiv/is-odd", + "name": "@mindiv/utils", "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@mindiv/is-odd", + "name": "@mindiv/utils", "version": "1.0.1", "license": "ISC", "bin": { diff --git a/src/index.ts b/src/index.ts index a4121a8..63790cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -export * from "./is-odd"; -export * from "./is-even"; -export * from "./is-prime"; -export * from "./is-multiple-of"; -export * from "./is-divisible-by"; -export * from "./mod"; -export * from "./clamp"; +export * from './is-odd'; +export * from './is-even'; +export * from './is-prime'; +export * from './is-multiple-of'; +export * from './is-divisible-by'; +export * from './mod'; +export * from './clamp'; +export * from './format-time'; diff --git a/test/format-time.test.ts b/test/format-time.test.ts index e69de29..95c839a 100644 --- a/test/format-time.test.ts +++ b/test/format-time.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { formatTime } from '../src'; + +describe('formatTime', () => { + const mockNow = new Date('2024-05-26T12:00:00Z'); + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(mockNow); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('seconds ago', () => { + it('should return "1s ago" for 1 second ago', () => { + const date = new Date(mockNow.getTime() - 1000); + expect(formatTime(date)).toBe('1s ago'); + }); + + it('should return "30s ago" for 30 seconds ago', () => { + const date = new Date(mockNow.getTime() - 30 * 1000); + expect(formatTime(date)).toBe('30s ago'); + }); + + it('should return "59s ago" for 59 seconds ago', () => { + const date = new Date(mockNow.getTime() - 59 * 1000); + expect(formatTime(date)).toBe('59s ago'); + }); + }); + + describe('minutes ago', () => { + it('should return "1m ago" for 1 minute ago', () => { + const date = new Date(mockNow.getTime() - 60 * 1000); + expect(formatTime(date)).toBe('1m ago'); + }); + + it('should return "30m ago" for 30 minutes ago', () => { + const date = new Date(mockNow.getTime() - 30 * 60 * 1000); + expect(formatTime(date)).toBe('30m ago'); + }); + + it('should return "59m ago" for 59 minutes ago', () => { + const date = new Date(mockNow.getTime() - 59 * 60 * 1000); + expect(formatTime(date)).toBe('59m ago'); + }); + }); + + describe('hours ago', () => { + it('should return "1h ago" for 1 hour ago', () => { + const date = new Date(mockNow.getTime() - 60 * 60 * 1000); + expect(formatTime(date)).toBe('1h ago'); + }); + + it('should return "12h ago" for 12 hours ago', () => { + const date = new Date(mockNow.getTime() - 12 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('12h ago'); + }); + + it('should return "23h ago" for 23 hours ago', () => { + const date = new Date(mockNow.getTime() - 23 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('23h ago'); + }); + }); + + describe('days ago', () => { + it('should return "1d ago" for 1 day ago', () => { + const date = new Date(mockNow.getTime() - 24 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('1d ago'); + }); + + it('should return "3d ago" for 3 days ago', () => { + const date = new Date(mockNow.getTime() - 3 * 24 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('3d ago'); + }); + + it('should return "6d ago" for 6 days ago', () => { + const date = new Date(mockNow.getTime() - 6 * 24 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('6d ago'); + }); + }); + + describe('date formats (7+ days ago)', () => { + it('should return "05-19" for 1 week ago (same year)', () => { + const date = new Date('2024-05-19T12:00:00Z'); + expect(formatTime(date)).toBe('05-19'); + }); + + it('should return "04-26" for 1 month ago (same year)', () => { + const date = new Date('2024-04-26T12:00:00Z'); + expect(formatTime(date)).toBe('04-26'); + }); + + it('should return "01-01" for January 1st (same year)', () => { + const date = new Date('2024-01-01T12:00:00Z'); + expect(formatTime(date)).toBe('01-01'); + }); + + it('should return "05-26-2023" for 1 year ago (different year)', () => { + const date = new Date('2023-05-26T12:00:00Z'); + expect(formatTime(date)).toBe('05-26-2023'); + }); + + it('should return "12-25-2022" for December 2022 (different year)', () => { + const date = new Date('2022-12-25T12:00:00Z'); + expect(formatTime(date)).toBe('12-25-2022'); + }); + }); + + describe('input types', () => { + it('should handle Date objects', () => { + const date = new Date(mockNow.getTime() - 5 * 60 * 1000); + expect(formatTime(date)).toBe('5m ago'); + }); + + it('should handle ISO date strings', () => { + const date = '2024-05-26T11:55:00Z'; + expect(formatTime(date)).toBe('5m ago'); + }); + + it('should handle timestamps', () => { + const date = mockNow.getTime() - 5 * 60 * 1000; + expect(formatTime(date)).toBe('5m ago'); + }); + }); + + describe('custom reference date', () => { + it('should use custom reference date when provided', () => { + const targetDate = new Date('2024-05-26T11:55:00Z'); + const referenceDate = new Date('2024-05-26T12:00:00Z'); + expect(formatTime(targetDate, referenceDate)).toBe('5m ago'); + }); + + it('should handle different reference date for date formatting', () => { + const targetDate = new Date('2024-04-01T12:00:00Z'); + const referenceDate = new Date('2024-05-26T12:00:00Z'); + expect(formatTime(targetDate, referenceDate)).toBe('04-01'); + }); + }); + + describe('edge cases', () => { + it('should handle exactly 0 seconds difference', () => { + expect(formatTime(mockNow, mockNow)).toBe('1s ago'); + }); + + it('should handle exactly 1 minute', () => { + const date = new Date(mockNow.getTime() - 60 * 1000); + expect(formatTime(date)).toBe('1m ago'); + }); + + it('should handle exactly 1 hour', () => { + const date = new Date(mockNow.getTime() - 60 * 60 * 1000); + expect(formatTime(date)).toBe('1h ago'); + }); + + it('should handle exactly 1 day', () => { + const date = new Date(mockNow.getTime() - 24 * 60 * 60 * 1000); + expect(formatTime(date)).toBe('1d ago'); + }); + + it('should handle exactly 7 days', () => { + const date = new Date('2024-05-19T12:00:00Z'); + expect(formatTime(date)).toBe('05-19'); + }); + }); + + describe('error handling', () => { + it('should throw error for invalid date input', () => { + expect(() => formatTime('invalid-date')).toThrow('Invalid date provided'); + }); + + it('should throw error for invalid reference date', () => { + const validDate = new Date(); + expect(() => formatTime(validDate, 'invalid-date')).toThrow( + 'Invalid date provided' + ); + }); + + it('should throw error for NaN date', () => { + expect(() => formatTime(new Date(NaN))).toThrow('Invalid date provided'); + }); + }); + + describe('date formatting edge cases', () => { + it('should properly pad single-digit months and days', () => { + const date = new Date('2024-01-05T12:00:00Z'); + expect(formatTime(date)).toBe('01-05'); + }); + + it('should handle leap years correctly', () => { + const date = new Date('2024-02-29T12:00:00Z'); + expect(formatTime(date)).toBe('02-29'); + }); + + it('should handle year boundaries correctly', () => { + vi.setSystemTime(new Date('2025-01-15T12:00:00Z')); + const date = new Date('2024-12-31T12:00:00Z'); + expect(formatTime(date)).toBe('12-31-2024'); + }); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts index ac1c744..8b28dd7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,17 +1 @@ -import { describe, it, expect } from "vitest"; -import { isOdd } from "../src"; - -describe("isOdd", () => { - it("returns true for odd numbers", () => { - expect(isOdd(1)).toBe(true); - expect(isOdd(-3)).toBe(true); - }); - - it("returns false for even numbers", () => { - expect(isOdd(2)).toBe(false); - }); - - it("throws error for non-integers", () => { - expect(() => isOdd(1.5)).toThrow(); - }); -}); +export * from './is-odd.test';