From dd2603c959206dc2291118b47ab3651a13ff8be7 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 18 Jul 2024 17:17:45 +1200 Subject: [PATCH 01/61] draft: DateTime module --- packages/effect/src/DateTime.ts | 295 ++++++++++++++++++++++++++++++++ packages/effect/src/index.ts | 5 + 2 files changed, 300 insertions(+) create mode 100644 packages/effect/src/DateTime.ts diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts new file mode 100644 index 0000000000..77b028f46c --- /dev/null +++ b/packages/effect/src/DateTime.ts @@ -0,0 +1,295 @@ +/** + * @since 3.6.0 + */ +import { IllegalArgumentException } from "effect/Cause" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Option from "effect/Option" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Predicate from "effect/Predicate" + +/** + * @since 3.6.0 + * @category type ids + */ +export const TypeId: unique symbol = Symbol.for("effect/DateTime") + +/** + * @since 3.6.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.6.0 + * @category models + */ +export type DateTime = DateTime.Utc | DateTime.WithZone + +/** + * @since 3.6.0 + * @category models + */ +export declare namespace DateTime { + /** + * @since 3.6.0 + * @category models + */ + export type Input = DateTime | Date | number + + /** + * @since 3.6.0 + * @category models + */ + export interface Proto extends Pipeable { + readonly [TypeId]: TypeId + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Utc extends Proto { + readonly _tag: "Utc" + readonly date: Date + } + + /** + * @since 3.6.0 + * @category models + */ + export interface WithZone extends Proto { + readonly _tag: "WithZone" + readonly utc: Utc + readonly zone: TimeZone + } +} + +/** + * @since 3.6.0 + * @category time zones + */ +export type TimeZone = TimeZone.Offset | TimeZone.Named + +/** + * @since 3.6.0 + * @category time zones + */ +export declare namespace TimeZone { + /** + * @since 3.6.0 + * @category time zones + */ + export interface Offset { + readonly _tag: "Offset" + readonly offset: number + } + + /** + * @since 3.6.0 + * @category time zones + */ + export interface Named { + readonly _tag: "Named" + readonly id: string + } +} + +/** + * @since 3.6.0 + * @category time zones + */ +export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZone")< + CurrentTimeZone, + TimeZone +>() {} + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + } +} +const ProtoUtc = { + ...Proto, + _tag: "Utc" +} +const ProtoWithZone = { + ...Proto, + _tag: "WithZone" +} + +/** + * @since 3.6.0 + * @category guards + */ +export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u, TypeId) + +/** + * @since 3.6.0 + * @category guards + */ +export const isUtc = (self: DateTime): self is DateTime.Utc => self._tag === "Utc" + +/** + * @since 3.6.0 + * @category guards + */ +export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._tag === "WithZone" + +/** + * @since 3.6.0 + * @category constructors + */ +export const fromEpochMillis = (epochMillis: number): DateTime => { + const self = Object.create(ProtoUtc) + self.date = new Date(epochMillis) + return self +} + +/** + * @since 3.6.0 + * @category constructors + */ +export const unsafeFromDate = (date: Date): DateTime => { + if (isNaN(date.getTime())) { + throw new IllegalArgumentException("Invalid date") + } + const self = Object.create(ProtoUtc) + self.date = date + return self +} + +/** + * @since 3.6.0 + * @category constructors + */ +export const fromInput = (input: DateTime.Input): DateTime => { + if (isDateTime(input)) { + return input + } else if (typeof input === "number") { + return fromEpochMillis(input) + } + return unsafeFromDate(input) +} + +/** + * @since 3.6.0 + * @category constructors + */ +export const fromDate: (date: Date) => Option.Option = Option.liftThrowable(unsafeFromDate) + +/** + * @since 3.6.0 + * @category constructors + */ +export const fromString = (input: string): Option.Option => fromDate(new Date(input)) + +/** + * @since 3.6.0 + * @category constructors + */ +export const now: Effect.Effect = Effect.map( + Effect.clock, + (clock) => fromEpochMillis(clock.unsafeCurrentTimeMillis()) +) + +/** + * @since 3.6.0 + * @category time zones + */ +export const setZone: { + (zone: TimeZone): (self: DateTime) => DateTime + (self: DateTime, zone: TimeZone): DateTime +} = dual(2, (self: DateTime, zone: TimeZone) => { + const selfWithZone = Object.create(ProtoWithZone) + selfWithZone.utc = self._tag === "Utc" ? self : self.utc + selfWithZone.zone = zone + return selfWithZone +}) + +/** + * @since 3.6.0 + * @category time zones + */ +export const setZoneOffset: { + (offset: number): (self: DateTime) => DateTime + (self: DateTime, offset: number): DateTime +} = dual(2, (self: DateTime, offset: number) => setZone(self, { _tag: "Offset", offset })) + +/** + * @since 3.6.0 + * @category time zones + */ +export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { + const format = new Intl.DateTimeFormat("en-US", { timeZone: zoneId }) + return { _tag: "Named", id: format.resolvedOptions().timeZone } +} + +/** + * @since 3.6.0 + * @category time zones + */ +export const makeZoneNamed: (zoneId: string) => Option.Option = Option.liftThrowable( + unsafeMakeZoneNamed +) + +/** + * @since 3.6.0 + * @category time zones + */ +export const setZoneNamed: { + (zoneId: string): (self: DateTime) => Option.Option + (self: DateTime, zoneId: string): Option.Option +} = dual(2, (self: DateTime, zoneId: string) => Option.map(makeZoneNamed(zoneId), (zone) => setZone(self, zone))) + +/** + * @since 3.6.0 + * @category time zones + */ +export const unsafeSetZoneNamed: { + (zoneId: string): (self: DateTime) => DateTime + (self: DateTime, zoneId: string): DateTime +} = dual(2, (self: DateTime, zoneId: string) => setZone(self, unsafeMakeZoneNamed(zoneId))) + +/** + * @since 3.6.0 + * @category conversions + */ +export const toEpochMillis = (self: DateTime): number => + self._tag === "WithZone" ? self.utc.date.getTime() : self.date.getTime() + +/** + * @since 3.6.0 + * @category pattern matching + */ +export const match: { + (options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onWithZone: (_: DateTime.WithZone) => B + }): (self: DateTime) => A | B + (self: DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onWithZone: (_: DateTime.WithZone) => B + }): A | B +} = dual(2, (self: DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onWithZone: (_: DateTime.WithZone) => B +}): A | B => self._tag === "Utc" ? options.onUtc(self) : options.onWithZone(self)) + +/** + * @since 3.6.0 + * @category pattern matching + */ +export const matchUtc: { + (f: (_: DateTime.Utc) => A): (self: DateTime) => A + (self: DateTime, f: (_: DateTime.Utc) => A): A +} = dual(2, (self: DateTime, f: (_: DateTime.Utc) => A) => f(self._tag === "WithZone" ? self.utc : self)) + +/** + * @since 3.6.0 + * @category time zones + */ +export const withCurrentZone = (self: DateTime): Effect.Effect => + Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) diff --git a/packages/effect/src/index.ts b/packages/effect/src/index.ts index eb5ce51c2d..121a8db705 100644 --- a/packages/effect/src/index.ts +++ b/packages/effect/src/index.ts @@ -187,6 +187,11 @@ export * as Cron from "./Cron.js" */ export * as Data from "./Data.js" +/** + * @since 3.6.0 + */ +export * as DateTime from "./DateTime.js" + /** * @since 2.0.0 */ From ca3605081d0e7175ebc0bb3cadb2585fbc798008 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 18 Jul 2024 17:28:32 +1200 Subject: [PATCH 02/61] current zone apis --- packages/effect/src/DateTime.ts | 42 +++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 77b028f46c..888f23f19b 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -253,6 +253,13 @@ export const unsafeSetZoneNamed: { (self: DateTime, zoneId: string): DateTime } = dual(2, (self: DateTime, zoneId: string) => setZone(self, unsafeMakeZoneNamed(zoneId))) +/** + * @since 3.6.0 + * @category time zones + */ +export const setZoneCurrent = (self: DateTime): Effect.Effect => + Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) + /** * @since 3.6.0 * @category conversions @@ -291,5 +298,36 @@ export const matchUtc: { * @since 3.6.0 * @category time zones */ -export const withCurrentZone = (self: DateTime): Effect.Effect => - Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) +export const withZone: { + (zone: TimeZone): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, zone: TimeZone): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, zone: TimeZone): Effect.Effect => + Effect.provideService(effect, CurrentTimeZone, zone) +) + +/** + * @since 3.6.0 + * @category time zones + */ +export const withZoneNamed: { + (zone: string): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, zone: string): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, zone: string): Effect.Effect => + Effect.flatMap( + Effect.try({ + try: () => unsafeMakeZoneNamed(zone), + catch: (e) => e as IllegalArgumentException + }), + (zone) => withZone(effect, zone) + ) +) + +/** + * @since 3.6.0 + * @category constructors + */ +export const nowInCurrentZone: Effect.Effect = Effect.flatMap(now, setZoneCurrent) From 6058b029720ec1dc035084e9aceec3869fa96754 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 15:43:23 +1200 Subject: [PATCH 03/61] add maths --- .changeset/clean-trainers-tap.md | 5 + packages/effect/src/DateTime.ts | 620 +++++++++++++++++++++++--- packages/effect/test/DateTime.test.ts | 61 +++ 3 files changed, 615 insertions(+), 71 deletions(-) create mode 100644 .changeset/clean-trainers-tap.md create mode 100644 packages/effect/test/DateTime.test.ts diff --git a/.changeset/clean-trainers-tap.md b/.changeset/clean-trainers-tap.md new file mode 100644 index 0000000000..dc6f103b68 --- /dev/null +++ b/.changeset/clean-trainers-tap.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add DateTime module diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 888f23f19b..b27169ffef 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1,13 +1,20 @@ /** * @since 3.6.0 */ -import { IllegalArgumentException } from "effect/Cause" -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import { dual } from "effect/Function" -import * as Option from "effect/Option" -import { type Pipeable, pipeArguments } from "effect/Pipeable" -import * as Predicate from "effect/Predicate" +import { Duration } from "effect" +import { IllegalArgumentException } from "./Cause.js" +import * as Context from "./Context.js" +import * as Effect from "./Effect.js" +import * as Either from "./Either.js" +import * as Equal from "./Equal.js" +import type { LazyArg } from "./Function.js" +import { dual, pipe } from "./Function.js" +import { globalValue } from "./GlobalValue.js" +import * as Hash from "./Hash.js" +import * as Inspectable from "./Inspectable.js" +import * as Option from "./Option.js" +import { type Pipeable, pipeArguments } from "./Pipeable.js" +import * as Predicate from "./Predicate.js" /** * @since 3.6.0 @@ -22,6 +29,9 @@ export const TypeId: unique symbol = Symbol.for("effect/DateTime") export type TypeId = typeof TypeId /** + * A `DateTime` represents a point in time. It can optionally have a time zone + * associated with it. + * * @since 3.6.0 * @category models */ @@ -42,7 +52,35 @@ export declare namespace DateTime { * @since 3.6.0 * @category models */ - export interface Proto extends Pipeable { + export type PreserveZone = A extends DateTime.WithZone ? DateTime.WithZone : DateTime.Utc + + /** + * @since 3.6.0 + * @category models + */ + export type Unit = + | "milli" + | "millis" + | "second" + | "seconds" + | "minute" + | "minutes" + | "hour" + | "hours" + | "day" + | "days" + | "week" + | "weeks" + | "month" + | "months" + | "year" + | "years" + + /** + * @since 3.6.0 + * @category models + */ + export interface Proto extends Pipeable, Inspectable.Inspectable { readonly [TypeId]: TypeId } @@ -52,7 +90,7 @@ export declare namespace DateTime { */ export interface Utc extends Proto { readonly _tag: "Utc" - readonly date: Date + readonly epochMillis: number } /** @@ -63,36 +101,60 @@ export declare namespace DateTime { readonly _tag: "WithZone" readonly utc: Utc readonly zone: TimeZone + /** @internal */ + plainDateCache?: number } } /** * @since 3.6.0 - * @category time zones + * @category type ids + */ +export const TimeZoneTypeId: unique symbol = Symbol.for("effect/DateTime/TimeZone") + +/** + * @since 3.6.0 + * @category type ids + */ +export type TimeZoneTypeId = typeof TimeZoneTypeId + +/** + * @since 3.6.0 + * @category models */ export type TimeZone = TimeZone.Offset | TimeZone.Named /** * @since 3.6.0 - * @category time zones + * @category models */ export declare namespace TimeZone { /** * @since 3.6.0 - * @category time zones + * @category models */ - export interface Offset { + export interface Proto extends Inspectable.Inspectable { + readonly [TimeZoneTypeId]: TimeZoneTypeId + } + + /** + * @since 3.6.0 + * @category models + */ + export interface Offset extends Proto { readonly _tag: "Offset" readonly offset: number } /** * @since 3.6.0 - * @category time zones + * @category models */ - export interface Named { + export interface Named extends Proto { readonly _tag: "Named" readonly id: string + /** @internal */ + readonly format: Intl.DateTimeFormat } } @@ -109,15 +171,87 @@ const Proto = { [TypeId]: TypeId, pipe() { return pipeArguments(this, arguments) - } + }, + ...Inspectable.BaseProto } const ProtoUtc = { ...Proto, - _tag: "Utc" + _tag: "Utc", + [Hash.symbol](this: DateTime.Utc) { + return Hash.cached(this, Hash.number(this.epochMillis)) + }, + [Equal.symbol](this: DateTime.Utc, that: unknown) { + return isDateTime(that) && that._tag === "Utc" && this.epochMillis === that.epochMillis + }, + toJSON(this: DateTime.Utc) { + return { + _op: "DateTime", + _tag: this._tag, + epochMillis: this.epochMillis + } + } } const ProtoWithZone = { ...Proto, - _tag: "WithZone" + _tag: "WithZone", + [Hash.symbol](this: DateTime.WithZone) { + return pipe( + Hash.hash(this.utc), + Hash.combine(Hash.hash(this.zone)), + Hash.cached(this) + ) + }, + [Equal.symbol](this: DateTime.WithZone, that: unknown) { + return isDateTime(that) && that._tag === "WithZone" && Equal.equals(this.utc, that.utc) && + Equal.equals(this.zone, that.zone) + }, + toJSON(this: DateTime.WithZone) { + return { + _id: "DateTime", + _tag: this._tag, + utc: this.utc.toJSON(), + zone: this.zone + } + } +} + +const ProtoTimeZone = { + [TimeZoneTypeId]: TimeZoneTypeId, + ...Inspectable.BaseProto +} + +const ProtoTimeZoneNamed = { + ...ProtoTimeZone, + [Hash.symbol](this: TimeZone.Named) { + return Hash.cached(this, Hash.string(`Named:${this.id}`)) + }, + [Equal.symbol](this: TimeZone.Named, that: unknown) { + return isTimeZone(that) && that._tag === "Named" && this.id === that.id + }, + toJSON(this: TimeZone.Named) { + return { + _id: "TimeZone", + _tag: "Named", + id: this.id + } + } +} + +const ProtoTimeZoneOffset = { + ...ProtoTimeZone, + [Hash.symbol](this: TimeZone.Offset) { + return Hash.cached(this, Hash.string(`Offset:${this.offset}`)) + }, + [Equal.symbol](this: TimeZone.Offset, that: unknown) { + return isTimeZone(that) && that._tag === "Offset" && this.offset === that.offset + }, + toJSON(this: TimeZone.Offset) { + return { + _id: "TimeZone", + _tag: "Offset", + offset: this.offset + } + } } /** @@ -126,6 +260,12 @@ const ProtoWithZone = { */ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u, TypeId) +/** + * @since 3.6.0 + * @category guards + */ +export const isTimeZone = (u: unknown): u is TimeZone => Predicate.hasProperty(u, TimeZoneTypeId) + /** * @since 3.6.0 * @category guards @@ -139,95 +279,169 @@ export const isUtc = (self: DateTime): self is DateTime.Utc => self._tag === "Ut export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._tag === "WithZone" /** + * Create a `DateTime` from the number of milliseconds since the Unix epoch. + * * @since 3.6.0 * @category constructors */ -export const fromEpochMillis = (epochMillis: number): DateTime => { +export const fromEpochMillis = (epochMillis: number): DateTime.Utc => { const self = Object.create(ProtoUtc) - self.date = new Date(epochMillis) + self.epochMillis = epochMillis return self } /** + * Create a `DateTime` from a `Date`. + * + * If the `Date` is invalid, an `IllegalArgumentException` will be thrown. + * * @since 3.6.0 * @category constructors */ -export const unsafeFromDate = (date: Date): DateTime => { - if (isNaN(date.getTime())) { +export const unsafeFromDate = (date: Date): DateTime.Utc => { + const epochMillis = date.getTime() + if (isNaN(epochMillis)) { throw new IllegalArgumentException("Invalid date") } const self = Object.create(ProtoUtc) - self.date = date + self.epochMillis = epochMillis return self } /** + * Create a `DateTime` from one of the following: + * - A `DateTime` + * - A `Date` instance (invalid dates will throw an `IllegalArgumentException`) + * - The `number` of milliseconds since the Unix epoch + * * @since 3.6.0 * @category constructors */ -export const fromInput = (input: DateTime.Input): DateTime => { +export const fromInput = (input: A): DateTime.PreserveZone => { if (isDateTime(input)) { - return input + return input as DateTime.PreserveZone } else if (typeof input === "number") { - return fromEpochMillis(input) + return fromEpochMillis(input) as DateTime.PreserveZone } - return unsafeFromDate(input) + return unsafeFromDate(input) as DateTime.PreserveZone } /** + * Safely create a `DateTime` from a `Date`, returning `None` if the `Date` is + * invalid. + * * @since 3.6.0 * @category constructors */ -export const fromDate: (date: Date) => Option.Option = Option.liftThrowable(unsafeFromDate) +export const fromDate: (date: Date) => Option.Option = Option.liftThrowable(unsafeFromDate) /** + * Parse a string into a `DateTime`, using `Date.parse`. + * * @since 3.6.0 * @category constructors */ -export const fromString = (input: string): Option.Option => fromDate(new Date(input)) +export const fromString = (input: string): Option.Option => fromDate(new Date(input)) /** + * Get the current time using the `Clock` service and convert it to a + * `DateTime`. + * * @since 3.6.0 * @category constructors */ -export const now: Effect.Effect = Effect.map( +export const now: Effect.Effect = Effect.map( Effect.clock, (clock) => fromEpochMillis(clock.unsafeCurrentTimeMillis()) ) /** + * Get the current time using `Date.now`. + * + * @since 3.6.0 + * @category constructors + */ +export const unsafeNow: LazyArg = () => fromEpochMillis(Date.now()) + +/** + * Set the time zone of a `DateTime`, returning a new `DateTime.WithZone`. + * * @since 3.6.0 * @category time zones */ export const setZone: { - (zone: TimeZone): (self: DateTime) => DateTime - (self: DateTime, zone: TimeZone): DateTime -} = dual(2, (self: DateTime, zone: TimeZone) => { + (zone: TimeZone): (self: DateTime.Input) => DateTime.WithZone + (self: DateTime.Input, zone: TimeZone): DateTime.WithZone +} = dual(2, (self: DateTime.Input, zone: TimeZone): DateTime.WithZone => { + const dt = fromInput(self) const selfWithZone = Object.create(ProtoWithZone) - selfWithZone.utc = self._tag === "Utc" ? self : self.utc + selfWithZone.utc = dt._tag === "Utc" ? dt : dt.utc selfWithZone.zone = zone return selfWithZone }) /** + * Add a fixed offset time zone to a `DateTime`. + * + * The offset is in milliseconds. + * * @since 3.6.0 * @category time zones */ export const setZoneOffset: { - (offset: number): (self: DateTime) => DateTime - (self: DateTime, offset: number): DateTime -} = dual(2, (self: DateTime, offset: number) => setZone(self, { _tag: "Offset", offset })) + (offset: number): (self: DateTime.Input) => DateTime.WithZone + (self: DateTime.Input, offset: number): DateTime.WithZone +} = dual(2, (self: DateTime.Input, offset: number): DateTime.WithZone => { + const zone = Object.create(ProtoTimeZoneOffset) + zone.offset = offset + return setZone(self, zone) +}) + +const validZoneCache = globalValue("effect/DateTime/validZoneCache", () => new Map()) + +const formatOptions: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "longOffset", + fractionalSecondDigits: 3, + hourCycle: "h23" +} /** + * Attempt to create a named time zone from a IANA time zone identifier. + * + * If the time zone is invalid, an `IllegalArgumentException` will be thrown. + * * @since 3.6.0 * @category time zones */ export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { - const format = new Intl.DateTimeFormat("en-US", { timeZone: zoneId }) - return { _tag: "Named", id: format.resolvedOptions().timeZone } + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + try { + const format = new Intl.DateTimeFormat("en-US", { + ...formatOptions, + timeZone: zoneId + }) + const zone = Object.create(ProtoTimeZoneNamed) + zone.id = zoneId + zone.format = format + validZoneCache.set(zoneId, zone) + return zone + } catch (_) { + throw new IllegalArgumentException(`Invalid time zone: ${zoneId}`) + } } /** + * Create a named time zone from a IANA time zone identifier. If the time zone + * is invalid, `None` will be returned. + * * @since 3.6.0 * @category time zones */ @@ -236,36 +450,169 @@ export const makeZoneNamed: (zoneId: string) => Option.Option = ) /** + * Set the time zone of a `DateTime` from an IANA time zone identifier. If the + * time zone is invalid, `None` will be returned. + * * @since 3.6.0 * @category time zones */ export const setZoneNamed: { - (zoneId: string): (self: DateTime) => Option.Option - (self: DateTime, zoneId: string): Option.Option -} = dual(2, (self: DateTime, zoneId: string) => Option.map(makeZoneNamed(zoneId), (zone) => setZone(self, zone))) + (zoneId: string): (self: DateTime.Input) => Option.Option + (self: DateTime.Input, zoneId: string): Option.Option +} = dual( + 2, + (self: DateTime.Input, zoneId: string): Option.Option => + Option.map(makeZoneNamed(zoneId), (zone) => setZone(self, zone)) +) /** + * Set the time zone of a `DateTime` from an IANA time zone identifier. If the + * time zone is invalid, an `IllegalArgumentException` will be thrown. + * * @since 3.6.0 * @category time zones */ export const unsafeSetZoneNamed: { - (zoneId: string): (self: DateTime) => DateTime - (self: DateTime, zoneId: string): DateTime -} = dual(2, (self: DateTime, zoneId: string) => setZone(self, unsafeMakeZoneNamed(zoneId))) + (zoneId: string): (self: DateTime.Input) => DateTime.WithZone + (self: DateTime.Input, zoneId: string): DateTime.WithZone +} = dual(2, (self: DateTime.Input, zoneId: string): DateTime.WithZone => setZone(self, unsafeMakeZoneNamed(zoneId))) /** + * Set the time zone of a `DateTime` to the current time zone, which is + * determined by the `CurrentTimeZone` service. + * * @since 3.6.0 * @category time zones */ -export const setZoneCurrent = (self: DateTime): Effect.Effect => +export const setZoneCurrent = (self: DateTime.Input): Effect.Effect => Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) /** + * Get the milliseconds since the Unix epoch of a `DateTime`. + * + * @since 3.6.0 + * @category conversions + */ +export const toEpochMillis = (self: DateTime.Input): number => { + const dt = fromInput(self) + return dt._tag === "WithZone" ? dt.utc.epochMillis : dt.epochMillis +} + +/** + * Get the UTC `Date` of a `DateTime`. + * * @since 3.6.0 * @category conversions */ -export const toEpochMillis = (self: DateTime): number => - self._tag === "WithZone" ? self.utc.date.getTime() : self.date.getTime() +export const toUtcDate = (self: DateTime.Input): Date => new Date(toEpochMillis(self)) + +/** + * Convert a `DateTime` to a `Date`, applying the time zone first if necessary. + * + * The returned Date will be offset by the time zone if the `DateTime` is a + * `DateTime.WithZone`. + * + * @since 3.6.0 + * @category conversions + */ +export const toPlainDate = (self: DateTime.Input): Date => { + const dt = fromInput(self) + if (dt._tag === "Utc") { + return new Date(dt.epochMillis) + } else if (dt.zone._tag === "Offset") { + return new Date(dt.utc.epochMillis + dt.zone.offset) + } else if (dt.plainDateCache !== undefined) { + return new Date(dt.plainDateCache) + } + const parts = dt.zone.format.formatToParts(dt.utc.epochMillis) + const date = new Date(0) + date.setUTCFullYear( + Number(parts[4].value), + Number(parts[0].value) - 1, + Number(parts[2].value) + ) + date.setUTCHours( + Number(parts[6].value), + Number(parts[8].value), + Number(parts[10].value), + Number(parts[12].value) + ) + dt.plainDateCache = date.getTime() + return date +} + +/** + * Calculate the time zone offset of a `DateTime` in milliseconds. + * + * @since 3.6.0 + * @category conversions + */ +export const zoneOffset = (self: DateTime.Input): number => { + const dt = fromInput(self) + if (dt._tag === "Utc") { + return 0 + } + const plainDate = toPlainDate(dt) + return plainDate.getTime() - toEpochMillis(dt) +} + +const gmtOffsetRegex = /GMT([+-])(\d{2}):(\d{2})/ +const calcutateOffset = (date: Date, zone: TimeZone.Named): number => { + const parts = zone.format.formatToParts(date) + const offset = parts[14].value + if (offset === "GMT") { + return 0 + } + const match = gmtOffsetRegex.exec(offset) + if (match === null) { + return zoneOffset(setZone(date, zone)) + } + const [, sign, hours, minutes] = match + return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 +} + +/** + * Modify a `DateTime` by applying a function to the underlying plain `Date`. + * + * The `Date` will first have the time zone applied if necessary, and then be + * converted back to a `DateTime` with the same time zone. + * + * @since 3.6.0 + * @category mapping + */ +export const mutate: { + (f: (plainDate: Date) => void): (self: A) => DateTime.PreserveZone + (self: A, f: (plainDate: Date) => void): DateTime.PreserveZone +} = dual(2, (self: DateTime.Input, f: (plainDate: Date) => void): DateTime => { + const dt = fromInput(self) + const plainDate = toPlainDate(dt) + const newPlainDate = new Date(plainDate.getTime()) + f(newPlainDate) + if (dt._tag === "Utc") { + return fromEpochMillis(newPlainDate.getTime()) + } else if (dt.zone._tag === "Offset") { + return setZone(fromEpochMillis(newPlainDate.getTime() - dt.zone.offset), dt.zone) + } + const offset = calcutateOffset(newPlainDate, dt.zone) + return setZone(fromEpochMillis(newPlainDate.getTime() - offset), dt.zone) +}) + +/** + * Transform a `DateTime` by applying a function to the number of milliseconds + * since the Unix epoch. + * + * @since 3.6.0 + * @category mapping + */ +export const mapEpochMillis: { + (f: (millis: number) => number): (self: DateTime.Input) => DateTime + (self: DateTime.Input, f: (millis: number) => number): DateTime +} = dual(2, (self: DateTime.Input, f: (millis: number) => number): DateTime => { + const dt = fromInput(self) + const prevEpochMillis = toEpochMillis(dt) + const newUtc = fromEpochMillis(f(prevEpochMillis)) + return dt._tag === "Utc" ? newUtc : setZone(newUtc, dt.zone) +}) /** * @since 3.6.0 @@ -275,59 +622,190 @@ export const match: { (options: { readonly onUtc: (_: DateTime.Utc) => A readonly onWithZone: (_: DateTime.WithZone) => B - }): (self: DateTime) => A | B - (self: DateTime, options: { + }): (self: DateTime.Input) => A | B + (self: DateTime.Input, options: { readonly onUtc: (_: DateTime.Utc) => A readonly onWithZone: (_: DateTime.WithZone) => B }): A | B -} = dual(2, (self: DateTime, options: { +} = dual(2, (self: DateTime.Input, options: { readonly onUtc: (_: DateTime.Utc) => A readonly onWithZone: (_: DateTime.WithZone) => B -}): A | B => self._tag === "Utc" ? options.onUtc(self) : options.onWithZone(self)) - -/** - * @since 3.6.0 - * @category pattern matching - */ -export const matchUtc: { - (f: (_: DateTime.Utc) => A): (self: DateTime) => A - (self: DateTime, f: (_: DateTime.Utc) => A): A -} = dual(2, (self: DateTime, f: (_: DateTime.Utc) => A) => f(self._tag === "WithZone" ? self.utc : self)) +}): A | B => { + const dt = fromInput(self) + return dt._tag === "Utc" ? options.onUtc(dt) : options.onWithZone(dt) +}) /** + * Provide the `CurrentTimeZone` to an effect. + * * @since 3.6.0 * @category time zones */ -export const withZone: { - (zone: TimeZone): (effect: Effect.Effect) => Effect.Effect - (effect: Effect.Effect, zone: TimeZone): Effect.Effect +export const withCurrentZone: { + (zone: TimeZone): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, zone: TimeZone): Effect.Effect> } = dual( 2, - (effect: Effect.Effect, zone: TimeZone): Effect.Effect => + (effect: Effect.Effect, zone: TimeZone): Effect.Effect> => Effect.provideService(effect, CurrentTimeZone, zone) ) /** + * Provide the `CurrentTimeZone` to an effect using an IANA time zone + * identifier. + * + * If the time zone is invalid, it will fail with an `IllegalArgumentException`. + * * @since 3.6.0 * @category time zones */ -export const withZoneNamed: { - (zone: string): (effect: Effect.Effect) => Effect.Effect - (effect: Effect.Effect, zone: string): Effect.Effect +export const withCurrentZoneNamed: { + (zone: string): ( + effect: Effect.Effect + ) => Effect.Effect> + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> } = dual( 2, - (effect: Effect.Effect, zone: string): Effect.Effect => + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> => Effect.flatMap( Effect.try({ try: () => unsafeMakeZoneNamed(zone), catch: (e) => e as IllegalArgumentException }), - (zone) => withZone(effect, zone) + (zone) => withCurrentZone(effect, zone) ) ) /** + * Get the current time as a `DateTime.WithZone`, using the `CurrentTimeZone`. + * * @since 3.6.0 * @category constructors */ -export const nowInCurrentZone: Effect.Effect = Effect.flatMap(now, setZoneCurrent) +export const nowInCurrentZone: Effect.Effect = Effect.flatMap( + now, + setZoneCurrent +) + +/** + * Calulate the difference between two `DateTime` values. + * + * If the `other` DateTime is before `self`, the result will be a negative + * `Duration`, returned as a `Left`. + * + * If the `other` DateTime is after `self`, the result will be a positive + * `Duration`, returned as a `Right`. + * + * @since 3.6.0 + * @category constructors + */ +export const diff: { + (other: DateTime.Input): (self: DateTime.Input) => Either.Either + (self: DateTime.Input, other: DateTime.Input): Either.Either +} = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { + const selfEpochMillis = toEpochMillis(self) + const otherEpochMillis = toEpochMillis(other) + const diffMillis = otherEpochMillis - selfEpochMillis + return diffMillis > 0 + ? Either.right(Duration.millis(diffMillis)) + : Either.left(Duration.millis(-diffMillis)) +}) + +/** + * Add the given `Duration` to a `DateTime`. + * + * @since 3.6.0 + * @category math + */ +export const addDuration: { + (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone + (self: A, duration: Duration.DurationInput): DateTime.PreserveZone +} = dual( + 2, + (self: DateTime.Input, duration: Duration.DurationInput): DateTime => + mapEpochMillis(self, (millis) => millis + Duration.toMillis(duration)) +) + +/** + * Subtract the given `Duration` from a `DateTime`. + * + * @since 3.6.0 + * @category math + */ +export const subtractDuration: { + (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone + (self: A, duration: Duration.DurationInput): DateTime.PreserveZone +} = dual( + 2, + (self: DateTime.Input, duration: Duration.DurationInput): DateTime => + mapEpochMillis(self, (millis) => millis - Duration.toMillis(duration)) +) + +const addMillis = (date: DateTime.Input, amount: number): DateTime => mapEpochMillis(date, (millis) => millis + amount) + +/** + * Add the given `amount` of `unit`'s to a `DateTime`. + * + * @since 3.6.0 + * @category math + */ +export const add: { + (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone + (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone +} = dual(3, (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime => { + switch (unit) { + case "millis": + case "milli": + return addMillis(self, amount) + case "seconds": + case "second": + return addMillis(self, amount * 1000) + case "minutes": + case "minute": + return addMillis(self, amount * 60 * 1000) + case "hours": + case "hour": + return addMillis(self, amount * 60 * 60 * 1000) + } + return mutate(self, (date) => { + switch (unit) { + case "days": + case "day": { + date.setUTCDate(date.getUTCDate() + amount) + return date + } + case "weeks": + case "week": { + date.setUTCDate(date.getUTCDate() + amount * 7) + return date + } + case "months": + case "month": { + date.setUTCMonth(date.getUTCMonth() + amount) + return date + } + case "years": + case "year": { + date.setUTCFullYear(date.getUTCFullYear() + amount) + return date + } + } + }) +}) + +/** + * Subtract the given `amount` of `unit`'s from a `DateTime`. + * + * @since 3.6.0 + * @category math + */ +export const subtract: { + (amount: number, unit: DateTime.Unit): (self: DateTime.Input) => DateTime + (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime +} = dual(3, (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime => add(self, -amount, unit)) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts new file mode 100644 index 0000000000..68849c182a --- /dev/null +++ b/packages/effect/test/DateTime.test.ts @@ -0,0 +1,61 @@ +import { DateTime, Duration, Effect, Either, TestClock } from "effect" +import { assert, describe, it } from "./utils/extend.js" + +describe("DateTime", () => { + describe("mutate", () => { + it.effect("should mutate the date", () => + Effect.gen(function*() { + const now = yield* DateTime.now + const tomorrow = DateTime.mutate(now, (date) => { + date.setUTCDate(date.getUTCDate() + 1) + }) + const diff = DateTime.diff(now, tomorrow) + assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) + })) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.mutate(now, (date) => { + date.setUTCMonth(date.getUTCMonth() + 6) + }) + assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toPlainDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + const plusOne = DateTime.mutate(future, (date) => { + date.setUTCDate(date.getUTCDate() + 1) + }) + assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + assert.strictEqual(DateTime.toPlainDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + })) + }) + + describe("add", () => { + it.effect("utc", () => + Effect.gen(function*() { + const now = yield* DateTime.now + const tomorrow = DateTime.add(now, 1, "day") + const diff = DateTime.diff(now, tomorrow) + assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) + })) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.add(now, 6, "months") + assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toPlainDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + const plusOne = DateTime.add(future, 1, "day") + assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + assert.strictEqual(DateTime.toPlainDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + const minusOne = DateTime.add(plusOne, -1, "day") + assert.strictEqual(DateTime.toUtcDate(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toPlainDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") + })) + }) +}) From 96374fec9b0cf8a8adedeca2fafe7302400422a7 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 16:00:40 +1200 Subject: [PATCH 04/61] wip --- packages/effect/src/DateTime.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index b27169ffef..8339a1712e 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -429,7 +429,7 @@ export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { timeZone: zoneId }) const zone = Object.create(ProtoTimeZoneNamed) - zone.id = zoneId + zone.id = format.resolvedOptions().timeZone zone.format = format validZoneCache.set(zoneId, zone) return zone @@ -556,8 +556,11 @@ export const zoneOffset = (self: DateTime.Input): number => { return plainDate.getTime() - toEpochMillis(dt) } -const gmtOffsetRegex = /GMT([+-])(\d{2}):(\d{2})/ -const calcutateOffset = (date: Date, zone: TimeZone.Named): number => { +const calcutateOffset = (date: Date, zone: TimeZone): number => + zone._tag === "Offset" ? zone.offset : calcutateNamedOffset(date, zone) + +const gmtOffsetRegex = /^GMT([+-])(\d{2}):(\d{2})$/ +const calcutateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const parts = zone.format.formatToParts(date) const offset = parts[14].value if (offset === "GMT") { @@ -565,6 +568,7 @@ const calcutateOffset = (date: Date, zone: TimeZone.Named): number => { } const match = gmtOffsetRegex.exec(offset) if (match === null) { + // fallback to using the plain date return zoneOffset(setZone(date, zone)) } const [, sign, hours, minutes] = match @@ -590,8 +594,6 @@ export const mutate: { f(newPlainDate) if (dt._tag === "Utc") { return fromEpochMillis(newPlainDate.getTime()) - } else if (dt.zone._tag === "Offset") { - return setZone(fromEpochMillis(newPlainDate.getTime() - dt.zone.offset), dt.zone) } const offset = calcutateOffset(newPlainDate, dt.zone) return setZone(fromEpochMillis(newPlainDate.getTime() - offset), dt.zone) From 651b832e9f78626eba2004642c680150305773a1 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 16:36:35 +1200 Subject: [PATCH 05/61] startOf / endOf --- packages/effect/src/DateTime.ts | 116 +++++++++++++++++++++++++- packages/effect/test/DateTime.test.ts | 68 +++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 8339a1712e..55b5799339 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -76,6 +76,16 @@ export declare namespace DateTime { | "year" | "years" + /** + * @since 3.6.0 + * @category models + */ + export type DatePart = + | "day" + | "week" + | "month" + | "year" + /** * @since 3.6.0 * @category models @@ -343,6 +353,16 @@ export const fromDate: (date: Date) => Option.Option = Option.lift */ export const fromString = (input: string): Option.Option => fromDate(new Date(input)) +/** + * Parse a string into a `DateTime`, using `Date.parse`. + * + * If the string is invalid, an `IllegalArgumentException` will be thrown. + * + * @since 3.6.0 + * @category constructors + */ +export const unsafeFromString = (input: string): DateTime.Utc => unsafeFromDate(new Date(input)) + /** * Get the current time using the `Clock` service and convert it to a * `DateTime`. @@ -808,6 +828,98 @@ export const add: { * @category math */ export const subtract: { - (amount: number, unit: DateTime.Unit): (self: DateTime.Input) => DateTime - (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime + (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone + (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone } = dual(3, (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime => add(self, -amount, unit)) + +/** + * Converts a `DateTime` to the start of the given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + */ +export const startOf: { + (part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => DateTime.PreserveZone + (self: A, part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): DateTime.PreserveZone +} = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime => + mutate(self, (date) => { + switch (part) { + case "day": { + date.setUTCHours(0, 0, 0, 0) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff) + date.setUTCHours(0, 0, 0, 0) + break + } + case "month": { + date.setUTCDate(1) + date.setUTCHours(0, 0, 0, 0) + break + } + case "year": { + date.setUTCMonth(0, 1) + date.setUTCHours(0, 0, 0, 0) + break + } + } + })) + +/** + * Converts a `DateTime` to the end of the given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + */ +export const endOf: { + (part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => DateTime.PreserveZone + (self: A, part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): DateTime.PreserveZone +} = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime => + mutate(self, (date) => { + switch (part) { + case "day": { + date.setUTCHours(23, 59, 59, 999) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff + 6) + date.setUTCHours(23, 59, 59, 999) + break + } + case "month": { + date.setUTCMonth(date.getUTCMonth() + 1, 0) + date.setUTCHours(23, 59, 59, 999) + break + } + case "year": { + date.setUTCMonth(11, 31) + date.setUTCHours(23, 59, 59, 999) + break + } + } + })) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 68849c182a..1d71b65c63 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -58,4 +58,72 @@ describe("DateTime", () => { assert.strictEqual(DateTime.toPlainDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) }) + + describe("endOf", () => { + it("month", () => { + const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(mar, "month") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-31T23:59:59.999Z") + }) + + it("feb leap year", () => { + const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") + const end = DateTime.endOf(feb, "month") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-02-29T23:59:59.999Z") + }) + + it("week", () => { + const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(start, "week") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-16T23:59:59.999Z") + }) + + it("week last day", () => { + const start = DateTime.unsafeFromString("2024-03-16T12:00:00.000Z") + const end = DateTime.endOf(start, "week") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-16T23:59:59.999Z") + }) + + it("week with options", () => { + const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.endOf(start, "week", { + weekStartsOn: 1 + }) + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-17T23:59:59.999Z") + }) + }) + + describe("startOf", () => { + it("month", () => { + const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(mar, "month") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-01T00:00:00.000Z") + }) + + it("feb leap year", () => { + const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") + const end = DateTime.startOf(feb, "month") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-02-01T00:00:00.000Z") + }) + + it("week", () => { + const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(start, "week") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-10T00:00:00.000Z") + }) + + it("week first day", () => { + const start = DateTime.unsafeFromString("2024-03-10T12:00:00.000Z") + const end = DateTime.startOf(start, "week") + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-10T00:00:00.000Z") + }) + + it("week with options", () => { + const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(start, "week", { + weekStartsOn: 1 + }) + assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-11T00:00:00.000Z") + }) + }) }) From 838fe1a9b5740bd6a21e5c50d737c3596e6c6a81 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 17:31:48 +1200 Subject: [PATCH 06/61] formatting --- packages/effect/src/DateTime.ts | 215 +++++++++++++++++++++++++- packages/effect/test/DateTime.test.ts | 61 +++++++- 2 files changed, 263 insertions(+), 13 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 55b5799339..972b3b7f15 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -86,6 +86,20 @@ export declare namespace DateTime { | "month" | "year" + /** + * @since 3.6.0 + * @category models + */ + export interface Parts { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly day: number + readonly month: number + readonly year: number + } + /** * @since 3.6.0 * @category models @@ -232,6 +246,7 @@ const ProtoTimeZone = { const ProtoTimeZoneNamed = { ...ProtoTimeZone, + _tag: "Named", [Hash.symbol](this: TimeZone.Named) { return Hash.cached(this, Hash.string(`Named:${this.id}`)) }, @@ -249,6 +264,7 @@ const ProtoTimeZoneNamed = { const ProtoTimeZoneOffset = { ...ProtoTimeZone, + _tag: "Offset", [Hash.symbol](this: TimeZone.Offset) { return Hash.cached(this, Hash.string(`Offset:${this.offset}`)) }, @@ -270,6 +286,8 @@ const ProtoTimeZoneOffset = { */ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u, TypeId) +const isDateTimeInput = (u: unknown): u is DateTime.Input => isDateTime(u) || u instanceof Date || typeof u === "number" + /** * @since 3.6.0 * @category guards @@ -535,7 +553,7 @@ export const toUtcDate = (self: DateTime.Input): Date => new Date(toEpochMillis( * @since 3.6.0 * @category conversions */ -export const toPlainDate = (self: DateTime.Input): Date => { +export const toAdjustedDate = (self: DateTime.Input): Date => { const dt = fromInput(self) if (dt._tag === "Utc") { return new Date(dt.epochMillis) @@ -572,7 +590,7 @@ export const zoneOffset = (self: DateTime.Input): number => { if (dt._tag === "Utc") { return 0 } - const plainDate = toPlainDate(dt) + const plainDate = toAdjustedDate(dt) return plainDate.getTime() - toEpochMillis(dt) } @@ -609,14 +627,14 @@ export const mutate: { (self: A, f: (plainDate: Date) => void): DateTime.PreserveZone } = dual(2, (self: DateTime.Input, f: (plainDate: Date) => void): DateTime => { const dt = fromInput(self) - const plainDate = toPlainDate(dt) - const newPlainDate = new Date(plainDate.getTime()) - f(newPlainDate) + const plainDate = toAdjustedDate(dt) + const newAdjustedDate = new Date(plainDate.getTime()) + f(newAdjustedDate) if (dt._tag === "Utc") { - return fromEpochMillis(newPlainDate.getTime()) + return fromEpochMillis(newAdjustedDate.getTime()) } - const offset = calcutateOffset(newPlainDate, dt.zone) - return setZone(fromEpochMillis(newPlainDate.getTime() - offset), dt.zone) + const offset = calcutateOffset(newAdjustedDate, dt.zone) + return setZone(fromEpochMillis(newAdjustedDate.getTime() - offset), dt.zone) }) /** @@ -636,6 +654,30 @@ export const mapEpochMillis: { return dt._tag === "Utc" ? newUtc : setZone(newUtc, dt.zone) }) +/** + * Using the time zone adjusted `Date`, apply a function to the `Date` and + * return the result. + * + * @since 3.6.0 + * @category mapping + */ +export const withAdjustedDate: { + (f: (date: Date) => A): (self: DateTime.Input) => A + (self: DateTime.Input, f: (date: Date) => A): A +} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toAdjustedDate(self))) + +/** + * Using the time zone adjusted `Date`, apply a function to the `Date` and + * return the result. + * + * @since 3.6.0 + * @category mapping + */ +export const withUtcDate: { + (f: (date: Date) => A): (self: DateTime.Input) => A + (self: DateTime.Input, f: (date: Date) => A): A +} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toUtcDate(self))) + /** * @since 3.6.0 * @category pattern matching @@ -923,3 +965,160 @@ export const endOf: { } } })) + +const dateToParts = (date: Date): DateTime.Parts => ({ + millis: date.getUTCMilliseconds(), + seconds: date.getUTCSeconds(), + minutes: date.getUTCMinutes(), + hours: date.getUTCHours(), + day: date.getUTCDate(), + month: date.getUTCMonth() + 1, + year: date.getUTCFullYear() +}) + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be time zone adjusted if necessary. + * + * @since 3.6.0 + * @category accessors + */ +export const toAdjustedParts = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be in UTC. + * + * @since 3.6.0 + * @category accessors + */ +export const toUtcParts = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * @since 3.6.0 + * @category formatting + */ +export const format: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime.Input) => string + ( + self: DateTime.Input, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = dual((args) => isDateTimeInput(args[0]), ( + self: DateTime.Input, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined +): string => new Intl.DateTimeFormat(options?.locale, options).format(toEpochMillis(self))) + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * This forces the time zone to be UTC. + * + * @since 3.6.0 + * @category formatting + */ +export const formatUtc: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime.Input) => string + ( + self: DateTime.Input, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = dual((args) => isDateTimeInput(args[0]), ( + self: DateTime.Input, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined +): string => + new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: "UTC" + }).format(toEpochMillis(self))) + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIntl: { + (format: Intl.DateTimeFormat): (self: DateTime.Input) => string + (self: DateTime.Input, format: Intl.DateTimeFormat): string +} = dual(2, (self: DateTime.Input, format: Intl.DateTimeFormat): string => format.format(toEpochMillis(self))) + +const timeZoneOffset = (self: TimeZone): string => { + if (self._tag === "Named") { + return self.id + } + const abs = Math.abs(self.offset) + const offsetHours = Math.floor(abs / (60 * 60 * 1000)) + const offsetMinutes = Math.round((abs % (60 * 60 * 1000)) / (60 * 1000)) + return `${self.offset < 0 ? "-" : "+"}${String(offsetHours).padStart(2, "0")}:${ + String(`${offsetMinutes}`).padStart(2, "0") + }` +} + +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API, using the + * embedded time zone. + * + * @since 3.6.0 + * @category formatting + */ +export const formatWithZone: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime.WithZone) => string + ( + self: DateTime.WithZone, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = dual((args) => isDateTime(args[0]), ( + self: DateTime.WithZone, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined +): string => + new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: timeZoneOffset(self.zone) + }).format(toEpochMillis(self))) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 1d71b65c63..1e84e90d05 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -23,12 +23,12 @@ describe("DateTime", () => { date.setUTCMonth(date.getUTCMonth() + 6) }) assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toPlainDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toAdjustedDate(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.mutate(future, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toPlainDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toAdjustedDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") })) }) @@ -49,13 +49,13 @@ describe("DateTime", () => { ) const future = DateTime.add(now, 6, "months") assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toPlainDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toAdjustedDate(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.add(future, 1, "day") assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toPlainDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toAdjustedDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") const minusOne = DateTime.add(plusOne, -1, "day") assert.strictEqual(DateTime.toUtcDate(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toPlainDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toAdjustedDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) }) @@ -126,4 +126,55 @@ describe("DateTime", () => { assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-11T00:00:00.000Z") }) }) + + describe("format", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.now + assert.strictEqual( + DateTime.format(now, { + dateStyle: "full", + timeStyle: "full", + timeZone: "UTC" + }), + "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" + ) + })) + }) + + describe("formatUtc", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.now + assert.strictEqual( + DateTime.formatUtc(now, { dateStyle: "full", timeStyle: "full" }), + "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" + ) + })) + }) + + describe("formatWithZone", () => { + it.effect("full", () => + Effect.gen(function*() { + const now = yield* DateTime.now.pipe( + Effect.flatMap(DateTime.setZoneNamed("Pacific/Auckland")) + ) + assert.strictEqual( + DateTime.formatWithZone(now, { dateStyle: "full", timeStyle: "full" }), + "Thursday, January 1, 1970 at 12:00:00 PM New Zealand Standard Time" + ) + })) + + // only works on node v22 + it.effect.skip("full with offset", () => + Effect.gen(function*() { + const now = yield* DateTime.now.pipe( + Effect.map(DateTime.setZoneOffset(10 * 60 * 60 * 1000)) + ) + assert.strictEqual( + DateTime.formatWithZone(now, { dateStyle: "full", timeStyle: "full" }), + "" + ) + })) + }) }) From 8bc462efd5bb33e3ba73fb9ef55a9fd2bc6f3b22 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 20:02:40 +1200 Subject: [PATCH 07/61] wip --- packages/effect/src/DateTime.ts | 79 ++++++++++++++++++--------- packages/effect/test/DateTime.test.ts | 62 ++++++++++----------- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 972b3b7f15..9b39d17385 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -126,7 +126,7 @@ export declare namespace DateTime { readonly utc: Utc readonly zone: TimeZone /** @internal */ - plainDateCache?: number + adjustedEpochMillis?: number } } @@ -542,7 +542,7 @@ export const toEpochMillis = (self: DateTime.Input): number => { * @since 3.6.0 * @category conversions */ -export const toUtcDate = (self: DateTime.Input): Date => new Date(toEpochMillis(self)) +export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis(self)) /** * Convert a `DateTime` to a `Date`, applying the time zone first if necessary. @@ -553,14 +553,14 @@ export const toUtcDate = (self: DateTime.Input): Date => new Date(toEpochMillis( * @since 3.6.0 * @category conversions */ -export const toAdjustedDate = (self: DateTime.Input): Date => { +export const toDateAdjusted = (self: DateTime.Input): Date => { const dt = fromInput(self) if (dt._tag === "Utc") { return new Date(dt.epochMillis) } else if (dt.zone._tag === "Offset") { return new Date(dt.utc.epochMillis + dt.zone.offset) - } else if (dt.plainDateCache !== undefined) { - return new Date(dt.plainDateCache) + } else if (dt.adjustedEpochMillis !== undefined) { + return new Date(dt.adjustedEpochMillis) } const parts = dt.zone.format.formatToParts(dt.utc.epochMillis) const date = new Date(0) @@ -575,7 +575,7 @@ export const toAdjustedDate = (self: DateTime.Input): Date => { Number(parts[10].value), Number(parts[12].value) ) - dt.plainDateCache = date.getTime() + dt.adjustedEpochMillis = date.getTime() return date } @@ -590,7 +590,7 @@ export const zoneOffset = (self: DateTime.Input): number => { if (dt._tag === "Utc") { return 0 } - const plainDate = toAdjustedDate(dt) + const plainDate = toDateAdjusted(dt) return plainDate.getTime() - toEpochMillis(dt) } @@ -627,8 +627,8 @@ export const mutate: { (self: A, f: (plainDate: Date) => void): DateTime.PreserveZone } = dual(2, (self: DateTime.Input, f: (plainDate: Date) => void): DateTime => { const dt = fromInput(self) - const plainDate = toAdjustedDate(dt) - const newAdjustedDate = new Date(plainDate.getTime()) + const adjustedDate = toDateAdjusted(dt) + const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) if (dt._tag === "Utc") { return fromEpochMillis(newAdjustedDate.getTime()) @@ -649,8 +649,7 @@ export const mapEpochMillis: { (self: DateTime.Input, f: (millis: number) => number): DateTime } = dual(2, (self: DateTime.Input, f: (millis: number) => number): DateTime => { const dt = fromInput(self) - const prevEpochMillis = toEpochMillis(dt) - const newUtc = fromEpochMillis(f(prevEpochMillis)) + const newUtc = fromEpochMillis(f(toEpochMillis(dt))) return dt._tag === "Utc" ? newUtc : setZone(newUtc, dt.zone) }) @@ -664,7 +663,7 @@ export const mapEpochMillis: { export const withAdjustedDate: { (f: (date: Date) => A): (self: DateTime.Input) => A (self: DateTime.Input, f: (date: Date) => A): A -} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toAdjustedDate(self))) +} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toDateAdjusted(self))) /** * Using the time zone adjusted `Date`, apply a function to the `Date` and @@ -676,7 +675,7 @@ export const withAdjustedDate: { export const withUtcDate: { (f: (date: Date) => A): (self: DateTime.Input) => A (self: DateTime.Input, f: (date: Date) => A): A -} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toUtcDate(self))) +} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toDateUtc(self))) /** * @since 3.6.0 @@ -757,6 +756,24 @@ export const nowInCurrentZone: Effect.Effect number + (self: DateTime.Input, other: DateTime.Input): number +} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => { + const selfEpochMillis = toEpochMillis(self) + const otherEpochMillis = toEpochMillis(other) + return otherEpochMillis - selfEpochMillis +}) + /** * Calulate the difference between two `DateTime` values. * @@ -769,13 +786,11 @@ export const nowInCurrentZone: Effect.Effect Either.Either (self: DateTime.Input, other: DateTime.Input): Either.Either } = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { - const selfEpochMillis = toEpochMillis(self) - const otherEpochMillis = toEpochMillis(other) - const diffMillis = otherEpochMillis - selfEpochMillis + const diffMillis = diff(self, other) return diffMillis > 0 ? Either.right(Duration.millis(diffMillis)) : Either.left(Duration.millis(-diffMillis)) @@ -984,7 +999,7 @@ const dateToParts = (date: Date): DateTime.Parts => ({ * @since 3.6.0 * @category accessors */ -export const toAdjustedParts = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) +export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) /** * Get the different parts of a `DateTime` as an object. @@ -994,7 +1009,7 @@ export const toAdjustedParts = (self: DateTime.Input): DateTime.Parts => withAdj * @since 3.6.0 * @category accessors */ -export const toUtcParts = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) +export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) /** * Format a `DateTime` as a string using the `DateTimeFormat` API. @@ -1088,8 +1103,12 @@ const timeZoneOffset = (self: TimeZone): string => { } /** - * Format a `DateTime` as a string using the `DateTimeFormat` API, using the - * embedded time zone. + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * The `timeZone` option is set to the offset of the time zone. + * + * Note: On Node versions < 22, fixed "Offset" zones will set the time zone to + * "UTC" and use the adjusted `Date`. * * @since 3.6.0 * @category formatting @@ -1117,8 +1136,16 @@ export const formatWithZone: { readonly locale?: string | undefined } | undefined -): string => - new Intl.DateTimeFormat(options?.locale, { - ...options, - timeZone: timeZoneOffset(self.zone) - }).format(toEpochMillis(self))) +): string => { + try { + return new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: timeZoneOffset(self.zone) + }).format(toEpochMillis(self)) + } catch (_) { + return new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: "UTC" + }).format(toDateAdjusted(self)) + } +}) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 1e84e90d05..b9a6a70549 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -9,7 +9,7 @@ describe("DateTime", () => { const tomorrow = DateTime.mutate(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) - const diff = DateTime.diff(now, tomorrow) + const diff = DateTime.diffDuration(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) @@ -22,13 +22,13 @@ describe("DateTime", () => { const future = DateTime.mutate(now, (date) => { date.setUTCMonth(date.getUTCMonth() + 6) }) - assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toAdjustedDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.mutate(future, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) - assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toAdjustedDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + assert.strictEqual(DateTime.toDateAdjusted(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") })) }) @@ -37,7 +37,7 @@ describe("DateTime", () => { Effect.gen(function*() { const now = yield* DateTime.now const tomorrow = DateTime.add(now, 1, "day") - const diff = DateTime.diff(now, tomorrow) + const diff = DateTime.diffDuration(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) @@ -48,14 +48,14 @@ describe("DateTime", () => { DateTime.withCurrentZoneNamed("Pacific/Auckland") ) const future = DateTime.add(now, 6, "months") - assert.strictEqual(DateTime.toUtcDate(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toAdjustedDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.add(future, 1, "day") - assert.strictEqual(DateTime.toUtcDate(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toAdjustedDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") + assert.strictEqual(DateTime.toDateAdjusted(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") const minusOne = DateTime.add(plusOne, -1, "day") - assert.strictEqual(DateTime.toUtcDate(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toAdjustedDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") + assert.strictEqual(DateTime.toDateAdjusted(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) }) @@ -63,25 +63,25 @@ describe("DateTime", () => { it("month", () => { const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(mar, "month") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-31T23:59:59.999Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-31T23:59:59.999Z") }) it("feb leap year", () => { const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") const end = DateTime.endOf(feb, "month") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-02-29T23:59:59.999Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-02-29T23:59:59.999Z") }) it("week", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-16T23:59:59.999Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-16T23:59:59.999Z") }) it("week last day", () => { const start = DateTime.unsafeFromString("2024-03-16T12:00:00.000Z") const end = DateTime.endOf(start, "week") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-16T23:59:59.999Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-16T23:59:59.999Z") }) it("week with options", () => { @@ -89,7 +89,7 @@ describe("DateTime", () => { const end = DateTime.endOf(start, "week", { weekStartsOn: 1 }) - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-17T23:59:59.999Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-17T23:59:59.999Z") }) }) @@ -97,25 +97,25 @@ describe("DateTime", () => { it("month", () => { const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(mar, "month") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-01T00:00:00.000Z") }) it("feb leap year", () => { const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") const end = DateTime.startOf(feb, "month") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-02-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-02-01T00:00:00.000Z") }) it("week", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-10T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-10T00:00:00.000Z") }) it("week first day", () => { const start = DateTime.unsafeFromString("2024-03-10T12:00:00.000Z") const end = DateTime.startOf(start, "week") - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-10T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-10T00:00:00.000Z") }) it("week with options", () => { @@ -123,7 +123,7 @@ describe("DateTime", () => { const end = DateTime.startOf(start, "week", { weekStartsOn: 1 }) - assert.strictEqual(DateTime.toUtcDate(end).toISOString(), "2024-03-11T00:00:00.000Z") + assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-11T00:00:00.000Z") }) }) @@ -156,8 +156,8 @@ describe("DateTime", () => { describe("formatWithZone", () => { it.effect("full", () => Effect.gen(function*() { - const now = yield* DateTime.now.pipe( - Effect.flatMap(DateTime.setZoneNamed("Pacific/Auckland")) + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") ) assert.strictEqual( DateTime.formatWithZone(now, { dateStyle: "full", timeStyle: "full" }), @@ -165,16 +165,14 @@ describe("DateTime", () => { ) })) - // only works on node v22 - it.effect.skip("full with offset", () => + it.effect("long with offset", () => Effect.gen(function*() { - const now = yield* DateTime.now.pipe( - Effect.map(DateTime.setZoneOffset(10 * 60 * 60 * 1000)) - ) - assert.strictEqual( - DateTime.formatWithZone(now, { dateStyle: "full", timeStyle: "full" }), - "" + const now = yield* DateTime.now + const formatted = now.pipe( + DateTime.setZoneOffset(10 * 60 * 60 * 1000), + DateTime.formatWithZone({ dateStyle: "long", timeStyle: "short" }) ) + assert.strictEqual(formatted, "January 1, 1970 at 10:00 AM") })) }) }) From f82caaa329b0bea852b5151795a6d95d74b7856b Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 20:09:15 +1200 Subject: [PATCH 08/61] rearrange --- packages/effect/src/DateTime.ts | 306 ++++++++++++++++++-------------- 1 file changed, 169 insertions(+), 137 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 9b39d17385..e2809c4889 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -182,15 +182,6 @@ export declare namespace TimeZone { } } -/** - * @since 3.6.0 - * @category time zones - */ -export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZone")< - CurrentTimeZone, - TimeZone ->() {} - const Proto = { [TypeId]: TypeId, pipe() { @@ -306,6 +297,10 @@ export const isUtc = (self: DateTime): self is DateTime.Utc => self._tag === "Ut */ export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._tag === "WithZone" +// ============================================================================= +// constructors +// ============================================================================= + /** * Create a `DateTime` from the number of milliseconds since the Unix epoch. * @@ -401,6 +396,10 @@ export const now: Effect.Effect = Effect.map( */ export const unsafeNow: LazyArg = () => fromEpochMillis(Date.now()) +// ============================================================================= +// time zones +// ============================================================================= + /** * Set the time zone of a `DateTime`, returning a new `DateTime.WithZone`. * @@ -515,26 +514,53 @@ export const unsafeSetZoneNamed: { (self: DateTime.Input, zoneId: string): DateTime.WithZone } = dual(2, (self: DateTime.Input, zoneId: string): DateTime.WithZone => setZone(self, unsafeMakeZoneNamed(zoneId))) +// ============================================================================= +// comparisons +// ============================================================================= + /** - * Set the time zone of a `DateTime` to the current time zone, which is - * determined by the `CurrentTimeZone` service. + * Calulate the difference between two `DateTime` values, returning the number + * of milliseconds the `other` DateTime is from `self`. + * + * If `other` is *after* `self`, the result will be a positive number. * * @since 3.6.0 - * @category time zones + * @category comparisons */ -export const setZoneCurrent = (self: DateTime.Input): Effect.Effect => - Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) +export const diff: { + (other: DateTime.Input): (self: DateTime.Input) => number + (self: DateTime.Input, other: DateTime.Input): number +} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => { + const selfEpochMillis = toEpochMillis(self) + const otherEpochMillis = toEpochMillis(other) + return otherEpochMillis - selfEpochMillis +}) /** - * Get the milliseconds since the Unix epoch of a `DateTime`. + * Calulate the difference between two `DateTime` values. + * + * If the `other` DateTime is before `self`, the result will be a negative + * `Duration`, returned as a `Left`. + * + * If the `other` DateTime is after `self`, the result will be a positive + * `Duration`, returned as a `Right`. * * @since 3.6.0 - * @category conversions + * @category constructors */ -export const toEpochMillis = (self: DateTime.Input): number => { - const dt = fromInput(self) - return dt._tag === "WithZone" ? dt.utc.epochMillis : dt.epochMillis -} +export const diffDuration: { + (other: DateTime.Input): (self: DateTime.Input) => Either.Either + (self: DateTime.Input, other: DateTime.Input): Either.Either +} = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { + const diffMillis = diff(self, other) + return diffMillis > 0 + ? Either.right(Duration.millis(diffMillis)) + : Either.left(Duration.millis(-diffMillis)) +}) + +// ============================================================================= +// conversions +// ============================================================================= /** * Get the UTC `Date` of a `DateTime`. @@ -594,6 +620,122 @@ export const zoneOffset = (self: DateTime.Input): number => { return plainDate.getTime() - toEpochMillis(dt) } +/** + * Get the milliseconds since the Unix epoch of a `DateTime`. + * + * @since 3.6.0 + * @category conversions + */ +export const toEpochMillis = (self: DateTime.Input): number => { + const dt = fromInput(self) + return dt._tag === "WithZone" ? dt.utc.epochMillis : dt.epochMillis +} + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be time zone adjusted if necessary. + * + * @since 3.6.0 + * @category conversions + */ +export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) + +/** + * Get the different parts of a `DateTime` as an object. + * + * The parts will be in UTC. + * + * @since 3.6.0 + * @category conversions + */ +export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) + +// ============================================================================= +// current time zone +// ============================================================================= + +/** + * @since 3.6.0 + * @category current time zone + */ +export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZone")< + CurrentTimeZone, + TimeZone +>() {} + +/** + * Set the time zone of a `DateTime` to the current time zone, which is + * determined by the `CurrentTimeZone` service. + * + * @since 3.6.0 + * @category current time zone + */ +export const setZoneCurrent = (self: DateTime.Input): Effect.Effect => + Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) + +/** + * Provide the `CurrentTimeZone` to an effect. + * + * @since 3.6.0 + * @category current time zone + */ +export const withCurrentZone: { + (zone: TimeZone): (effect: Effect.Effect) => Effect.Effect> + (effect: Effect.Effect, zone: TimeZone): Effect.Effect> +} = dual( + 2, + (effect: Effect.Effect, zone: TimeZone): Effect.Effect> => + Effect.provideService(effect, CurrentTimeZone, zone) +) + +/** + * Provide the `CurrentTimeZone` to an effect using an IANA time zone + * identifier. + * + * If the time zone is invalid, it will fail with an `IllegalArgumentException`. + * + * @since 3.6.0 + * @category current time zone + */ +export const withCurrentZoneNamed: { + (zone: string): ( + effect: Effect.Effect + ) => Effect.Effect> + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> +} = dual( + 2, + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> => + Effect.flatMap( + Effect.try({ + try: () => unsafeMakeZoneNamed(zone), + catch: (e) => e as IllegalArgumentException + }), + (zone) => withCurrentZone(effect, zone) + ) +) + +/** + * Get the current time as a `DateTime.WithZone`, using the `CurrentTimeZone`. + * + * @since 3.6.0 + * @category current time zone + */ +export const nowInCurrentZone: Effect.Effect = Effect.flatMap( + now, + setZoneCurrent +) + +// ============================================================================= +// mapping +// ============================================================================= + const calcutateOffset = (date: Date, zone: TimeZone): number => zone._tag === "Offset" ? zone.offset : calcutateNamedOffset(date, zone) @@ -679,7 +821,7 @@ export const withUtcDate: { /** * @since 3.6.0 - * @category pattern matching + * @category mapping */ export const match: { (options: { @@ -698,103 +840,9 @@ export const match: { return dt._tag === "Utc" ? options.onUtc(dt) : options.onWithZone(dt) }) -/** - * Provide the `CurrentTimeZone` to an effect. - * - * @since 3.6.0 - * @category time zones - */ -export const withCurrentZone: { - (zone: TimeZone): (effect: Effect.Effect) => Effect.Effect> - (effect: Effect.Effect, zone: TimeZone): Effect.Effect> -} = dual( - 2, - (effect: Effect.Effect, zone: TimeZone): Effect.Effect> => - Effect.provideService(effect, CurrentTimeZone, zone) -) - -/** - * Provide the `CurrentTimeZone` to an effect using an IANA time zone - * identifier. - * - * If the time zone is invalid, it will fail with an `IllegalArgumentException`. - * - * @since 3.6.0 - * @category time zones - */ -export const withCurrentZoneNamed: { - (zone: string): ( - effect: Effect.Effect - ) => Effect.Effect> - ( - effect: Effect.Effect, - zone: string - ): Effect.Effect> -} = dual( - 2, - ( - effect: Effect.Effect, - zone: string - ): Effect.Effect> => - Effect.flatMap( - Effect.try({ - try: () => unsafeMakeZoneNamed(zone), - catch: (e) => e as IllegalArgumentException - }), - (zone) => withCurrentZone(effect, zone) - ) -) - -/** - * Get the current time as a `DateTime.WithZone`, using the `CurrentTimeZone`. - * - * @since 3.6.0 - * @category constructors - */ -export const nowInCurrentZone: Effect.Effect = Effect.flatMap( - now, - setZoneCurrent -) - -/** - * Calulate the difference between two `DateTime` values, returning the number - * of milliseconds the `other` DateTime is from `self`. - * - * If `other` is *after* `self`, the result will be a positive number. - * - * @since 3.6.0 - * @category constructors - */ -export const diff: { - (other: DateTime.Input): (self: DateTime.Input) => number - (self: DateTime.Input, other: DateTime.Input): number -} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => { - const selfEpochMillis = toEpochMillis(self) - const otherEpochMillis = toEpochMillis(other) - return otherEpochMillis - selfEpochMillis -}) - -/** - * Calulate the difference between two `DateTime` values. - * - * If the `other` DateTime is before `self`, the result will be a negative - * `Duration`, returned as a `Left`. - * - * If the `other` DateTime is after `self`, the result will be a positive - * `Duration`, returned as a `Right`. - * - * @since 3.6.0 - * @category constructors - */ -export const diffDuration: { - (other: DateTime.Input): (self: DateTime.Input) => Either.Either - (self: DateTime.Input, other: DateTime.Input): Either.Either -} = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { - const diffMillis = diff(self, other) - return diffMillis > 0 - ? Either.right(Duration.millis(diffMillis)) - : Either.left(Duration.millis(-diffMillis)) -}) +// ============================================================================= +// math +// ============================================================================= /** * Add the given `Duration` to a `DateTime`. @@ -991,25 +1039,9 @@ const dateToParts = (date: Date): DateTime.Parts => ({ year: date.getUTCFullYear() }) -/** - * Get the different parts of a `DateTime` as an object. - * - * The parts will be time zone adjusted if necessary. - * - * @since 3.6.0 - * @category accessors - */ -export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) - -/** - * Get the different parts of a `DateTime` as an object. - * - * The parts will be in UTC. - * - * @since 3.6.0 - * @category accessors - */ -export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) +// ============================================================================= +// formatting +// ============================================================================= /** * Format a `DateTime` as a string using the `DateTimeFormat` API. From 1585b7cc6e7208c1f15fca9a70a54d5c50f786db Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 20:16:42 +1200 Subject: [PATCH 09/61] add instances --- packages/effect/src/DateTime.ts | 57 +++++++++++++++++++++++---- packages/effect/test/DateTime.test.ts | 4 +- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index e2809c4889..6eae0923b7 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -7,12 +7,14 @@ import * as Context from "./Context.js" import * as Effect from "./Effect.js" import * as Either from "./Either.js" import * as Equal from "./Equal.js" +import * as Equivalence_ from "./Equivalence.js" import type { LazyArg } from "./Function.js" import { dual, pipe } from "./Function.js" import { globalValue } from "./GlobalValue.js" import * as Hash from "./Hash.js" import * as Inspectable from "./Inspectable.js" import * as Option from "./Option.js" +import * as Order_ from "./Order.js" import { type Pipeable, pipeArguments } from "./Pipeable.js" import * as Predicate from "./Predicate.js" @@ -271,13 +273,22 @@ const ProtoTimeZoneOffset = { } } +// ============================================================================= +// guards +// ============================================================================= + /** * @since 3.6.0 * @category guards */ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u, TypeId) -const isDateTimeInput = (u: unknown): u is DateTime.Input => isDateTime(u) || u instanceof Date || typeof u === "number" +/** + * @since 3.6.0 + * @category guards + */ +export const isDateTimeInput = (u: unknown): u is DateTime.Input => + isDateTime(u) || u instanceof Date || typeof u === "number" /** * @since 3.6.0 @@ -297,6 +308,28 @@ export const isUtc = (self: DateTime): self is DateTime.Utc => self._tag === "Ut */ export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._tag === "WithZone" +// ============================================================================= +// instances +// ============================================================================= + +/** + * @since 3.6.0 + * @category instances + */ +export const Equivalence: Equivalence_.Equivalence = Equivalence_.make((a, b) => + toEpochMillis(a) === toEpochMillis(b) +) + +/** + * @since 3.6.0 + * @category instances + */ +export const Order: Order_.Order = Order_.make((a, b) => { + const aMillis = toEpochMillis(a) + const bMillis = toEpochMillis(b) + return aMillis < bMillis ? -1 : aMillis > bMillis ? 1 : 0 +}) + // ============================================================================= // constructors // ============================================================================= @@ -530,11 +563,7 @@ export const unsafeSetZoneNamed: { export const diff: { (other: DateTime.Input): (self: DateTime.Input) => number (self: DateTime.Input, other: DateTime.Input): number -} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => { - const selfEpochMillis = toEpochMillis(self) - const otherEpochMillis = toEpochMillis(other) - return otherEpochMillis - selfEpochMillis -}) +} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => toEpochMillis(other) - toEpochMillis(self)) /** * Calulate the difference between two `DateTime` values. @@ -548,7 +577,7 @@ export const diff: { * @since 3.6.0 * @category constructors */ -export const diffDuration: { +export const diffDurationEither: { (other: DateTime.Input): (self: DateTime.Input) => Either.Either (self: DateTime.Input, other: DateTime.Input): Either.Either } = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { @@ -558,6 +587,20 @@ export const diffDuration: { : Either.left(Duration.millis(-diffMillis)) }) +/** + * Calulate the distance between two `DateTime` values. + * + * @since 3.6.0 + * @category constructors + */ +export const diffDuration: { + (other: DateTime.Input): (self: DateTime.Input) => Duration.Duration + (self: DateTime.Input, other: DateTime.Input): Duration.Duration +} = dual( + 2, + (self: DateTime.Input, other: DateTime.Input): Duration.Duration => Duration.millis(Math.abs(diff(self, other))) +) + // ============================================================================= // conversions // ============================================================================= diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index b9a6a70549..231cc8c12c 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -9,7 +9,7 @@ describe("DateTime", () => { const tomorrow = DateTime.mutate(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) - const diff = DateTime.diffDuration(now, tomorrow) + const diff = DateTime.diffDurationEither(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) @@ -37,7 +37,7 @@ describe("DateTime", () => { Effect.gen(function*() { const now = yield* DateTime.now const tomorrow = DateTime.add(now, 1, "day") - const diff = DateTime.diffDuration(now, tomorrow) + const diff = DateTime.diffDurationEither(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) From 129073408bc3b0b748536bb6ecf9bbae1ebc947e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 20:38:08 +1200 Subject: [PATCH 10/61] improve parts apis --- packages/effect/src/DateTime.ts | 99 ++++++++++++++++++++++++--- packages/effect/test/DateTime.test.ts | 33 ++++++--- 2 files changed, 113 insertions(+), 19 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 6eae0923b7..ebba020714 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -98,6 +98,7 @@ export declare namespace DateTime { readonly minutes: number readonly hours: number readonly day: number + readonly weekDay: number readonly month: number readonly year: number } @@ -117,6 +118,10 @@ export declare namespace DateTime { export interface Utc extends Proto { readonly _tag: "Utc" readonly epochMillis: number + /** @internal */ + partsAdjusted?: Parts + /** @internal */ + partsUtc?: Parts } /** @@ -129,6 +134,10 @@ export declare namespace DateTime { readonly zone: TimeZone /** @internal */ adjustedEpochMillis?: number + /** @internal */ + partsAdjusted?: Parts + /** @internal */ + partsUtc?: Parts } } @@ -189,7 +198,10 @@ const Proto = { pipe() { return pipeArguments(this, arguments) }, - ...Inspectable.BaseProto + ...Inspectable.BaseProto, + toJSON(this: DateTime) { + return toDateUtc(this).toJSON() + } } const ProtoUtc = { ...Proto, @@ -200,12 +212,12 @@ const ProtoUtc = { [Equal.symbol](this: DateTime.Utc, that: unknown) { return isDateTime(that) && that._tag === "Utc" && this.epochMillis === that.epochMillis }, - toJSON(this: DateTime.Utc) { - return { + toString(this: DateTime.Utc) { + return Inspectable.format({ _op: "DateTime", _tag: this._tag, epochMillis: this.epochMillis - } + }) } } const ProtoWithZone = { @@ -222,13 +234,13 @@ const ProtoWithZone = { return isDateTime(that) && that._tag === "WithZone" && Equal.equals(this.utc, that.utc) && Equal.equals(this.zone, that.zone) }, - toJSON(this: DateTime.WithZone) { - return { + toString(this: DateTime.WithZone) { + return Inspectable.format({ _id: "DateTime", _tag: this._tag, utc: this.utc.toJSON(), zone: this.zone - } + }) } } @@ -391,6 +403,30 @@ export const fromInput = (input: A): DateTime.Preserve */ export const fromDate: (date: Date) => Option.Option = Option.liftThrowable(unsafeFromDate) +/** + * Convert a partial `DateTime.Parts` into a `DateTime`. + * + * If a part is missing, it will default to `0`. + * + * @since 3.6.0 + * @category constructors + */ +export const fromParts = (parts: Partial>): DateTime.Utc => { + const date = new Date(0) + date.setUTCFullYear( + parts.year ?? 0, + parts.month ? parts.month - 1 : 0, + parts.day ?? 0 + ) + date.setUTCHours( + parts.hours ?? 0, + parts.minutes ?? 0, + parts.seconds ?? 0, + parts.millis ?? 0 + ) + return fromEpochMillis(date.getTime()) +} + /** * Parse a string into a `DateTime`, using `Date.parse`. * @@ -682,7 +718,14 @@ export const toEpochMillis = (self: DateTime.Input): number => { * @since 3.6.0 * @category conversions */ -export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => withAdjustedDate(self, dateToParts) +export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => { + const dt = fromInput(self) + if (dt.partsAdjusted !== undefined) { + return dt.partsAdjusted + } + dt.partsAdjusted = withAdjustedDate(self, dateToParts) + return dt.partsAdjusted +} /** * Get the different parts of a `DateTime` as an object. @@ -692,7 +735,44 @@ export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => withAdj * @since 3.6.0 * @category conversions */ -export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => withUtcDate(self, dateToParts) +export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => { + const dt = fromInput(self) + if (dt.partsUtc !== undefined) { + return dt.partsUtc + } + dt.partsUtc = withUtcDate(self, dateToParts) + return dt.partsUtc +} + +/** + * Get a part of a `DateTime` as a number. + * + * The part will be in the UTC time zone. + * + * @since 3.6.0 + * @category conversions + * @example + * import { DataTime } from "effect" + * + * const now = + */ +export const toPartUtc: { + (part: keyof DateTime.Parts): (self: DateTime.Input) => number + (self: DateTime.Input, part: keyof DateTime.Parts): number +} = dual(2, (self: DateTime.Input, part: keyof DateTime.Parts): number => toPartsUtc(self)[part]) + +/** + * Get a part of a `DateTime` as a number. + * + * The part will be time zone adjusted if necessary. + * + * @since 3.6.0 + * @category conversions + */ +export const toPartAdjusted: { + (part: keyof DateTime.Parts): (self: DateTime.Input) => number + (self: DateTime.Input, part: keyof DateTime.Parts): number +} = dual(2, (self: DateTime.Input, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) // ============================================================================= // current time zone @@ -1078,6 +1158,7 @@ const dateToParts = (date: Date): DateTime.Parts => ({ minutes: date.getUTCMinutes(), hours: date.getUTCHours(), day: date.getUTCDate(), + weekDay: date.getUTCDay(), month: date.getUTCMonth() + 1, year: date.getUTCFullYear() }) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 231cc8c12c..1c61a160f3 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -63,25 +63,26 @@ describe("DateTime", () => { it("month", () => { const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(mar, "month") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-31T23:59:59.999Z") + assert.strictEqual(end.toJSON(), "2024-03-31T23:59:59.999Z") }) it("feb leap year", () => { const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") const end = DateTime.endOf(feb, "month") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-02-29T23:59:59.999Z") + assert.strictEqual(end.toJSON(), "2024-02-29T23:59:59.999Z") }) it("week", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-16T23:59:59.999Z") + assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") + assert.strictEqual(DateTime.toPartAdjusted(end, "weekDay"), 6) }) it("week last day", () => { const start = DateTime.unsafeFromString("2024-03-16T12:00:00.000Z") const end = DateTime.endOf(start, "week") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-16T23:59:59.999Z") + assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") }) it("week with options", () => { @@ -89,7 +90,7 @@ describe("DateTime", () => { const end = DateTime.endOf(start, "week", { weekStartsOn: 1 }) - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-17T23:59:59.999Z") + assert.strictEqual(end.toJSON(), "2024-03-17T23:59:59.999Z") }) }) @@ -97,25 +98,26 @@ describe("DateTime", () => { it("month", () => { const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(mar, "month") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-01T00:00:00.000Z") + assert.strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) it("feb leap year", () => { const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") const end = DateTime.startOf(feb, "month") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-02-01T00:00:00.000Z") + assert.strictEqual(end.toJSON(), "2024-02-01T00:00:00.000Z") }) it("week", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-10T00:00:00.000Z") + assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") + assert.strictEqual(DateTime.toPartAdjusted(end, "weekDay"), 0) }) it("week first day", () => { const start = DateTime.unsafeFromString("2024-03-10T12:00:00.000Z") const end = DateTime.startOf(start, "week") - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-10T00:00:00.000Z") + assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") }) it("week with options", () => { @@ -123,7 +125,7 @@ describe("DateTime", () => { const end = DateTime.startOf(start, "week", { weekStartsOn: 1 }) - assert.strictEqual(DateTime.toDateUtc(end).toISOString(), "2024-03-11T00:00:00.000Z") + assert.strictEqual(end.toJSON(), "2024-03-11T00:00:00.000Z") }) }) @@ -175,4 +177,15 @@ describe("DateTime", () => { assert.strictEqual(formatted, "January 1, 1970 at 10:00 AM") })) }) + + describe("fromParts", () => { + it("partial", () => { + const date = DateTime.fromParts({ + year: 2024, + month: 12, + day: 25 + }) + console.log(DateTime.toDateUtc(date)) + }) + }) }) From 91f0b838cf02eca185107d86301242ff1644a36d Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 21:01:23 +1200 Subject: [PATCH 11/61] adjusted apis require DateTime.WithZone --- packages/effect/src/DateTime.ts | 62 +++++++++++++-------------- packages/effect/test/DateTime.test.ts | 4 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index ebba020714..9580c4c153 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -203,6 +203,7 @@ const Proto = { return toDateUtc(this).toJSON() } } + const ProtoUtc = { ...Proto, _tag: "Utc", @@ -220,6 +221,7 @@ const ProtoUtc = { }) } } + const ProtoWithZone = { ...Proto, _tag: "WithZone", @@ -658,16 +660,13 @@ export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis( * @since 3.6.0 * @category conversions */ -export const toDateAdjusted = (self: DateTime.Input): Date => { - const dt = fromInput(self) - if (dt._tag === "Utc") { - return new Date(dt.epochMillis) - } else if (dt.zone._tag === "Offset") { - return new Date(dt.utc.epochMillis + dt.zone.offset) - } else if (dt.adjustedEpochMillis !== undefined) { - return new Date(dt.adjustedEpochMillis) +export const toDateAdjusted = (self: DateTime.WithZone): Date => { + if (self.zone._tag === "Offset") { + return new Date(self.utc.epochMillis + self.zone.offset) + } else if (self.adjustedEpochMillis !== undefined) { + return new Date(self.adjustedEpochMillis) } - const parts = dt.zone.format.formatToParts(dt.utc.epochMillis) + const parts = self.zone.format.formatToParts(self.utc.epochMillis) const date = new Date(0) date.setUTCFullYear( Number(parts[4].value), @@ -680,7 +679,7 @@ export const toDateAdjusted = (self: DateTime.Input): Date => { Number(parts[10].value), Number(parts[12].value) ) - dt.adjustedEpochMillis = date.getTime() + self.adjustedEpochMillis = date.getTime() return date } @@ -718,13 +717,12 @@ export const toEpochMillis = (self: DateTime.Input): number => { * @since 3.6.0 * @category conversions */ -export const toPartsAdjusted = (self: DateTime.Input): DateTime.Parts => { - const dt = fromInput(self) - if (dt.partsAdjusted !== undefined) { - return dt.partsAdjusted +export const toPartsAdjusted = (self: DateTime.WithZone): DateTime.Parts => { + if (self.partsAdjusted !== undefined) { + return self.partsAdjusted } - dt.partsAdjusted = withAdjustedDate(self, dateToParts) - return dt.partsAdjusted + self.partsAdjusted = withAdjustedDate(self, dateToParts) + return self.partsAdjusted } /** @@ -770,9 +768,9 @@ export const toPartUtc: { * @category conversions */ export const toPartAdjusted: { - (part: keyof DateTime.Parts): (self: DateTime.Input) => number - (self: DateTime.Input, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime.Input, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) + (part: keyof DateTime.Parts): (self: DateTime.WithZone) => number + (self: DateTime.WithZone, part: keyof DateTime.Parts): number +} = dual(2, (self: DateTime.WithZone, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) // ============================================================================= // current time zone @@ -859,11 +857,11 @@ export const nowInCurrentZone: Effect.Effect - zone._tag === "Offset" ? zone.offset : calcutateNamedOffset(date, zone) +const calculateOffset = (date: Date, zone: TimeZone): number => + zone._tag === "Offset" ? zone.offset : calculateNamedOffset(date, zone) const gmtOffsetRegex = /^GMT([+-])(\d{2}):(\d{2})$/ -const calcutateNamedOffset = (date: Date, zone: TimeZone.Named): number => { +const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const parts = zone.format.formatToParts(date) const offset = parts[14].value if (offset === "GMT") { @@ -871,7 +869,7 @@ const calcutateNamedOffset = (date: Date, zone: TimeZone.Named): number => { } const match = gmtOffsetRegex.exec(offset) if (match === null) { - // fallback to using the plain date + // fallback to using the adjusted date return zoneOffset(setZone(date, zone)) } const [, sign, hours, minutes] = match @@ -879,10 +877,10 @@ const calcutateNamedOffset = (date: Date, zone: TimeZone.Named): number => { } /** - * Modify a `DateTime` by applying a function to the underlying plain `Date`. + * Modify a `DateTime` by applying a function to the underlying adjusted `Date`. * * The `Date` will first have the time zone applied if necessary, and then be - * converted back to a `DateTime` with the same time zone. + * converted back to a `DateTime` within the same time zone. * * @since 3.6.0 * @category mapping @@ -892,13 +890,15 @@ export const mutate: { (self: A, f: (plainDate: Date) => void): DateTime.PreserveZone } = dual(2, (self: DateTime.Input, f: (plainDate: Date) => void): DateTime => { const dt = fromInput(self) + if (dt._tag === "Utc") { + const date = toDateUtc(dt) + f(date) + return fromEpochMillis(date.getTime()) + } const adjustedDate = toDateAdjusted(dt) const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) - if (dt._tag === "Utc") { - return fromEpochMillis(newAdjustedDate.getTime()) - } - const offset = calcutateOffset(newAdjustedDate, dt.zone) + const offset = calculateOffset(newAdjustedDate, dt.zone) return setZone(fromEpochMillis(newAdjustedDate.getTime() - offset), dt.zone) }) @@ -926,9 +926,9 @@ export const mapEpochMillis: { * @category mapping */ export const withAdjustedDate: { - (f: (date: Date) => A): (self: DateTime.Input) => A + (f: (date: Date) => A): (self: DateTime.WithZone) => A (self: DateTime.Input, f: (date: Date) => A): A -} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toDateAdjusted(self))) +} = dual(2, (self: DateTime.WithZone, f: (date: Date) => A): A => f(toDateAdjusted(self))) /** * Using the time zone adjusted `Date`, apply a function to the `Date` and diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 1c61a160f3..9c6c23ace8 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -76,7 +76,7 @@ describe("DateTime", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") - assert.strictEqual(DateTime.toPartAdjusted(end, "weekDay"), 6) + assert.strictEqual(DateTime.toPartUtc(end, "weekDay"), 6) }) it("week last day", () => { @@ -111,7 +111,7 @@ describe("DateTime", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") - assert.strictEqual(DateTime.toPartAdjusted(end, "weekDay"), 0) + assert.strictEqual(DateTime.toPartUtc(end, "weekDay"), 0) }) it("week first day", () => { From 187efc5bd8a886333cdb21e5fabb85e195125acb Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 21:03:56 +1200 Subject: [PATCH 12/61] reword adjusted jsdocs --- packages/effect/src/DateTime.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 9580c4c153..eb80398f8d 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -652,10 +652,7 @@ export const diffDuration: { export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis(self)) /** - * Convert a `DateTime` to a `Date`, applying the time zone first if necessary. - * - * The returned Date will be offset by the time zone if the `DateTime` is a - * `DateTime.WithZone`. + * Convert a `DateTime` to a `Date`, applying the time zone first. * * @since 3.6.0 * @category conversions @@ -712,7 +709,7 @@ export const toEpochMillis = (self: DateTime.Input): number => { /** * Get the different parts of a `DateTime` as an object. * - * The parts will be time zone adjusted if necessary. + * The parts will be time zone adjusted. * * @since 3.6.0 * @category conversions @@ -762,7 +759,7 @@ export const toPartUtc: { /** * Get a part of a `DateTime` as a number. * - * The part will be time zone adjusted if necessary. + * The part will be time zone adjusted. * * @since 3.6.0 * @category conversions @@ -877,9 +874,9 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { } /** - * Modify a `DateTime` by applying a function to the underlying adjusted `Date`. + * Modify a `DateTime` by applying a function to the underlying `Date`. * - * The `Date` will first have the time zone applied if necessary, and then be + * The `Date` will first have the time zone applied if possible, and then be * converted back to a `DateTime` within the same time zone. * * @since 3.6.0 From da1fd4b644e54a6b016efbb61548e644498d6f8c Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 21:08:37 +1200 Subject: [PATCH 13/61] fix docs --- packages/effect/src/DateTime.ts | 20 ++++++++++---------- packages/effect/test/DateTime.test.ts | 11 ++++++++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index eb80398f8d..90d46e11de 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -397,8 +397,7 @@ export const fromInput = (input: A): DateTime.Preserve } /** - * Safely create a `DateTime` from a `Date`, returning `None` if the `Date` is - * invalid. + * Safely create a `DateTime` from a `Date`, returning `None` if the `Date` is invalid. * * @since 3.6.0 * @category constructors @@ -408,7 +407,7 @@ export const fromDate: (date: Date) => Option.Option = Option.lift /** * Convert a partial `DateTime.Parts` into a `DateTime`. * - * If a part is missing, it will default to `0`. + * If a part is missing, it will default to the smallest possible value. (months will start at 1, days at 1, hours at 0 etc.) * * @since 3.6.0 * @category constructors @@ -418,7 +417,7 @@ export const fromParts = (parts: Partial>): D date.setUTCFullYear( parts.year ?? 0, parts.month ? parts.month - 1 : 0, - parts.day ?? 0 + parts.day ?? 1 ) date.setUTCHours( parts.hours ?? 0, @@ -448,8 +447,7 @@ export const fromString = (input: string): Option.Option => fromDa export const unsafeFromString = (input: string): DateTime.Utc => unsafeFromDate(new Date(input)) /** - * Get the current time using the `Clock` service and convert it to a - * `DateTime`. + * Get the current time using the `Clock` service and convert it to a `DateTime`. * * @since 3.6.0 * @category constructors @@ -747,11 +745,13 @@ export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => { * @since 3.6.0 * @category conversions * @example - * import { DataTime } from "effect" + * import { DateTime } from "effect" * - * const now = + * const now = DateTime.fromParts({ year: 2024 }) + * const year = DateTime.getPartUtc(now, "year") + * assert.strictEqual(year, 2024) */ -export const toPartUtc: { +export const getPartUtc: { (part: keyof DateTime.Parts): (self: DateTime.Input) => number (self: DateTime.Input, part: keyof DateTime.Parts): number } = dual(2, (self: DateTime.Input, part: keyof DateTime.Parts): number => toPartsUtc(self)[part]) @@ -764,7 +764,7 @@ export const toPartUtc: { * @since 3.6.0 * @category conversions */ -export const toPartAdjusted: { +export const getPartAdjusted: { (part: keyof DateTime.Parts): (self: DateTime.WithZone) => number (self: DateTime.WithZone, part: keyof DateTime.Parts): number } = dual(2, (self: DateTime.WithZone, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 9c6c23ace8..4875a7d3c5 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -76,7 +76,7 @@ describe("DateTime", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") - assert.strictEqual(DateTime.toPartUtc(end, "weekDay"), 6) + assert.strictEqual(DateTime.getPartUtc(end, "weekDay"), 6) }) it("week last day", () => { @@ -111,7 +111,7 @@ describe("DateTime", () => { const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") - assert.strictEqual(DateTime.toPartUtc(end, "weekDay"), 0) + assert.strictEqual(DateTime.getPartUtc(end, "weekDay"), 0) }) it("week first day", () => { @@ -185,7 +185,12 @@ describe("DateTime", () => { month: 12, day: 25 }) - console.log(DateTime.toDateUtc(date)) + assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + }) + + it("month is set correctly", () => { + const date = DateTime.fromParts({ year: 2024 }) + assert.strictEqual(date.toJSON(), "2024-01-01T00:00:00.000Z") }) }) }) From a53e6c9975a2b9234e80423b346f1680f4170575 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 21:56:05 +1200 Subject: [PATCH 14/61] formatIso --- packages/effect/src/DateTime.ts | 59 +++++++++++++++++++-------- packages/effect/test/DateTime.test.ts | 17 ++++++++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 90d46e11de..3cc633ee19 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -679,20 +679,33 @@ export const toDateAdjusted = (self: DateTime.WithZone): Date => { } /** - * Calculate the time zone offset of a `DateTime` in milliseconds. + * Calculate the time zone offset of a `DateTime.WithZone` in milliseconds. * * @since 3.6.0 * @category conversions */ -export const zoneOffset = (self: DateTime.Input): number => { - const dt = fromInput(self) - if (dt._tag === "Utc") { - return 0 - } - const plainDate = toDateAdjusted(dt) - return plainDate.getTime() - toEpochMillis(dt) +export const zoneOffset = (self: DateTime.WithZone): number => { + const plainDate = toDateAdjusted(self) + return plainDate.getTime() - toEpochMillis(self) +} + +const offsetToString = (offset: number): string => { + const abs = Math.abs(offset) + const hours = Math.floor(abs / (60 * 60 * 1000)) + const minutes = Math.round((abs % (60 * 60 * 1000)) / (60 * 1000)) + return `${offset < 0 ? "-" : "+"}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}` } +/** + * Calculate the time zone offset of a `DateTime` in milliseconds. + * + * The offset is formatted as "±HH:MM". + * + * @since 3.6.0 + * @category conversions + */ +export const zoneOffsetString = (self: DateTime.WithZone): string => offsetToString(zoneOffset(self)) + /** * Get the milliseconds since the Unix epoch of a `DateTime`. * @@ -1243,16 +1256,11 @@ export const formatIntl: { (self: DateTime.Input, format: Intl.DateTimeFormat): string } = dual(2, (self: DateTime.Input, format: Intl.DateTimeFormat): string => format.format(toEpochMillis(self))) -const timeZoneOffset = (self: TimeZone): string => { +const intlTimeZone = (self: TimeZone): string => { if (self._tag === "Named") { return self.id } - const abs = Math.abs(self.offset) - const offsetHours = Math.floor(abs / (60 * 60 * 1000)) - const offsetMinutes = Math.round((abs % (60 * 60 * 1000)) / (60 * 1000)) - return `${self.offset < 0 ? "-" : "+"}${String(offsetHours).padStart(2, "0")}:${ - String(`${offsetMinutes}`).padStart(2, "0") - }` + return offsetToString(self.offset) } /** @@ -1293,7 +1301,7 @@ export const formatWithZone: { try { return new Intl.DateTimeFormat(options?.locale, { ...options, - timeZone: timeZoneOffset(self.zone) + timeZone: intlTimeZone(self.zone) }).format(toEpochMillis(self)) } catch (_) { return new Intl.DateTimeFormat(options?.locale, { @@ -1302,3 +1310,22 @@ export const formatWithZone: { }).format(toDateAdjusted(self)) } }) + +/** + * Format a `DateTime` as a UTC ISO string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIso = (self: DateTime.Input): string => toDateUtc(self).toISOString() + +/** + * Format a `DateTime.WithZone` as a ISO string with an offset. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoOffset = (self: DateTime.WithZone): string => { + const date = toDateAdjusted(self) + return `${date.toISOString().slice(0, 19)}${zoneOffsetString(self)}` +} diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 4875a7d3c5..a4da706419 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -193,4 +193,21 @@ describe("DateTime", () => { assert.strictEqual(date.toJSON(), "2024-01-01T00:00:00.000Z") }) }) + + describe("formatIso", () => { + it("full", () => { + const now = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + assert.strictEqual(DateTime.formatIso(now), "2024-03-15T12:00:00.000Z") + }) + }) + + describe("formatIsoOffset", () => { + it.effect("correctly adds offset", () => + Effect.gen(function*() { + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00+12:00") + })) + }) }) From 98210ffc43ca00cfaf7b2eecde101b7504520ce2 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 21:57:56 +1200 Subject: [PATCH 15/61] fix import --- packages/effect/src/DateTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 3cc633ee19..a4f3094c37 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1,9 +1,9 @@ /** * @since 3.6.0 */ -import { Duration } from "effect" import { IllegalArgumentException } from "./Cause.js" import * as Context from "./Context.js" +import * as Duration from "./Duration.js" import * as Effect from "./Effect.js" import * as Either from "./Either.js" import * as Equal from "./Equal.js" From a3f7e88b2217a53348581136cd4e03825efd10c0 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 22:16:33 +1200 Subject: [PATCH 16/61] simplify WithZone model --- packages/effect/src/DateTime.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index a4f3094c37..53afcc5b70 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -119,9 +119,7 @@ export declare namespace DateTime { readonly _tag: "Utc" readonly epochMillis: number /** @internal */ - partsAdjusted?: Parts - /** @internal */ - partsUtc?: Parts + partsUtc: Parts } /** @@ -130,7 +128,7 @@ export declare namespace DateTime { */ export interface WithZone extends Proto { readonly _tag: "WithZone" - readonly utc: Utc + readonly epochMillis: number readonly zone: TimeZone /** @internal */ adjustedEpochMillis?: number @@ -227,20 +225,20 @@ const ProtoWithZone = { _tag: "WithZone", [Hash.symbol](this: DateTime.WithZone) { return pipe( - Hash.hash(this.utc), + Hash.number(this.epochMillis), Hash.combine(Hash.hash(this.zone)), Hash.cached(this) ) }, [Equal.symbol](this: DateTime.WithZone, that: unknown) { - return isDateTime(that) && that._tag === "WithZone" && Equal.equals(this.utc, that.utc) && + return isDateTime(that) && that._tag === "WithZone" && this.epochMillis === that.epochMillis && Equal.equals(this.zone, that.zone) }, toString(this: DateTime.WithZone) { return Inspectable.format({ _id: "DateTime", _tag: this._tag, - utc: this.utc.toJSON(), + epochMillis: this.epochMillis, zone: this.zone }) } @@ -481,8 +479,9 @@ export const setZone: { } = dual(2, (self: DateTime.Input, zone: TimeZone): DateTime.WithZone => { const dt = fromInput(self) const selfWithZone = Object.create(ProtoWithZone) - selfWithZone.utc = dt._tag === "Utc" ? dt : dt.utc + selfWithZone.epochMillis = dt.epochMillis selfWithZone.zone = zone + selfWithZone.partsUtc = dt.partsUtc return selfWithZone }) @@ -657,11 +656,11 @@ export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis( */ export const toDateAdjusted = (self: DateTime.WithZone): Date => { if (self.zone._tag === "Offset") { - return new Date(self.utc.epochMillis + self.zone.offset) + return new Date(self.epochMillis + self.zone.offset) } else if (self.adjustedEpochMillis !== undefined) { return new Date(self.adjustedEpochMillis) } - const parts = self.zone.format.formatToParts(self.utc.epochMillis) + const parts = self.zone.format.formatToParts(self.epochMillis) const date = new Date(0) date.setUTCFullYear( Number(parts[4].value), @@ -712,10 +711,7 @@ export const zoneOffsetString = (self: DateTime.WithZone): string => offsetToStr * @since 3.6.0 * @category conversions */ -export const toEpochMillis = (self: DateTime.Input): number => { - const dt = fromInput(self) - return dt._tag === "WithZone" ? dt.utc.epochMillis : dt.epochMillis -} +export const toEpochMillis = (self: DateTime.Input): number => fromInput(self).epochMillis /** * Get the different parts of a `DateTime` as an object. From f04a69d32995586669bf13837f4efdbd15c2c72b Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 22:31:25 +1200 Subject: [PATCH 17/61] test .endOf with time zones --- packages/effect/test/DateTime.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index a4da706419..775385d3aa 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -1,6 +1,8 @@ import { DateTime, Duration, Effect, Either, TestClock } from "effect" import { assert, describe, it } from "./utils/extend.js" +const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) + describe("DateTime", () => { describe("mutate", () => { it.effect("should mutate the date", () => @@ -15,7 +17,7 @@ describe("DateTime", () => { it.effect("correctly preserves the time zone", () => Effect.gen(function*() { - yield* TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) + yield* setTo2024NZ const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) @@ -43,7 +45,7 @@ describe("DateTime", () => { it.effect("correctly preserves the time zone", () => Effect.gen(function*() { - yield* TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) + yield* setTo2024NZ const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) @@ -92,6 +94,17 @@ describe("DateTime", () => { }) assert.strictEqual(end.toJSON(), "2024-03-17T23:59:59.999Z") }) + + it.effect("correctly preserves the time zone", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.nowInCurrentZone.pipe( + DateTime.withCurrentZoneNamed("Pacific/Auckland") + ) + const future = DateTime.endOf(now, "month") + assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-01-31T10:59:59.999Z") + assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-01-31T23:59:59.999Z") + })) }) describe("startOf", () => { From 6171b7ccf443f84e463a148ac31fcac227d2f07a Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jul 2024 23:29:59 +1200 Subject: [PATCH 18/61] add CurrentTimeZone layers --- packages/effect/src/DateTime.ts | 76 ++++++++++++++++++++++++--- packages/effect/test/DateTime.test.ts | 10 ++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 53afcc5b70..cdba92912c 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -13,6 +13,7 @@ import { dual, pipe } from "./Function.js" import { globalValue } from "./GlobalValue.js" import * as Hash from "./Hash.js" import * as Inspectable from "./Inspectable.js" +import * as Layer from "./Layer.js" import * as Option from "./Option.js" import * as Order_ from "./Order.js" import { type Pipeable, pipeArguments } from "./Pipeable.js" @@ -554,6 +555,34 @@ export const makeZoneNamed: (zoneId: string) => Option.Option = unsafeMakeZoneNamed ) +/** + * Create a named time zone from a IANA time zone identifier. If the time zone + * is invalid, it will fail with an `IllegalArgumentException`. + * + * @since 3.6.0 + * @category time zones + */ +export const makeZoneNamedEffect = (zoneId: string): Effect.Effect => + Effect.try({ + try: () => unsafeMakeZoneNamed(zoneId), + catch: (e) => e as IllegalArgumentException + }) + +/** + * Create a named time zone from the system's local time zone. + * + * @since 3.6.0 + * @category time zones + */ +export const makeZoneLocal = (): TimeZone.Named => { + const format = new Intl.DateTimeFormat(undefined, formatOptions) + const zone = Object.create(ProtoTimeZoneNamed) + zone.id = format.resolvedOptions().timeZone + zone.format = format + validZoneCache.set(zone.id, zone) + return zone +} + /** * Set the time zone of a `DateTime` from an IANA time zone identifier. If the * time zone is invalid, `None` will be returned. @@ -816,6 +845,17 @@ export const withCurrentZone: { Effect.provideService(effect, CurrentTimeZone, zone) ) +/** + * Provide the `CurrentTimeZone` to an effect. + * + * @since 3.6.0 + * @category current time zone + */ +export const withCurrentZoneLocal = ( + effect: Effect.Effect +): Effect.Effect> => + Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(makeZoneLocal)) + /** * Provide the `CurrentTimeZone` to an effect using an IANA time zone * identifier. @@ -839,13 +879,7 @@ export const withCurrentZoneNamed: { effect: Effect.Effect, zone: string ): Effect.Effect> => - Effect.flatMap( - Effect.try({ - try: () => unsafeMakeZoneNamed(zone), - catch: (e) => e as IllegalArgumentException - }), - (zone) => withCurrentZone(effect, zone) - ) + Effect.provideServiceEffect(effect, CurrentTimeZone, makeZoneNamedEffect(zone)) ) /** @@ -859,6 +893,34 @@ export const nowInCurrentZone: Effect.Effect => Layer.succeed(CurrentTimeZone, zone) + +/** + * Create a Layer from the given IANA time zone identifier. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneNamed = (zoneId: string): Layer.Layer => + Layer.effect(CurrentTimeZone, makeZoneNamedEffect(zoneId)) + +/** + * Create a Layer from the given IANA time zone identifier. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneLocal: Layer.Layer = Layer.sync( + CurrentTimeZone, + makeZoneLocal +) + // ============================================================================= // mapping // ============================================================================= diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 775385d3aa..05cfc0b866 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -223,4 +223,14 @@ describe("DateTime", () => { assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00+12:00") })) }) + + describe("layerCurrentZoneNamed", () => { + it.effect("correctly adds offset", () => + Effect.gen(function*() { + const now = yield* DateTime.nowInCurrentZone + assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00+12:00") + }).pipe( + Effect.provide(DateTime.layerCurrentZoneNamed("Pacific/Auckland")) + )) + }) }) From 34efcee5c0e4dc662d32ac31a63c13d18c990ae7 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 00:05:22 +1200 Subject: [PATCH 19/61] clean-up --- packages/effect/src/DateTime.ts | 62 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index cdba92912c..51d8f8f465 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -372,9 +372,7 @@ export const unsafeFromDate = (date: Date): DateTime.Utc => { if (isNaN(epochMillis)) { throw new IllegalArgumentException("Invalid date") } - const self = Object.create(ProtoUtc) - self.epochMillis = epochMillis - return self + return fromEpochMillis(epochMillis) } /** @@ -468,6 +466,14 @@ export const unsafeNow: LazyArg = () => fromEpochMillis(Date.now() // time zones // ============================================================================= +const makeWithZone = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): DateTime.WithZone => { + const self = Object.create(ProtoWithZone) + self.epochMillis = epochMillis + self.zone = zone + self.partsUtc = partsUtc + return self +} + /** * Set the time zone of a `DateTime`, returning a new `DateTime.WithZone`. * @@ -479,11 +485,7 @@ export const setZone: { (self: DateTime.Input, zone: TimeZone): DateTime.WithZone } = dual(2, (self: DateTime.Input, zone: TimeZone): DateTime.WithZone => { const dt = fromInput(self) - const selfWithZone = Object.create(ProtoWithZone) - selfWithZone.epochMillis = dt.epochMillis - selfWithZone.zone = zone - selfWithZone.partsUtc = dt.partsUtc - return selfWithZone + return makeWithZone(dt.epochMillis, zone, dt.partsUtc) }) /** @@ -517,6 +519,18 @@ const formatOptions: Intl.DateTimeFormatOptions = { hourCycle: "h23" } +const makeZoneIntl = (format: Intl.DateTimeFormat): TimeZone.Named => { + const zoneId = format.resolvedOptions().timeZone + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + const zone = Object.create(ProtoTimeZoneNamed) + zone.id = zoneId + zone.format = format + validZoneCache.set(zoneId, zone) + return zone +} + /** * Attempt to create a named time zone from a IANA time zone identifier. * @@ -530,15 +544,12 @@ export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { return validZoneCache.get(zoneId)! } try { - const format = new Intl.DateTimeFormat("en-US", { - ...formatOptions, - timeZone: zoneId - }) - const zone = Object.create(ProtoTimeZoneNamed) - zone.id = format.resolvedOptions().timeZone - zone.format = format - validZoneCache.set(zoneId, zone) - return zone + return makeZoneIntl( + new Intl.DateTimeFormat("en-US", { + ...formatOptions, + timeZone: zoneId + }) + ) } catch (_) { throw new IllegalArgumentException(`Invalid time zone: ${zoneId}`) } @@ -574,14 +585,7 @@ export const makeZoneNamedEffect = (zoneId: string): Effect.Effect { - const format = new Intl.DateTimeFormat(undefined, formatOptions) - const zone = Object.create(ProtoTimeZoneNamed) - zone.id = format.resolvedOptions().timeZone - zone.format = format - validZoneCache.set(zone.id, zone) - return zone -} +export const makeZoneLocal = (): TimeZone.Named => makeZoneIntl(new Intl.DateTimeFormat("en-US", formatOptions)) /** * Set the time zone of a `DateTime` from an IANA time zone identifier. If the @@ -938,7 +942,7 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const match = gmtOffsetRegex.exec(offset) if (match === null) { // fallback to using the adjusted date - return zoneOffset(setZone(date, zone)) + return zoneOffset(makeWithZone(date.getTime(), zone)) } const [, sign, hours, minutes] = match return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 @@ -967,7 +971,7 @@ export const mutate: { const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) const offset = calculateOffset(newAdjustedDate, dt.zone) - return setZone(fromEpochMillis(newAdjustedDate.getTime() - offset), dt.zone) + return makeWithZone(newAdjustedDate.getTime() - offset, dt.zone) }) /** @@ -982,8 +986,8 @@ export const mapEpochMillis: { (self: DateTime.Input, f: (millis: number) => number): DateTime } = dual(2, (self: DateTime.Input, f: (millis: number) => number): DateTime => { const dt = fromInput(self) - const newUtc = fromEpochMillis(f(toEpochMillis(dt))) - return dt._tag === "Utc" ? newUtc : setZone(newUtc, dt.zone) + const millis = f(toEpochMillis(dt)) + return dt._tag === "Utc" ? fromEpochMillis(millis) : makeWithZone(millis, dt.zone) }) /** From 93503d20471c5b99743cebd8b684c1e8b9936c66 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 00:28:04 +1200 Subject: [PATCH 20/61] handle months with less days in .add --- packages/effect/src/DateTime.ts | 6 +++++- packages/effect/test/DateTime.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 51d8f8f465..0af7795bd2 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1109,7 +1109,11 @@ export const add: { } case "months": case "month": { - date.setUTCMonth(date.getUTCMonth() + amount) + const day = date.getUTCDate() + date.setUTCMonth(date.getUTCMonth() + amount + 1, 0) + if (day < date.getUTCDate()) { + date.setUTCDate(day) + } return date } case "years": diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 05cfc0b866..ef7f22d2a0 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -43,6 +43,16 @@ describe("DateTime", () => { assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) + it("to month with less days", () => { + const jan = DateTime.fromParts({ year: 2023, month: 1, day: 31 }) + let feb = DateTime.add(jan, 1, "month") + assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") + + const mar = DateTime.fromParts({ year: 2023, month: 3, day: 31 }) + feb = DateTime.add(mar, -1, "month") + assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") + }) + it.effect("correctly preserves the time zone", () => Effect.gen(function*() { yield* setTo2024NZ From a64d3e2870ad8bd59b94bf98c9f20222864244c9 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 00:54:04 +1200 Subject: [PATCH 21/61] add order apis --- packages/effect/src/DateTime.ts | 114 +++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 0af7795bd2..49d5299e50 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -15,7 +15,7 @@ import * as Hash from "./Hash.js" import * as Inspectable from "./Inspectable.js" import * as Layer from "./Layer.js" import * as Option from "./Option.js" -import * as Order_ from "./Order.js" +import * as order from "./Order.js" import { type Pipeable, pipeArguments } from "./Pipeable.js" import * as Predicate from "./Predicate.js" @@ -330,19 +330,50 @@ export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._t * @category instances */ export const Equivalence: Equivalence_.Equivalence = Equivalence_.make((a, b) => - toEpochMillis(a) === toEpochMillis(b) + a.epochMillis === b.epochMillis ) /** * @since 3.6.0 * @category instances */ -export const Order: Order_.Order = Order_.make((a, b) => { +export const OrderInput: order.Order = order.make((a, b) => { const aMillis = toEpochMillis(a) const bMillis = toEpochMillis(b) return aMillis < bMillis ? -1 : aMillis > bMillis ? 1 : 0 }) +/** + * @since 3.6.0 + * @category instances + */ +export const Order: order.Order = OrderInput + +/** + * @since 3.6.0 + */ +export const clamp: { + (options: { + minimum: DateTime.Input + maximum: DateTime.Input + }): (self: DateTime.Input) => DateTime + (self: DateTime.Input, options: { + minimum: DateTime.Input + maximum: DateTime.Input + }): DateTime +} = dual( + 2, + (self: DateTime.Input, options: { + minimum: DateTime.Input + maximum: DateTime.Input + }): DateTime => + _clamp(fromInput(self), { + minimum: fromInput(options.minimum), + maximum: fromInput(options.maximum) + }) +) +const _clamp = order.clamp(Order) + // ============================================================================= // constructors // ============================================================================= @@ -669,6 +700,83 @@ export const diffDuration: { (self: DateTime.Input, other: DateTime.Input): Duration.Duration => Duration.millis(Math.abs(diff(self, other))) ) +/** + * @since 3.6.0 + * @category comparisons + */ +export const min: { + (that: DateTime.Input): (self: DateTime.Input) => DateTime + (self: DateTime.Input, that: DateTime.Input): DateTime +} = dual(2, (self: DateTime.Input, that: DateTime.Input): DateTime => _min(fromInput(self), fromInput(that))) +const _min = order.min(Order) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const max: { + (that: DateTime.Input): (self: DateTime.Input) => DateTime + (self: DateTime.Input, that: DateTime.Input): DateTime +} = dual(2, (self: DateTime.Input, that: DateTime.Input): DateTime => _max(fromInput(self), fromInput(that))) +const _max = order.max(Order) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const greaterThan: { + (that: DateTime.Input): (self: DateTime.Input) => boolean + (self: DateTime.Input, that: DateTime.Input): boolean +} = order.greaterThan(OrderInput) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const greaterThanOrEqualTo: { + (that: DateTime.Input): (self: DateTime.Input) => boolean + (self: DateTime.Input, that: DateTime.Input): boolean +} = order.greaterThanOrEqualTo(OrderInput) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const lessThan: { + (that: DateTime.Input): (self: DateTime.Input) => boolean + (self: DateTime.Input, that: DateTime.Input): boolean +} = order.lessThan(OrderInput) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const lessThanOrEqualTo: { + (that: DateTime.Input): (self: DateTime.Input) => boolean + (self: DateTime.Input, that: DateTime.Input): boolean +} = order.lessThanOrEqualTo(OrderInput) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const between: { + (options: { minimum: DateTime.Input; maximum: DateTime.Input }): (self: DateTime.Input) => boolean + (self: DateTime.Input, options: { minimum: DateTime.Input; maximum: DateTime.Input }): boolean +} = order.between(OrderInput) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const isFuture = (self: DateTime.Input): Effect.Effect => Effect.map(now, lessThan(self)) + +/** + * @since 3.6.0 + * @category comparisons + */ +export const isPast = (self: DateTime.Input): Effect.Effect => Effect.map(now, greaterThan(self)) + // ============================================================================= // conversions // ============================================================================= From 9a78e317b439d5d6e3fc5d6708cb8834b63b17b6 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sat, 20 Jul 2024 13:12:38 +1200 Subject: [PATCH 22/61] setParts --- packages/effect/src/DateTime.ts | 89 ++++++++++++++++++++++++--- packages/effect/test/DateTime.test.ts | 73 ++++++++++++++++++++-- 2 files changed, 151 insertions(+), 11 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 49d5299e50..0865b07d5a 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -919,6 +919,65 @@ export const getPartAdjusted: { (self: DateTime.WithZone, part: keyof DateTime.Parts): number } = dual(2, (self: DateTime.WithZone, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) +const setParts = (date: Date, parts: Partial): void => { + if (parts.year !== undefined) { + date.setUTCFullYear(parts.year) + } + if (parts.month !== undefined) { + date.setUTCMonth(parts.month - 1) + } + if (parts.day !== undefined) { + date.setUTCDate(parts.day) + } + if (parts.weekDay !== undefined) { + const diff = parts.weekDay - date.getUTCDay() + date.setUTCDate(date.getUTCDate() + diff) + } + if (parts.hours !== undefined) { + date.setUTCHours(parts.hours) + } + if (parts.minutes !== undefined) { + date.setUTCMinutes(parts.minutes) + } + if (parts.seconds !== undefined) { + date.setUTCSeconds(parts.seconds) + } + if (parts.millis !== undefined) { + date.setUTCMilliseconds(parts.millis) + } +} + +/** + * Set the different parts of a `DateTime` as an object. + * + * The Date will be time zone adjusted. + * + * @since 3.6.0 + * @category conversions + */ +export const setPartsAdjusted: { + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone +} = dual( + 2, + (self: DateTime.Input, parts: Partial): DateTime => + mutateAdjusted(self, (date) => setParts(date, parts)) +) + +/** + * Set the different parts of a `DateTime` as an object. + * + * @since 3.6.0 + * @category conversions + */ +export const setPartsUtc: { + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone +} = dual( + 2, + (self: DateTime.Input, parts: Partial): DateTime => mutateUtc(self, (date) => setParts(date, parts)) +) + // ============================================================================= // current time zone // ============================================================================= @@ -1065,10 +1124,10 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { * @since 3.6.0 * @category mapping */ -export const mutate: { - (f: (plainDate: Date) => void): (self: A) => DateTime.PreserveZone - (self: A, f: (plainDate: Date) => void): DateTime.PreserveZone -} = dual(2, (self: DateTime.Input, f: (plainDate: Date) => void): DateTime => { +export const mutateAdjusted: { + (f: (date: Date) => void): (self: A) => DateTime.PreserveZone + (self: A, f: (date: Date) => void): DateTime.PreserveZone +} = dual(2, (self: DateTime.Input, f: (date: Date) => void): DateTime => { const dt = fromInput(self) if (dt._tag === "Utc") { const date = toDateUtc(dt) @@ -1082,6 +1141,22 @@ export const mutate: { return makeWithZone(newAdjustedDate.getTime() - offset, dt.zone) }) +/** + * Modify a `DateTime` by applying a function to the underlying UTC `Date`. + * + * @since 3.6.0 + * @category mapping + */ +export const mutateUtc: { + (f: (date: Date) => void): (self: A) => DateTime.PreserveZone + (self: A, f: (date: Date) => void): DateTime.PreserveZone +} = dual(2, (self: DateTime.Input, f: (date: Date) => void): DateTime => + mapEpochMillis(self, (millis) => { + const date = new Date(millis) + f(date) + return date.getTime() + })) + /** * Transform a `DateTime` by applying a function to the number of milliseconds * since the Unix epoch. @@ -1203,7 +1278,7 @@ export const add: { case "hour": return addMillis(self, amount * 60 * 60 * 1000) } - return mutate(self, (date) => { + return mutateAdjusted(self, (date) => { switch (unit) { case "days": case "day": { @@ -1263,7 +1338,7 @@ export const startOf: { } = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => - mutate(self, (date) => { + mutateAdjusted(self, (date) => { switch (part) { case "day": { date.setUTCHours(0, 0, 0, 0) @@ -1309,7 +1384,7 @@ export const endOf: { } = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => - mutate(self, (date) => { + mutateAdjusted(self, (date) => { switch (part) { case "day": { date.setUTCHours(23, 59, 59, 999) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index ef7f22d2a0..6e08cc4462 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -4,11 +4,11 @@ import { assert, describe, it } from "./utils/extend.js" const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) describe("DateTime", () => { - describe("mutate", () => { + describe("mutateAdjusted", () => { it.effect("should mutate the date", () => Effect.gen(function*() { const now = yield* DateTime.now - const tomorrow = DateTime.mutate(now, (date) => { + const tomorrow = DateTime.mutateAdjusted(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) const diff = DateTime.diffDurationEither(now, tomorrow) @@ -21,12 +21,12 @@ describe("DateTime", () => { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) - const future = DateTime.mutate(now, (date) => { + const future = DateTime.mutateAdjusted(now, (date) => { date.setUTCMonth(date.getUTCMonth() + 6) }) assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-07-01T00:00:00.000Z") - const plusOne = DateTime.mutate(future, (date) => { + const plusOne = DateTime.mutateAdjusted(future, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") @@ -217,6 +217,71 @@ describe("DateTime", () => { }) }) + describe("setPartsUtc", () => { + it("partial", () => { + const date = DateTime.fromParts({ + year: 2024, + month: 12, + day: 25 + }) + assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsUtc(date, { + year: 2023, + month: 1 + }) + assert.strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + + it("ignores time zones", () => { + const date = DateTime.fromParts({ + year: 2024, + month: 12, + day: 25 + }).pipe(DateTime.unsafeSetZoneNamed("Pacific/Auckland")) + assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsUtc(date, { + year: 2023, + month: 1 + }) + assert.strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + }) + + describe("setPartsAdjusted", () => { + it("partial", () => { + const date = DateTime.fromParts({ + year: 2024, + month: 12, + day: 25 + }) + assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsAdjusted(date, { + year: 2023, + month: 1 + }) + assert.strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") + }) + + it("accounts for time zone", () => { + const date = DateTime.fromParts({ + year: 2024, + month: 12, + day: 25 + }).pipe(DateTime.unsafeSetZoneNamed("Pacific/Auckland")) + assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") + + const updated = DateTime.setPartsAdjusted(date, { + year: 2023, + month: 6, + hours: 12 + }) + assert.strictEqual(updated.toJSON(), "2023-06-25T00:00:00.000Z") + }) + }) + describe("formatIso", () => { it("full", () => { const now = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") From f366ac31796d6977f5f0429a2f413e69c09aea50 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sat, 20 Jul 2024 13:52:14 +1200 Subject: [PATCH 23/61] DateTimeUtcFromSelf schema --- packages/schema/src/Schema.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index c95033f020..aaf1ee7ac0 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -12,6 +12,7 @@ import * as chunk_ from "effect/Chunk" import * as config_ from "effect/Config" import * as configError_ from "effect/ConfigError" import * as data_ from "effect/Data" +import * as dateTime from "effect/DateTime" import * as duration_ from "effect/Duration" import * as Effect from "effect/Effect" import * as either_ from "effect/Either" @@ -5919,6 +5920,27 @@ export class DateFromNumber extends transform( { strict: true, decode: (n) => new Date(n), encode: (d) => d.getTime() } ).annotations({ identifier: "DateFromNumber" }) {} +/** + * Describes a schema that represents a `DateTime.Utc` instance. + * + * @category DateTime constructors + * @since 0.67.0 + */ +export class DateTimeUtcFromSelf extends declare( + (u) => dateTime.isDateTime(u) && dateTime.isUtc(u), + { + identifier: "DateTimeUtcFromSelf", + description: "a DateTime.Utc instance", + pretty: (): pretty_.Pretty => (dateTime) => `DateTime.Utc(${JSON.stringify(dateTime)})`, + arbitrary: (): LazyArbitrary => (fc) => + fc.date().map((date) => dateTime.unsafeFromDate(date)), + equivalence: () => dateTime.Equivalence + } +) { + static override annotations: (annotations: Annotations.Schema) => typeof DateTimeUtcFromSelf = + super.annotations +} + /** * @category Option utils * @since 0.67.0 From c98ea795f34153c1eab742d9373138159a13647e Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sat, 20 Jul 2024 15:04:42 +1200 Subject: [PATCH 24/61] DateTimeUtcFromNumber --- packages/effect/src/DateTime.ts | 37 ++++++++++++++--------- packages/schema/src/Schema.ts | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 0865b07d5a..cfa6745b35 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -381,15 +381,30 @@ const _clamp = order.clamp(Order) /** * Create a `DateTime` from the number of milliseconds since the Unix epoch. * + * If the number is invalid, an `IllegalArgumentException` will be thrown. + * * @since 3.6.0 * @category constructors */ -export const fromEpochMillis = (epochMillis: number): DateTime.Utc => { +export const unsafeFromEpochMillis = (epochMillis: number): DateTime.Utc => { + if (Number.isNaN(epochMillis) || !Number.isFinite(epochMillis)) { + throw new IllegalArgumentException("Invalid date") + } const self = Object.create(ProtoUtc) self.epochMillis = epochMillis return self } +/** + * Create a `DateTime` from the number of milliseconds since the Unix epoch. + * + * @since 3.6.0 + * @category constructors + */ +export const fromEpochMillis: (epochMillis: number) => Option.Option = Option.liftThrowable( + unsafeFromEpochMillis +) + /** * Create a `DateTime` from a `Date`. * @@ -398,13 +413,7 @@ export const fromEpochMillis = (epochMillis: number): DateTime.Utc => { * @since 3.6.0 * @category constructors */ -export const unsafeFromDate = (date: Date): DateTime.Utc => { - const epochMillis = date.getTime() - if (isNaN(epochMillis)) { - throw new IllegalArgumentException("Invalid date") - } - return fromEpochMillis(epochMillis) -} +export const unsafeFromDate = (date: Date): DateTime.Utc => unsafeFromEpochMillis(date.getTime()) /** * Create a `DateTime` from one of the following: @@ -419,7 +428,7 @@ export const fromInput = (input: A): DateTime.Preserve if (isDateTime(input)) { return input as DateTime.PreserveZone } else if (typeof input === "number") { - return fromEpochMillis(input) as DateTime.PreserveZone + return unsafeFromEpochMillis(input) as DateTime.PreserveZone } return unsafeFromDate(input) as DateTime.PreserveZone } @@ -453,7 +462,7 @@ export const fromParts = (parts: Partial>): D parts.seconds ?? 0, parts.millis ?? 0 ) - return fromEpochMillis(date.getTime()) + return unsafeFromEpochMillis(date.getTime()) } /** @@ -482,7 +491,7 @@ export const unsafeFromString = (input: string): DateTime.Utc => unsafeFromDate( */ export const now: Effect.Effect = Effect.map( Effect.clock, - (clock) => fromEpochMillis(clock.unsafeCurrentTimeMillis()) + (clock) => unsafeFromEpochMillis(clock.unsafeCurrentTimeMillis()) ) /** @@ -491,7 +500,7 @@ export const now: Effect.Effect = Effect.map( * @since 3.6.0 * @category constructors */ -export const unsafeNow: LazyArg = () => fromEpochMillis(Date.now()) +export const unsafeNow: LazyArg = () => unsafeFromEpochMillis(Date.now()) // ============================================================================= // time zones @@ -1132,7 +1141,7 @@ export const mutateAdjusted: { if (dt._tag === "Utc") { const date = toDateUtc(dt) f(date) - return fromEpochMillis(date.getTime()) + return unsafeFromEpochMillis(date.getTime()) } const adjustedDate = toDateAdjusted(dt) const newAdjustedDate = new Date(adjustedDate.getTime()) @@ -1170,7 +1179,7 @@ export const mapEpochMillis: { } = dual(2, (self: DateTime.Input, f: (millis: number) => number): DateTime => { const dt = fromInput(self) const millis = f(toEpochMillis(dt)) - return dt._tag === "Utc" ? fromEpochMillis(millis) : makeWithZone(millis, dt.zone) + return dt._tag === "Utc" ? unsafeFromEpochMillis(millis) : makeWithZone(millis, dt.zone) }) /** diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index aaf1ee7ac0..ffd973d58d 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5941,6 +5941,58 @@ export class DateTimeUtcFromSelf extends declare( super.annotations } +/** + * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeFromEpochMillis` constructor. + * + * @category DateTime transformations + * @since 0.67.0 + */ +export class DateTimeUtcFromNumber extends transformOrFail( + Number$, + DateTimeUtcFromSelf, + { + strict: true, + decode: (n, _, ast) => { + try { + return ParseResult.succeed(dateTime.unsafeFromEpochMillis(n)) + } catch (_) { + return ParseResult.fail(new ParseResult.Type(ast, n)) + } + }, + encode: (dt) => ParseResult.succeed(dateTime.toEpochMillis(dt)) + } +).annotations({ identifier: "DateTimeUtcFromNumber" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof DateTimeUtcFromNumber = super.annotations +} + +/** + * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeFromString` constructor. + * + * @category DateTime transformations + * @since 0.67.0 + */ +export class DateTimeUtcFromString extends transformOrFail( + String$, + DateTimeUtcFromSelf, + { + strict: true, + decode: (s, _, ast) => { + try { + return ParseResult.succeed(dateTime.unsafeFromString(s)) + } catch (_) { + return ParseResult.fail(new ParseResult.Type(ast, s)) + } + }, + encode: (dt) => ParseResult.succeed(dateTime.formatIso(dt)) + } +).annotations({ identifier: "DateTimeUtcFromString" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof DateTimeUtcFromString = super.annotations +} + /** * @category Option utils * @since 0.67.0 From 65df5033f8b05f6dca3731fa622b4120680a984e Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sat, 20 Jul 2024 19:37:11 +1200 Subject: [PATCH 25/61] add string to input --- packages/effect/src/DateTime.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index cfa6745b35..ea8043ba1c 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -49,7 +49,7 @@ export declare namespace DateTime { * @since 3.6.0 * @category models */ - export type Input = DateTime | Date | number + export type Input = DateTime | Date | number | string /** * @since 3.6.0 @@ -301,7 +301,7 @@ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u * @category guards */ export const isDateTimeInput = (u: unknown): u is DateTime.Input => - isDateTime(u) || u instanceof Date || typeof u === "number" + isDateTime(u) || u instanceof Date || typeof u === "number" || typeof u === "string" /** * @since 3.6.0 @@ -386,14 +386,7 @@ const _clamp = order.clamp(Order) * @since 3.6.0 * @category constructors */ -export const unsafeFromEpochMillis = (epochMillis: number): DateTime.Utc => { - if (Number.isNaN(epochMillis) || !Number.isFinite(epochMillis)) { - throw new IllegalArgumentException("Invalid date") - } - const self = Object.create(ProtoUtc) - self.epochMillis = epochMillis - return self -} +export const unsafeFromEpochMillis = (epochMillis: number): DateTime.Utc => unsafeFromDate(new Date(epochMillis)) /** * Create a `DateTime` from the number of milliseconds since the Unix epoch. @@ -413,7 +406,15 @@ export const fromEpochMillis: (epochMillis: number) => Option.Option unsafeFromEpochMillis(date.getTime()) +export const unsafeFromDate = (date: Date): DateTime.Utc => { + const epochMillis = date.getTime() + if (Number.isNaN(epochMillis)) { + throw new IllegalArgumentException("Invalid date") + } + const self = Object.create(ProtoUtc) + self.epochMillis = epochMillis + return self +} /** * Create a `DateTime` from one of the following: @@ -427,10 +428,10 @@ export const unsafeFromDate = (date: Date): DateTime.Utc => unsafeFromEpochMilli export const fromInput = (input: A): DateTime.PreserveZone => { if (isDateTime(input)) { return input as DateTime.PreserveZone - } else if (typeof input === "number") { - return unsafeFromEpochMillis(input) as DateTime.PreserveZone + } else if (input instanceof Date) { + return unsafeFromDate(input) as DateTime.PreserveZone } - return unsafeFromDate(input) as DateTime.PreserveZone + return unsafeFromDate(new Date(input)) as DateTime.PreserveZone } /** From 5704e2b52984cff0151c21993b305d0d6aae8519 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 22:02:26 +1200 Subject: [PATCH 26/61] move Utc & Zoned to top level --- packages/effect/src/DateTime.ts | 655 +++++++++--------- packages/effect/test/DateTime.test.ts | 42 +- packages/schema/src/Schema.ts | 229 +++++- .../test/Schema/DateTime/DateTime.test.ts | 57 ++ 4 files changed, 607 insertions(+), 376 deletions(-) create mode 100644 packages/schema/test/Schema/DateTime/DateTime.test.ts diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index ea8043ba1c..607e116a55 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -38,7 +38,34 @@ export type TypeId = typeof TypeId * @since 3.6.0 * @category models */ -export type DateTime = DateTime.Utc | DateTime.WithZone +export type DateTime = Utc | Zoned + +/** + * @since 3.6.0 + * @category models + */ +export interface Utc extends DateTime.Proto { + readonly _tag: "Utc" + readonly epochMillis: number + /** @internal */ + partsUtc: DateTime.Parts +} + +/** + * @since 3.6.0 + * @category models + */ +export interface Zoned extends DateTime.Proto { + readonly _tag: "Zoned" + readonly epochMillis: number + readonly zone: TimeZone + /** @internal */ + adjustedEpochMillis?: number + /** @internal */ + partsAdjusted?: DateTime.Parts + /** @internal */ + partsUtc?: DateTime.Parts +} /** * @since 3.6.0 @@ -49,13 +76,13 @@ export declare namespace DateTime { * @since 3.6.0 * @category models */ - export type Input = DateTime | Date | number | string + export type Input = DateTime | Partial | Date | number | string /** * @since 3.6.0 * @category models */ - export type PreserveZone = A extends DateTime.WithZone ? DateTime.WithZone : DateTime.Utc + export type PreserveZone = A extends Zoned ? Zoned : Utc /** * @since 3.6.0 @@ -111,33 +138,6 @@ export declare namespace DateTime { export interface Proto extends Pipeable, Inspectable.Inspectable { readonly [TypeId]: TypeId } - - /** - * @since 3.6.0 - * @category models - */ - export interface Utc extends Proto { - readonly _tag: "Utc" - readonly epochMillis: number - /** @internal */ - partsUtc: Parts - } - - /** - * @since 3.6.0 - * @category models - */ - export interface WithZone extends Proto { - readonly _tag: "WithZone" - readonly epochMillis: number - readonly zone: TimeZone - /** @internal */ - adjustedEpochMillis?: number - /** @internal */ - partsAdjusted?: Parts - /** @internal */ - partsUtc?: Parts - } } /** @@ -206,42 +206,33 @@ const Proto = { const ProtoUtc = { ...Proto, _tag: "Utc", - [Hash.symbol](this: DateTime.Utc) { + [Hash.symbol](this: Utc) { return Hash.cached(this, Hash.number(this.epochMillis)) }, - [Equal.symbol](this: DateTime.Utc, that: unknown) { + [Equal.symbol](this: Utc, that: unknown) { return isDateTime(that) && that._tag === "Utc" && this.epochMillis === that.epochMillis }, - toString(this: DateTime.Utc) { - return Inspectable.format({ - _op: "DateTime", - _tag: this._tag, - epochMillis: this.epochMillis - }) + toString(this: Utc) { + return `DateTime.Utc(${toDateUtc(this).toJSON()})` } } -const ProtoWithZone = { +const ProtoZoned = { ...Proto, - _tag: "WithZone", - [Hash.symbol](this: DateTime.WithZone) { + _tag: "Zoned", + [Hash.symbol](this: Zoned) { return pipe( Hash.number(this.epochMillis), Hash.combine(Hash.hash(this.zone)), Hash.cached(this) ) }, - [Equal.symbol](this: DateTime.WithZone, that: unknown) { - return isDateTime(that) && that._tag === "WithZone" && this.epochMillis === that.epochMillis && + [Equal.symbol](this: Zoned, that: unknown) { + return isDateTime(that) && that._tag === "Zoned" && this.epochMillis === that.epochMillis && Equal.equals(this.zone, that.zone) }, - toString(this: DateTime.WithZone) { - return Inspectable.format({ - _id: "DateTime", - _tag: this._tag, - epochMillis: this.epochMillis, - zone: this.zone - }) + toString(this: Zoned) { + return `DateTime.Zoned(${toDateUtc(this).toJSON()}, ${zoneToString(this.zone)})` } } @@ -300,26 +291,31 @@ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u * @since 3.6.0 * @category guards */ -export const isDateTimeInput = (u: unknown): u is DateTime.Input => - isDateTime(u) || u instanceof Date || typeof u === "number" || typeof u === "string" +export const isTimeZone = (u: unknown): u is TimeZone => Predicate.hasProperty(u, TimeZoneTypeId) /** * @since 3.6.0 * @category guards */ -export const isTimeZone = (u: unknown): u is TimeZone => Predicate.hasProperty(u, TimeZoneTypeId) +export const isTimeZoneOffset = (u: unknown): u is TimeZone.Offset => isTimeZone(u) && u._tag === "Offset" /** * @since 3.6.0 * @category guards */ -export const isUtc = (self: DateTime): self is DateTime.Utc => self._tag === "Utc" +export const isTimeZoneNamed = (u: unknown): u is TimeZone.Named => isTimeZone(u) && u._tag === "Named" /** * @since 3.6.0 * @category guards */ -export const isWithZone = (self: DateTime): self is DateTime.WithZone => self._tag === "WithZone" +export const isUtc = (self: DateTime): self is Utc => self._tag === "Utc" + +/** + * @since 3.6.0 + * @category guards + */ +export const isZoned = (self: DateTime): self is Zoned => self._tag === "Zoned" // ============================================================================= // instances @@ -337,66 +333,27 @@ export const Equivalence: Equivalence_.Equivalence = Equivalence_.make * @since 3.6.0 * @category instances */ -export const OrderInput: order.Order = order.make((a, b) => { - const aMillis = toEpochMillis(a) - const bMillis = toEpochMillis(b) - return aMillis < bMillis ? -1 : aMillis > bMillis ? 1 : 0 -}) - -/** - * @since 3.6.0 - * @category instances - */ -export const Order: order.Order = OrderInput +export const Order: order.Order = order.make((self, that) => + self.epochMillis < that.epochMillis ? -1 : self.epochMillis > that.epochMillis ? 1 : 0 +) /** * @since 3.6.0 */ export const clamp: { - (options: { - minimum: DateTime.Input - maximum: DateTime.Input - }): (self: DateTime.Input) => DateTime - (self: DateTime.Input, options: { - minimum: DateTime.Input - maximum: DateTime.Input - }): DateTime -} = dual( - 2, - (self: DateTime.Input, options: { - minimum: DateTime.Input - maximum: DateTime.Input - }): DateTime => - _clamp(fromInput(self), { - minimum: fromInput(options.minimum), - maximum: fromInput(options.maximum) - }) -) -const _clamp = order.clamp(Order) + (options: { minimum: DateTime; maximum: DateTime }): (self: DateTime) => DateTime + (self: DateTime, options: { minimum: DateTime; maximum: DateTime }): DateTime +} = order.clamp(Order) // ============================================================================= // constructors // ============================================================================= -/** - * Create a `DateTime` from the number of milliseconds since the Unix epoch. - * - * If the number is invalid, an `IllegalArgumentException` will be thrown. - * - * @since 3.6.0 - * @category constructors - */ -export const unsafeFromEpochMillis = (epochMillis: number): DateTime.Utc => unsafeFromDate(new Date(epochMillis)) - -/** - * Create a `DateTime` from the number of milliseconds since the Unix epoch. - * - * @since 3.6.0 - * @category constructors - */ -export const fromEpochMillis: (epochMillis: number) => Option.Option = Option.liftThrowable( - unsafeFromEpochMillis -) +const makeUtc = (epochMillis: number): Utc => { + const self = Object.create(ProtoUtc) + self.epochMillis = epochMillis + return self +} /** * Create a `DateTime` from a `Date`. @@ -406,83 +363,88 @@ export const fromEpochMillis: (epochMillis: number) => Option.Option { +export const unsafeFromDate = (date: Date): Utc => { const epochMillis = date.getTime() if (Number.isNaN(epochMillis)) { throw new IllegalArgumentException("Invalid date") } - const self = Object.create(ProtoUtc) - self.epochMillis = epochMillis - return self + return makeUtc(epochMillis) } /** * Create a `DateTime` from one of the following: + * * - A `DateTime` * - A `Date` instance (invalid dates will throw an `IllegalArgumentException`) * - The `number` of milliseconds since the Unix epoch + * - An object with the parts of a date + * - A `string` that can be parsed by `Date.parse` * * @since 3.6.0 * @category constructors */ -export const fromInput = (input: A): DateTime.PreserveZone => { +export const unsafeMake = (input: A): DateTime.PreserveZone => { if (isDateTime(input)) { return input as DateTime.PreserveZone } else if (input instanceof Date) { return unsafeFromDate(input) as DateTime.PreserveZone + } else if (typeof input === "object") { + const date = new Date(0) + date.setUTCFullYear( + input.year ?? 0, + input.month ? input.month - 1 : 0, + input.day ?? 1 + ) + date.setUTCHours( + input.hours ?? 0, + input.minutes ?? 0, + input.seconds ?? 0, + input.millis ?? 0 + ) + if (input.weekDay !== undefined) { + const diff = input.weekDay - date.getUTCDay() + date.setUTCDate(date.getUTCDate() + diff) + } + return unsafeFromDate(date) as DateTime.PreserveZone } return unsafeFromDate(new Date(input)) as DateTime.PreserveZone } /** - * Safely create a `DateTime` from a `Date`, returning `None` if the `Date` is invalid. - * - * @since 3.6.0 - * @category constructors - */ -export const fromDate: (date: Date) => Option.Option = Option.liftThrowable(unsafeFromDate) - -/** - * Convert a partial `DateTime.Parts` into a `DateTime`. + * Create a `DateTime` from one of the following: * - * If a part is missing, it will default to the smallest possible value. (months will start at 1, days at 1, hours at 0 etc.) + * - A `DateTime` + * - A `Date` instance (invalid dates will throw an `IllegalArgumentException`) + * - The `number` of milliseconds since the Unix epoch + * - An object with the parts of a date + * - A `string` that can be parsed by `Date.parse` * - * @since 3.6.0 - * @category constructors - */ -export const fromParts = (parts: Partial>): DateTime.Utc => { - const date = new Date(0) - date.setUTCFullYear( - parts.year ?? 0, - parts.month ? parts.month - 1 : 0, - parts.day ?? 1 - ) - date.setUTCHours( - parts.hours ?? 0, - parts.minutes ?? 0, - parts.seconds ?? 0, - parts.millis ?? 0 - ) - return unsafeFromEpochMillis(date.getTime()) -} - -/** - * Parse a string into a `DateTime`, using `Date.parse`. + * If the input is invalid, `None` will be returned. * * @since 3.6.0 * @category constructors */ -export const fromString = (input: string): Option.Option => fromDate(new Date(input)) +export const make: (input: A) => Option.Option> = Option + .liftThrowable(unsafeMake) /** - * Parse a string into a `DateTime`, using `Date.parse`. + * Create a `DateTime.Zoned` from a string. * - * If the string is invalid, an `IllegalArgumentException` will be thrown. + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sssZ IANA/TimeZone`. * * @since 3.6.0 * @category constructors */ -export const unsafeFromString = (input: string): DateTime.Utc => unsafeFromDate(new Date(input)) +export const makeZonedFromString = (input: string): Option.Option => { + const parts = input.split(" ") + if (parts.length !== 2) { + return Option.none() + } + return Option.flatMap( + make(parts[0]), + (dt) => Option.map(zoneFromString(parts[1]), (zone) => setZone(dt, zone)) + ) +} /** * Get the current time using the `Clock` service and convert it to a `DateTime`. @@ -490,9 +452,9 @@ export const unsafeFromString = (input: string): DateTime.Utc => unsafeFromDate( * @since 3.6.0 * @category constructors */ -export const now: Effect.Effect = Effect.map( +export const now: Effect.Effect = Effect.map( Effect.clock, - (clock) => unsafeFromEpochMillis(clock.unsafeCurrentTimeMillis()) + (clock) => makeUtc(clock.unsafeCurrentTimeMillis()) ) /** @@ -501,14 +463,14 @@ export const now: Effect.Effect = Effect.map( * @since 3.6.0 * @category constructors */ -export const unsafeNow: LazyArg = () => unsafeFromEpochMillis(Date.now()) +export const unsafeNow: LazyArg = () => makeUtc(Date.now()) // ============================================================================= // time zones // ============================================================================= -const makeWithZone = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): DateTime.WithZone => { - const self = Object.create(ProtoWithZone) +const makeZoned = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): Zoned => { + const self = Object.create(ProtoZoned) self.epochMillis = epochMillis self.zone = zone self.partsUtc = partsUtc @@ -516,17 +478,16 @@ const makeWithZone = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.P } /** - * Set the time zone of a `DateTime`, returning a new `DateTime.WithZone`. + * Set the time zone of a `DateTime`, returning a new `DateTime.Zoned`. * * @since 3.6.0 * @category time zones */ export const setZone: { - (zone: TimeZone): (self: DateTime.Input) => DateTime.WithZone - (self: DateTime.Input, zone: TimeZone): DateTime.WithZone -} = dual(2, (self: DateTime.Input, zone: TimeZone): DateTime.WithZone => { - const dt = fromInput(self) - return makeWithZone(dt.epochMillis, zone, dt.partsUtc) + (zone: TimeZone): (self: DateTime) => Zoned + (self: DateTime, zone: TimeZone): Zoned +} = dual(2, (self: DateTime, zone: TimeZone): Zoned => { + return makeZoned(self.epochMillis, zone, self.partsUtc) }) /** @@ -538,13 +499,9 @@ export const setZone: { * @category time zones */ export const setZoneOffset: { - (offset: number): (self: DateTime.Input) => DateTime.WithZone - (self: DateTime.Input, offset: number): DateTime.WithZone -} = dual(2, (self: DateTime.Input, offset: number): DateTime.WithZone => { - const zone = Object.create(ProtoTimeZoneOffset) - zone.offset = offset - return setZone(self, zone) -}) + (offset: number): (self: DateTime) => Zoned + (self: DateTime, offset: number): Zoned +} = dual(2, (self: DateTime, offset: number): Zoned => setZone(self, zoneMakeOffset(offset))) const validZoneCache = globalValue("effect/DateTime/validZoneCache", () => new Map()) @@ -560,7 +517,7 @@ const formatOptions: Intl.DateTimeFormatOptions = { hourCycle: "h23" } -const makeZoneIntl = (format: Intl.DateTimeFormat): TimeZone.Named => { +const zoneMakeIntl = (format: Intl.DateTimeFormat): TimeZone.Named => { const zoneId = format.resolvedOptions().timeZone if (validZoneCache.has(zoneId)) { return validZoneCache.get(zoneId)! @@ -580,12 +537,12 @@ const makeZoneIntl = (format: Intl.DateTimeFormat): TimeZone.Named => { * @since 3.6.0 * @category time zones */ -export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { +export const zoneUnsafeMakeNamed = (zoneId: string): TimeZone.Named => { if (validZoneCache.has(zoneId)) { return validZoneCache.get(zoneId)! } try { - return makeZoneIntl( + return zoneMakeIntl( new Intl.DateTimeFormat("en-US", { ...formatOptions, timeZone: zoneId @@ -596,6 +553,18 @@ export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { } } +/** + * Create a fixed offset time zone. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneMakeOffset = (offset: number): TimeZone.Offset => { + const zone = Object.create(ProtoTimeZoneOffset) + zone.offset = offset + return zone +} + /** * Create a named time zone from a IANA time zone identifier. If the time zone * is invalid, `None` will be returned. @@ -603,8 +572,8 @@ export const unsafeMakeZoneNamed = (zoneId: string): TimeZone.Named => { * @since 3.6.0 * @category time zones */ -export const makeZoneNamed: (zoneId: string) => Option.Option = Option.liftThrowable( - unsafeMakeZoneNamed +export const zoneMakeNamed: (zoneId: string) => Option.Option = Option.liftThrowable( + zoneUnsafeMakeNamed ) /** @@ -614,9 +583,9 @@ export const makeZoneNamed: (zoneId: string) => Option.Option = * @since 3.6.0 * @category time zones */ -export const makeZoneNamedEffect = (zoneId: string): Effect.Effect => +export const zoneMakeNamedEffect = (zoneId: string): Effect.Effect => Effect.try({ - try: () => unsafeMakeZoneNamed(zoneId), + try: () => zoneUnsafeMakeNamed(zoneId), catch: (e) => e as IllegalArgumentException }) @@ -626,7 +595,34 @@ export const makeZoneNamedEffect = (zoneId: string): Effect.Effect makeZoneIntl(new Intl.DateTimeFormat("en-US", formatOptions)) +export const zoneMakeLocal = (): TimeZone.Named => zoneMakeIntl(new Intl.DateTimeFormat("en-US", formatOptions)) + +/** + * Try parse a TimeZone from a string + * + * @since 3.6.0 + * @category time zones + */ +export const zoneFromString = (zone: string): Option.Option => { + if (zone.startsWith("GMT")) { + const offset = parseGmtOffset(zone) + return offset === null ? Option.none() : Option.some(zoneMakeOffset(offset)) + } + return zoneMakeNamed(zone) +} + +/** + * Format a `TimeZone` as a string. + * + * @since 3.6.0 + * @category time zones + */ +export const zoneToString = (self: TimeZone): string => { + if (self._tag === "Offset") { + return `GMT${offsetToString(self.offset)}` + } + return self.id +} /** * Set the time zone of a `DateTime` from an IANA time zone identifier. If the @@ -636,12 +632,12 @@ export const makeZoneLocal = (): TimeZone.Named => makeZoneIntl(new Intl.DateTim * @category time zones */ export const setZoneNamed: { - (zoneId: string): (self: DateTime.Input) => Option.Option - (self: DateTime.Input, zoneId: string): Option.Option + (zoneId: string): (self: DateTime) => Option.Option + (self: DateTime, zoneId: string): Option.Option } = dual( 2, - (self: DateTime.Input, zoneId: string): Option.Option => - Option.map(makeZoneNamed(zoneId), (zone) => setZone(self, zone)) + (self: DateTime, zoneId: string): Option.Option => + Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone)) ) /** @@ -652,9 +648,9 @@ export const setZoneNamed: { * @category time zones */ export const unsafeSetZoneNamed: { - (zoneId: string): (self: DateTime.Input) => DateTime.WithZone - (self: DateTime.Input, zoneId: string): DateTime.WithZone -} = dual(2, (self: DateTime.Input, zoneId: string): DateTime.WithZone => setZone(self, unsafeMakeZoneNamed(zoneId))) + (zoneId: string): (self: DateTime) => Zoned + (self: DateTime, zoneId: string): Zoned +} = dual(2, (self: DateTime, zoneId: string): Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId))) // ============================================================================= // comparisons @@ -670,9 +666,9 @@ export const unsafeSetZoneNamed: { * @category comparisons */ export const diff: { - (other: DateTime.Input): (self: DateTime.Input) => number - (self: DateTime.Input, other: DateTime.Input): number -} = dual(2, (self: DateTime.Input, other: DateTime.Input): number => toEpochMillis(other) - toEpochMillis(self)) + (other: DateTime): (self: DateTime) => number + (self: DateTime, other: DateTime): number +} = dual(2, (self: DateTime, other: DateTime): number => toEpochMillis(other) - toEpochMillis(self)) /** * Calulate the difference between two `DateTime` values. @@ -687,9 +683,9 @@ export const diff: { * @category constructors */ export const diffDurationEither: { - (other: DateTime.Input): (self: DateTime.Input) => Either.Either - (self: DateTime.Input, other: DateTime.Input): Either.Either -} = dual(2, (self: DateTime.Input, other: DateTime.Input): Either.Either => { + (other: DateTime): (self: DateTime) => Either.Either + (self: DateTime, other: DateTime): Either.Either +} = dual(2, (self: DateTime, other: DateTime): Either.Either => { const diffMillis = diff(self, other) return diffMillis > 0 ? Either.right(Duration.millis(diffMillis)) @@ -703,11 +699,11 @@ export const diffDurationEither: { * @category constructors */ export const diffDuration: { - (other: DateTime.Input): (self: DateTime.Input) => Duration.Duration - (self: DateTime.Input, other: DateTime.Input): Duration.Duration + (other: DateTime): (self: DateTime) => Duration.Duration + (self: DateTime, other: DateTime): Duration.Duration } = dual( 2, - (self: DateTime.Input, other: DateTime.Input): Duration.Duration => Duration.millis(Math.abs(diff(self, other))) + (self: DateTime, other: DateTime): Duration.Duration => Duration.millis(Math.abs(diff(self, other))) ) /** @@ -715,77 +711,75 @@ export const diffDuration: { * @category comparisons */ export const min: { - (that: DateTime.Input): (self: DateTime.Input) => DateTime - (self: DateTime.Input, that: DateTime.Input): DateTime -} = dual(2, (self: DateTime.Input, that: DateTime.Input): DateTime => _min(fromInput(self), fromInput(that))) -const _min = order.min(Order) + (that: DateTime): (self: DateTime) => DateTime + (self: DateTime, that: DateTime): DateTime +} = order.min(Order) /** * @since 3.6.0 * @category comparisons */ export const max: { - (that: DateTime.Input): (self: DateTime.Input) => DateTime - (self: DateTime.Input, that: DateTime.Input): DateTime -} = dual(2, (self: DateTime.Input, that: DateTime.Input): DateTime => _max(fromInput(self), fromInput(that))) -const _max = order.max(Order) + (that: DateTime): (self: DateTime) => DateTime + (self: DateTime, that: DateTime): DateTime +} = order.max(Order) /** * @since 3.6.0 * @category comparisons */ export const greaterThan: { - (that: DateTime.Input): (self: DateTime.Input) => boolean - (self: DateTime.Input, that: DateTime.Input): boolean -} = order.greaterThan(OrderInput) + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = order.greaterThan(Order) /** * @since 3.6.0 * @category comparisons */ export const greaterThanOrEqualTo: { - (that: DateTime.Input): (self: DateTime.Input) => boolean - (self: DateTime.Input, that: DateTime.Input): boolean -} = order.greaterThanOrEqualTo(OrderInput) + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = order.greaterThanOrEqualTo(Order) /** * @since 3.6.0 * @category comparisons */ export const lessThan: { - (that: DateTime.Input): (self: DateTime.Input) => boolean - (self: DateTime.Input, that: DateTime.Input): boolean -} = order.lessThan(OrderInput) + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = order.lessThan(Order) /** * @since 3.6.0 * @category comparisons */ export const lessThanOrEqualTo: { - (that: DateTime.Input): (self: DateTime.Input) => boolean - (self: DateTime.Input, that: DateTime.Input): boolean -} = order.lessThanOrEqualTo(OrderInput) + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = order.lessThanOrEqualTo(Order) /** * @since 3.6.0 * @category comparisons */ export const between: { - (options: { minimum: DateTime.Input; maximum: DateTime.Input }): (self: DateTime.Input) => boolean - (self: DateTime.Input, options: { minimum: DateTime.Input; maximum: DateTime.Input }): boolean -} = order.between(OrderInput) + (options: { minimum: DateTime; maximum: DateTime }): (self: DateTime) => boolean + (self: DateTime, options: { minimum: DateTime; maximum: DateTime }): boolean +} = order.between(Order) /** * @since 3.6.0 * @category comparisons */ -export const isFuture = (self: DateTime.Input): Effect.Effect => Effect.map(now, lessThan(self)) +export const isFuture = (self: DateTime): Effect.Effect => Effect.map(now, lessThan(self)) /** * @since 3.6.0 * @category comparisons */ -export const isPast = (self: DateTime.Input): Effect.Effect => Effect.map(now, greaterThan(self)) +export const isPast = (self: DateTime): Effect.Effect => Effect.map(now, greaterThan(self)) // ============================================================================= // conversions @@ -797,7 +791,7 @@ export const isPast = (self: DateTime.Input): Effect.Effect => Effect.m * @since 3.6.0 * @category conversions */ -export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis(self)) +export const toDateUtc = (self: DateTime): Date => new Date(toEpochMillis(self)) /** * Convert a `DateTime` to a `Date`, applying the time zone first. @@ -805,7 +799,7 @@ export const toDateUtc = (self: DateTime.Input): Date => new Date(toEpochMillis( * @since 3.6.0 * @category conversions */ -export const toDateAdjusted = (self: DateTime.WithZone): Date => { +export const toDateAdjusted = (self: Zoned): Date => { if (self.zone._tag === "Offset") { return new Date(self.epochMillis + self.zone.offset) } else if (self.adjustedEpochMillis !== undefined) { @@ -829,12 +823,12 @@ export const toDateAdjusted = (self: DateTime.WithZone): Date => { } /** - * Calculate the time zone offset of a `DateTime.WithZone` in milliseconds. + * Calculate the time zone offset of a `DateTime.Zoned` in milliseconds. * * @since 3.6.0 * @category conversions */ -export const zoneOffset = (self: DateTime.WithZone): number => { +export const zoneOffset = (self: Zoned): number => { const plainDate = toDateAdjusted(self) return plainDate.getTime() - toEpochMillis(self) } @@ -854,7 +848,15 @@ const offsetToString = (offset: number): string => { * @since 3.6.0 * @category conversions */ -export const zoneOffsetString = (self: DateTime.WithZone): string => offsetToString(zoneOffset(self)) +export const zoneOffsetISOString = (self: Zoned): string => offsetToString(zoneOffset(self)) + +/** + * Format a `DateTime.Zoned` as a string. + * + * @since 3.6.0 + * @category conversions + */ +export const toStringZoned = (self: Zoned): string => `${formatIso(self)} ${zoneToString(self.zone)}` /** * Get the milliseconds since the Unix epoch of a `DateTime`. @@ -862,7 +864,7 @@ export const zoneOffsetString = (self: DateTime.WithZone): string => offsetToStr * @since 3.6.0 * @category conversions */ -export const toEpochMillis = (self: DateTime.Input): number => fromInput(self).epochMillis +export const toEpochMillis = (self: DateTime): number => self.epochMillis /** * Get the different parts of a `DateTime` as an object. @@ -872,7 +874,7 @@ export const toEpochMillis = (self: DateTime.Input): number => fromInput(self).e * @since 3.6.0 * @category conversions */ -export const toPartsAdjusted = (self: DateTime.WithZone): DateTime.Parts => { +export const toPartsAdjusted = (self: Zoned): DateTime.Parts => { if (self.partsAdjusted !== undefined) { return self.partsAdjusted } @@ -888,13 +890,12 @@ export const toPartsAdjusted = (self: DateTime.WithZone): DateTime.Parts => { * @since 3.6.0 * @category conversions */ -export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => { - const dt = fromInput(self) - if (dt.partsUtc !== undefined) { - return dt.partsUtc +export const toPartsUtc = (self: DateTime): DateTime.Parts => { + if (self.partsUtc !== undefined) { + return self.partsUtc } - dt.partsUtc = withUtcDate(self, dateToParts) - return dt.partsUtc + self.partsUtc = withUtcDate(self, dateToParts) + return self.partsUtc } /** @@ -912,9 +913,9 @@ export const toPartsUtc = (self: DateTime.Input): DateTime.Parts => { * assert.strictEqual(year, 2024) */ export const getPartUtc: { - (part: keyof DateTime.Parts): (self: DateTime.Input) => number - (self: DateTime.Input, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime.Input, part: keyof DateTime.Parts): number => toPartsUtc(self)[part]) + (part: keyof DateTime.Parts): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.Parts): number +} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toPartsUtc(self)[part]) /** * Get a part of a `DateTime` as a number. @@ -925,9 +926,9 @@ export const getPartUtc: { * @category conversions */ export const getPartAdjusted: { - (part: keyof DateTime.Parts): (self: DateTime.WithZone) => number - (self: DateTime.WithZone, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime.WithZone, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) + (part: keyof DateTime.Parts): (self: Zoned) => number + (self: Zoned, part: keyof DateTime.Parts): number +} = dual(2, (self: Zoned, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) const setParts = (date: Date, parts: Partial): void => { if (parts.year !== undefined) { @@ -966,12 +967,11 @@ const setParts = (date: Date, parts: Partial): void => { * @category conversions */ export const setPartsAdjusted: { - (parts: Partial): (self: A) => DateTime.PreserveZone - (self: A, parts: Partial): DateTime.PreserveZone + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime.Input, parts: Partial): DateTime => - mutateAdjusted(self, (date) => setParts(date, parts)) + (self: DateTime, parts: Partial): DateTime => mutateAdjusted(self, (date) => setParts(date, parts)) ) /** @@ -981,11 +981,11 @@ export const setPartsAdjusted: { * @category conversions */ export const setPartsUtc: { - (parts: Partial): (self: A) => DateTime.PreserveZone - (self: A, parts: Partial): DateTime.PreserveZone + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime.Input, parts: Partial): DateTime => mutateUtc(self, (date) => setParts(date, parts)) + (self: DateTime, parts: Partial): DateTime => mutateUtc(self, (date) => setParts(date, parts)) ) // ============================================================================= @@ -1008,7 +1008,7 @@ export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZon * @since 3.6.0 * @category current time zone */ -export const setZoneCurrent = (self: DateTime.Input): Effect.Effect => +export const setZoneCurrent = (self: DateTime): Effect.Effect => Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) /** @@ -1035,7 +1035,7 @@ export const withCurrentZone: { export const withCurrentZoneLocal = ( effect: Effect.Effect ): Effect.Effect> => - Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(makeZoneLocal)) + Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(zoneMakeLocal)) /** * Provide the `CurrentTimeZone` to an effect using an IANA time zone @@ -1060,16 +1060,16 @@ export const withCurrentZoneNamed: { effect: Effect.Effect, zone: string ): Effect.Effect> => - Effect.provideServiceEffect(effect, CurrentTimeZone, makeZoneNamedEffect(zone)) + Effect.provideServiceEffect(effect, CurrentTimeZone, zoneMakeNamedEffect(zone)) ) /** - * Get the current time as a `DateTime.WithZone`, using the `CurrentTimeZone`. + * Get the current time as a `DateTime.Zoned`, using the `CurrentTimeZone`. * * @since 3.6.0 * @category current time zone */ -export const nowInCurrentZone: Effect.Effect = Effect.flatMap( +export const nowInCurrentZone: Effect.Effect = Effect.flatMap( now, setZoneCurrent ) @@ -1089,7 +1089,7 @@ export const layerCurrentZone = (zone: TimeZone): Layer.Layer = * @category current time zone */ export const layerCurrentZoneNamed = (zoneId: string): Layer.Layer => - Layer.effect(CurrentTimeZone, makeZoneNamedEffect(zoneId)) + Layer.effect(CurrentTimeZone, zoneMakeNamedEffect(zoneId)) /** * Create a Layer from the given IANA time zone identifier. @@ -1099,7 +1099,7 @@ export const layerCurrentZoneNamed = (zoneId: string): Layer.Layer = Layer.sync( CurrentTimeZone, - makeZoneLocal + zoneMakeLocal ) // ============================================================================= @@ -1110,19 +1110,27 @@ const calculateOffset = (date: Date, zone: TimeZone): number => zone._tag === "Offset" ? zone.offset : calculateNamedOffset(date, zone) const gmtOffsetRegex = /^GMT([+-])(\d{2}):(\d{2})$/ +const parseGmtOffset = (offset: string): number | null => { + const match = gmtOffsetRegex.exec(offset) + if (match === null) { + return null + } + const [, sign, hours, minutes] = match + return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 +} + const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const parts = zone.format.formatToParts(date) const offset = parts[14].value if (offset === "GMT") { return 0 } - const match = gmtOffsetRegex.exec(offset) - if (match === null) { + const result = parseGmtOffset(offset) + if (result === null) { // fallback to using the adjusted date - return zoneOffset(makeWithZone(date.getTime(), zone)) + return zoneOffset(makeZoned(date.getTime(), zone)) } - const [, sign, hours, minutes] = match - return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 + return result } /** @@ -1135,20 +1143,19 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { * @category mapping */ export const mutateAdjusted: { - (f: (date: Date) => void): (self: A) => DateTime.PreserveZone - (self: A, f: (date: Date) => void): DateTime.PreserveZone -} = dual(2, (self: DateTime.Input, f: (date: Date) => void): DateTime => { - const dt = fromInput(self) - if (dt._tag === "Utc") { - const date = toDateUtc(dt) + (f: (date: Date) => void): (self: A) => DateTime.PreserveZone + (self: A, f: (date: Date) => void): DateTime.PreserveZone +} = dual(2, (self: DateTime, f: (date: Date) => void): DateTime => { + if (self._tag === "Utc") { + const date = toDateUtc(self) f(date) - return unsafeFromEpochMillis(date.getTime()) + return makeUtc(date.getTime()) } - const adjustedDate = toDateAdjusted(dt) + const adjustedDate = toDateAdjusted(self) const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) - const offset = calculateOffset(newAdjustedDate, dt.zone) - return makeWithZone(newAdjustedDate.getTime() - offset, dt.zone) + const offset = calculateOffset(newAdjustedDate, self.zone) + return makeZoned(newAdjustedDate.getTime() - offset, self.zone) }) /** @@ -1158,9 +1165,9 @@ export const mutateAdjusted: { * @category mapping */ export const mutateUtc: { - (f: (date: Date) => void): (self: A) => DateTime.PreserveZone - (self: A, f: (date: Date) => void): DateTime.PreserveZone -} = dual(2, (self: DateTime.Input, f: (date: Date) => void): DateTime => + (f: (date: Date) => void): (self: A) => DateTime.PreserveZone + (self: A, f: (date: Date) => void): DateTime.PreserveZone +} = dual(2, (self: DateTime, f: (date: Date) => void): DateTime => mapEpochMillis(self, (millis) => { const date = new Date(millis) f(date) @@ -1175,12 +1182,11 @@ export const mutateUtc: { * @category mapping */ export const mapEpochMillis: { - (f: (millis: number) => number): (self: DateTime.Input) => DateTime - (self: DateTime.Input, f: (millis: number) => number): DateTime -} = dual(2, (self: DateTime.Input, f: (millis: number) => number): DateTime => { - const dt = fromInput(self) - const millis = f(toEpochMillis(dt)) - return dt._tag === "Utc" ? unsafeFromEpochMillis(millis) : makeWithZone(millis, dt.zone) + (f: (millis: number) => number): (self: A) => DateTime.PreserveZone + (self: A, f: (millis: number) => number): DateTime.PreserveZone +} = dual(2, (self: DateTime, f: (millis: number) => number): DateTime => { + const millis = f(toEpochMillis(self)) + return self._tag === "Utc" ? makeUtc(millis) : makeZoned(millis, self.zone) }) /** @@ -1191,9 +1197,9 @@ export const mapEpochMillis: { * @category mapping */ export const withAdjustedDate: { - (f: (date: Date) => A): (self: DateTime.WithZone) => A - (self: DateTime.Input, f: (date: Date) => A): A -} = dual(2, (self: DateTime.WithZone, f: (date: Date) => A): A => f(toDateAdjusted(self))) + (f: (date: Date) => A): (self: Zoned) => A + (self: Zoned, f: (date: Date) => A): A +} = dual(2, (self: Zoned, f: (date: Date) => A): A => f(toDateAdjusted(self))) /** * Using the time zone adjusted `Date`, apply a function to the `Date` and @@ -1203,9 +1209,9 @@ export const withAdjustedDate: { * @category mapping */ export const withUtcDate: { - (f: (date: Date) => A): (self: DateTime.Input) => A - (self: DateTime.Input, f: (date: Date) => A): A -} = dual(2, (self: DateTime.Input, f: (date: Date) => A): A => f(toDateUtc(self))) + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime, f: (date: Date) => A): A => f(toDateUtc(self))) /** * @since 3.6.0 @@ -1213,20 +1219,17 @@ export const withUtcDate: { */ export const match: { (options: { - readonly onUtc: (_: DateTime.Utc) => A - readonly onWithZone: (_: DateTime.WithZone) => B - }): (self: DateTime.Input) => A | B - (self: DateTime.Input, options: { - readonly onUtc: (_: DateTime.Utc) => A - readonly onWithZone: (_: DateTime.WithZone) => B + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B + }): (self: DateTime) => A | B + (self: DateTime, options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B }): A | B -} = dual(2, (self: DateTime.Input, options: { - readonly onUtc: (_: DateTime.Utc) => A - readonly onWithZone: (_: DateTime.WithZone) => B -}): A | B => { - const dt = fromInput(self) - return dt._tag === "Utc" ? options.onUtc(dt) : options.onWithZone(dt) -}) +} = dual(2, (self: DateTime, options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B +}): A | B => self._tag === "Utc" ? options.onUtc(self) : options.onZoned(self)) // ============================================================================= // math @@ -1239,11 +1242,11 @@ export const match: { * @category math */ export const addDuration: { - (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone - (self: A, duration: Duration.DurationInput): DateTime.PreserveZone + (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone + (self: A, duration: Duration.DurationInput): DateTime.PreserveZone } = dual( 2, - (self: DateTime.Input, duration: Duration.DurationInput): DateTime => + (self: DateTime, duration: Duration.DurationInput): DateTime => mapEpochMillis(self, (millis) => millis + Duration.toMillis(duration)) ) @@ -1254,15 +1257,15 @@ export const addDuration: { * @category math */ export const subtractDuration: { - (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone - (self: A, duration: Duration.DurationInput): DateTime.PreserveZone + (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone + (self: A, duration: Duration.DurationInput): DateTime.PreserveZone } = dual( 2, - (self: DateTime.Input, duration: Duration.DurationInput): DateTime => + (self: DateTime, duration: Duration.DurationInput): DateTime => mapEpochMillis(self, (millis) => millis - Duration.toMillis(duration)) ) -const addMillis = (date: DateTime.Input, amount: number): DateTime => mapEpochMillis(date, (millis) => millis + amount) +const addMillis = (date: DateTime, amount: number): DateTime => mapEpochMillis(date, (millis) => millis + amount) /** * Add the given `amount` of `unit`'s to a `DateTime`. @@ -1271,9 +1274,9 @@ const addMillis = (date: DateTime.Input, amount: number): DateTime => mapEpochMi * @category math */ export const add: { - (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone - (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone -} = dual(3, (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime => { + (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone + (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone +} = dual(3, (self: DateTime, amount: number, unit: DateTime.Unit): DateTime => { switch (unit) { case "millis": case "milli": @@ -1325,9 +1328,9 @@ export const add: { * @category math */ export const subtract: { - (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone - (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone -} = dual(3, (self: DateTime.Input, amount: number, unit: DateTime.Unit): DateTime => add(self, -amount, unit)) + (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone + (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone +} = dual(3, (self: DateTime, amount: number, unit: DateTime.Unit): DateTime => add(self, -amount, unit)) /** * Converts a `DateTime` to the start of the given `part`. @@ -1341,11 +1344,11 @@ export const subtract: { export const startOf: { (part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined - }): (self: A) => DateTime.PreserveZone - (self: A, part: DateTime.DatePart, options?: { + }): (self: A) => DateTime.PreserveZone + (self: A, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { +} = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutateAdjusted(self, (date) => { @@ -1387,11 +1390,11 @@ export const startOf: { export const endOf: { (part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined - }): (self: A) => DateTime.PreserveZone - (self: A, part: DateTime.DatePart, options?: { + }): (self: A) => DateTime.PreserveZone + (self: A, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual((args) => typeof args[1] === "string", (self: DateTime.Input, part: DateTime.DatePart, options?: { +} = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutateAdjusted(self, (date) => { @@ -1449,17 +1452,17 @@ export const format: { readonly locale?: string | undefined } | undefined - ): (self: DateTime.Input) => string + ): (self: DateTime) => string ( - self: DateTime.Input, + self: DateTime, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined } | undefined ): string -} = dual((args) => isDateTimeInput(args[0]), ( - self: DateTime.Input, +} = dual((args) => isDateTime(args[0]), ( + self: DateTime, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined @@ -1482,17 +1485,17 @@ export const formatUtc: { readonly locale?: string | undefined } | undefined - ): (self: DateTime.Input) => string + ): (self: DateTime) => string ( - self: DateTime.Input, + self: DateTime, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined } | undefined ): string -} = dual((args) => isDateTimeInput(args[0]), ( - self: DateTime.Input, +} = dual((args) => isDateTime(args[0]), ( + self: DateTime, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined @@ -1511,9 +1514,9 @@ export const formatUtc: { * @category formatting */ export const formatIntl: { - (format: Intl.DateTimeFormat): (self: DateTime.Input) => string - (self: DateTime.Input, format: Intl.DateTimeFormat): string -} = dual(2, (self: DateTime.Input, format: Intl.DateTimeFormat): string => format.format(toEpochMillis(self))) + (format: Intl.DateTimeFormat): (self: DateTime) => string + (self: DateTime, format: Intl.DateTimeFormat): string +} = dual(2, (self: DateTime, format: Intl.DateTimeFormat): string => format.format(toEpochMillis(self))) const intlTimeZone = (self: TimeZone): string => { if (self._tag === "Named") { @@ -1533,16 +1536,16 @@ const intlTimeZone = (self: TimeZone): string => { * @since 3.6.0 * @category formatting */ -export const formatWithZone: { +export const formatZoned: { ( options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined } | undefined - ): (self: DateTime.WithZone) => string + ): (self: Zoned) => string ( - self: DateTime.WithZone, + self: Zoned, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined @@ -1550,7 +1553,7 @@ export const formatWithZone: { | undefined ): string } = dual((args) => isDateTime(args[0]), ( - self: DateTime.WithZone, + self: Zoned, options?: | Intl.DateTimeFormatOptions & { readonly locale?: string | undefined @@ -1576,15 +1579,15 @@ export const formatWithZone: { * @since 3.6.0 * @category formatting */ -export const formatIso = (self: DateTime.Input): string => toDateUtc(self).toISOString() +export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString() /** - * Format a `DateTime.WithZone` as a ISO string with an offset. + * Format a `DateTime.Zoned` as a ISO string with an offset. * * @since 3.6.0 * @category formatting */ -export const formatIsoOffset = (self: DateTime.WithZone): string => { +export const formatIsoOffset = (self: Zoned): string => { const date = toDateAdjusted(self) - return `${date.toISOString().slice(0, 19)}${zoneOffsetString(self)}` + return `${date.toISOString().slice(0, 19)}${zoneOffsetISOString(self)}` } diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 6e08cc4462..a1bf94e275 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -44,11 +44,11 @@ describe("DateTime", () => { })) it("to month with less days", () => { - const jan = DateTime.fromParts({ year: 2023, month: 1, day: 31 }) + const jan = DateTime.unsafeMake({ year: 2023, month: 1, day: 31 }) let feb = DateTime.add(jan, 1, "month") assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") - const mar = DateTime.fromParts({ year: 2023, month: 3, day: 31 }) + const mar = DateTime.unsafeMake({ year: 2023, month: 3, day: 31 }) feb = DateTime.add(mar, -1, "month") assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") }) @@ -73,32 +73,32 @@ describe("DateTime", () => { describe("endOf", () => { it("month", () => { - const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(mar, "month") assert.strictEqual(end.toJSON(), "2024-03-31T23:59:59.999Z") }) it("feb leap year", () => { - const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") + const feb = DateTime.unsafeMake("2024-02-15T12:00:00.000Z") const end = DateTime.endOf(feb, "month") assert.strictEqual(end.toJSON(), "2024-02-29T23:59:59.999Z") }) it("week", () => { - const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") assert.strictEqual(DateTime.getPartUtc(end, "weekDay"), 6) }) it("week last day", () => { - const start = DateTime.unsafeFromString("2024-03-16T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-16T12:00:00.000Z") const end = DateTime.endOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") }) it("week with options", () => { - const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week", { weekStartsOn: 1 }) @@ -119,32 +119,32 @@ describe("DateTime", () => { describe("startOf", () => { it("month", () => { - const mar = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(mar, "month") assert.strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) it("feb leap year", () => { - const feb = DateTime.unsafeFromString("2024-02-15T12:00:00.000Z") + const feb = DateTime.unsafeMake("2024-02-15T12:00:00.000Z") const end = DateTime.startOf(feb, "month") assert.strictEqual(end.toJSON(), "2024-02-01T00:00:00.000Z") }) it("week", () => { - const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") assert.strictEqual(DateTime.getPartUtc(end, "weekDay"), 0) }) it("week first day", () => { - const start = DateTime.unsafeFromString("2024-03-10T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-10T12:00:00.000Z") const end = DateTime.startOf(start, "week") assert.strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") }) it("week with options", () => { - const start = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const start = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week", { weekStartsOn: 1 }) @@ -185,7 +185,7 @@ describe("DateTime", () => { DateTime.withCurrentZoneNamed("Pacific/Auckland") ) assert.strictEqual( - DateTime.formatWithZone(now, { dateStyle: "full", timeStyle: "full" }), + DateTime.formatZoned(now, { dateStyle: "full", timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 PM New Zealand Standard Time" ) })) @@ -195,7 +195,7 @@ describe("DateTime", () => { const now = yield* DateTime.now const formatted = now.pipe( DateTime.setZoneOffset(10 * 60 * 60 * 1000), - DateTime.formatWithZone({ dateStyle: "long", timeStyle: "short" }) + DateTime.formatZoned({ dateStyle: "long", timeStyle: "short" }) ) assert.strictEqual(formatted, "January 1, 1970 at 10:00 AM") })) @@ -203,7 +203,7 @@ describe("DateTime", () => { describe("fromParts", () => { it("partial", () => { - const date = DateTime.fromParts({ + const date = DateTime.unsafeMake({ year: 2024, month: 12, day: 25 @@ -212,14 +212,14 @@ describe("DateTime", () => { }) it("month is set correctly", () => { - const date = DateTime.fromParts({ year: 2024 }) + const date = DateTime.unsafeMake({ year: 2024 }) assert.strictEqual(date.toJSON(), "2024-01-01T00:00:00.000Z") }) }) describe("setPartsUtc", () => { it("partial", () => { - const date = DateTime.fromParts({ + const date = DateTime.unsafeMake({ year: 2024, month: 12, day: 25 @@ -234,7 +234,7 @@ describe("DateTime", () => { }) it("ignores time zones", () => { - const date = DateTime.fromParts({ + const date = DateTime.unsafeMake({ year: 2024, month: 12, day: 25 @@ -251,7 +251,7 @@ describe("DateTime", () => { describe("setPartsAdjusted", () => { it("partial", () => { - const date = DateTime.fromParts({ + const date = DateTime.unsafeMake({ year: 2024, month: 12, day: 25 @@ -266,7 +266,7 @@ describe("DateTime", () => { }) it("accounts for time zone", () => { - const date = DateTime.fromParts({ + const date = DateTime.unsafeMake({ year: 2024, month: 12, day: 25 @@ -284,7 +284,7 @@ describe("DateTime", () => { describe("formatIso", () => { it("full", () => { - const now = DateTime.unsafeFromString("2024-03-15T12:00:00.000Z") + const now = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") assert.strictEqual(DateTime.formatIso(now), "2024-03-15T12:00:00.000Z") }) }) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index ffd973d58d..9daa31a179 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5923,74 +5923,245 @@ export class DateFromNumber extends transform( /** * Describes a schema that represents a `DateTime.Utc` instance. * - * @category DateTime constructors - * @since 0.67.0 + * @category DateTime.Utc constructors + * @since 0.68.26 */ export class DateTimeUtcFromSelf extends declare( (u) => dateTime.isDateTime(u) && dateTime.isUtc(u), { identifier: "DateTimeUtcFromSelf", description: "a DateTime.Utc instance", - pretty: (): pretty_.Pretty => (dateTime) => `DateTime.Utc(${JSON.stringify(dateTime)})`, - arbitrary: (): LazyArbitrary => (fc) => - fc.date().map((date) => dateTime.unsafeFromDate(date)), + pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), + arbitrary: (): LazyArbitrary => (fc) => fc.date().map((date) => dateTime.unsafeFromDate(date)), equivalence: () => dateTime.Equivalence } ) { - static override annotations: (annotations: Annotations.Schema) => typeof DateTimeUtcFromSelf = - super.annotations + static override annotations: (annotations: Annotations.Schema) => typeof DateTimeUtcFromSelf = super + .annotations } +const decodeDateTime = (input: A, _: ParseOptions, ast: AST.AST) => + ParseResult.try({ + try: () => dateTime.unsafeMake(input), + catch: () => new ParseResult.Type(ast, input) + }) + /** * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeFromEpochMillis` constructor. * - * @category DateTime transformations - * @since 0.67.0 + * @category DateTime.Utc transformations + * @since 0.68.26 */ export class DateTimeUtcFromNumber extends transformOrFail( Number$, DateTimeUtcFromSelf, { strict: true, - decode: (n, _, ast) => { - try { - return ParseResult.succeed(dateTime.unsafeFromEpochMillis(n)) - } catch (_) { - return ParseResult.fail(new ParseResult.Type(ast, n)) - } - }, + decode: decodeDateTime, encode: (dt) => ParseResult.succeed(dateTime.toEpochMillis(dt)) } ).annotations({ identifier: "DateTimeUtcFromNumber" }) { static override annotations: ( - annotations: Annotations.Schema + annotations: Annotations.Schema ) => typeof DateTimeUtcFromNumber = super.annotations } /** * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeFromString` constructor. * - * @category DateTime transformations - * @since 0.67.0 + * @category DateTime.Utc transformations + * @since 0.68.26 */ -export class DateTimeUtcFromString extends transformOrFail( +export class DateTimeUtc extends transformOrFail( String$, DateTimeUtcFromSelf, + { + strict: true, + decode: decodeDateTime, + encode: (dt) => ParseResult.succeed(dateTime.formatIso(dt)) + } +).annotations({ identifier: "DateTime.Utc" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof DateTimeUtc = super.annotations +} + +const timeZoneOffsetArbitrary = (): LazyArbitrary => (fc) => + fc.integer({ min: -12 * 60 * 60 * 1000, max: 12 * 60 * 60 * 1000 }).map((offset) => dateTime.zoneMakeOffset(offset)) + +/** + * Describes a schema that represents a `TimeZone.Offset` instance. + * + * @category TimeZone constructors + * @since 0.68.26 + */ +export class TimeZoneOffsetFromSelf extends declare( + dateTime.isTimeZoneOffset, + { + identifier: "TimeZoneOffsetFromSelf", + description: "a TimeZone.Offset instance", + pretty: (): pretty_.Pretty => (zone) => `TimeZone.Offset(${zone.offset})`, + arbitrary: timeZoneOffsetArbitrary + } +) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof TimeZoneOffsetFromSelf = super.annotations +} + +/** + * Defines a schema that attempts to convert a `number` to a `TimeZone.Offset` instance using the `DateTime.makeZoneOffset` constructor. + * + * @category TimeZone transformations + * @since 0.68.26 + */ +export class TimeZoneOffset extends transform( + Number$, + TimeZoneOffsetFromSelf, + { strict: true, decode: (n) => dateTime.zoneMakeOffset(n), encode: (tz) => tz.offset } +).annotations({ identifier: "TimeZoneOffset" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof TimeZoneOffset = super.annotations +} + +const timeZoneNamedArbitrary = (): LazyArbitrary => (fc) => + fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map((id) => dateTime.zoneUnsafeMakeNamed(id)) + +/** + * Describes a schema that represents a `TimeZone.Named` instance. + * + * @category TimeZone constructors + * @since 0.68.26 + */ +export class TimeZoneNamedFromSelf extends declare( + dateTime.isTimeZoneNamed, + { + identifier: "TimeZoneNamedFromSelf", + description: "a TimeZone.Named instance", + pretty: (): pretty_.Pretty => (zone) => `TimeZone.Named(${zone.id})`, + arbitrary: timeZoneNamedArbitrary + } +) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof TimeZoneNamedFromSelf = super.annotations +} + +/** + * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.unsafeMakeZoneNamed` constructor. + * + * @category TimeZone transformations + * @since 0.68.26 + */ +export class TimeZoneNamed extends transformOrFail( + String$, + TimeZoneNamedFromSelf, + { + strict: true, + decode: (s, _, ast) => + ParseResult.try({ + try: () => dateTime.zoneUnsafeMakeNamed(s), + catch: () => new ParseResult.Type(ast, s) + }), + encode: (tz) => ParseResult.succeed(tz.id) + } +).annotations({ identifier: "TimeZoneNamed" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof TimeZoneNamed = super.annotations +} + +/** + * @category api interface + * @since 0.68.26 + */ +export interface TimeZoneFromSelf extends Union<[typeof TimeZoneOffsetFromSelf, typeof TimeZoneNamedFromSelf]> { + annotations(annotations: Annotations.Schema): TimeZoneFromSelf +} + +/** + * @category TimeZone constructors + * @since 0.68.26 + */ +export const TimeZoneFromSelf: TimeZoneFromSelf = Union(TimeZoneOffsetFromSelf, TimeZoneNamedFromSelf) + +/** + * Defines a schema that attempts to convert a `string` to a `TimeZone` using the `DateTime.makeZoneFromString` constructor. + * + * @category TimeZone transformations + * @since 0.68.26 + */ +export class TimeZone extends transformOrFail( + String$, + TimeZoneFromSelf, { strict: true, decode: (s, _, ast) => { - try { - return ParseResult.succeed(dateTime.unsafeFromString(s)) - } catch (_) { - return ParseResult.fail(new ParseResult.Type(ast, s)) - } + const zone = dateTime.zoneFromString(s) + return zone._tag === "Some" ? ParseResult.succeed(zone.value) : ParseResult.fail(new ParseResult.Type(ast, s)) }, - encode: (dt) => ParseResult.succeed(dateTime.formatIso(dt)) + encode: (tz) => ParseResult.succeed(dateTime.zoneToString(tz)) + } +).annotations({ identifier: "TimeZone" }) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof TimeZone = super.annotations +} + +const timeZoneArbitrary: LazyArbitrary = (fc) => + fc.oneof( + timeZoneOffsetArbitrary()(fc), + timeZoneNamedArbitrary()(fc) + ) + +/** + * Describes a schema that represents a `DateTime.Zoned` instance. + * + * @category DateTime.Zoned constructors + * @since 0.68.26 + */ +export class DateTimeZonedFromSelf extends declare( + (u) => dateTime.isDateTime(u) && dateTime.isZoned(u), + { + identifier: "DateTimeZonedFromSelf", + description: "a DateTime.Zoned instance", + pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), + arbitrary: (): LazyArbitrary => (fc) => + fc.date().chain((date) => + timeZoneArbitrary(fc).map((zone) => dateTime.setZone(dateTime.unsafeFromDate(date), zone)) + ), + equivalence: () => dateTime.Equivalence + } +) { + static override annotations: ( + annotations: Annotations.Schema + ) => typeof DateTimeZonedFromSelf = super.annotations +} + +/** + * Defines a schema that attempts to convert a `string` to a `DateTime.Zoned` instance. + * + * @category DateTime.Zoned transformations + * @since 0.68.26 + */ +export class DateTimeZoned extends transformOrFail( + String$, + DateTimeZonedFromSelf, + { + strict: true, + decode: (s, _, ast) => { + const dateTimeZoned = dateTime.makeZonedFromString(s) + return dateTimeZoned._tag === "Some" + ? ParseResult.succeed(dateTimeZoned.value) + : ParseResult.fail(new ParseResult.Type(ast, s)) + }, + encode: (dt) => ParseResult.succeed(dateTime.toStringZoned(dt)) } -).annotations({ identifier: "DateTimeUtcFromString" }) { +).annotations({ identifier: "DateTime.Zoned" }) { static override annotations: ( - annotations: Annotations.Schema - ) => typeof DateTimeUtcFromString = super.annotations + annotations: Annotations.Schema + ) => typeof DateTimeZoned = super.annotations } /** diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts new file mode 100644 index 0000000000..e58d84b5aa --- /dev/null +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -0,0 +1,57 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/TestUtils" +import { DateTime } from "effect" +import { describe, it } from "vitest" + +describe("DateTime.Utc", () => { + const schema = S.DateTimeUtc + + it("property tests", () => { + Util.roundtrip(schema) + }) + + it("decoding", async () => { + await Util.expectDecodeUnknownSuccess( + schema, + "1970-01-01T00:00:00.000Z", + DateTime.unsafeMake(0) + ) + await Util.expectDecodeUnknownFailure( + schema, + "a", + `DateTime.Utc +└─ Transformation process failure + └─ Expected DateTime.Utc, actual "a"` + ) + }) + + it("encoding", async () => { + await Util.expectEncodeSuccess(schema, DateTime.unsafeMake(0), "1970-01-01T00:00:00.000Z") + }) +}) + +describe("DateTime.WithZone", () => { + const schema = S.DateTimeZoned + const dt = DateTime.unsafeMake(0).pipe( + DateTime.unsafeSetZoneNamed("Europe/London") + ) + + it("property tests", () => { + Util.roundtrip(schema) + }) + + it("decoding", async () => { + await Util.expectDecodeUnknownSuccess(schema, "1970-01-01T00:00:00.000Z Europe/London", dt) + await Util.expectDecodeUnknownFailure( + schema, + "a", + `DateTime.WithZone +└─ Transformation process failure + └─ Expected DateTime.WithZone, actual "a"` + ) + }) + + it("encoding", async () => { + await Util.expectEncodeSuccess(schema, dt, "1970-01-01T00:00:00.000Z Europe/London") + }) +}) From 5ca2209a8064ab328f6285fc2c7cb0ada11c4ca2 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 22:07:55 +1200 Subject: [PATCH 27/61] fixes --- packages/effect/src/DateTime.ts | 2 +- packages/schema/test/Schema/DateTime/DateTime.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 607e116a55..4cf031cf9a 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -908,7 +908,7 @@ export const toPartsUtc = (self: DateTime): DateTime.Parts => { * @example * import { DateTime } from "effect" * - * const now = DateTime.fromParts({ year: 2024 }) + * const now = DateTime.unsafeMake({ year: 2024 }) * const year = DateTime.getPartUtc(now, "year") * assert.strictEqual(year, 2024) */ diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index e58d84b5aa..4330f77867 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -30,7 +30,7 @@ describe("DateTime.Utc", () => { }) }) -describe("DateTime.WithZone", () => { +describe("DateTime.Zoned", () => { const schema = S.DateTimeZoned const dt = DateTime.unsafeMake(0).pipe( DateTime.unsafeSetZoneNamed("Europe/London") @@ -45,9 +45,9 @@ describe("DateTime.WithZone", () => { await Util.expectDecodeUnknownFailure( schema, "a", - `DateTime.WithZone + `DateTime.Zoned └─ Transformation process failure - └─ Expected DateTime.WithZone, actual "a"` + └─ Expected DateTime.Zoned, actual "a"` ) }) From 6e5f4221836110e8b2719a900c93b0a55a86c0ba Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 23:09:27 +1200 Subject: [PATCH 28/61] add some examples --- packages/effect/src/DateTime.ts | 325 ++++++++++++++++-- packages/effect/test/DateTime.test.ts | 4 +- packages/schema/src/Schema.ts | 30 +- .../test/Schema/DateTime/DateTime.test.ts | 4 +- 4 files changed, 312 insertions(+), 51 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 4cf031cf9a..cb1056acb6 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -277,6 +277,14 @@ const ProtoTimeZoneOffset = { } } +const makeZonedProto = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): Zoned => { + const self = Object.create(ProtoZoned) + self.epochMillis = epochMillis + self.zone = zone + self.partsUtc = partsUtc + return self +} + // ============================================================================= // guards // ============================================================================= @@ -382,6 +390,17 @@ export const unsafeFromDate = (date: Date): Utc => { * * @since 3.6.0 * @category constructors + * @example + * import { DateTime } from "effect" + * + * // from Date + * DateTime.unsafeMake(new Date()) + * + * // from parts + * DateTime.unsafeMake({ year: 2024 }) + * + * // from string + * DateTime.unsafeMake("2024-01-01") */ export const unsafeMake = (input: A): DateTime.PreserveZone => { if (isDateTime(input)) { @@ -410,6 +429,50 @@ export const unsafeMake = (input: A): DateTime.Preserv return unsafeFromDate(new Date(input)) as DateTime.PreserveZone } +/** + * Create a `DateTime.Zoned` using `DateTime.unsafeMake` and a time zone. + * + * The input is treated as UTC and then the time zone is applied. + * + * @since 3.6.0 + * @category constructors + * @example + * import { DateTime } from "effect" + * + * DateTime.unsafeMakeZoned(new Date(), "Europe/London") + */ +export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string): Zoned => { + const self = unsafeMake(input) + if (typeof zone === "number") { + return setZoneOffset(self, zone) + } + const parsedZone = zoneFromString(zone) + if (Option.isNone(parsedZone)) { + throw new IllegalArgumentException(`Invalid time zone: ${zone}`) + } + return setZone(self, parsedZone.value) +} + +/** + * Create a `DateTime.Zoned` using `DateTime.make` and a time zone. + * + * If the date time input or time zone is invalid, `None` will be returned. + * + * @since 3.6.0 + * @category constructors + * @example + * import { DateTime } from "effect" + * + * DateTime.makeZoned(new Date(), "Europe/London") + */ +export const makeZoned = (input: DateTime.Input, zone: number | string): Option.Option => + Option.flatMap(make(input), (dt) => { + if (typeof zone === "number") { + return Option.some(setZoneOffset(dt, zone)) + } + return Option.map(zoneFromString(zone), (zone) => setZone(dt, zone)) + }) + /** * Create a `DateTime` from one of the following: * @@ -423,6 +486,17 @@ export const unsafeMake = (input: A): DateTime.Preserv * * @since 3.6.0 * @category constructors + * @example + * import { DateTime } from "effect" + * + * // from Date + * DateTime.make(new Date()) + * + * // from parts + * DateTime.make({ year: 2024 }) + * + * // from string + * DateTime.make("2024-01-01") */ export const make: (input: A) => Option.Option> = Option .liftThrowable(unsafeMake) @@ -451,6 +525,12 @@ export const makeZonedFromString = (input: string): Option.Option => { * * @since 3.6.0 * @category constructors + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * }) */ export const now: Effect.Effect = Effect.map( Effect.clock, @@ -469,25 +549,27 @@ export const unsafeNow: LazyArg = () => makeUtc(Date.now()) // time zones // ============================================================================= -const makeZoned = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): Zoned => { - const self = Object.create(ProtoZoned) - self.epochMillis = epochMillis - self.zone = zone - self.partsUtc = partsUtc - return self -} - /** * Set the time zone of a `DateTime`, returning a new `DateTime.Zoned`. * * @since 3.6.0 * @category time zones + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const zone = DateTime.zoneUnsafeMakeNamed("Europe/London") + * + * // set the time zone + * const zoned: DateTime.Zoned = DateTime.setZone(now, zone) + * }) */ export const setZone: { (zone: TimeZone): (self: DateTime) => Zoned (self: DateTime, zone: TimeZone): Zoned } = dual(2, (self: DateTime, zone: TimeZone): Zoned => { - return makeZoned(self.epochMillis, zone, self.partsUtc) + return makeZonedProto(self.epochMillis, zone, self.partsUtc) }) /** @@ -497,6 +579,15 @@ export const setZone: { * * @since 3.6.0 * @category time zones + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * + * // set the offset time zone in milliseconds + * const zoned: DateTime.Zoned = DateTime.setZoneOffset(now, 3 * 60 * 60 * 1000) + * }) */ export const setZoneOffset: { (offset: number): (self: DateTime) => Zoned @@ -597,6 +688,8 @@ export const zoneMakeNamedEffect = (zoneId: string): Effect.Effect zoneMakeIntl(new Intl.DateTimeFormat("en-US", formatOptions)) +const offsetZoneRegex = /^(?:GMT|[+-])/ + /** * Try parse a TimeZone from a string * @@ -604,8 +697,8 @@ export const zoneMakeLocal = (): TimeZone.Named => zoneMakeIntl(new Intl.DateTim * @category time zones */ export const zoneFromString = (zone: string): Option.Option => { - if (zone.startsWith("GMT")) { - const offset = parseGmtOffset(zone) + if (offsetZoneRegex.test(zone)) { + const offset = parseOffset(zone) return offset === null ? Option.none() : Option.some(zoneMakeOffset(offset)) } return zoneMakeNamed(zone) @@ -616,10 +709,18 @@ export const zoneFromString = (zone: string): Option.Option => { * * @since 3.6.0 * @category time zones + * @example + * import { DateTime, Effect } from "effect" + * + * // Outputs "+03:00" + * DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) + * + * // Outputs "Europe/London" + * DateTime.zoneToString(DateTime.zoneUnsafeMakeNamed("Europe/London")) */ export const zoneToString = (self: TimeZone): string => { if (self._tag === "Offset") { - return `GMT${offsetToString(self.offset)}` + return offsetToString(self.offset) } return self.id } @@ -630,6 +731,14 @@ export const zoneToString = (self: TimeZone): string => { * * @since 3.6.0 * @category time zones + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * // set the time zone, returns an Option + * DateTime.setZoneNamed(now, "Europe/London") + * }) */ export const setZoneNamed: { (zoneId: string): (self: DateTime) => Option.Option @@ -646,6 +755,14 @@ export const setZoneNamed: { * * @since 3.6.0 * @category time zones + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * // set the time zone + * DateTime.unsafeSetZoneNamed(now, "Europe/London") + * }) */ export const unsafeSetZoneNamed: { (zoneId: string): (self: DateTime) => Zoned @@ -664,8 +781,18 @@ export const unsafeSetZoneNamed: { * * @since 3.6.0 * @category comparisons + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, 1, "minute") + * + * // returns 60000 + * DateTime.distance(now, other) + * }) */ -export const diff: { +export const distance: { (other: DateTime): (self: DateTime) => number (self: DateTime, other: DateTime): number } = dual(2, (self: DateTime, other: DateTime): number => toEpochMillis(other) - toEpochMillis(self)) @@ -681,12 +808,24 @@ export const diff: { * * @since 3.6.0 * @category constructors + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, 1, "minute") + * + * // returns Either.right(Duration.minutes(1)) + * DateTime.distanceDurationEither(now, other) + * + * // returns Either.left(Duration.minutes(1)) + * DateTime.distanceDurationEither(other, now) + * }) */ -export const diffDurationEither: { +export const distanceDurationEither: { (other: DateTime): (self: DateTime) => Either.Either (self: DateTime, other: DateTime): Either.Either } = dual(2, (self: DateTime, other: DateTime): Either.Either => { - const diffMillis = diff(self, other) + const diffMillis = distance(self, other) return diffMillis > 0 ? Either.right(Duration.millis(diffMillis)) : Either.left(Duration.millis(-diffMillis)) @@ -697,13 +836,22 @@ export const diffDurationEither: { * * @since 3.6.0 * @category constructors + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * const other = DateTime.add(now, 1, "minute") + * + * // returns Duration.minutes(1) + * DateTime.distanceDuration(now, other) + * }) */ -export const diffDuration: { +export const distanceDuration: { (other: DateTime): (self: DateTime) => Duration.Duration (self: DateTime, other: DateTime): Duration.Duration } = dual( 2, - (self: DateTime, other: DateTime): Duration.Duration => Duration.millis(Math.abs(diff(self, other))) + (self: DateTime, other: DateTime): Duration.Duration => Duration.millis(Math.abs(distance(self, other))) ) /** @@ -829,8 +977,8 @@ export const toDateAdjusted = (self: Zoned): Date => { * @category conversions */ export const zoneOffset = (self: Zoned): number => { - const plainDate = toDateAdjusted(self) - return plainDate.getTime() - toEpochMillis(self) + const date = toDateAdjusted(self) + return date.getTime() - toEpochMillis(self) } const offsetToString = (offset: number): string => { @@ -853,6 +1001,8 @@ export const zoneOffsetISOString = (self: Zoned): string => offsetToString(zoneO /** * Format a `DateTime.Zoned` as a string. * + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sssZ IANA/TimeZone`. + * * @since 3.6.0 * @category conversions */ @@ -878,7 +1028,7 @@ export const toPartsAdjusted = (self: Zoned): DateTime.Parts => { if (self.partsAdjusted !== undefined) { return self.partsAdjusted } - self.partsAdjusted = withAdjustedDate(self, dateToParts) + self.partsAdjusted = withDateAdjusted(self, dateToParts) return self.partsAdjusted } @@ -894,7 +1044,7 @@ export const toPartsUtc = (self: DateTime): DateTime.Parts => { if (self.partsUtc !== undefined) { return self.partsUtc } - self.partsUtc = withUtcDate(self, dateToParts) + self.partsUtc = withDateUtc(self, dateToParts) return self.partsUtc } @@ -924,6 +1074,12 @@ export const getPartUtc: { * * @since 3.6.0 * @category conversions + * @example + * import { DateTime } from "effect" + * + * const now = DateTime.unsafeMakeZoned({ year: 2024 }, "Europe/London") + * const year = DateTime.getPartAdjusted(now, "year") + * assert.strictEqual(year, 2024) */ export const getPartAdjusted: { (part: keyof DateTime.Parts): (self: Zoned) => number @@ -1007,6 +1163,15 @@ export class CurrentTimeZone extends Context.Tag("effect/DateTime/CurrentTimeZon * * @since 3.6.0 * @category current time zone + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * const now = yield* DateTime.now + * + * // set the time zone to "Europe/London" + * const zoned = yield* DateTime.setZoneCurrent(now) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) */ export const setZoneCurrent = (self: DateTime): Effect.Effect => Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) @@ -1016,6 +1181,14 @@ export const setZoneCurrent = (self: DateTime): Effect.Effect(effect: Effect.Effect) => Effect.Effect> @@ -1027,10 +1200,18 @@ export const withCurrentZone: { ) /** - * Provide the `CurrentTimeZone` to an effect. + * Provide the `CurrentTimeZone` to an effect, using the system's local time + * zone. * * @since 3.6.0 * @category current time zone + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the system's local time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneLocal) */ export const withCurrentZoneLocal = ( effect: Effect.Effect @@ -1045,6 +1226,13 @@ export const withCurrentZoneLocal = ( * * @since 3.6.0 * @category current time zone + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) */ export const withCurrentZoneNamed: { (zone: string): ( @@ -1068,6 +1256,13 @@ export const withCurrentZoneNamed: { * * @since 3.6.0 * @category current time zone + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) */ export const nowInCurrentZone: Effect.Effect = Effect.flatMap( now, @@ -1109,9 +1304,9 @@ export const layerCurrentZoneLocal: Layer.Layer = Layer.sync( const calculateOffset = (date: Date, zone: TimeZone): number => zone._tag === "Offset" ? zone.offset : calculateNamedOffset(date, zone) -const gmtOffsetRegex = /^GMT([+-])(\d{2}):(\d{2})$/ -const parseGmtOffset = (offset: string): number | null => { - const match = gmtOffsetRegex.exec(offset) +const offsetRegex = /([+-])(\d{2}):(\d{2})$/ +const parseOffset = (offset: string): number | null => { + const match = offsetRegex.exec(offset) if (match === null) { return null } @@ -1125,10 +1320,10 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { if (offset === "GMT") { return 0 } - const result = parseGmtOffset(offset) + const result = parseOffset(offset) if (result === null) { // fallback to using the adjusted date - return zoneOffset(makeZoned(date.getTime(), zone)) + return zoneOffset(makeZonedProto(date.getTime(), zone)) } return result } @@ -1155,7 +1350,7 @@ export const mutateAdjusted: { const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) const offset = calculateOffset(newAdjustedDate, self.zone) - return makeZoned(newAdjustedDate.getTime() - offset, self.zone) + return makeZonedProto(newAdjustedDate.getTime() - offset, self.zone) }) /** @@ -1180,13 +1375,20 @@ export const mutateUtc: { * * @since 3.6.0 * @category mapping + * @example + * import { DateTime } from "effect" + * + * // add 10 milliseconds + * DateTime.unsafeMake(0).pipe( + * DateTime.mapEpochMillis((millis) => millis + 10) + * ) */ export const mapEpochMillis: { (f: (millis: number) => number): (self: A) => DateTime.PreserveZone (self: A, f: (millis: number) => number): DateTime.PreserveZone } = dual(2, (self: DateTime, f: (millis: number) => number): DateTime => { const millis = f(toEpochMillis(self)) - return self._tag === "Utc" ? makeUtc(millis) : makeZoned(millis, self.zone) + return self._tag === "Utc" ? makeUtc(millis) : makeZonedProto(millis, self.zone) }) /** @@ -1195,8 +1397,15 @@ export const mapEpochMillis: { * * @since 3.6.0 * @category mapping + * @example + * import { DateTime } from "effect" + * + * // get the time zone adjusted date in milliseconds + * DateTime.unsafeMakeZoned(0, "Pacific/Auckland").pipe( + * DateTime.withDateAdjusted((date) => date.getTime()) + * ) */ -export const withAdjustedDate: { +export const withDateAdjusted: { (f: (date: Date) => A): (self: Zoned) => A (self: Zoned, f: (date: Date) => A): A } = dual(2, (self: Zoned, f: (date: Date) => A): A => f(toDateAdjusted(self))) @@ -1207,8 +1416,15 @@ export const withAdjustedDate: { * * @since 3.6.0 * @category mapping + * @example + * import { DateTime } from "effect" + * + * // get the date in milliseconds + * DateTime.unsafeMake(0).pipe( + * DateTime.withDateUtc((date) => date.getTime()) + * ) */ -export const withUtcDate: { +export const withDateUtc: { (f: (date: Date) => A): (self: DateTime) => A (self: DateTime, f: (date: Date) => A): A } = dual(2, (self: DateTime, f: (date: Date) => A): A => f(toDateUtc(self))) @@ -1240,6 +1456,13 @@ export const match: { * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.addDuration("5 minutes") + * ) */ export const addDuration: { (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone @@ -1255,6 +1478,13 @@ export const addDuration: { * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.subtractDuration("5 minutes") + * ) */ export const subtractDuration: { (duration: Duration.DurationInput): (self: A) => DateTime.PreserveZone @@ -1270,8 +1500,18 @@ const addMillis = (date: DateTime, amount: number): DateTime => mapEpochMillis(d /** * Add the given `amount` of `unit`'s to a `DateTime`. * + * The time zone is taken into account when adding days, weeks, months, and + * years. + * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.add(5, "minutes") + * ) */ export const add: { (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone @@ -1326,6 +1566,13 @@ export const add: { * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.unsafeMake(0).pipe( + * DateTime.subtract(5, "minutes") + * ) */ export const subtract: { (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone @@ -1340,6 +1587,14 @@ export const subtract: { * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( + * DateTime.startOf("day"), + * DateTime.formatIso + * ) */ export const startOf: { (part: DateTime.DatePart, options?: { @@ -1386,6 +1641,14 @@ export const startOf: { * * @since 3.6.0 * @category math + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T23:59:59.999Z" + * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( + * DateTime.endOf("day"), + * DateTime.formatIso + * ) */ export const endOf: { (part: DateTime.DatePart, options?: { diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index a1bf94e275..8d322ee95c 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -11,7 +11,7 @@ describe("DateTime", () => { const tomorrow = DateTime.mutateAdjusted(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) - const diff = DateTime.diffDurationEither(now, tomorrow) + const diff = DateTime.distanceDurationEither(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) @@ -39,7 +39,7 @@ describe("DateTime", () => { Effect.gen(function*() { const now = yield* DateTime.now const tomorrow = DateTime.add(now, 1, "day") - const diff = DateTime.diffDurationEither(now, tomorrow) + const diff = DateTime.distanceDurationEither(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 9daa31a179..1c0b3f294e 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5947,7 +5947,7 @@ const decodeDateTime = (input: A, _: ParseOpt }) /** - * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeFromEpochMillis` constructor. + * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. * * @category DateTime.Utc transformations * @since 0.68.26 @@ -5967,7 +5967,7 @@ export class DateTimeUtcFromNumber extends transformOrFail( } /** - * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeFromString` constructor. + * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. * * @category DateTime.Utc transformations * @since 0.68.26 @@ -6010,7 +6010,7 @@ export class TimeZoneOffsetFromSelf extends declare( } /** - * Defines a schema that attempts to convert a `number` to a `TimeZone.Offset` instance using the `DateTime.makeZoneOffset` constructor. + * Defines a schema that attempts to convert a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. * * @category TimeZone transformations * @since 0.68.26 @@ -6049,7 +6049,7 @@ export class TimeZoneNamedFromSelf extends declare( } /** - * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.unsafeMakeZoneNamed` constructor. + * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.zoneUnsafeMakeNamed` constructor. * * @category TimeZone transformations * @since 0.68.26 @@ -6087,7 +6087,7 @@ export interface TimeZoneFromSelf extends Union<[typeof TimeZoneOffsetFromSelf, export const TimeZoneFromSelf: TimeZoneFromSelf = Union(TimeZoneOffsetFromSelf, TimeZoneNamedFromSelf) /** - * Defines a schema that attempts to convert a `string` to a `TimeZone` using the `DateTime.makeZoneFromString` constructor. + * Defines a schema that attempts to convert a `string` to a `TimeZone` using the `DateTime.zoneFromString` constructor. * * @category TimeZone transformations * @since 0.68.26 @@ -6097,10 +6097,11 @@ export class TimeZone extends transformOrFail( TimeZoneFromSelf, { strict: true, - decode: (s, _, ast) => { - const zone = dateTime.zoneFromString(s) - return zone._tag === "Some" ? ParseResult.succeed(zone.value) : ParseResult.fail(new ParseResult.Type(ast, s)) - }, + decode: (s, _, ast) => + option_.match(dateTime.zoneFromString(s), { + onNone: () => ParseResult.fail(new ParseResult.Type(ast, s)), + onSome: ParseResult.succeed + }), encode: (tz) => ParseResult.succeed(dateTime.zoneToString(tz)) } ).annotations({ identifier: "TimeZone" }) { @@ -6150,12 +6151,11 @@ export class DateTimeZoned extends transformOrFail( DateTimeZonedFromSelf, { strict: true, - decode: (s, _, ast) => { - const dateTimeZoned = dateTime.makeZonedFromString(s) - return dateTimeZoned._tag === "Some" - ? ParseResult.succeed(dateTimeZoned.value) - : ParseResult.fail(new ParseResult.Type(ast, s)) - }, + decode: (s, _, ast) => + option_.match(dateTime.makeZonedFromString(s), { + onNone: () => ParseResult.fail(new ParseResult.Type(ast, s)), + onSome: ParseResult.succeed + }), encode: (dt) => ParseResult.succeed(dateTime.toStringZoned(dt)) } ).annotations({ identifier: "DateTime.Zoned" }) { diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index 4330f77867..edb1cc4add 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -32,9 +32,7 @@ describe("DateTime.Utc", () => { describe("DateTime.Zoned", () => { const schema = S.DateTimeZoned - const dt = DateTime.unsafeMake(0).pipe( - DateTime.unsafeSetZoneNamed("Europe/London") - ) + const dt = DateTime.unsafeMakeZoned(0, "Europe/London") it("property tests", () => { Util.roundtrip(schema) From cbd5a842994e9995f57104cb76fa4f31edcfa759 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 23:20:45 +1200 Subject: [PATCH 29/61] test decode failure for Zoned --- packages/schema/test/Schema/DateTime/DateTime.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index edb1cc4add..e78d99496b 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -40,6 +40,13 @@ describe("DateTime.Zoned", () => { it("decoding", async () => { await Util.expectDecodeUnknownSuccess(schema, "1970-01-01T00:00:00.000Z Europe/London", dt) + await Util.expectDecodeUnknownFailure( + schema, + "1970-01-01T00:00:00.000Z", + `DateTime.Zoned +└─ Transformation process failure + └─ Expected DateTime.Zoned, actual "1970-01-01T00:00:00.000Z"` + ) await Util.expectDecodeUnknownFailure( schema, "a", From be68b2ca50d6e46b8abdfc17eac40cf1a6c595cb Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 23:34:02 +1200 Subject: [PATCH 30/61] update changesets --- .changeset/clean-trainers-tap.md | 29 +++++++++++++++++++++++++++++ .changeset/red-bottles-cough.md | 5 +++++ 2 files changed, 34 insertions(+) create mode 100644 .changeset/red-bottles-cough.md diff --git a/.changeset/clean-trainers-tap.md b/.changeset/clean-trainers-tap.md index dc6f103b68..bf9650fdf2 100644 --- a/.changeset/clean-trainers-tap.md +++ b/.changeset/clean-trainers-tap.md @@ -3,3 +3,32 @@ --- add DateTime module + +The `DateTime` module provides functionality for working with time, including +support for time zones and daylight saving time. + +It has two main data types: `DateTime.Utc` and `DateTime.Zoned`. + +A `DateTime.Utc` represents a time in Coordinated Universal Time (UTC), and +a `DateTime.Zoned` contains both a UTC timestamp and a time zone. + +There is also a `CurrentTimeZone` service, for setting a time zone contextually. + +```ts +import { DateTime, Effect } from "effect"; + +Effect.gen(function* () { + // Get the current time in the current time zone + const now = yield* DateTime.nowInCurrentZone; + + // Math functions are included + const tomorrow = DateTime.add(now, 1, "day"); + + // Convert to a different time zone + // The UTC portion of the `DateTime` is preserved and only the time zone is + // changed + const sydneyTime = tomorrow.pipe( + DateTime.unsafeSetZoneNamed("Australia/Sydney"), + ); +}).pipe(Effect.withCurrentZoneNamed("America/New_York")); +``` diff --git a/.changeset/red-bottles-cough.md b/.changeset/red-bottles-cough.md new file mode 100644 index 0000000000..cc82c9de7f --- /dev/null +++ b/.changeset/red-bottles-cough.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add schemas for working with the DateTime module From f1c8c22d6d10b84a8c053e30bff999fb3bcc3144 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 23:50:10 +1200 Subject: [PATCH 31/61] use Clock.currentTimeMillis for now --- packages/effect/src/DateTime.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index cb1056acb6..96a0702c46 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -2,6 +2,7 @@ * @since 3.6.0 */ import { IllegalArgumentException } from "./Cause.js" +import * as Clock from "./Clock.js" import * as Context from "./Context.js" import * as Duration from "./Duration.js" import * as Effect from "./Effect.js" @@ -532,10 +533,7 @@ export const makeZonedFromString = (input: string): Option.Option => { * const now = yield* DateTime.now * }) */ -export const now: Effect.Effect = Effect.map( - Effect.clock, - (clock) => makeUtc(clock.unsafeCurrentTimeMillis()) -) +export const now: Effect.Effect = Effect.map(Clock.currentTimeMillis, makeUtc) /** * Get the current time using `Date.now`. From 21ddda7490c6a9e2c43884c64029e4bc864b5fb2 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 20 Jul 2024 23:58:58 +1200 Subject: [PATCH 32/61] wip --- packages/effect/src/DateTime.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 96a0702c46..e41d90a832 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -410,21 +410,7 @@ export const unsafeMake = (input: A): DateTime.Preserv return unsafeFromDate(input) as DateTime.PreserveZone } else if (typeof input === "object") { const date = new Date(0) - date.setUTCFullYear( - input.year ?? 0, - input.month ? input.month - 1 : 0, - input.day ?? 1 - ) - date.setUTCHours( - input.hours ?? 0, - input.minutes ?? 0, - input.seconds ?? 0, - input.millis ?? 0 - ) - if (input.weekDay !== undefined) { - const diff = input.weekDay - date.getUTCDay() - date.setUTCDate(date.getUTCDate() + diff) - } + setParts(date, input) return unsafeFromDate(date) as DateTime.PreserveZone } return unsafeFromDate(new Date(input)) as DateTime.PreserveZone @@ -433,7 +419,7 @@ export const unsafeMake = (input: A): DateTime.Preserv /** * Create a `DateTime.Zoned` using `DateTime.unsafeMake` and a time zone. * - * The input is treated as UTC and then the time zone is applied. + * The input is treated as UTC and then the time zone is attached. * * @since 3.6.0 * @category constructors @@ -457,6 +443,8 @@ export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string): Z /** * Create a `DateTime.Zoned` using `DateTime.make` and a time zone. * + * The input is treated as UTC and then the time zone is attached. + * * If the date time input or time zone is invalid, `None` will be returned. * * @since 3.6.0 From 9103614247b3325270fdda0b26b800479596bd64 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 00:33:06 +1200 Subject: [PATCH 33/61] support TimeZone in makeZoned --- packages/effect/src/DateTime.ts | 23 ++++++++++------------- packages/schema/src/Schema.ts | 4 +--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index e41d90a832..ee66693164 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -198,7 +198,9 @@ const Proto = { pipe() { return pipeArguments(this, arguments) }, - ...Inspectable.BaseProto, + [Inspectable.NodeInspectSymbol](this: DateTime) { + return this.toString() + }, toJSON(this: DateTime) { return toDateUtc(this).toJSON() } @@ -428,9 +430,11 @@ export const unsafeMake = (input: A): DateTime.Preserv * * DateTime.unsafeMakeZoned(new Date(), "Europe/London") */ -export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string): Zoned => { +export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string | TimeZone): Zoned => { const self = unsafeMake(input) - if (typeof zone === "number") { + if (isTimeZone(zone)) { + return setZone(self, zone) + } else if (typeof zone === "number") { return setZoneOffset(self, zone) } const parsedZone = zoneFromString(zone) @@ -454,13 +458,8 @@ export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string): Z * * DateTime.makeZoned(new Date(), "Europe/London") */ -export const makeZoned = (input: DateTime.Input, zone: number | string): Option.Option => - Option.flatMap(make(input), (dt) => { - if (typeof zone === "number") { - return Option.some(setZoneOffset(dt, zone)) - } - return Option.map(zoneFromString(zone), (zone) => setZone(dt, zone)) - }) +export const makeZoned: (input: DateTime.Input, zone: string | number | TimeZone) => Option.Option = Option + .liftThrowable(unsafeMakeZoned) /** * Create a `DateTime` from one of the following: @@ -554,9 +553,7 @@ export const unsafeNow: LazyArg = () => makeUtc(Date.now()) export const setZone: { (zone: TimeZone): (self: DateTime) => Zoned (self: DateTime, zone: TimeZone): Zoned -} = dual(2, (self: DateTime, zone: TimeZone): Zoned => { - return makeZonedProto(self.epochMillis, zone, self.partsUtc) -}) +} = dual(2, (self: DateTime, zone: TimeZone): Zoned => makeZonedProto(self.epochMillis, zone, self.partsUtc)) /** * Add a fixed offset time zone to a `DateTime`. diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 1c0b3f294e..1cd829bd66 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6129,9 +6129,7 @@ export class DateTimeZonedFromSelf extends declare( description: "a DateTime.Zoned instance", pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), arbitrary: (): LazyArbitrary => (fc) => - fc.date().chain((date) => - timeZoneArbitrary(fc).map((zone) => dateTime.setZone(dateTime.unsafeFromDate(date), zone)) - ), + fc.date().chain((date) => timeZoneArbitrary(fc).map((zone) => dateTime.unsafeMakeZoned(date, zone))), equivalence: () => dateTime.Equivalence } ) { From f1300e48d41d15a5b79fbd4e57529d3cffdceb8c Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 09:32:05 +1200 Subject: [PATCH 34/61] Update packages/effect/src/DateTime.ts --- packages/effect/src/DateTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index ee66693164..7e6d13280d 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1270,7 +1270,7 @@ export const layerCurrentZoneNamed = (zoneId: string): Layer.Layer Date: Sun, 21 Jul 2024 09:50:18 +1200 Subject: [PATCH 35/61] rename zoned apis --- packages/effect/src/DateTime.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 7e6d13280d..33c041c6c4 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -959,7 +959,7 @@ export const toDateAdjusted = (self: Zoned): Date => { * @since 3.6.0 * @category conversions */ -export const zoneOffset = (self: Zoned): number => { +export const zonedOffset = (self: Zoned): number => { const date = toDateAdjusted(self) return date.getTime() - toEpochMillis(self) } @@ -979,7 +979,7 @@ const offsetToString = (offset: number): string => { * @since 3.6.0 * @category conversions */ -export const zoneOffsetISOString = (self: Zoned): string => offsetToString(zoneOffset(self)) +export const zonedOffsetISOString = (self: Zoned): string => offsetToString(zonedOffset(self)) /** * Format a `DateTime.Zoned` as a string. @@ -1260,6 +1260,15 @@ export const nowInCurrentZone: Effect.Effect = Ef */ export const layerCurrentZone = (zone: TimeZone): Layer.Layer => Layer.succeed(CurrentTimeZone, zone) +/** + * Create a Layer from the given time zone offset. + * + * @since 3.6.0 + * @category current time zone + */ +export const layerCurrentZoneOffset = (offset: number): Layer.Layer => + Layer.succeed(CurrentTimeZone, zoneMakeOffset(offset)) + /** * Create a Layer from the given IANA time zone identifier. * @@ -1306,7 +1315,7 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const result = parseOffset(offset) if (result === null) { // fallback to using the adjusted date - return zoneOffset(makeZonedProto(date.getTime(), zone)) + return zonedOffset(makeZonedProto(date.getTime(), zone)) } return result } @@ -1835,5 +1844,5 @@ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString */ export const formatIsoOffset = (self: Zoned): string => { const date = toDateAdjusted(self) - return `${date.toISOString().slice(0, 19)}${zoneOffsetISOString(self)}` + return `${date.toISOString().slice(0, 19)}${zonedOffsetISOString(self)}` } From 758dee394457942b28c45a7bbfab558fc0f98613 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sun, 21 Jul 2024 10:48:42 +1200 Subject: [PATCH 36/61] add inputInTimeZone option --- packages/effect/src/DateTime.ts | 41 ++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 33c041c6c4..637423c2b8 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -421,7 +421,9 @@ export const unsafeMake = (input: A): DateTime.Preserv /** * Create a `DateTime.Zoned` using `DateTime.unsafeMake` and a time zone. * - * The input is treated as UTC and then the time zone is attached. + * The input is treated as UTC and then the time zone is attached, unless + * `inputInTimeZone` is set to `true`. In that case, the input is treated as + * already in the time zone. * * @since 3.6.0 * @category constructors @@ -430,18 +432,29 @@ export const unsafeMake = (input: A): DateTime.Preserv * * DateTime.unsafeMakeZoned(new Date(), "Europe/London") */ -export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string | TimeZone): Zoned => { +export const unsafeMakeZoned = (input: DateTime.Input, options: { + readonly timeZone: number | string | TimeZone + readonly inputInTimeZone?: boolean | undefined +}): Zoned => { const self = unsafeMake(input) - if (isTimeZone(zone)) { - return setZone(self, zone) - } else if (typeof zone === "number") { - return setZoneOffset(self, zone) + let zone: TimeZone + if (isTimeZone(options.timeZone)) { + zone = options.timeZone + } else if (typeof options.timeZone === "number") { + zone = zoneMakeOffset(options.timeZone) + } else { + const parsedZone = zoneFromString(options.timeZone) + if (Option.isNone(parsedZone)) { + throw new IllegalArgumentException(`Invalid time zone: ${options.timeZone}`) + } + zone = parsedZone.value } - const parsedZone = zoneFromString(zone) - if (Option.isNone(parsedZone)) { - throw new IllegalArgumentException(`Invalid time zone: ${zone}`) + if (options.inputInTimeZone !== true) { + return makeZonedProto(self.epochMillis, zone, self.partsUtc) } - return setZone(self, parsedZone.value) + const date = toDateUtc(self) + const offset = calculateOffset(date, zone) + return makeZonedProto(date.getTime() - offset, zone) } /** @@ -458,7 +471,13 @@ export const unsafeMakeZoned = (input: DateTime.Input, zone: number | string | T * * DateTime.makeZoned(new Date(), "Europe/London") */ -export const makeZoned: (input: DateTime.Input, zone: string | number | TimeZone) => Option.Option = Option +export const makeZoned: ( + input: DateTime.Input, + options: { + readonly timeZone: number | string | TimeZone + readonly inputInTimeZone?: boolean | undefined + } +) => Option.Option = Option .liftThrowable(unsafeMakeZoned) /** From ea5a8dda3aeee4adeac712b43f4e8a808794fe4a Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sun, 21 Jul 2024 13:50:27 +1200 Subject: [PATCH 37/61] makeZonedFromAdjusted --- packages/effect/src/DateTime.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 637423c2b8..f6b3c7acd0 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -452,9 +452,7 @@ export const unsafeMakeZoned = (input: DateTime.Input, options: { if (options.inputInTimeZone !== true) { return makeZonedProto(self.epochMillis, zone, self.partsUtc) } - const date = toDateUtc(self) - const offset = calculateOffset(date, zone) - return makeZonedProto(date.getTime() - offset, zone) + return makeZonedFromAdjusted(self.epochMillis, zone) } /** @@ -1312,8 +1310,10 @@ export const layerCurrentZoneLocal: Layer.Layer = Layer.sync( // mapping // ============================================================================= -const calculateOffset = (date: Date, zone: TimeZone): number => - zone._tag === "Offset" ? zone.offset : calculateNamedOffset(date, zone) +const makeZonedFromAdjusted = (adjustedMillis: number, zone: TimeZone): Zoned => { + const offset = zone._tag === "Offset" ? zone.offset : calculateNamedOffset(adjustedMillis, zone) + return makeZonedProto(adjustedMillis - offset, zone) +} const offsetRegex = /([+-])(\d{2}):(\d{2})$/ const parseOffset = (offset: string): number | null => { @@ -1325,8 +1325,8 @@ const parseOffset = (offset: string): number | null => { return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 } -const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { - const parts = zone.format.formatToParts(date) +const calculateNamedOffset = (adjustedMillis: number, zone: TimeZone.Named): number => { + const parts = zone.format.formatToParts(adjustedMillis) const offset = parts[14].value if (offset === "GMT") { return 0 @@ -1334,7 +1334,7 @@ const calculateNamedOffset = (date: Date, zone: TimeZone.Named): number => { const result = parseOffset(offset) if (result === null) { // fallback to using the adjusted date - return zonedOffset(makeZonedProto(date.getTime(), zone)) + return zonedOffset(makeZonedProto(adjustedMillis, zone)) } return result } @@ -1360,8 +1360,7 @@ export const mutateAdjusted: { const adjustedDate = toDateAdjusted(self) const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) - const offset = calculateOffset(newAdjustedDate, self.zone) - return makeZonedProto(newAdjustedDate.getTime() - offset, self.zone) + return makeZonedFromAdjusted(newAdjustedDate.getTime(), self.zone) }) /** From ef8eb0fdb622a705cfa0bbfa574f6638a833acfa Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sun, 21 Jul 2024 13:58:36 +1200 Subject: [PATCH 38/61] fix docs --- packages/effect/src/DateTime.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index f6b3c7acd0..2ac0a67e43 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -430,7 +430,7 @@ export const unsafeMake = (input: A): DateTime.Preserv * @example * import { DateTime } from "effect" * - * DateTime.unsafeMakeZoned(new Date(), "Europe/London") + * DateTime.unsafeMakeZoned(new Date(), { timeZone: "Europe/London" }) */ export const unsafeMakeZoned = (input: DateTime.Input, options: { readonly timeZone: number | string | TimeZone @@ -467,7 +467,7 @@ export const unsafeMakeZoned = (input: DateTime.Input, options: { * @example * import { DateTime } from "effect" * - * DateTime.makeZoned(new Date(), "Europe/London") + * DateTime.makeZoned(new Date(), { timeZone: "Europe/London" }) */ export const makeZoned: ( input: DateTime.Input, @@ -1077,7 +1077,7 @@ export const getPartUtc: { * @example * import { DateTime } from "effect" * - * const now = DateTime.unsafeMakeZoned({ year: 2024 }, "Europe/London") + * const now = DateTime.unsafeMakeZoned({ year: 2024 }, { timeZone: "Europe/London" }) * const year = DateTime.getPartAdjusted(now, "year") * assert.strictEqual(year, 2024) */ @@ -1411,7 +1411,7 @@ export const mapEpochMillis: { * import { DateTime } from "effect" * * // get the time zone adjusted date in milliseconds - * DateTime.unsafeMakeZoned(0, "Pacific/Auckland").pipe( + * DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }) * DateTime.withDateAdjusted((date) => date.getTime()) * ) */ From e5219a72950545ed004ea4e4873157e334827fb9 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sun, 21 Jul 2024 14:05:05 +1200 Subject: [PATCH 39/61] fix schema --- packages/effect/src/DateTime.ts | 2 +- packages/schema/src/Schema.ts | 2 +- packages/schema/test/Schema/DateTime/DateTime.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 2ac0a67e43..4279ee4351 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1411,7 +1411,7 @@ export const mapEpochMillis: { * import { DateTime } from "effect" * * // get the time zone adjusted date in milliseconds - * DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }) + * DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }).pipe( * DateTime.withDateAdjusted((date) => date.getTime()) * ) */ diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 1cd829bd66..2374667931 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6129,7 +6129,7 @@ export class DateTimeZonedFromSelf extends declare( description: "a DateTime.Zoned instance", pretty: (): pretty_.Pretty => (dateTime) => dateTime.toString(), arbitrary: (): LazyArbitrary => (fc) => - fc.date().chain((date) => timeZoneArbitrary(fc).map((zone) => dateTime.unsafeMakeZoned(date, zone))), + fc.date().chain((date) => timeZoneArbitrary(fc).map((timeZone) => dateTime.unsafeMakeZoned(date, { timeZone }))), equivalence: () => dateTime.Equivalence } ) { diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index e78d99496b..81bf715d60 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -32,7 +32,7 @@ describe("DateTime.Utc", () => { describe("DateTime.Zoned", () => { const schema = S.DateTimeZoned - const dt = DateTime.unsafeMakeZoned(0, "Europe/London") + const dt = DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }) it("property tests", () => { Util.roundtrip(schema) From c1ed071a0eeb5daf0c5e59d5306e56f2dea4d11c Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 19:57:21 +1200 Subject: [PATCH 40/61] floorTime apis --- packages/effect/src/DateTime.ts | 70 ++++++++++++++++++++++----- packages/effect/test/DateTime.test.ts | 10 ++++ 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 4279ee4351..bbdad151a8 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -996,7 +996,7 @@ const offsetToString = (offset: number): string => { * @since 3.6.0 * @category conversions */ -export const zonedOffsetISOString = (self: Zoned): string => offsetToString(zonedOffset(self)) +export const zonedOffsetIso = (self: Zoned): string => offsetToString(zonedOffset(self)) /** * Format a `DateTime.Zoned` as a string. @@ -1016,6 +1016,17 @@ export const toStringZoned = (self: Zoned): string => `${formatIso(self)} ${zone */ export const toEpochMillis = (self: DateTime): number => self.epochMillis +const dateToParts = (date: Date): DateTime.Parts => ({ + millis: date.getUTCMilliseconds(), + seconds: date.getUTCSeconds(), + minutes: date.getUTCMinutes(), + hours: date.getUTCHours(), + day: date.getUTCDate(), + weekDay: date.getUTCDay(), + month: date.getUTCMonth() + 1, + year: date.getUTCFullYear() +}) + /** * Get the different parts of a `DateTime` as an object. * @@ -1643,6 +1654,50 @@ export const startOf: { } })) +/** + * Remove the time aspect of a `DateTime`. + * + * @since 3.6.0 + * @category math + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( + * DateTime.floorTimeUtc, + * DateTime.formatIso + * ) + */ +export const floorTimeUtc = (self: DateTime): Utc => + withDateUtc(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + +/** + * Remove the time aspect of a `DateTime.WithZone`, first adjusting for the time + * zone. + * + * @since 3.6.0 + * @category math + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMakeZoned("2024-01-01T00:00:00Z", { + * timeZone: "Pacific/Auckland", + * inputInTimeZone: true + * }).pipe( + * DateTime.floorTimeAdjusted, + * DateTime.formatIso + * ) + */ +export const floorTimeAdjusted = (self: Zoned): Utc => + withDateAdjusted(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + /** * Converts a `DateTime` to the end of the given `part`. * @@ -1697,17 +1752,6 @@ export const endOf: { } })) -const dateToParts = (date: Date): DateTime.Parts => ({ - millis: date.getUTCMilliseconds(), - seconds: date.getUTCSeconds(), - minutes: date.getUTCMinutes(), - hours: date.getUTCHours(), - day: date.getUTCDate(), - weekDay: date.getUTCDay(), - month: date.getUTCMonth() + 1, - year: date.getUTCFullYear() -}) - // ============================================================================= // formatting // ============================================================================= @@ -1862,5 +1906,5 @@ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString */ export const formatIsoOffset = (self: Zoned): string => { const date = toDateAdjusted(self) - return `${date.toISOString().slice(0, 19)}${zonedOffsetISOString(self)}` + return `${date.toISOString().slice(0, 19)}${zonedOffsetIso(self)}` } diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 8d322ee95c..cb01193085 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -308,4 +308,14 @@ describe("DateTime", () => { Effect.provide(DateTime.layerCurrentZoneNamed("Pacific/Auckland")) )) }) + + describe("floorTimeAdjusted", () => { + it("removes time", () => { + const dt = DateTime.unsafeMakeZoned("2024-01-01T01:00:00Z", { + timeZone: "Pacific/Auckland", + inputInTimeZone: true + }).pipe(DateTime.floorTimeAdjusted) + assert.strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") + }) + }) }) From 7a82583c2c14f437b36c66587edd967860829959 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 20:04:34 +1200 Subject: [PATCH 41/61] widen input types for adjusted apis --- packages/effect/src/DateTime.ts | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index bbdad151a8..2b687a8c7f 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -939,7 +939,7 @@ export const isPast = (self: DateTime): Effect.Effect => Effect.map(now * @since 3.6.0 * @category conversions */ -export const toDateUtc = (self: DateTime): Date => new Date(toEpochMillis(self)) +export const toDateUtc = (self: DateTime): Date => new Date(self.epochMillis) /** * Convert a `DateTime` to a `Date`, applying the time zone first. @@ -947,8 +947,10 @@ export const toDateUtc = (self: DateTime): Date => new Date(toEpochMillis(self)) * @since 3.6.0 * @category conversions */ -export const toDateAdjusted = (self: Zoned): Date => { - if (self.zone._tag === "Offset") { +export const toDateAdjusted = (self: DateTime): Date => { + if (self._tag === "Utc") { + return new Date(self.epochMillis) + } else if (self.zone._tag === "Offset") { return new Date(self.epochMillis + self.zone.offset) } else if (self.adjustedEpochMillis !== undefined) { return new Date(self.adjustedEpochMillis) @@ -1035,8 +1037,10 @@ const dateToParts = (date: Date): DateTime.Parts => ({ * @since 3.6.0 * @category conversions */ -export const toPartsAdjusted = (self: Zoned): DateTime.Parts => { - if (self.partsAdjusted !== undefined) { +export const toPartsAdjusted = (self: DateTime): DateTime.Parts => { + if (self._tag === "Utc") { + return toPartsUtc(self) + } else if (self.partsAdjusted !== undefined) { return self.partsAdjusted } self.partsAdjusted = withDateAdjusted(self, dateToParts) @@ -1093,9 +1097,9 @@ export const getPartUtc: { * assert.strictEqual(year, 2024) */ export const getPartAdjusted: { - (part: keyof DateTime.Parts): (self: Zoned) => number - (self: Zoned, part: keyof DateTime.Parts): number -} = dual(2, (self: Zoned, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) + (part: keyof DateTime.Parts): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.Parts): number +} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) const setParts = (date: Date, parts: Partial): void => { if (parts.year !== undefined) { @@ -1427,9 +1431,9 @@ export const mapEpochMillis: { * ) */ export const withDateAdjusted: { - (f: (date: Date) => A): (self: Zoned) => A - (self: Zoned, f: (date: Date) => A): A -} = dual(2, (self: Zoned, f: (date: Date) => A): A => f(toDateAdjusted(self))) + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime, f: (date: Date) => A): A => f(toDateAdjusted(self))) /** * Using the time zone adjusted `Date`, apply a function to the `Date` and @@ -1675,7 +1679,7 @@ export const floorTimeUtc = (self: DateTime): Utc => }) /** - * Remove the time aspect of a `DateTime.WithZone`, first adjusting for the time + * Remove the time aspect of a `DateTime.Zoned`, first adjusting for the time * zone. * * @since 3.6.0 @@ -1684,7 +1688,7 @@ export const floorTimeUtc = (self: DateTime): Utc => * import { DateTime } from "effect" * * // returns "2024-01-01T00:00:00Z" - * DateTime.unsafeMakeZoned("2024-01-01T00:00:00Z", { + * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { * timeZone: "Pacific/Auckland", * inputInTimeZone: true * }).pipe( @@ -1692,7 +1696,7 @@ export const floorTimeUtc = (self: DateTime): Utc => * DateTime.formatIso * ) */ -export const floorTimeAdjusted = (self: Zoned): Utc => +export const floorTimeAdjusted = (self: DateTime): Utc => withDateAdjusted(self, (date) => { date.setUTCHours(0, 0, 0, 0) return makeUtc(date.getTime()) @@ -1904,7 +1908,7 @@ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString * @since 3.6.0 * @category formatting */ -export const formatIsoOffset = (self: Zoned): string => { +export const formatIsoOffset = (self: DateTime): string => { const date = toDateAdjusted(self) - return `${date.toISOString().slice(0, 19)}${zonedOffsetIso(self)}` + return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, 19)}${zonedOffsetIso(self)}` } From 5687565f2edbc5a07ff5d0053c0f2bf64ffdd98f Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 20:48:35 +1200 Subject: [PATCH 42/61] 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 2b687a8c7f..f417d7e7dc 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 cb01193085..40488a7cfb 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 2374667931..6f48ff16d3 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6154,7 +6154,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 81bf715d60..07194588a1 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]") }) }) From 9226a8dd286d47c7ec65251a0084079684ea4806 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 20:58:48 +1200 Subject: [PATCH 43/61] fix tests --- packages/effect/test/DateTime.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 40488a7cfb..f81edddc1f 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -295,7 +295,7 @@ describe("DateTime", () => { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) - assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00+12:00") + assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") })) }) @@ -303,7 +303,7 @@ describe("DateTime", () => { it.effect("correctly adds offset", () => Effect.gen(function*() { const now = yield* DateTime.nowInCurrentZone - assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00+12:00") + assert.strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") }).pipe( Effect.provide(DateTime.layerCurrentZoneNamed("Pacific/Auckland")) )) From 42347d15e3fc0ab8c3083b824ce77c4d488d912f Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 21:01:41 +1200 Subject: [PATCH 44/61] remove schema static annotations --- packages/schema/src/Schema.ts | 59 ++++++----------------------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 6f48ff16d3..3aca45707b 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5935,10 +5935,7 @@ export class DateTimeUtcFromSelf extends declare( arbitrary: (): LazyArbitrary => (fc) => fc.date().map((date) => dateTime.unsafeFromDate(date)), equivalence: () => dateTime.Equivalence } -) { - static override annotations: (annotations: Annotations.Schema) => typeof DateTimeUtcFromSelf = super - .annotations -} +) {} const decodeDateTime = (input: A, _: ParseOptions, ast: AST.AST) => ParseResult.try({ @@ -5960,11 +5957,7 @@ export class DateTimeUtcFromNumber extends transformOrFail( decode: decodeDateTime, encode: (dt) => ParseResult.succeed(dateTime.toEpochMillis(dt)) } -).annotations({ identifier: "DateTimeUtcFromNumber" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof DateTimeUtcFromNumber = super.annotations -} +).annotations({ identifier: "DateTimeUtcFromNumber" }) {} /** * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. @@ -5980,11 +5973,7 @@ export class DateTimeUtc extends transformOrFail( decode: decodeDateTime, encode: (dt) => ParseResult.succeed(dateTime.formatIso(dt)) } -).annotations({ identifier: "DateTime.Utc" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof DateTimeUtc = super.annotations -} +).annotations({ identifier: "DateTime.Utc" }) {} const timeZoneOffsetArbitrary = (): LazyArbitrary => (fc) => fc.integer({ min: -12 * 60 * 60 * 1000, max: 12 * 60 * 60 * 1000 }).map((offset) => dateTime.zoneMakeOffset(offset)) @@ -6003,11 +5992,7 @@ export class TimeZoneOffsetFromSelf extends declare( pretty: (): pretty_.Pretty => (zone) => `TimeZone.Offset(${zone.offset})`, arbitrary: timeZoneOffsetArbitrary } -) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof TimeZoneOffsetFromSelf = super.annotations -} +) {} /** * Defines a schema that attempts to convert a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. @@ -6019,11 +6004,7 @@ export class TimeZoneOffset extends transform( Number$, TimeZoneOffsetFromSelf, { strict: true, decode: (n) => dateTime.zoneMakeOffset(n), encode: (tz) => tz.offset } -).annotations({ identifier: "TimeZoneOffset" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof TimeZoneOffset = super.annotations -} +).annotations({ identifier: "TimeZoneOffset" }) {} const timeZoneNamedArbitrary = (): LazyArbitrary => (fc) => fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map((id) => dateTime.zoneUnsafeMakeNamed(id)) @@ -6042,11 +6023,7 @@ export class TimeZoneNamedFromSelf extends declare( pretty: (): pretty_.Pretty => (zone) => `TimeZone.Named(${zone.id})`, arbitrary: timeZoneNamedArbitrary } -) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof TimeZoneNamedFromSelf = super.annotations -} +) {} /** * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.zoneUnsafeMakeNamed` constructor. @@ -6066,11 +6043,7 @@ export class TimeZoneNamed extends transformOrFail( }), encode: (tz) => ParseResult.succeed(tz.id) } -).annotations({ identifier: "TimeZoneNamed" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof TimeZoneNamed = super.annotations -} +).annotations({ identifier: "TimeZoneNamed" }) {} /** * @category api interface @@ -6104,11 +6077,7 @@ export class TimeZone extends transformOrFail( }), encode: (tz) => ParseResult.succeed(dateTime.zoneToString(tz)) } -).annotations({ identifier: "TimeZone" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof TimeZone = super.annotations -} +).annotations({ identifier: "TimeZone" }) {} const timeZoneArbitrary: LazyArbitrary = (fc) => fc.oneof( @@ -6132,11 +6101,7 @@ export class DateTimeZonedFromSelf extends declare( fc.date().chain((date) => timeZoneArbitrary(fc).map((timeZone) => dateTime.unsafeMakeZoned(date, { timeZone }))), equivalence: () => dateTime.Equivalence } -) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof DateTimeZonedFromSelf = super.annotations -} +) {} /** * Defines a schema that attempts to convert a `string` to a `DateTime.Zoned` instance. @@ -6156,11 +6121,7 @@ export class DateTimeZoned extends transformOrFail( }), encode: (dt) => ParseResult.succeed(dateTime.zonedToString(dt)) } -).annotations({ identifier: "DateTime.Zoned" }) { - static override annotations: ( - annotations: Annotations.Schema - ) => typeof DateTimeZoned = super.annotations -} +).annotations({ identifier: "DateTime.Zoned" }) {} /** * @category Option utils From dfc064da7e8eaf34e9aa7e218cbb1366aec6b6fb Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 21:14:36 +1200 Subject: [PATCH 45/61] toString & schema tweaks --- packages/effect/src/DateTime.ts | 10 +++++++++- packages/schema/src/Schema.ts | 14 +++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index f417d7e7dc..c45f40eba5 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -241,7 +241,9 @@ const ProtoZoned = { const ProtoTimeZone = { [TimeZoneTypeId]: TimeZoneTypeId, - ...Inspectable.BaseProto + [Inspectable.NodeInspectSymbol](this: TimeZone) { + return this.toString() + } } const ProtoTimeZoneNamed = { @@ -253,6 +255,9 @@ const ProtoTimeZoneNamed = { [Equal.symbol](this: TimeZone.Named, that: unknown) { return isTimeZone(that) && that._tag === "Named" && this.id === that.id }, + toString(this: TimeZone.Named) { + return `TimeZone.Named(${this.id})` + }, toJSON(this: TimeZone.Named) { return { _id: "TimeZone", @@ -271,6 +276,9 @@ const ProtoTimeZoneOffset = { [Equal.symbol](this: TimeZone.Offset, that: unknown) { return isTimeZone(that) && that._tag === "Offset" && this.offset === that.offset }, + toString(this: TimeZone.Offset) { + return `TimeZone.Offset(${offsetToString(this.offset)})` + }, toJSON(this: TimeZone.Offset) { return { _id: "TimeZone", diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 3aca45707b..21b135ca33 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5989,13 +5989,13 @@ export class TimeZoneOffsetFromSelf extends declare( { identifier: "TimeZoneOffsetFromSelf", description: "a TimeZone.Offset instance", - pretty: (): pretty_.Pretty => (zone) => `TimeZone.Offset(${zone.offset})`, + pretty: (): pretty_.Pretty => (zone) => zone.toString(), arbitrary: timeZoneOffsetArbitrary } ) {} /** - * Defines a schema that attempts to convert a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. + * Defines a schema that converts a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. * * @category TimeZone transformations * @since 0.68.26 @@ -6003,11 +6003,11 @@ export class TimeZoneOffsetFromSelf extends declare( export class TimeZoneOffset extends transform( Number$, TimeZoneOffsetFromSelf, - { strict: true, decode: (n) => dateTime.zoneMakeOffset(n), encode: (tz) => tz.offset } -).annotations({ identifier: "TimeZoneOffset" }) {} + { strict: true, decode: dateTime.zoneMakeOffset, encode: (tz) => tz.offset } +).annotations({ identifier: "TimeZone.Offset" }) {} const timeZoneNamedArbitrary = (): LazyArbitrary => (fc) => - fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map((id) => dateTime.zoneUnsafeMakeNamed(id)) + fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map(dateTime.zoneUnsafeMakeNamed) /** * Describes a schema that represents a `TimeZone.Named` instance. @@ -6020,7 +6020,7 @@ export class TimeZoneNamedFromSelf extends declare( { identifier: "TimeZoneNamedFromSelf", description: "a TimeZone.Named instance", - pretty: (): pretty_.Pretty => (zone) => `TimeZone.Named(${zone.id})`, + pretty: (): pretty_.Pretty => (zone) => zone.toString(), arbitrary: timeZoneNamedArbitrary } ) {} @@ -6043,7 +6043,7 @@ export class TimeZoneNamed extends transformOrFail( }), encode: (tz) => ParseResult.succeed(tz.id) } -).annotations({ identifier: "TimeZoneNamed" }) {} +).annotations({ identifier: "TimeZone.Named" }) {} /** * @category api interface From 11b829eb7eb1ad71580b2a3a887428fe02bd638c Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 21:35:30 +1200 Subject: [PATCH 46/61] remove periods from schema identifier's --- packages/schema/src/Schema.ts | 8 ++++---- .../schema/test/Schema/DateTime/DateTime.test.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 21b135ca33..b360f037a9 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5973,7 +5973,7 @@ export class DateTimeUtc extends transformOrFail( decode: decodeDateTime, encode: (dt) => ParseResult.succeed(dateTime.formatIso(dt)) } -).annotations({ identifier: "DateTime.Utc" }) {} +).annotations({ identifier: "DateTimeUtc" }) {} const timeZoneOffsetArbitrary = (): LazyArbitrary => (fc) => fc.integer({ min: -12 * 60 * 60 * 1000, max: 12 * 60 * 60 * 1000 }).map((offset) => dateTime.zoneMakeOffset(offset)) @@ -6004,7 +6004,7 @@ export class TimeZoneOffset extends transform( Number$, TimeZoneOffsetFromSelf, { strict: true, decode: dateTime.zoneMakeOffset, encode: (tz) => tz.offset } -).annotations({ identifier: "TimeZone.Offset" }) {} +).annotations({ identifier: "TimeZoneOffset" }) {} const timeZoneNamedArbitrary = (): LazyArbitrary => (fc) => fc.constantFrom(...Intl.supportedValuesOf("timeZone")).map(dateTime.zoneUnsafeMakeNamed) @@ -6043,7 +6043,7 @@ export class TimeZoneNamed extends transformOrFail( }), encode: (tz) => ParseResult.succeed(tz.id) } -).annotations({ identifier: "TimeZone.Named" }) {} +).annotations({ identifier: "TimeZoneNamed" }) {} /** * @category api interface @@ -6121,7 +6121,7 @@ export class DateTimeZoned extends transformOrFail( }), encode: (dt) => ParseResult.succeed(dateTime.zonedToString(dt)) } -).annotations({ identifier: "DateTime.Zoned" }) {} +).annotations({ identifier: "DateTimeZoned" }) {} /** * @category Option utils diff --git a/packages/schema/test/Schema/DateTime/DateTime.test.ts b/packages/schema/test/Schema/DateTime/DateTime.test.ts index 07194588a1..75138c2d2b 100644 --- a/packages/schema/test/Schema/DateTime/DateTime.test.ts +++ b/packages/schema/test/Schema/DateTime/DateTime.test.ts @@ -19,9 +19,9 @@ describe("DateTime.Utc", () => { await Util.expectDecodeUnknownFailure( schema, "a", - `DateTime.Utc + `DateTimeUtc └─ Transformation process failure - └─ Expected DateTime.Utc, actual "a"` + └─ Expected DateTimeUtc, actual "a"` ) }) @@ -43,16 +43,16 @@ describe("DateTime.Zoned", () => { await Util.expectDecodeUnknownFailure( schema, "1970-01-01T00:00:00.000Z", - `DateTime.Zoned + `DateTimeZoned └─ Transformation process failure - └─ Expected DateTime.Zoned, actual "1970-01-01T00:00:00.000Z"` + └─ Expected DateTimeZoned, actual "1970-01-01T00:00:00.000Z"` ) await Util.expectDecodeUnknownFailure( schema, "a", - `DateTime.Zoned + `DateTimeZoned └─ Transformation process failure - └─ Expected DateTime.Zoned, actual "a"` + └─ Expected DateTimeZoned, actual "a"` ) }) From 27300539739192f4c6359dcd704569b03edc1c91 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 21 Jul 2024 22:01:36 +1200 Subject: [PATCH 47/61] adjust zonedStringRegex --- packages/effect/src/DateTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index c45f40eba5..f4d0697974 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -514,7 +514,7 @@ export const makeZoned: ( export const make: (input: A) => Option.Option> = Option .liftThrowable(unsafeMake) -const zonedStringRegex = /^(.{24,35})\[(.+)\]$/ +const zonedStringRegex = /^(.{17,35})\[(.+)\]$/ /** * Create a `DateTime.Zoned` from a string. From 036bc95bf7863b1b484326a363f543b67134efee Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 09:28:21 +1200 Subject: [PATCH 48/61] remove Adjusted suffix --- packages/effect/src/DateTime.ts | 114 +++++++++++--------------- packages/effect/test/DateTime.test.ts | 30 +++---- 2 files changed, 62 insertions(+), 82 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index f4d0697974..1d7d09fbea 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -235,7 +235,7 @@ const ProtoZoned = { Equal.equals(this.zone, that.zone) }, toString(this: Zoned) { - return `DateTime.Zoned(${toDateUtc(this).toJSON()}, ${zoneToString(this.zone)})` + return `DateTime.Zoned(${zonedToString(this)})` } } @@ -420,7 +420,7 @@ export const unsafeMake = (input: A): DateTime.Preserv return unsafeFromDate(input) as DateTime.PreserveZone } else if (typeof input === "object") { const date = new Date(0) - setParts(date, input) + setPartsDate(date, input) return unsafeFromDate(date) as DateTime.PreserveZone } return unsafeFromDate(new Date(input)) as DateTime.PreserveZone @@ -956,7 +956,7 @@ export const toDateUtc = (self: DateTime): Date => new Date(self.epochMillis) * @since 3.6.0 * @category conversions */ -export const toDateAdjusted = (self: DateTime): Date => { +export const toDate = (self: DateTime): Date => { if (self._tag === "Utc") { return new Date(self.epochMillis) } else if (self.zone._tag === "Offset") { @@ -988,7 +988,7 @@ export const toDateAdjusted = (self: DateTime): Date => { * @category conversions */ export const zonedOffset = (self: Zoned): number => { - const date = toDateAdjusted(self) + const date = toDate(self) return date.getTime() - toEpochMillis(self) } @@ -1047,13 +1047,13 @@ const dateToParts = (date: Date): DateTime.Parts => ({ * @since 3.6.0 * @category conversions */ -export const toPartsAdjusted = (self: DateTime): DateTime.Parts => { +export const toParts = (self: DateTime): DateTime.Parts => { if (self._tag === "Utc") { return toPartsUtc(self) } else if (self.partsAdjusted !== undefined) { return self.partsAdjusted } - self.partsAdjusted = withDateAdjusted(self, dateToParts) + self.partsAdjusted = withDate(self, dateToParts) return self.partsAdjusted } @@ -1103,15 +1103,15 @@ export const getPartUtc: { * import { DateTime } from "effect" * * const now = DateTime.unsafeMakeZoned({ year: 2024 }, { timeZone: "Europe/London" }) - * const year = DateTime.getPartAdjusted(now, "year") + * const year = DateTime.getPart(now, "year") * assert.strictEqual(year, 2024) */ -export const getPartAdjusted: { +export const getPart: { (part: keyof DateTime.Parts): (self: DateTime) => number (self: DateTime, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toPartsAdjusted(self)[part]) +} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toParts(self)[part]) -const setParts = (date: Date, parts: Partial): void => { +const setPartsDate = (date: Date, parts: Partial): void => { if (parts.year !== undefined) { date.setUTCFullYear(parts.year) } @@ -1147,12 +1147,12 @@ const setParts = (date: Date, parts: Partial): void => { * @since 3.6.0 * @category conversions */ -export const setPartsAdjusted: { +export const setParts: { (parts: Partial): (self: A) => DateTime.PreserveZone (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime, parts: Partial): DateTime => mutateAdjusted(self, (date) => setParts(date, parts)) + (self: DateTime, parts: Partial): DateTime => mutate(self, (date) => setPartsDate(date, parts)) ) /** @@ -1166,9 +1166,33 @@ export const setPartsUtc: { (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime, parts: Partial): DateTime => mutateUtc(self, (date) => setParts(date, parts)) + (self: DateTime, parts: Partial): DateTime => mutateUtc(self, (date) => setPartsDate(date, parts)) ) +/** + * Remove the time aspect of a `DateTime`, first adjusting for the time + * zone. It will return a `DateTime.Utc` only containing the date. + * + * @since 3.6.0 + * @category conversions + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { + * timeZone: "Pacific/Auckland", + * inputInTimeZone: true + * }).pipe( + * DateTime.removeTime, + * DateTime.formatIso + * ) + */ +export const removeTime = (self: DateTime): Utc => + withDate(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + // ============================================================================= // current time zone // ============================================================================= @@ -1373,7 +1397,7 @@ const calculateNamedOffset = (adjustedMillis: number, zone: TimeZone.Named): num * @since 3.6.0 * @category mapping */ -export const mutateAdjusted: { +export const mutate: { (f: (date: Date) => void): (self: A) => DateTime.PreserveZone (self: A, f: (date: Date) => void): DateTime.PreserveZone } = dual(2, (self: DateTime, f: (date: Date) => void): DateTime => { @@ -1382,7 +1406,7 @@ export const mutateAdjusted: { f(date) return makeUtc(date.getTime()) } - const adjustedDate = toDateAdjusted(self) + const adjustedDate = toDate(self) const newAdjustedDate = new Date(adjustedDate.getTime()) f(newAdjustedDate) return makeZonedFromAdjusted(newAdjustedDate.getTime(), self.zone) @@ -1437,13 +1461,13 @@ export const mapEpochMillis: { * * // get the time zone adjusted date in milliseconds * DateTime.unsafeMakeZoned(0, { timeZone: "Europe/London" }).pipe( - * DateTime.withDateAdjusted((date) => date.getTime()) + * DateTime.withDate((date) => date.getTime()) * ) */ -export const withDateAdjusted: { +export const withDate: { (f: (date: Date) => A): (self: DateTime) => A (self: DateTime, f: (date: Date) => A): A -} = dual(2, (self: DateTime, f: (date: Date) => A): A => f(toDateAdjusted(self))) +} = dual(2, (self: DateTime, f: (date: Date) => A): A => f(toDate(self))) /** * Using the time zone adjusted `Date`, apply a function to the `Date` and @@ -1566,7 +1590,7 @@ export const add: { case "hour": return addMillis(self, amount * 60 * 60 * 1000) } - return mutateAdjusted(self, (date) => { + return mutate(self, (date) => { switch (unit) { case "days": case "day": { @@ -1641,7 +1665,7 @@ export const startOf: { } = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => - mutateAdjusted(self, (date) => { + mutate(self, (date) => { switch (part) { case "day": { date.setUTCHours(0, 0, 0, 0) @@ -1668,50 +1692,6 @@ export const startOf: { } })) -/** - * Remove the time aspect of a `DateTime`. - * - * @since 3.6.0 - * @category math - * @example - * import { DateTime } from "effect" - * - * // returns "2024-01-01T00:00:00Z" - * DateTime.unsafeMake("2024-01-01T12:00:00Z").pipe( - * DateTime.floorTimeUtc, - * DateTime.formatIso - * ) - */ -export const floorTimeUtc = (self: DateTime): Utc => - withDateUtc(self, (date) => { - date.setUTCHours(0, 0, 0, 0) - return makeUtc(date.getTime()) - }) - -/** - * Remove the time aspect of a `DateTime.Zoned`, first adjusting for the time - * zone. - * - * @since 3.6.0 - * @category math - * @example - * import { DateTime } from "effect" - * - * // returns "2024-01-01T00:00:00Z" - * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { - * timeZone: "Pacific/Auckland", - * inputInTimeZone: true - * }).pipe( - * DateTime.floorTimeAdjusted, - * DateTime.formatIso - * ) - */ -export const floorTimeAdjusted = (self: DateTime): Utc => - withDateAdjusted(self, (date) => { - date.setUTCHours(0, 0, 0, 0) - return makeUtc(date.getTime()) - }) - /** * Converts a `DateTime` to the end of the given `part`. * @@ -1739,7 +1719,7 @@ export const endOf: { } = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => - mutateAdjusted(self, (date) => { + mutate(self, (date) => { switch (part) { case "day": { date.setUTCHours(23, 59, 59, 999) @@ -1900,7 +1880,7 @@ export const formatZoned: { return new Intl.DateTimeFormat(options?.locale, { ...options, timeZone: "UTC" - }).format(toDateAdjusted(self)) + }).format(toDate(self)) } }) @@ -1919,6 +1899,6 @@ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString * @category formatting */ export const formatIsoOffset = (self: DateTime): string => { - const date = toDateAdjusted(self) + const date = toDate(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 f81edddc1f..4fc2395db3 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -4,11 +4,11 @@ import { assert, describe, it } from "./utils/extend.js" const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) describe("DateTime", () => { - describe("mutateAdjusted", () => { + describe("mutate", () => { it.effect("should mutate the date", () => Effect.gen(function*() { const now = yield* DateTime.now - const tomorrow = DateTime.mutateAdjusted(now, (date) => { + const tomorrow = DateTime.mutate(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) const diff = DateTime.distanceDurationEither(now, tomorrow) @@ -21,16 +21,16 @@ describe("DateTime", () => { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) - const future = DateTime.mutateAdjusted(now, (date) => { + const future = DateTime.mutate(now, (date) => { date.setUTCMonth(date.getUTCMonth() + 6) }) assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-07-01T00:00:00.000Z") - const plusOne = DateTime.mutateAdjusted(future, (date) => { + assert.strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") + const plusOne = DateTime.mutate(future, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toDateAdjusted(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") })) }) @@ -61,13 +61,13 @@ describe("DateTime", () => { ) const future = DateTime.add(now, 6, "months") assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.add(future, 1, "day") assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") - assert.strictEqual(DateTime.toDateAdjusted(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") + assert.strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") const minusOne = DateTime.add(plusOne, -1, "day") assert.strictEqual(DateTime.toDateUtc(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") - assert.strictEqual(DateTime.toDateAdjusted(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") + assert.strictEqual(DateTime.toDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) }) @@ -113,7 +113,7 @@ describe("DateTime", () => { ) const future = DateTime.endOf(now, "month") assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-01-31T10:59:59.999Z") - assert.strictEqual(DateTime.toDateAdjusted(future).toISOString(), "2024-01-31T23:59:59.999Z") + assert.strictEqual(DateTime.toDate(future).toISOString(), "2024-01-31T23:59:59.999Z") })) }) @@ -249,7 +249,7 @@ describe("DateTime", () => { }) }) - describe("setPartsAdjusted", () => { + describe("setParts", () => { it("partial", () => { const date = DateTime.unsafeMake({ year: 2024, @@ -258,7 +258,7 @@ describe("DateTime", () => { }) assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") - const updated = DateTime.setPartsAdjusted(date, { + const updated = DateTime.setParts(date, { year: 2023, month: 1 }) @@ -273,7 +273,7 @@ describe("DateTime", () => { }).pipe(DateTime.unsafeSetZoneNamed("Pacific/Auckland")) assert.strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") - const updated = DateTime.setPartsAdjusted(date, { + const updated = DateTime.setParts(date, { year: 2023, month: 6, hours: 12 @@ -309,12 +309,12 @@ describe("DateTime", () => { )) }) - describe("floorTimeAdjusted", () => { + describe("removeTime", () => { it("removes time", () => { const dt = DateTime.unsafeMakeZoned("2024-01-01T01:00:00Z", { timeZone: "Pacific/Auckland", inputInTimeZone: true - }).pipe(DateTime.floorTimeAdjusted) + }).pipe(DateTime.removeTime) assert.strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") }) }) From 9c5f493690b26e73da45e89695b2e17f5c78b747 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 10:08:51 +1200 Subject: [PATCH 49/61] docs adjustments --- packages/effect/src/DateTime.ts | 94 ++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 1d7d09fbea..57cfee975f 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -816,7 +816,8 @@ export const distance: { * `Duration`, returned as a `Right`. * * @since 3.6.0 - * @category constructors + * @category comparisons + * @example * import { DateTime, Effect } from "effect" * * Effect.gen(function* () { @@ -844,7 +845,8 @@ export const distanceDurationEither: { * Calulate the distance between two `DateTime` values. * * @since 3.6.0 - * @category constructors + * @category comparisons + * @example * import { DateTime, Effect } from "effect" * * Effect.gen(function* () { @@ -1028,6 +1030,34 @@ export const zonedToString = (self: Zoned): string => */ export const toEpochMillis = (self: DateTime): number => self.epochMillis +/** + * Remove the time aspect of a `DateTime`, first adjusting for the time + * zone. It will return a `DateTime.Utc` only containing the date. + * + * @since 3.6.0 + * @category conversions + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { + * timeZone: "Pacific/Auckland", + * inputInTimeZone: true + * }).pipe( + * DateTime.removeTime, + * DateTime.formatIso + * ) + */ +export const removeTime = (self: DateTime): Utc => + withDate(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + +// ============================================================================= +// parts +// ============================================================================= + const dateToParts = (date: Date): DateTime.Parts => ({ millis: date.getUTCMilliseconds(), seconds: date.getUTCSeconds(), @@ -1045,7 +1075,7 @@ const dateToParts = (date: Date): DateTime.Parts => ({ * The parts will be time zone adjusted. * * @since 3.6.0 - * @category conversions + * @category parts */ export const toParts = (self: DateTime): DateTime.Parts => { if (self._tag === "Utc") { @@ -1063,7 +1093,7 @@ export const toParts = (self: DateTime): DateTime.Parts => { * The parts will be in UTC. * * @since 3.6.0 - * @category conversions + * @category parts */ export const toPartsUtc = (self: DateTime): DateTime.Parts => { if (self.partsUtc !== undefined) { @@ -1079,7 +1109,7 @@ export const toPartsUtc = (self: DateTime): DateTime.Parts => { * The part will be in the UTC time zone. * * @since 3.6.0 - * @category conversions + * @category parts * @example * import { DateTime } from "effect" * @@ -1098,7 +1128,7 @@ export const getPartUtc: { * The part will be time zone adjusted. * * @since 3.6.0 - * @category conversions + * @category parts * @example * import { DateTime } from "effect" * @@ -1145,7 +1175,7 @@ const setPartsDate = (date: Date, parts: Partial): void => { * The Date will be time zone adjusted. * * @since 3.6.0 - * @category conversions + * @category parts */ export const setParts: { (parts: Partial): (self: A) => DateTime.PreserveZone @@ -1159,7 +1189,7 @@ export const setParts: { * Set the different parts of a `DateTime` as an object. * * @since 3.6.0 - * @category conversions + * @category parts */ export const setPartsUtc: { (parts: Partial): (self: A) => DateTime.PreserveZone @@ -1169,30 +1199,6 @@ export const setPartsUtc: { (self: DateTime, parts: Partial): DateTime => mutateUtc(self, (date) => setPartsDate(date, parts)) ) -/** - * Remove the time aspect of a `DateTime`, first adjusting for the time - * zone. It will return a `DateTime.Utc` only containing the date. - * - * @since 3.6.0 - * @category conversions - * @example - * import { DateTime } from "effect" - * - * // returns "2024-01-01T00:00:00Z" - * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { - * timeZone: "Pacific/Auckland", - * inputInTimeZone: true - * }).pipe( - * DateTime.removeTime, - * DateTime.formatIso - * ) - */ -export const removeTime = (self: DateTime): Utc => - withDate(self, (date) => { - date.setUTCHours(0, 0, 0, 0) - return makeUtc(date.getTime()) - }) - // ============================================================================= // current time zone // ============================================================================= @@ -1267,6 +1273,30 @@ export const withCurrentZoneLocal = ( ): Effect.Effect> => Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(zoneMakeLocal)) +/** + * Provide the `CurrentTimeZone` to an effect, using a offset. + * + * @since 3.6.0 + * @category current time zone + * @example + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function* () { + * // will use the system's local time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneOffset(3 * 60 * 60 * 1000)) + */ +export const withCurrentZoneOffset: { + (offset: number): ( + effect: Effect.Effect + ) => Effect.Effect> + (effect: Effect.Effect, offset: number): Effect.Effect> +} = dual( + 2, + (effect: Effect.Effect, offset: number): Effect.Effect> => + Effect.provideService(effect, CurrentTimeZone, zoneMakeOffset(offset)) +) + /** * Provide the `CurrentTimeZone` to an effect using an IANA time zone * identifier. From 5202aea4133978333b7a31758eaaa41b84885054 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 12:23:28 +1200 Subject: [PATCH 50/61] adjust formatting apis --- packages/effect/src/DateTime.ts | 109 +++++++++----------------- packages/effect/test/DateTime.test.ts | 8 +- packages/schema/src/Schema.ts | 2 +- 3 files changed, 44 insertions(+), 75 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 57cfee975f..b384b029f5 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -235,7 +235,7 @@ const ProtoZoned = { Equal.equals(this.zone, that.zone) }, toString(this: Zoned) { - return `DateTime.Zoned(${zonedToString(this)})` + return `DateTime.Zoned(${formatIsoZoned(this)})` } } @@ -1011,17 +1011,6 @@ const offsetToString = (offset: number): string => { */ export const zonedOffsetIso = (self: Zoned): string => offsetToString(zonedOffset(self)) -/** - * Format a `DateTime.Zoned` as a string. - * - * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. - * - * @since 3.6.0 - * @category conversions - */ -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`. * @@ -1780,9 +1769,21 @@ export const endOf: { // formatting // ============================================================================= +const intlTimeZone = (self: TimeZone): string => { + if (self._tag === "Named") { + return self.id + } + return offsetToString(self.offset) +} + /** * Format a `DateTime` as a string using the `DateTimeFormat` API. * + * The `timeZone` option is set to the offset of the time zone. + * + * Note: On Node versions < 22, fixed "Offset" zones will set the time zone to + * "UTC" and use the adjusted `Date`. + * * @since 3.6.0 * @category formatting */ @@ -1809,7 +1810,19 @@ export const format: { readonly locale?: string | undefined } | undefined -): string => new Intl.DateTimeFormat(options?.locale, options).format(toEpochMillis(self))) +): string => { + try { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: self._tag === "Utc" ? "UTC" : intlTimeZone(self.zone), + ...options + }).format(self.epochMillis) + } catch (_) { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: "UTC", + ...options + }).format(toDate(self)) + } +}) /** * Format a `DateTime` as a string using the `DateTimeFormat` API. @@ -1846,7 +1859,7 @@ export const formatUtc: { new Intl.DateTimeFormat(options?.locale, { ...options, timeZone: "UTC" - }).format(toEpochMillis(self))) + }).format(self.epochMillis)) /** * Format a `DateTime` as a string using the `DateTimeFormat` API. @@ -1857,62 +1870,7 @@ export const formatUtc: { export const formatIntl: { (format: Intl.DateTimeFormat): (self: DateTime) => string (self: DateTime, format: Intl.DateTimeFormat): string -} = dual(2, (self: DateTime, format: Intl.DateTimeFormat): string => format.format(toEpochMillis(self))) - -const intlTimeZone = (self: TimeZone): string => { - if (self._tag === "Named") { - return self.id - } - return offsetToString(self.offset) -} - -/** - * Format a `DateTime` as a string using the `DateTimeFormat` API. - * - * The `timeZone` option is set to the offset of the time zone. - * - * Note: On Node versions < 22, fixed "Offset" zones will set the time zone to - * "UTC" and use the adjusted `Date`. - * - * @since 3.6.0 - * @category formatting - */ -export const formatZoned: { - ( - options?: - | Intl.DateTimeFormatOptions & { - readonly locale?: string | undefined - } - | undefined - ): (self: Zoned) => string - ( - self: Zoned, - options?: - | Intl.DateTimeFormatOptions & { - readonly locale?: string | undefined - } - | undefined - ): string -} = dual((args) => isDateTime(args[0]), ( - self: Zoned, - options?: - | Intl.DateTimeFormatOptions & { - readonly locale?: string | undefined - } - | undefined -): string => { - try { - return new Intl.DateTimeFormat(options?.locale, { - ...options, - timeZone: intlTimeZone(self.zone) - }).format(toEpochMillis(self)) - } catch (_) { - return new Intl.DateTimeFormat(options?.locale, { - ...options, - timeZone: "UTC" - }).format(toDate(self)) - } -}) +} = dual(2, (self: DateTime, format: Intl.DateTimeFormat): string => format.format(self.epochMillis)) /** * Format a `DateTime` as a UTC ISO string. @@ -1932,3 +1890,14 @@ export const formatIsoOffset = (self: DateTime): string => { const date = toDate(self) return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, -1)}${zonedOffsetIso(self)}` } + +/** + * Format a `DateTime.Zoned` as a string. + * + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoZoned = (self: Zoned): string => + self.zone._tag === "Offset" ? formatIsoOffset(self) : `${formatIsoOffset(self)}[${self.zone.id}]` diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 4fc2395db3..365f5b89d1 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -178,14 +178,14 @@ describe("DateTime", () => { })) }) - describe("formatWithZone", () => { + describe("format zoned", () => { it.effect("full", () => Effect.gen(function*() { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) assert.strictEqual( - DateTime.formatZoned(now, { dateStyle: "full", timeStyle: "full" }), + DateTime.format(now, { dateStyle: "full", timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 PM New Zealand Standard Time" ) })) @@ -195,7 +195,7 @@ describe("DateTime", () => { const now = yield* DateTime.now const formatted = now.pipe( DateTime.setZoneOffset(10 * 60 * 60 * 1000), - DateTime.formatZoned({ dateStyle: "long", timeStyle: "short" }) + DateTime.format({ dateStyle: "long", timeStyle: "short" }) ) assert.strictEqual(formatted, "January 1, 1970 at 10:00 AM") })) @@ -336,7 +336,7 @@ describe("DateTime", () => { 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.map(DateTime.formatIsoZoned), Option.flatMap(DateTime.makeZonedFromString) ) assert.deepStrictEqual(dt.zone, DateTime.zoneUnsafeMakeNamed("Pacific/Auckland")) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index b360f037a9..6fae0aff2a 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6119,7 +6119,7 @@ export class DateTimeZoned extends transformOrFail( onNone: () => ParseResult.fail(new ParseResult.Type(ast, s)), onSome: ParseResult.succeed }), - encode: (dt) => ParseResult.succeed(dateTime.zonedToString(dt)) + encode: (dt) => ParseResult.succeed(dateTime.formatIsoZoned(dt)) } ).annotations({ identifier: "DateTimeZoned" }) {} From 009d9de52c8bd3eba68b00b920bbe61e7f40e1ab Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 14:31:29 +1200 Subject: [PATCH 51/61] fix changeset --- .changeset/clean-trainers-tap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/clean-trainers-tap.md b/.changeset/clean-trainers-tap.md index bf9650fdf2..cbabf51d39 100644 --- a/.changeset/clean-trainers-tap.md +++ b/.changeset/clean-trainers-tap.md @@ -30,5 +30,5 @@ Effect.gen(function* () { const sydneyTime = tomorrow.pipe( DateTime.unsafeSetZoneNamed("Australia/Sydney"), ); -}).pipe(Effect.withCurrentZoneNamed("America/New_York")); +}).pipe(DateTime.withCurrentZoneNamed("America/New_York")); ``` From 68f0e0410bfa1884e6cef536afae318ca2c8b274 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 19:58:05 +1200 Subject: [PATCH 52/61] support converting input with setZone apis --- packages/effect/src/DateTime.ts | 54 ++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index b384b029f5..43c1a6b6db 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -519,7 +519,7 @@ const zonedStringRegex = /^(.{17,35})\[(.+)\]$/ /** * Create a `DateTime.Zoned` from 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 constructors @@ -577,9 +577,18 @@ export const unsafeNow: LazyArg = () => makeUtc(Date.now()) * }) */ export const setZone: { - (zone: TimeZone): (self: DateTime) => Zoned - (self: DateTime, zone: TimeZone): Zoned -} = dual(2, (self: DateTime, zone: TimeZone): Zoned => makeZonedProto(self.epochMillis, zone, self.partsUtc)) + (zone: TimeZone, options?: { + readonly inputInTimeZone?: boolean | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zone: TimeZone, options?: { + readonly inputInTimeZone?: boolean | undefined + }): Zoned +} = dual(2, (self: DateTime, zone: TimeZone, options?: { + readonly inputInTimeZone?: boolean | undefined +}): Zoned => + options?.inputInTimeZone === true + ? makeZonedFromAdjusted(self.epochMillis, zone) + : makeZonedProto(self.epochMillis, zone, self.partsUtc)) /** * Add a fixed offset time zone to a `DateTime`. @@ -599,9 +608,15 @@ export const setZone: { * }) */ export const setZoneOffset: { - (offset: number): (self: DateTime) => Zoned - (self: DateTime, offset: number): Zoned -} = dual(2, (self: DateTime, offset: number): Zoned => setZone(self, zoneMakeOffset(offset))) + (offset: number, options?: { + readonly inputInTimeZone?: boolean | undefined + }): (self: DateTime) => Zoned + (self: DateTime, offset: number, options?: { + readonly inputInTimeZone?: boolean | undefined + }): Zoned +} = dual(2, (self: DateTime, offset: number, options?: { + readonly inputInTimeZone?: boolean | undefined +}): Zoned => setZone(self, zoneMakeOffset(offset), options)) const validZoneCache = globalValue("effect/DateTime/validZoneCache", () => new Map()) @@ -750,12 +765,17 @@ export const zoneToString = (self: TimeZone): string => { * }) */ export const setZoneNamed: { - (zoneId: string): (self: DateTime) => Option.Option - (self: DateTime, zoneId: string): Option.Option + (zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined + }): (self: DateTime) => Option.Option + (self: DateTime, zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined + }): Option.Option } = dual( 2, - (self: DateTime, zoneId: string): Option.Option => - Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone)) + (self: DateTime, zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined + }): Option.Option => Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone, options)) ) /** @@ -774,9 +794,15 @@ export const setZoneNamed: { * }) */ export const unsafeSetZoneNamed: { - (zoneId: string): (self: DateTime) => Zoned - (self: DateTime, zoneId: string): Zoned -} = dual(2, (self: DateTime, zoneId: string): Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId))) + (zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined + }): Zoned +} = dual(2, (self: DateTime, zoneId: string, options?: { + readonly inputInTimeZone?: boolean | undefined +}): Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId), options)) // ============================================================================= // comparisons From 676a1fbe074361f56d19d1f043e2c46b900050b6 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 21:36:00 +1200 Subject: [PATCH 53/61] s/inputInTimeZone/adjustForTimeZone --- packages/effect/src/DateTime.ts | 36 +++++++++++++-------------- packages/effect/test/DateTime.test.ts | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 43c1a6b6db..4a89cab608 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -430,7 +430,7 @@ export const unsafeMake = (input: A): DateTime.Preserv * Create a `DateTime.Zoned` using `DateTime.unsafeMake` and a time zone. * * The input is treated as UTC and then the time zone is attached, unless - * `inputInTimeZone` is set to `true`. In that case, the input is treated as + * `adjustForTimeZone` is set to `true`. In that case, the input is treated as * already in the time zone. * * @since 3.6.0 @@ -442,7 +442,7 @@ export const unsafeMake = (input: A): DateTime.Preserv */ export const unsafeMakeZoned = (input: DateTime.Input, options: { readonly timeZone: number | string | TimeZone - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned => { const self = unsafeMake(input) let zone: TimeZone @@ -457,7 +457,7 @@ export const unsafeMakeZoned = (input: DateTime.Input, options: { } zone = parsedZone.value } - if (options.inputInTimeZone !== true) { + if (options.adjustForTimeZone !== true) { return makeZonedProto(self.epochMillis, zone, self.partsUtc) } return makeZonedFromAdjusted(self.epochMillis, zone) @@ -481,7 +481,7 @@ export const makeZoned: ( input: DateTime.Input, options: { readonly timeZone: number | string | TimeZone - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined } ) => Option.Option = Option .liftThrowable(unsafeMakeZoned) @@ -578,15 +578,15 @@ export const unsafeNow: LazyArg = () => makeUtc(Date.now()) */ export const setZone: { (zone: TimeZone, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): (self: DateTime) => Zoned (self: DateTime, zone: TimeZone, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned } = dual(2, (self: DateTime, zone: TimeZone, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned => - options?.inputInTimeZone === true + options?.adjustForTimeZone === true ? makeZonedFromAdjusted(self.epochMillis, zone) : makeZonedProto(self.epochMillis, zone, self.partsUtc)) @@ -609,13 +609,13 @@ export const setZone: { */ export const setZoneOffset: { (offset: number, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): (self: DateTime) => Zoned (self: DateTime, offset: number, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned } = dual(2, (self: DateTime, offset: number, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned => setZone(self, zoneMakeOffset(offset), options)) const validZoneCache = globalValue("effect/DateTime/validZoneCache", () => new Map()) @@ -766,15 +766,15 @@ export const zoneToString = (self: TimeZone): string => { */ export const setZoneNamed: { (zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): (self: DateTime) => Option.Option (self: DateTime, zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Option.Option } = dual( 2, (self: DateTime, zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Option.Option => Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone, options)) ) @@ -795,13 +795,13 @@ export const setZoneNamed: { */ export const unsafeSetZoneNamed: { (zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): (self: DateTime) => Zoned (self: DateTime, zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned } = dual(2, (self: DateTime, zoneId: string, options?: { - readonly inputInTimeZone?: boolean | undefined + readonly adjustForTimeZone?: boolean | undefined }): Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId), options)) // ============================================================================= @@ -1057,7 +1057,7 @@ export const toEpochMillis = (self: DateTime): number => self.epochMillis * // returns "2024-01-01T00:00:00Z" * DateTime.unsafeMakeZoned("2024-01-01T05:00:00Z", { * timeZone: "Pacific/Auckland", - * inputInTimeZone: true + * adjustForTimeZone: true * }).pipe( * DateTime.removeTime, * DateTime.formatIso diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 365f5b89d1..853cfd6c2d 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -313,7 +313,7 @@ describe("DateTime", () => { it("removes time", () => { const dt = DateTime.unsafeMakeZoned("2024-01-01T01:00:00Z", { timeZone: "Pacific/Auckland", - inputInTimeZone: true + adjustForTimeZone: true }).pipe(DateTime.removeTime) assert.strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") }) From 2ad90f72e125fafe0a44edcd01688afb19397094 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 21:57:38 +1200 Subject: [PATCH 54/61] unsafeIsPast/unsafeIsFuture --- packages/effect/src/DateTime.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 4a89cab608..5d4dbed162 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -960,12 +960,24 @@ export const between: { */ export const isFuture = (self: DateTime): Effect.Effect => Effect.map(now, lessThan(self)) +/** + * @since 3.6.0 + * @category comparisons + */ +export const unsafeIsFuture = (self: DateTime): boolean => lessThan(unsafeNow(), self) + /** * @since 3.6.0 * @category comparisons */ export const isPast = (self: DateTime): Effect.Effect => Effect.map(now, greaterThan(self)) +/** + * @since 3.6.0 + * @category comparisons + */ +export const unsafeIsPast = (self: DateTime): boolean => greaterThan(unsafeNow(), self) + // ============================================================================= // conversions // ============================================================================= From 4bdd73cff861957fc59d3a0d099a2635a0589dee Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 22:05:33 +1200 Subject: [PATCH 55/61] formatIsoDate --- packages/effect/src/DateTime.ts | 16 ++++++++++++++++ packages/effect/test/DateTime.test.ts | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 5d4dbed162..0547e23245 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1918,6 +1918,22 @@ export const formatIntl: { */ export const formatIso = (self: DateTime): string => toDateUtc(self).toISOString() +/** + * Format a `DateTime` as a time zone adjusted ISO date string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoDate = (self: DateTime): string => toDate(self).toISOString().slice(0, 10) + +/** + * Format a `DateTime` as a UTC ISO date string. + * + * @since 3.6.0 + * @category formatting + */ +export const formatIsoDateUtc = (self: DateTime): string => toDateUtc(self).toISOString().slice(0, 10) + /** * Format a `DateTime.Zoned` as a ISO string with an offset. * diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 853cfd6c2d..327ad94a88 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -124,6 +124,14 @@ describe("DateTime", () => { assert.strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) + it("month duplicated", () => { + const mar = DateTime.unsafeMake("2024-03-15T12:00:00.000Z") + const end = DateTime.startOf(mar, "month").pipe( + DateTime.startOf("month") + ) + assert.strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") + }) + it("feb leap year", () => { const feb = DateTime.unsafeMake("2024-02-15T12:00:00.000Z") const end = DateTime.startOf(feb, "month") @@ -159,8 +167,7 @@ describe("DateTime", () => { assert.strictEqual( DateTime.format(now, { dateStyle: "full", - timeStyle: "full", - timeZone: "UTC" + timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" ) From 12bd55f27fa94fdab6b805718885eb7ee2396d33 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 22:09:49 +1200 Subject: [PATCH 56/61] fix args predicates --- packages/effect/src/DateTime.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 0547e23245..2e1fa020de 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -306,6 +306,8 @@ const makeZonedProto = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime */ export const isDateTime = (u: unknown): u is DateTime => Predicate.hasProperty(u, TypeId) +const isDateTimeArgs = (args: IArguments) => isDateTime(args[0]) + /** * @since 3.6.0 * @category guards @@ -583,7 +585,7 @@ export const setZone: { (self: DateTime, zone: TimeZone, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned -} = dual(2, (self: DateTime, zone: TimeZone, options?: { +} = dual(isDateTimeArgs, (self: DateTime, zone: TimeZone, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned => options?.adjustForTimeZone === true @@ -614,7 +616,7 @@ export const setZoneOffset: { (self: DateTime, offset: number, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned -} = dual(2, (self: DateTime, offset: number, options?: { +} = dual(isDateTimeArgs, (self: DateTime, offset: number, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned => setZone(self, zoneMakeOffset(offset), options)) @@ -772,7 +774,7 @@ export const setZoneNamed: { readonly adjustForTimeZone?: boolean | undefined }): Option.Option } = dual( - 2, + isDateTimeArgs, (self: DateTime, zoneId: string, options?: { readonly adjustForTimeZone?: boolean | undefined }): Option.Option => Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone, options)) @@ -800,7 +802,7 @@ export const unsafeSetZoneNamed: { (self: DateTime, zoneId: string, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned -} = dual(2, (self: DateTime, zoneId: string, options?: { +} = dual(isDateTimeArgs, (self: DateTime, zoneId: string, options?: { readonly adjustForTimeZone?: boolean | undefined }): Zoned => setZone(self, zoneUnsafeMakeNamed(zoneId), options)) @@ -1719,7 +1721,7 @@ export const startOf: { (self: A, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { +} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutate(self, (date) => { @@ -1773,7 +1775,7 @@ export const endOf: { (self: A, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual((args) => typeof args[1] === "string", (self: DateTime, part: DateTime.DatePart, options?: { +} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.DatePart, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutate(self, (date) => { @@ -1841,7 +1843,7 @@ export const format: { } | undefined ): string -} = dual((args) => isDateTime(args[0]), ( +} = dual(isDateTimeArgs, ( self: DateTime, options?: | Intl.DateTimeFormatOptions & { @@ -1886,7 +1888,7 @@ export const formatUtc: { } | undefined ): string -} = dual((args) => isDateTime(args[0]), ( +} = dual(isDateTimeArgs, ( self: DateTime, options?: | Intl.DateTimeFormatOptions & { From ba2860c4d6eda25c775c3c4365fe2d49e71a47b0 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 22 Jul 2024 22:45:48 +1200 Subject: [PATCH 57/61] formatLocal --- packages/effect/src/DateTime.ts | 33 +++++++++++++++++++++++++++++++++ packages/schema/src/Schema.ts | 24 ++++++++++++------------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 2e1fa020de..403a685ab7 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1864,6 +1864,39 @@ export const format: { } }) +/** + * Format a `DateTime` as a string using the `DateTimeFormat` API. + * + * It will use the system's local time zone. + * + * @since 3.6.0 + * @category formatting + */ +export const formatLocal: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined +): string => new Intl.DateTimeFormat(options?.locale, options).format(self.epochMillis)) + /** * Format a `DateTime` as a string using the `DateTimeFormat` API. * diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 6fae0aff2a..9c0c3fb556 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -5924,7 +5924,7 @@ export class DateFromNumber extends transform( * Describes a schema that represents a `DateTime.Utc` instance. * * @category DateTime.Utc constructors - * @since 0.68.26 + * @since 0.68.27 */ export class DateTimeUtcFromSelf extends declare( (u) => dateTime.isDateTime(u) && dateTime.isUtc(u), @@ -5947,7 +5947,7 @@ const decodeDateTime = (input: A, _: ParseOpt * Defines a schema that attempts to convert a `number` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. * * @category DateTime.Utc transformations - * @since 0.68.26 + * @since 0.68.27 */ export class DateTimeUtcFromNumber extends transformOrFail( Number$, @@ -5963,7 +5963,7 @@ export class DateTimeUtcFromNumber extends transformOrFail( * Defines a schema that attempts to convert a `string` to a `DateTime.Utc` instance using the `DateTime.unsafeMake` constructor. * * @category DateTime.Utc transformations - * @since 0.68.26 + * @since 0.68.27 */ export class DateTimeUtc extends transformOrFail( String$, @@ -5982,7 +5982,7 @@ const timeZoneOffsetArbitrary = (): LazyArbitrary => ( * Describes a schema that represents a `TimeZone.Offset` instance. * * @category TimeZone constructors - * @since 0.68.26 + * @since 0.68.27 */ export class TimeZoneOffsetFromSelf extends declare( dateTime.isTimeZoneOffset, @@ -5998,7 +5998,7 @@ export class TimeZoneOffsetFromSelf extends declare( * Defines a schema that converts a `number` to a `TimeZone.Offset` instance using the `DateTime.zoneMakeOffset` constructor. * * @category TimeZone transformations - * @since 0.68.26 + * @since 0.68.27 */ export class TimeZoneOffset extends transform( Number$, @@ -6013,7 +6013,7 @@ const timeZoneNamedArbitrary = (): LazyArbitrary => (fc * Describes a schema that represents a `TimeZone.Named` instance. * * @category TimeZone constructors - * @since 0.68.26 + * @since 0.68.27 */ export class TimeZoneNamedFromSelf extends declare( dateTime.isTimeZoneNamed, @@ -6029,7 +6029,7 @@ export class TimeZoneNamedFromSelf extends declare( * Defines a schema that attempts to convert a `string` to a `TimeZone.Named` instance using the `DateTime.zoneUnsafeMakeNamed` constructor. * * @category TimeZone transformations - * @since 0.68.26 + * @since 0.68.27 */ export class TimeZoneNamed extends transformOrFail( String$, @@ -6047,7 +6047,7 @@ export class TimeZoneNamed extends transformOrFail( /** * @category api interface - * @since 0.68.26 + * @since 0.68.27 */ export interface TimeZoneFromSelf extends Union<[typeof TimeZoneOffsetFromSelf, typeof TimeZoneNamedFromSelf]> { annotations(annotations: Annotations.Schema): TimeZoneFromSelf @@ -6055,7 +6055,7 @@ export interface TimeZoneFromSelf extends Union<[typeof TimeZoneOffsetFromSelf, /** * @category TimeZone constructors - * @since 0.68.26 + * @since 0.68.27 */ export const TimeZoneFromSelf: TimeZoneFromSelf = Union(TimeZoneOffsetFromSelf, TimeZoneNamedFromSelf) @@ -6063,7 +6063,7 @@ export const TimeZoneFromSelf: TimeZoneFromSelf = Union(TimeZoneOffsetFromSelf, * Defines a schema that attempts to convert a `string` to a `TimeZone` using the `DateTime.zoneFromString` constructor. * * @category TimeZone transformations - * @since 0.68.26 + * @since 0.68.27 */ export class TimeZone extends transformOrFail( String$, @@ -6089,7 +6089,7 @@ const timeZoneArbitrary: LazyArbitrary = (fc) => * Describes a schema that represents a `DateTime.Zoned` instance. * * @category DateTime.Zoned constructors - * @since 0.68.26 + * @since 0.68.27 */ export class DateTimeZonedFromSelf extends declare( (u) => dateTime.isDateTime(u) && dateTime.isZoned(u), @@ -6107,7 +6107,7 @@ export class DateTimeZonedFromSelf extends declare( * Defines a schema that attempts to convert a `string` to a `DateTime.Zoned` instance. * * @category DateTime.Zoned transformations - * @since 0.68.26 + * @since 0.68.27 */ export class DateTimeZoned extends transformOrFail( String$, From 486b4588dce9423ba90c125593b3e980d3eb097b Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 25 Jul 2024 15:25:33 +1200 Subject: [PATCH 58/61] fixes for ios --- packages/effect/src/DateTime.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 403a685ab7..fd13dbcb92 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1006,18 +1006,18 @@ export const toDate = (self: DateTime): Date => { } else if (self.adjustedEpochMillis !== undefined) { return new Date(self.adjustedEpochMillis) } - const parts = self.zone.format.formatToParts(self.epochMillis) + const parts = self.zone.format.formatToParts(self.epochMillis).filter((_) => _.type !== "literal") const date = new Date(0) date.setUTCFullYear( - Number(parts[4].value), + Number(parts[2].value), Number(parts[0].value) - 1, - Number(parts[2].value) + Number(parts[1].value) ) date.setUTCHours( - Number(parts[6].value), - Number(parts[8].value), - Number(parts[10].value), - Number(parts[12].value) + Number(parts[3].value), + Number(parts[4].value), + Number(parts[5].value), + Number(parts[6].value) ) self.adjustedEpochMillis = date.getTime() return date @@ -1434,8 +1434,7 @@ const parseOffset = (offset: string): number | null => { } const calculateNamedOffset = (adjustedMillis: number, zone: TimeZone.Named): number => { - const parts = zone.format.formatToParts(adjustedMillis) - const offset = parts[14].value + const offset = zone.format.formatToParts(adjustedMillis).find((_) => _.type === "timeZoneName")?.value ?? "" if (offset === "GMT") { return 0 } From bb6f61679b08dcea3c15d3d6f7ce693d24486de0 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 26 Jul 2024 10:30:08 +1200 Subject: [PATCH 59/61] support parts for add/subtract --- packages/effect/src/DateTime.ts | 186 +++++++++++++++----------- packages/effect/test/DateTime.test.ts | 20 ++- 2 files changed, 125 insertions(+), 81 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index fd13dbcb92..b89999e44a 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -19,6 +19,7 @@ import * as Option from "./Option.js" import * as order from "./Order.js" import { type Pipeable, pipeArguments } from "./Pipeable.js" import * as Predicate from "./Predicate.js" +import type { Mutable } from "./Types.js" /** * @since 3.6.0 @@ -49,7 +50,7 @@ export interface Utc extends DateTime.Proto { readonly _tag: "Utc" readonly epochMillis: number /** @internal */ - partsUtc: DateTime.Parts + partsUtc: DateTime.PartsWithWeekday } /** @@ -63,9 +64,9 @@ export interface Zoned extends DateTime.Proto { /** @internal */ adjustedEpochMillis?: number /** @internal */ - partsAdjusted?: DateTime.Parts + partsAdjusted?: DateTime.PartsWithWeekday /** @internal */ - partsUtc?: DateTime.Parts + partsUtc?: DateTime.PartsWithWeekday } /** @@ -121,7 +122,7 @@ export declare namespace DateTime { * @since 3.6.0 * @category models */ - export interface Parts { + export interface PartsWithWeekday { readonly millis: number readonly seconds: number readonly minutes: number @@ -132,6 +133,35 @@ export declare namespace DateTime { readonly year: number } + /** + * @since 3.6.0 + * @category models + */ + export interface Parts { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly day: number + readonly month: number + readonly year: number + } + + /** + * @since 3.6.0 + * @category models + */ + export interface PartsForMath { + readonly millis: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly days: number + readonly weeks: number + readonly months: number + readonly years: number + } + /** * @since 3.6.0 * @category models @@ -288,7 +318,7 @@ const ProtoTimeZoneOffset = { } } -const makeZonedProto = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.Parts): Zoned => { +const makeZonedProto = (epochMillis: number, zone: TimeZone, partsUtc?: DateTime.PartsWithWeekday): Zoned => { const self = Object.create(ProtoZoned) self.epochMillis = epochMillis self.zone = zone @@ -823,7 +853,7 @@ export const unsafeSetZoneNamed: { * * Effect.gen(function* () { * const now = yield* DateTime.now - * const other = DateTime.add(now, 1, "minute") + * const other = DateTime.add(now, { minutes: 1 }) * * // returns 60000 * DateTime.distance(now, other) @@ -850,7 +880,7 @@ export const distance: { * * Effect.gen(function* () { * const now = yield* DateTime.now - * const other = DateTime.add(now, 1, "minute") + * const other = DateTime.add(now, { minutes: 1 }) * * // returns Either.right(Duration.minutes(1)) * DateTime.distanceDurationEither(now, other) @@ -879,7 +909,7 @@ export const distanceDurationEither: { * * Effect.gen(function* () { * const now = yield* DateTime.now - * const other = DateTime.add(now, 1, "minute") + * const other = DateTime.add(now, { minutes: 1 }) * * // returns Duration.minutes(1) * DateTime.distanceDuration(now, other) @@ -1087,7 +1117,7 @@ export const removeTime = (self: DateTime): Utc => // parts // ============================================================================= -const dateToParts = (date: Date): DateTime.Parts => ({ +const dateToParts = (date: Date): DateTime.PartsWithWeekday => ({ millis: date.getUTCMilliseconds(), seconds: date.getUTCSeconds(), minutes: date.getUTCMinutes(), @@ -1106,7 +1136,7 @@ const dateToParts = (date: Date): DateTime.Parts => ({ * @since 3.6.0 * @category parts */ -export const toParts = (self: DateTime): DateTime.Parts => { +export const toParts = (self: DateTime): DateTime.PartsWithWeekday => { if (self._tag === "Utc") { return toPartsUtc(self) } else if (self.partsAdjusted !== undefined) { @@ -1124,7 +1154,7 @@ export const toParts = (self: DateTime): DateTime.Parts => { * @since 3.6.0 * @category parts */ -export const toPartsUtc = (self: DateTime): DateTime.Parts => { +export const toPartsUtc = (self: DateTime): DateTime.PartsWithWeekday => { if (self.partsUtc !== undefined) { return self.partsUtc } @@ -1147,9 +1177,9 @@ export const toPartsUtc = (self: DateTime): DateTime.Parts => { * assert.strictEqual(year, 2024) */ export const getPartUtc: { - (part: keyof DateTime.Parts): (self: DateTime) => number - (self: DateTime, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toPartsUtc(self)[part]) + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime, part: keyof DateTime.PartsWithWeekday): number => toPartsUtc(self)[part]) /** * Get a part of a `DateTime` as a number. @@ -1166,11 +1196,11 @@ export const getPartUtc: { * assert.strictEqual(year, 2024) */ export const getPart: { - (part: keyof DateTime.Parts): (self: DateTime) => number - (self: DateTime, part: keyof DateTime.Parts): number -} = dual(2, (self: DateTime, part: keyof DateTime.Parts): number => toParts(self)[part]) + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime, part: keyof DateTime.PartsWithWeekday): number => toParts(self)[part]) -const setPartsDate = (date: Date, parts: Partial): void => { +const setPartsDate = (date: Date, parts: Partial): void => { if (parts.year !== undefined) { date.setUTCFullYear(parts.year) } @@ -1207,11 +1237,12 @@ const setPartsDate = (date: Date, parts: Partial): void => { * @category parts */ export const setParts: { - (parts: Partial): (self: A) => DateTime.PreserveZone - (self: A, parts: Partial): DateTime.PreserveZone + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime, parts: Partial): DateTime => mutate(self, (date) => setPartsDate(date, parts)) + (self: DateTime, parts: Partial): DateTime => + mutate(self, (date) => setPartsDate(date, parts)) ) /** @@ -1221,11 +1252,12 @@ export const setParts: { * @category parts */ export const setPartsUtc: { - (parts: Partial): (self: A) => DateTime.PreserveZone - (self: A, parts: Partial): DateTime.PreserveZone + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone } = dual( 2, - (self: DateTime, parts: Partial): DateTime => mutateUtc(self, (date) => setPartsDate(date, parts)) + (self: DateTime, parts: Partial): DateTime => + mutateUtc(self, (date) => setPartsDate(date, parts)) ) // ============================================================================= @@ -1447,7 +1479,7 @@ const calculateNamedOffset = (adjustedMillis: number, zone: TimeZone.Named): num } /** - * Modify a `DateTime` by applying a function to the underlying `Date`. + * Modify a `DateTime` by applying a function to a cloned `Date` instance. * * The `Date` will first have the time zone applied if possible, and then be * converted back to a `DateTime` within the same time zone. @@ -1471,7 +1503,7 @@ export const mutate: { }) /** - * Modify a `DateTime` by applying a function to the underlying UTC `Date`. + * Modify a `DateTime` by applying a function to a cloned UTC `Date` instance. * * @since 3.6.0 * @category mapping @@ -1612,7 +1644,9 @@ export const subtractDuration: { mapEpochMillis(self, (millis) => millis - Duration.toMillis(duration)) ) -const addMillis = (date: DateTime, amount: number): DateTime => mapEpochMillis(date, (millis) => millis + amount) +const addMillis = (date: Date, amount: number): void => { + date.setTime(date.getTime() + amount) +} /** * Add the given `amount` of `unit`'s to a `DateTime`. @@ -1627,56 +1661,52 @@ const addMillis = (date: DateTime, amount: number): DateTime => mapEpochMillis(d * * // add 5 minutes * DateTime.unsafeMake(0).pipe( - * DateTime.add(5, "minutes") + * DateTime.add({ minutes: 5 }) * ) */ export const add: { - (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone - (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone -} = dual(3, (self: DateTime, amount: number, unit: DateTime.Unit): DateTime => { - switch (unit) { - case "millis": - case "milli": - return addMillis(self, amount) - case "seconds": - case "second": - return addMillis(self, amount * 1000) - case "minutes": - case "minute": - return addMillis(self, amount * 60 * 1000) - case "hours": - case "hour": - return addMillis(self, amount * 60 * 60 * 1000) - } - return mutate(self, (date) => { - switch (unit) { - case "days": - case "day": { - date.setUTCDate(date.getUTCDate() + amount) - return date - } - case "weeks": - case "week": { - date.setUTCDate(date.getUTCDate() + amount * 7) - return date - } - case "months": - case "month": { - const day = date.getUTCDate() - date.setUTCMonth(date.getUTCMonth() + amount + 1, 0) - if (day < date.getUTCDate()) { - date.setUTCDate(day) - } - return date + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone +} = dual(2, (self: DateTime, parts: Partial): DateTime => + mutate(self, (date) => { + if (parts.millis) { + addMillis(date, parts.millis) + } + if (parts.seconds) { + addMillis(date, parts.seconds * 1000) + } + if (parts.minutes) { + addMillis(date, parts.minutes * 60 * 1000) + } + if (parts.hours) { + addMillis(date, parts.hours * 60 * 60 * 1000) + } + if (parts.days) { + date.setUTCDate(date.getUTCDate() + parts.days) + } + if (parts.weeks) { + date.setUTCDate(date.getUTCDate() + parts.weeks * 7) + } + if (parts.months) { + const day = date.getUTCDate() + date.setUTCMonth(date.getUTCMonth() + parts.months + 1, 0) + if (day < date.getUTCDate()) { + date.setUTCDate(day) } - case "years": - case "year": { - date.setUTCFullYear(date.getUTCFullYear() + amount) - return date + } + if (parts.years) { + const day = date.getUTCDate() + const month = date.getUTCMonth() + date.setUTCFullYear( + date.getUTCFullYear() + parts.years, + month + 1, + 0 + ) + if (day < date.getUTCDate()) { + date.setUTCDate(day) } } - }) -}) + })) /** * Subtract the given `amount` of `unit`'s from a `DateTime`. @@ -1688,13 +1718,19 @@ export const add: { * * // subtract 5 minutes * DateTime.unsafeMake(0).pipe( - * DateTime.subtract(5, "minutes") + * DateTime.subtract({ minutes: 5 }) * ) */ export const subtract: { - (amount: number, unit: DateTime.Unit): (self: A) => DateTime.PreserveZone - (self: A, amount: number, unit: DateTime.Unit): DateTime.PreserveZone -} = dual(3, (self: DateTime, amount: number, unit: DateTime.Unit): DateTime => add(self, -amount, unit)) + (parts: Partial): (self: A) => DateTime.PreserveZone + (self: A, parts: Partial): DateTime.PreserveZone +} = dual(2, (self: DateTime, parts: Partial): DateTime => { + const newParts = {} as Partial> + for (const key in parts) { + newParts[key as keyof DateTime.PartsForMath] = -1 * parts[key as keyof DateTime.PartsForMath]! + } + return add(self, newParts) +}) /** * Converts a `DateTime` to the start of the given `part`. diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 327ad94a88..11eb37d06a 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -38,18 +38,18 @@ describe("DateTime", () => { it.effect("utc", () => Effect.gen(function*() { const now = yield* DateTime.now - const tomorrow = DateTime.add(now, 1, "day") + const tomorrow = DateTime.add(now, { days: 1 }) const diff = DateTime.distanceDurationEither(now, tomorrow) assert.deepStrictEqual(diff, Either.right(Duration.decode("1 day"))) })) it("to month with less days", () => { const jan = DateTime.unsafeMake({ year: 2023, month: 1, day: 31 }) - let feb = DateTime.add(jan, 1, "month") + let feb = DateTime.add(jan, { months: 1 }) assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") const mar = DateTime.unsafeMake({ year: 2023, month: 3, day: 31 }) - feb = DateTime.add(mar, -1, "month") + feb = DateTime.subtract(mar, { months: 1 }) assert.strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") }) @@ -59,16 +59,24 @@ describe("DateTime", () => { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) - const future = DateTime.add(now, 6, "months") + const future = DateTime.add(now, { months: 6 }) assert.strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") assert.strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") - const plusOne = DateTime.add(future, 1, "day") + const plusOne = DateTime.add(future, { days: 1 }) assert.strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") assert.strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") - const minusOne = DateTime.add(plusOne, -1, "day") + const minusOne = DateTime.subtract(plusOne, { days: 1 }) assert.strictEqual(DateTime.toDateUtc(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") assert.strictEqual(DateTime.toDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) + + it.effect("leap years", () => + Effect.gen(function*() { + yield* setTo2024NZ + const now = yield* DateTime.make({ year: 2024, month: 2, day: 29 }) + const future = DateTime.add(now, { years: 1 }) + assert.strictEqual(DateTime.formatIso(future), "2025-02-28T00:00:00.000Z") + })) }) describe("endOf", () => { From 7bd074049a458cdc7be8b6921f840b064874657f Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 26 Jul 2024 11:39:57 +1200 Subject: [PATCH 60/61] support more units in startOf/endOf --- packages/effect/src/DateTime.ts | 66 +++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index b89999e44a..732cc39921 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -90,33 +90,35 @@ export declare namespace DateTime { * @since 3.6.0 * @category models */ - export type Unit = + export type Unit = UnitSingular | UnitPlural + + /** + * @since 3.6.0 + * @category models + */ + export type UnitSingular = | "milli" - | "millis" | "second" - | "seconds" | "minute" - | "minutes" | "hour" - | "hours" | "day" - | "days" | "week" - | "weeks" | "month" - | "months" | "year" - | "years" /** * @since 3.6.0 * @category models */ - export type DatePart = - | "day" - | "week" - | "month" - | "year" + export type UnitPlural = + | "millis" + | "seconds" + | "minutes" + | "hours" + | "days" + | "weeks" + | "months" + | "years" /** * @since 3.6.0 @@ -1750,17 +1752,29 @@ export const subtract: { * ) */ export const startOf: { - (part: DateTime.DatePart, options?: { + (part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): (self: A) => DateTime.PreserveZone - (self: A, part: DateTime.DatePart, options?: { + (self: A, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.DatePart, options?: { +} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutate(self, (date) => { switch (part) { + case "second": { + date.setUTCMilliseconds(0) + break + } + case "minute": { + date.setUTCSeconds(0, 0) + break + } + case "hour": { + date.setUTCMinutes(0, 0, 0) + break + } case "day": { date.setUTCHours(0, 0, 0, 0) break @@ -1804,17 +1818,29 @@ export const startOf: { * ) */ export const endOf: { - (part: DateTime.DatePart, options?: { + (part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): (self: A) => DateTime.PreserveZone - (self: A, part: DateTime.DatePart, options?: { + (self: A, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime.PreserveZone -} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.DatePart, options?: { +} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutate(self, (date) => { switch (part) { + case "second": { + date.setUTCMilliseconds(999) + break + } + case "minute": { + date.setUTCSeconds(59, 999) + break + } + case "hour": { + date.setUTCMinutes(59, 59, 999) + break + } case "day": { date.setUTCHours(23, 59, 59, 999) break From e4871d62f972a4b99015f46e1041386b1fa47fe1 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 26 Jul 2024 12:13:02 +1200 Subject: [PATCH 61/61] implement DateTime.nearest --- packages/effect/src/DateTime.ts | 197 ++++++++++++++++---------- packages/effect/test/DateTime.test.ts | 26 ++++ 2 files changed, 150 insertions(+), 73 deletions(-) diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts index 732cc39921..5e1b6cc094 100644 --- a/packages/effect/src/DateTime.ts +++ b/packages/effect/src/DateTime.ts @@ -1734,6 +1734,47 @@ export const subtract: { return add(self, newParts) }) +function startOfDate(date: Date, part: DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) { + switch (part) { + case "second": { + date.setUTCMilliseconds(0) + break + } + case "minute": { + date.setUTCSeconds(0, 0) + break + } + case "hour": { + date.setUTCMinutes(0, 0, 0) + break + } + case "day": { + date.setUTCHours(0, 0, 0, 0) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff) + date.setUTCHours(0, 0, 0, 0) + break + } + case "month": { + date.setUTCDate(1) + date.setUTCHours(0, 0, 0, 0) + break + } + case "year": { + date.setUTCMonth(0, 1) + date.setUTCHours(0, 0, 0, 0) + break + } + } +} + /** * Converts a `DateTime` to the start of the given `part`. * @@ -1760,45 +1801,48 @@ export const startOf: { }): DateTime.PreserveZone } = dual(isDateTimeArgs, (self: DateTime, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined -}): DateTime => - mutate(self, (date) => { - switch (part) { - case "second": { - date.setUTCMilliseconds(0) - break - } - case "minute": { - date.setUTCSeconds(0, 0) - break - } - case "hour": { - date.setUTCMinutes(0, 0, 0) - break - } - case "day": { - date.setUTCHours(0, 0, 0, 0) - break - } - case "week": { - const weekStartsOn = options?.weekStartsOn ?? 0 - const day = date.getUTCDay() - const diff = (day - weekStartsOn + 7) % 7 - date.setUTCDate(date.getUTCDate() - diff) - date.setUTCHours(0, 0, 0, 0) - break - } - case "month": { - date.setUTCDate(1) - date.setUTCHours(0, 0, 0, 0) - break - } - case "year": { - date.setUTCMonth(0, 1) - date.setUTCHours(0, 0, 0, 0) - break - } +}): DateTime => mutate(self, (date) => startOfDate(date, part, options))) + +function endOfDate(date: Date, part: DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) { + switch (part) { + case "second": { + date.setUTCMilliseconds(999) + break } - })) + case "minute": { + date.setUTCSeconds(59, 999) + break + } + case "hour": { + date.setUTCMinutes(59, 59, 999) + break + } + case "day": { + date.setUTCHours(23, 59, 59, 999) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff + 6) + date.setUTCHours(23, 59, 59, 999) + break + } + case "month": { + date.setUTCMonth(date.getUTCMonth() + 1, 0) + date.setUTCHours(23, 59, 59, 999) + break + } + case "year": { + date.setUTCMonth(11, 31) + date.setUTCHours(23, 59, 59, 999) + break + } + } +} /** * Converts a `DateTime` to the end of the given `part`. @@ -1826,43 +1870,50 @@ export const endOf: { }): DateTime.PreserveZone } = dual(isDateTimeArgs, (self: DateTime, part: DateTime.UnitSingular, options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime => mutate(self, (date) => endOfDate(date, part, options))) + +/** + * Converts a `DateTime` to the nearest given `part`. + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * @since 3.6.0 + * @category math + * @example + * import { DateTime } from "effect" + * + * // returns "2024-01-02T00:00:00Z" + * DateTime.unsafeMake("2024-01-01T12:01:00Z").pipe( + * DateTime.nearest("day"), + * DateTime.formatIso + * ) + */ +export const nearest: { + (part: DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => DateTime.PreserveZone + (self: A, part: DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): DateTime.PreserveZone +} = dual(isDateTimeArgs, (self: DateTime, part: DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined }): DateTime => mutate(self, (date) => { - switch (part) { - case "second": { - date.setUTCMilliseconds(999) - break - } - case "minute": { - date.setUTCSeconds(59, 999) - break - } - case "hour": { - date.setUTCMinutes(59, 59, 999) - break - } - case "day": { - date.setUTCHours(23, 59, 59, 999) - break - } - case "week": { - const weekStartsOn = options?.weekStartsOn ?? 0 - const day = date.getUTCDay() - const diff = (day - weekStartsOn + 7) % 7 - date.setUTCDate(date.getUTCDate() - diff + 6) - date.setUTCHours(23, 59, 59, 999) - break - } - case "month": { - date.setUTCMonth(date.getUTCMonth() + 1, 0) - date.setUTCHours(23, 59, 59, 999) - break - } - case "year": { - date.setUTCMonth(11, 31) - date.setUTCHours(23, 59, 59, 999) - break - } + if (part === "milli") return + const millis = date.getTime() + const start = new Date(millis) + startOfDate(start, part, options) + const startMillis = start.getTime() + const end = new Date(millis) + endOfDate(end, part, options) + const endMillis = end.getTime() + 1 + const diffStart = millis - startMillis + const diffEnd = endMillis - millis + if (diffStart < diffEnd) { + date.setTime(startMillis) + } else { + date.setTime(endMillis) } })) diff --git a/packages/effect/test/DateTime.test.ts b/packages/effect/test/DateTime.test.ts index 11eb37d06a..e8454bb87d 100644 --- a/packages/effect/test/DateTime.test.ts +++ b/packages/effect/test/DateTime.test.ts @@ -168,6 +168,32 @@ describe("DateTime", () => { }) }) + describe("nearest", () => { + it("month up", () => { + const mar = DateTime.unsafeMake("2024-03-16T12:00:00.000Z") + const end = DateTime.nearest(mar, "month") + assert.strictEqual(end.toJSON(), "2024-04-01T00:00:00.000Z") + }) + + it("month down", () => { + const mar = DateTime.unsafeMake("2024-03-16T11:00:00.000Z") + const end = DateTime.nearest(mar, "month") + assert.strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") + }) + + it("second up", () => { + const mar = DateTime.unsafeMake("2024-03-20T12:00:00.500Z") + const end = DateTime.nearest(mar, "second") + assert.strictEqual(end.toJSON(), "2024-03-20T12:00:01.000Z") + }) + + it("second down", () => { + const mar = DateTime.unsafeMake("2024-03-20T12:00:00.400Z") + const end = DateTime.nearest(mar, "second") + assert.strictEqual(end.toJSON(), "2024-03-20T12:00:00.000Z") + }) + }) + describe("format", () => { it.effect("full", () => Effect.gen(function*() {