diff --git a/docs/.vitepress/api-pages.ts b/docs/.vitepress/api-pages.ts index 3a4144123d5..fcea7f72b39 100644 --- a/docs/.vitepress/api-pages.ts +++ b/docs/.vitepress/api-pages.ts @@ -5,6 +5,7 @@ export const apiPages = [ { text: 'Faker', link: '/api/faker.html' }, { text: 'SimpleFaker', link: '/api/simpleFaker.html' }, { text: 'Randomizer', link: '/api/randomizer.html' }, + { text: 'Distributors', link: '/api/distributors.html' }, { text: 'Utilities', link: '/api/utils.html' }, { text: 'Modules', diff --git a/docs/.vitepress/components/api-docs/refreshable-code.vue b/docs/.vitepress/components/api-docs/refreshable-code.vue index e9ebac1748f..23cfcc87fd7 100644 --- a/docs/.vitepress/components/api-docs/refreshable-code.vue +++ b/docs/.vitepress/components/api-docs/refreshable-code.vue @@ -25,10 +25,13 @@ function initRefresh(): Element[] { let lineIndex = 0; const result: Element[] = []; while (lineIndex < domLines.length) { - // Skip empty and preparatory lines (no '^faker.' invocation) + // Skip empty and preparatory lines (no recorded invocation) + // Keep in sync with ref scripts/shared/refreshable-code.ts if ( domLines[lineIndex]?.children.length === 0 || - !/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '') + !/^\w*faker\w*\.|^distributor\(/i.test( + domLines[lineIndex]?.textContent ?? '' + ) ) { lineIndex++; continue; diff --git a/scripts/apidocs/generate.ts b/scripts/apidocs/generate.ts index 81021c5fbd3..f798c303209 100644 --- a/scripts/apidocs/generate.ts +++ b/scripts/apidocs/generate.ts @@ -8,6 +8,7 @@ import type { RawApiDocsPage } from './processing/class'; import { processModuleClasses, processProjectClasses, + processProjectDistributors, processProjectInterfaces, processProjectUtilities, } from './processing/class'; @@ -26,6 +27,7 @@ export function processComponents(project: Project): RawApiDocsPage[] { return [ ...processProjectClasses(project), ...processProjectInterfaces(project), + processProjectDistributors(project), processProjectUtilities(project), ...processModuleClasses(project), ]; diff --git a/scripts/apidocs/processing/class.ts b/scripts/apidocs/processing/class.ts index 6e73d8f5df5..8171e191d8a 100644 --- a/scripts/apidocs/processing/class.ts +++ b/scripts/apidocs/processing/class.ts @@ -1,4 +1,5 @@ import type { ClassDeclaration, InterfaceDeclaration, Project } from 'ts-morph'; +import { wrapCode } from '../../shared/markdown'; import { required, valuesForKeys } from '../utils/value-checks'; import { newProcessingError } from './error'; import type { JSDocableLikeNode } from './jsdocs'; @@ -12,6 +13,7 @@ import type { RawApiDocsMethod } from './method'; import { processClassConstructors, processClassMethods, + processDistributorFunctions, processInterfaceMethods, processUtilityFunctions, } from './method'; @@ -196,6 +198,34 @@ export function processProjectUtilities(project: Project): RawApiDocsPage { }; } +// Distributors + +export function processProjectDistributors(project: Project): RawApiDocsPage { + console.log(`- Distributors`); + + const distributor = required( + project + .getSourceFile('src/distributors/distributor.ts') + ?.getTypeAlias('Distributor'), + 'Distributor' + ); + + const jsdocs = getJsDocs(distributor); + const description = `${getDescription(jsdocs)} + +${wrapCode(distributor.getText().replace(/export /, ''))}`; + + return { + title: 'Distributors', + camelTitle: 'distributors', + category: undefined, + deprecated: undefined, + description, + examples: getExamples(jsdocs), + methods: processDistributorFunctions(project), + }; +} + // Helpers function preparePage( diff --git a/scripts/apidocs/processing/method.ts b/scripts/apidocs/processing/method.ts index d46987f381a..6057b25b620 100644 --- a/scripts/apidocs/processing/method.ts +++ b/scripts/apidocs/processing/method.ts @@ -144,6 +144,17 @@ export function processUtilityFunctions(project: Project): RawApiDocsMethod[] { ); } +export function processDistributorFunctions( + project: Project +): RawApiDocsMethod[] { + return processMethodLikes( + Object.values(getAllFunctions(project)).filter((fn) => + fn.getSourceFile().getFilePath().includes('/src/distributors/') + ), + (f) => f.getNameOrThrow() + ); +} + // Method-likes type MethodLikeDeclaration = SignatureLikeDeclaration & diff --git a/scripts/shared/markdown.ts b/scripts/shared/markdown.ts index 72a28ef44e9..e2f4ccab18f 100644 --- a/scripts/shared/markdown.ts +++ b/scripts/shared/markdown.ts @@ -26,6 +26,12 @@ const htmlSanitizeOptions: sanitizeHtml.IOptions = { 'span', 'strong', 'ul', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', ], allowedAttributes: { a: ['href', 'target', 'rel'], @@ -33,6 +39,9 @@ const htmlSanitizeOptions: sanitizeHtml.IOptions = { div: ['class'], pre: ['class', 'dir', 'style', 'v-pre', 'tabindex'], span: ['class', 'style'], + table: ['tabindex'], + th: ['style'], + td: ['style'], }, selfClosing: [], }; @@ -49,6 +58,18 @@ function comparableSanitizedHtml(html: string): string { .replaceAll(' ', ''); } +/** + * Wraps the given code in a markdown code block. + * + * @param code The code to wrap. + * + * @returns The wrapped code. + */ +export function wrapCode(code: string): string { + const delimiter = '```'; + return `${delimiter}ts\n${code}\n${delimiter}`; +} + /** * Converts a Typescript code block to an HTML string and sanitizes it. * @@ -57,8 +78,7 @@ function comparableSanitizedHtml(html: string): string { * @returns The converted HTML string. */ export async function codeToHtml(code: string): Promise { - const delimiter = '```'; - return mdToHtml(`${delimiter}ts\n${code}\n${delimiter}`); + return mdToHtml(wrapCode(code)); } /** diff --git a/scripts/shared/refreshable-code.ts b/scripts/shared/refreshable-code.ts index ba7e02bab63..6406a7f3dcb 100644 --- a/scripts/shared/refreshable-code.ts +++ b/scripts/shared/refreshable-code.ts @@ -4,20 +4,21 @@ export async function toRefreshableCode( name: string, exampleCode: string ): Promise { - if (!/^\w*faker\w*\./im.test(exampleCode)) { - // No recordable faker calls in examples - return 'undefined'; - } - const exampleLines = exampleCode .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments .replaceAll(/^import .*$/gm, '') // Remove imports .replaceAll( - // record results of faker calls - /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim, + // record results of relevant calls + // Keep in sync with docs/.vitepress/components/api-docs/refreshable-code.vue + /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?|distributor\(.+\));?$/gim, `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` ); + if (!exampleLines.includes('try { result.push(')) { + // No recordable calls in examples + return 'undefined'; + } + const fullMethod = `async (): Promise => { await enableFaker(); const result: unknown[] = []; diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts new file mode 100644 index 00000000000..7061e8f3540 --- /dev/null +++ b/src/distributors/distributor.ts @@ -0,0 +1,39 @@ +import type { Randomizer } from '../randomizer'; + +/** + * A function that determines the distribution of generated values. + * Values generated by a randomizer are considered uniformly distributed, distributor functions can be used to change this. + * If many results are collected the results form a limited distribution between `0` and `1`. + * So an exponential distributor's values will resemble a limited exponential distribution. + * + * Common examples of distributor functions are: + * + * - Uniform distributor: All values have the same likelihood. + * - Normal distributor: Values are more likely to be close to a specific value. + * - Exponential distributor: Values are biased towards `0`/`1` (depending on options). + * + * Distributor functions can be used by some faker functions such as `faker.number.int()` and `faker.number.float()`. + * + * Please note that the result from the distributor function is processed further by the function accepting it. + * E.g. a distributor result of `0.5` within a call to `faker.number.int({ min: 10, max: 20 })` will result in `15`. + * + * @param randomizer The randomizer to use for generating values. + * + * @returns Generates a random float between 0 (inclusive) and 1 (exclusive). + * + * @example + * import { Distributor, Randomizer, faker } from '@faker-js/faker'; + * + * const alwaysMin: Distributor = () => 0; + * faker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 + * faker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 + * faker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 + * + * const uniform: Distributor = (randomizer: Randomizer) => randomizer.next(); + * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 5 + * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 2 + * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 9 + * + * @since 10.5.0 + */ +export type Distributor = (randomizer: Randomizer) => number; diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts new file mode 100644 index 00000000000..7b1a3857c46 --- /dev/null +++ b/src/distributors/exponential.ts @@ -0,0 +1,106 @@ +import { FakerError } from '../errors/faker-error'; +import type { Distributor } from './distributor'; +import { uniformDistributor } from './uniform'; + +/** + * Creates a new function that generates power-law/exponentially distributed values. + * This function uses `(base ** next() - 1) / (base - 1)` to spread the values. + * + * The following table shows the rough distribution of values generated using `exponentialDistributor({ base: x })`: + * + * | Result | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 | + * | :-------: | -------: | -------: | -----: | -----: | ------: | + * | 0.0 - 0.1 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% | + * | 0.1 - 0.2 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% | + * | 0.2 - 0.3 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 0.3 - 0.4 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% | + * | 0.4 - 0.5 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% | + * | 0.5 - 0.6 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% | + * | 0.6 - 0.7 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 0.7 - 0.8 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% | + * | 0.8 - 0.9 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% | + * | 0.9 - 1.0 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% | + * + * The following table shows the rough distribution of values generated using `exponentialDistributor({ bias: x })`: + * + * | Result | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 | + * | :-------: | ------: | ------: | -----: | -----: | -----: | + * | 0.0 - 0.1 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% | + * | 0.1 - 0.2 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% | + * | 0.2 - 0.3 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% | + * | 0.3 - 0.4 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 0.4 - 0.5 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% | + * | 0.5 - 0.6 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% | + * | 0.6 - 0.7 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% | + * | 0.7 - 0.8 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 0.8 - 0.9 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% | + * | 0.9 - 1.0 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% | + * + * @param options The options for generating the distributor. + * @param options.base The base of the exponential distribution. Should be greater than 0. Defaults to `2`. + * The higher/more above `1` the `base`, the more likely the number will be closer to the minimum value. + * The lower/closer to zero the `base`, the more likely the number will be closer to the maximum value. + * Values of `1` will generate a uniform distributor. + * Can alternatively be configured using the `bias` option. + * @param options.bias An alternative way to specify the `base`. Also accepts values below zero. Defaults to `-1`. + * The higher/more positive the `bias`, the more likely the number will be closer to the maximum value. + * The lower/more negative the `bias`, the more likely the number will be closer to the minimum value. + * Values of `0` will generate a uniform distributor. + * Can alternatively be configured using the `base` option. + * + * @example + * import { exponentialDistributor, generateMersenne53Randomizer } from '@faker-js/faker'; + * + * const randomizer = generateMersenne53Randomizer(); + * const distributor = exponentialDistributor(); + * distributor(randomizer) // 0.04643770898904198 + * distributor(randomizer) // 0.13436127925491848 + * distributor(randomizer) // 0.4202905589842396 + * distributor(randomizer) // 0.5164955927828387 + * distributor(randomizer) // 0.3476359433171099 + * + * @since 10.5.0 + */ +export function exponentialDistributor( + options?: + | { + /** + * The base of the exponential distribution. Should be greater than 0. + * The higher/more above `1` the `base`, the more likely the number will be closer to the minimum value. + * The lower/closer to zero the `base`, the more likely the number will be closer to the maximum value. + * Values of `1` will generate a uniform distribution. + * Can alternatively be configured using the `bias` option. + * + * @default 2 + */ + base?: number; + } + | { + /** + * An alternative way to specify the `base`. Also accepts values below zero. + * The higher/more positive the `bias`, the more likely the number will be closer to the maximum value. + * The lower/more negative the `bias`, the more likely the number will be closer to the minimum value. + * Values of `0` will generate a uniform distribution. + * Can alternatively be configured using the `base` option. + * + * @default -1 + */ + bias?: number; + } +): Distributor; +export function exponentialDistributor( + options: { + base?: number; + bias?: number; + } = {} +): Distributor { + const { bias = -1, base = bias <= 0 ? -bias + 1 : 1 / (bias + 1) } = options; + + if (base === 1) { + return uniformDistributor(); + } else if (base <= 0) { + throw new FakerError('Base should be greater than 0.'); + } + + return ({ next }) => (base ** next() - 1) / (base - 1); +} diff --git a/src/distributors/uniform.ts b/src/distributors/uniform.ts new file mode 100644 index 00000000000..a6e20faabe4 --- /dev/null +++ b/src/distributors/uniform.ts @@ -0,0 +1,41 @@ +import type { Distributor } from './distributor'; + +/** + * Creates a new function that generates uniformly distributed values. + * The likelihood of each value is the same. + * + * The following table shows the rough distribution of values generated using `uniformDistributor()`: + * + * | Result | Uniform | + * | :-------: | ------: | + * | 0.0 - 0.1 | 10.0% | + * | 0.1 - 0.2 | 10.0% | + * | 0.2 - 0.3 | 10.0% | + * | 0.3 - 0.4 | 10.0% | + * | 0.4 - 0.5 | 10.0% | + * | 0.5 - 0.6 | 10.0% | + * | 0.6 - 0.7 | 10.0% | + * | 0.7 - 0.8 | 10.0% | + * | 0.8 - 0.9 | 10.0% | + * | 0.9 - 1.0 | 10.0% | + * + * @returns A new uniform distributor function. + * + * @example + * import { generateMersenne53Randomizer, uniformDistributor } from '@faker-js/faker'; + * + * const randomizer = generateMersenne53Randomizer(); + * const distributor = uniformDistributor(); + * distributor(randomizer) // 0.9100215692561207 + * distributor(randomizer) // 0.791632947887336 + * distributor(randomizer) // 0.14770035310214324 + * distributor(randomizer) // 0.28282249581185814 + * distributor(randomizer) // 0.017890944117802343 + * + * @since 10.5.0 + */ +export function uniformDistributor(): Distributor { + return UNIFORM_DISTRIBUTOR; +} + +const UNIFORM_DISTRIBUTOR: Distributor = ({ next }) => next(); diff --git a/src/index.ts b/src/index.ts index 641380ca80a..ef2e33e924e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,9 @@ export type { VehicleDefinition, WordDefinition, } from './definitions'; +export type { Distributor } from './distributors/distributor'; +export { exponentialDistributor } from './distributors/exponential'; +export { uniformDistributor } from './distributors/uniform'; export { FakerError } from './errors/faker-error'; export { Faker } from './faker'; export * from './locale'; diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index dc5df50c158..59238b0cde5 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -1,3 +1,5 @@ +import type { Distributor } from '../../distributors/distributor'; +import { uniformDistributor } from '../../distributors/uniform'; import { FakerError } from '../../errors/faker-error'; import { SimpleModuleBase } from '../../internal/module-base'; @@ -24,6 +26,7 @@ export class NumberModule extends SimpleModuleBase { * @param options.min Lower bound for generated number. Defaults to `0`. * @param options.max Upper bound for generated number. Defaults to `Number.MAX_SAFE_INTEGER`. * @param options.multipleOf Generated number will be a multiple of the given integer. Defaults to `1`. + * @param options.distributor A function to determine the distribution of generated values. Defaults to `uniformDistributor()`. * * @throws {FakerError} When `min` is greater than `max`. * @throws {FakerError} When there are no suitable integers between `min` and `max`. @@ -63,13 +66,24 @@ export class NumberModule extends SimpleModuleBase { * @default 1 */ multipleOf?: number; + /** + * A function to determine the distribution of generated values. + * + * @default uniformDistributor() + */ + distributor?: Distributor; } = {} ): number { if (typeof options === 'number') { options = { max: options }; } - const { min = 0, max = Number.MAX_SAFE_INTEGER, multipleOf = 1 } = options; + const { + min = 0, + max = Number.MAX_SAFE_INTEGER, + multipleOf = 1, + distributor = uniformDistributor(), + } = options; if (!Number.isInteger(multipleOf)) { throw new FakerError(`multipleOf should be an integer.`); @@ -96,8 +110,7 @@ export class NumberModule extends SimpleModuleBase { throw new FakerError(`Max ${max} should be greater than min ${min}.`); } - const { randomizer } = this.faker.fakerCore; - const real = randomizer.next(); + const real = distributor(this.faker.fakerCore.randomizer); const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution return Math.floor(real * delta + effectiveMin) * multipleOf; } @@ -110,6 +123,7 @@ export class NumberModule extends SimpleModuleBase { * @param options.max Upper bound for generated number, exclusive, unless `multipleOf` or `fractionDigits` are passed. Defaults to `1.0`. * @param options.multipleOf The generated number will be a multiple of this parameter. Only one of `multipleOf` or `fractionDigits` should be passed. * @param options.fractionDigits The maximum number of digits to appear after the decimal point, for example `2` will round to 2 decimal points. Only one of `multipleOf` or `fractionDigits` should be passed. + * @param options.distributor A function to determine the distribution of generated values. Defaults to `uniformDistributor()`. * * @throws {FakerError} When `min` is greater than `max`. * @throws {FakerError} When `multipleOf` is not a positive number. @@ -153,6 +167,12 @@ export class NumberModule extends SimpleModuleBase { * The generated number will be a multiple of this parameter. Only one of `multipleOf` or `fractionDigits` should be passed. */ multipleOf?: number; + /** + * A function to determine the distribution of generated values. + * + * @default uniformDistributor() + */ + distributor?: Distributor; } = {} ): number { if (typeof options === 'number') { @@ -167,6 +187,7 @@ export class NumberModule extends SimpleModuleBase { fractionDigits, multipleOf: originalMultipleOf, multipleOf = fractionDigits == null ? undefined : 10 ** -fractionDigits, + distributor = uniformDistributor(), } = options; if (max < min) { @@ -205,12 +226,12 @@ export class NumberModule extends SimpleModuleBase { const int = this.int({ min: min * factor, max: max * factor, + distributor, }); return int / factor; } - const { randomizer } = this.faker.fakerCore; - const real = randomizer.next(); + const real = distributor(this.faker.fakerCore.randomizer); return real * (max - min) + min; } diff --git a/test/distributors/exponential.spec.ts b/test/distributors/exponential.spec.ts new file mode 100644 index 00000000000..0d3b4387943 --- /dev/null +++ b/test/distributors/exponential.spec.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from 'vitest'; +import { exponentialDistributor } from '../../src/distributors/exponential'; +import { FakerError } from '../../src/errors/faker-error'; +import { generateMersenne53Randomizer } from '../../src/utils/mersenne'; + +describe('exponentialDistributor', () => { + it('should generate an exponential distribution', () => { + const distributor = exponentialDistributor(); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeGreaterThan(125); + expect(results[9]).toBeLessThan(85); + }); + + it('should prefer base over bias if both are set', () => { + const distributor = exponentialDistributor({ base: 0.1, bias: -9 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeLessThan(50); + expect(results[9]).toBeGreaterThan(250); + }); + + describe('base option', () => { + it('should throw an error if base is invalid', () => { + expect(() => exponentialDistributor({ base: 0 })).toThrow( + new FakerError('Base should be greater than 0.') + ); + expect(() => exponentialDistributor({ base: -1 })).toThrow( + new FakerError('Base should be greater than 0.') + ); + }); + + it('should generate a distribution biased towards the maximum value when base is less than 1', () => { + const distributor = exponentialDistributor({ base: 0.1 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeLessThan(50); + expect(results[9]).toBeGreaterThan(250); + }); + + it('should generate a uniform distribution when base is 1', () => { + const distributor = exponentialDistributor({ base: 1 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + for (const [index, count] of results.entries()) { + expect(count, `Bucket ${index} has too few values`).toBeGreaterThan(75); + expect(count, `Bucket ${index} has too many values`).toBeLessThan(125); + } + }); + + it('should generate a distribution biased towards the minimum value when base is greater than 1', () => { + const distributor = exponentialDistributor({ base: 10 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeGreaterThan(250); + expect(results[9]).toBeLessThan(50); + }); + }); + + describe('bias option', () => { + it('should generate a distribution biased towards the minimum value when bias is negative', () => { + const distributor = exponentialDistributor({ bias: -9 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeGreaterThan(250); + expect(results[9]).toBeLessThan(50); + }); + + it('should generate a uniform distribution when bias is 0', () => { + const distributor = exponentialDistributor({ bias: 0 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + for (const [index, count] of results.entries()) { + expect(count, `Bucket ${index} has too few values`).toBeGreaterThan(75); + expect(count, `Bucket ${index} has too many values`).toBeLessThan(125); + } + }); + + it('should generate a distribution biased towards the maximum value when bias is positive', () => { + const distributor = exponentialDistributor({ bias: 9 }); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + expect(results[0]).toBeLessThan(50); + expect(results[9]).toBeGreaterThan(250); + }); + }); +}); diff --git a/test/distributors/uniform.spec.ts b/test/distributors/uniform.spec.ts new file mode 100644 index 00000000000..c0d3e133f8e --- /dev/null +++ b/test/distributors/uniform.spec.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { uniformDistributor } from '../../src/distributors/uniform'; +import { generateMersenne53Randomizer } from '../../src/utils/mersenne'; + +describe('uniformDistributor', () => { + it('should generate a uniform distribution', () => { + const distributor = uniformDistributor(); + const randomizer = generateMersenne53Randomizer(0); + + const results = Array.from({ length: 10 }, () => 0); + + for (let i = 0; i < 1000; i++) { + const value = distributor(randomizer); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + results[Math.floor(value * 10)]++; + } + + for (const [index, count] of results.entries()) { + expect(count, `Bucket ${index} has too few values`).toBeGreaterThan(75); + expect(count, `Bucket ${index} has too many values`).toBeLessThan(125); + } + }); +}); diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index be663dd6f8f..8ac8f572cb9 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -277,6 +277,17 @@ describe('number', () => { new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); + + it('should generate a number based on the provided distributor', () => { + let distributorCall = 0; + const distributor = () => (distributorCall++ % 4 === 0 ? 0.999 : 0); + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + results[faker.number.int({ max: 9, distributor })]++; + } + + expect(results).toEqual([750, 0, 0, 0, 0, 0, 0, 0, 0, 250]); + }); }); describe('float', () => { @@ -445,6 +456,17 @@ describe('number', () => { new FakerError(`Max ${max} should be greater than min ${min}.`) ); }); + + it('should generate a number based on the provided distributor', () => { + let distributorCall = 0; + const distributor = () => (distributorCall++ % 4 === 0 ? 0.999 : 0); + const results = Array.from({ length: 10 }, () => 0); + for (let i = 0; i < 1000; i++) { + results[Math.floor(faker.number.float({ max: 10, distributor }))]++; + } + + expect(results).toEqual([750, 0, 0, 0, 0, 0, 0, 0, 0, 250]); + }); }); describe('binary', () => { diff --git a/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap b/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap index 9da805707b3..1c91c55eadd 100644 --- a/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap +++ b/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap @@ -26,6 +26,13 @@ exports[`check docs completeness > all modules and methods are present 1`] = ` "seed", ], ], + [ + "distributors", + [ + "exponentialDistributor", + "uniformDistributor", + ], + ], [ "utils", [