Repository: dubzzz/fast-check
Pull request #1076 (2020-10-19 21:11:38)

✨ Add constraints-based signature for integer and nat
<!-- 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/1076/builds/64496) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 605067878cd743a2a0d7fc23c179cb76eb76e7e9:

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

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

Coverage increased (+0.02%) to 96.375% when pulling **605067878cd743a2a0d7fc23c179cb76eb76e7e9 on uniform-int** into **09169a476af4fa7deac328bc3adf74c310c1b910 on master**.


Patch:
diff --git a/codemods/unify-signatures/snippet-5.js b/codemods/unify-signatures/snippet-5.js
index 691cb12f..41657e7e 100644
--- a/codemods/unify-signatures/snippet-5.js
+++ b/codemods/unify-signatures/snippet-5.js
@@ -95,3 +95,21 @@ 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 });
+
+// nat
+
+fc.nat();
+fc.nat(10);
+fc.nat(0x7fffffff);
+fc.nat(2147483647);
+
+// integer
+
+fc.integer();
+fc.integer(10);
+fc.integer(0x7fffffff);
+fc.integer(2147483647);
+fc.integer(-1, 10);
+fc.integer(-0x80000000, 0x7fffffff);
+fc.integer(-2147483648, 2147483647);
+fc.integer({ min: -1, max: 1 });
diff --git a/codemods/unify-signatures/transform.cjs b/codemods/unify-signatures/transform.cjs
index def915ae..fae0d4f8 100644
--- a/codemods/unify-signatures/transform.cjs
+++ b/codemods/unify-signatures/transform.cjs
@@ -154,6 +154,11 @@ module.exports = function (file, api, options) {
   }
 
   function isNumericValue(argument, value) {
+    if (value < 0) {
+      return (
+        argument.type === 'UnaryExpression' && argument.operator === '-' && isNumericValue(argument.argument, -value)
+      );
+    }
     return isNumeric(argument) && argument.value === value;
   }
   function getArrayLength(argument) {
@@ -395,6 +400,39 @@ module.exports = function (file, api, options) {
           }
           break;
         }
