Skip to content

Commit

Permalink
feat: support min/max exclusive (fixes #50)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerAberbach committed Dec 26, 2024
1 parent 6fab1ca commit fcc5d50
Show file tree
Hide file tree
Showing 28 changed files with 766 additions and 88 deletions.
17 changes: 15 additions & 2 deletions src/arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ export const numberArbitrary = (

export type NumberArbitrary = {
type: `number`
min: number
max: number
min: {
value: number
exclusive: boolean
}
max: {
value: number
exclusive: boolean
}
isInteger: boolean
}

Expand Down Expand Up @@ -244,6 +250,13 @@ const getArbitraryKey = (arbitrary: Arbitrary): ArbitraryKey => {
case `constant`:
return keyalesce([arbitrary.type, arbitrary.value])
case `number`:
return keyalesce([
arbitrary.type,
arbitrary.min.value,
arbitrary.min.exclusive,
arbitrary.max.value,
arbitrary.max.exclusive,
])
case `bigint`:
return keyalesce([arbitrary.type, arbitrary.min, arbitrary.max])
case `string`:
Expand Down
46 changes: 29 additions & 17 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,50 +371,62 @@ const NumberArbitrary = ({
filter(
([, { isInteger, min, max }]) =>
arbitrary.isInteger === isInteger &&
(arbitrary.min === min.value ||
((!arbitrary.min.exclusive && arbitrary.min.value === min.value) ||
min.configurable === true ||
(min.configurable === `higher` && arbitrary.min >= min.value)) &&
(arbitrary.max === max.value ||
(min.configurable === `higher` &&
arbitrary.min.value >= min.value)) &&
((!arbitrary.max.exclusive && arbitrary.max.value === max.value) ||
max.configurable === true ||
(max.configurable === `lower` && arbitrary.max <= max.value)),
(max.configurable === `lower` && arbitrary.max.value <= max.value)),
),
minBy(([, a], [, b]) => {
const matchCount1 =
Number(arbitrary.min === a.min.value) +
Number(arbitrary.max === a.max.value)
Number(arbitrary.min.value === a.min.value) +
Number(arbitrary.max.value === a.max.value)
const matchCount2 =
Number(arbitrary.min === b.min.value) +
Number(arbitrary.max === b.max.value)
Number(arbitrary.min.value === b.min.value) +
Number(arbitrary.max.value === b.max.value)
if (matchCount1 !== matchCount2) {
return matchCount2 - matchCount1
}

const delta1 =
Math.abs(arbitrary.min - a.min.value) +
Math.abs(arbitrary.max - a.max.value)
Math.abs(arbitrary.min.value - a.min.value) +
Math.abs(arbitrary.max.value - a.max.value)
const delta2 =
Math.abs(arbitrary.min - b.min.value) +
Math.abs(arbitrary.max - b.max.value)
Math.abs(arbitrary.min.value - b.min.value) +
Math.abs(arbitrary.max.value - b.max.value)
return delta1 - delta2
}),
get,
)

let arbitraryMin = arbitrary.min === min.value ? null : arbitrary.min
let arbitraryMin =
!arbitrary.min.exclusive && arbitrary.min.value === min.value
? null
: arbitrary.min
if (arbitraryMin !== null && min.normalize) {
arbitraryMin = min.normalize(arbitraryMin)
arbitraryMin = { ...arbitraryMin, value: min.normalize(arbitraryMin.value) }
}

let arbitraryMax = arbitrary.max === max.value ? null : arbitrary.max
let arbitraryMax =
!arbitrary.max.exclusive && arbitrary.max.value === max.value
? null
: arbitrary.max
if (arbitraryMax !== null && max.normalize) {
arbitraryMax = max.normalize(arbitraryMax)
arbitraryMax = { ...arbitraryMax, value: max.normalize(arbitraryMax.value) }
}

return CallExpression({
name: `fc.${name}`,
args: [
ObjectExpression({
properties: { min: arbitraryMin, max: arbitraryMax },
properties: {
[arbitraryMin?.exclusive ? `minExclusive` : `min`]:
arbitraryMin?.value,
[arbitraryMax?.exclusive ? `maxExclusive` : `max`]:
arbitraryMax?.value,
},
singlePropertyOneLine: true,
}),
],
Expand Down
10 changes: 10 additions & 0 deletions src/constraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
getMaxItemsAsNumeric,
getMaxLengthAsNumeric,
getMaxValueAsNumeric,
getMaxValueExclusiveAsNumeric,
getMinItemsAsNumeric,
getMinLengthAsNumeric,
getMinValueAsNumeric,
getMinValueExclusiveAsNumeric,
} from '@typespec/compiler'
import type { Numeric, Program, Type } from '@typespec/compiler'
import keyalesce from 'keyalesce'
Expand All @@ -21,6 +23,8 @@ export const getConstraints = (program: Program, type: Type): Constraints =>
entries({
min: getMinValueAsNumeric(program, type),
max: getMaxValueAsNumeric(program, type),
minExclusive: getMinValueExclusiveAsNumeric(program, type),
maxExclusive: getMaxValueExclusiveAsNumeric(program, type),
minLength: getMinLengthAsNumeric(program, type),
maxLength: getMaxLengthAsNumeric(program, type),
minItems: getMinItemsAsNumeric(program, type),
Expand All @@ -34,6 +38,8 @@ export const getConstraints = (program: Program, type: Type): Constraints =>
export type Constraints = {
min?: Numeric
max?: Numeric
minExclusive?: Numeric
maxExclusive?: Numeric
minLength?: Numeric
maxLength?: Numeric
minItems?: Numeric
Expand All @@ -53,6 +59,8 @@ const memoize = (constraints: Constraints): Constraints => {
const getConstraintsKey = ({
min,
max,
minExclusive,
maxExclusive,
minLength,
maxLength,
minItems,
Expand All @@ -61,6 +69,8 @@ const getConstraintsKey = ({
keyalesce([
min?.asBigInt(),
max?.asBigInt(),
minExclusive?.asBigInt(),
maxExclusive?.asBigInt(),
minLength?.asBigInt(),
maxLength?.asBigInt(),
minItems?.asBigInt(),
Expand Down
76 changes: 64 additions & 12 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ import type {
} from './arbitrary.ts'
import {
fastCheckNumerics,
maxOrUndefined,
minOrUndefined,
maxOfMinBounds,
minOfMaxBounds,
numerics,
} from './numerics.ts'
import type { Bound } from './numerics.ts'
import normalizeArbitrary from './normalize.ts'
import { collectSharedArbitraries } from './dependency-graph.ts'
import type { SharedArbitraries } from './dependency-graph.ts'
Expand Down Expand Up @@ -345,13 +346,38 @@ const convertNumber = (
constraints: Constraints,
{ min, max, isInteger }: { min: number; max: number; isInteger: boolean },
): Arbitrary => {
const arbitrary = numberArbitrary({
min: maxOrUndefined(constraints.min?.asNumber() ?? undefined, min),
max: minOrUndefined(constraints.max?.asNumber() ?? undefined, max),
isInteger,
})
const minBound = [
{ value: constraints.min?.asNumber(), exclusive: false },
{ value: constraints.minExclusive?.asNumber(), exclusive: true },
{ value: min, exclusive: false },
]
.filter((bound): bound is Bound<number> => bound.value != null)
.reduce(maxOfMinBounds)
const maxBound = [
{ value: constraints.max?.asNumber(), exclusive: false },
{ value: constraints.maxExclusive?.asNumber(), exclusive: true },
{ value: max, exclusive: false },
]
.filter((bound): bound is Bound<number> => bound.value != null)
.reduce(minOfMaxBounds)
if (isInteger) {
if (minBound.exclusive) {
minBound.exclusive = false
minBound.value++
}
if (maxBound.exclusive) {
maxBound.exclusive = false
maxBound.value--
}
}

const arbitrary = numberArbitrary({ min: minBound, max: maxBound, isInteger })

const hasDefaultConstraints = arbitrary.min === min && arbitrary.max === max
const hasDefaultConstraints =
!arbitrary.min.exclusive &&
arbitrary.min.value === min &&
!arbitrary.max.exclusive &&
arbitrary.max.value === max
if (!hasDefaultConstraints) {
return arbitrary
}
Expand All @@ -360,8 +386,10 @@ const convertNumber = (
values(fastCheckNumerics),
any(
numeric =>
arbitrary.min === numeric.min.value &&
arbitrary.max === numeric.max.value &&
!arbitrary.min.exclusive &&
arbitrary.min.value === numeric.min.value &&
!arbitrary.max.exclusive &&
arbitrary.max.value === numeric.max.value &&
arbitrary.isInteger === numeric.isInteger,
),
)
Expand All @@ -377,9 +405,33 @@ const convertBigInt = (
constraints: Constraints,
{ min, max }: { min?: bigint; max?: bigint } = {},
): Arbitrary => {
const minBounds = [
{ value: constraints.min?.asBigInt(), exclusive: false },
{ value: constraints.minExclusive?.asBigInt(), exclusive: true },
{ value: min, exclusive: false },
].filter((bound): bound is Bound<bigint> => bound.value != null)
const minBound =
minBounds.length === 0 ? undefined : minBounds.reduce(maxOfMinBounds)
if (minBound?.exclusive) {
minBound.value++
minBound.exclusive = false
}

const maxBounds = [
{ value: constraints.max?.asBigInt(), exclusive: false },
{ value: constraints.maxExclusive?.asBigInt(), exclusive: true },
{ value: max, exclusive: false },
].filter((bound): bound is Bound<bigint> => bound.value != null)
const maxBound =
maxBounds.length === 0 ? undefined : maxBounds.reduce(minOfMaxBounds)
if (maxBound?.exclusive) {
maxBound.value--
maxBound.exclusive = false
}

const arbitrary = bigintArbitrary({
min: maxOrUndefined(constraints.min?.asBigInt() ?? undefined, min),
max: minOrUndefined(constraints.max?.asBigInt() ?? undefined, max),
min: minBound?.value,
max: maxBound?.value,
})

const hasDefaultConstraints =
Expand Down
73 changes: 31 additions & 42 deletions src/numerics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,50 +93,39 @@ export type FastCheckNumeric = {
isInteger: boolean
}

export const minOrUndefined = <
const A extends bigint | number | undefined,
const B extends bigint | number | undefined,
export const maxOfMinBounds = <
A extends bigint | number,
B extends bigint | number,
>(
a: A,
b: B,
): A extends undefined
? B extends undefined
? undefined
: Exclude<A | B, undefined>
: Exclude<A | B, undefined> => {
const min = (() => {
if (a === undefined) {
return b
} else if (b === undefined) {
return a
} else {
return a < b ? a : b
}
})()
// eslint-disable-next-line typescript/no-unsafe-return
return min as any
min1: Bound<A>,
min2: Bound<B>,
): Bound<A | B> => {
if (min1.value > min2.value) {
return min1
} else if (min2.value > min1.value) {
return min2
} else {
return { value: min1.value, exclusive: min1.exclusive || min2.exclusive }
}
}

export const maxOrUndefined = <
const A extends bigint | number | undefined,
const B extends bigint | number | undefined,
export const minOfMaxBounds = <
A extends bigint | number,
B extends bigint | number,
>(
a: A,
b: B,
): A extends undefined
? B extends undefined
? undefined
: Exclude<A | B, undefined>
: Exclude<A | B, undefined> => {
const max = (() => {
if (a === undefined) {
return b
} else if (b === undefined) {
return a
} else {
return a > b ? a : b
}
})()
// eslint-disable-next-line typescript/no-unsafe-return
return max as any
min1: Bound<A>,
min2: Bound<B>,
): Bound<A | B> => {
if (min1.value < min2.value) {
return min1
} else if (min2.value < min1.value) {
return min2
} else {
return { value: min1.value, exclusive: min1.exclusive || min2.exclusive }
}
}

export type Bound<N extends bigint | number> = {
value: N
exclusive: boolean
}
Loading

0 comments on commit fcc5d50

Please sign in to comment.