diff --git a/src/tools/epoch-converter/epoch-converter.e2e.spec.ts b/src/tools/epoch-converter/epoch-converter.e2e.spec.ts index d393e8e2..ebc4ca34 100644 --- a/src/tools/epoch-converter/epoch-converter.e2e.spec.ts +++ b/src/tools/epoch-converter/epoch-converter.e2e.spec.ts @@ -13,7 +13,8 @@ test.describe('Tool - Epoch converter', () => { await page.getByPlaceholder('Enter epoch timestamp').fill('1750452480'); await page.getByRole('button', { name: 'Convert to Date' }).click(); - await expect(page.getByText('6/23/2025, 12:31:00 PM')).toBeVisible(); + await expect(page.getByText('GMT (UTC): Fri, 20 Jun 2025 20:48:00 GMT')).toBeVisible(); + await expect(page.getByText('Local Time: Fri, 20 Jun 2025, 13:48:00 GMT-7')).toBeVisible(); }); test('Converts known date to epoch timestamp', async ({ page }) => { @@ -28,4 +29,40 @@ test.describe('Tool - Epoch converter', () => { await expect(page.getByText('1750452480')).toBeVisible(); }); + + test('Shows error if year is not 4 digits', async ({ page }) => { + await page.getByPlaceholder('YYYY').fill('99'); + await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); + await expect(page.getByText('Year must be 4 digits')).toBeVisible(); + }); + + test('Shows error if month is invalid', async ({ page }) => { + await page.getByPlaceholder('MM').first().fill('00'); + await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); + await expect(page.getByText('Month must be between 1 and 12')).toBeVisible(); + }); + + test('Shows error if day is invalid', async ({ page }) => { + await page.getByPlaceholder('DD').fill('0'); + await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); + await expect(page.getByText('Day must be between 1 and 31')).toBeVisible(); + }); + + test('Shows error if hour is invalid', async ({ page }) => { + await page.getByPlaceholder('HH').fill('24'); + await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); + await expect(page.getByText('Hour must be between 0 and 23')).toBeVisible(); + }); + + test('Shows error if minute is invalid', async ({ page }) => { + await page.getByPlaceholder('MM').nth(1).fill('61'); + await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); + await expect(page.getByText('Minute must be between 0 and 59')).toBeVisible(); + }); + + test('Shows error if second is invalid', async ({ page }) => { + await page.getByPlaceholder('SS').fill('99'); + await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); + await expect(page.getByText('Second must be between 0 and 59')).toBeVisible(); + }); }); diff --git a/src/tools/epoch-converter/epoch-converter.service.test.ts b/src/tools/epoch-converter/epoch-converter.service.test.ts index 8fdd31ba..b841fa3b 100644 --- a/src/tools/epoch-converter/epoch-converter.service.test.ts +++ b/src/tools/epoch-converter/epoch-converter.service.test.ts @@ -1,12 +1,22 @@ +import process from 'node:process'; import { describe, expect, it } from 'vitest'; -import { dateToEpoch, epochToDate } from './epoch-converter.service'; +import { type DateParts, dateToEpoch, epochToDate, getISODateString } from './epoch-converter.service'; + +process.env.TZ = 'America/Vancouver'; describe('epochToDate', () => { - it('converts known epoch seconds to correct local date string', () => { + it('converts known epoch seconds to correct formatted date (America/Vancouver)', () => { const epoch = 1750707060; - const expectedDate = '6/23/2025, 12:31:00 PM'; - const result = epochToDate(epoch); - expect(result).toBe(expectedDate); + const expectedUTC = 'Mon, 23 Jun 2025 19:31:00 GMT'; + const expectedLocal = 'Mon, Jun 23, 2025, 12:31:00 PDT'; + + const result = epochToDate(epoch, { + timeZone: 'America/Vancouver', + locale: 'en-US', + }); + + expect(result.local).toBe(expectedLocal); + expect(result.utc).toBe(expectedUTC); }); it('throws for invalid string input', () => { @@ -19,13 +29,21 @@ describe('epochToDate', () => { }); describe('dateToEpoch', () => { - it('converts a known date to correct epoch (2025-06-20 13:48:00)', () => { + it('converts a known local date to correct epoch (2025-06-20 13:48:00)', () => { const input = '2025-06-20T13:48:00'; + const expectedEpoch = 1750452480; const result = dateToEpoch(input); expect(result).toBe(expectedEpoch); }); + it('converts a UTC date to correct epoch', () => { + const input = '2025-06-20T13:48:00'; + const expectedEpoch = 1750427280; + const result = dateToEpoch(input, { parseAsUTC: true }); + expect(result).toBe(expectedEpoch); + }); + it('throws for invalid date string', () => { expect(() => dateToEpoch('not-a-date')).toThrowError(TypeError); }); @@ -34,3 +52,93 @@ describe('dateToEpoch', () => { expect(() => dateToEpoch('')).toThrowError(TypeError); }); }); + +describe('epochToDate - Year 2038 boundary', () => { + it('converts max 32-bit signed int epoch correctly (2038-01-19 03:14:07)', () => { + const epoch = 2147483647; + + const result = epochToDate(epoch, { + timeZone: 'America/Vancouver', + locale: 'en-US', + }); + + const expectedLocal = 'Mon, Jan 18, 2038, 19:14:07 PST'; + + const expectedUTC = 'Tue, 19 Jan 2038 03:14:07 GMT'; + + expect(result.local).toBe(expectedLocal); + expect(result.utc).toBe(expectedUTC); + }); + + it('handles epoch just after 32-bit boundary (2038-01-19 03:14:08)', () => { + const epoch = 2147483648; + + const result = epochToDate(epoch, { + timeZone: 'America/Vancouver', + locale: 'en-US', + }); + + const expectedLocal = 'Mon, Jan 18, 2038, 19:14:08 PST'; + + const expectedUTC = 'Tue, 19 Jan 2038 03:14:08 GMT'; + + expect(result.local).toBe(expectedLocal); + expect(result.utc).toBe(expectedUTC); + }); +}); + +describe('dateToEpoch - Year 2038 boundary (UTC)', () => { + it('converts "2038-01-19T03:14:07" to epoch 2147483647 in UTC mode', () => { + const result = dateToEpoch('2038-01-19T03:14:07', { parseAsUTC: true }); + expect(result).toBe(2147483647); + }); + + it('converts "2038-01-19T03:14:08" to epoch 2147483648 in UTC mode', () => { + const result = dateToEpoch('2038-01-19T03:14:08', { parseAsUTC: true }); + expect(result).toBe(2147483648); + }); +}); + +describe('getISODateString', () => { + it('generates a correctly padded ISO date string from date parts', () => { + const parts: DateParts = { + year: '2025', + month: '6', + day: '3', + hour: '9', + minute: '5', + second: '1', + }; + + const isoString = getISODateString(parts); + expect(isoString).toBe('2025-06-03T09:05:01'); + }); + + it('handles already padded inputs correctly', () => { + const parts: DateParts = { + year: '2025', + month: '12', + day: '31', + hour: '23', + minute: '59', + second: '59', + }; + + const isoString = getISODateString(parts); + expect(isoString).toBe('2025-12-31T23:59:59'); + }); + + it('handles all-zero input values', () => { + const parts: DateParts = { + year: '2025', + month: '0', + day: '0', + hour: '0', + minute: '0', + second: '0', + }; + + const isoString = getISODateString(parts); + expect(isoString).toBe('2025-00-00T00:00:00'); + }); +}); diff --git a/src/tools/epoch-converter/epoch-converter.service.ts b/src/tools/epoch-converter/epoch-converter.service.ts index 3f527f86..ee12bf68 100644 --- a/src/tools/epoch-converter/epoch-converter.service.ts +++ b/src/tools/epoch-converter/epoch-converter.service.ts @@ -1,19 +1,76 @@ -// Convert Epoch to Human Readable Date -export function epochToDate(epoch: string | number): string { +const MILLISECONDS_THRESHOLD = 1_000_000_000_000; +const MILLISECONDS_IN_SECOND = 1000; + +export interface DateParts { + year: string + month: string + day: string + hour: string + minute: string + second: string +} + +/** + * Converts a Unix epoch timestamp (in seconds or milliseconds) to a human-readable date. + * + * @param epoch - The epoch timestamp to convert (number or string). + * @param options - Optional locale and timeZone settings for local time formatting. + * @returns An object with both the local formatted string and UTC string. + */ +export function epochToDate( + epoch: string | number, + options?: { timeZone?: string; locale?: string }, +): { local: string; utc: string } { const num = typeof epoch === 'string' ? Number.parseInt(epoch, 10) : epoch; if (Number.isNaN(num)) { throw new TypeError('Invalid epoch timestamp'); } - const timestamp = num < 1e12 ? num * 1000 : num; + const isSecondsPrecision = num < MILLISECONDS_THRESHOLD; + const timestampInMs = isSecondsPrecision + ? num * MILLISECONDS_IN_SECOND + : num; - return new Date(timestamp).toLocaleString(); + const date = new Date(timestampInMs); + + const local = date.toLocaleString(options?.locale || 'en-US', { + timeZone: options?.timeZone, + weekday: 'short', + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short', + }); + + const utc = date.toUTCString(); + + return { local, utc }; } -// Convert Human Readable Date to Epoch -export function dateToEpoch(dateString: string): number { - const date = new Date(dateString); +/** + * Converts a human-readable ISO date string into a Unix epoch timestamp (in seconds). + * + * @param dateString - A string in ISO format (e.g. "2025-06-20T13:48:00"). + * @param options - Optional flag to interpret the date string as UTC time. + * @returns Epoch time as a number in seconds. + */ +export function dateToEpoch( + dateString: string, + options?: { + parseAsUTC?: boolean // if true, the date string will be parsed as UTC + }, +): number { + const shouldNormalizeToUTC = options?.parseAsUTC === true && !dateString.endsWith('Z'); + const normalizedDateString = shouldNormalizeToUTC + ? `${dateString}Z` + : dateString; + + const date = new Date(normalizedDateString); if (Number.isNaN(date.getTime())) { throw new TypeError('Invalid date string'); @@ -21,3 +78,22 @@ export function dateToEpoch(dateString: string): number { return Math.floor(date.getTime() / 1000); } + +/** + * Creates an ISO 8601 date string (e.g., "2025-06-20T13:48:00") from date parts. + * Ensures all components are zero-padded to 2 digits. + * + * @param parts - An object containing the year, month, day, hour, minute, and second. + * @returns A valid ISO string. + */ +export function getISODateString({ + year, + month, + day, + hour, + minute, + second, +}: DateParts): string { + const pad = (value: string) => value.toString().padStart(2, '0'); + return `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:${pad(second)}`; +} diff --git a/src/tools/epoch-converter/epoch-converter.vue b/src/tools/epoch-converter/epoch-converter.vue index aaaa5abd..0d6c7364 100644 --- a/src/tools/epoch-converter/epoch-converter.vue +++ b/src/tools/epoch-converter/epoch-converter.vue @@ -1,13 +1,16 @@ @@ -51,6 +106,16 @@ function pad(value: string): string { Epoch to Date + + + Epoch is interpreted as: + + 10 digits → seconds (e.g. 1718822594) + 13 digits → milliseconds (e.g. 1718822594000) + + Epoch values outside supported JavaScript range (±8.64e15) may result in invalid dates. + + - - Human-Readable Date: {{ dateOutput }} + + Local Time: {{ dateOutput.local }} + GMT (UTC): {{ dateOutput.utc }} + + {{ epochInputError }} + + @@ -99,17 +169,22 @@ function pad(value: string): string { - - Convert to Epoch - + + + Convert to Epoch (Local) + + + Convert to Epoch (UTC) + + Epoch Timestamp: {{ epochOutput }} - - {{ error }} + + {{ dateInputError }} diff --git a/src/tools/index.ts b/src/tools/index.ts index 67a1e6f6..d38359d5 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as gzipDecompressor } from './gzip-decompressor'; import { tool as epochConverter } from './epoch-converter'; import { tool as emailNormalizer } from './email-normalizer';
1718822594
1718822594000