+        case 'nat': {
+          if (p.value.arguments.length === 1 && p.value.arguments[0].type !== 'ObjectExpression') {
+            // fc.nat(max) -> fc.nat({max})
+            const simplifyMax = options.simplifyMax && isNumericValue(p.value.arguments[0], 0x7fffffff);
+            p.value.arguments = computeNewArguments(
+              [],
+              [!simplifyMax && j.property('init', j.identifier('max'), p.value.arguments[0])]
+            );
+          }
+          break;
+        }
+        case 'integer': {
+          if (p.value.arguments.length === 1 && p.value.arguments[0].type !== 'ObjectExpression') {
+            // fc.integer(max) -> fc.integer({max})
+            const simplifyMax = options.simplifyMax && isNumericValue(p.value.arguments[0], 0x7fffffff);
+            p.value.arguments = computeNewArguments(
+              [],
+              [!simplifyMax && j.property('init', j.identifier('max'), p.value.arguments[0])]
+            );
+          } else if (p.value.arguments.length === 2) {
+            // fc.integer(min, max) -> fc.integer({min, max})
+            const simplifyMin = options.simplifyMin && isNumericValue(p.value.arguments[0], -0x80000000);
+            const simplifyMax = options.simplifyMax && isNumericValue(p.value.arguments[1], 0x7fffffff);
+            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 155e622e..7bbfa520 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -88,13 +88,14 @@ fc.boolean()
 *&#8195;Signatures*
 
 - `fc.integer()`
-- `fc.integer(maxValue)`
-- `fc.integer(minValue, maxValue)`
+- `fc.integer({min?, max?})`
+- `fc.integer(min, max)`
+- _`fc.integer(max)`_ — _not recommended ([#992](https://github.com/dubzzz/fast-check/issues/992))_
 
 *&#8195;with:*
 
-- `minValue?` — default: `-2147483648` — _lower bound of the range (included)_
-- `maxValue?` — default: `2147483647` — _upper bound of the range (included)_
+- `min?` — default: `-2147483648` — _lower bound of the range (included)_
+- `max?` — default: `2147483647` — _upper bound of the range (included)_
 
 *&#8195;Usages*
 
@@ -110,6 +111,14 @@ fc.integer(1000)
 fc.integer(-99, 99)
 // Note: All possible integers between `-99` (included) and `99` (included)
 // Examples of generated values: 2, -1, 91, -2, 3…
+
+fc.integer({min: -99, max: 99})
+// Note: All possible integers between `-99` (included) and `99` (included)
+// Examples of generated values: -4, 4, 8, 2, 1…
+
+fc.integer({min: 65536})
+// Note: All possible integers between `65536` (included) and `2147483647` (included)
+// Examples of generated values: 65557, 65547, 65562, 1836480947, 1490866554…
 ```
 </details>
 
@@ -126,11 +135,12 @@ fc.integer(-99, 99)
 *&#8195;Signatures*
 
 - `fc.nat()`
-- `fc.nat(maxValue)`
+- `fc.nat({max?})`
+- `fc.nat(max)`
 
 *&#8195;with:*
 
-- `maxValue?` — default: `2147483647` — _upper bound of the range (included)_
+- `max?` — default: `2147483647` — _upper bound of the range (included)_
 
 *&#8195;Usages*
 
@@ -142,6 +152,10 @@ fc.nat()
 fc.nat(1000)
 // Note: All possible integers between `0` (included) and `1000` (included)
 // Examples of generated values: 299, 1, 225, 750, 4…
+
+fc.nat({max: 1000})
+// Note: All possible integers between `0` (included) and `1000` (included)
+// Examples of generated values: 0, 833, 7, 496, 4…
 ```
 </details>
 
diff --git a/src/check/arbitrary/IntegerArbitrary.ts b/src/check/arbitrary/IntegerArbitrary.ts
index f9c4d010..b0c39ca5 100644
--- a/src/check/arbitrary/IntegerArbitrary.ts
+++ b/src/check/arbitrary/IntegerArbitrary.ts
@@ -13,12 +13,8 @@ class IntegerArbitrary extends ArbitraryWithShrink<number> {
   static MAX_INT: number = 0x7fffffff | 0;
 
   private biasedIntegerArbitrary: Arbitrary<number> | null = null;
-  readonly min: number;
-  readonly max: number;
-  constructor(min?: number, max?: number) {
+  constructor(readonly min: number, readonly max: number) {
     super();
-    this.min = min === undefined ? IntegerArbitrary.MIN_INT : min;
-    this.max = max === undefined ? IntegerArbitrary.MAX_INT : max;
   }
   private wrapper(value: number, shrunkOnce: boolean): Shrinkable<number> {
     return new Shrinkable(value, () => this.shrink(value, shrunkOnce).map((v) => this.wrapper(v, true)));
@@ -42,6 +38,53 @@ class IntegerArbitrary extends ArbitraryWithShrink<number> {
   }
 }
 
+/**
+ * Constraints to be applied on {@link integer}
+ * @public
+ */
+export interface IntegerConstraints {
+  /**
+   * Lower bound for the generated integers (included)
+   * @defaultValue -0x80000000
+   */
+  min?: number;
+  /**
+   * Upper bound for the generated integers (included)
+   * @defaultValue 0x7fffffff
+   */
+  max?: number;
+}
+
+/**
+ * Build fully set IntegerConstraints from a partial data
+ * @internal
+ */
+function buildCompleteIntegerConstraints(constraints: IntegerConstraints): Required<IntegerConstraints> {
+  const min = constraints.min !== undefined ? constraints.min : IntegerArbitrary.MIN_INT;
+  const max = constraints.max !== undefined ? constraints.max : IntegerArbitrary.MAX_INT;
+  return { min, max };
+}
+
+/**
+ * Extract constraints from args received by integer
+ * @internal
+ */
+function extractIntegerConstraints(args: [] | [number] | [number, number] | [IntegerConstraints]): IntegerConstraints {
+  if (args[0] === undefined) {
+    // integer()
+    return {};
+  } // args.length > 0
+
+  if (args[1] === undefined) {
+    const sargs = args as typeof args & [unknown]; // exactly 1 arg specified
+    if (typeof sargs[0] === 'number') return { max: sargs[0] }; // integer(max)
+    return sargs[0]; // integer(constraints)
+  } // args.length > 1
+
+  const sargs = args as typeof args & [unknown, unknown];
+  return { min: sargs[0], max: sargs[1] }; // integer(min, max)
+}
+
 /**
  * For integers between -2147483648 (included) and 2147483647 (included)
  * @public
@@ -52,6 +95,10 @@ function integer(): ArbitraryWithShrink<number>;
  *
  * @param max - Upper bound for the generated integers (eg.: 2147483647, Number.MAX_SAFE_INTEGER)
  *
+ * @remarks
+ * Superceded by `fc.integer({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 integer(max: number): ArbitraryWithShrink<number>;
@@ -61,13 +108,26 @@ function integer(max: number): ArbitraryWithShrink<number>;
  * @param min - Lower bound for the generated integers (eg.: 0, Number.MIN_SAFE_INTEGER)
  * @param max - Upper bound for the generated integers (eg.: 2147483647, Number.MAX_SAFE_INTEGER)
  *
+ * @remarks
+ * You may prefer to use `fc.integer({min, max})` instead.
+ *
  * @public
  */
 function integer(min: number, max: number): ArbitraryWithShrink<number>;
-function integer(a?: number, b?: number): ArbitraryWithShrink<number> {
-  if (a !== undefined && b !== undefined && a > b)
+/**
+ * For integers between min (included) and max (included)
+ *
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @public
+ */
+function integer(constraints: IntegerConstraints): ArbitraryWithShrink<number>;
+function integer(...args: [] | [number] | [number, number] | [IntegerConstraints]): ArbitraryWithShrink<number> {
+  const constraints = buildCompleteIntegerConstraints(extractIntegerConstraints(args));
+  if (constraints.min > constraints.max) {
     throw new Error('fc.integer maximum value should be equal or greater than the minimum one');
-  return b === undefined ? new IntegerArbitrary(undefined, a) : new IntegerArbitrary(a, b);
+  }
+  return new IntegerArbitrary(constraints.min, constraints.max);
 }
 
 /**
@@ -78,6 +138,18 @@ function maxSafeInteger(): ArbitraryWithShrink<number> {
   return integer(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
 }
 
+/**
+ * Constraints to be applied on {@link nat}
+ * @public
+ */
+export interface NatConstraints {
+  /**
+   * Upper bound for the generated postive integers (included)
+   * @defaultValue 0x7fffffff
+   */
+  max?: number;
+}
+
 /**
  * For positive integers between 0 (included) and 2147483647 (included)
  * @public
@@ -88,12 +160,26 @@ function nat(): ArbitraryWithShrink<number>;
  *
  * @param max - Upper bound for the generated integers
  *
+ * @remarks
+ * You may prefer to use `fc.nat({max})` instead.
+ *
  * @public
  */
 function nat(max: number): ArbitraryWithShrink<number>;
-function nat(a?: number): ArbitraryWithShrink<number> {
-  if (a !== undefined && a < 0) throw new Error('fc.nat value should be greater than or equal to 0');
-  return new IntegerArbitrary(0, a);
+/**
+ * For positive integers between 0 (included) and max (included)
+ *
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @public
+ */
+function nat(constraints: NatConstraints): ArbitraryWithShrink<number>;
+function nat(arg?: number | NatConstraints): ArbitraryWithShrink<number> {
+  const max = typeof arg === 'number' ? arg : arg && arg.max !== undefined ? arg.max : IntegerArbitrary.MAX_INT;
+  if (max < 0) {
+    throw new Error('fc.nat value should be greater than or equal to 0');
+  }
+  return new IntegerArbitrary(0, max);
 }
 
 /**
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index 49450ad7..0998d147 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -44,7 +44,14 @@ import { double, float, DoubleConstraints, FloatConstraints } from './check/arbi
 import { frequency, WeightedArbitrary, FrequencyValue } from './check/arbitrary/FrequencyArbitrary';
 import { compareBooleanFunc, compareFunc, func } from './check/arbitrary/FunctionArbitrary';
 import { domain } from './check/arbitrary/HostArbitrary';
-import { integer, maxSafeInteger, maxSafeNat, nat } from './check/arbitrary/IntegerArbitrary';
+import {
+  integer,
+  maxSafeInteger,
+  maxSafeNat,
+  nat,
+  IntegerConstraints,
+  NatConstraints,
+} from './check/arbitrary/IntegerArbitrary';
 import { ipV4, ipV4Extended, ipV6 } from './check/arbitrary/IpArbitrary';
 import { letrec } from './check/arbitrary/LetRecArbitrary';
 import { lorem, LoremConstraints } from './check/arbitrary/LoremArbitrary';
@@ -294,9 +301,11 @@ export {
   DoubleConstraints,
   FalsyContraints,
   FloatConstraints,
+  IntegerConstraints,
   JsonSharedConstraints,
   LoremConstraints,
   MixedCaseConstraints,
+  NatConstraints,
   ObjectConstraints,
   OptionConstraints,
   RecordConstraints,


Test patch:
diff --git a/test/unit/check/arbitrary/DateArbitrary.utest.spec.ts b/test/unit/check/arbitrary/DateArbitrary.utest.spec.ts
index 4090d454..850035a8 100644
--- a/test/unit/check/arbitrary/DateArbitrary.utest.spec.ts
+++ b/test/unit/check/arbitrary/DateArbitrary.utest.spec.ts
@@ -2,11 +2,16 @@ import { date } from '../../../../src/check/arbitrary/DateArbitrary';
 import * as stubRng from '../../stubs/generators';
 import { mocked } from 'ts-jest/utils';
 import * as fc from '../../../../lib/fast-check';
+import { ArbitraryWithShrink } from '../../../../src/check/arbitrary/definition/ArbitraryWithShrink';
 
 jest.mock('../../../../src/check/arbitrary/IntegerArbitrary');
-import * as IntegerArbitraryMock from '../../../../src/check/arbitrary/IntegerArbitrary';
+import * as _IntegerArbitraryMock from '../../../../src/check/arbitrary/IntegerArbitrary';
 import { arbitraryFor } from './generic/ArbitraryBuilder';
 
+const IntegerArbitraryMock: {
+  integer: (min: number, max: number) => ArbitraryWithShrink<number>;
+} = _IntegerArbitraryMock;
+
 const mrng = () => stubRng.mutable.nocall();
 
 describe('DateArbitrary', () => {
diff --git a/test/unit/check/arbitrary/IntegerArbitrary.spec.ts b/test/unit/check/arbitrary/IntegerArbitrary.spec.ts
index 243460f9..ac490771 100644
--- a/test/unit/check/arbitrary/IntegerArbitrary.spec.ts
+++ b/test/unit/check/arbitrary/IntegerArbitrary.spec.ts
@@ -6,6 +6,7 @@ import { integer, nat, maxSafeNat, maxSafeInteger } from '../../../../src/check/
 import * as genericHelper from './generic/GenericArbitraryHelper';
 
 import * as stubRng from '../../stubs/generators';
+import { generateOneValue } from './generic/GenerateOneValue';
 
 const isStrictlySmallerInteger = (v1: number, v2: number) => Math.abs(v1) < Math.abs(v2);
 
@@ -160,6 +161,13 @@ describe('IntegerArbitrary', () => {
         isValidValue: (g: number) => typeof g === 'number' && -0x80000000 <= g && g <= 0x7fffffff,
       });
     });
+    describe('Given minimal value only [between min and 2**31 -1]', () => {
+      genericHelper.isValidArbitrary((min: number) => integer({ min }), {
+        seedGenerator: fc.integer(),
+        isStrictlySmallerValue: isStrictlySmallerInteger,
+        isValidValue: (g: number, min: number) => typeof g === 'number' && min <= g && g <= 0x7fffffff,
+      });
+    });
     describe('Given maximal value only [between -2**31 and max]', () => {
       genericHelper.isValidArbitrary((maxValue: number) => integer(maxValue), {
         seedGenerator: fc.integer(),
@@ -178,6 +186,26 @@ describe('IntegerArbitrary', () => {
         }
       );
     });
+    describe('Still support older signatures', () => {
+      it('Should support fc.integer(max)', () => {
+        fc.assert(
+          fc.property(fc.integer(), fc.integer(), (seed, max) => {
+            const refArbitrary = integer({ max });
+            const otherArbitrary = integer(max);
+            expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+          })
+        );
+      });
+      it('Should support fc.integer(min, max)', () => {
+        fc.assert(
+          fc.property(fc.integer(), genericHelper.minMax(fc.integer()), (seed, constraints) => {
+            const refArbitrary = integer(constraints);
+            const otherArbitrary = integer(constraints.min, constraints.max);
+            expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+          })
+        );
+      });
+    });
   });
   describe('maxSafeInteger', () => {
     describe('Given no constraints [between MIN_SAFE_INTEGER and MAX_SAFE_INTEGER]', () => {
@@ -208,6 +236,17 @@ describe('IntegerArbitrary', () => {
         isValidValue: (g: number, maxValue: number) => typeof g === 'number' && g >= 0 && g <= maxValue,
       });
     });
+    describe('Still support older signatures', () => {
+      it('Should support fc.nat(max)', () => {
+        fc.assert(
+          fc.property(fc.integer(), fc.nat(), (seed, max) => {
+            const refArbitrary = nat({ max });
+            const otherArbitrary = nat(max);
+            expect(generateOneValue(seed, otherArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
+          })
+        );
+      });
+    });
   });
   describe('maxSafeNat', () => {
     describe('Given no constraints [between 0 and MAX_SAFE_INTEGER]', () => {
diff --git a/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts b/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts
index d2dba4eb..98646360 100644
--- a/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts
+++ b/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts
@@ -1,13 +1,19 @@
 import { uuid, uuidV } from '../../../../src/check/arbitrary/UuidArbitrary';
 import * as stubRng from '../../stubs/generators';
 import { mocked } from 'ts-jest/utils';
+import { ArbitraryWithShrink } from '../../../../src/check/arbitrary/definition/ArbitraryWithShrink';
 
 jest.mock('../../../../src/check/arbitrary/IntegerArbitrary');
 jest.mock('../../../../src/check/arbitrary/TupleArbitrary');
-import * as IntegerArbitraryMock from '../../../../src/check/arbitrary/IntegerArbitrary';
+import * as _IntegerArbitraryMock from '../../../../src/check/arbitrary/IntegerArbitrary';
 import * as TupleArbitraryMock from '../../../../src/check/arbitrary/TupleArbitrary';
 import { arbitraryFor } from './generic/ArbitraryBuilder';
 
+const IntegerArbitraryMock: {
+  integer: (min: number, max: number) => ArbitraryWithShrink<number>;
+  nat: (max: number) => ArbitraryWithShrink<number>;
+} = _IntegerArbitraryMock;
+
 const mrng = () => stubRng.mutable.nocall();
 
 describe('UuidArbitrary', () => {