Skip to content

Commit

Permalink
fix: number
Browse files Browse the repository at this point in the history
  • Loading branch information
soc221b committed Jan 3, 2025
1 parent c5f1df7 commit b94e223
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 137 deletions.
146 changes: 25 additions & 121 deletions src/zod-number-faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,131 +2,52 @@ import * as z from 'zod'
import { runFake } from './faker'
import { ZodTypeFaker } from './zod-type-faker'

const exponents = Array(54)
.fill(null)
.map((_, i) => i)

const precisions = Array(16)
.fill(null)
.map((_, i) => i + 1)

export class ZodNumberFaker extends ZodTypeFaker<z.ZodNumber> {
fake(): z.infer<z.ZodNumber> {
const { min, max, precision } = this.resolveCheck()
const result = runFake(faker =>
precision === 1
? faker.number.int({
min: Math.ceil(min),
max: Math.floor(max),
})
: faker.number.float({
min,
max,
multipleOf: precision,
}),
)
return this.schema.parse(result)
}

private resolveCheck() {
if (
this.schema._def.checks.some(check => check.kind === 'finite') === false &&
this.schema._def.checks.some(check => check.kind === 'int') === false &&
this.schema._def.checks.some(check => check.kind === 'max') === false &&
this.schema._def.checks.some(check => check.kind === 'multipleOf') === false &&
runFake(faker => faker.datatype.boolean())
) {
return { min: Infinity, max: Infinity, precision: 1 }
}
if (
this.schema._def.checks.some(check => check.kind === 'finite') === false &&
this.schema._def.checks.some(check => check.kind === 'int') === false &&
this.schema._def.checks.some(check => check.kind === 'min') === false &&
this.schema._def.checks.some(check => check.kind === 'multipleOf') === false &&
runFake(faker => faker.datatype.boolean())
) {
return { min: -Infinity, max: -Infinity, precision: 1 }
}

let min =
-1 *
(Math.pow(
2,
runFake(faker => faker.helpers.arrayElement(exponents)),
) -
1)
let max =
Math.pow(
2,
runFake(faker => faker.helpers.arrayElement(exponents)),
) - 1
let precision =
1 /
Math.pow(
10,
runFake(faker => faker.helpers.arrayElement(precisions)),
)

let min: undefined | number = undefined
let max: undefined | number = undefined
let multipleOf: undefined | number = undefined
let int: boolean = false
let finite: boolean = false
for (const check of this.schema._def.checks) {
switch (check.kind) {
case 'min':
min = Math.max(min, check.value)
min = check.value + (check.inclusive ? 0 : 0.000000000000001)
break
case 'max':
max = Math.min(max, check.value)
break
case 'int':
precision = 1
max = check.value - (check.inclusive ? 0 : 0.000000000000001)
break
case 'multipleOf':
return { min: check.value, max: check.value, precision: 0.1 }
case 'finite':
break
/* istanbul ignore next */
default:
const _: never = check
throw Error('unimplemented')
}
}

for (const check of this.schema._def.checks) {
switch (check.kind) {
case 'min':
if (check.inclusive) {
max = Math.max(min, max)
} else {
min = min + findMinimumOffsetPrecision(min)
max = Math.max(min, max)
}
multipleOf = check.value
break
case 'max':
if (check.inclusive) {
min = Math.min(min, max)
} else {
max = max - findMinimumOffsetPrecision(max)
min = Math.min(min, max)
}
case 'int':
int = true
break
case 'finite':
case 'int':
case 'multipleOf':
finite = true
break
/* istanbul ignore next */
default:
const _: never = check
throw Error('unimplemented')
}
}

if (max - min < 1) {
precision = 1 / 1e16
if (multipleOf !== undefined) {
return multipleOf
}

return {
min,
max,
precision,
if (finite === false && int === false && multipleOf === undefined) {
if (min === undefined && runFake(faker => faker.datatype.boolean({ probability: 0.2 }))) {
return -Infinity
}
if (max === undefined && runFake(faker => faker.datatype.boolean({ probability: 0.2 }))) {
return Infinity
}
}

min ??= Number.MIN_SAFE_INTEGER
max ??= Number.MAX_SAFE_INTEGER
const method = int ? 'int' : 'float'
return runFake(faker => faker.number[method]({ min, max, multipleOf }))
}

static create(schema: z.ZodNumber): ZodNumberFaker {
Expand All @@ -135,20 +56,3 @@ export class ZodNumberFaker extends ZodTypeFaker<z.ZodNumber> {
}

export const zodNumberFaker: typeof ZodNumberFaker.create = ZodNumberFaker.create

function findMinimumOffsetPrecision(number: number) {
number = Math.abs(number)
let max = 1
let min = 1 / 1e16
let prevMid = max
let mid = min + (max - min) / 2
let count = 0
while (++count < 99) {
if (prevMid <= Number.EPSILON) break
if (number + mid === number) break
prevMid = mid
max = mid
mid = min + (max - min) / 2
}
return prevMid
}
96 changes: 80 additions & 16 deletions tests/zod-number-faker.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as z from 'zod'
import { zodNumberFaker, ZodNumberFaker } from '../src/zod-number-faker'
import { expectType, TypeEqual } from 'ts-expect'
import { testMultipleTimes } from './util'

test('ZodNumberFaker should assert parameters', () => {
const invalidSchema = void 0 as any
Expand Down Expand Up @@ -40,100 +39,165 @@ test('ZodNumberFaker.fake should return a valid data', () => {
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('gt', () => {
test('gt', () => {
const schema = z.number().gt(1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('gte', () => {
test('gte', () => {
const schema = z.number().gte(1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('min', () => {
test('min', () => {
const schema = z.number().min(1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('lt', () => {
test('lt', () => {
const schema = z.number().gt(-1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('lte', () => {
test('lte', () => {
const schema = z.number().gte(-1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('max', () => {
test('max', () => {
const schema = z.number().max(-1e9)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('int', () => {
test('int', () => {
const schema = z.number().int()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('positive', () => {
test('positive', () => {
const schema = z.number().positive()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('non-positive', () => {
test('non-positive', () => {
const schema = z.number().nonpositive()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('negative', () => {
test('negative', () => {
const schema = z.number().negative()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('non-negative', () => {
test('non-negative', () => {
const schema = z.number().nonnegative()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('multipleOf', () => {
const schema = z.number().multipleOf(0.1 + 0.2)
test('multipleOf', () => {
const schema = z.number().multipleOf(37)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('finite', () => {
test('finite', () => {
const schema = z.number().finite()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

testMultipleTimes('safe', () => {
test('safe', () => {
const schema = z.number().safe()
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

describe('edge case', () => {
test('positive + int', () => {
const schema = z.number().positive().int().lte(1)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(1)
expect(schema.safeParse(data).success).toBe(true)
})

test('nonpositive + int', () => {
const schema = z.number().nonpositive().int().gte(0)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(0)
expect(schema.safeParse(data).success).toBe(true)
})

test('negative + int', () => {
const schema = z.number().negative().int().gte(-1)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(-1)
expect(schema.safeParse(data).success).toBe(true)
})

test('nonnegative + int', () => {
const schema = z.number().nonnegative().int().lte(0)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(0)
expect(schema.safeParse(data).success).toBe(true)
})

test('positive + float', () => {
const schema = z.number().positive().lte(0.000000000000001)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(0.000000000000001)
expect(schema.safeParse(data).success).toBe(true)
})

test('negative + float', () => {
const schema = z.number().negative().gte(-0.000000000000001)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(data).toBe(-0.000000000000001)
expect(schema.safeParse(data).success).toBe(true)
})

test('multipleOf', () => {
// https://github.com/colinhacks/zod/blob/v3.24.1/src/types.ts#L1480
const schema = z.number().multipleOf(0.000001)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})

test('multipleOf', () => {
const schema = z.number().multipleOf(1_234_567_890)
const faker = zodNumberFaker(schema)
const data = faker.fake()
expect(schema.safeParse(data).success).toBe(true)
})
})

0 comments on commit b94e223

Please sign in to comment.