diff --git a/docs/guide/upgrading_v9/2756.md b/docs/guide/upgrading_v9/2756.md new file mode 100644 index 00000000000..da0be8ebba5 --- /dev/null +++ b/docs/guide/upgrading_v9/2756.md @@ -0,0 +1,6 @@ +### Changed default mode from birthdate + +Previously, the method had defaults that were unclear in their specific impact. +Now, the method requires either none or all of the `min`, `max` and `mode` options. + +We also improved the error messages in case of invalid min/max age/year ranges. diff --git a/src/modules/date/index.ts b/src/modules/date/index.ts index 19dea55c3e0..f851c62e624 100644 --- a/src/modules/date/index.ts +++ b/src/modules/date/index.ts @@ -357,90 +357,199 @@ export class SimpleDateModule extends SimpleModuleBase { } /** - * Returns a random birthdate. + * Returns a random birthdate. By default, the birthdate is generated for an adult between 18 and 80 years old. + * But you can customize the `'age'` range or the `'year'` range to generate a more specific birthdate. * - * @param options The options to use to generate the birthdate. If no options are set, an age between 18 and 80 (inclusive) is generated. - * @param options.min The minimum age or year to generate a birthdate. - * @param options.max The maximum age or year to generate a birthdate. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `now`. - * @param options.mode The mode to generate the birthdate. Supported modes are `'age'` and `'year'` . + * @param options The options to use to generate the birthdate. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * + * @example + * faker.date.birthdate() // 1977-07-10T01:37:30.719Z + * + * @since 7.0.0 + */ + birthdate(options?: { + /** + * The date to use as reference point for the newly generated date. + * + * @default faker.defaultRefDate() + */ + refDate?: string | Date | number; + }): Date; + /** + * Returns a random birthdate for a given age range. + * + * @param options The options to use to generate the birthdate. + * @param options.mode `'age'` to generate a birthdate based on the age range. It is also possible to generate a birthdate based on a `'year'` range. + * @param options.min The minimum age to generate a birthdate for. + * @param options.max The maximum age to generate a birthdate for. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * + * @example + * faker.date.birthdate({ mode: 'age', min: 18, max: 65 }) // 2003-11-02T20:03:20.116Z * - * There are two modes available `'age'` and `'year'`: - * - `'age'`: The min and max options define the age of the person (e.g. `18` - `42`). - * - `'year'`: The min and max options define the range the birthdate may be in (e.g. `1900` - `2000`). + * @since 7.0.0 + */ + birthdate(options: { + /** + * `'age'` to generate a birthdate based on the age range. + * It is also possible to generate a birthdate based on a `'year'` range. + */ + mode: 'age'; + /** + * The minimum age to generate a birthdate for. + */ + min: number; + /** + * The maximum age to generate a birthdate for. + */ + max: number; + /** + * The date to use as reference point for the newly generated date. + * + * @default faker.defaultRefDate() + */ + refDate?: string | Date | number; + }): Date; + /** + * Returns a random birthdate in the given range of years. + * + * @param options The options to use to generate the birthdate. + * @param options.mode `'year'` to generate a birthdate based on the year range. It is also possible to generate a birthdate based on a `'age'` range. + * @param options.min The minimum year to generate a birthdate in. + * @param options.max The maximum year to generate a birthdate in. + * + * @example + * faker.date.birthdate({ mode: 'year', min: 1900, max: 2000 }) // 1940-08-20T08:53:07.538Z + * + * @since 7.0.0 + */ + birthdate(options: { + /** + * `'year'` to generate a birthdate based on the year range. + * It is also possible to generate a birthdate based on an `'age'` range. + */ + mode: 'year'; + /** + * The minimum year to generate a birthdate in. + */ + min: number; + /** + * The maximum year to generate a birthdate in. + */ + max: number; + }): Date; + /** + * Returns a random birthdate. By default, the birthdate is generated for an adult between 18 and 80 years old. + * But you can customize the `'age'` range or the `'year'` range to generate a more specific birthdate. * - * Defaults to `year`. + * @param options The options to use to generate the birthdate. + * @param options.mode Either `'age'` or `'year'` to generate a birthdate based on the age or year range. + * @param options.min The minimum age or year to generate a birthdate in. + * @param options.max The maximum age or year to generate a birthdate in. + * @param options.refDate The date to use as reference point for the newly generated date. + * Only used when `mode` is `'age'`. + * Defaults to `faker.defaultRefDate()`. * * @example * faker.date.birthdate() // 1977-07-10T01:37:30.719Z - * faker.date.birthdate({ min: 18, max: 65, mode: 'age' }) // 2003-11-02T20:03:20.116Z - * faker.date.birthdate({ min: 1900, max: 2000, mode: 'year' }) // 1940-08-20T08:53:07.538Z + * faker.date.birthdate({ mode: 'age', min: 18, max: 65 }) // 2003-11-02T20:03:20.116Z + * faker.date.birthdate({ mode: 'year', min: 1900, max: 2000 }) // 1940-08-20T08:53:07.538Z * * @since 7.0.0 */ + birthdate( + options?: + | { + /** + * The date to use as reference point for the newly generated date. + * + * @default faker.defaultRefDate() + */ + refDate?: string | Date | number; + } + | { + /** + * Either `'age'` or `'year'` to generate a birthdate based on the age or year range. + */ + mode: 'age' | 'year'; + /** + * The minimum age/year to generate a birthdate for/in. + */ + min: number; + /** + * The maximum age/year to generate a birthdate for/in. + */ + max: number; + /** + * The date to use as reference point for the newly generated date. + * Only used when `mode` is `'age'`. + * + * @default faker.defaultRefDate() + */ + refDate?: string | Date | number; + } + ): Date; birthdate( options: { - /** - * The minimum age or year to generate a birthdate. - * - * @default 18 - */ + mode?: 'age' | 'year'; min?: number; - /** - * The maximum age or year to generate a birthdate. - * - * @default 80 - */ max?: number; - /** - * The mode to generate the birthdate. Supported modes are `'age'` and `'year'` . - * - * There are two modes available `'age'` and `'year'`: - * - `'age'`: The min and max options define the age of the person (e.g. `18` - `42`). - * - `'year'`: The min and max options define the range the birthdate may be in (e.g. `1900` - `2000`). - * - * @default 'year' - */ - mode?: 'age' | 'year'; - /** - * The date to use as reference point for the newly generated date. - * - * @default faker.defaultRefDate() - */ refDate?: string | Date | number; } = {} ): Date { - const { mode = 'year', refDate = this.faker.defaultRefDate() } = options; - const date = toDate(refDate); - const refYear = date.getUTCFullYear(); - - // If no min or max is specified, generate a random date between (now - 80) years and (now - 18) years respectively - // So that people can still be considered as adults in most cases - - // Convert to epoch timestamps - let min: number; - let max: number; - if (mode === 'age') { - min = new Date(date).setUTCFullYear(refYear - (options.max ?? 80) - 1); - max = new Date(date).setUTCFullYear(refYear - (options.min ?? 18)); - } else { - // Avoid generating dates the first and last date of the year - // to avoid running into other years depending on the timezone. - min = new Date(Date.UTC(0, 0, 2)).setUTCFullYear( - options.min ?? refYear - 80 - ); - max = new Date(Date.UTC(0, 11, 30)).setUTCFullYear( - options.max ?? refYear - 19 - ); - } - - if (max < min) { + const { + mode = 'age', + min = 18, + max = 80, + refDate: rawRefDate = this.faker.defaultRefDate(), + mode: originalMode, + min: originalMin, + max: originalMax, + } = options; + + // TODO @ST-DDT 2024-03-17: Remove check in v10 + const optionsSet = [originalMin, originalMax, originalMode].filter( + (x) => x != null + ).length; + if (optionsSet % 3 !== 0) { throw new FakerError( - `Max ${options.max} should be larger than or equal to min ${options.min}.` + "The 'min', 'max', and 'mode' options must be set together." ); } - return new Date(this.faker.number.int({ min, max })); + const refDate = toDate(rawRefDate); + const refYear = refDate.getUTCFullYear(); + + switch (mode) { + case 'age': { + const from = new Date(refDate).setUTCFullYear(refYear - max - 1); + const to = new Date(refDate).setUTCFullYear(refYear - min); + + if (from > to) { + throw new FakerError( + `Max age ${max} should be greater than or equal to min age ${min}.` + ); + } + + return this.between({ from, to }); + } + + case 'year': { + // Avoid generating dates on the first and last date of the year + // to avoid running into other years depending on the timezone. + const from = new Date(Date.UTC(0, 0, 2)).setUTCFullYear(min); + const to = new Date(Date.UTC(0, 11, 30)).setUTCFullYear(max); + + if (from > to) { + throw new FakerError( + `Max year ${max} should be greater than or equal to min year ${min}.` + ); + } + + return this.between({ from, to }); + } + } } } diff --git a/test/modules/__snapshots__/date.spec.ts.snap b/test/modules/__snapshots__/date.spec.ts.snap index f67a339dfd3..d73d48cd6e7 100644 --- a/test/modules/__snapshots__/date.spec.ts.snap +++ b/test/modules/__snapshots__/date.spec.ts.snap @@ -67,17 +67,13 @@ exports[`date > 42 > betweens > with string dates and count 1`] = ` exports[`date > 42 > birthdate > with age and refDate 1`] = `1980-07-07T19:06:53.165Z`; -exports[`date > 42 > birthdate > with age mode and refDate 1`] = `1963-09-27T06:10:42.813Z`; - exports[`date > 42 > birthdate > with age range and refDate 1`] = `1962-12-27T20:14:08.437Z`; -exports[`date > 42 > birthdate > with only refDate 1`] = `1964-03-22T08:05:48.849Z`; - -exports[`date > 42 > birthdate > with year and refDate 1`] = `0020-07-07T19:06:53.165Z`; +exports[`date > 42 > birthdate > with only refDate 1`] = `1963-09-27T06:10:42.813Z`; -exports[`date > 42 > birthdate > with year mode and refDate 1`] = `1964-03-22T08:05:48.849Z`; +exports[`date > 42 > birthdate > with year 1`] = `2000-05-16T22:59:36.655Z`; -exports[`date > 42 > birthdate > with year range and refDate 1`] = `0057-12-20T11:59:38.353Z`; +exports[`date > 42 > birthdate > with year range 1`] = `1937-10-30T15:52:21.843Z`; exports[`date > 42 > future > with only Date refDate 1`] = `2021-07-08T10:07:33.524Z`; @@ -195,17 +191,13 @@ exports[`date > 1211 > betweens > with string dates and count 1`] = ` exports[`date > 1211 > birthdate > with age and refDate 1`] = `1981-01-26T13:16:31.426Z`; -exports[`date > 1211 > birthdate > with age mode and refDate 1`] = `1998-08-21T21:24:31.101Z`; - exports[`date > 1211 > birthdate > with age range and refDate 1`] = `1996-10-13T01:44:07.954Z`; -exports[`date > 1211 > birthdate > with only refDate 1`] = `1998-07-25T13:16:47.251Z`; +exports[`date > 1211 > birthdate > with only refDate 1`] = `1998-08-21T21:24:31.101Z`; -exports[`date > 1211 > birthdate > with year and refDate 1`] = `0021-01-26T13:16:31.426Z`; +exports[`date > 1211 > birthdate > with year 1`] = `2000-12-04T01:16:03.291Z`; -exports[`date > 1211 > birthdate > with year mode and refDate 1`] = `1998-07-25T13:16:47.251Z`; - -exports[`date > 1211 > birthdate > with year range and refDate 1`] = `0113-12-03T19:45:28.165Z`; +exports[`date > 1211 > birthdate > with year range 1`] = `1993-10-11T07:45:00.030Z`; exports[`date > 1211 > future > with only Date refDate 1`] = `2022-01-26T14:59:27.356Z`; @@ -321,17 +313,13 @@ exports[`date > 1337 > betweens > with string dates and count 1`] = ` exports[`date > 1337 > birthdate > with age and refDate 1`] = `1980-05-27T14:46:44.794Z`; -exports[`date > 1337 > birthdate > with age mode and refDate 1`] = `1956-08-25T03:56:58.153Z`; - exports[`date > 1337 > birthdate > with age range and refDate 1`] = `1956-02-15T21:16:37.850Z`; -exports[`date > 1337 > birthdate > with only refDate 1`] = `1957-03-31T18:18:16.563Z`; - -exports[`date > 1337 > birthdate > with year and refDate 1`] = `0020-05-27T14:46:44.794Z`; +exports[`date > 1337 > birthdate > with only refDate 1`] = `1956-08-25T03:56:58.153Z`; -exports[`date > 1337 > birthdate > with year mode and refDate 1`] = `1957-03-31T18:18:16.563Z`; +exports[`date > 1337 > birthdate > with year 1`] = `2000-04-06T02:45:32.287Z`; -exports[`date > 1337 > birthdate > with year range and refDate 1`] = `0046-08-09T19:19:14.289Z`; +exports[`date > 1337 > birthdate > with year range 1`] = `1926-06-20T07:18:01.782Z`; exports[`date > 1337 > future > with only Date refDate 1`] = `2021-05-28T08:29:26.600Z`; diff --git a/test/modules/date.spec.ts b/test/modules/date.spec.ts index 432573c76ca..d459db337e4 100644 --- a/test/modules/date.spec.ts +++ b/test/modules/date.spec.ts @@ -106,37 +106,27 @@ describe('date', () => { t.describe('birthdate', (t) => { t.it('with only refDate', { refDate }) - .it('with age mode and refDate', { - mode: 'age', - refDate, - }) .it('with age and refDate', { + mode: 'age', min: 40, max: 40, - mode: 'age', refDate, }) .it('with age range and refDate', { + mode: 'age', min: 20, max: 80, - mode: 'age', refDate, }) - .it('with year mode and refDate', { + .it('with year', { mode: 'year', - refDate, - }) - .it('with year and refDate', { min: 2000, max: 2000, - mode: 'age', - refDate, }) - .it('with year range and refDate', { + .it('with year range', { + mode: 'year', min: 1900, max: 2000, - mode: 'age', - refDate, }); }); }); @@ -543,9 +533,9 @@ describe('date', () => { expect(birthdate).toBeInstanceOf(Date); }); - it('returns a random birthdate between two years', () => { + it('returns a random birthdate in one year', () => { const min = 1990; - const max = 2000; + const max = 1990; const birthdate = faker.date.birthdate({ min, max, mode: 'year' }); @@ -557,53 +547,72 @@ describe('date', () => { expect(birthdate.getUTCFullYear()).toBeLessThanOrEqual(max); }); - it('returns a random birthdate that is 18+ by default', () => { - // Generate the latest possible value => youngest - faker.seed(2855577693); - - const refDate = new Date(); - const birthdate = faker.date.birthdate({ refDate }); - expect(birthdate).toBeInstanceOf(Date); - const value = birthdate.valueOf(); - const refDateValue = refDate.valueOf(); - expect(value).toBeLessThanOrEqual(refDateValue); - const deltaDate = new Date(refDateValue - value); - expect(deltaDate.getUTCFullYear() - 1970).toBeGreaterThanOrEqual(18); - }); - - it('returns a random birthdate in one year', () => { + it('returns a random birthdate between two years', () => { const min = 1990; - const max = 1990; + const max = 2000; const birthdate = faker.date.birthdate({ min, max, mode: 'year' }); // birthdate is a date object expect(birthdate).toBeInstanceOf(Date); - expect(birthdate.toISOString()).not.toMatch(/T00:00:00.000Z/); // Generated date is between min and max expect(birthdate.getUTCFullYear()).toBeGreaterThanOrEqual(min); expect(birthdate.getUTCFullYear()).toBeLessThanOrEqual(max); }); + it('returns a random birthdate for specific age', () => { + const min = 21; + const max = 21; + const refDate = new Date(); + + const birthdate = faker.date.birthdate({ + min, + max, + refDate, + mode: 'age', + }); + + expect(birthdate).toBeInstanceOf(Date); + const value = birthdate.valueOf(); + const refDateValue = refDate.valueOf(); + expect(value).toBeLessThanOrEqual(refDateValue); + const deltaDate = new Date(refDateValue - value); + expect(deltaDate.getUTCFullYear() - 1970).toBe(21); + }); + it('returns a random birthdate between two ages', () => { - const min = 4; - const max = 5; + const min = 21; + const max = 22; + const refDate = new Date(); const birthdate = faker.date.birthdate({ min, max, mode: 'age' }); - // birthdate is a date object expect(birthdate).toBeInstanceOf(Date); - - // Generated date is between min and max - expect(birthdate.getUTCFullYear()).toBeGreaterThanOrEqual( - new Date().getUTCFullYear() - max - 1 - ); - expect(birthdate.getUTCFullYear()).toBeLessThanOrEqual( - new Date().getUTCFullYear() - min - ); + const value = birthdate.valueOf(); + const refDateValue = refDate.valueOf(); + expect(value).toBeLessThanOrEqual(refDateValue); + const deltaDate = new Date(refDateValue - value); + expect(deltaDate.getUTCFullYear() - 1970).toBeGreaterThanOrEqual(21); + expect(deltaDate.getUTCFullYear() - 1970).toBeLessThanOrEqual(22); }); + it.each(['min', 'max', 'mode'] as const)( + "should throw an error when '%s' is not provided", + (key) => { + const options = { min: 18, max: 80, mode: 'age' } as const; + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete options[key]; + + expect(() => faker.date.birthdate(options)).toThrow( + new FakerError( + `The 'min', 'max', and 'mode' options must be set together.` + ) + ); + } + ); + it('should throw an error when the min > max year', () => { const min = 2000; const max = 1990; @@ -612,7 +621,21 @@ describe('date', () => { faker.date.birthdate({ min, max, mode: 'year' }) ).toThrow( new FakerError( - `Max 1990 should be larger than or equal to min 2000.` + `Max year 1990 should be greater than or equal to min year 2000.` + ) + ); + }); + + it('should throw an error when the min > max age', () => { + const min = 31; + const max = 25; + const refDate = Date.UTC(2020, 0, 1); + + expect(() => + faker.date.birthdate({ min, max, refDate, mode: 'age' }) + ).toThrow( + new FakerError( + `Max age 25 should be greater than or equal to min age 31.` ) ); });