Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions src/format-time.ts
Original file line number Diff line number Diff line change
@@ -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 };
15 changes: 8 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
202 changes: 202 additions & 0 deletions test/format-time.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
18 changes: 1 addition & 17 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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';
17 changes: 17 additions & 0 deletions test/is-odd.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});