Added more test cases including github server, updated functions in epoch-converter.service.ts
This commit is contained in:
		
							parent
							
								
									00b1148e3a
								
							
						
					
					
						commit
						965e6ccd50
					
				| @ -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(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -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'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -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)}`; | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,16 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import { dateToEpoch, epochToDate } from './epoch-converter.service.ts'; | ||||
| import { type DateParts, dateToEpoch, epochToDate, getISODateString } from './epoch-converter.service.ts'; | ||||
| 
 | ||||
| const epochInput = ref(''); | ||||
| const dateOutput = ref(''); | ||||
| const dateOutput = ref<{ local: string; utc: string } | null>(null); | ||||
| const epochOutput = ref(''); | ||||
| const error = ref<string | null>(null); | ||||
| 
 | ||||
| const dateParts = ref({ | ||||
| const epochInputError = ref<string | null>(null); | ||||
| const dateInputError = ref<string | null>(null); | ||||
| 
 | ||||
| const dateParts = ref<DateParts>({ | ||||
|   year: '', | ||||
|   month: '', | ||||
|   day: '', | ||||
| @ -16,32 +19,84 @@ const dateParts = ref({ | ||||
|   second: '', | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * Converts a Unix epoch timestamp to human-readable date strings | ||||
|  * Accepts seconds (10-digit) or milliseconds (13-digit) | ||||
|  */ | ||||
| function convertEpochToDate() { | ||||
|   error.value = null; | ||||
|   try { | ||||
|     dateOutput.value = epochToDate(epochInput.value); | ||||
|   } | ||||
|   catch (e: any) { | ||||
|     error.value = e.message; | ||||
|     epochInputError.value = e.message; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function convertDateToEpoch() { | ||||
|   error.value = null; | ||||
| /** | ||||
|  * Converts a Unix epoch timestamp to human-readable date strings | ||||
|  * Accepts seconds (10-digit) or milliseconds (13-digit) | ||||
|  */ | ||||
| function convertDateToEpochLocal() { | ||||
|   try { | ||||
|     const { year, month, day, hour, minute, second } = dateParts.value; | ||||
| 
 | ||||
|     const isoString = `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:${pad(second)}`; | ||||
|     validateDateParts(dateParts.value); | ||||
|     const isoString = getISODateString(dateParts.value); | ||||
|     epochOutput.value = dateToEpoch(isoString); | ||||
|     dateInputError.value = null; | ||||
|   } | ||||
|   catch (e: any) { | ||||
|     error.value = e.message; | ||||
|     epochOutput.value = ''; | ||||
|     dateInputError.value = e.message; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function pad(value: string): string { | ||||
|   return value.toString().padStart(2, '0'); | ||||
| /** | ||||
|  * Converts UTC date string parts to epoch (seconds) | ||||
|  */ | ||||
| function convertDateToEpochUTC() { | ||||
|   try { | ||||
|     validateDateParts(dateParts.value); | ||||
|     const isoString = getISODateString(dateParts.value); | ||||
|     epochOutput.value = dateToEpoch(isoString, { utc: true }); | ||||
|     dateInputError.value = null; | ||||
|   } | ||||
|   catch (e: any) { | ||||
|     epochOutput.value = ''; | ||||
|     dateInputError.value = e.message; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function validateDateParts(parts: DateParts): void { | ||||
|   const { year, month, day, hour, minute, second } = parts; | ||||
| 
 | ||||
|   if (!/^\d{4}$/.test(year)) { | ||||
|     throw new Error('Year must be 4 digits'); | ||||
|   } | ||||
|   if (+month < 1 || +month > 12) { | ||||
|     throw new Error('Month must be between 1 and 12'); | ||||
|   } | ||||
|   if (+day < 1 || +day > 31) { | ||||
|     throw new Error('Day must be between 1 and 31'); | ||||
|   } | ||||
|   if (+hour < 0 || +hour > 23) { | ||||
|     throw new Error('Hour must be between 0 and 23'); | ||||
|   } | ||||
|   if (+minute < 0 || +minute > 59) { | ||||
|     throw new Error('Minute must be between 0 and 59'); | ||||
|   } | ||||
|   if (+second < 0 || +second > 59) { | ||||
|     throw new Error('Second must be between 0 and 59'); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Clear errors on input | ||||
| watch(epochInput, () => { | ||||
|   epochInputError.value = null; | ||||
| }); | ||||
| 
 | ||||
| watch(dateParts, () => { | ||||
|   dateInputError.value = null; | ||||
| }, { deep: true }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| @ -51,6 +106,16 @@ function pad(value: string): string { | ||||
|       <div class="mb-2 font-semibold"> | ||||
|         Epoch to Date | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="mb-2 text-xs text-neutral-400"> | ||||
|         Epoch is interpreted as: | ||||
|         <ul class="mt-1 list-disc pl-4"> | ||||
|           <li><strong>10 digits</strong> → seconds (e.g. <code>1718822594</code>)</li> | ||||
|           <li><strong>13 digits</strong> → milliseconds (e.g. <code>1718822594000</code>)</li> | ||||
|         </ul> | ||||
|         Epoch values outside supported JavaScript range (±8.64e15) may result in invalid dates. | ||||
|       </div> | ||||
| 
 | ||||
|       <c-input-text | ||||
|         v-model:value="epochInput" | ||||
|         placeholder="Enter epoch timestamp (e.g. 1718822594)" | ||||
| @ -61,11 +126,16 @@ function pad(value: string): string { | ||||
|         Convert to Date | ||||
|       </c-button> | ||||
| 
 | ||||
|       <div v-if="dateOutput" class="mt-4 text-sm text-green-400"> | ||||
|         Human-Readable Date: <strong>{{ dateOutput }}</strong> | ||||
|       <div v-if="dateOutput" class="mt-4 text-sm text-green-400 space-y-1"> | ||||
|         <div>Local Time: <strong>{{ dateOutput.local }}</strong></div> | ||||
|         <div>GMT (UTC): <strong>{{ dateOutput.utc }}</strong></div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <c-alert v-if="epochInputError" type="error" class="mb-4 mt-4"> | ||||
|       {{ epochInputError }} | ||||
|     </c-alert> | ||||
| 
 | ||||
|     <!-- Date to Epoch --> | ||||
|     <div class="mb-8"> | ||||
|       <div class="mb-2 font-semibold"> | ||||
| @ -99,17 +169,22 @@ function pad(value: string): string { | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <c-button @click="convertDateToEpoch"> | ||||
|         Convert to Epoch | ||||
|       <div class="mt-2 flex gap-4"> | ||||
|         <c-button @click="convertDateToEpochLocal"> | ||||
|           Convert to Epoch (Local) | ||||
|         </c-button> | ||||
|         <c-button @click="convertDateToEpochUTC"> | ||||
|           Convert to Epoch (UTC) | ||||
|         </c-button> | ||||
|       </div> | ||||
| 
 | ||||
|       <div v-if="epochOutput" class="mt-4 text-sm text-green-400"> | ||||
|         Epoch Timestamp: <strong>{{ epochOutput }}</strong> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <c-alert v-if="error" type="error" class="mt-6"> | ||||
|       {{ error }} | ||||
|     <c-alert v-if="dateInputError" type="error" class="mb-4 mt-4"> | ||||
|       {{ dateInputError }} | ||||
|     </c-alert> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| @ -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'; | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user