Repository: dubzzz/fast-check
Pull request #1068 (2020-10-18 21:34:26)

✨ Add constraints-based signature for float and double
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

Related to #992

✔️ New feature
❌ Fix an issue
❌ Documentation improvement
❌ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None.

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1068/builds/64121) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 738d4ecb12615f41f7a169ab01556fa67f331861:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-ip2cp)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-q6126)| Configuration |

coveralls: 
[![Coverage Status](https://coveralls.io/builds/34264254/badge)](https://coveralls.io/builds/34264254)

Coverage increased (+0.03%) to 96.353% when pulling **738d4ecb12615f41f7a169ab01556fa67f331861 on uniform-float** into **5a565ec7d433b06b6b67ad11861c1a9328047829 on master**.


Patch:
diff --git a/codemods/unify-signatures/snippet-5.js b/codemods/unify-signatures/snippet-5.js
index 3dc8b4a6..691cb12f 100644
--- a/codemods/unify-signatures/snippet-5.js
+++ b/codemods/unify-signatures/snippet-5.js
@@ -84,3 +84,14 @@ fc.bigUint();
 fc.bigUint(3n);
 fc.bigUint(BigInt(3));
 fc.bigUint({ max: 3n });
+
+// float
+
+fc.float();
+fc.float(1.0);
+fc.float(2.0);
+fc.float(0.0, 1.0);
+fc.float(0.0, 2.0);
+fc.float(-1.0, 1.0);
+fc.float(-1.0, 2.0);
+fc.float({ min: -1.0, max: 2.0 });
diff --git a/codemods/unify-signatures/transform.cjs b/codemods/unify-signatures/transform.cjs
index 4d690b1e..def915ae 100644
--- a/codemods/unify-signatures/transform.cjs
+++ b/codemods/unify-signatures/transform.cjs
@@ -372,6 +372,29 @@ module.exports = function (file, api, options) {
           }
           break;
         }
+        case 'double':
+        case 'float': {
+          if (p.value.arguments.length === 1 && p.value.arguments[0].type !== 'ObjectExpression') {
+            // fc.float(max) -> fc.float({max})
+            const simplifyMax = options.simplifyMax && isNumericValue(p.value.arguments[0], 1.0);
+            p.value.arguments = computeNewArguments(
+              [],
+              [!simplifyMax && j.property('init', j.identifier('max'), p.value.arguments[0])]
+            );
+          } else if (p.value.arguments.length === 2) {
+            // fc.float(min, max) -> fc.float({min, max})
+            const simplifyMin = options.simplifyMin && isNumericValue(p.value.arguments[0], 0.0);
+            const simplifyMax = options.simplifyMax && isNumericValue(p.value.arguments[1], 1.0);
+            p.value.arguments = computeNewArguments(
+              [],
+              [
+                !simplifyMin && j.property('init', j.identifier('min'), p.value.arguments[0]),
+                !simplifyMax && j.property('init', j.identifier('max'), p.value.arguments[1]),
+              ]
+            );
+          }
+          break;
+        }
       }
       return p;
     })
diff --git a/documentation/Arbitraries.md b/documentation/Arbitraries.md
index 8531cf17..155e622e 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -202,13 +202,14 @@ fc.maxSafeNat()
 *&#8195;Signatures*
 
 - `fc.float()`
