diff --git a/.changeset/bright-clocks-count.md b/.changeset/bright-clocks-count.md new file mode 100644 index 00000000000..8f3df8ee01a --- /dev/null +++ b/.changeset/bright-clocks-count.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `DateTime.toEpochSeconds` and `DateTime.fromEpochSeconds` for converting date-time values to and from Unix epoch seconds. diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index afa1ab1f9c6..c121861a27a 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -652,6 +652,22 @@ export const fromDateUnsafe: (date: Date) => Utc = Internal.fromDateUnsafe */ export const makeUnsafe: (input: A) => DateTime.PreserveZone = Internal.makeUnsafe +/** + * Creates a `DateTime.Utc` from the number of seconds since the Unix epoch. + * + * **Example** (Creating from epoch seconds) + * + * ```ts import.meta.vitest + * import { DateTime } from "effect" + * + * DateTime.fromEpochSeconds(1704067200).toJSON() // => "2024-01-01T00:00:00.000Z" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromEpochSeconds: (seconds: number) => Utc = Internal.fromEpochSeconds + /** * Create a `DateTime.Zoned` using `DateTime.makeUnsafe` and a time zone. * @@ -1600,6 +1616,28 @@ export const zonedOffsetIso: (self: Zoned) => string = Internal.zonedOffsetIso */ export const toEpochMillis: (self: DateTime) => number = Internal.toEpochMillis +/** + * Converts a `DateTime` to the number of seconds since the Unix epoch. + * + * **Details** + * + * This returns the UTC timestamp regardless of any time zone information. + * The result is floored to the nearest second. + * + * **Example** (Reading epoch seconds) + * + * ```ts import.meta.vitest + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + * DateTime.toEpochSeconds(dt) // => 1704067200 + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const toEpochSeconds: (self: DateTime) => number = Internal.toEpochSeconds + /** * Removes the time aspect of a `DateTime`, first adjusting for the time * zone. It will return a `DateTime.Utc` only containing the date. diff --git a/packages/effect/src/internal/dateTime.ts b/packages/effect/src/internal/dateTime.ts index 63a9c2cd8b9..287ae8ef94e 100644 --- a/packages/effect/src/internal/dateTime.ts +++ b/packages/effect/src/internal/dateTime.ts @@ -600,6 +600,12 @@ export const zonedOffsetIso = (self: DateTime.Zoned): string => offsetToString(z /** @internal */ export const toEpochMillis = (self: DateTime.DateTime): number => self.epochMilliseconds +/** @internal */ +export const toEpochSeconds = (self: DateTime.DateTime): number => Math.floor(self.epochMilliseconds / 1000) + +/** @internal */ +export const fromEpochSeconds = (seconds: number): DateTime.Utc => makeUtc(seconds * 1000) + /** @internal */ export const removeTime = (self: DateTime.DateTime): DateTime.Utc => withDate(self, (date) => { diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 36555333037..88d68b0db48 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -381,6 +381,32 @@ describe("DateTime", () => { }) }) + describe("toEpochSeconds", () => { + it("returns epoch seconds", () => { + const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + strictEqual(DateTime.toEpochSeconds(dt), 1704067200) + }) + + it("floors to nearest second", () => { + const dt = DateTime.makeUnsafe("2024-01-01T00:00:00.999Z") + strictEqual(DateTime.toEpochSeconds(dt), 1704067200) + }) + }) + + describe("fromEpochSeconds", () => { + it("creates DateTime from epoch seconds", () => { + const dt = DateTime.fromEpochSeconds(1704067200) + strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") + }) + + it("roundtrips with toEpochSeconds", () => { + const original = DateTime.makeUnsafe("2024-06-15T12:30:00Z") + const seconds = DateTime.toEpochSeconds(original) + const restored = DateTime.fromEpochSeconds(seconds) + strictEqual(DateTime.toEpochSeconds(restored), seconds) + }) + }) + describe("makeZonedFromString", () => { it.effect("parses an instant with an offset and IANA zone", () => Effect.gen(function*() {