From 34e62e34110d28d807bfe9d2764e362298c1bd34 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 18 Jan 2025 15:52:48 +0100 Subject: [PATCH 01/19] feat(number): add exponentialDistribution function --- scripts/apidocs/utils/markdown.ts | 9 + src/modules/number/index.ts | 161 ++++++++++++++++++ .../modules/__snapshots__/number.spec.ts.snap | 42 +++++ test/modules/number.spec.ts | 112 ++++++++++++ .../verify-jsdoc-tags.spec.ts.snap | 1 + 5 files changed, 325 insertions(+) diff --git a/scripts/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts index 2787a611bf4..5b81d7d534e 100644 --- a/scripts/apidocs/utils/markdown.ts +++ b/scripts/apidocs/utils/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', 'v-pre', 'tabindex'], span: ['class', 'style'], + table: ['tabindex'], + th: ['style'], + td: ['style'], }, selfClosing: [], }; diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index f5f5c27fab6..aefe4ad1fe7 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -534,4 +534,165 @@ export class NumberModule extends SimpleModuleBase { return result; } + + /** + * Generates a random number between `min` and `max` using an exponential distribution. + * The lower bound is inclusive, but the upper bound is exclusive. + * + * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, base: x }))`: + * + * | Value | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 | + * | :---: | -------: | -------: | -----: | -----: | ------: | + * | 0 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% | + * | 1 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% | + * | 2 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 3 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% | + * | 4 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% | + * | 5 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% | + * | 6 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 7 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% | + * | 8 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% | + * | 9 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% | + * + * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, bias: x }))`: + * + * | Value | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 | + * | :---: | ------: | ------: | -----: | -----: | -----: | + * | 0 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% | + * | 1 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% | + * | 2 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% | + * | 3 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 4 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% | + * | 5 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% | + * | 6 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% | + * | 7 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 8 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% | + * | 9 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% | + * + * @param options The options for generating the number. + * @param options.min The minimum value to generate (inclusive). Defaults to `0`. + * @param options.max The maximum value to generate (exclusive). Defaults to `1`. + * @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 distribution. + * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, base: x }))`: + * + * | Value | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 | + * | :---: | -------: | -------: | -----: | -----: | ------: | + * | 0 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% | + * | 1 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% | + * | 2 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 3 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% | + * | 4 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% | + * | 5 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% | + * | 6 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 7 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% | + * | 8 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% | + * | 9 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% | + * + * Can alternatively be configured using the `bias` option. `base` takes precedence over `bias`. + * @param options.bias 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. + * + * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, bias: x }))`: + * + * | Value | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 | + * | :---: | ------: | ------: | -----: | -----: | -----: | + * | 0 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% | + * | 1 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% | + * | 2 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% | + * | 3 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% | + * | 4 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% | + * | 5 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% | + * | 6 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% | + * | 7 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | + * | 8 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% | + * | 9 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% | + * + * This option is ignored if `base` is specified. + * + * Defaults to `-1`. + * + * @throws If `base` is less than or equal to `0`. + * @throws If `max` is less than `min`. + * + * @example + * faker.number.exponentialDistribution() // 0.41928964795957224 + * faker.number.exponentialDistribution(10) // 1.656598169056771 + * faker.number.exponentialDistribution({ min: 10, max: 100 }) // 88.7273250669911 + * faker.number.exponentialDistribution({ min: 0, max: 100, base: 10 }) // 6.9442760672808745 + * faker.number.exponentialDistribution({ min: 0, max: 100, bias: 10 }) // 67.03715679154617 + * + * @since 9.5.0 + */ + exponentialDistribution( + options: + | number + | { + /** + * The minimum value to generate (inclusive). + * + * @default 0 + */ + min?: number; + /** + * The maximum value to generate (exclusive). + * + * @default 1 + */ + max?: number; + /** + * 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 distribution. + * Can alternatively be configured using the `bias` option. `base` takes precedence over `bias`. + * + * @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. + * This option is ignored if `base` is specified. + * + * @default -1 + */ + bias?: number; + } = {} + ): number { + if (typeof options === 'number') { + options = { min: 0, max: options }; + } + + const { + min = 0, + max = 1, + bias = -1, + base = bias <= 0 ? -bias + 1 : 1 / (bias + 1), + } = options; + + if (base === 1) { + return this.faker.number.float({ min, max }); + } else if (base <= 0) { + throw new FakerError('Base should be greater than 0.'); + } + + if (max === min) { + return min; + } else if (max < min) { + throw new FakerError(`Max ${max} should be greater than min ${min}.`); + } + + const exponent = this.faker.number.float(); + const factor = (base ** exponent - 1) / (base - 1); + return min + (max - min) * factor; + } } diff --git a/test/modules/__snapshots__/number.spec.ts.snap b/test/modules/__snapshots__/number.spec.ts.snap index 516abc9ffa8..db0e573981b 100644 --- a/test/modules/__snapshots__/number.spec.ts.snap +++ b/test/modules/__snapshots__/number.spec.ts.snap @@ -20,6 +20,20 @@ exports[`number > 42 > binary > with options 1`] = `"100"`; exports[`number > 42 > binary > with value 1`] = `"0"`; +exports[`number > 42 > exponentialDistribution > noArgs 1`] = `0.29642623304954707`; + +exports[`number > 42 > exponentialDistribution > with high base 1`] = `0.15209599448489752`; + +exports[`number > 42 > exponentialDistribution > with high bias 1`] = `0.6420630212280508`; + +exports[`number > 42 > exponentialDistribution > with low base 1`] = `0.6420630212280508`; + +exports[`number > 42 > exponentialDistribution > with low bias 1`] = `0.15209599448489752`; + +exports[`number > 42 > exponentialDistribution > with max 1`] = `2.9642623304954707`; + +exports[`number > 42 > exponentialDistribution > with min and max 1`] = `36.67836097445924`; + exports[`number > 42 > float > with max 1`] = `25.84326820046801`; exports[`number > 42 > float > with min 1`] = `-25.89477488956341`; @@ -84,6 +98,20 @@ exports[`number > 1211 > binary > with options 1`] = `"1010"`; exports[`number > 1211 > binary > with value 1`] = `"1"`; +exports[`number > 1211 > exponentialDistribution > noArgs 1`] = `0.9033226590337899`; + +exports[`number > 1211 > exponentialDistribution > with high base 1`] = `0.8313808279511881`; + +exports[`number > 1211 > exponentialDistribution > with high bias 1`] = `0.9801213540567579`; + +exports[`number > 1211 > exponentialDistribution > with low base 1`] = `0.9801213540567579`; + +exports[`number > 1211 > exponentialDistribution > with low bias 1`] = `0.8313808279511881`; + +exports[`number > 1211 > exponentialDistribution > with max 1`] = `9.0332265903379`; + +exports[`number > 1211 > exponentialDistribution > with min and max 1`] = `91.29903931304109`; + exports[`number > 1211 > float > with max 1`] = `64.06789061927832`; exports[`number > 1211 > float > with min 1`] = `-2.0736333821888806`; @@ -148,6 +176,20 @@ exports[`number > 1337 > binary > with options 1`] = `"10"`; exports[`number > 1337 > binary > with value 1`] = `"0"`; +exports[`number > 1337 > exponentialDistribution > noArgs 1`] = `0.1991604233570674`; + +exports[`number > 1337 > exponentialDistribution > with high base 1`] = `0.092022676113987`; + +exports[`number > 1337 > exponentialDistribution > with high bias 1`] = `0.5033501285097729`; + +exports[`number > 1337 > exponentialDistribution > with low base 1`] = `0.5033501285097729`; + +exports[`number > 1337 > exponentialDistribution > with low bias 1`] = `0.092022676113987`; + +exports[`number > 1337 > exponentialDistribution > with max 1`] = `1.991604233570674`; + +exports[`number > 1337 > exponentialDistribution > with min and max 1`] = `27.924438102136065`; + exports[`number > 1337 > float > with max 1`] = `18.079702576075135`; exports[`number > 1337 > float > with min 1`] = `-30.73293897432999`; diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 9af0d58a6f9..9d36268203a 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -65,6 +65,16 @@ describe('number', () => { .it('with max as 3999', { max: 3999 }) .it('with min and max', { min: 100, max: 502 }); }); + + t.describe('exponentialDistribution', (t) => { + t.it('noArgs') + .it('with max', 10) + .it('with low base', { base: 0.1 }) + .it('with high base', { base: 10 }) + .it('with low bias', { bias: -9 }) + .it('with high bias', { bias: 9 }) + .it('with min and max', { min: 10, max: 100 }); + }); }); describe(`random seeded tests for seed ${faker.seed()}`, () => { @@ -697,6 +707,108 @@ describe('number', () => { }).toThrow(new FakerError('Max 100 should be greater than min 500.')); }); }); + + describe('exponentialDistribution', () => { + it('should generate a number between 0 and 1 by default', () => { + const actual = faker.number.exponentialDistribution(); + expect(actual).toBeTypeOf('number'); + expect(actual).toBeGreaterThanOrEqual(0); + expect(actual).toBeLessThan(1); + }); + + it('should generate a number between 0 and 10', () => { + const actual = faker.number.exponentialDistribution(10); + expect(actual).toBeTypeOf('number'); + expect(actual).toBeGreaterThanOrEqual(0); + expect(actual).toBeLessThan(10); + }); + + it('should generate a number between 10 and 100', () => { + const actual = faker.number.exponentialDistribution({ + min: 10, + max: 100, + }); + expect(actual).toBeTypeOf('number'); + expect(actual).toBeGreaterThanOrEqual(10); + expect(actual).toBeLessThan(100); + }); + + it('should generate a number with low base', () => { + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[ + Math.floor( + faker.number.exponentialDistribution({ max: 10, base: 0.1 }) + ) + ]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); + + it('should generate a number with high base', () => { + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[ + Math.floor( + faker.number.exponentialDistribution({ max: 10, base: 10 }) + ) + ]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with low bias', () => { + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[ + Math.floor( + faker.number.exponentialDistribution({ max: 10, bias: -9 }) + ) + ]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with high bias', () => { + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[ + Math.floor( + faker.number.exponentialDistribution({ max: 10, bias: 9 }) + ) + ]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); + }); + + it('should throw when min > max', () => { + const min = 10; + const max = 9; + + expect(() => { + faker.number.exponentialDistribution({ min, max }); + }).toThrow( + new FakerError(`Max ${max} should be greater than min ${min}.`) + ); + }); + + it('should throw when base is less than or equal to 0', () => { + expect(() => { + faker.number.exponentialDistribution({ base: 0 }); + }).toThrow(new FakerError('Base should be greater than 0.')); + expect(() => { + faker.number.exponentialDistribution({ base: -1 }); + }).toThrow(new FakerError('Base should be greater than 0.')); + }); }); describe('value range tests', () => { 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 c13b2811a53..bcac342a246 100644 --- a/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap +++ b/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap @@ -329,6 +329,7 @@ exports[`check docs completeness > all modules and methods are present 1`] = ` [ "bigInt", "binary", + "exponentialDistribution", "float", "hex", "int", From d3236f4a26984c61d36c6a846f1c448c23c3aa91 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 19 Jan 2025 17:03:09 +0100 Subject: [PATCH 02/19] chore: simplify tables --- src/modules/number/index.ts | 114 ++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index aefe4ad1fe7..308d7503c4a 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -539,35 +539,35 @@ export class NumberModule extends SimpleModuleBase { * Generates a random number between `min` and `max` using an exponential distribution. * The lower bound is inclusive, but the upper bound is exclusive. * - * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, base: x }))`: - * - * | Value | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 | - * | :---: | -------: | -------: | -----: | -----: | ------: | - * | 0 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% | - * | 1 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% | - * | 2 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | - * | 3 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% | - * | 4 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% | - * | 5 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% | - * | 6 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% | - * | 7 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% | - * | 8 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% | - * | 9 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% | - * - * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, bias: x }))`: - * - * | Value | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 | - * | :---: | ------: | ------: | -----: | -----: | -----: | - * | 0 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% | - * | 1 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% | - * | 2 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% | - * | 3 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% | - * | 4 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% | - * | 5 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% | - * | 6 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% | - * | 7 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | - * | 8 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% | - * | 9 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% | + * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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 `exponentialDistribution({ 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 number. * @param options.min The minimum value to generate (inclusive). Defaults to `0`. @@ -577,20 +577,20 @@ export class NumberModule extends SimpleModuleBase { * 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. - * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, base: x }))`: - * - * | Value | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 | - * | :---: | -------: | -------: | -----: | -----: | ------: | - * | 0 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% | - * | 1 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% | - * | 2 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | - * | 3 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% | - * | 4 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% | - * | 5 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% | - * | 6 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% | - * | 7 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% | - * | 8 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% | - * | 9 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% | + * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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% | * * Can alternatively be configured using the `bias` option. `base` takes precedence over `bias`. * @param options.bias An alternative way to specify the `base`. Also accepts values below zero. @@ -599,20 +599,20 @@ export class NumberModule extends SimpleModuleBase { * 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. * - * The following table shows the rough distribution of values generated using `Math.floor(exponentialDistribution({ min: 0, max: 10, bias: x }))`: - * - * | Value | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 | - * | :---: | ------: | ------: | -----: | -----: | -----: | - * | 0 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% | - * | 1 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% | - * | 2 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% | - * | 3 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% | - * | 4 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% | - * | 5 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% | - * | 6 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% | - * | 7 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% | - * | 8 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% | - * | 9 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% | + * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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% | * * This option is ignored if `base` is specified. * From 294fa790b7ce8150950da98986f064138df4ede3 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 17 Feb 2025 21:46:10 +0100 Subject: [PATCH 03/19] feat(number): introduce distributor functions --- src/distributors/distributor.ts | 30 +++ src/distributors/exponential.ts | 119 ++++++++++ src/distributors/uniform.ts | 38 ++++ src/index.ts | 3 + src/modules/number/index.ts | 188 ++-------------- .../modules/__snapshots__/number.spec.ts.snap | 42 ---- test/modules/number.spec.ts | 207 ++++++++---------- .../verify-jsdoc-tags.spec.ts.snap | 1 - 8 files changed, 308 insertions(+), 320 deletions(-) create mode 100644 src/distributors/distributor.ts create mode 100644 src/distributors/exponential.ts create mode 100644 src/distributors/uniform.ts diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts new file mode 100644 index 00000000000..c414a8bd7d7 --- /dev/null +++ b/src/distributors/distributor.ts @@ -0,0 +1,30 @@ +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 will values 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 more likely to be close to 0. + * + * @param randomizer The randomizer to use for generating values. + * + * @returns Generates a random float between 0 (inclusive) and 1 (exclusive). + * + * @example + * import { Distributor, Randomizer, simpleFaker } from '@faker-js/faker'; + * + * const alwaysMin: Distributor = () => 0; + * const uniform: Distributor = (randomizer: Randomizer) => randomizer.next(); + * + * simpleFaker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 + * simpleFaker.number.int({ min: 0, max: 10, distributor: uniform }); // 5 + * + * @since 9.6.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..311d59f296f --- /dev/null +++ b/src/distributors/exponential.ts @@ -0,0 +1,119 @@ +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 + * const distributor = exponentialDistributor(); + * distributor(randomizer) // 0.04643770898904198 + * distributor(randomizer) // 0.13436127925491848 + * distributor(randomizer) // 0.4202905589842396 + * distributor(randomizer) // 0.5164955927828387 + * distributor(randomizer) // 0.3476359433171099 + * + * @since 9.6.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; +/** + * Creates a new function that generates exponentially distributed values. + * This function uses `(base ** next() - 1) / (base - 1)` to spread the values. + * + * @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. + */ +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..cfbe4d5d295 --- /dev/null +++ b/src/distributors/uniform.ts @@ -0,0 +1,38 @@ +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 + * const distributor = uniformDistributor(); + * distributor(randomizer) // 0.9100215692561207 + * distributor(randomizer) // 0.791632947887336 + * distributor(randomizer) // 0.14770035310214324 + * distributor(randomizer) // 0.28282249581185814 + * distributor(randomizer) // 0.017890944117802343 + * + * @since 9.6.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 a8c4ecea739..e0748bc1a78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,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 type { FakerOptions } from './faker'; diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index 308d7503c4a..5e04876354a 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'; @@ -63,13 +65,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.`); @@ -98,7 +111,7 @@ export class NumberModule extends SimpleModuleBase { // @ts-expect-error: access private member field const randomizer = this.faker._randomizer; - const real = randomizer.next(); + const real = distributor(randomizer); const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution return Math.floor(real * delta + effectiveMin) * multipleOf; } @@ -154,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') { @@ -168,6 +187,7 @@ export class NumberModule extends SimpleModuleBase { fractionDigits, multipleOf: originalMultipleOf, multipleOf = fractionDigits == null ? undefined : 10 ** -fractionDigits, + distributor = uniformDistributor(), } = options; if (max === min) { @@ -210,13 +230,14 @@ export class NumberModule extends SimpleModuleBase { const int = this.int({ min: min * factor, max: max * factor, + distributor, }); return int / factor; } // @ts-expect-error: access private member field const randomizer = this.faker._randomizer; - const real = randomizer.next(); + const real = distributor(randomizer); return real * (max - min) + min; } @@ -534,165 +555,4 @@ export class NumberModule extends SimpleModuleBase { return result; } - - /** - * Generates a random number between `min` and `max` using an exponential distribution. - * The lower bound is inclusive, but the upper bound is exclusive. - * - * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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 `exponentialDistribution({ 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 number. - * @param options.min The minimum value to generate (inclusive). Defaults to `0`. - * @param options.max The maximum value to generate (exclusive). Defaults to `1`. - * @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 distribution. - * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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% | - * - * Can alternatively be configured using the `bias` option. `base` takes precedence over `bias`. - * @param options.bias 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. - * - * The following table shows the rough distribution of values generated using `exponentialDistribution({ 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% | - * - * This option is ignored if `base` is specified. - * - * Defaults to `-1`. - * - * @throws If `base` is less than or equal to `0`. - * @throws If `max` is less than `min`. - * - * @example - * faker.number.exponentialDistribution() // 0.41928964795957224 - * faker.number.exponentialDistribution(10) // 1.656598169056771 - * faker.number.exponentialDistribution({ min: 10, max: 100 }) // 88.7273250669911 - * faker.number.exponentialDistribution({ min: 0, max: 100, base: 10 }) // 6.9442760672808745 - * faker.number.exponentialDistribution({ min: 0, max: 100, bias: 10 }) // 67.03715679154617 - * - * @since 9.5.0 - */ - exponentialDistribution( - options: - | number - | { - /** - * The minimum value to generate (inclusive). - * - * @default 0 - */ - min?: number; - /** - * The maximum value to generate (exclusive). - * - * @default 1 - */ - max?: number; - /** - * 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 distribution. - * Can alternatively be configured using the `bias` option. `base` takes precedence over `bias`. - * - * @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. - * This option is ignored if `base` is specified. - * - * @default -1 - */ - bias?: number; - } = {} - ): number { - if (typeof options === 'number') { - options = { min: 0, max: options }; - } - - const { - min = 0, - max = 1, - bias = -1, - base = bias <= 0 ? -bias + 1 : 1 / (bias + 1), - } = options; - - if (base === 1) { - return this.faker.number.float({ min, max }); - } else if (base <= 0) { - throw new FakerError('Base should be greater than 0.'); - } - - if (max === min) { - return min; - } else if (max < min) { - throw new FakerError(`Max ${max} should be greater than min ${min}.`); - } - - const exponent = this.faker.number.float(); - const factor = (base ** exponent - 1) / (base - 1); - return min + (max - min) * factor; - } } diff --git a/test/modules/__snapshots__/number.spec.ts.snap b/test/modules/__snapshots__/number.spec.ts.snap index db0e573981b..516abc9ffa8 100644 --- a/test/modules/__snapshots__/number.spec.ts.snap +++ b/test/modules/__snapshots__/number.spec.ts.snap @@ -20,20 +20,6 @@ exports[`number > 42 > binary > with options 1`] = `"100"`; exports[`number > 42 > binary > with value 1`] = `"0"`; -exports[`number > 42 > exponentialDistribution > noArgs 1`] = `0.29642623304954707`; - -exports[`number > 42 > exponentialDistribution > with high base 1`] = `0.15209599448489752`; - -exports[`number > 42 > exponentialDistribution > with high bias 1`] = `0.6420630212280508`; - -exports[`number > 42 > exponentialDistribution > with low base 1`] = `0.6420630212280508`; - -exports[`number > 42 > exponentialDistribution > with low bias 1`] = `0.15209599448489752`; - -exports[`number > 42 > exponentialDistribution > with max 1`] = `2.9642623304954707`; - -exports[`number > 42 > exponentialDistribution > with min and max 1`] = `36.67836097445924`; - exports[`number > 42 > float > with max 1`] = `25.84326820046801`; exports[`number > 42 > float > with min 1`] = `-25.89477488956341`; @@ -98,20 +84,6 @@ exports[`number > 1211 > binary > with options 1`] = `"1010"`; exports[`number > 1211 > binary > with value 1`] = `"1"`; -exports[`number > 1211 > exponentialDistribution > noArgs 1`] = `0.9033226590337899`; - -exports[`number > 1211 > exponentialDistribution > with high base 1`] = `0.8313808279511881`; - -exports[`number > 1211 > exponentialDistribution > with high bias 1`] = `0.9801213540567579`; - -exports[`number > 1211 > exponentialDistribution > with low base 1`] = `0.9801213540567579`; - -exports[`number > 1211 > exponentialDistribution > with low bias 1`] = `0.8313808279511881`; - -exports[`number > 1211 > exponentialDistribution > with max 1`] = `9.0332265903379`; - -exports[`number > 1211 > exponentialDistribution > with min and max 1`] = `91.29903931304109`; - exports[`number > 1211 > float > with max 1`] = `64.06789061927832`; exports[`number > 1211 > float > with min 1`] = `-2.0736333821888806`; @@ -176,20 +148,6 @@ exports[`number > 1337 > binary > with options 1`] = `"10"`; exports[`number > 1337 > binary > with value 1`] = `"0"`; -exports[`number > 1337 > exponentialDistribution > noArgs 1`] = `0.1991604233570674`; - -exports[`number > 1337 > exponentialDistribution > with high base 1`] = `0.092022676113987`; - -exports[`number > 1337 > exponentialDistribution > with high bias 1`] = `0.5033501285097729`; - -exports[`number > 1337 > exponentialDistribution > with low base 1`] = `0.5033501285097729`; - -exports[`number > 1337 > exponentialDistribution > with low bias 1`] = `0.092022676113987`; - -exports[`number > 1337 > exponentialDistribution > with max 1`] = `1.991604233570674`; - -exports[`number > 1337 > exponentialDistribution > with min and max 1`] = `27.924438102136065`; - exports[`number > 1337 > float > with max 1`] = `18.079702576075135`; exports[`number > 1337 > float > with min 1`] = `-30.73293897432999`; diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 9d36268203a..7359a7efcca 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -1,6 +1,11 @@ import { isHexadecimal, isOctal } from 'validator'; import { describe, expect, it, vi } from 'vitest'; -import { FakerError, SimpleFaker, faker } from '../../src'; +import { + FakerError, + SimpleFaker, + exponentialDistributor, + faker, +} from '../../src'; import { seededTests } from '../support/seeded-runs'; import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils'; import { times } from './../support/times'; @@ -65,16 +70,6 @@ describe('number', () => { .it('with max as 3999', { max: 3999 }) .it('with min and max', { min: 100, max: 502 }); }); - - t.describe('exponentialDistribution', (t) => { - t.it('noArgs') - .it('with max', 10) - .it('with low base', { base: 0.1 }) - .it('with high base', { base: 10 }) - .it('with low bias', { bias: -9 }) - .it('with high bias', { bias: 9 }) - .it('with min and max', { min: 10, max: 100 }); - }); }); describe(`random seeded tests for seed ${faker.seed()}`, () => { @@ -274,6 +269,50 @@ describe('number', () => { new FakerError(`No suitable integer value between 2.1 and 2.9 found.`) ); }); + + it('should generate a number with low base', () => { + const distributor = exponentialDistributor({ base: 0.1 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[faker.number.int({ max: 9, distributor })]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); + + it('should generate a number with high base', () => { + const distributor = exponentialDistributor({ base: 10 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[faker.number.int({ max: 9, distributor })]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with low bias', () => { + const distributor = exponentialDistributor({ bias: -9 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[faker.number.int({ max: 9, distributor })]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with high bias', () => { + const distributor = exponentialDistributor({ bias: 9 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[faker.number.int({ max: 9, distributor })]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); }); describe('float', () => { @@ -416,6 +455,50 @@ describe('number', () => { new FakerError(`Max ${max} should be greater than min ${min}.`) ); }); + + it('should generate a number with low base', () => { + const distributor = exponentialDistributor({ base: 0.1 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[Math.floor(faker.number.float({ max: 10, distributor }))]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); + + it('should generate a number with high base', () => { + const distributor = exponentialDistributor({ base: 10 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[Math.floor(faker.number.float({ max: 10, distributor }))]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with low bias', () => { + const distributor = exponentialDistributor({ bias: -9 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[Math.floor(faker.number.float({ max: 10, distributor }))]++; + } + + expect(results[0]).toBeGreaterThan(200); + expect(results[9]).toBeLessThan(75); + }); + + it('should generate a number with high bias', () => { + const distributor = exponentialDistributor({ bias: 9 }); + const results = Array.from({ length: 10 }, (_, i) => i); + for (let i = 0; i < 1000; i++) { + results[Math.floor(faker.number.float({ max: 10, distributor }))]++; + } + + expect(results[0]).toBeLessThan(75); + expect(results[9]).toBeGreaterThan(200); + }); }); describe('binary', () => { @@ -707,108 +790,6 @@ describe('number', () => { }).toThrow(new FakerError('Max 100 should be greater than min 500.')); }); }); - - describe('exponentialDistribution', () => { - it('should generate a number between 0 and 1 by default', () => { - const actual = faker.number.exponentialDistribution(); - expect(actual).toBeTypeOf('number'); - expect(actual).toBeGreaterThanOrEqual(0); - expect(actual).toBeLessThan(1); - }); - - it('should generate a number between 0 and 10', () => { - const actual = faker.number.exponentialDistribution(10); - expect(actual).toBeTypeOf('number'); - expect(actual).toBeGreaterThanOrEqual(0); - expect(actual).toBeLessThan(10); - }); - - it('should generate a number between 10 and 100', () => { - const actual = faker.number.exponentialDistribution({ - min: 10, - max: 100, - }); - expect(actual).toBeTypeOf('number'); - expect(actual).toBeGreaterThanOrEqual(10); - expect(actual).toBeLessThan(100); - }); - - it('should generate a number with low base', () => { - const results = Array.from({ length: 10 }, (_, i) => i); - for (let i = 0; i < 1000; i++) { - results[ - Math.floor( - faker.number.exponentialDistribution({ max: 10, base: 0.1 }) - ) - ]++; - } - - expect(results[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); - }); - - it('should generate a number with high base', () => { - const results = Array.from({ length: 10 }, (_, i) => i); - for (let i = 0; i < 1000; i++) { - results[ - Math.floor( - faker.number.exponentialDistribution({ max: 10, base: 10 }) - ) - ]++; - } - - expect(results[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with low bias', () => { - const results = Array.from({ length: 10 }, (_, i) => i); - for (let i = 0; i < 1000; i++) { - results[ - Math.floor( - faker.number.exponentialDistribution({ max: 10, bias: -9 }) - ) - ]++; - } - - expect(results[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with high bias', () => { - const results = Array.from({ length: 10 }, (_, i) => i); - for (let i = 0; i < 1000; i++) { - results[ - Math.floor( - faker.number.exponentialDistribution({ max: 10, bias: 9 }) - ) - ]++; - } - - expect(results[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); - }); - }); - - it('should throw when min > max', () => { - const min = 10; - const max = 9; - - expect(() => { - faker.number.exponentialDistribution({ min, max }); - }).toThrow( - new FakerError(`Max ${max} should be greater than min ${min}.`) - ); - }); - - it('should throw when base is less than or equal to 0', () => { - expect(() => { - faker.number.exponentialDistribution({ base: 0 }); - }).toThrow(new FakerError('Base should be greater than 0.')); - expect(() => { - faker.number.exponentialDistribution({ base: -1 }); - }).toThrow(new FakerError('Base should be greater than 0.')); - }); }); describe('value range tests', () => { 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 bcac342a246..c13b2811a53 100644 --- a/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap +++ b/test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap @@ -329,7 +329,6 @@ exports[`check docs completeness > all modules and methods are present 1`] = ` [ "bigInt", "binary", - "exponentialDistribution", "float", "hex", "int", From 2d02ceba7703abffc7acea59eaca1a19a4a0a963 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 17 Feb 2025 21:52:15 +0100 Subject: [PATCH 04/19] docs: fix missing param tags --- src/modules/number/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index 5e04876354a..1eaccd05680 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -26,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 When `min` is greater than `max`. * @throws When there are no suitable integers between `min` and `max`. @@ -124,6 +125,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 When `min` is greater than `max`. * @throws When `multipleOf` is negative. From 9e6021026228ced0efa6ef5296e16a937beff49a Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 17 Feb 2025 22:51:18 +0100 Subject: [PATCH 05/19] docs: add distributors api docs --- docs/.vitepress/api-pages.ts | 1 + scripts/apidocs/generate.ts | 2 ++ scripts/apidocs/processing/class.ts | 30 +++++++++++++++++++ scripts/apidocs/processing/method.ts | 11 +++++++ scripts/apidocs/utils/markdown.ts | 15 ++++++++-- src/distributors/distributor.ts | 11 +++++-- src/distributors/exponential.ts | 3 ++ src/distributors/uniform.ts | 3 ++ .../verify-jsdoc-tags.spec.ts.snap | 7 +++++ 9 files changed, 78 insertions(+), 5 deletions(-) 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/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 33fa213e4cc..2b4ea2b80ff 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 '../utils/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') + ?.getTypeAliases()[0], + '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 51879caba64..4631af9d2cd 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/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts index 5b81d7d534e..1c19b51b7f9 100644 --- a/scripts/apidocs/utils/markdown.ts +++ b/scripts/apidocs/utils/markdown.ts @@ -58,6 +58,18 @@ function comparableSanitizedHtml(html: string): string { .replaceAll(' ', ''); } +/** + * Wraps the given code in a 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. * @@ -66,8 +78,7 @@ function comparableSanitizedHtml(html: string): string { * @returns The converted HTML string. */ export function codeToHtml(code: string): string { - const delimiter = '```'; - return mdToHtml(`${delimiter}ts\n${code}\n${delimiter}`); + return mdToHtml(wrapCode(code)); } /** diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index c414a8bd7d7..0432cbadc18 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -12,18 +12,23 @@ import type { Randomizer } from '../randomizer'; * - Normal distributor: Values are more likely to be close to a specific value. * - Exponential distributor: Values are more likely to be close to 0. * + * 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, simpleFaker } from '@faker-js/faker'; + * import { Distributor, Randomizer, faker } from '@faker-js/faker'; * * const alwaysMin: Distributor = () => 0; * const uniform: Distributor = (randomizer: Randomizer) => randomizer.next(); * - * simpleFaker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 - * simpleFaker.number.int({ min: 0, max: 10, distributor: uniform }); // 5 + * faker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2 + * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 5 * * @since 9.6.0 */ diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts index 311d59f296f..4dfd86ec68b 100644 --- a/src/distributors/exponential.ts +++ b/src/distributors/exponential.ts @@ -49,6 +49,9 @@ import { uniformDistributor } from './uniform'; * 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 diff --git a/src/distributors/uniform.ts b/src/distributors/uniform.ts index cfbe4d5d295..e1796c570ef 100644 --- a/src/distributors/uniform.ts +++ b/src/distributors/uniform.ts @@ -22,6 +22,9 @@ import type { Distributor } from './distributor'; * @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 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 07983094b8c..995fb8a92d6 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", [ From 4958bb5dfe0ff130b59c3e93e763935b1b641d4e Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Fri, 9 Jan 2026 11:09:54 +0100 Subject: [PATCH 06/19] docs: minor rephrasing for more natural language --- scripts/apidocs/utils/markdown.ts | 2 +- src/distributors/distributor.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts index 1c19b51b7f9..2dbf1a113b0 100644 --- a/scripts/apidocs/utils/markdown.ts +++ b/scripts/apidocs/utils/markdown.ts @@ -59,7 +59,7 @@ function comparableSanitizedHtml(html: string): string { } /** - * Wraps the given code in a code block. + * Wraps the given code in markdown a code block. * * @param code The code to wrap. * diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index 0432cbadc18..9bd58e417d1 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -4,7 +4,7 @@ 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 will values resemble a limited exponential distribution. + * So an exponential distributor's values will resemble a limited exponential distribution. * * Common examples of distributor functions are: * @@ -25,10 +25,14 @@ import type { Randomizer } from '../randomizer'; * import { Distributor, Randomizer, faker } from '@faker-js/faker'; * * const alwaysMin: Distributor = () => 0; - * const uniform: Distributor = (randomizer: Randomizer) => randomizer.next(); - * * 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 9.6.0 */ From b71f3a94ed2d9548fdfa25a644ab63f5af859c20 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Fri, 9 Jan 2026 11:12:05 +0100 Subject: [PATCH 07/19] docs: update @since tags to 10.3.0 --- src/distributors/distributor.ts | 2 +- src/distributors/exponential.ts | 2 +- src/distributors/uniform.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index 9bd58e417d1..58636f33fce 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -34,6 +34,6 @@ import type { Randomizer } from '../randomizer'; * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 2 * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 9 * - * @since 9.6.0 + * @since 10.3.0 */ export type Distributor = (randomizer: Randomizer) => number; diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts index 4dfd86ec68b..ac6064f73da 100644 --- a/src/distributors/exponential.ts +++ b/src/distributors/exponential.ts @@ -59,7 +59,7 @@ import { uniformDistributor } from './uniform'; * distributor(randomizer) // 0.5164955927828387 * distributor(randomizer) // 0.3476359433171099 * - * @since 9.6.0 + * @since 10.3.0 */ export function exponentialDistributor( options?: diff --git a/src/distributors/uniform.ts b/src/distributors/uniform.ts index e1796c570ef..57c8361cc52 100644 --- a/src/distributors/uniform.ts +++ b/src/distributors/uniform.ts @@ -32,7 +32,7 @@ import type { Distributor } from './distributor'; * distributor(randomizer) // 0.28282249581185814 * distributor(randomizer) // 0.017890944117802343 * - * @since 9.6.0 + * @since 10.3.0 */ export function uniformDistributor(): Distributor { return UNIFORM_DISTRIBUTOR; From 1c1fd446851e39c2480c928b100d27ece77baf39 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Thu, 26 Feb 2026 21:35:10 +0100 Subject: [PATCH 08/19] docs: fix typo in jsdoc --- scripts/apidocs/utils/markdown.ts | 131 ++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 scripts/apidocs/utils/markdown.ts diff --git a/scripts/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts new file mode 100644 index 00000000000..677b022286f --- /dev/null +++ b/scripts/apidocs/utils/markdown.ts @@ -0,0 +1,131 @@ +import sanitizeHtml from 'sanitize-html'; +import type { MarkdownRenderer } from 'vitepress'; +import { createMarkdownRenderer } from 'vitepress'; +import vitepressConfig from '../../../docs/.vitepress/config'; +import { FILE_PATH_API_DOCS } from './paths'; + +let markdown: MarkdownRenderer; + +export async function initMarkdownRenderer(): Promise { + markdown = await createMarkdownRenderer( + FILE_PATH_API_DOCS, + vitepressConfig.markdown, + '/' + ); +} + +const htmlSanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: [ + 'a', + 'button', + 'code', + 'div', + 'li', + 'p', + 'pre', + 'span', + 'strong', + 'ul', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + ], + allowedAttributes: { + a: ['href', 'target', 'rel'], + button: ['class', 'title'], + div: ['class'], + pre: ['class', 'v-pre', 'tabindex'], + span: ['class', 'style'], + table: ['tabindex'], + th: ['style'], + td: ['style'], + }, + selfClosing: [], +}; + +function comparableSanitizedHtml(html: string): string { + return html + .replaceAll(/&#x[0-9A-F]{2};/g, (x) => + String.fromCodePoint(Number.parseInt(x.slice(3, -1), 16)) + ) + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('&', '&') + .replaceAll('=""', '') + .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. + * + * @param code The code to convert. + * + * @returns The converted HTML string. + */ +export function codeToHtml(code: string): string { + return mdToHtml(wrapCode(code)); +} + +/** + * Converts Markdown to an HTML string and sanitizes it. + * + * @param md The markdown to convert. + * @param inline Whether to render the markdown as inline, without a wrapping `

` tag. Defaults to `false`. + * + * @returns The converted HTML string. + */ +export function mdToHtml(md: string, inline?: boolean): string; +/** + * Converts Markdown to an HTML string and sanitizes it. + * + * @param md The markdown to convert. + * @param inline Whether to render the markdown as inline, without a wrapping `

` tag. Defaults to `false`. + * + * @returns The converted HTML string. + */ +export function mdToHtml( + md: string | undefined, + inline?: boolean +): string | undefined; +export function mdToHtml( + md: string | undefined, + inline: boolean = false +): string | undefined { + if (md == null) { + return undefined; + } + + const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md); + + const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); + // Revert some escaped characters for comparison. + if (comparableSanitizedHtml(rawHtml) === comparableSanitizedHtml(safeHtml)) { + return adjustUrls(safeHtml); + } + + console.debug('Rejected unsafe md:\n', md); + console.error('Rejected unsafe html:\n', rawHtml); + console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml)); + console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml)); + console.log('-'.repeat(80)); + throw new Error('Found unsafe html'); +} + +export function adjustUrls(description: string): string { + return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/'); +} From 127f1b6f9611b5916b9fcd73f74bc04aac3c61f3 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Thu, 26 Feb 2026 21:36:14 +0100 Subject: [PATCH 09/19] docs: update since tags to 10.4.0 --- src/distributors/distributor.ts | 2 +- src/distributors/exponential.ts | 2 +- src/distributors/uniform.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index 58636f33fce..ee99b856e8f 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -34,6 +34,6 @@ import type { Randomizer } from '../randomizer'; * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 2 * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 9 * - * @since 10.3.0 + * @since 10.4.0 */ export type Distributor = (randomizer: Randomizer) => number; diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts index ac6064f73da..2bb905b40cf 100644 --- a/src/distributors/exponential.ts +++ b/src/distributors/exponential.ts @@ -59,7 +59,7 @@ import { uniformDistributor } from './uniform'; * distributor(randomizer) // 0.5164955927828387 * distributor(randomizer) // 0.3476359433171099 * - * @since 10.3.0 + * @since 10.4.0 */ export function exponentialDistributor( options?: diff --git a/src/distributors/uniform.ts b/src/distributors/uniform.ts index 57c8361cc52..73f4a04209c 100644 --- a/src/distributors/uniform.ts +++ b/src/distributors/uniform.ts @@ -32,7 +32,7 @@ import type { Distributor } from './distributor'; * distributor(randomizer) // 0.28282249581185814 * distributor(randomizer) // 0.017890944117802343 * - * @since 10.3.0 + * @since 10.4.0 */ export function uniformDistributor(): Distributor { return UNIFORM_DISTRIBUTOR; From 9e7c8469e08f6339e5e0ff05584bee321dab5370 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Thu, 26 Feb 2026 21:45:53 +0100 Subject: [PATCH 10/19] chore: remove duplicated markdown api doc file since it has been moved --- scripts/apidocs/processing/class.ts | 2 +- scripts/apidocs/utils/markdown.ts | 131 ---------------------------- scripts/shared/markdown.ts | 2 +- 3 files changed, 2 insertions(+), 133 deletions(-) delete mode 100644 scripts/apidocs/utils/markdown.ts diff --git a/scripts/apidocs/processing/class.ts b/scripts/apidocs/processing/class.ts index 273fe8758d5..fe2d89c2d14 100644 --- a/scripts/apidocs/processing/class.ts +++ b/scripts/apidocs/processing/class.ts @@ -1,5 +1,5 @@ import type { ClassDeclaration, InterfaceDeclaration, Project } from 'ts-morph'; -import { wrapCode } from '../utils/markdown'; +import { wrapCode } from '../../shared/markdown'; import { required, valuesForKeys } from '../utils/value-checks'; import { newProcessingError } from './error'; import type { JSDocableLikeNode } from './jsdocs'; diff --git a/scripts/apidocs/utils/markdown.ts b/scripts/apidocs/utils/markdown.ts deleted file mode 100644 index 677b022286f..00000000000 --- a/scripts/apidocs/utils/markdown.ts +++ /dev/null @@ -1,131 +0,0 @@ -import sanitizeHtml from 'sanitize-html'; -import type { MarkdownRenderer } from 'vitepress'; -import { createMarkdownRenderer } from 'vitepress'; -import vitepressConfig from '../../../docs/.vitepress/config'; -import { FILE_PATH_API_DOCS } from './paths'; - -let markdown: MarkdownRenderer; - -export async function initMarkdownRenderer(): Promise { - markdown = await createMarkdownRenderer( - FILE_PATH_API_DOCS, - vitepressConfig.markdown, - '/' - ); -} - -const htmlSanitizeOptions: sanitizeHtml.IOptions = { - allowedTags: [ - 'a', - 'button', - 'code', - 'div', - 'li', - 'p', - 'pre', - 'span', - 'strong', - 'ul', - 'table', - 'thead', - 'tbody', - 'tr', - 'th', - 'td', - ], - allowedAttributes: { - a: ['href', 'target', 'rel'], - button: ['class', 'title'], - div: ['class'], - pre: ['class', 'v-pre', 'tabindex'], - span: ['class', 'style'], - table: ['tabindex'], - th: ['style'], - td: ['style'], - }, - selfClosing: [], -}; - -function comparableSanitizedHtml(html: string): string { - return html - .replaceAll(/&#x[0-9A-F]{2};/g, (x) => - String.fromCodePoint(Number.parseInt(x.slice(3, -1), 16)) - ) - .replaceAll('>', '>') - .replaceAll('<', '<') - .replaceAll('&', '&') - .replaceAll('=""', '') - .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. - * - * @param code The code to convert. - * - * @returns The converted HTML string. - */ -export function codeToHtml(code: string): string { - return mdToHtml(wrapCode(code)); -} - -/** - * Converts Markdown to an HTML string and sanitizes it. - * - * @param md The markdown to convert. - * @param inline Whether to render the markdown as inline, without a wrapping `

` tag. Defaults to `false`. - * - * @returns The converted HTML string. - */ -export function mdToHtml(md: string, inline?: boolean): string; -/** - * Converts Markdown to an HTML string and sanitizes it. - * - * @param md The markdown to convert. - * @param inline Whether to render the markdown as inline, without a wrapping `

` tag. Defaults to `false`. - * - * @returns The converted HTML string. - */ -export function mdToHtml( - md: string | undefined, - inline?: boolean -): string | undefined; -export function mdToHtml( - md: string | undefined, - inline: boolean = false -): string | undefined { - if (md == null) { - return undefined; - } - - const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md); - - const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); - // Revert some escaped characters for comparison. - if (comparableSanitizedHtml(rawHtml) === comparableSanitizedHtml(safeHtml)) { - return adjustUrls(safeHtml); - } - - console.debug('Rejected unsafe md:\n', md); - console.error('Rejected unsafe html:\n', rawHtml); - console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml)); - console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml)); - console.log('-'.repeat(80)); - throw new Error('Found unsafe html'); -} - -export function adjustUrls(description: string): string { - return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/'); -} diff --git a/scripts/shared/markdown.ts b/scripts/shared/markdown.ts index 1d991d52631..7d4b6733dc3 100644 --- a/scripts/shared/markdown.ts +++ b/scripts/shared/markdown.ts @@ -59,7 +59,7 @@ function comparableSanitizedHtml(html: string): string { } /** - * Wraps the given code in markdown a code block. + * Wraps the given code in a markdown code block. * * @param code The code to wrap. * From dc40f791b48ba1817f2275a5da2ab23e99620374 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 12:49:54 +0200 Subject: [PATCH 11/19] chore: fix merge issue --- scripts/shared/markdown.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/shared/markdown.ts b/scripts/shared/markdown.ts index da6b808a612..e2f4ccab18f 100644 --- a/scripts/shared/markdown.ts +++ b/scripts/shared/markdown.ts @@ -78,8 +78,7 @@ export function wrapCode(code: 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)); } /** From f00cc78c06c0edacad59bd8f31468dd43deb8248 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 12:57:28 +0200 Subject: [PATCH 12/19] chore: bump since --- src/distributors/distributor.ts | 2 +- src/distributors/exponential.ts | 2 +- src/distributors/uniform.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index ee99b856e8f..3698b683c29 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -34,6 +34,6 @@ import type { Randomizer } from '../randomizer'; * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 2 * faker.number.int({ min: 0, max: 10, distributor: uniform }); // 9 * - * @since 10.4.0 + * @since 10.5.0 */ export type Distributor = (randomizer: Randomizer) => number; diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts index 2bb905b40cf..ccc60f081c2 100644 --- a/src/distributors/exponential.ts +++ b/src/distributors/exponential.ts @@ -59,7 +59,7 @@ import { uniformDistributor } from './uniform'; * distributor(randomizer) // 0.5164955927828387 * distributor(randomizer) // 0.3476359433171099 * - * @since 10.4.0 + * @since 10.5.0 */ export function exponentialDistributor( options?: diff --git a/src/distributors/uniform.ts b/src/distributors/uniform.ts index 73f4a04209c..a6e20faabe4 100644 --- a/src/distributors/uniform.ts +++ b/src/distributors/uniform.ts @@ -32,7 +32,7 @@ import type { Distributor } from './distributor'; * distributor(randomizer) // 0.28282249581185814 * distributor(randomizer) // 0.017890944117802343 * - * @since 10.4.0 + * @since 10.5.0 */ export function uniformDistributor(): Distributor { return UNIFORM_DISTRIBUTOR; From bc50745ea395fd9f4c6f95a898a1ab6d74502b94 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 13:18:18 +0200 Subject: [PATCH 13/19] docs: refreshable distributor examples --- .../components/api-docs/refreshable-code.vue | 3 ++- scripts/shared/refreshable-code.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/components/api-docs/refreshable-code.vue b/docs/.vitepress/components/api-docs/refreshable-code.vue index e9ebac1748f..71f8ac7776a 100644 --- a/docs/.vitepress/components/api-docs/refreshable-code.vue +++ b/docs/.vitepress/components/api-docs/refreshable-code.vue @@ -28,7 +28,8 @@ function initRefresh(): Element[] { // Skip empty and preparatory lines (no '^faker.' invocation) if ( domLines[lineIndex]?.children.length === 0 || - !/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '') + (!/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '') && + !/^distributor\(/.test(domLines[lineIndex]?.textContent ?? '')) ) { lineIndex++; continue; diff --git a/scripts/shared/refreshable-code.ts b/scripts/shared/refreshable-code.ts index ba7e02bab63..b43aaf45bd2 100644 --- a/scripts/shared/refreshable-code.ts +++ b/scripts/shared/refreshable-code.ts @@ -4,7 +4,10 @@ export async function toRefreshableCode( name: string, exampleCode: string ): Promise { - if (!/^\w*faker\w*\./im.test(exampleCode)) { + if ( + !/^\w*faker\w*\./im.test(exampleCode) && + !/^distributor\(/im.test(exampleCode) + ) { // No recordable faker calls in examples return 'undefined'; } @@ -16,6 +19,11 @@ export async function toRefreshableCode( // record results of faker calls /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim, `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` + ) + .replaceAll( + // record distributor calls + /^(distributor\(.+\));?$/gim, + `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` ); const fullMethod = `async (): Promise => { From b66c3e0f43590a164f97148603012560c1e60681 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 13:54:50 +0200 Subject: [PATCH 14/19] chore: merge code paths --- .../components/api-docs/refreshable-code.vue | 6 +++--- scripts/shared/refreshable-code.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/.vitepress/components/api-docs/refreshable-code.vue b/docs/.vitepress/components/api-docs/refreshable-code.vue index 71f8ac7776a..f10674311e2 100644 --- a/docs/.vitepress/components/api-docs/refreshable-code.vue +++ b/docs/.vitepress/components/api-docs/refreshable-code.vue @@ -25,11 +25,11 @@ 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 ?? '') && - !/^distributor\(/.test(domLines[lineIndex]?.textContent ?? '')) + !/^\w*faker\w*\.|^distributor\(/i.test(domLines[lineIndex]?.textContent ?? '') ) { lineIndex++; continue; diff --git a/scripts/shared/refreshable-code.ts b/scripts/shared/refreshable-code.ts index b43aaf45bd2..aa9f94181c1 100644 --- a/scripts/shared/refreshable-code.ts +++ b/scripts/shared/refreshable-code.ts @@ -8,7 +8,7 @@ export async function toRefreshableCode( !/^\w*faker\w*\./im.test(exampleCode) && !/^distributor\(/im.test(exampleCode) ) { - // No recordable faker calls in examples + // No recordable calls in examples return 'undefined'; } @@ -16,13 +16,9 @@ export async function toRefreshableCode( .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments .replaceAll(/^import .*$/gm, '') // Remove imports .replaceAll( - // record results of faker calls - /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim, - `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` - ) - .replaceAll( - // record distributor calls - /^(distributor\(.+\));?$/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` ); From 5d3df1fcaf89429fc8d365658053714151de87f3 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 14:02:19 +0200 Subject: [PATCH 15/19] chore: simplify --- .../components/api-docs/refreshable-code.vue | 4 +++- scripts/shared/refreshable-code.ts | 13 +++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/.vitepress/components/api-docs/refreshable-code.vue b/docs/.vitepress/components/api-docs/refreshable-code.vue index f10674311e2..23cfcc87fd7 100644 --- a/docs/.vitepress/components/api-docs/refreshable-code.vue +++ b/docs/.vitepress/components/api-docs/refreshable-code.vue @@ -29,7 +29,9 @@ function initRefresh(): Element[] { // Keep in sync with ref scripts/shared/refreshable-code.ts if ( domLines[lineIndex]?.children.length === 0 || - !/^\w*faker\w*\.|^distributor\(/i.test(domLines[lineIndex]?.textContent ?? '') + !/^\w*faker\w*\.|^distributor\(/i.test( + domLines[lineIndex]?.textContent ?? '' + ) ) { lineIndex++; continue; diff --git a/scripts/shared/refreshable-code.ts b/scripts/shared/refreshable-code.ts index aa9f94181c1..6406a7f3dcb 100644 --- a/scripts/shared/refreshable-code.ts +++ b/scripts/shared/refreshable-code.ts @@ -4,14 +4,6 @@ export async function toRefreshableCode( name: string, exampleCode: string ): Promise { - if ( - !/^\w*faker\w*\./im.test(exampleCode) && - !/^distributor\(/im.test(exampleCode) - ) { - // No recordable calls in examples - return 'undefined'; - } - const exampleLines = exampleCode .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments .replaceAll(/^import .*$/gm, '') // Remove imports @@ -22,6 +14,11 @@ export async function toRefreshableCode( `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[] = []; From 6c544ec177df06c5ede63cf2df81f3f9236f0551 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 5 Apr 2026 14:40:53 +0200 Subject: [PATCH 16/19] chore: edit after review --- scripts/apidocs/processing/class.ts | 2 +- src/distributors/distributor.ts | 2 +- test/modules/number.spec.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/apidocs/processing/class.ts b/scripts/apidocs/processing/class.ts index fe2d89c2d14..8171e191d8a 100644 --- a/scripts/apidocs/processing/class.ts +++ b/scripts/apidocs/processing/class.ts @@ -206,7 +206,7 @@ export function processProjectDistributors(project: Project): RawApiDocsPage { const distributor = required( project .getSourceFile('src/distributors/distributor.ts') - ?.getTypeAliases()[0], + ?.getTypeAlias('Distributor'), 'Distributor' ); diff --git a/src/distributors/distributor.ts b/src/distributors/distributor.ts index 3698b683c29..7061e8f3540 100644 --- a/src/distributors/distributor.ts +++ b/src/distributors/distributor.ts @@ -10,7 +10,7 @@ import type { Randomizer } from '../randomizer'; * * - 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 more likely to be close to 0. + * - 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()`. * diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index f026da459f0..b40e28774a1 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -285,7 +285,7 @@ describe('number', () => { it('should generate a number with low base', () => { const distributor = exponentialDistributor({ base: 0.1 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[faker.number.int({ max: 9, distributor })]++; } @@ -296,7 +296,7 @@ describe('number', () => { it('should generate a number with high base', () => { const distributor = exponentialDistributor({ base: 10 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[faker.number.int({ max: 9, distributor })]++; } @@ -307,7 +307,7 @@ describe('number', () => { it('should generate a number with low bias', () => { const distributor = exponentialDistributor({ bias: -9 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[faker.number.int({ max: 9, distributor })]++; } @@ -318,7 +318,7 @@ describe('number', () => { it('should generate a number with high bias', () => { const distributor = exponentialDistributor({ bias: 9 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[faker.number.int({ max: 9, distributor })]++; } @@ -497,7 +497,7 @@ describe('number', () => { it('should generate a number with low base', () => { const distributor = exponentialDistributor({ base: 0.1 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[Math.floor(faker.number.float({ max: 10, distributor }))]++; } @@ -508,7 +508,7 @@ describe('number', () => { it('should generate a number with high base', () => { const distributor = exponentialDistributor({ base: 10 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[Math.floor(faker.number.float({ max: 10, distributor }))]++; } @@ -519,7 +519,7 @@ describe('number', () => { it('should generate a number with low bias', () => { const distributor = exponentialDistributor({ bias: -9 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[Math.floor(faker.number.float({ max: 10, distributor }))]++; } @@ -530,7 +530,7 @@ describe('number', () => { it('should generate a number with high bias', () => { const distributor = exponentialDistributor({ bias: 9 }); - const results = Array.from({ length: 10 }, (_, i) => i); + const results = Array.from({ length: 10 }, () => 0); for (let i = 0; i < 1000; i++) { results[Math.floor(faker.number.float({ max: 10, distributor }))]++; } From 53824361896731d33eeb1d810d7fdc5feb5ea151 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Wed, 15 Apr 2026 23:32:02 +0200 Subject: [PATCH 17/19] chore: remove redundant jsdocs --- src/distributors/exponential.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/distributors/exponential.ts b/src/distributors/exponential.ts index ccc60f081c2..7b1a3857c46 100644 --- a/src/distributors/exponential.ts +++ b/src/distributors/exponential.ts @@ -88,22 +88,6 @@ export function exponentialDistributor( bias?: number; } ): Distributor; -/** - * Creates a new function that generates exponentially distributed values. - * This function uses `(base ** next() - 1) / (base - 1)` to spread the values. - * - * @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. - */ export function exponentialDistributor( options: { base?: number; From b6d164d514b9cb0ab74420c6e03991c6845c9d41 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 16 Apr 2026 00:14:16 +0200 Subject: [PATCH 18/19] chore: add tests --- test/distributors/exponential.spec.ts | 154 ++++++++++++++++++++++++++ test/distributors/uniform.spec.ts | 24 ++++ 2 files changed, 178 insertions(+) create mode 100644 test/distributors/exponential.spec.ts create mode 100644 test/distributors/uniform.spec.ts 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); + } + }); +}); From 2311597331c45e8c77b4fe58b830212c4497536c Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Fri, 17 Apr 2026 19:15:00 +0200 Subject: [PATCH 19/19] test: simplify number module tests --- test/modules/number.spec.ts | 89 ++++--------------------------------- 1 file changed, 9 insertions(+), 80 deletions(-) diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 106a77eb479..8ac8f572cb9 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -1,11 +1,6 @@ import { isHexadecimal, isOctal } from 'validator'; import { describe, expect, it, vi } from 'vitest'; -import { - FakerError, - SimpleFaker, - exponentialDistributor, - faker, -} from '../../src'; +import { FakerError, SimpleFaker, faker } from '../../src'; import { seededTests } from '../support/seeded-runs'; import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils'; import { times } from './../support/times'; @@ -283,48 +278,15 @@ describe('number', () => { ); }); - it('should generate a number with low base', () => { - const distributor = exponentialDistributor({ base: 0.1 }); + 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[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); - }); - - it('should generate a number with high base', () => { - const distributor = exponentialDistributor({ base: 10 }); - const results = Array.from({ length: 10 }, () => 0); - for (let i = 0; i < 1000; i++) { - results[faker.number.int({ max: 9, distributor })]++; - } - - expect(results[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with low bias', () => { - const distributor = exponentialDistributor({ bias: -9 }); - const results = Array.from({ length: 10 }, () => 0); - for (let i = 0; i < 1000; i++) { - results[faker.number.int({ max: 9, distributor })]++; - } - - expect(results[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with high bias', () => { - const distributor = exponentialDistributor({ bias: 9 }); - const results = Array.from({ length: 10 }, () => 0); - for (let i = 0; i < 1000; i++) { - results[faker.number.int({ max: 9, distributor })]++; - } - - expect(results[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); + expect(results).toEqual([750, 0, 0, 0, 0, 0, 0, 0, 0, 250]); }); }); @@ -495,48 +457,15 @@ describe('number', () => { ); }); - it('should generate a number with low base', () => { - const distributor = exponentialDistributor({ base: 0.1 }); - 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[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); - }); - - it('should generate a number with high base', () => { - const distributor = exponentialDistributor({ base: 10 }); - 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[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with low bias', () => { - const distributor = exponentialDistributor({ bias: -9 }); - 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[0]).toBeGreaterThan(200); - expect(results[9]).toBeLessThan(75); - }); - - it('should generate a number with high bias', () => { - const distributor = exponentialDistributor({ bias: 9 }); + 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[0]).toBeLessThan(75); - expect(results[9]).toBeGreaterThan(200); + expect(results).toEqual([750, 0, 0, 0, 0, 0, 0, 0, 0, 250]); }); });