From 1782ef4a4e2fe6af52e0b21a0c8946a6adb6556a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 20 Nov 2024 09:21:37 +0100 Subject: [PATCH] feat: add nowInBigIntNanoSeconds to utils (#756) # Motivation We actually use `nowInBigIntNanoSeconds` everywhere so let's add it to utils. # Changes - Copy function `nowInBigIntNanoSeconds` to `@dfinity/utils`. --------- Signed-off-by: David Dal Busco Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/utils/README.md | 11 + packages/utils/src/utils/date.utils.spec.ts | 443 ++++++++++---------- packages/utils/src/utils/date.utils.ts | 8 + 4 files changed, 253 insertions(+), 210 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9e85bca..b215f3311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added `memoToNeuronSubaccount` and `memoToNeuronAccountIdentifier`. - Support new neuron field `voting_power_refreshed_timestamp_seconds`. +- Add `nowInBigIntNanoSeconds` to `@dfinity/utils`, a trivial function that is actually used across all our dapps. ## Build diff --git a/packages/utils/README.md b/packages/utils/README.md index f7133d3b8..2575e3cd7 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -55,6 +55,7 @@ npm i @dfinity/agent @dfinity/candid @dfinity/principal - [decodeBase32](#gear-decodebase32) - [bigEndianCrc32](#gear-bigendiancrc32) - [secondsToDuration](#gear-secondstoduration) +- [nowInBigIntNanoSeconds](#gear-nowinbigintnanoseconds) - [debounce](#gear-debounce) - [toNullable](#gear-tonullable) - [fromNullable](#gear-fromnullable) @@ -302,6 +303,16 @@ Parameters: [:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/utils/src/utils/date.utils.ts#L43) +#### :gear: nowInBigIntNanoSeconds + +Returns the current timestamp in nanoseconds as a `bigint`. + +| Function | Type | +| ------------------------ | -------------- | +| `nowInBigIntNanoSeconds` | `() => bigint` | + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/utils/src/utils/date.utils.ts#L115) + #### :gear: debounce | Function | Type | diff --git a/packages/utils/src/utils/date.utils.spec.ts b/packages/utils/src/utils/date.utils.spec.ts index e48afa609..76323a5d2 100644 --- a/packages/utils/src/utils/date.utils.spec.ts +++ b/packages/utils/src/utils/date.utils.spec.ts @@ -1,221 +1,244 @@ import { describe } from "@jest/globals"; -import { I18nSecondsToDuration, secondsToDuration } from "./date.utils"; +import { + I18nSecondsToDuration, + nowInBigIntNanoSeconds, + secondsToDuration, +} from "./date.utils"; -const EN_TIME = { - year: "year", - year_plural: "years", - month: "month", - month_plural: "months", - day: "day", - day_plural: "days", - hour: "hour", - hour_plural: "hours", - minute: "minute", - minute_plural: "minutes", - second: "second", - second_plural: "seconds", -}; - -const FR_TIME = { - year: "an", - year_plural: "ans", - month: "mois", - month_plural: "mois", - day: "jour", - day_plural: "jours", - hour: "heure", - hour_plural: "heures", - minute: "minute", - minute_plural: "minutes", - second: "seconde", - second_plural: "secondes", -}; +describe("date.utils", () => { + const EN_TIME = { + year: "year", + year_plural: "years", + month: "month", + month_plural: "months", + day: "day", + day_plural: "days", + hour: "hour", + hour_plural: "hours", + minute: "minute", + minute_plural: "minutes", + second: "second", + second_plural: "seconds", + }; -const test = ( - i18nResult: I18nSecondsToDuration, - i18n?: I18nSecondsToDuration, -) => { - // This function should not be smart. It should just make it easier to add - // numbers together to get the number of seconds we want to test. - const renderSeconds = ({ - nonLeapYears = 0, - days = 0, - hours = 0, - minutes = 0, - seconds = 0, - }: { - nonLeapYears?: number; - days?: number; - hours?: number; - minutes?: number; - seconds?: number; - }) => { - days += 365 * nonLeapYears; - hours += 24 * days; - minutes += 60 * hours; - seconds += 60 * minutes; - return secondsToDuration({ seconds: BigInt(seconds), i18n }); + const FR_TIME = { + year: "an", + year_plural: "ans", + month: "mois", + month_plural: "mois", + day: "jour", + day_plural: "jours", + hour: "heure", + hour_plural: "heures", + minute: "minute", + minute_plural: "minutes", + second: "seconde", + second_plural: "secondes", }; - it("should give year details", () => { - expect(renderSeconds({ nonLeapYears: 1 })).toBe(`1 ${i18nResult.year}`); - expect(renderSeconds({ nonLeapYears: 1, seconds: 59 })).toBe( - `1 ${i18nResult.year}`, - ); - expect(renderSeconds({ nonLeapYears: 1, minutes: 59 })).toBe( - `1 ${i18nResult.year}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 1, hours: 23 })).toBe( - `1 ${i18nResult.year}, 23 ${i18nResult.hour_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 1, days: 1, seconds: -1 })).toBe( - `1 ${i18nResult.year}, 23 ${i18nResult.hour_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 1, days: 1 })).toBe( - `1 ${i18nResult.year}, 1 ${i18nResult.day}`, - ); - expect(renderSeconds({ nonLeapYears: 1, days: 2 })).toBe( - `1 ${i18nResult.year}, 2 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 2, seconds: -1 })).toBe( - `1 ${i18nResult.year}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 2 })).toBe( - `2 ${i18nResult.year_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 2, minutes: 59 })).toBe( - `2 ${i18nResult.year_plural}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 2, hours: 23 })).toBe( - `2 ${i18nResult.year_plural}, 23 ${i18nResult.hour_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 2, days: 1 })).toBe( - `2 ${i18nResult.year_plural}, 1 ${i18nResult.day}`, - ); - expect(renderSeconds({ nonLeapYears: 2, days: 2 })).toBe( - `2 ${i18nResult.year_plural}, 2 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 3, seconds: -1 })).toBe( - `2 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 3 })).toBe( - `3 ${i18nResult.year_plural}`, - ); - // 4 actual years have a leap day so we add 1 day to 4 nonLeap years. - expect(renderSeconds({ nonLeapYears: 4, days: 1, seconds: -1 })).toBe( - `3 ${i18nResult.year_plural}, 365 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 4, days: 1 })).toBe( - `4 ${i18nResult.year_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 5, days: 1, seconds: -1 })).toBe( - `4 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 5, days: 1 })).toBe( - `5 ${i18nResult.year_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 6, days: 1, seconds: -1 })).toBe( - `5 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 6, days: 1 })).toBe( - `6 ${i18nResult.year_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 7, days: 1, seconds: -1 })).toBe( - `6 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 7, days: 1 })).toBe( - `7 ${i18nResult.year_plural}`, - ); - // 4 actual years have 2 leap days so we add 2 days to 8 nonLeap years. - expect(renderSeconds({ nonLeapYears: 8, days: 2, seconds: -1 })).toBe( - `7 ${i18nResult.year_plural}, 365 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 8, days: 2 })).toBe( - `8 ${i18nResult.year_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 9, days: 2, seconds: -1 })).toBe( - `8 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, - ); - expect(renderSeconds({ nonLeapYears: 9, days: 2 })).toBe( - `9 ${i18nResult.year_plural}`, - ); - }); + const test = ( + i18nResult: I18nSecondsToDuration, + i18n?: I18nSecondsToDuration, + ) => { + // This function should not be smart. It should just make it easier to add + // numbers together to get the number of seconds we want to test. + const renderSeconds = ({ + nonLeapYears = 0, + days = 0, + hours = 0, + minutes = 0, + seconds = 0, + }: { + nonLeapYears?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + }) => { + days += 365 * nonLeapYears; + hours += 24 * days; + minutes += 60 * hours; + seconds += 60 * minutes; + return secondsToDuration({ seconds: BigInt(seconds), i18n }); + }; - it("should give day details", () => { - expect(renderSeconds({ days: 1 })).toBe(`1 ${i18nResult.day}`); - expect(renderSeconds({ days: 1, seconds: 59 })).toBe(`1 ${i18nResult.day}`); - expect(renderSeconds({ days: 1, minutes: 59 })).toBe( - `1 ${i18nResult.day}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ days: 1, hours: 1 })).toBe( - `1 ${i18nResult.day}, 1 ${i18nResult.hour}`, - ); - expect(renderSeconds({ days: 1, hours: 2 })).toBe( - `1 ${i18nResult.day}, 2 ${i18nResult.hour_plural}`, - ); - expect(renderSeconds({ days: 2, seconds: -1 })).toBe( - `1 ${i18nResult.day}, 23 ${i18nResult.hour_plural}`, - ); - expect(renderSeconds({ days: 2 })).toBe(`2 ${i18nResult.day_plural}`); - expect(renderSeconds({ days: 365, seconds: -1 })).toBe( - `364 ${i18nResult.day_plural}, 23 ${i18nResult.hour_plural}`, - ); - }); + it("should give year details", () => { + expect(renderSeconds({ nonLeapYears: 1 })).toBe(`1 ${i18nResult.year}`); + expect(renderSeconds({ nonLeapYears: 1, seconds: 59 })).toBe( + `1 ${i18nResult.year}`, + ); + expect(renderSeconds({ nonLeapYears: 1, minutes: 59 })).toBe( + `1 ${i18nResult.year}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 1, hours: 23 })).toBe( + `1 ${i18nResult.year}, 23 ${i18nResult.hour_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 1, days: 1, seconds: -1 })).toBe( + `1 ${i18nResult.year}, 23 ${i18nResult.hour_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 1, days: 1 })).toBe( + `1 ${i18nResult.year}, 1 ${i18nResult.day}`, + ); + expect(renderSeconds({ nonLeapYears: 1, days: 2 })).toBe( + `1 ${i18nResult.year}, 2 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 2, seconds: -1 })).toBe( + `1 ${i18nResult.year}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 2 })).toBe( + `2 ${i18nResult.year_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 2, minutes: 59 })).toBe( + `2 ${i18nResult.year_plural}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 2, hours: 23 })).toBe( + `2 ${i18nResult.year_plural}, 23 ${i18nResult.hour_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 2, days: 1 })).toBe( + `2 ${i18nResult.year_plural}, 1 ${i18nResult.day}`, + ); + expect(renderSeconds({ nonLeapYears: 2, days: 2 })).toBe( + `2 ${i18nResult.year_plural}, 2 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 3, seconds: -1 })).toBe( + `2 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 3 })).toBe( + `3 ${i18nResult.year_plural}`, + ); + // 4 actual years have a leap day so we add 1 day to 4 nonLeap years. + expect(renderSeconds({ nonLeapYears: 4, days: 1, seconds: -1 })).toBe( + `3 ${i18nResult.year_plural}, 365 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 4, days: 1 })).toBe( + `4 ${i18nResult.year_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 5, days: 1, seconds: -1 })).toBe( + `4 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 5, days: 1 })).toBe( + `5 ${i18nResult.year_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 6, days: 1, seconds: -1 })).toBe( + `5 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 6, days: 1 })).toBe( + `6 ${i18nResult.year_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 7, days: 1, seconds: -1 })).toBe( + `6 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 7, days: 1 })).toBe( + `7 ${i18nResult.year_plural}`, + ); + // 4 actual years have 2 leap days so we add 2 days to 8 nonLeap years. + expect(renderSeconds({ nonLeapYears: 8, days: 2, seconds: -1 })).toBe( + `7 ${i18nResult.year_plural}, 365 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 8, days: 2 })).toBe( + `8 ${i18nResult.year_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 9, days: 2, seconds: -1 })).toBe( + `8 ${i18nResult.year_plural}, 364 ${i18nResult.day_plural}`, + ); + expect(renderSeconds({ nonLeapYears: 9, days: 2 })).toBe( + `9 ${i18nResult.year_plural}`, + ); + }); - it("should give hour details", () => { - expect(renderSeconds({ hours: 1 })).toBe(`1 ${i18nResult.hour}`); - expect(renderSeconds({ hours: 1, seconds: 59 })).toBe( - `1 ${i18nResult.hour}`, - ); - expect(renderSeconds({ hours: 1, minutes: 59 })).toBe( - `1 ${i18nResult.hour}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ hours: 2, seconds: -1 })).toBe( - `1 ${i18nResult.hour}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ hours: 2 })).toBe(`2 ${i18nResult.hour_plural}`); - expect(renderSeconds({ hours: 2, minutes: 59 })).toBe( - `2 ${i18nResult.hour_plural}, 59 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ hours: 24, seconds: -1 })).toBe( - `23 ${i18nResult.hour_plural}, 59 ${i18nResult.minute_plural}`, - ); - }); + it("should give day details", () => { + expect(renderSeconds({ days: 1 })).toBe(`1 ${i18nResult.day}`); + expect(renderSeconds({ days: 1, seconds: 59 })).toBe( + `1 ${i18nResult.day}`, + ); + expect(renderSeconds({ days: 1, minutes: 59 })).toBe( + `1 ${i18nResult.day}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ days: 1, hours: 1 })).toBe( + `1 ${i18nResult.day}, 1 ${i18nResult.hour}`, + ); + expect(renderSeconds({ days: 1, hours: 2 })).toBe( + `1 ${i18nResult.day}, 2 ${i18nResult.hour_plural}`, + ); + expect(renderSeconds({ days: 2, seconds: -1 })).toBe( + `1 ${i18nResult.day}, 23 ${i18nResult.hour_plural}`, + ); + expect(renderSeconds({ days: 2 })).toBe(`2 ${i18nResult.day_plural}`); + expect(renderSeconds({ days: 365, seconds: -1 })).toBe( + `364 ${i18nResult.day_plural}, 23 ${i18nResult.hour_plural}`, + ); + }); - it("should give minute details", () => { - expect(renderSeconds({ minutes: 1 })).toBe(`1 ${i18nResult.minute}`); - expect(renderSeconds({ minutes: 1, seconds: 1 })).toBe( - `1 ${i18nResult.minute}`, - ); - expect(renderSeconds({ minutes: 1, seconds: 59 })).toBe( - `1 ${i18nResult.minute}`, - ); - expect(renderSeconds({ minutes: 2 })).toBe(`2 ${i18nResult.minute_plural}`); - expect(renderSeconds({ minutes: 2, seconds: 59 })).toBe( - `2 ${i18nResult.minute_plural}`, - ); - expect(renderSeconds({ minutes: 60, seconds: -1 })).toBe( - `59 ${i18nResult.minute_plural}`, - ); - }); + it("should give hour details", () => { + expect(renderSeconds({ hours: 1 })).toBe(`1 ${i18nResult.hour}`); + expect(renderSeconds({ hours: 1, seconds: 59 })).toBe( + `1 ${i18nResult.hour}`, + ); + expect(renderSeconds({ hours: 1, minutes: 59 })).toBe( + `1 ${i18nResult.hour}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ hours: 2, seconds: -1 })).toBe( + `1 ${i18nResult.hour}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ hours: 2 })).toBe(`2 ${i18nResult.hour_plural}`); + expect(renderSeconds({ hours: 2, minutes: 59 })).toBe( + `2 ${i18nResult.hour_plural}, 59 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ hours: 24, seconds: -1 })).toBe( + `23 ${i18nResult.hour_plural}, 59 ${i18nResult.minute_plural}`, + ); + }); - it("should give seconds details", () => { - expect(secondsToDuration({ seconds: BigInt(2), i18n })).toBe( - `2 ${i18nResult.second_plural}`, - ); - expect(secondsToDuration({ seconds: BigInt(59), i18n })).toBe( - `59 ${i18nResult.second_plural}`, - ); - }); + it("should give minute details", () => { + expect(renderSeconds({ minutes: 1 })).toBe(`1 ${i18nResult.minute}`); + expect(renderSeconds({ minutes: 1, seconds: 1 })).toBe( + `1 ${i18nResult.minute}`, + ); + expect(renderSeconds({ minutes: 1, seconds: 59 })).toBe( + `1 ${i18nResult.minute}`, + ); + expect(renderSeconds({ minutes: 2 })).toBe( + `2 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ minutes: 2, seconds: 59 })).toBe( + `2 ${i18nResult.minute_plural}`, + ); + expect(renderSeconds({ minutes: 60, seconds: -1 })).toBe( + `59 ${i18nResult.minute_plural}`, + ); + }); - it("should give a second details", () => { - expect(secondsToDuration({ seconds: BigInt(1), i18n })).toBe( - `1 ${i18nResult.second}`, - ); - }); -}; + it("should give seconds details", () => { + expect(secondsToDuration({ seconds: BigInt(2), i18n })).toBe( + `2 ${i18nResult.second_plural}`, + ); + expect(secondsToDuration({ seconds: BigInt(59), i18n })).toBe( + `59 ${i18nResult.second_plural}`, + ); + }); -describe("secondsToDuration default lang", () => test(EN_TIME, undefined)); -describe.each([EN_TIME, FR_TIME])("secondsToDuration %p", (time) => - test(time, time), -); + it("should give a second details", () => { + expect(secondsToDuration({ seconds: BigInt(1), i18n })).toBe( + `1 ${i18nResult.second}`, + ); + }); + }; + + describe("secondsToDuration default lang", () => test(EN_TIME, undefined)); + describe.each([EN_TIME, FR_TIME])("secondsToDuration %p", (time) => + test(time, time), + ); + + describe("nowInBigIntNanoSeconds", () => { + it("should return the current timestamp in nanoseconds as a bigint", () => { + const mockDateNow = 1698416400000; + jest.spyOn(Date, "now").mockReturnValue(mockDateNow); + + const expectedNanoSeconds = BigInt(mockDateNow) * BigInt(1e6); + + const result = nowInBigIntNanoSeconds(); + + expect(result).toBe(expectedNanoSeconds); + }); + }); +}); diff --git a/packages/utils/src/utils/date.utils.ts b/packages/utils/src/utils/date.utils.ts index b74446a4c..3cffd0713 100644 --- a/packages/utils/src/utils/date.utils.ts +++ b/packages/utils/src/utils/date.utils.ts @@ -106,3 +106,11 @@ const createLabel = (labelKey: LabelKey, amount: bigint): LabelInfo => ({ labelKey, amount: Number(amount), }); + +/** + * Returns the current timestamp in nanoseconds as a `bigint`. + * + * @returns {bigint} The current timestamp in nanoseconds. + */ +export const nowInBigIntNanoSeconds = (): bigint => + BigInt(Date.now()) * BigInt(1e6);