From 162d3174ef63d6888a0616d8408dcc4219336ad0 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 20:48:35 +1200 Subject: [PATCH] use temporal format for zoned strings --- packages/effect/src/DateTime.ts | 22 ++++++++------- packages/effect/test/DateTime.test.ts | 27 ++++++++++++++++++- packages/schema/src/Schema.ts | 2 +- .../test/Schema/DateTime/DateTime.test.ts | 4 +-- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 2b687a8c7fa..f417d7e7dc0 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -506,6 +506,8 @@ export const makeZoned: ( export const make: (input: A) => Option.Option> = Option .liftThrowable(unsafeMake) +const zonedStringRegex = /^(.{24,35})\[(.+)\]$/ + /** * Create a `DateTime.Zoned` from a string. * @@ -515,14 +517,13 @@ export const make: (input: A) => Option.Option => { - const parts = input.split(" ") - if (parts.length !== 2) { - return Option.none() + const match = zonedStringRegex.exec(input) + if (match === null) { + const offset = parseOffset(input) + return offset ? makeZoned(input, { timeZone: offset }) : Option.none() } - return Option.flatMap( - make(parts[0]), - (dt) => Option.map(zoneFromString(parts[1]), (zone) => setZone(dt, zone)) - ) + const [, isoString, timeZone] = match + return makeZoned(isoString, { timeZone }) } /** @@ -1003,12 +1004,13 @@ export const zonedOffsetIso = (self: Zoned): string => offsetToString(zonedOffse /** * Format a `DateTime.Zoned` as a string. * - * It uses the format: `YYYY-MM-DDTHH:mm:ss.sssZ IANA/TimeZone`. + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. * * @since 3.6.0 * @category conversions */ -export const toStringZoned = (self: Zoned): string => `${formatIso(self)} ${zoneToString(self.zone)}` +export const zonedToString = (self: Zoned): string => + self.zone._tag === "Offset" ? formatIsoOffset(self) : `${formatIsoOffset(self)}[${self.zone.id}]` /** * Get the milliseconds since the Unix epoch of a `DateTime`. @@ -1910,5 +1912,5 @@ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString */ export const formatIsoOffset = (self: DateTime): string => { const date = toDateAdjusted(self) - return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, 19)}${zonedOffsetIso(self)}` + return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, -1)}${zonedOffsetIso(self)}` } diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index cb01193085c..40488a7cfb1 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -1,4 +1,4 @@ -import { DateTime, Duration, Effect, Either, TestClock } from "effect" +import { DateTime, Duration, Effect, Either, Option, TestClock } from "effect" import { assert, describe, it } from "./utils/extend.js" const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) @@ -318,4 +318,29 @@ describe("DateTime", () => { assert.strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") }) }) + + describe("makeZonedFromString", () => { + it.effect("parses time + zone", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]") + assert.strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + + it.effect("only offset", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00") + assert.strictEqual(dt.zone._tag, "Offset") + assert.strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + + it.effect("roundtrip", () => + Effect.gen(function*() { + const dt = yield* DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]").pipe( + Option.map(DateTime.zonedToString), + Option.flatMap(DateTime.makeZonedFromString) + ) + assert.deepStrictEqual(dt.zone, DateTime.zoneUnsafeMakeNamed("Pacific/Auckland")) + assert.strictEqual(dt.toJSON(), "2024-07-21T08:12:34.112Z") + })) + }) }) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index b924f507fa3..c028a34aa6c 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6081,7 +6081,7 @@ export class DateTimeZoned extends transformOrFail( onNone: () => ParseResult.fail(new ParseResult.Type(ast, s)), onSome: ParseResult.succeed }), - encode: (dt) => ParseResult.succeed(dateTime.toStringZoned(dt)) + encode: (dt) => ParseResult.succeed(dateTime.zonedToString(dt)) } ).annotations({ identifier: "DateTime.Zoned" }) { static override annotations: ( diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index 81bf715d606..07194588a18 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -39,7 +39,7 @@ describe("DateTime.Zoned", () => { }) it("decoding", async () => { - await Util.expectDecodeUnknownSuccess(schema, "1970-01-01T00:00:00.000Z Europe/London", dt) + await Util.expectDecodeUnknownSuccess(schema, "1970-01-01T01:00:00.000+01:00[Europe/London]", dt) await Util.expectDecodeUnknownFailure( schema, "1970-01-01T00:00:00.000Z", @@ -57,6 +57,6 @@ describe("DateTime.Zoned", () => { }) it("encoding", async () => { - await Util.expectEncodeSuccess(schema, dt, "1970-01-01T00:00:00.000Z Europe/London") + await Util.expectEncodeSuccess(schema, dt, "1970-01-01T01:00:00.000+01:00[Europe/London]") }) })