Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/module-tree/generate-module-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const autoGeneratedCommentHeader = `/*

function patchModuleImports(moduleName: string, importHelper: ImportHelper) {
switch (moduleName) {
case 'date': {
importHelper.addTypeImports('../../utils/types', 'FixedLengthArray');
break;
}
case 'finance': {
importHelper.removeImports('./bitcoin-address', 'BitcoinAddressFamily');
break;
Expand Down
85 changes: 65 additions & 20 deletions src/modules/date/betweens.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,64 @@
import type { FakerCore } from '../../core';
import type { NumberOrRange } from '../../utils/types';
import type { FixedLengthArray, NumberOrRange } from '../../utils/types';
import { fakeHelpersMultiple } from '../helpers/multiple';
import { fakeDateBetween } from './between';

/**
* Options for generating dates between two boundaries.
*/
export interface BetweensOptions {
/** The early date boundary. */
from: string | Date | number;
/** The late date boundary. */
to: string | Date | number;
/**
* The number of dates to generate.
*
* @default 3
*/
count?: NumberOrRange;
}

/**
* Generates random dates between the given boundaries. The dates will be returned in an array sorted in chronological order.
*
* @template TCount The explicit number or range of dates to generate.
*
* @param fakerCore The FakerCore to use.
* @param options The options object.
* @param options.from The early date boundary.
* @param options.to The late date boundary.
* @param options.count The number of dates to generate. Defaults to `3`.
* Explicit integer literal counts from 0 to 100 infer a fixed-length tuple.
* Other counts return an array.
*
* @throws {FakerError} If `from` or `to` are not provided.
* @throws {FakerError} If `from` is after `to`.
*
* @example
* fakeDateBetweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z' })
* // [
* // '2022-07-02T06:00:00.000Z',
* // '2024-12-31T12:00:00.000Z',
* // '2027-07-02T18:00:00.000Z'
* // ]
* fakeDateBetweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: 2 })
* // [ '2023-05-02T16:00:00.000Z', '2026-09-01T08:00:00.000Z' ]
* fakeDateBetweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: { min: 2, max: 5 }})
* // [
* // 2021-12-19T06:35:40.191Z,
* // 2022-09-10T08:03:51.351Z,
* // 2023-04-19T11:41:17.501Z
* // ]
*
* @since 11.0.0
*
* @experimental
*/
export function fakeDateBetweens<const TCount extends NumberOrRange>(
fakerCore: FakerCore,
options: BetweensOptions & { count: TCount }
): FixedLengthArray<Date, TCount>;
/**
* Generates random dates between the given boundaries. The dates will be returned in an array sorted in chronological order.
*
Expand All @@ -11,6 +67,8 @@ import { fakeDateBetween } from './between';
* @param options.from The early date boundary.
* @param options.to The late date boundary.
* @param options.count The number of dates to generate. Defaults to `3`.
* Explicit integer literal counts from 0 to 100 infer a fixed-length tuple.
* Other counts return an array.
*
* @throws {FakerError} If `from` or `to` are not provided.
* @throws {FakerError} If `from` is after `to`.
Expand All @@ -37,29 +95,16 @@ import { fakeDateBetween } from './between';
*/
export function fakeDateBetweens(
fakerCore: FakerCore,
options: {
/**
* The early date boundary.
*/
from: string | Date | number;
/**
* The late date boundary.
*/
to: string | Date | number;
/**
* The number of dates to generate.
*
* @default 3
*/
count?: NumberOrRange;
}
options: BetweensOptions
): Date[];
export function fakeDateBetweens(
fakerCore: FakerCore,
options: BetweensOptions
): Date[] {
const { from, to, count = 3 } = options;
return fakeHelpersMultiple(
fakerCore,
() => fakeDateBetween(fakerCore, { from, to }),
{
count,
}
{ count }
).toSorted((a, b) => a.getTime() - b.getTime());
}
59 changes: 42 additions & 17 deletions src/modules/date/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ModuleBase } from '../../internal/module-base';
import type { NumberOrRange } from '../../utils/types';
import type { FixedLengthArray, NumberOrRange } from '../../utils/types';
import { fakeDateAnytime } from './anytime';
import { fakeDateBetween } from './between';
import type { BetweensOptions } from './betweens';
import { fakeDateBetweens } from './betweens';
import { fakeDateBirthdate } from './birthdate';
import { fakeDateFuture } from './future';
Expand Down Expand Up @@ -160,10 +161,14 @@ export class SimpleDateModule extends ModuleBase {
/**
* Generates random dates between the given boundaries. The dates will be returned in an array sorted in chronological order.
*
* @template TCount The explicit number or range of dates to generate.
*
* @param options The options object.
* @param options.from The early date boundary.
* @param options.to The late date boundary.
* @param options.count The number of dates to generate. Defaults to `3`.
* Explicit integer literal counts from 0 to 100 infer a fixed-length tuple.
* Other counts return an array.
*
* @throws {FakerError} If `from` or `to` are not provided.
* @throws {FakerError} If `from` is after `to`.
Expand All @@ -186,22 +191,42 @@ export class SimpleDateModule extends ModuleBase {
*
* @since 8.0.0
*/
betweens(options: {
/**
* The early date boundary.
*/
from: string | Date | number;
/**
* The late date boundary.
*/
to: string | Date | number;
/**
* The number of dates to generate.
*
* @default 3
*/
count?: NumberOrRange;
}): Date[] {
betweens<const TCount extends NumberOrRange>(
options: BetweensOptions & { count: TCount }
): FixedLengthArray<Date, TCount>;
/**
* Generates random dates between the given boundaries. The dates will be returned in an array sorted in chronological order.
*
* @param options The options object.
* @param options.from The early date boundary.
* @param options.to The late date boundary.
* @param options.count The number of dates to generate. Defaults to `3`.
* Explicit integer literal counts from 0 to 100 infer a fixed-length tuple.
* Other counts return an array.
*
* @throws {FakerError} If `from` or `to` are not provided.
* @throws {FakerError} If `from` is after `to`.
*
* @example
* faker.date.betweens({ from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z' })
* // [
* // '2022-07-02T06:00:00.000Z',
* // '2024-12-31T12:00:00.000Z',
* // '2027-07-02T18:00:00.000Z'
* // ]
* faker.date.betweens({ from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: 2 })
* // [ '2023-05-02T16:00:00.000Z', '2026-09-01T08:00:00.000Z' ]
* faker.date.betweens({ from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: { min: 2, max: 5 }})
* // [
* // 2021-12-19T06:35:40.191Z,
* // 2022-09-10T08:03:51.351Z,
* // 2023-04-19T11:41:17.501Z
* // ]
*
* @since 11.0.0
*/
betweens(options: BetweensOptions): Date[];
betweens(options: BetweensOptions): Date[] {
return fakeDateBetweens(this.fakerCore, options);
}

Expand Down
26 changes: 26 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,29 @@ export enum LengthStrategy {
* The strategy to apply when no words with a matching length are found.
*/
export type LengthStrategyType = `${LengthStrategy}`;

/**
* An array with a known length for small non-negative integer literals.
* Other counts remain arrays to keep type instantiation bounded.
*/
export type FixedLengthArray<
T,
TCount,
TResult extends T[] = [],
> = TCount extends number
? number extends TCount
? T[]
: `${TCount}` extends `-${string}` | `${string}.${string}`
? T[]
: BuildFixedLengthArray<T, TCount, TResult>
: T[];

type BuildFixedLengthArray<
T,
TCount extends number,
TResult extends T[],
> = TResult['length'] extends TCount
? TResult
: TResult['length'] extends 100
? T[]
: BuildFixedLengthArray<T, TCount, [...TResult, T]>;
79 changes: 79 additions & 0 deletions test/modules/date.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expectTypeOf, it } from 'vitest';
import { faker } from '../../src';
import { createFakerCore } from '../../src/core';
import { fakeDateBetweens } from '../../src/modules/date/betweens';
import type { NumberOrRange } from '../../src/utils/types';

describe('date.betweens', () => {
const core = createFakerCore();
const boundaries = { from: '2026-01-01', to: '2026-12-31' };

it('infers tuples for explicit integer literal counts', () => {
expectTypeOf(
faker.date.betweens({ ...boundaries, count: 0 })
).toEqualTypeOf<[]>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: 2 })
).toEqualTypeOf<[Date, Date]>();
expectTypeOf(
fakeDateBetweens(core, { ...boundaries, count: 2 })
).toEqualTypeOf<[Date, Date]>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: 100 })['length']
).toEqualTypeOf<100>();
});

it('keeps arrays for omitted, undefined, dynamic and ranged counts', () => {
const count = 2 as number;
expectTypeOf(faker.date.betweens(boundaries)).toEqualTypeOf<Date[]>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: undefined })
).toEqualTypeOf<Date[]>();
expectTypeOf(faker.date.betweens({ ...boundaries, count })).toEqualTypeOf<
Date[]
>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: { min: 1, max: 3 } })
).toEqualTypeOf<Date[]>();
});

it('rejects unknown option properties', () => {
// @ts-expect-error Unknown option properties must not become accepted.
const actual = faker.date.betweens({ ...boundaries, count: 2, typo: true });
expectTypeOf(actual).toEqualTypeOf<[Date, Date]>();
const standalone = fakeDateBetweens(core, {
...boundaries,
count: 2,
// @ts-expect-error The standalone surface has the same option validation.
typo: true,
});
expectTypeOf(standalone).toEqualTypeOf<[Date, Date]>();
});

it('does not recurse without bound for large, fractional or negative counts', () => {
expectTypeOf(
faker.date.betweens({ ...boundaries, count: 101 })
).toEqualTypeOf<Date[]>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: 1.5 })
).toEqualTypeOf<Date[]>();
expectTypeOf(
faker.date.betweens({ ...boundaries, count: -1 })
).toEqualTypeOf<Date[]>();
});

it('distributes over counts without promising a fixed length for unknown options', () => {
const union = 1 as 1 | 2;
expectTypeOf(
faker.date.betweens({ ...boundaries, count: union })
).toEqualTypeOf<[Date] | [Date, Date]>();
const optionalCount: typeof boundaries & { count?: 2 } = boundaries;
expectTypeOf(faker.date.betweens(optionalCount)).toEqualTypeOf<Date[]>();
const optionalUndefined = 2 as 2 | undefined;
expectTypeOf(
faker.date.betweens({ ...boundaries, count: optionalUndefined })
).toEqualTypeOf<Date[]>();
const options: typeof boundaries & { count?: NumberOrRange } = boundaries;
expectTypeOf(faker.date.betweens(options)).toEqualTypeOf<Date[]>();
});
});
Loading