Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add option to produce non-integer on double #4917

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .yarn/versions/bb14e24d.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
releases:
fast-check: minor

declined:
- "@fast-check/ava"
- "@fast-check/jest"
- "@fast-check/vitest"
- "@fast-check/worker"
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { DoubleConstraints } from '../../double';

const safeNegativeInfinity = Number.NEGATIVE_INFINITY;
const safePositiveInfinity = Number.POSITIVE_INFINITY;
const safeMaxValue = Number.MAX_VALUE;

// The last floating point value available with 64 bits floating point numbers is: 4503599627370495.5
// The start of integers' world is: 4503599627370496 = 2**52 = 2**(significand_size_with_sign-1)
export const maxNonIntegerValue = 4503599627370495.5;
export const onlyIntegersAfterThisValue = 4503599627370496;

/**
* Refine source constraints receive by a double to focus only on non-integer values.
* @param constraints - Source constraints to be refined
*/
export function refineConstraintsForDoubleOnly(
constraints: Omit<DoubleConstraints, 'noInteger'>,
): Omit<DoubleConstraints, 'noInteger'> {
const {
noDefaultInfinity = false,
minExcluded = false,
maxExcluded = false,
min = noDefaultInfinity ? -safeMaxValue : safeNegativeInfinity,
max = noDefaultInfinity ? safeMaxValue : safePositiveInfinity,
} = constraints;

const effectiveMin = minExcluded
? min < -maxNonIntegerValue
? -onlyIntegersAfterThisValue
: Math.max(min, -maxNonIntegerValue)
: min === safeNegativeInfinity
? Math.max(min, -onlyIntegersAfterThisValue)
: Math.max(min, -maxNonIntegerValue);
const effectiveMax = maxExcluded
? max > maxNonIntegerValue
? onlyIntegersAfterThisValue
: Math.min(max, maxNonIntegerValue)
: max === safePositiveInfinity
? Math.min(max, onlyIntegersAfterThisValue)
: Math.min(max, maxNonIntegerValue);

const fullConstraints: Required<Omit<DoubleConstraints, 'noInteger'>> = {
noDefaultInfinity: false, // already handled locally
minExcluded, // exclusion still need to be applied
maxExcluded,
min: effectiveMin,
max: effectiveMax,
noNaN: constraints.noNaN || false,
};
return fullConstraints;
}

export function doubleOnlyMapper(value: number): number {
return value === onlyIntegersAfterThisValue
? safePositiveInfinity
: value === -onlyIntegersAfterThisValue
? safeNegativeInfinity
: value;
}

Check warning on line 59 in packages/fast-check/src/arbitrary/_internals/helpers/DoubleOnlyHelpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/_internals/helpers/DoubleOnlyHelpers.ts#L54-L59

Added lines #L54 - L59 were not covered by tests

export function doubleOnlyUnmapper(value: unknown): number {
if (typeof value !== 'number') throw new Error('Unsupported type');
return value === safePositiveInfinity
? onlyIntegersAfterThisValue
: value === safeNegativeInfinity
? -onlyIntegersAfterThisValue
: value;
}

Check warning on line 68 in packages/fast-check/src/arbitrary/_internals/helpers/DoubleOnlyHelpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/_internals/helpers/DoubleOnlyHelpers.ts#L62-L68

Added lines #L62 - L68 were not covered by tests
52 changes: 40 additions & 12 deletions packages/fast-check/src/arbitrary/double.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
import { arrayInt64 } from './_internals/ArrayInt64Arbitrary';
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { doubleToIndex, indexToDouble } from './_internals/helpers/DoubleHelpers';
import {
doubleOnlyMapper,
doubleOnlyUnmapper,
refineConstraintsForDoubleOnly,
} from './_internals/helpers/DoubleOnlyHelpers';

const safeNumberIsInteger = Number.isInteger;
const safeNumberIsNaN = Number.isNaN;

const safeNegativeInfinity = Number.NEGATIVE_INFINITY;
Expand Down Expand Up @@ -63,6 +69,13 @@
* @remarks Since 2.8.0
*/
noNaN?: boolean;
/**
* When set to true, Number.isInteger(value) will be false for any generated value.
* Note: -infinity and +infinity, or NaN can stil be generated except if you rejected them via another constraint.
* @defaultValue false
* @remarks Since 3.18.0
*/
noInteger?: boolean;
}

