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",