mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-08-06 14:05:18 +02:00
Added more test cases including github server, updated functions in epoch-converter.service.ts
This commit is contained in:
parent
00b1148e3a
commit
965e6ccd50
5 changed files with 330 additions and 33 deletions
|
@ -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
|
||||
</c-button>
|
||||
<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…
Add table
Add a link
Reference in a new issue