diff --git a/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts
index 732cc399214..5e1b6cc0942 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 11eb37d06a1..e8454bb87d4 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*() {