Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(date-time-converter): handle timestamp in microseconds, UTC and more #903

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"release": "node ./scripts/release.mjs"
},
"dependencies": {
"@date-fns/utc": "^1.2.0",
"@it-tools/bip39": "^0.0.4",
"@it-tools/oggen": "^1.3.0",
"@sindresorhus/slugify": "^2.2.1",
Expand Down
24 changes: 16 additions & 8 deletions pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test.describe('Date time converter - json to yaml', () => {

test('Format is auto detected from a date and the date is correctly converted', async ({ page }) => {
const initialFormat = await page.getByTestId('date-time-converter-format-select').innerText();
expect(initialFormat.trim()).toEqual('Timestamp');
expect(initialFormat.trim()).toEqual('Unix timestamp');

await page.getByTestId('date-time-converter-input').fill('2023-04-12T23:10:24+02:00');

Expand All @@ -22,6 +22,7 @@ test.describe('Date time converter - json to yaml', () => {
'Wed Apr 12 2023 23:10:24 GMT+0200 (Central European Summer Time)',
);
expect((await page.getByTestId('ISO 8601').inputValue()).trim()).toEqual('2023-04-12T23:10:24+02:00');
expect((await page.getByTestId('ISO 8601 UTC').inputValue()).trim()).toEqual('2023-04-12T21:10:24.000Z');
expect((await page.getByTestId('ISO 9075').inputValue()).trim()).toEqual('2023-04-12 23:10:24');
expect((await page.getByTestId('Unix timestamp').inputValue()).trim()).toEqual('1681333824');
expect((await page.getByTestId('RFC 7231').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
Expand Down
76 changes: 76 additions & 0 deletions src/tools/date-time-converter/date-time-converter.models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { describe, expect, test } from 'vitest';
import {
dateToExcelFormat,
excelFormatToDate,
fromJSDate,
fromTimestamp,
isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isJSDate,
isMongoObjectId,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isTimestampMicroSeconds,
isUTCDateString,
isUnixTimestamp,
toJSDate,
} from './date-time-converter.models';

describe('date-time-converter models', () => {
Expand Down Expand Up @@ -113,6 +118,45 @@ describe('date-time-converter models', () => {
expect(isTimestamp('foo')).toBe(false);
expect(isTimestamp('')).toBe(false);
});

test('should return true for valid Unix timestamps in microseconds', () => {
expect(isTimestamp('1701227351995845')).toBe(true);
});

test('should return false for invalid Unix timestamps in microseconds', () => {
expect(isTimestamp('170122735199584')).toBe(false);
expect(isTimestamp('17012273519958')).toBe(false);
});
});

describe('isTimestampMicroSeconds', () => {
test('should return true for valid Unix timestamps in microseconds', () => {
expect(isTimestampMicroSeconds('1649792026123123')).toBe(true);
expect(isTimestampMicroSeconds('1701227351995845')).toBe(true);
});

test('should return false for invalid Unix timestamps in microseconds', () => {
expect(isTimestampMicroSeconds('foo')).toBe(false);
expect(isTimestampMicroSeconds('')).toBe(false);
});

test('should return false for invalid Unix timestamps not in microseconds', () => {
expect(isTimestampMicroSeconds('170122735199584')).toBe(false);
expect(isTimestampMicroSeconds('17012273519958')).toBe(false);
});
});

describe('fromTimestamp', () => {
test('should return valid Date for valid Unix timestamps in microseconds', () => {
expect(fromTimestamp('1649792026123123').toString()).toBe(new Date(1649792026123).toString());
expect(fromTimestamp('1701227351995845').toString()).toBe(new Date(1701227351995).toString());
expect(fromTimestamp('0').toString()).toBe(new Date(0).toString());
});

test('should return Date(0) for invalid Unix timestamps not in microseconds', () => {
expect(fromTimestamp('170122735199584').toString()).toBe(new Date(0).toString());
expect(fromTimestamp('17012273519958').toString()).toBe(new Date(0).toString());
});
});

describe('isUTCDateString', () => {
Expand Down Expand Up @@ -177,4 +221,36 @@ describe('date-time-converter models', () => {
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
});
});

describe('isJSDate', () => {
test('a JS date is a new Date()', () => {
expect(isJSDate('new Date(2000, 0)')).toBe(true);
expect(isJSDate('new Date(2000, 0, 1, 12, 12)')).toBe(true);
expect(isJSDate('new Date(2000, 0, 1, 12, 12, 12)')).toBe(true);
expect(isJSDate('new Date(2000, 0, 1, 12, 12, 12, 1)')).toBe(true);

expect(isJSDate('new Date(2000)')).toBe(false);
expect(isJSDate('')).toBe(false);
expect(isJSDate('foo')).toBe(false);
expect(isJSDate('1.1.1')).toBe(false);
});
});

describe('fromJSDate', () => {
test('convert a JS new Date() to date', () => {
expect(fromJSDate('new Date(2000, 0)')).toEqual(new Date(2000, 0));
expect(fromJSDate('new Date(2000, 0, 1, 12, 12)')).toEqual(new Date(2000, 0, 1, 12, 12));
expect(fromJSDate('new Date(2000, 0, 1, 12, 12, 12)')).toEqual(new Date(2000, 0, 1, 12, 12, 12));
expect(fromJSDate('new Date(2000, 0, 1, 12, 12, 12, 1)')).toEqual(new Date(2000, 0, 1, 12, 12, 12, 1));
});
});

describe('toJSDate', () => {
test('convert a date to JS new Date()', () => {
expect(toJSDate(new Date(2000, 0))).toEqual('new Date(2000, 0, 1, 0, 0, 0, 0);');
expect(toJSDate(new Date(2000, 0, 1, 12, 12))).toEqual('new Date(2000, 0, 1, 12, 12, 0, 0);');
expect(toJSDate(new Date(2000, 0, 1, 12, 12, 12))).toEqual('new Date(2000, 0, 1, 12, 12, 12, 0);');
expect(toJSDate(new Date(2000, 0, 1, 12, 12, 12, 1))).toEqual('new Date(2000, 0, 1, 12, 12, 12, 1);');
});
});
});
31 changes: 30 additions & 1 deletion src/tools/date-time-converter/date-time-converter.models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash';
import { addMilliseconds } from 'date-fns';

export {
isISO8601DateTimeString,
Expand All @@ -12,6 +13,11 @@ export {
dateToExcelFormat,
excelFormatToDate,
isExcelFormat,
fromTimestamp,
isTimestampMicroSeconds,
isJSDate,
fromJSDate,
toJSDate,
};

const ISO8601_REGEX
Expand All @@ -26,6 +32,8 @@ const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:

const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;

const JS_DATE_REGEX = /^new\s+Date\(\s*(?:(\d+)\s*,\s*)(?:(\d|11)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*)?)?)?)?)?(\d+)\)\s*;?$/;

function createRegexMatcher(regex: RegExp) {
return (date?: string) => !_.isNil(date) && regex.test(date);
}
Expand All @@ -35,9 +43,19 @@ const isISO9075DateString = createRegexMatcher(ISO9075_REGEX);
const isRFC3339DateString = createRegexMatcher(RFC3339_REGEX);
const isRFC7231DateString = createRegexMatcher(RFC7231_REGEX);
const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
const isTimestamp = createRegexMatcher(/^([0-9]{1,13}|[0-9]{16})$/);
const isTimestampMilliSeconds = createRegexMatcher(/^[0-9]{1,13}$/);
const isTimestampMicroSeconds = createRegexMatcher(/^[0-9]{16}$/);
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);

const isJSDate = createRegexMatcher(JS_DATE_REGEX);
function fromJSDate(date: string): Date {
const res = JS_DATE_REGEX.exec(date);
const parts = (res || []).filter(p => p !== undefined).map(p => Number.parseInt(p, 10)).slice(1);
return new (Function.prototype.bind.apply(Date, [null, ...parts]))();
}
const toJSDate = (date: Date) => `new Date(${date.getFullYear()}, ${date.getMonth()}, ${date.getDate()}, ${date.getHours()}, ${date.getMinutes()}, ${date.getSeconds()}, ${date.getMilliseconds()});`;

const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);

function isUTCDateString(date?: string) {
Expand All @@ -60,3 +78,14 @@ function dateToExcelFormat(date: Date) {
function excelFormatToDate(excelFormat: string | number) {
return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
}

function fromTimestamp(timestamp: string, type: 'auto' | 'milliseconds' | 'microseconds' = 'auto') {
let milliSeconds = 0;
if (type === 'microseconds' || isTimestampMicroSeconds(timestamp)) {
milliSeconds = Number(timestamp) / 1000;
}
else if (type === 'milliseconds' || isTimestampMilliSeconds(timestamp)) {
milliSeconds = Number(timestamp);
}
return addMilliseconds(new Date(0), milliSeconds);
}
20 changes: 18 additions & 2 deletions src/tools/date-time-converter/date-time-converter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ import {
isDate,
isValid,
parseISO,
parseJSON,
} from 'date-fns';
import { UTCDate } from '@date-fns/utc';
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
import {
dateToExcelFormat,
excelFormatToDate,
fromJSDate,
fromTimestamp,
isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isJSDate,
isMongoObjectId,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isUTCDateString,
isUnixTimestamp,
toJSDate,
} from './date-time-converter.models';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
Expand All @@ -46,6 +50,12 @@ const formats: DateFormat[] = [
toDate: parseISO,
formatMatcher: date => isISO8601DateTimeString(date),
},
{
name: 'ISO 8601 UTC',
fromDate: date => (new UTCDate(date)).toISOString(),
toDate: parseISO,
formatMatcher: date => isISO8601DateTimeString(date),
},
{
name: 'ISO 9075',
fromDate: formatISO9075,
Expand Down Expand Up @@ -73,7 +83,7 @@ const formats: DateFormat[] = [
{
name: 'Timestamp',
fromDate: date => String(getTime(date)),
toDate: ms => parseJSON(+ms),
toDate: ms => fromTimestamp(ms),
formatMatcher: date => isTimestamp(date),
},
{
Expand All @@ -94,6 +104,12 @@ const formats: DateFormat[] = [
toDate: excelFormatToDate,
formatMatcher: isExcelFormat,
},
{
name: 'JS Date',
fromDate: date => toJSDate(date),
toDate: date => fromJSDate(date),
formatMatcher: isJSDate,
},
];

const formatIndex = ref(6);
Expand Down
Loading