From 88ecf60587207ab6c937d50953773bc127d8cd03 Mon Sep 17 00:00:00 2001 From: sharevb Date: Sun, 25 Feb 2024 12:52:34 +0100 Subject: [PATCH 1/3] fix(date-time-converter): handle timestamp in microseconds Fix #783 --- .../date-time-converter.models.test.ts | 41 +++++++++++++++++++ .../date-time-converter.models.ts | 18 +++++++- .../date-time-converter.vue | 4 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts index c2c7bee9..c375e3d2 100644 --- a/src/tools/date-time-converter/date-time-converter.models.test.ts +++ b/src/tools/date-time-converter/date-time-converter.models.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest'; import { dateToExcelFormat, excelFormatToDate, + fromTimestamp, isExcelFormat, isISO8601DateTimeString, isISO9075DateString, @@ -113,6 +114,46 @@ 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(isTimestamp('1649792026123123')).toBe(true); + expect(isTimestamp('1701227351995845')).toBe(true); + expect(isTimestamp('0')).toBe(true); + }); + + test('should return false for invalid Unix timestamps in microseconds', () => { + expect(isTimestamp('foo')).toBe(false); + expect(isTimestamp('')).toBe(false); + }); + + test('should return false for invalid Unix timestamps not in microseconds', () => { + expect(isTimestamp('170122735199584')).toBe(false); + expect(isTimestamp('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', () => { diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts index f5eedbfa..330c16ec 100644 --- a/src/tools/date-time-converter/date-time-converter.models.ts +++ b/src/tools/date-time-converter/date-time-converter.models.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { addMilliseconds } from 'date-fns'; export { isISO8601DateTimeString, @@ -12,6 +13,8 @@ export { dateToExcelFormat, excelFormatToDate, isExcelFormat, + fromTimestamp, + isTimestampMicroSeconds, }; const ISO8601_REGEX @@ -35,7 +38,9 @@ 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 isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX); @@ -60,3 +65,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); +} diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue index 5636ed46..06aa7e3e 100644 --- a/src/tools/date-time-converter/date-time-converter.vue +++ b/src/tools/date-time-converter/date-time-converter.vue @@ -10,12 +10,12 @@ import { isDate, isValid, parseISO, - parseJSON, } from 'date-fns'; import type { DateFormat, ToDateMapper } from './date-time-converter.types'; import { dateToExcelFormat, excelFormatToDate, + fromTimestamp, isExcelFormat, isISO8601DateTimeString, isISO9075DateString, @@ -73,7 +73,7 @@ const formats: DateFormat[] = [ { name: 'Timestamp', fromDate: date => String(getTime(date)), - toDate: ms => parseJSON(+ms), + toDate: ms => fromTimestamp(ms), formatMatcher: date => isTimestamp(date), }, { From 6fd79d6e060f21d33a9a0e285a0f20275554462d Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sun, 1 Sep 2024 18:14:36 +0200 Subject: [PATCH 2/3] fix(date-time-converter): add UTC ISO Display and JS Date Constructor Fix #1198 --- package.json | 1 + pnpm-lock.yaml | 24 ++++++++----- .../date-time-converter.models.test.ts | 35 +++++++++++++++++++ .../date-time-converter.models.ts | 13 +++++++ .../date-time-converter.vue | 16 +++++++++ 5 files changed, 81 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 65f29dbd..97c827f3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6f7c32f..be0af6be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@date-fns/utc': + specifier: ^1.2.0 + version: 1.2.0 '@it-tools/bip39': specifier: ^0.0.4 version: 0.0.4 @@ -1905,6 +1908,10 @@ packages: vue: 3.3.4 dev: false + /@date-fns/utc@1.2.0: + resolution: {integrity: sha512-YLq+crMPJiBmIdkRmv9nZuZy1mVtMlDcUKlg4mvI0UsC/dZeIaGoGB5p/C4FrpeOhZ7zBTK03T58S0DFkRNMnw==} + dev: false + /@emotion/hash@0.8.0: resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} dev: false @@ -3341,7 +3348,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.7.2(vue@3.3.4) + '@vueuse/shared': 11.0.3(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3983,10 +3990,10 @@ packages: - vue dev: false - /@vueuse/shared@10.7.2(vue@3.3.4): - resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} + /@vueuse/shared@11.0.3(vue@3.3.4): + resolution: {integrity: sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==} dependencies: - vue-demi: 0.14.6(vue@3.3.4) + vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -9120,8 +9127,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.5(vue@3.3.4): - resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} + /vue-demi@0.14.10(vue@3.3.4): + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9135,8 +9142,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.6(vue@3.3.4): - resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + /vue-demi@0.14.5(vue@3.3.4): + resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} engines: {node: '>=12'} hasBin: true requiresBuild: true @@ -9426,6 +9433,7 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts index c375e3d2..fb6cc63f 100644 --- a/src/tools/date-time-converter/date-time-converter.models.test.ts +++ b/src/tools/date-time-converter/date-time-converter.models.test.ts @@ -2,16 +2,19 @@ import { describe, expect, test } from 'vitest'; import { dateToExcelFormat, excelFormatToDate, + fromJSDate, fromTimestamp, isExcelFormat, isISO8601DateTimeString, isISO9075DateString, + isJSDate, isMongoObjectId, isRFC3339DateString, isRFC7231DateString, isTimestamp, isUTCDateString, isUnixTimestamp, + toJSDate, } from './date-time-converter.models'; describe('date-time-converter models', () => { @@ -218,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);'); + }); + }); }); diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts index 330c16ec..e959a29b 100644 --- a/src/tools/date-time-converter/date-time-converter.models.ts +++ b/src/tools/date-time-converter/date-time-converter.models.ts @@ -15,6 +15,9 @@ export { isExcelFormat, fromTimestamp, isTimestampMicroSeconds, + isJSDate, + fromJSDate, + toJSDate, }; const ISO8601_REGEX @@ -29,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); } @@ -43,6 +48,14 @@ 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) { diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue index 06aa7e3e..d4919851 100644 --- a/src/tools/date-time-converter/date-time-converter.vue +++ b/src/tools/date-time-converter/date-time-converter.vue @@ -11,20 +11,24 @@ import { isValid, parseISO, } 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'; @@ -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, @@ -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); From 3ef7e50b5b185030973af05d2fd685aa480bd4b0 Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sun, 1 Sep 2024 18:21:03 +0200 Subject: [PATCH 3/3] fix: milliseconds test and e2e --- .../date-time-converter.e2e.spec.ts | 3 ++- .../date-time-converter.models.test.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts index 249dd754..dd5ed779 100644 --- a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts +++ b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts @@ -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'); @@ -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'); diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts index fb6cc63f..fca174cc 100644 --- a/src/tools/date-time-converter/date-time-converter.models.test.ts +++ b/src/tools/date-time-converter/date-time-converter.models.test.ts @@ -12,6 +12,7 @@ import { isRFC3339DateString, isRFC7231DateString, isTimestamp, + isTimestampMicroSeconds, isUTCDateString, isUnixTimestamp, toJSDate, @@ -130,19 +131,18 @@ describe('date-time-converter models', () => { describe('isTimestampMicroSeconds', () => { test('should return true for valid Unix timestamps in microseconds', () => { - expect(isTimestamp('1649792026123123')).toBe(true); - expect(isTimestamp('1701227351995845')).toBe(true); - expect(isTimestamp('0')).toBe(true); + expect(isTimestampMicroSeconds('1649792026123123')).toBe(true); + expect(isTimestampMicroSeconds('1701227351995845')).toBe(true); }); test('should return false for invalid Unix timestamps in microseconds', () => { - expect(isTimestamp('foo')).toBe(false); - expect(isTimestamp('')).toBe(false); + expect(isTimestampMicroSeconds('foo')).toBe(false); + expect(isTimestampMicroSeconds('')).toBe(false); }); test('should return false for invalid Unix timestamps not in microseconds', () => { - expect(isTimestamp('170122735199584')).toBe(false); - expect(isTimestamp('17012273519958')).toBe(false); + expect(isTimestampMicroSeconds('170122735199584')).toBe(false); + expect(isTimestampMicroSeconds('17012273519958')).toBe(false); }); });