Skip to content

Commit

Permalink
💥 Include invalid dates by default (#5589)
Browse files Browse the repository at this point in the history
**Description**

<!-- Please provide a short description and potentially linked issues
justifying the need for this PR -->

In v3, `fc.date` was not producing "Invalid Date" values by default. One
has to specify `noInvalidDate: false` to get some. With this PR we now
default the flag to false when not specified: in other words, the new
default will be to produce invalid dates except asked explicitly not to.

After that change, if you want not to produce "Invalid Date", you'll
have to use:

```js
// in v3, it was only producing valid dates
fc.date()

// equivalent in v4
fc.date({noInvalidDate: true})

//---

// in v3, including invalid dates required users to pass an extra constraint to the arbitrary
fc.date({noInvalidDate: false})

// equivalent in v4
fc.date()
```

You can also prepare yourself to v4 by already toggling: `noInvalidDate:
false` on your `fc.date`.

<!-- * Your PR is fixing a bug or regression? Check for existing issues
related to this bug and link them -->
<!-- * Your PR is adding a new feature? Make sure there is a related
issue or discussion attached to it -->

<!-- You can provide any additional context to help into understanding
what's this PR is attempting to solve: reproduction of a bug, code
snippets... -->

**Checklist** — _Don't delete this checklist and make sure you do the
following before opening the PR_

- [x] The name of my PR follows [gitmoji](https://gitmoji.dev/)
specification
- [x] My PR references one of several related issues (if any)
- [x] New features or breaking changes must come with an associated
Issue or Discussion
- [x] My PR does not add any new dependency without an associated Issue
or Discussion
- [x] My PR includes bumps details, please run `yarn bump` and flag the
impacts properly
- [x] My PR adds relevant tests and they would have failed without my PR
(when applicable)

<!-- More about contributing at
https://github.com/dubzzz/fast-check/blob/main/CONTRIBUTING.md -->

**Advanced**

<!-- How to fill the advanced section is detailed below! -->

**💥 Breaking change**:  Change defaults on date

<!-- [Category] Please use one of the categories below, it will help us
into better understanding the urgency of the PR -->
<!-- * ✨ Introduce new features -->
<!-- * 📝 Add or update documentation -->
<!-- * ✅ Add or update tests -->
<!-- * 🐛 Fix a bug -->
<!-- * 🏷️ Add or update types -->
<!-- * ⚡️ Improve performance -->
<!-- * _Other(s):_ ... -->

<!-- [Impacts] Please provide a comma separated list of the potential
impacts that might be introduced by this change -->
<!-- * Generated values: Can your change impact any of the existing
generators in terms of generated values, if so which ones? when? -->
<!-- * Shrink values: Can your change impact any of the existing
generators in terms of shrink values, if so which ones? when? -->
<!-- * Performance: Can it require some typings changes on user side?
Please give more details -->
<!-- * Typings: Is there a potential performance impact? In which cases?
-->
  • Loading branch information
dubzzz authored Jan 9, 2025
1 parent c5d0635 commit d5108bb
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-adults-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fast-check": major
---

💥 Include invalid dates by default
4 changes: 2 additions & 2 deletions packages/fast-check/src/arbitrary/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface DateConstraints {
max?: Date;
/**
* When set to true, no more "Invalid Date" can be generated.
* @defaultValue true
* @defaultValue false
* @remarks Since 3.13.0
*/
noInvalidDate?: boolean;
Expand All @@ -48,7 +48,7 @@ export function date(constraints: DateConstraints = {}): Arbitrary<Date> {
// Date min and max in ECMAScript specification : https://stackoverflow.com/a/11526569/3707828
const intMin = constraints.min !== undefined ? safeGetTime(constraints.min) : -8640000000000000;
const intMax = constraints.max !== undefined ? safeGetTime(constraints.max) : 8640000000000000;
const noInvalidDate = constraints.noInvalidDate === undefined || constraints.noInvalidDate;
const noInvalidDate = constraints.noInvalidDate;
if (safeNumberIsNaN(intMin)) throw new Error('fc.date min must be valid instance of Date');
if (safeNumberIsNaN(intMax)) throw new Error('fc.date max must be valid instance of Date');
if (intMin > intMax) throw new Error('fc.date max must be greater or equal to min');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
describe('timeToDateUnmapper', () => {
it('should be able to revert any mapped date correctly even invalid ones', () => {
fc.assert(
fc.property(fc.date({ noInvalidDate: false }), (d) => {
fc.property(fc.date(), (d) => {
// Arrange / Act
const rev = timeToDateUnmapper(d);
const revRev = timeToDateMapper(rev);
Expand All @@ -22,26 +22,22 @@ describe('timeToDateUnmapper', () => {
});
});

describe('timeToDateUnmapperWithNane', () => {
describe('timeToDateUnmapperWithNaN', () => {
it('should be able to revert any mapped date correctly even invalid once', () => {
fc.assert(
fc.property(
fc.date({ noInvalidDate: false }),
fc.integer({ min: -8640000000000000, max: 8640000000000001 }),
(d, nanValue) => {
// Arrange / Act
const rev = timeToDateUnmapperWithNaN(nanValue)(d);
const revRev = timeToDateMapperWithNaN(nanValue)(rev);
fc.property(fc.date(), fc.integer({ min: -8640000000000000, max: 8640000000000001 }), (d, nanValue) => {
// Arrange / Act
const rev = timeToDateUnmapperWithNaN(nanValue)(d);
const revRev = timeToDateMapperWithNaN(nanValue)(rev);

// Assert
if (d.getTime() === nanValue) {
expect(rev).toBe(nanValue);
expect(revRev.getTime()).toEqual(Number.NaN);
} else {
expect(revRev.getTime()).toEqual(d.getTime());
}
},
),
// Assert
if (d.getTime() === nanValue) {
expect(rev).toBe(nanValue);
expect(revRev.getTime()).toEqual(Number.NaN);
} else {
expect(revRev.getTime()).toEqual(d.getTime());
}
}),
);
});
});
20 changes: 15 additions & 5 deletions packages/fast-check/test/unit/arbitrary/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('date', () => {
fc.assert(
fc.property(constraintsArb(), (constraints) => {
// Arrange
const withInvalidDates = constraints.noInvalidDate === false;
const withInvalidDates = !constraints.noInvalidDate;
const { instance, map } = fakeArbitrary<number>();
const { instance: mappedInstance } = fakeArbitrary<Date>();
const integer = vi.spyOn(IntegerMock, 'integer');
Expand Down Expand Up @@ -115,7 +115,7 @@ describe('date', () => {
const d = mapper(rangeMin! + (mod % (rangeMax! - rangeMin! + 1))) as Date;

// Assert
if (constraints.noInvalidDate !== false || !Number.isNaN(d.getTime())) {
if (constraints.noInvalidDate || !Number.isNaN(d.getTime())) {
expect(d.getTime()).not.toBe(Number.NaN);
if (constraints.min) expect(d.getTime()).toBeGreaterThanOrEqual(constraints.min.getTime());
if (constraints.max) expect(d.getTime()).toBeLessThanOrEqual(constraints.max.getTime());
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('date (integration)', () => {
const extraParameters: fc.Arbitrary<Extra> = constraintsArb();

const isCorrect = (d: Date, extra: Extra) => {
if (extra.noInvalidDate || extra.noInvalidDate === undefined) {
if (extra.noInvalidDate) {
expect(d.getTime()).not.toBe(Number.NaN);
} else if (Number.isNaN(d.getTime())) {
return;
Expand Down Expand Up @@ -203,7 +203,13 @@ describe('date (integration)', () => {

function constraintsArb() {
return fc
.tuple(fc.date(), fc.date(), fc.boolean(), fc.boolean(), fc.option(fc.boolean(), { nil: undefined }))
.tuple(
fc.date({ noInvalidDate: true }),
fc.date({ noInvalidDate: true }),
fc.boolean(),
fc.boolean(),
fc.option(fc.boolean(), { nil: undefined }),
)
.map(([d1, d2, withMin, withMax, noInvalidDate]) => {
const min = d1 < d2 ? d1 : d2;
const max = d1 < d2 ? d2 : d1;
Expand All @@ -213,7 +219,11 @@ function constraintsArb() {

function invalidRangeConstraintsArb() {
return fc
.tuple(fc.date(), fc.date(), fc.option(fc.boolean(), { nil: undefined }))
.tuple(
fc.date({ noInvalidDate: true }),
fc.date({ noInvalidDate: true }),
fc.option(fc.boolean(), { nil: undefined }),
)
.filter(([d1, d2]) => +d1 !== +d2)
.map(([d1, d2, noInvalidDate]) => {
const min = d1 < d2 ? d1 : d2;
Expand Down
4 changes: 2 additions & 2 deletions website/docs/core-blocks/arbitraries/composites/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ fc.record(
);
// Note: All keys except 'id' will be optional values. id has been marked as required.
// Examples of generated values:
// • {"id":"46045be9-0009-4000-8000-0008ffffffed","name":"Karen","age":11,"birthday":new Date("2100-12-31T23:59:59.996Z")}
// • {"id":"46045be9-0009-4000-8000-0008ffffffed","name":"Karen","age":11,"birthday":new Date("2100-12-31T23:59:59.997Z")}
// • {"id":"fffffffe-0015-4000-95a0-f8e9ffffffe7","name":"Karen","birthday":new Date("1970-01-01T00:00:00.018Z")}
// • {"id":"e2b066ec-000b-4000-bfff-ffe7ccb1828d","name":"Karen","age":17}
// • {"id":"43b7d8e5-d043-42ef-8000-001a00000005","age":16,"birthday":new Date("2004-10-16T22:01:09.416Z")}
// • {"id":"00000007-2008-452e-8000-00133ed36be7","name":"Karen","age":6,"birthday":new Date("2100-12-31T23:59:59.981Z")}
// • {"id":"00000007-2008-452e-8000-00133ed36be7","name":"Karen","age":6,"birthday":new Date("2100-12-31T23:59:59.982Z")}
// • …

fc.record(
Expand Down
15 changes: 12 additions & 3 deletions website/docs/core-blocks/arbitraries/primitives/date.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Generate any possible dates in the specified range. Both the lower bound and upp

- `min?` — default: `new Date(-8640000000000000)`_lower bound of the range (included)_
- `max?` — default: `new Date(8640000000000000)`_upper bound of the range (included)_
- `noInvalidDate?` — default: `true`_when `true` the Date "Invalid Date" will never be defined_
- `noInvalidDate?` — default: `false`_when `true` the Date "Invalid Date" will never be defined_

**Usages:**

Expand All @@ -41,7 +41,7 @@ fc.date({ min: new Date('2000-01-01T00:00:00.000Z') });
// • new Date("2000-01-01T00:00:00.039Z")
// • new Date("2000-01-01T00:00:00.047Z")
// • new Date("2000-01-01T00:00:00.003Z")
// • new Date("+275760-09-12T23:59:59.981Z")
// • new Date("+275760-09-12T23:59:59.982Z")
// • …

fc.date({ max: new Date('2000-01-01T00:00:00.000Z') });
Expand All @@ -58,9 +58,18 @@ fc.date({ min: new Date('2000-01-01T00:00:00.000Z'), max: new Date('2000-12-31T2
// • new Date("2000-05-15T03:02:40.263Z")
// • new Date("2000-10-22T03:00:45.936Z")
// • new Date("2000-02-25T19:00:10.679Z")
// • new Date("2000-12-31T23:59:59.996Z")
// • new Date("2000-12-31T23:59:59.997Z")
// • new Date("2000-01-04T14:12:03.484Z")
// • …

fc.date({ noInvalidDate: true });
// Examples of generated values:
// • new Date("-043663-07-08T11:17:34.486Z")
// • new Date("-169183-12-11T00:28:46.358Z")
// • new Date("1969-12-31T23:59:59.988Z")
// • new Date("1969-12-31T23:59:59.984Z")
// • new Date("-271821-04-20T00:00:00.033Z")
// • …
```

Resources: [API reference](https://fast-check.dev/api-reference/functions/date.html).
Expand Down
11 changes: 11 additions & 0 deletions website/docs/migration/from-3.x-to-4.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ Version 4 of fast-check introduces significant changes as part of its major rele

To ensure a smoother migration to version 4, we recommend first upgrading to the latest minor release of version 3. Then, review and address the following deprecation notices to align your codebase with supported patterns.

### Changes on `date`

In version 4, the `date` arbitrary will generate any `Date` instances by default, including Invalid Date. If your code cannot handle invalid dates, you should add the `noInvalidDate: true` constraint to the configuration of your date builder to exclude such values.

```diff
-fc.date();
+fc.date({ noInvalidDate: true });
```

Related pull requests: [#5589](https://github.com/dubzzz/fast-check/pull/5589)

### Changes on `record`

In earlier versions, the `record` arbitrary included a flag named `withDeletedKeys`. Starting with version 2.11.0, this flag was deprecated and replaced by a new flag called `requiredKeys`. In version 4.0.0, the deprecated `withDeletedKeys` flag has been removed entirely.
Expand Down

0 comments on commit d5108bb

Please sign in to comment.