Skip to content

Commit 4fac4cb

Browse files
authored
✨ Support excluded min/max in float (#4105)
* ✨ Support excluded min/max in `float` Up-to-now, it was not possible to ask for float between min and max with either min or max or both not in the range. Following the request #4046, we thought about it and started to offer ways to do so. * versions * restrictsome tests
1 parent 19d9cbc commit 4fac4cb

File tree

4 files changed

+73
-6
lines changed

4 files changed

+73
-6
lines changed

.yarn/versions/9cf55ded.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
releases:
2+
fast-check: minor
3+
4+
declined:
5+
- "@fast-check/ava"
6+
- "@fast-check/jest"
7+
- "@fast-check/vitest"
8+
- "@fast-check/worker"

packages/fast-check/src/arbitrary/float.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,26 @@ export interface FloatConstraints {
2121
* @remarks Since 2.8.0
2222
*/
2323
min?: number;
24+
/**
25+
* Should the lower bound (aka min) be excluded?
26+
* Note: Excluding min=Number.NEGATIVE_INFINITY would result into having min set to -3.4028234663852886e+38.
27+
* @defaultValue false
28+
* @remarks Since 3.12.0
29+
*/
30+
minExcluded?: boolean;
2431
/**
2532
* Upper bound for the generated 32-bit floats (included)
2633
* @defaultValue Number.POSITIVE_INFINITY, 3.4028234663852886e+38 when noDefaultInfinity is true
2734
* @remarks Since 2.8.0
2835
*/
2936
max?: number;
37+
/**
38+
* Should the upper bound (aka max) be excluded?
39+
* Note: Excluding max=Number.POSITIVE_INFINITY would result into having max set to 3.4028234663852886e+38.
40+
* @defaultValue false
41+
* @remarks Since 3.12.0
42+
*/
43+
maxExcluded?: boolean;
3044
/**
3145
* By default, lower and upper bounds are -infinity and +infinity.
3246
* By setting noDefaultInfinity to true, you move those defaults to minimal and maximal finite values.
@@ -82,11 +96,15 @@ export function float(constraints: FloatConstraints = {}): Arbitrary<number> {
8296
const {
8397
noDefaultInfinity = false,
8498
noNaN = false,
99+
minExcluded = false,
100+
maxExcluded = false,
85101
min = noDefaultInfinity ? -MAX_VALUE_32 : safeNegativeInfinity,
86102
max = noDefaultInfinity ? MAX_VALUE_32 : safePositiveInfinity,
87103
} = constraints;
88-
const minIndex = safeFloatToIndex(min, 'min');
89-
const maxIndex = safeFloatToIndex(max, 'max');
104+
const minIndexRaw = safeFloatToIndex(min, 'min');
105+
const minIndex = minExcluded ? minIndexRaw + 1 : minIndexRaw;
106+
const maxIndexRaw = safeFloatToIndex(max, 'max');
107+
const maxIndex = maxExcluded ? maxIndexRaw - 1 : maxIndexRaw;
90108
if (minIndex > maxIndex) {
91109
// Comparing min and max might be problematic in case min=+0 and max=-0
92110
// For that reason, we prefer to compare computed index to be safer

packages/fast-check/test/unit/arbitrary/float.spec.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,32 @@ describe('float', () => {
8080
);
8181
});
8282

83+
it('should reject any constraints defining min (not-NaN) equal to max if one is exclusive', () => {
84+
fc.assert(
85+
fc.property(
86+
float32raw(),
87+
fc.record({ noDefaultInfinity: fc.boolean(), noNaN: fc.boolean() }, { withDeletedKeys: true }),
88+
fc.constantFrom('min', 'max', 'both'),
89+
(f, otherCt, exclusiveMode) => {
90+
// Arrange
91+
fc.pre(isNotNaN32bits(f));
92+
spyInteger();
93+
94+
// Act / Assert
95+
expect(() =>
96+
float({
97+
...otherCt,
98+
min: f,
99+
max: f,
100+
minExcluded: exclusiveMode === 'min' || exclusiveMode === 'both',
101+
maxExcluded: exclusiveMode === 'max' || exclusiveMode === 'both',
102+
})
103+
).toThrowError();
104+
}
105+
)
106+
);
107+
});
108+
83109
it('should reject non-32-bit or NaN floating point numbers if specified for min', () => {
84110
fc.assert(
85111
fc.property(float64raw(), (f64) => {
@@ -136,10 +162,16 @@ describe('float', () => {
136162
expect(integer).not.toHaveBeenCalled();
137163
});
138164

139-
it('should properly convert integer value for index between min and max into its associated float value', () =>
165+
it('should properly convert integer value for index between min and max into its associated float value', () => {
166+
const withoutExcludedConstraints = {
167+
...defaultFloatRecordConstraints,
168+
minExcluded: fc.constant(false),
169+
maxExcluded: fc.constant(false),
170+
};
171+
140172
fc.assert(
141173
fc.property(
142-
fc.option(floatConstraints(), { nil: undefined }),
174+
fc.option(floatConstraints(withoutExcludedConstraints), { nil: undefined }),
143175
fc.maxSafeNat(),
144176
fc.option(fc.integer({ min: 2 }), { nil: undefined }),
145177
(ct, mod, biasFactor) => {
@@ -159,7 +191,8 @@ describe('float', () => {
159191
expect(f).toBe(indexToFloat(arbitraryGeneratedIndex));
160192
}
161193
)
162-
));
194+
);
195+
});
163196

164197
describe('with NaN', () => {
165198
const withNaNRecordConstraints = { ...defaultFloatRecordConstraints, noNaN: fc.constant(false) };
@@ -236,13 +269,15 @@ describe('float', () => {
236269
const { min, max } = minMaxForConstraints(ct);
237270
const minIndex = floatToIndex(min);
238271
const maxIndex = floatToIndex(max);
272+
const expectedMinIndex = ct.minExcluded ? minIndex + 1 : minIndex;
273+
const expectedMaxIndex = ct.maxExcluded ? maxIndex - 1 : maxIndex;
239274

240275
// Act
241276
float(ct);
242277

243278
// Assert
244279
expect(integer).toHaveBeenCalledTimes(1);
245-
expect(integer).toHaveBeenCalledWith({ min: minIndex, max: maxIndex });
280+
expect(integer).toHaveBeenCalledWith({ min: expectedMinIndex, max: expectedMaxIndex });
246281
})
247282
);
248283
});

website/docs/core-blocks/arbitraries/primitives/number.md

+6
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ It always generates valid 32-bit floating point values.
136136

137137
- `min?` — default: `-∞` and `-3.4028234663852886e+38` when `noDefaultInfinity:true`_lower bound for the generated 32-bit floats (included)_
138138
- `max?` — default: `+∞` and `+3.4028234663852886e+38` when `noDefaultInfinity:true`_upper bound for the generated 32-bit floats (included)_
139+
- `minExcluded?` — default: `false`_do not include `min` in the set of possible values_
140+
- `maxExcluded?` — default: `false`_do not include `max` in the set of possible values_
139141
- `noDefaultInfinity?` — default: `false`_use finite values for `min` and `max` by default_
140142
- `noNaN?` — default: `false`_do not generate `Number.NaN`_
141143

@@ -159,6 +161,10 @@ fc.float({ noDefaultInfinity: true, min: Number.NEGATIVE_INTEGER, max: Number.PO
159161
// should not be set to -∞ and +∞. It does not forbid the user to explicitely set them to -∞ and +∞.
160162
// Examples of generated values: -5.435122013092041, 1981086548623360, -2.2481372319305137e-9, -2.5223372357846707e-44, 5.606418179297701e-30…
161163

164+
fc.float({ min: 0, max: 1, maxExcluded: true });
165+
// Note: All possible 32-bit floating point values between 0 (included) and 1 (excluded)
166+
// Examples of generated values: 3.2229864679470793e-44, 2.4012229232976108e-20, 1.1826533935374394e-27, 0.9999997615814209, 3.783505853677006e-44…
167+
162168
fc.integer({ min: 0, max: (1 << 24) - 1 })
163169
.map((v) => v / (1 << 24))
164170
.noBias();

0 commit comments

Comments
 (0)