Skip to content

Commit

Permalink
add Date filters (less than, greater than, between), closes #3606 (#3619
Browse files Browse the repository at this point in the history
)
  • Loading branch information
gcanti authored Sep 17, 2024
1 parent cd75658 commit f56ab78
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-balloons-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

add Date filters (less than, greater than, between), closes #3606
122 changes: 121 additions & 1 deletion packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5960,6 +5960,126 @@ export const validDate =
})
)

/**
* @category type id
* @since 0.73.1
*/
export const LessThanDateTypeId: unique symbol = Symbol.for("@effect/schema/TypeId/LessThanDate")

/**
* @category Date filters
* @since 0.73.1
*/
export const lessThanDate = <A extends Date>(
max: Date,
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): filter<Schema<A, I, R>> =>
self.pipe(
filter((a) => a < max, {
typeId: { id: LessThanDateTypeId, annotation: { max } },
description: `a date before ${util_.formatDate(max)}`,
...annotations
})
)

/**
* @category type id
* @since 0.73.1
*/
export const LessThanOrEqualToDateTypeId: unique symbol = Symbol.for(
"@effect/schema/TypeId/LessThanOrEqualToDate"
)

/**
* @category Date filters
* @since 0.73.1
*/
export const lessThanOrEqualToDate = <A extends Date>(
max: Date,
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): filter<Schema<A, I, R>> =>
self.pipe(
filter((a) => a <= max, {
typeId: { id: LessThanDateTypeId, annotation: { max } },
description: `a date before or equal to ${util_.formatDate(max)}`,
...annotations
})
)

/**
* @category type id
* @since 0.73.1
*/
export const GreaterThanDateTypeId: unique symbol = Symbol.for("@effect/schema/TypeId/GreaterThanDate")

/**
* @category Date filters
* @since 0.73.1
*/
export const greaterThanDate = <A extends Date>(
min: Date,
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): filter<Schema<A, I, R>> =>
self.pipe(
filter((a) => a > min, {
typeId: { id: GreaterThanDateTypeId, annotation: { min } },
description: `a date after ${util_.formatDate(min)}`,
...annotations
})
)

/**
* @category type id
* @since 0.73.1
*/
export const GreaterThanOrEqualToDateTypeId: unique symbol = Symbol.for(
"@effect/schema/TypeId/GreaterThanOrEqualToDate"
)

/**
* @category Date filters
* @since 0.73.1
*/
export const greaterThanOrEqualToDate = <A extends Date>(
min: Date,
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): filter<Schema<A, I, R>> =>
self.pipe(
filter((a) => a >= min, {
typeId: { id: GreaterThanOrEqualToDateTypeId, annotation: { min } },
description: `a date after or equal to ${util_.formatDate(min)}`,
...annotations
})
)

/**
* @category type id
* @since 0.73.1
*/
export const BetweenDateTypeId: unique symbol = Symbol.for("@effect/schema/TypeId/BetweenDate")

/**
* @category Date filters
* @since 0.73.1
*/
export const betweenDate = <A extends Date>(
minimum: Date,
maximum: Date,
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): filter<Schema<A, I, R>> =>
self.pipe(
filter((a) => a <= maximum && a >= minimum, {
typeId: { id: BetweenDateTypeId, annotation: { maximum, minimum } },
description: `a date between ${util_.formatDate(minimum)} and ${util_.formatDate(maximum)}`,
...annotations
})
)

/**
* Describes a schema that accommodates potentially invalid `Date` instances,
* such as `new Date("Invalid Date")`, without rejection.
Expand Down Expand Up @@ -6007,7 +6127,7 @@ export class ValidDateFromSelf extends DateFromSelf.pipe(
export class DateFromString extends transform(
String$,
DateFromSelf,
{ strict: true, decode: (s) => new Date(s), encode: (d) => d.toISOString() }
{ strict: true, decode: (s) => new Date(s), encode: (d) => util_.formatDate(d) }
).annotations({ identifier: "DateFromString" }) {}

/** @ignore */
Expand Down
12 changes: 11 additions & 1 deletion packages/schema/src/internal/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export const memoizeThunk = <A>(f: () => A): () => A => {
}
}

/** @internal */
export const formatDate = (date: Date): string => {
try {
return date.toISOString()
} catch (e) {
return String(date)
}
}

