Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(number): add distributor functions #3375

Open
wants to merge 7 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/api-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions scripts/apidocs/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { RawApiDocsPage } from './processing/class';
import {
processModuleClasses,
processProjectClasses,
processProjectDistributors,
processProjectInterfaces,
processProjectUtilities,
} from './processing/class';
Expand All @@ -26,6 +27,7 @@ export function processComponents(project: Project): RawApiDocsPage[] {
return [
...processProjectClasses(project),
...processProjectInterfaces(project),
processProjectDistributors(project),
processProjectUtilities(project),
...processModuleClasses(project),
];
Expand Down
30 changes: 30 additions & 0 deletions scripts/apidocs/processing/class.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +13,7 @@ import type { RawApiDocsMethod } from './method';
import {
processClassConstructors,
processClassMethods,
processDistributorFunctions,
processInterfaceMethods,
processUtilityFunctions,
} from './method';
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions scripts/apidocs/processing/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
24 changes: 22 additions & 2 deletions scripts/apidocs/utils/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,22 @@ const htmlSanitizeOptions: sanitizeHtml.IOptions = {
'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: [],
};
Expand All @@ -49,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.
*
Expand All @@ -57,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));
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/distributors/distributor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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.
*
* Distributor functions can be used by some faker functions such as `faker.number.int()` and `faker.number.float()`.
*
* Please note that the result from the distributor function is processed further by the function accepting it.
* E.g. a distributor result of `0.5` within a call to `faker.number.int({ min: 10, max: 20 })` will result in `15`.
*
* @param randomizer The randomizer to use for generating values.
*
* @returns Generates a random float between 0 (inclusive) and 1 (exclusive).
*
* @example
* import { Distributor, Randomizer, faker } from '@faker-js/faker';
*
* const alwaysMin: Distributor = () => 0;
* const uniform: Distributor = (randomizer: Randomizer) => randomizer.next();
*
* faker.number.int({ min: 2, max: 10, distributor: alwaysMin }); // 2
* faker.number.int({ min: 0, max: 10, distributor: uniform }); // 5
*
* @since 9.6.0
*/
export type Distributor = (randomizer: Randomizer) => number;
122 changes: 122 additions & 0 deletions src/distributors/exponential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { FakerError } from '../errors/faker-error';
import type { Distributor } from './distributor';
import { uniformDistributor } from './uniform';