/**
Expand All @@ -84,18 +97,13 @@
return doubleToIndex(value);
}

/**
* For 64-bit floating point numbers:
* - sign: 1 bit
* - significand: 52 bits
* - exponent: 11 bits
*
* @param constraints - Constraints to apply when building instances (since 2.8.0)
*
* @remarks Since 0.0.6
* @public
*/
export function double(constraints: DoubleConstraints = {}): Arbitrary<number> {
/** @internal */
function numberIsNotInteger(value: number): boolean {
return !safeNumberIsInteger(value);
}

Check warning on line 103 in packages/fast-check/src/arbitrary/double.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/double.ts#L101-L103

Added lines #L101 - L103 were not covered by tests

/** @internal */
function anyDouble(constraints: Omit<DoubleConstraints, 'noInteger'>): Arbitrary<number> {
const {
noDefaultInfinity = false,
noNaN = false,
Expand Down Expand Up @@ -137,3 +145,23 @@
},
);
}

/**
* For 64-bit floating point numbers:
* - sign: 1 bit
* - significand: 52 bits
* - exponent: 11 bits
*
* @param constraints - Constraints to apply when building instances (since 2.8.0)
*
* @remarks Since 0.0.6
* @public
*/
export function double(constraints: DoubleConstraints = {}): Arbitrary<number> {
if (!constraints.noInteger) {
return anyDouble(constraints);
}
return anyDouble(refineConstraintsForDoubleOnly(constraints))
.map(doubleOnlyMapper, doubleOnlyUnmapper)
.filter(numberIsNotInteger);
}

Check warning on line 167 in packages/fast-check/src/arbitrary/double.ts

View check run for this annotation

Codecov / codecov/patch

packages/fast-check/src/arbitrary/double.ts#L164-L167

Added lines #L164 - L167 were not covered by tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, it, expect } from 'vitest';
import * as fc from 'fast-check';
import { doubleToIndex, indexToDouble } from '../../../../../src/arbitrary/_internals/helpers/DoubleHelpers';
import {
maxNonIntegerValue,
onlyIntegersAfterThisValue,
refineConstraintsForDoubleOnly,
} from '../../../../../src/arbitrary/_internals/helpers/DoubleOnlyHelpers';
import { add64 } from '../../../../../src/arbitrary/_internals/helpers/ArrayInt64';

describe('maxNonIntegerValue', () => {
it('should be immediately followed by an integer', () => {
// Arrange / Act
const next = nextDouble(maxNonIntegerValue);

// Assert
expect(Number.isInteger(next)).toBe(true);
});

it('should be followed by a number immediatelly followed by an integer', () => {
// Arrange / Act
const next = nextDouble(maxNonIntegerValue);
const nextNext = nextDouble(next);

// Assert
expect(Number.isInteger(nextNext)).toBe(true);
});

it('should be immediately followed by onlyIntegersAfterThisValue', () => {
// Arrange / Act / Assert
expect(nextDouble(maxNonIntegerValue)).toBe(onlyIntegersAfterThisValue);
});
});

describe('refineConstraintsForDoubleOnly', () => {
describe('no excluded', () => {
it('should properly refine default constraints', () => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({})).toEqual({
minExcluded: false,
min: -onlyIntegersAfterThisValue, // min included, but its value will be replaced by -inf in mapper
maxExcluded: false,
max: onlyIntegersAfterThisValue, // max included, but its value will be replaced by +inf in mapper
noDefaultInfinity: false,
noNaN: false,
});
});

it('should properly refine when constraints reject infinities', () => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ noDefaultInfinity: true })).toEqual({
minExcluded: false,
min: -maxNonIntegerValue,
maxExcluded: false,
max: maxNonIntegerValue,
noDefaultInfinity: false,
noNaN: false,
});
});

it('should properly refine when constraints ask for onlyIntegersAfterThisValue or above (excluding infinite)', () => {
fc.assert(
fc.property(
fc.double({ noDefaultInfinity: true, noNaN: true, min: onlyIntegersAfterThisValue }),
(boundary) => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ min: -boundary, max: boundary })).toEqual({
minExcluded: false,
min: -maxNonIntegerValue, // min has been adapted to better fit the float range
maxExcluded: false,
max: maxNonIntegerValue, // max has been adapted to better fit the float range
noDefaultInfinity: false,
noNaN: false,
});
},
),
);
});

it('should properly refine when constraints ask for maxNonIntegerValue or below', () => {
fc.assert(
fc.property(fc.double({ noNaN: true, min: 1, max: maxNonIntegerValue }), (boundary) => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ min: -boundary, max: boundary })).toEqual({
minExcluded: false,
min: -boundary, // min was already in the accepted range
maxExcluded: false,
max: boundary, // max was already in the accepted range
noDefaultInfinity: false,
noNaN: false,
});
}),
);
});
});