/** @internal */
export const formatUnknown = (u: unknown): string => {
if (Predicate.isString(u)) {
Expand All @@ -46,9 +55,10 @@ export const formatUnknown = (u: unknown): string => {
|| u == null
|| Predicate.isBoolean(u)
|| Predicate.isSymbol(u)
|| Predicate.isDate(u)
) {
return String(u)
} else if (Predicate.isDate(u)) {
return formatDate(u)
} else if (Predicate.isBigInt(u)) {
return String(u) + "n"
} else if (
Expand Down
37 changes: 37 additions & 0 deletions packages/schema/test/Schema/Date/betweenDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("betweenDate", () => {
const schema = S.DateFromSelf.pipe(S.betweenDate(new Date(-1), new Date(1)))

it("decoding", async () => {
await Util.expectDecodeUnknownSuccess(
schema,
new Date(-1)
)
await Util.expectDecodeUnknownSuccess(
schema,
new Date(0)
)
await Util.expectDecodeUnknownSuccess(
schema,
new Date(1)
)

await Util.expectDecodeUnknownFailure(
schema,
new Date(-2),
`a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z
└─ Predicate refinement failure
└─ Expected a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z, actual 1969-12-31T23:59:59.998Z`
)
await Util.expectDecodeUnknownFailure(
schema,
new Date(2),
`a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z
└─ Predicate refinement failure
└─ Expected a date between 1969-12-31T23:59:59.999Z and 1970-01-01T00:00:00.001Z, actual 1970-01-01T00:00:00.002Z`
)
})
})
29 changes: 29 additions & 0 deletions packages/schema/test/Schema/Date/greaterThanDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("greaterThanDate", () => {
const schema = S.DateFromSelf.pipe(S.greaterThanDate(new Date(0)))

it("decoding", async () => {
await Util.expectDecodeUnknownSuccess(
schema,
new Date(1)
)

await Util.expectDecodeUnknownFailure(
schema,
new Date(0),
`a date after 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date after 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.000Z`
)
await Util.expectDecodeUnknownFailure(
schema,
new Date(-1),
`a date after 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date after 1970-01-01T00:00:00.000Z, actual 1969-12-31T23:59:59.999Z`
)
})
})
26 changes: 26 additions & 0 deletions packages/schema/test/Schema/Date/greaterThanOrEqualToDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("greaterThanOrEqualToDate", () => {
const schema = S.DateFromSelf.pipe(S.greaterThanOrEqualToDate(new Date(0)))

it("decoding", async () => {
await Util.expectDecodeUnknownSuccess(
schema,
new Date(1)
)
await Util.expectDecodeUnknownSuccess(
schema,
new Date(0)
)

await Util.expectDecodeUnknownFailure(
schema,
new Date(-1),
`a date after or equal to 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date after or equal to 1970-01-01T00:00:00.000Z, actual 1969-12-31T23:59:59.999Z`
)
})
})
29 changes: 29 additions & 0 deletions packages/schema/test/Schema/Date/lessThanDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("lessThanDate", () => {
const schema = S.DateFromSelf.pipe(S.lessThanDate(new Date(0)))

it("decoding", async () => {
await Util.expectDecodeUnknownSuccess(
schema,
new Date(-1)
)

await Util.expectDecodeUnknownFailure(
schema,
new Date(0),
`a date before 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date before 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.000Z`
)
await Util.expectDecodeUnknownFailure(
schema,
new Date(1),
`a date before 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date before 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.001Z`
)
})
})
26 changes: 26 additions & 0 deletions packages/schema/test/Schema/Date/lessThanOrEqualToDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("lessThanOrEqualToDate", () => {
const schema = S.DateFromSelf.pipe(S.lessThanOrEqualToDate(new Date(0)))

it("decoding", async () => {
await Util.expectDecodeUnknownSuccess(
schema,
new Date(-1)
)
await Util.expectDecodeUnknownSuccess(
schema,
new Date(0)
)

await Util.expectDecodeUnknownFailure(
schema,
new Date(1),
`a date before or equal to 1970-01-01T00:00:00.000Z
└─ Predicate refinement failure
└─ Expected a date before or equal to 1970-01-01T00:00:00.000Z, actual 1970-01-01T00:00:00.001Z`
)
})
})
2 changes: 1 addition & 1 deletion packages/schema/test/Schema/Either/EitherFromUnion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe("EitherFromUnion", () => {
└─ Encoded side transformation failure
└─ NumberFromString
└─ Type side transformation failure
└─ Expected number, actual ${new Date(0).toString()}`
└─ Expected number, actual ${new Date(0).toISOString()}`
)
})
})

0 comments on commit f56ab78

Please sign in to comment.