-- `fc.float(maxValue)`
-- `fc.float(minValue, maxValue)`
+- `fc.float({min?, max?})`
+- `fc.float(min, max)`
+- _`fc.float(max)`_ — _not recommended ([#992](https://github.com/dubzzz/fast-check/issues/992))_
 
 *&#8195;with:*
 
-- `minValue?` — default: `0.0` — _lower bound of the range (included)_
-- `maxValue?` — default: `1.0` — _upper bound of the range (excluded)_
+- `min?` — default: `0.0` — _lower bound of the range (included)_
+- `max?` — default: `1.0` — _upper bound of the range (excluded)_
 
 *&#8195;Usages*
 
@@ -217,13 +218,13 @@ fc.float()
 // Note: All possible 32-bit floating point values between `0.0` (included) and `1.0` (excluded)
 // Examples of generated values: 0.731347382068634, 1.1920928955078125e-7, 0.6597227454185486, 0.5946863293647766, 0.6302104592323303…
 
-fc.float(100)
+fc.float({max: 100})
 // Note: All possible 32-bit floating point values between `0.0` (included) and `100.0` (excluded)
-// Examples of generated values: 0.00007748603820800781, 0.00007152557373046875, 0.00013113021850585938, 52.37404108047485, 0.000035762786865234375…
+// Examples of generated values: 0.00004172325134277344, 0.000011920928955078125, 0.0001251697540283203, 60.498785972595215, 0.000029802322387695312…
 
-fc.float(-100, 100)
+fc.float({min: -100, max: 100})
 // Note: All possible 32-bit floating point values between `-100.0` (included) and `100.0` (excluded)
-// Examples of generated values: -99.99992847442627, 55.83081245422363, -99.99979734420776, -20.923829078674316, -99.99991655349731…
+// Examples of generated values: -99.9997615814209, -99.99988079071045, 81.314218044281, -99.99974966049194, -99.99978542327881…
 ```
 </details>
 
@@ -240,13 +241,14 @@ fc.float(-100, 100)
 *&#8195;Signatures*
 
 - `fc.double()`
-- `fc.double(maxValue)`
-- `fc.double(minValue, maxValue)`
+- `fc.double({min?, max?})`
+- `fc.double(min, max)`
+- _`fc.double(max)`_ — _not recommended ([#992](https://github.com/dubzzz/fast-check/issues/992))_
 
 *&#8195;with:*
 
-- `minValue?` — default: `0.0` — _lower bound of the range (included)_
-- `maxValue?` — default: `1.0` — _upper bound of the range (excluded)_
+- `min?` — default: `0.0` — _lower bound of the range (included)_
+- `max?` — default: `1.0` — _upper bound of the range (excluded)_
 
 *&#8195;Usages*
 
@@ -255,13 +257,13 @@ fc.double()
 // Note: All possible floating point values between `0.0` (included) and `1.0` (excluded)
 // Examples of generated values: 0.4530413804731288, 0.8226463198661805, 0.3829372459587349, 0.7186836451292051, 0.8065718412399292…
 
-fc.double(100)
+fc.double({max: 100})
 // Note: All possible floating point values between `0.0` (included) and `100.0` (excluded)
-// Examples of generated values: 0.000019014520535876045, 98.91013210040657, 0.00003648309874204614, 20.497548580169944, 0.00001937150981845548…
+// Examples of generated values: 78.93341034650808, 0.000034884977251348204, 98.5225079632713, 10.198144676861675, 0.000008702743647948097…
 
-fc.double(-100, 100)
+fc.double({min: -100, max: 100})
 // Note: All possible floating point values between `-100.0` (included) and `100.0` (excluded)
-// Examples of generated values: -99.999970715887, -99.99996384938794, -99.99996463982544, -69.75060565839972, -99.99994324436676…
+// Examples of generated values: -99.99994117592425, 37.9800438880923, 1.4435261487962805, -99.9999970197672, -99.99993741512299…
 ```
 </details>
 
diff --git a/src/check/arbitrary/FloatingPointArbitrary.ts b/src/check/arbitrary/FloatingPointArbitrary.ts
index f5830a53..f203ddd5 100644
--- a/src/check/arbitrary/FloatingPointArbitrary.ts
+++ b/src/check/arbitrary/FloatingPointArbitrary.ts
@@ -13,6 +13,17 @@ const floatInternal = (): Arbitrary<number> => {
   return next(24).map((v) => v / (1 << 24));
 };
 
+/**
+ * Constraints to be applied on {@link float}
+ * @public
+ */
+export interface FloatConstraints {
+  /** Lower bound for the generated floats (included) */
+  min?: number;
+  /** Upper bound for the generated floats (excluded) */
+  max?: number;
+}
+
 /**
  * For floating point numbers between 0.0 (included) and 1.0 (excluded) - accuracy of `1 / 2**24`
  * @public
@@ -23,6 +34,10 @@ function float(): Arbitrary<number>;
  *
  * @param max - Upper bound of the generated floating point
  *
+ * @remarks
+ * Superceded by `fc.float({max})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
+ * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/master/codemods/unify-signatures | our codemod script}.
+ *
  * @public
  */
 function float(max: number): Arbitrary<number>;
@@ -32,13 +47,32 @@ function float(max: number): Arbitrary<number>;
  * @param min - Lower bound of the generated floating point
  * @param max - Upper bound of the generated floating point
  *
+ * @remarks
+ * You may prefer to use `fc.float({min, max})` instead.
+ *
  * @public
  */
 function float(min: number, max: number): Arbitrary<number>;
-function float(a?: number, b?: number): Arbitrary<number> {
-  if (a === undefined) return floatInternal();
-  if (b === undefined) return floatInternal().map((v) => v * a);
-  return floatInternal().map((v) => a + v * (b - a));
+/**
+ * For floating point numbers in range defined by constraints - accuracy of `(max - min) / 2**24`
+ *
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @public
+ */
+function float(constraints: FloatConstraints): Arbitrary<number>;
+function float(...args: [] | [number] | [number, number] | [FloatConstraints]): Arbitrary<number> {
+  if (typeof args[0] === 'object') {
+    const min = args[0].min !== undefined ? args[0].min : 0;
+    const max = args[0].max !== undefined ? args[0].max : 1;
+    return floatInternal().map((v) => min + v * (max - min));
+  } else {
+    const a = args[0];
+    const b = args[1];
+    if (a === undefined) return floatInternal();
+    if (b === undefined) return floatInternal().map((v) => v * a);
+    return floatInternal().map((v) => a + v * (b - a));
+  }
 }
 
 /** @internal */ const doubleFactor = Math.pow(2, 27);
@@ -50,6 +84,17 @@ const doubleInternal = (): Arbitrary<number> => {
   return tuple(next(26), next(27)).map((v) => (v[0] * doubleFactor + v[1]) * doubleDivisor);
 };
 
+/**
+ * Constraints to be applied on {@link double}
+ * @public
+ */
+export interface DoubleConstraints {
+  /** Lower bound for the generated doubles (included) */
+  min?: number;
+  /** Upper bound for the generated doubles (excluded) */
+  max?: number;
+}
+
 /**
  * For floating point numbers between 0.0 (included) and 1.0 (excluded) - accuracy of `1 / 2**53`
  * @public
@@ -60,6 +105,10 @@ function double(): Arbitrary<number>;
  *
  * @param max - Upper bound of the generated floating point
  *
+ * @remarks
+ * Superceded by `fc.double({max})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
+ * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/master/codemods/unify-signatures | our codemod script}.
+ *
  * @public
  */
 function double(max: number): Arbitrary<number>;
@@ -69,13 +118,32 @@ function double(max: number): Arbitrary<number>;
  * @param min - Lower bound of the generated floating point
  * @param max - Upper bound of the generated floating point
  *
+ * @remarks
+ * You may prefer to use `fc.double({min, max})` instead.
+ *
  * @public
  */
 function double(min: number, max: number): Arbitrary<number>;
-function double(a?: number, b?: number): Arbitrary<number> {
-  if (a === undefined) return doubleInternal();
-  if (b === undefined) return doubleInternal().map((v) => v * a);
-  return doubleInternal().map((v) => a + v * (b - a));
+/**
+ * For floating point numbers in range defined by constraints - accuracy of `(max - min) / 2**53`
+ *
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @public
+ */
+function double(constraints: DoubleConstraints): Arbitrary<number>;
+function double(...args: [] | [number] | [number, number] | [DoubleConstraints]): Arbitrary<number> {
+  if (typeof args[0] === 'object') {
+    const min = args[0].min !== undefined ? args[0].min : 0;
+    const max = args[0].max !== undefined ? args[0].max : 1;
+    return doubleInternal().map((v) => min + v * (max - min));
+  } else {
+    const a = args[0];
+    const b = args[1];
+    if (a === undefined) return doubleInternal();
+    if (b === undefined) return doubleInternal().map((v) => v * a);
+    return doubleInternal().map((v) => a + v * (b - a));
+  }
 }
 
 export { float, double };
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index 4a310749..49450ad7 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -40,7 +40,7 @@ import { Arbitrary } from './check/arbitrary/definition/Arbitrary';
 import { Shrinkable } from './check/arbitrary/definition/Shrinkable';
 import { dictionary } from './check/arbitrary/DictionaryArbitrary';
 import { emailAddress } from './check/arbitrary/EmailArbitrary';
-import { double, float } from './check/arbitrary/FloatingPointArbitrary';
+import { double, float, DoubleConstraints, FloatConstraints } from './check/arbitrary/FloatingPointArbitrary';
 import { frequency, WeightedArbitrary, FrequencyValue } from './check/arbitrary/FrequencyArbitrary';
 import { compareBooleanFunc, compareFunc, func } from './check/arbitrary/FunctionArbitrary';
 import { domain } from './check/arbitrary/HostArbitrary';
@@ -291,7 +291,9 @@ export {
   BigIntConstraints,
   BigUintConstraints,
   CommandsContraints,
+  DoubleConstraints,
   FalsyContraints,
+  FloatConstraints,
   JsonSharedConstraints,
   LoremConstraints,
   MixedCaseConstraints,


Test patch:
diff --git a/test/unit/check/arbitrary/FloatingPointArbitrary.spec.ts b/test/unit/check/arbitrary/FloatingPointArbitrary.spec.ts
index b6a7953b..75237908 100644
--- a/test/unit/check/arbitrary/FloatingPointArbitrary.spec.ts
+++ b/test/unit/check/arbitrary/FloatingPointArbitrary.spec.ts
@@ -7,6 +7,7 @@ import { float, double } from '../../../../src/check/arbitrary/FloatingPointArbi
 import * as genericHelper from './generic/GenericArbitraryHelper';
 
 import * as stubRng from '../../stubs/generators';
+import { generateOneValue } from './generic/GenerateOneValue';
 
 const MAX_TRIES = 100;
 describe('FloatingPointArbitrary', () => {
@@ -34,25 +35,59 @@ describe('FloatingPointArbitrary', () => {
         isValidValue: (g: number) => typeof g === 'number' && 0.0 <= g && g < 1.0,
       });
     });
+    describe('Given minimal value only [between min (included) and 1 (excluded)]', () => {
+      genericHelper.isValidArbitrary((min: number) => float({ min }), {
+        seedGenerator: fc.integer(-0x7fffffff, 99).map((v) => v / 100.0), // must be <1
+        isValidValue: (g: number, min: number) => typeof g === 'number' && min <= g && g < 1,
+      });
+    });
     describe('Given maximal value only [between 0 (included) and max (excluded)]', () => {
-      genericHelper.isValidArbitrary((maxValue: number) => float(maxValue), {
-        seedGenerator: fc.nat().map((v) => (v + 1) / 100.0), // must be != 0
+      genericHelper.isValidArbitrary((max: number) => float({ max }), {
+        seedGenerator: fc.nat().map((v) => (v + 1) / 100.0), // must be >0
         isStrictlySmallerValue: (a, b) => a < b,
-        isValidValue: (g: number, maxValue: number) => typeof g === 'number' && 0.0 <= g && g < maxValue,
+        isValidValue: (g: number, max: number) => typeof g === 'number' && 0.0 <= g && g < max,
       });
     });
     describe('Given minimal and maximal values [between min (included) and max (excluded)]', () => {
-      genericHelper.isValidArbitrary(
-        (constraints: { min: number; max: number }) => float(constraints.min, constraints.max),
-        {
-          seedGenerator: genericHelper
-            .minMax(fc.integer())
-            .filter(({ min, max }: { min: number; max: number }) => min !== max)
-            .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
-          isValidValue: (g: number, constraints: { min: number; max: number }) =>
-            typeof g === 'number' && constraints.min <= g && g < constraints.max,
-        }
-      );
+      genericHelper.isValidArbitrary((constraints: { min: number; max: number }) => float(constraints), {
+        seedGenerator: genericHelper
+          .minMax(fc.integer())
+          .filter(({ min, max }: { min: number; max: number }) => min !== max)
+          .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
+        isValidValue: (g: number, constraints: { min: number; max: number }) =>
+          typeof g === 'number' && constraints.min <= g && g < constraints.max,
+      });
+    });
+    describe('Still support older signatures', () => {
+      it('Should support fc.float(max)', () => {
+        fc.assert(
+          fc.property(
+            fc.integer(),
+            fc.nat().map((v) => (v + 1) / 100.0),
+            (seed, max) => {
+              const refArbitrary = float({ max });
+              const otherArbitrary = float(max);
+              expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+            }
+          )
+        );
+      });
+      it('Should support fc.float(min, max)', () => {
+        fc.assert(
+          fc.property(
+            fc.integer(),
+            genericHelper
+              .minMax(fc.integer())
+              .filter(({ min, max }: { min: number; max: number }) => min !== max)
+              .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
+            (seed, constraints) => {
+              const refArbitrary = float(constraints);
+              const otherArbitrary = float(constraints.min, constraints.max);
+              expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+            }
+          )
+        );
+      });
     });
   });
   describe('double', () => {
@@ -69,25 +104,59 @@ describe('FloatingPointArbitrary', () => {
         isValidValue: (g: number) => typeof g === 'number' && 0.0 <= g && g < 1.0,
       });
     });
+    describe('Given minimal value only [between min (included) and 1 (excluded)]', () => {
+      genericHelper.isValidArbitrary((min: number) => double({ min }), {
+        seedGenerator: fc.integer(-0x7fffffff, 99).map((v) => v / 100.0), // must be <1
+        isValidValue: (g: number, min: number) => typeof g === 'number' && min <= g && g < 1,
+      });
+    });
     describe('Given maximal value only [between 0 (included) and max (excluded)]', () => {
-      genericHelper.isValidArbitrary((maxValue: number) => double(maxValue), {
-        seedGenerator: fc.nat().map((v) => (v + 1) / 100.0), // must be != 0
+      genericHelper.isValidArbitrary((max: number) => double({ max }), {
+        seedGenerator: fc.nat().map((v) => (v + 1) / 100.0), // must be >0
         isStrictlySmallerValue: (a, b) => a < b,
-        isValidValue: (g: number, maxValue: number) => typeof g === 'number' && 0.0 <= g && g < maxValue,
+        isValidValue: (g: number, max: number) => typeof g === 'number' && 0.0 <= g && g < max,
       });
     });
     describe('Given minimal and maximal values [between min (included) and max (excluded)]', () => {
-      genericHelper.isValidArbitrary(
-        (constraints: { min: number; max: number }) => double(constraints.min, constraints.max),
-        {
-          seedGenerator: genericHelper
-            .minMax(fc.integer())
-            .filter(({ min, max }: { min: number; max: number }) => min !== max)
-            .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
-          isValidValue: (g: number, constraints: { min: number; max: number }) =>
-            typeof g === 'number' && constraints.min <= g && g < constraints.max,
-        }
-      );
+      genericHelper.isValidArbitrary((constraints: { min: number; max: number }) => double(constraints), {
+        seedGenerator: genericHelper
+          .minMax(fc.integer())
+          .filter(({ min, max }: { min: number; max: number }) => min !== max)
+          .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
+        isValidValue: (g: number, constraints: { min: number; max: number }) =>
+          typeof g === 'number' && constraints.min <= g && g < constraints.max,
+      });
+    });
+    describe('Still support older signatures', () => {
+      it('Should support fc.double(max)', () => {
+        fc.assert(
+          fc.property(
+            fc.integer(),
+            fc.nat().map((v) => (v + 1) / 100.0),
+            (seed, max) => {
+              const refArbitrary = double({ max });
+              const otherArbitrary = double(max);
+              expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+            }
+          )
+        );
+      });
+      it('Should support fc.double(min, max)', () => {
+        fc.assert(
+          fc.property(
+            fc.integer(),
+            genericHelper
+              .minMax(fc.integer())
+              .filter(({ min, max }: { min: number; max: number }) => min !== max)
+              .map(({ min, max }: { min: number; max: number }) => ({ min: min / 100.0, max: max / 100.0 })),
+            (seed, constraints) => {
+              const refArbitrary = double(constraints);
+              const otherArbitrary = double(constraints.min, constraints.max);
+              expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+            }
+          )
+        );
+      });
     });
   });
 });
diff --git a/test/unit/check/runner/configuration/QualifiedParameters.spec.ts b/test/unit/check/runner/configuration/QualifiedParameters.spec.ts
index d4e81e0d..4bd5a43f 100644
--- a/test/unit/check/runner/configuration/QualifiedParameters.spec.ts
+++ b/test/unit/check/runner/configuration/QualifiedParameters.spec.ts
@@ -77,7 +77,7 @@ describe('QualifiedParameters', () => {
     describe('Seeds outside of 32 bits range', () => {
       const seedsOutsideRangeArb = fc.oneof(
         fc.double(),
-        fc.double(Number.MIN_VALUE, Number.MAX_VALUE),
+        fc.double({ min: Number.MIN_VALUE, max: Number.MAX_VALUE }),
         fc.integer(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
         fc.constantFrom(Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NaN)
       );