describe('with excluded', () => {
const excluded = { minExcluded: true, maxExcluded: true };

it('should properly refine default constraints', () => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ ...excluded })).toEqual({
minExcluded: true,
min: -onlyIntegersAfterThisValue, // min excluded so it only starts at -maxNonIntegerValue
maxExcluded: true,
max: onlyIntegersAfterThisValue, /// min excluded so it only starts at -maxNonIntegerValue
noDefaultInfinity: false,
noNaN: false,
});
});

it('should properly refine when constraints reject infinities', () => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ ...excluded, noDefaultInfinity: true })).toEqual({
minExcluded: true,
min: -onlyIntegersAfterThisValue, // min excluded so it only starts at -maxNonIntegerValue
maxExcluded: true,
max: onlyIntegersAfterThisValue, // min excluded so it only starts at -maxNonIntegerValue
noDefaultInfinity: false,
noNaN: false,
});
});

it('should properly refine when constraints ask for onlyIntegersAfterThisValue or above (excluding infinite)', () => {
fc.assert(
fc.property(
fc.double({ noDefaultInfinity: true, noNaN: true, min: onlyIntegersAfterThisValue }),
(boundary) => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ ...excluded, min: -boundary, max: boundary })).toEqual({
minExcluded: true,
min: -onlyIntegersAfterThisValue, // min has been adapted to better fit the float range, values only starts at -maxNonIntegerValue
maxExcluded: true,
max: onlyIntegersAfterThisValue, // max has been adapted to better fit the float range, values only starts at maxNonIntegerValue
noDefaultInfinity: false,
noNaN: false,
});
},
),
);
});

it('should properly refine when constraints ask for maxNonIntegerValue or below', () => {
fc.assert(
fc.property(fc.double({ noNaN: true, min: 1, max: maxNonIntegerValue }), (boundary) => {
// Arrange / Act / Assert
expect(refineConstraintsForDoubleOnly({ ...excluded, min: -boundary, max: boundary })).toEqual({
minExcluded: true,
min: -boundary, // min was already in the accepted range
maxExcluded: true,
max: boundary, // max was already in the accepted range
noDefaultInfinity: false,
noNaN: false,
});
}),
);
});
});
});

// Helpers

function nextDouble(value: number): number {
const index = doubleToIndex(value);
const nextIndex = add64(index, { sign: 1, data: [0, 1] });
return indexToDouble(nextIndex);
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ Generate _Float64Array_
- `max?` — default: `+∞` and `Number.MAX_VALUE` when `noDefaultInfinity:true` — _upper bound for the generated 32-bit floats (included)_
- `noDefaultInfinity?` — default: `false` — _use finite values for `min` and `max` by default_
- `noNaN?` — default: `false` — _do not generate `Number.NaN`_
- `noInteger?` — default: `false` — _do not generate values matching `Number.isInteger`_
- `minLength?` — default: `0` — _minimal length (included)_
- `maxLength?` — default: `0x7fffffff` [more](/docs/configuration/larger-entries-by-default/#size-explained) — _maximal length (included)_
- `size?` — default: `undefined` [more](/docs/configuration/larger-entries-by-default/#size-explained) — _how large should the generated values be?_
Expand Down
5 changes: 5 additions & 0 deletions website/docs/core-blocks/arbitraries/primitives/number.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ The lower and upper bounds are included into the range of possible values.
- `maxExcluded?` — default: `false` — _do not include `max` in the set of possible values_
- `noDefaultInfinity?` — default: `false` — _use finite values for `min` and `max` by default_
- `noNaN?` — default: `false` — _do not generate `Number.NaN`_
- `noInteger?` — default: `false` — _do not generate values matching `Number.isInteger`_

**Usages:**

Expand All @@ -222,6 +223,10 @@ fc.double({ min: 0, max: 1, maxExcluded: true });
// Note: All possible floating point values between 0 (included) and 1 (excluded)
// Examples of generated values: 4.8016271592767985e-73, 4.8825963576686075e-55, 0.9999999999999967, 0.9999999999999959, 2.5e-322…

fc.double({ noInteger: true });
// Note: All possible floating point values but no integer
// Examples of generated values: -2.3e-322, -4503599627370495.5, -1.8524776326185756e-119, -9.4e-323, 7e-323…

fc.tuple(fc.integer({ min: 0, max: (1 << 26) - 1 }), fc.integer({ min: 0, max: (1 << 27) - 1 }))
.map((v) => (v[0] * Math.pow(2, 27) + v[1]) * Math.pow(2, -53))
.noBias();
Expand Down
Loading