/**
* Creates a new function that generates power-law/exponentially distributed values.
* This function uses `(base ** next() - 1) / (base - 1)` to spread the values.
*
* The following table shows the rough distribution of values generated using `exponentialDistributor({ base: x })`:
*
* | Result | Base 0.1 | Base 0.5 | Base 1 | Base 2 | Base 10 |
* | :-------: | -------: | -------: | -----: | -----: | ------: |
* | 0.0 - 0.1 | 4.1% | 7.4% | 10.0% | 13.8% | 27.8% |
* | 0.1 - 0.2 | 4.5% | 7.8% | 10.0% | 12.5% | 16.9% |
* | 0.2 - 0.3 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% |
* | 0.3 - 0.4 | 5.7% | 8.7% | 10.0% | 10.7% | 9.4% |
* | 0.4 - 0.5 | 6.6% | 9.3% | 10.0% | 10.0% | 7.8% |
* | 0.5 - 0.6 | 7.8% | 9.9% | 10.0% | 9.3% | 6.6% |
* | 0.6 - 0.7 | 9.4% | 10.7% | 10.0% | 8.8% | 5.7% |
* | 0.7 - 0.8 | 12.1% | 11.5% | 10.0% | 8.2% | 5.0% |
* | 0.8 - 0.9 | 16.9% | 12.6% | 10.0% | 7.8% | 4.5% |
* | 0.9 - 1.0 | 27.9% | 13.8% | 10.0% | 7.5% | 4.1% |
*
* The following table shows the rough distribution of values generated using `exponentialDistributor({ bias: x })`:
*
* | Result | Bias -9 | Bias -1 | Bias 0 | Bias 1 | Bias 9 |
* | :-------: | ------: | ------: | -----: | -----: | -----: |
* | 0.0 - 0.1 | 27.9% | 13.7% | 10.0% | 7.4% | 4.1% |
* | 0.1 - 0.2 | 16.9% | 12.5% | 10.0% | 7.8% | 4.5% |
* | 0.2 - 0.3 | 12.1% | 11.6% | 10.0% | 8.3% | 5.1% |
* | 0.3 - 0.4 | 9.5% | 10.7% | 10.0% | 8.8% | 5.7% |
* | 0.4 - 0.5 | 7.8% | 10.0% | 10.0% | 9.3% | 6.6% |
* | 0.5 - 0.6 | 6.6% | 9.3% | 10.0% | 9.9% | 7.7% |
* | 0.6 - 0.7 | 5.7% | 8.8% | 10.0% | 10.7% | 9.5% |
* | 0.7 - 0.8 | 5.0% | 8.2% | 10.0% | 11.5% | 12.1% |
* | 0.8 - 0.9 | 4.5% | 7.8% | 10.0% | 12.6% | 16.8% |
* | 0.9 - 1.0 | 4.1% | 7.4% | 10.0% | 13.7% | 27.9% |
*
* @param options The options for generating the distributor.
* @param options.base The base of the exponential distribution. Should be greater than 0. Defaults to `2`.
* The higher/more above `1` the `base`, the more likely the number will be closer to the minimum value.
* The lower/closer to zero the `base`, the more likely the number will be closer to the maximum value.
* Values of `1` will generate a uniform distributor.
* Can alternatively be configured using the `bias` option.
* @param options.bias An alternative way to specify the `base`. Also accepts values below zero. Defaults to `-1`.
* The higher/more positive the `bias`, the more likely the number will be closer to the maximum value.
* The lower/more negative the `bias`, the more likely the number will be closer to the minimum value.
* Values of `0` will generate a uniform distributor.
* Can alternatively be configured using the `base` option.
*
* @example
* import { exponentialDistributor, generateMersenne53Randomizer } from '@faker-js/faker';
*
* const randomizer = generateMersenne53Randomizer();
* const distributor = exponentialDistributor();
* distributor(randomizer) // 0.04643770898904198
* distributor(randomizer) // 0.13436127925491848
* distributor(randomizer) // 0.4202905589842396
* distributor(randomizer) // 0.5164955927828387
* distributor(randomizer) // 0.3476359433171099
*
* @since 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();

Check warning on line 116 in src/distributors/exponential.ts

View check run for this annotation

Codecov / codecov/patch

src/distributors/exponential.ts#L116

Added line #L116 was not covered by tests
} else if (base <= 0) {
throw new FakerError('Base should be greater than 0.');
}

Check warning on line 119 in src/distributors/exponential.ts

View check run for this annotation

Codecov / codecov/patch

src/distributors/exponential.ts#L118-L119

Added lines #L118 - L119 were not covered by tests

return ({ next }) => (base ** next() - 1) / (base - 1);
}
41 changes: 41 additions & 0 deletions src/distributors/uniform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Distributor } from './distributor';

/**
* Creates a new function that generates uniformly distributed values.
* The likelihood of each value is the same.
*
* The following table shows the rough distribution of values generated using `uniformDistributor()`:
*
* | Result | Uniform |
* | :-------: | ------: |
* | 0.0 - 0.1 | 10.0% |
* | 0.1 - 0.2 | 10.0% |
* | 0.2 - 0.3 | 10.0% |
* | 0.3 - 0.4 | 10.0% |
* | 0.4 - 0.5 | 10.0% |
* | 0.5 - 0.6 | 10.0% |
* | 0.6 - 0.7 | 10.0% |
* | 0.7 - 0.8 | 10.0% |
* | 0.8 - 0.9 | 10.0% |
* | 0.9 - 1.0 | 10.0% |
*
* @returns A new uniform distributor function.
*
* @example
* import { generateMersenne53Randomizer, uniformDistributor } from '@faker-js/faker';
*
* const randomizer = generateMersenne53Randomizer();
* const distributor = uniformDistributor();
* distributor(randomizer) // 0.9100215692561207
* distributor(randomizer) // 0.791632947887336
* distributor(randomizer) // 0.14770035310214324
* distributor(randomizer) // 0.28282249581185814
* distributor(randomizer) // 0.017890944117802343
*
* @since 9.6.0
*/
export function uniformDistributor(): Distributor {
return UNIFORM_DISTRIBUTOR;
}

const UNIFORM_DISTRIBUTOR: Distributor = ({ next }) => next();
Comment on lines +37 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought quite long about it. What exactly was your reasoning to extract the uniform function implementation into it's own function expression? The only benifit I was able to see, would be function reference comparison:

const a = uniformDistributor();
const b = uniformDistributor();
a === b // true

Was that your reasoning? If yes, is this really desired? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is exposed as fn because the other distributions are as well.

The value is defined as a const because it is used as default for the number functions. This might be related to my usual backend development habits.

The a===b part is unintentional.

3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading