diff --git a/.changeset/sweet-bees-stare.md b/.changeset/sweet-bees-stare.md new file mode 100644 index 00000000000..399beb650d4 --- /dev/null +++ b/.changeset/sweet-bees-stare.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Arbitrary: optimize date-based refinements diff --git a/packages/effect/src/Arbitrary.ts b/packages/effect/src/Arbitrary.ts index 494c3567309..081807c900f 100644 --- a/packages/effect/src/Arbitrary.ts +++ b/packages/effect/src/Arbitrary.ts @@ -5,7 +5,7 @@ import * as Arr from "./Array.js" import * as FastCheck from "./FastCheck.js" import * as errors_ from "./internal/schema/errors.js" -import * as filters_ from "./internal/schema/filters.js" +import * as schemaId_ from "./internal/schema/schemaId.js" import * as util_ from "./internal/schema/util.js" import * as Option from "./Option.js" import * as Predicate from "./Predicate.js" @@ -28,7 +28,7 @@ export interface LazyArbitrary { export interface ArbitraryGenerationContext { readonly maxDepth: number readonly depthIdentifier?: string - readonly constraints?: StringConstraints | NumberConstraints | BigIntConstraints | ArrayConstraints + readonly constraints?: StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConstraints } /** @@ -110,6 +110,8 @@ class Deferred { } case "BigIntConstraints": return (fc) => fc.bigInt(config.constraints) + case "DateConstraints": + return (fc) => fc.date(config.constraints) case "ArrayConstraints": return goTupleType(config.ast, ctx, path, config.constraints) } @@ -232,6 +234,32 @@ export const makeArrayConstraints = (options: { return out } +interface DateConstraints { + readonly _tag: "DateConstraints" + readonly constraints: FastCheck.DateConstraints +} + +/** @internal */ +export const makeDateConstraints = (options: { + readonly min?: Date | undefined + readonly max?: Date | undefined + readonly noInvalidDate?: boolean | undefined +}): DateConstraints => { + const out: Types.Mutable = { + _tag: "DateConstraints", + constraints: { + noInvalidDate: options.noInvalidDate ?? false + } + } + if (Predicate.isDate(options.min)) { + out.constraints.min = options.min + } + if (Predicate.isDate(options.max)) { + out.constraints.max = options.max + } + return out +} + interface ArrayConfig extends ArrayConstraints { readonly ast: AST.TupleType } @@ -246,7 +274,7 @@ const makeArrayConfig = (options: { } } -type Config = StringConstraints | NumberConstraints | BigIntConstraints | ArrayConfig +type Config = StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConfig const go = ( ast: AST.AST, @@ -291,8 +319,14 @@ export const toOp = ( path: ReadonlyArray ): Op => { switch (ast._tag) { - case "Declaration": + case "Declaration": { + const TypeAnnotationId: any = ast.annotations[AST.SchemaIdAnnotationId] + switch (TypeAnnotationId) { + case schemaId_.DateFromSelfSchemaId: + return new Deferred(makeDateConstraints(ast.annotations[TypeAnnotationId] as any)) + } return new Succeed(go(ast, ctx, path)) + } case "Literal": return new Succeed((fc) => fc.constant(ast.literal)) case "UniqueSymbol": @@ -482,7 +516,7 @@ const goTupleType = ( } } -type Constraints = StringConstraints | NumberConstraints | BigIntConstraints | ArrayConstraints +type Constraints = StringConstraints | NumberConstraints | BigIntConstraints | DateConstraints | ArrayConstraints const getConstraints = (_tag: Constraints["_tag"], ast: AST.Refinement): Constraints | undefined => { const TypeAnnotationId: any = ast.annotations[AST.SchemaIdAnnotationId] @@ -493,7 +527,7 @@ const getConstraints = (_tag: Constraints["_tag"], ast: AST.Refinement): Constra return makeStringConstraints(jsonSchema) case "NumberConstraints": { switch (TypeAnnotationId) { - case filters_.NonNaNSchemaId: + case schemaId_.NonNaNSchemaId: return makeNumberConstraints({ noNaN: true }) default: return makeNumberConstraints({ @@ -509,6 +543,8 @@ const getConstraints = (_tag: Constraints["_tag"], ast: AST.Refinement): Constra } case "BigIntConstraints": return makeBigIntConstraints(ast.annotations[TypeAnnotationId] as any) + case "DateConstraints": + return makeDateConstraints(ast.annotations[TypeAnnotationId] as any) case "ArrayConstraints": return makeArrayConstraints({ minLength: jsonSchema.minItems, @@ -517,21 +553,23 @@ const getConstraints = (_tag: Constraints["_tag"], ast: AST.Refinement): Constra } } +function getMax(n1: Date | undefined, n2: Date | undefined): Date | undefined function getMax(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined function getMax(n1: number | undefined, n2: number | undefined): number | undefined function getMax( - n1: bigint | number | undefined, - n2: bigint | number | undefined -): bigint | number | undefined { + n1: bigint | number | Date | undefined, + n2: bigint | number | Date | undefined +): bigint | number | Date | undefined { return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n2 : n1 } +function getMin(n1: Date | undefined, n2: Date | undefined): Date | undefined function getMin(n1: bigint | undefined, n2: bigint | undefined): bigint | undefined function getMin(n1: number | undefined, n2: number | undefined): number | undefined function getMin( - n1: bigint | number | undefined, - n2: bigint | number | undefined -): bigint | number | undefined { + n1: bigint | number | Date | undefined, + n2: bigint | number | Date | undefined +): bigint | number | Date | undefined { return n1 === undefined ? n2 : n2 === undefined ? n1 : n1 <= n2 ? n1 : n2 } @@ -575,6 +613,16 @@ const merge = (c1: Config, c2: Constraints | undefined): Config => { } break } + case "DateConstraints": { + if (c2._tag === "DateConstraints") { + return makeDateConstraints({ + min: getMax(c1.constraints.min, c2.constraints.min), + max: getMin(c1.constraints.max, c2.constraints.max), + noInvalidDate: getOr(c1.constraints.noInvalidDate, c2.constraints.noInvalidDate) + }) + } + break + } case "ArrayConstraints": { if (c2._tag === "ArrayConstraints") { return makeArrayConfig({ diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index d75e95898cf..89cb8071dc2 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -29,7 +29,7 @@ import { globalValue } from "./GlobalValue.js" import * as hashMap_ from "./HashMap.js" import * as hashSet_ from "./HashSet.js" import * as errors_ from "./internal/schema/errors.js" -import * as filters_ from "./internal/schema/filters.js" +import * as schemaId_ from "./internal/schema/schemaId.js" import * as util_ from "./internal/schema/util.js" import * as list_ from "./List.js" import * as number_ from "./Number.js" @@ -4035,7 +4035,7 @@ export const trimmed = * @category schema id * @since 3.10.0 */ -export const MaxLengthSchemaId: unique symbol = filters_.MaxLengthSchemaId +export const MaxLengthSchemaId: unique symbol = schemaId_.MaxLengthSchemaId /** * @category schema id @@ -4068,7 +4068,7 @@ export const maxLength = ( * @category schema id * @since 3.10.0 */ -export const MinLengthSchemaId: unique symbol = filters_.MinLengthSchemaId +export const MinLengthSchemaId: unique symbol = schemaId_.MinLengthSchemaId /** * @category schema id @@ -4343,7 +4343,7 @@ export class Uppercased extends String$.pipe( * @category schema id * @since 3.10.0 */ -export const LengthSchemaId: unique symbol = filters_.LengthSchemaId +export const LengthSchemaId: unique symbol = schemaId_.LengthSchemaId /** * @category schema id @@ -4676,7 +4676,7 @@ export { * @category schema id * @since 3.10.0 */ -export const FiniteSchemaId: unique symbol = filters_.FiniteSchemaId +export const FiniteSchemaId: unique symbol = schemaId_.FiniteSchemaId /** * @category schema id @@ -4707,7 +4707,7 @@ export const finite = * @category schema id * @since 3.10.0 */ -export const GreaterThanSchemaId: unique symbol = filters_.GreaterThanSchemaId +export const GreaterThanSchemaId: unique symbol = schemaId_.GreaterThanSchemaId /** * @category schema id @@ -4722,15 +4722,15 @@ export type GreaterThanSchemaId = typeof GreaterThanSchemaId * @since 3.10.0 */ export const greaterThan = ( - min: number, + exclusiveMinimum: number, annotations?: Annotations.Filter ) => (self: Schema): filter> => self.pipe( - filter((a) => a > min, { + filter((a) => a > exclusiveMinimum, { schemaId: GreaterThanSchemaId, - description: min === 0 ? "a positive number" : `a number greater than ${min}`, - jsonSchema: { exclusiveMinimum: min }, + description: exclusiveMinimum === 0 ? "a positive number" : `a number greater than ${exclusiveMinimum}`, + jsonSchema: { exclusiveMinimum }, ...annotations }) ) @@ -4739,7 +4739,7 @@ export const greaterThan = ( * @category schema id * @since 3.10.0 */ -export const GreaterThanOrEqualToSchemaId: unique symbol = filters_.GreaterThanOrEqualToSchemaId +export const GreaterThanOrEqualToSchemaId: unique symbol = schemaId_.GreaterThanOrEqualToSchemaId /** * @category schema id @@ -4754,15 +4754,15 @@ export type GreaterThanOrEqualToSchemaId = typeof GreaterThanOrEqualToSchemaId * @since 3.10.0 */ export const greaterThanOrEqualTo = ( - min: number, + minimum: number, annotations?: Annotations.Filter ) => (self: Schema): filter> => self.pipe( - filter((a) => a >= min, { + filter((a) => a >= minimum, { schemaId: GreaterThanOrEqualToSchemaId, - description: min === 0 ? "a non-negative number" : `a number greater than or equal to ${min}`, - jsonSchema: { minimum: min }, + description: minimum === 0 ? "a non-negative number" : `a number greater than or equal to ${minimum}`, + jsonSchema: { minimum }, ...annotations }) ) @@ -4795,7 +4795,7 @@ export const multipleOf = ( * @category schema id * @since 3.10.0 */ -export const IntSchemaId: unique symbol = filters_.IntSchemaId +export const IntSchemaId: unique symbol = schemaId_.IntSchemaId /** * @category schema id @@ -4823,7 +4823,7 @@ export const int = * @category schema id * @since 3.10.0 */ -export const LessThanSchemaId: unique symbol = filters_.LessThanSchemaId +export const LessThanSchemaId: unique symbol = schemaId_.LessThanSchemaId /** * @category schema id @@ -4838,13 +4838,13 @@ export type LessThanSchemaId = typeof LessThanSchemaId * @since 3.10.0 */ export const lessThan = - (max: number, annotations?: Annotations.Filter) => + (exclusiveMaximum: number, annotations?: Annotations.Filter) => (self: Schema): filter> => self.pipe( - filter((a) => a < max, { + filter((a) => a < exclusiveMaximum, { schemaId: LessThanSchemaId, - description: max === 0 ? "a negative number" : `a number less than ${max}`, - jsonSchema: { exclusiveMaximum: max }, + description: exclusiveMaximum === 0 ? "a negative number" : `a number less than ${exclusiveMaximum}`, + jsonSchema: { exclusiveMaximum }, ...annotations }) ) @@ -4853,7 +4853,7 @@ export const lessThan = * @category schema id * @since 3.10.0 */ -export const LessThanOrEqualToSchemaId: unique symbol = filters_.LessThanOrEqualToSchemaId +export const LessThanOrEqualToSchemaId: unique symbol = schemaId_.LessThanOrEqualToSchemaId /** * @category schema id @@ -4868,15 +4868,15 @@ export type LessThanOrEqualToSchemaId = typeof LessThanOrEqualToSchemaId * @since 3.10.0 */ export const lessThanOrEqualTo = ( - max: number, + maximum: number, annotations?: Annotations.Filter ) => (self: Schema): filter> => self.pipe( - filter((a) => a <= max, { + filter((a) => a <= maximum, { schemaId: LessThanOrEqualToSchemaId, - description: max === 0 ? "a non-positive number" : `a number less than or equal to ${max}`, - jsonSchema: { maximum: max }, + description: maximum === 0 ? "a non-positive number" : `a number less than or equal to ${maximum}`, + jsonSchema: { maximum }, ...annotations }) ) @@ -4885,7 +4885,7 @@ export const lessThanOrEqualTo = ( * @category schema id * @since 3.10.0 */ -export const BetweenSchemaId: unique symbol = filters_.BetweenSchemaId +export const BetweenSchemaId: unique symbol = schemaId_.BetweenSchemaId /** * @category schema id @@ -4900,16 +4900,16 @@ export type BetweenSchemaId = typeof BetweenSchemaId * @since 3.10.0 */ export const between = ( - min: number, - max: number, + minimum: number, + maximum: number, annotations?: Annotations.Filter ) => (self: Schema): filter> => self.pipe( - filter((a) => a >= min && a <= max, { + filter((a) => a >= minimum && a <= maximum, { schemaId: BetweenSchemaId, - description: `a number between ${min} and ${max}`, - jsonSchema: { maximum: max, minimum: min }, + description: `a number between ${minimum} and ${maximum}`, + jsonSchema: { minimum, maximum }, ...annotations }) ) @@ -4918,7 +4918,7 @@ export const between = ( * @category schema id * @since 3.10.0 */ -export const NonNaNSchemaId: unique symbol = filters_.NonNaNSchemaId +export const NonNaNSchemaId: unique symbol = schemaId_.NonNaNSchemaId /** * @category schema id @@ -5078,7 +5078,7 @@ export class NonNegative extends Number$.pipe( * @category schema id * @since 3.10.0 */ -export const JsonNumberSchemaId: unique symbol = filters_.JsonNumberSchemaId +export const JsonNumberSchemaId: unique symbol = schemaId_.JsonNumberSchemaId /** * @category schema id @@ -5146,7 +5146,7 @@ export { * @category schema id * @since 3.10.0 */ -export const GreaterThanBigIntSchemaId: unique symbol = filters_.GreaterThanBigintSchemaId +export const GreaterThanBigIntSchemaId: unique symbol = schemaId_.GreaterThanBigintSchemaId /** * @category schema id @@ -5176,7 +5176,7 @@ export const greaterThanBigInt = ( * @category schema id * @since 3.10.0 */ -export const GreaterThanOrEqualToBigIntSchemaId: unique symbol = filters_.GreaterThanOrEqualToBigIntSchemaId +export const GreaterThanOrEqualToBigIntSchemaId: unique symbol = schemaId_.GreaterThanOrEqualToBigIntSchemaId /** * @category schema id @@ -5208,7 +5208,7 @@ export const greaterThanOrEqualToBigInt = ( * @category schema id * @since 3.10.0 */ -export const LessThanBigIntSchemaId: unique symbol = filters_.LessThanBigIntSchemaId +export const LessThanBigIntSchemaId: unique symbol = schemaId_.LessThanBigIntSchemaId /** * @category schema id @@ -5238,7 +5238,7 @@ export const lessThanBigInt = ( * @category schema id * @since 3.10.0 */ -export const LessThanOrEqualToBigIntSchemaId: unique symbol = filters_.LessThanOrEqualToBigIntSchemaId +export const LessThanOrEqualToBigIntSchemaId: unique symbol = schemaId_.LessThanOrEqualToBigIntSchemaId /** * @category schema id @@ -5268,7 +5268,7 @@ export const lessThanOrEqualToBigInt = ( * @category schema id * @since 3.10.0 */ -export const BetweenBigIntSchemaId: unique symbol = filters_.BetweenBigintSchemaId +export const BetweenBigIntSchemaId: unique symbol = schemaId_.BetweenBigintSchemaId /** * @category schema id @@ -5289,7 +5289,7 @@ export const betweenBigInt = ( self.pipe( filter((a) => a >= min && a <= max, { schemaId: BetweenBigIntSchemaId, - [BetweenBigIntSchemaId]: { max, min }, + [BetweenBigIntSchemaId]: { min, max }, description: `a bigint between ${min}n and ${max}n`, ...annotations }) @@ -5921,7 +5921,7 @@ export const StringFromHex: Schema = makeEncodingTransformation( * @category schema id * @since 3.10.0 */ -export const MinItemsSchemaId: unique symbol = filters_.MinItemsSchemaId +export const MinItemsSchemaId: unique symbol = schemaId_.MinItemsSchemaId /** * @category schema id @@ -5962,7 +5962,7 @@ export const minItems = ( * @category schema id * @since 3.10.0 */ -export const MaxItemsSchemaId: unique symbol = filters_.MaxItemsSchemaId +export const MaxItemsSchemaId: unique symbol = schemaId_.MaxItemsSchemaId /** * @category schema id @@ -5993,7 +5993,7 @@ export const maxItems = ( * @category schema id * @since 3.10.0 */ -export const ItemsCountSchemaId: unique symbol = filters_.ItemsCountSchemaId +export const ItemsCountSchemaId: unique symbol = schemaId_.ItemsCountSchemaId /** * @category schema id @@ -6091,6 +6091,7 @@ export const validDate = self.pipe( filter((a) => !Number.isNaN(a.getTime()), { schemaId: ValidDateSchemaId, + [ValidDateSchemaId]: { noInvalidDate: true }, description: "a valid Date", ...annotations }) @@ -6207,20 +6208,32 @@ export const BetweenDateSchemaId: unique symbol = Symbol.for("effect/SchemaId/Be * @since 3.10.0 */ export const betweenDate = ( - minimum: Date, - maximum: Date, + min: Date, + max: Date, annotations?: Annotations.Filter ) => (self: Schema): filter> => self.pipe( - filter((a) => a <= maximum && a >= minimum, { + filter((a) => a <= max && a >= min, { schemaId: BetweenDateSchemaId, - [BetweenDateSchemaId]: { maximum, minimum }, - description: `a date between ${util_.formatDate(minimum)} and ${util_.formatDate(maximum)}`, + [BetweenDateSchemaId]: { max, min }, + description: `a date between ${util_.formatDate(min)} and ${util_.formatDate(max)}`, ...annotations }) ) +/** + * @category schema id + * @since 3.11.8 + */ +export const DateFromSelfSchemaId: unique symbol = schemaId_.DateFromSelfSchemaId + +/** + * @category schema id + * @since 3.11.8 + */ +export type DateFromSelfSchemaId = typeof DateFromSelfSchemaId + /** * Describes a schema that accommodates potentially invalid `Date` instances, * such as `new Date("Invalid Date")`, without rejection. @@ -6232,6 +6245,8 @@ export class DateFromSelf extends declare( Predicate.isDate, { identifier: "DateFromSelf", + schemaId: DateFromSelfSchemaId, + [DateFromSelfSchemaId]: { noInvalidDate: false }, description: "a potentially invalid Date instance", pretty: () => (date) => `new Date(${JSON.stringify(date)})`, arbitrary: () => (fc) => fc.date({ noInvalidDate: false }), diff --git a/packages/effect/src/internal/schema/filters.ts b/packages/effect/src/internal/schema/schemaId.ts similarity index 95% rename from packages/effect/src/internal/schema/filters.ts rename to packages/effect/src/internal/schema/schemaId.ts index 444e133a42b..21164b9e3c2 100644 --- a/packages/effect/src/internal/schema/filters.ts +++ b/packages/effect/src/internal/schema/schemaId.ts @@ -1,5 +1,10 @@ import type * as Schema from "../../Schema.js" +/** @internal */ +export const DateFromSelfSchemaId: Schema.DateFromSelfSchemaId = Symbol.for( + "effect/SchemaId/DateFromSelf" +) as Schema.DateFromSelfSchemaId + /** @internal */ export const GreaterThanSchemaId: Schema.GreaterThanSchemaId = Symbol.for( "effect/SchemaId/GreaterThan" diff --git a/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts b/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts index f467fcded5c..b77fd22df8a 100644 --- a/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts +++ b/packages/effect/test/Schema/Arbitrary/Arbitrary.test.ts @@ -12,6 +12,7 @@ const expectConstraints = ( | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType ) => { const ast = schema.ast @@ -23,6 +24,7 @@ const expectConstraints = ( case "StringConstraints": case "NumberConstraints": case "BigIntConstraints": + case "DateConstraints": return expect(op.config).toEqual(constraints) case "ArrayConstraints": { const { ast: _ast, ...rest } = op.config @@ -146,6 +148,16 @@ schema (NeverKeyword): never`) expectValidArbitrary(schema) }) + it("DateFromSelf", () => { + const schema = S.DateFromSelf + expectValidArbitrary(schema) + }) + + it("DurationFromSelf", () => { + const schema = S.DurationFromSelf + expectValidArbitrary(schema) + }) + describe("TemplateLiteral", () => { it("a", () => { const schema = S.TemplateLiteral(S.Literal("a")) @@ -723,6 +735,47 @@ details: Generating an Arbitrary for this schema requires at least one enum`) expectValidArbitrary(schema) }) }) + + describe("date filters", () => { + it("ValidDateFromSelf", () => { + const schema = S.ValidDateFromSelf + expectConstraints(schema, Arbitrary.makeDateConstraints({ noInvalidDate: true })) + expectValidArbitrary(schema) + }) + + it("lessThanOrEqualTo", () => { + const schema = S.DateFromSelf.pipe(S.lessThanOrEqualToDate(new Date(5))) + expectConstraints(schema, Arbitrary.makeDateConstraints({ noInvalidDate: false, max: new Date(5) })) + expectValidArbitrary(schema) + }) + + it("greaterThanOrEqualTo", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanOrEqualToDate(new Date(2))) + expectConstraints(schema, Arbitrary.makeDateConstraints({ noInvalidDate: false, min: new Date(2) })) + expectValidArbitrary(schema) + }) + + it("lessThan", () => { + const schema = S.DateFromSelf.pipe(S.lessThanDate(new Date(5))) + expectConstraints(schema, Arbitrary.makeDateConstraints({ noInvalidDate: false, max: new Date(5) })) + expectValidArbitrary(schema) + }) + + it("greaterThan", () => { + const schema = S.DateFromSelf.pipe(S.greaterThanDate(new Date(2))) + expectConstraints(schema, Arbitrary.makeDateConstraints({ noInvalidDate: false, min: new Date(2) })) + expectValidArbitrary(schema) + }) + + it("between", () => { + const schema = S.DateFromSelf.pipe(S.betweenDate(new Date(2), new Date(5))) + expectConstraints( + schema, + Arbitrary.makeDateConstraints({ noInvalidDate: false, min: new Date(2), max: new Date(5) }) + ) + expectValidArbitrary(schema) + }) + }) }) describe("Transformation", () => {