From ec25afbba3c03e0269e4f3b19e3c6e719039cc1e Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 23 May 2026 16:32:54 +0200 Subject: [PATCH 1/2] infra: add scripts to run transformation --- scripts/generate-module-tree.ts | 215 ++++++++++++++++++++++ scripts/shared/character-case.ts | 19 ++ scripts/shared/paths.ts | 2 +- scripts/temp-module-filter.ts | 6 + scripts/temp-tranform-once.ts | 299 +++++++++++++++++++++++++++++++ 5 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-module-tree.ts create mode 100644 scripts/shared/character-case.ts create mode 100644 scripts/temp-module-filter.ts create mode 100644 scripts/temp-tranform-once.ts diff --git a/scripts/generate-module-tree.ts b/scripts/generate-module-tree.ts new file mode 100644 index 00000000000..e02a1c9c3da --- /dev/null +++ b/scripts/generate-module-tree.ts @@ -0,0 +1,215 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { SyntaxKind } from 'ts-morph'; +import { getDeprecated, getJsDocs } from './apidocs/processing/jsdocs'; +import { getProject } from './apidocs/project'; +import { + toCamelCase, + toKebabCase, + toPascalCase, +} from './shared/character-case'; +import { formatTypescript } from './shared/format'; +import { FILE_PATH_SRC } from './shared/paths'; +import { ALLOWED_MODULES } from './temp-module-filter'; + +const project = getProject(); + +const directories = project + .getDirectoryOrThrow('src') + .getDirectoryOrThrow('modules') + .getDirectories(); + +const moduleNames = new Set(directories.map((dir) => dir.getBaseName())); + +//#region Module +for (const directory of directories) { + const moduleName = directory.getBaseName(); + if (!ALLOWED_MODULES.has(toPascalCase(`${moduleName}Module`))) { + continue; + } + + console.log(`Processing module: ${moduleName}`); + //#region Index + const indexFile = directory.getSourceFileOrThrow('index.ts'); + + const header = indexFile + .getStatements()[0] + ?.getLeadingCommentRanges() + .map((c) => c.getText()); + + const imports = new Set([ + `import { SimpleModuleBase } from '../../internal/module-base';`, + `import { ModuleBase } from '../../internal/module-base';`, + `import type { Faker } from '../../faker';`, + `import type { LiteralUnion } from '../../internal/types';`, + `import type { Distributor } from '../../distributors/distributor';`, + ]); + if (moduleName === 'image') { + imports.add(`import type { SexType } from '../person';`); + } + + const exports: string[] = indexFile + .getExportDeclarations() + .map((exp) => exp.getText()); + + const typesFile = directory.getSourceFile('_types.ts'); + if (typesFile) { + const typesToImport = [ + typesFile.getEnums(), + typesFile.getTypeAliases(), + typesFile.getInterfaces(), + ] + .flat() + .filter((decl) => decl.isExported()) + .map((decl) => decl.getName()); + + if (typesToImport.length > 0) { + imports.add( + `import type { ${typesToImport.join(', ')} } from './_types';` + ); + } + } + + const content: string[] = []; + const classes = indexFile?.getClasses() ?? []; + + //#region Module Classes + for (const cls of classes) { + content.push(getJsDocs(cls).getText()); + const methodNames = cls.getMethods().map((method) => method.getName()); + for (const method of cls.getMethods()) { + if (method.getName() !== 'fake') { + method.remove(); + } + } + + for (const methodName of methodNames) { + if (methodName === 'fake') { + continue; + } + + const methodFile = directory.getSourceFileOrThrow( + `${toKebabCase(methodName)}.ts` + ); + + const typesToImport = [ + methodFile.getEnums(), + methodFile.getTypeAliases(), + methodFile.getInterfaces(), + ] + .flat() + .filter((decl) => decl.isExported()) + .map((decl) => decl.getName()); + + imports.add( + `import { ${methodName} as ${toCamelCase(moduleName, methodName)} } from './${toKebabCase(methodName)}';` + ); + if (typesToImport.length > 0) { + imports.add( + `import type { ${typesToImport.join(', ')} } from './${toKebabCase(methodName)}';` + ); + } + + const functions = methodFile + .getChildrenOfKind(SyntaxKind.FunctionDeclaration) + .filter((fn) => fn.isExported()) + .filter((fn) => fn.getName() === methodName); + + const parts: string[] = []; + + const restoreFakerTreeInvocations = ( + _: string, + module: string, + method: string + ): string => + methodNames.includes(`${module}${method}`) + ? `faker.${moduleName}.${module}${method}(` + : moduleNames.has(module) + ? `faker.${module}.${toCamelCase(method)}(` + : `faker.${module}${method}(`; + + for (const child of functions) { + //#region Module Functions + const jsDocs = child.getJsDocs()[0]; + + if (child.hasBody()) { + const params = child + .getSignature() + .getParameters() + .slice(1) + .map((param) => param.getName()); + + const isDeprecated = jsDocs && getDeprecated(jsDocs); + + child.setBodyText( + `${ + isDeprecated + ? '// eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal call\n' + : '' + }return ${toCamelCase(moduleName, methodName)}(this.faker.fakerCore, ${params.join(', ')});` + ); + } + + if (jsDocs) { + const description = jsDocs + .getFullText() + // Param + .replace(' * @param fakerCore The FakerCore to use.\n', '') + .replaceAll(/ +\*\n +\*\n/g, ' *\n') + // Examples + .replaceAll( + new RegExp(`${methodName}\\(fakerCore(?:, ?)?`, 'g'), + `faker.${moduleName}.${methodName}(` + ) + // Method References + .replaceAll( + /\b([a-z]+)([A-Z][a-zA-Z]+)\(fakerCore(?:, ?)?/g, + restoreFakerTreeInvocations + ) + .replaceAll( + /\b([a-zA-Z]+)\(fakerCore(?:, ?)?/g, + (_, method: string) => + `faker.${moduleName}.${toCamelCase(method)}(` + ); + + parts.push(description); + } + + const signature = child + .getSignature() + .getDeclaration() + .getText() + // Adapt signature + .replace('export function ', '') + .replace(/\((\n +)?fakerCore: FakerCore,?/, '(') + // Adapt nested options defaults + .replaceAll( + /(?<= +\* .*?)\bgetDefaultRefDate\(fakerCore(?:, ?)?/g, + 'faker.defaultRefDate(' + ) + .replaceAll( + /(?<= +\* .*?)\b([a-z]+)([A-Z][a-zA-Z]+)\(fakerCore(?:, ?)?/g, + restoreFakerTreeInvocations + ); + + parts.push(signature); + //#endregion + } + + cls.addMember(parts.join('\n')); + } + //#endregion + + content.push(cls.getText(), ''); + } + + content.unshift(...header, ...imports, '', ...exports, ''); + + writeFileSync( + resolve(FILE_PATH_SRC, 'modules', moduleName, 'index.ts'), + await formatTypescript(content.join('\n')), + 'utf8' + ); + //#endregion +} +//#endregion diff --git a/scripts/shared/character-case.ts b/scripts/shared/character-case.ts new file mode 100644 index 00000000000..b2827ab8ae3 --- /dev/null +++ b/scripts/shared/character-case.ts @@ -0,0 +1,19 @@ +export function toKebabCase(...values: string[]): string { + return values + .join('-') + .replaceAll(/([a-z])([A-Z])/g, '$1-$2') + .replaceAll(/[\s_]+/g, '-') + .toLowerCase(); +} + +export function toCamelCase(...values: string[]): string { + const text = values + .flatMap((value) => value.split(/[\s_-]+/)) + .map(toPascalCase) + .join(''); + return text.substring(0, 1).toLowerCase() + text.substring(1); +} + +export function toPascalCase(value: string): string { + return value.substring(0, 1).toUpperCase() + value.substring(1); +} diff --git a/scripts/shared/paths.ts b/scripts/shared/paths.ts index 8bf15b7bbe0..4942a08533b 100644 --- a/scripts/shared/paths.ts +++ b/scripts/shared/paths.ts @@ -25,7 +25,7 @@ export const FILE_PATH_DOCS_LOCALES = resolve(FILE_PATH_DOCS, 'locales'); /** * The path to the src directory. */ -const FILE_PATH_SRC = resolve(FILE_PATH_PROJECT, 'src'); +export const FILE_PATH_SRC = resolve(FILE_PATH_PROJECT, 'src'); /** * The path to the locale source files. */ diff --git a/scripts/temp-module-filter.ts b/scripts/temp-module-filter.ts new file mode 100644 index 00000000000..ec15aab07af --- /dev/null +++ b/scripts/temp-module-filter.ts @@ -0,0 +1,6 @@ +export const ALLOWED_MODULES = new Set([ + 'HelpersModule', + 'StringModule', + 'NumberModule', + 'DatatypeModule', +]); diff --git a/scripts/temp-tranform-once.ts b/scripts/temp-tranform-once.ts new file mode 100644 index 00000000000..08038d88841 --- /dev/null +++ b/scripts/temp-tranform-once.ts @@ -0,0 +1,299 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { ClassDeclaration, MethodDeclaration, Project } from 'ts-morph'; +import { SyntaxKind } from 'ts-morph'; +import { newProcessingError } from './apidocs/processing/error'; +import type { SignatureLikeDeclaration } from './apidocs/processing/signature'; +import { getProject } from './apidocs/project'; +import { required } from './apidocs/utils/value-checks'; +import { formatTypescript } from './shared/format'; +import { FILE_PATH_SRC } from './shared/paths'; +import { ALLOWED_MODULES } from './temp-module-filter'; + +const coreName = 'fakerCore'; + +await generate(); + +async function generate(): Promise { + console.log('Reading project'); + const project = getProject(); + console.log('Processing modules'); + await processModuleClasses(project); +} + +// Modules + +export async function processModuleClasses(project: Project): Promise { + await processModules( + Object.values( + getAllClasses( + project, + (module: string): boolean => + module.endsWith('Module') && + !module.startsWith('Simple') && + ALLOWED_MODULES.has(module) + ) + ).toSorted((a, b) => a.getNameOrThrow().localeCompare(b.getNameOrThrow())) + ); +} + +function getAllClasses( + project: Project, + filter: (name: string) => boolean = () => true +): Record { + return Object.fromEntries( + project + .getSourceFiles() + .flatMap((file) => file.getClasses()) + .map((clazz) => [clazz.getNameOrThrow(), clazz] as const) + .filter(([name]) => filter(name)) + ); +} + +async function processModules(modules: ClassDeclaration[]): Promise { + for (const module of modules) { + try { + await processModule(module); + } catch (error: unknown) { + throw newProcessingError({ + type: 'module', + name: getModuleName(module), + source: module, + cause: error, + }); + } + } +} + +async function processModule(module: ClassDeclaration): Promise { + const moduleName = getModuleName(module); + console.log(`Processing module: ${moduleName}`); + await processClassMethods(module, moduleName, getImports(module)); +} + +function getModuleName(module: ClassDeclaration): string { + return required(module.getName(), 'module name').replace(/Module$/, ''); +} + +function getImports(module: ClassDeclaration): string { + return module + .getSourceFile() + .getImportDeclarations() + .map((importDecl) => importDecl.getText()) + .join('\n'); +} + +export async function processClassMethods( + clazz: ClassDeclaration, + moduleName: string, + imports: string +): Promise { + await processMethods(getAllMethods(clazz), moduleName, imports); +} + +function getAllMethods(clazz: ClassDeclaration): MethodDeclaration[] { + const parents: ClassDeclaration[] = [clazz]; + let parent: ClassDeclaration | undefined = clazz; + while ((parent = parent.getBaseClass()) != null) { + parents.unshift(parent); + } + + const methods: Record = {}; + + for (const parent of parents) { + for (const method of parent.getMethods()) { + methods[method.getName()] = method; + } + } + + return Object.values(methods).toSorted((a, b) => + a.getName().localeCompare(b.getName()) + ); +} + +async function processMethods( + methods: MethodDeclaration[], + moduleName: string, + imports: string +): Promise { + for (const method of methods.filter( + (method) => !method.hasModifier(SyntaxKind.PrivateKeyword) + )) { + const name = method.getName(); + try { + await processMethod(moduleName, name, method, imports); + } catch (error) { + throw newProcessingError({ + type: 'method', + name, + source: method, + cause: error, + }); + } + } +} + +function toKebabCase(str: string): string { + return str.replaceAll(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} + +function toCamelCase(value: string, ...more: string[]): string { + return ( + value.substring(0, 1).toLowerCase() + + value.substring(1) + + more.map(toPascalCase).join('') + ); +} + +function toPascalCase(value: string): string { + return value.substring(0, 1).toUpperCase() + value.substring(1); +} + +async function processMethod( + moduleName: string, + name: string, + method: MethodDeclaration, + imports: string +): Promise { + if (name === 'fake') { + return; + } + + console.log(` - ${name}`); + + // Get all signatures (overloads) and implementation + const overloads = method.getOverloads(); + const signatureDeclarations: SignatureLikeDeclaration[] = + overloads.length > 0 ? [...overloads, method] : [method]; + + const importsByFile: Record> = {}; + + let fileBody = ''; + + const moduleDocsReplacer: (substring: string, ...args: string[]) => string = ( + _: string, + module: string, + method: string, + closer: string = ', ' + ) => { + if (module === toCamelCase(moduleName)) { + return `${method}(${coreName}${closer}`; + } + + return `${toCamelCase(module, method)}(${coreName}${closer}`; + }; + + for (const signature of signatureDeclarations) { + let jsdocs = signature.getJsDocs()[0]?.getText().trim() ?? ''; + let code = signature.getText().trim(); + + jsdocs = jsdocs + .replace( + /(\* @(?!template)|\*\/)/, + `* @param ${coreName} The FakerCore to use.\n $1` + ) + .replace(/(@param .*)\n *\* (@see|@example)/, `$1\n *\n * $2`) + // Replace calls to faker.defaultRefDate() in jsdocs (mostly defaults) + .replaceAll( + /\bfaker\.defaultRefDate\(\)/g, + 'getDefaultRefDate(fakerCore)' + ) + // Calls to modules in jsdocs (mostly examples) + .replaceAll(/\bfaker\.(\w+)\.(\w+)\((\))?/g, moduleDocsReplacer); + + const moduleCallReplacer: (substring: string, ...args: string[]) => string = + // eslint-disable-next-line unicorn/consistent-function-scoping + (_: string, module: string, method: string, closer: string = ', ') => { + if (module === 'this') { + module = moduleName; + } + + if (module === moduleName && method === name) { + return `${name}(${coreName}${closer}`; + } + + if (method === 'fake') { + return `new Faker(${coreName}).helpers.fake(`; + } + + const asName = code.includes(`${method} =`) + ? toCamelCase(module, method) + : method; + + (importsByFile[`../${toCamelCase(module)}/${toKebabCase(method)}`] ??= + new Set()).add(asName === method ? asName : `${method} as ${asName}`); + return `${asName}(${coreName}${closer}`; + }; + + // Option Parameter Defaults + code = code.replaceAll( + /(?<= +\* .*?)\bfaker\.(\w+)\.(\w+)\((\))?/g, + moduleDocsReplacer + ); + + // Add core parameter and export keyword + code = code + .replaceAll( + new RegExp(`^${name}(<.*>)?\\(`, 'gm'), + `export function ${name}$1(${coreName}: FakerCore, ` + ) + .replaceAll(', ):', '):'); + + // Calls to other modules + code = code.replaceAll( + /\b(?:this\.)?faker\.(\w+)\s*\.(\w+)\((\))?/g, + moduleCallReplacer + ); + + // Calls to own module + code = code.replaceAll(/\b(this)\.(\w+)\((\))?/g, moduleCallReplacer); + + // Replace locale data access + code = code.replaceAll(/\bthis\.faker\.definitions\b/g, 'fakerCore.locale'); + + // Replace default reference date access + code = code.replaceAll( + /\bthis\.faker\.defaultRefDate\(\)/g, + 'getDefaultRefDate(fakerCore)' + ); + + // Replace any remaining this.faker with parameter + code = code.replaceAll(/\bthis\.faker\b/g, coreName); + code = code.replaceAll('fakerCore.fakerCore', 'fakerCore'); + + fileBody += `${jsdocs}\n${code}\n`; + } + + const fileImports = [ + "import type { FakerCore } from '../../core'", + "import { Faker } from '../../faker'", + "import { getDefaultRefDate } from '../../utils/get-default-ref-date'", + imports, + ...Object.entries(importsByFile).map(([file, imports]) => { + return `import { ${[...imports].join(', ')} } from '${file}';`; + }), + ].join('\n'); + + let fileContent = `${fileImports}\n\n${fileBody}`; + + fileContent = fileContent.replaceAll( + '@default faker.defaultRefDate()', + '@default getDefaultRefDate(fakerCore)' + ); + + // Format the file content + try { + fileContent = await formatTypescript(fileContent); + } catch { + // ignore + } + + const outputPath = resolve( + FILE_PATH_SRC, + 'modules', + moduleName, + `${toKebabCase(name)}.ts` + ); + + writeFileSync(outputPath, fileContent, 'utf8'); +} From 6b73a31408f5b950b557bff726b74261958c9126 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 23 May 2026 16:32:54 +0200 Subject: [PATCH 2/2] feat: transform core modules to standalone module functions --- src/modules/datatype/boolean.ts | 54 ++ src/modules/datatype/index.ts | 19 +- src/modules/helpers/{eval.ts => _eval.ts} | 0 .../helpers/{luhn-check.ts => _luhn-check.ts} | 0 src/modules/helpers/array-element.ts | 32 + src/modules/helpers/array-elements.ts | 70 ++ src/modules/helpers/enum-value.ts | 36 + src/modules/helpers/from-reg-exp.ts | 330 +++++++++ src/modules/helpers/index.ts | 637 ++---------------- src/modules/helpers/maybe.ts | 38 ++ src/modules/helpers/multiple.ts | 51 ++ src/modules/helpers/mustache.ts | 42 ++ src/modules/helpers/object-entry.ts | 25 + src/modules/helpers/object-key.ts | 25 + src/modules/helpers/object-value.ts | 25 + src/modules/helpers/range-to-number.ts | 38 ++ .../helpers/replace-credit-card-symbols.ts | 160 +++++ src/modules/helpers/replace-symbols.ts | 74 ++ src/modules/helpers/shuffle.ts | 106 +++ src/modules/helpers/slugify.ts | 23 + src/modules/helpers/unique-array.ts | 53 ++ src/modules/helpers/weighted-array-element.ts | 66 ++ src/modules/number/big-int.ts | 102 +++ src/modules/number/binary.ts | 54 ++ src/modules/number/float.ts | 127 ++++ src/modules/number/hex.ts | 52 ++ src/modules/number/index.ts | 237 +------ src/modules/number/int.ts | 103 +++ src/modules/number/octal.ts | 54 ++ src/modules/number/roman-numeral.ts | 94 +++ src/modules/phone/index.ts | 4 +- src/modules/string/_types.ts | 78 +++ src/modules/string/alpha.ts | 103 +++ src/modules/string/alphanumeric.ts | 104 +++ src/modules/string/binary.ts | 56 ++ src/modules/string/from-characters.ts | 63 ++ src/modules/string/hexadecimal.ts | 102 +++ src/modules/string/index.ts | 401 +---------- src/modules/string/nanoid.ts | 62 ++ src/modules/string/numeric.ts | 110 +++ src/modules/string/octal.ts | 58 ++ src/modules/string/sample.ts | 49 ++ src/modules/string/symbol.ts | 74 ++ src/modules/string/ulid.ts | 38 ++ src/modules/string/uuid.ts | 139 +++- test/modules/finance.spec.ts | 2 +- test/modules/helpers-eval.spec.ts | 2 +- test/modules/helpers.spec.ts | 2 +- test/modules/number.spec.ts | 9 +- test/modules/phone.spec.ts | 2 +- 50 files changed, 2957 insertions(+), 1228 deletions(-) create mode 100644 src/modules/datatype/boolean.ts rename src/modules/helpers/{eval.ts => _eval.ts} (100%) rename src/modules/helpers/{luhn-check.ts => _luhn-check.ts} (100%) create mode 100644 src/modules/helpers/array-element.ts create mode 100644 src/modules/helpers/array-elements.ts create mode 100644 src/modules/helpers/enum-value.ts create mode 100644 src/modules/helpers/from-reg-exp.ts create mode 100644 src/modules/helpers/maybe.ts create mode 100644 src/modules/helpers/multiple.ts create mode 100644 src/modules/helpers/mustache.ts create mode 100644 src/modules/helpers/object-entry.ts create mode 100644 src/modules/helpers/object-key.ts create mode 100644 src/modules/helpers/object-value.ts create mode 100644 src/modules/helpers/range-to-number.ts create mode 100644 src/modules/helpers/replace-credit-card-symbols.ts create mode 100644 src/modules/helpers/replace-symbols.ts create mode 100644 src/modules/helpers/shuffle.ts create mode 100644 src/modules/helpers/slugify.ts create mode 100644 src/modules/helpers/unique-array.ts create mode 100644 src/modules/helpers/weighted-array-element.ts create mode 100644 src/modules/number/big-int.ts create mode 100644 src/modules/number/binary.ts create mode 100644 src/modules/number/float.ts create mode 100644 src/modules/number/hex.ts create mode 100644 src/modules/number/int.ts create mode 100644 src/modules/number/octal.ts create mode 100644 src/modules/number/roman-numeral.ts create mode 100644 src/modules/string/_types.ts create mode 100644 src/modules/string/alpha.ts create mode 100644 src/modules/string/alphanumeric.ts create mode 100644 src/modules/string/binary.ts create mode 100644 src/modules/string/from-characters.ts create mode 100644 src/modules/string/hexadecimal.ts create mode 100644 src/modules/string/nanoid.ts create mode 100644 src/modules/string/numeric.ts create mode 100644 src/modules/string/octal.ts create mode 100644 src/modules/string/sample.ts create mode 100644 src/modules/string/symbol.ts create mode 100644 src/modules/string/ulid.ts diff --git a/src/modules/datatype/boolean.ts b/src/modules/datatype/boolean.ts new file mode 100644 index 00000000000..b2cf6d802ed --- /dev/null +++ b/src/modules/datatype/boolean.ts @@ -0,0 +1,54 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; + +/** + * Returns the boolean value true or false. + * + * **Note:** + * A probability of `0.75` results in `true` being returned `75%` of the calls; likewise `0.3` => `30%`. + * If the probability is `<= 0.0`, it will always return `false`. + * If the probability is `>= 1.0`, it will always return `true`. + * The probability is limited to two decimal places. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object or the probability (`[0.00, 1.00]`) of returning `true`. + * @param options.probability The probability (`[0.00, 1.00]`) of returning `true`. Defaults to `0.5`. + * + * @example + * boolean(fakerCore) // false + * boolean(fakerCore, 0.9) // true + * boolean(fakerCore, { probability: 0.1 }) // false + * + * @since 5.5.0 + */ +export function boolean( + fakerCore: FakerCore, + options: + | number + | { + /** + * The probability (`[0.00, 1.00]`) of returning `true`. + * + * @default 0.5 + */ + probability?: number; + } = {} +): boolean { + if (typeof options === 'number') { + options = { + probability: options, + }; + } + + const { probability = 0.5 } = options; + if (probability <= 0) { + return false; + } + + if (probability >= 1) { + // This check is required to avoid returning false when float() returns 1 + return true; + } + + return float(fakerCore) < probability; +} diff --git a/src/modules/datatype/index.ts b/src/modules/datatype/index.ts index 6bcc4f83aee..21d192fb2e4 100644 --- a/src/modules/datatype/index.ts +++ b/src/modules/datatype/index.ts @@ -1,4 +1,5 @@ import { SimpleModuleBase } from '../../internal/module-base'; +import { boolean as datatypeBoolean } from './boolean'; /** * Module to generate boolean values. @@ -39,22 +40,6 @@ export class DatatypeModule extends SimpleModuleBase { probability?: number; } = {} ): boolean { - if (typeof options === 'number') { - options = { - probability: options, - }; - } - - const { probability = 0.5 } = options; - if (probability <= 0) { - return false; - } - - if (probability >= 1) { - // This check is required to avoid returning false when float() returns 1 - return true; - } - - return this.faker.number.float() < probability; + return datatypeBoolean(this.faker.fakerCore, options); } } diff --git a/src/modules/helpers/eval.ts b/src/modules/helpers/_eval.ts similarity index 100% rename from src/modules/helpers/eval.ts rename to src/modules/helpers/_eval.ts diff --git a/src/modules/helpers/luhn-check.ts b/src/modules/helpers/_luhn-check.ts similarity index 100% rename from src/modules/helpers/luhn-check.ts rename to src/modules/helpers/_luhn-check.ts diff --git a/src/modules/helpers/array-element.ts b/src/modules/helpers/array-element.ts new file mode 100644 index 00000000000..f27df9ce42b --- /dev/null +++ b/src/modules/helpers/array-element.ts @@ -0,0 +1,32 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { int } from '../number/int'; + +/** + * Returns random element from the given array. + * + * @template T The type of the elements to pick from. + * + * @param fakerCore The FakerCore to use. + * @param array The array to pick the value from. + * + * @throws {FakerError} If the given array is empty. + * + * @example + * arrayElement(fakerCore, ['cat', 'dog', 'mouse']) // 'dog' + * + * @since 6.3.0 + */ +export function arrayElement( + fakerCore: FakerCore, + array: ReadonlyArray +): T { + if (array.length === 0) { + throw new FakerError('Cannot get value from empty dataset.'); + } + + const index = + array.length > 1 ? int(fakerCore, { max: array.length - 1 }) : 0; + + return array[index]; +} diff --git a/src/modules/helpers/array-elements.ts b/src/modules/helpers/array-elements.ts new file mode 100644 index 00000000000..864ebfb2634 --- /dev/null +++ b/src/modules/helpers/array-elements.ts @@ -0,0 +1,70 @@ +import type { FakerCore } from '../../core'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { shuffle } from '../helpers/shuffle'; +import { int } from '../number/int'; + +/** + * Returns a subset with random elements of the given array in random order. + * + * @template T The type of the elements to pick from. + * + * @param fakerCore The FakerCore to use. + * @param array Array to pick the value from. + * @param count Number or range of elements to pick. + * When not provided, random number of elements will be picked. + * When value exceeds array boundaries, it will be limited to stay inside. + * + * @example + * arrayElements(fakerCore, ['cat', 'dog', 'mouse']) // ['mouse', 'cat'] + * arrayElements(fakerCore, [1, 2, 3, 4, 5], 2) // [4, 2] + * arrayElements(fakerCore, [1, 2, 3, 4, 5], { min: 2, max: 4 }) // [3, 5, 1] + * + * @since 6.3.0 + */ +export function arrayElements( + fakerCore: FakerCore, + array: ReadonlyArray, + count?: + | number + | { + /** + * The minimum number of elements to pick. + */ + min: number; + /** + * The maximum number of elements to pick. + */ + max: number; + } +): T[] { + if (array.length === 0) { + return []; + } + + const numElements = rangeToNumber( + fakerCore, + count ?? { min: 1, max: array.length } + ); + + if (numElements >= array.length) { + return shuffle(fakerCore, array); + } else if (numElements <= 0) { + return []; + } + + const arrayCopy = [...array]; + let i = array.length; + const min = i - numElements; + let temp: T; + let index: number; + + // Shuffle the last `count` elements of the array + while (i-- > min) { + index = int(fakerCore, i); + temp = arrayCopy[index]; + arrayCopy[index] = arrayCopy[i]; + arrayCopy[i] = temp; + } + + return arrayCopy.slice(min); +} diff --git a/src/modules/helpers/enum-value.ts b/src/modules/helpers/enum-value.ts new file mode 100644 index 00000000000..2ecb0224983 --- /dev/null +++ b/src/modules/helpers/enum-value.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random value from an Enum object. + * + * This does the same as `objectValue` except that it ignores (the values assigned to) the numeric keys added for TypeScript enums. + * + * @template T Type of generic enums, automatically inferred by TypeScript. + * + * @param fakerCore The FakerCore to use. + * @param enumObject Enum to pick the value from. + * + * @example + * enum Color { Red, Green, Blue } + * enumValue(fakerCore, Color) // 1 (Green) + * + * enum Direction { North = 'North', South = 'South'} + * enumValue(fakerCore, Direction) // 'South' + * + * enum HttpStatus { Ok = 200, Created = 201, BadRequest = 400, Unauthorized = 401 } + * enumValue(fakerCore, HttpStatus) // 200 (Ok) + * + * @since 8.0.0 + */ +export function enumValue>( + fakerCore: FakerCore, + enumObject: T +): T[keyof T] { + // ignore numeric keys added by TypeScript + const keys: Array = Object.keys(enumObject).filter((key) => + Number.isNaN(Number(key)) + ); + const randomKey = arrayElement(fakerCore, keys); + return enumObject[randomKey]; +} diff --git a/src/modules/helpers/from-reg-exp.ts b/src/modules/helpers/from-reg-exp.ts new file mode 100644 index 00000000000..2121c8367f6 --- /dev/null +++ b/src/modules/helpers/from-reg-exp.ts @@ -0,0 +1,330 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; +import { multiple } from '../helpers/multiple'; +import { int } from '../number/int'; +import { alphanumeric } from '../string/alphanumeric'; +import { fromCharacters } from '../string/from-characters'; + +/** + * Returns a number based on given RegEx-based quantifier symbol or quantifier values. + * + * @param fakerCore The FakerCore to use. + * @param quantifierSymbol Quantifier symbols can be either of these: `?`, `*`, `+`. + * @param quantifierMin Quantifier minimum value. If given without a maximum, this will be used as the quantifier value. + * @param quantifierMax Quantifier maximum value. Will randomly get a value between the minimum and maximum if both are provided. + * + * @returns a random number based on the given quantifier parameters. + * + * @example + * getRepetitionsBasedOnQuantifierParameters(fakerCore, '*', null, null) // 3 + * getRepetitionsBasedOnQuantifierParameters(fakerCore, null, 10, null) // 10 + * getRepetitionsBasedOnQuantifierParameters(fakerCore, null, 5, 8) // 6 + * + * @since 8.0.0 + */ +function getRepetitionsBasedOnQuantifierParameters( + fakerCore: FakerCore, + quantifierSymbol: string, + quantifierMin: string, + quantifierMax: string +): number { + let repetitions = 1; + if (quantifierSymbol) { + switch (quantifierSymbol) { + case '?': { + repetitions = boolean(fakerCore) ? 0 : 1; + break; + } + + case '*': { + let limit = 1; + while (boolean(fakerCore)) { + limit *= 2; + } + + repetitions = int(fakerCore, { min: 0, max: limit }); + break; + } + + case '+': { + let limit = 1; + while (boolean(fakerCore)) { + limit *= 2; + } + + repetitions = int(fakerCore, { min: 1, max: limit }); + break; + } + + default: { + throw new FakerError('Unknown quantifier symbol provided.'); + } + } + } else if (quantifierMin != null && quantifierMax != null) { + repetitions = int(fakerCore, { + min: Number.parseInt(quantifierMin), + max: Number.parseInt(quantifierMax), + }); + } else if (quantifierMin != null && quantifierMax == null) { + repetitions = Number.parseInt(quantifierMin); + } + + return repetitions; +} + +/** + * Generates a string matching the given regex like expressions. + * + * This function doesn't provide full support of actual `RegExp`. + * Features such as grouping, anchors and character classes are not supported. + * If you are looking for a library that randomly generates strings based on + * `RegExp`s, see [randexp.js](https://github.com/fent/randexp.js) + * + * Supported patterns: + * - `x{times}` => Repeat the `x` exactly `times` times. + * - `x{min,max}` => Repeat the `x` `min` to `max` times. + * - `[x-y]` => Randomly get a character between `x` and `y` (inclusive). + * - `[x-y]{times}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `times` times. + * - `[x-y]{min,max}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `min` to `max` times. + * - `[^...]` => Randomly get an ASCII number or letter character that is not in the given range. (e.g. `[^0-9]` will get a random non-numeric character). + * - `[-...]` => Include dashes in the range. Must be placed after the negate character `^` and before any character sets if used (e.g. `[^-0-9]` will not get any numeric characters or dashes). + * - `/[x-y]/i` => Randomly gets an uppercase or lowercase character between `x` and `y` (inclusive). + * - `x?` => Randomly decide to include or not include `x`. + * - `[x-y]?` => Randomly decide to include or not include characters between `x` and `y` (inclusive). + * - `x*` => Repeat `x` 0 or more times. + * - `[x-y]*` => Repeat characters between `x` and `y` (inclusive) 0 or more times. + * - `x+` => Repeat `x` 1 or more times. + * - `[x-y]+` => Repeat characters between `x` and `y` (inclusive) 1 or more times. + * - `.` => returns a wildcard ASCII character that can be any number, character or symbol. Can be combined with quantifiers as well. + * + * @param fakerCore The FakerCore to use. + * @param pattern The template string/RegExp to generate a matching string for. + * + * @throws {FakerError} If min value is more than max value in quantifier, e.g. `#{10,5}`. + * @throws {FakerError} If an invalid quantifier symbol is passed in. + * + * @example + * fromRegExp(fakerCore, '#{5}') // '#####' + * fromRegExp(fakerCore, '#{2,9}') // '#######' + * fromRegExp(fakerCore, '[1-7]') // '5' + * fromRegExp(fakerCore, '#{3}test[1-5]') // '###test3' + * fromRegExp(fakerCore, '[0-9a-dmno]') // '5' + * fromRegExp(fakerCore, '[^a-zA-Z0-8]') // '9' + * fromRegExp(fakerCore, '[a-d0-6]{2,8}') // 'a0dc45b0' + * fromRegExp(fakerCore, '[-a-z]{5}') // 'a-zab' + * fromRegExp(fakerCore, /[A-Z0-9]{4}-[A-Z0-9]{4}/) // 'BS4G-485H' + * fromRegExp(fakerCore, /[A-Z]{5}/i) // 'pDKfh' + * fromRegExp(fakerCore, /.{5}/) // '14(#B' + * fromRegExp(fakerCore, /Joh?n/) // 'Jon' + * fromRegExp(fakerCore, /ABC*DE/) // 'ABDE' + * fromRegExp(fakerCore, /bee+p/) // 'beeeeeeeep' + * + * @since 8.0.0 + */ +export function fromRegExp( + fakerCore: FakerCore, + pattern: string | RegExp +): string { + let isCaseInsensitive = false; + + if (pattern instanceof RegExp) { + isCaseInsensitive = pattern.flags.includes('i'); + pattern = pattern.source.replace(/^\^+/, '').replace(/\$+$/, ''); + } + + let min: number; + let max: number; + let repetitions: number; + + // Deal with single wildcards + const SINGLE_CHAR_REG = + /([.A-Za-z0-9])(?:\{(\d+)(?:,(\d+)|)\}|(\?|\*|\+))(?![^[]*]|[^{]*})/; + let token = SINGLE_CHAR_REG.exec(pattern); + while (token != null) { + const quantifierMin: string = token[2]; + const quantifierMax: string = token[3]; + const quantifierSymbol: string = token[4]; + + repetitions = getRepetitionsBasedOnQuantifierParameters( + fakerCore, + quantifierSymbol, + quantifierMin, + quantifierMax + ); + + let replacement: string; + if (token[1] === '.') { + replacement = alphanumeric(fakerCore, repetitions); + } else if (isCaseInsensitive) { + replacement = fromCharacters( + fakerCore, + [token[1].toLowerCase(), token[1].toUpperCase()], + repetitions + ); + } else { + replacement = token[1].repeat(repetitions); + } + + pattern = + pattern.slice(0, token.index) + + replacement + + pattern.slice(token.index + token[0].length); + token = SINGLE_CHAR_REG.exec(pattern); + } + + const SINGLE_RANGE_REG = /(\d-\d|\w-\w|\d|\w|[-!@#$&()`.+,/"])/; + const RANGE_ALPHANUMEMRIC_REG = + /\[(\^|)(-|)(.+?)\](?:\{(\d+)(?:,(\d+)|)\}|(\?|\*|\+)|)/; + // Deal with character classes with quantifiers `[a-z0-9]{min[, max]}` + token = RANGE_ALPHANUMEMRIC_REG.exec(pattern); + while (token != null) { + const isNegated = token[1] === '^'; + const includesDash: boolean = token[2] === '-'; + const quantifierMin: string = token[4]; + const quantifierMax: string = token[5]; + const quantifierSymbol: string = token[6]; + + const rangeCodes: number[] = []; + + let ranges = token[3]; + let range = SINGLE_RANGE_REG.exec(ranges); + + if (includesDash) { + // 45 is the ascii code for '-' + rangeCodes.push(45); + } + + while (range != null) { + if (range[0].includes('-')) { + // handle ranges + const rangeMinMax = range[0] + .split('-') + .map((x) => x.codePointAt(0) ?? Number.NaN); + min = rangeMinMax[0]; + max = rangeMinMax[1]; + // throw error if min larger than max + if (min > max) { + throw new FakerError('Character range provided is out of order.'); + } + + for (let i = min; i <= max; i++) { + if ( + isCaseInsensitive && + Number.isNaN(Number(String.fromCodePoint(i))) + ) { + const ch = String.fromCodePoint(i); + rangeCodes.push( + ch.toUpperCase().codePointAt(0) ?? Number.NaN, + ch.toLowerCase().codePointAt(0) ?? Number.NaN + ); + } else { + rangeCodes.push(i); + } + } + } else { + // handle non-ranges + if (isCaseInsensitive && Number.isNaN(Number(range[0]))) { + rangeCodes.push( + range[0].toUpperCase().codePointAt(0) ?? Number.NaN, + range[0].toLowerCase().codePointAt(0) ?? Number.NaN + ); + } else { + rangeCodes.push(range[0].codePointAt(0) ?? Number.NaN); + } + } + + ranges = ranges.substring(range[0].length); + range = SINGLE_RANGE_REG.exec(ranges); + } + + repetitions = getRepetitionsBasedOnQuantifierParameters( + fakerCore, + quantifierSymbol, + quantifierMin, + quantifierMax + ); + + if (isNegated) { + let index; + // 0-9 + for (let i = 48; i <= 57; i++) { + index = rangeCodes.indexOf(i); + if (index > -1) { + rangeCodes.splice(index, 1); + continue; + } + + rangeCodes.push(i); + } + + // A-Z + for (let i = 65; i <= 90; i++) { + index = rangeCodes.indexOf(i); + if (index > -1) { + rangeCodes.splice(index, 1); + continue; + } + + rangeCodes.push(i); + } + + // a-z + for (let i = 97; i <= 122; i++) { + index = rangeCodes.indexOf(i); + if (index > -1) { + rangeCodes.splice(index, 1); + continue; + } + + rangeCodes.push(i); + } + } + + const generatedString = multiple( + fakerCore, + () => String.fromCodePoint(arrayElement(fakerCore, rangeCodes)), + { count: repetitions } + ).join(''); + + pattern = + pattern.slice(0, token.index) + + generatedString + + pattern.slice(token.index + token[0].length); + token = RANGE_ALPHANUMEMRIC_REG.exec(pattern); + } + + const RANGE_REP_REG = /(.)\{(\d+),(\d+)\}/; + // Deal with quantifier ranges `{min,max}` + token = RANGE_REP_REG.exec(pattern); + while (token != null) { + min = Number.parseInt(token[2]); + max = Number.parseInt(token[3]); + // throw error if min larger than max + if (min > max) { + throw new FakerError('Numbers out of order in {} quantifier.'); + } + + repetitions = int(fakerCore, { min, max }); + pattern = + pattern.slice(0, token.index) + + token[1].repeat(repetitions) + + pattern.slice(token.index + token[0].length); + token = RANGE_REP_REG.exec(pattern); + } + + const REP_REG = /(.)\{(\d+)\}/; + // Deal with repeat `{num}` + token = REP_REG.exec(pattern); + while (token != null) { + repetitions = Number.parseInt(token[2]); + pattern = + pattern.slice(0, token.index) + + token[1].repeat(repetitions) + + pattern.slice(token.index + token[0].length); + token = REP_REG.exec(pattern); + } + + return pattern; +} diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 4874da9f806..c2304245969 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -1,202 +1,23 @@ -import type { Faker, SimpleFaker } from '../..'; -import { FakerError } from '../../errors/faker-error'; +import type { Faker } from '../../faker'; import { SimpleModuleBase } from '../../internal/module-base'; -import { fakeEval } from './eval'; -import { luhnCheckValue } from './luhn-check'; - -/** - * Returns a number based on given RegEx-based quantifier symbol or quantifier values. - * - * @param faker The Faker instance to use. - * @param quantifierSymbol Quantifier symbols can be either of these: `?`, `*`, `+`. - * @param quantifierMin Quantifier minimum value. If given without a maximum, this will be used as the quantifier value. - * @param quantifierMax Quantifier maximum value. Will randomly get a value between the minimum and maximum if both are provided. - * - * @returns a random number based on the given quantifier parameters. - * - * @example - * getRepetitionsBasedOnQuantifierParameters(faker, '*', null, null) // 3 - * getRepetitionsBasedOnQuantifierParameters(faker, null, 10, null) // 10 - * getRepetitionsBasedOnQuantifierParameters(faker, null, 5, 8) // 6 - * - * @since 8.0.0 - */ -function getRepetitionsBasedOnQuantifierParameters( - faker: SimpleFaker, - quantifierSymbol: string, - quantifierMin: string, - quantifierMax: string -) { - let repetitions = 1; - if (quantifierSymbol) { - switch (quantifierSymbol) { - case '?': { - repetitions = faker.datatype.boolean() ? 0 : 1; - break; - } - - case '*': { - let limit = 1; - while (faker.datatype.boolean()) { - limit *= 2; - } - - repetitions = faker.number.int({ min: 0, max: limit }); - break; - } - - case '+': { - let limit = 1; - while (faker.datatype.boolean()) { - limit *= 2; - } - - repetitions = faker.number.int({ min: 1, max: limit }); - break; - } - - default: { - throw new FakerError('Unknown quantifier symbol provided.'); - } - } - } else if (quantifierMin != null && quantifierMax != null) { - repetitions = faker.number.int({ - min: Number.parseInt(quantifierMin), - max: Number.parseInt(quantifierMax), - }); - } else if (quantifierMin != null && quantifierMax == null) { - repetitions = Number.parseInt(quantifierMin); - } - - return repetitions; -} - -/** - * Replaces the regex like expressions in the given string with matching values. - * - * Note: This method will be removed in v9. - * - * Supported patterns: - * - `.{times}` => Repeat the character exactly `times` times. - * - `.{min,max}` => Repeat the character `min` to `max` times. - * - `[min-max]` => Generate a number between min and max (inclusive). - * - * @internal - * - * @param faker The Faker instance to use. - * @param string The template string to parse. - * - * @example - * legacyRegexpStringParse(faker) // '' - * legacyRegexpStringParse(faker, '#{5}') // '#####' - * legacyRegexpStringParse(faker, '#{2,9}') // '#######' - * legacyRegexpStringParse(faker, '[500-15000]') // '8375' - * legacyRegexpStringParse(faker, '#{3}test[1-5]') // '###test3' - * - * @since 5.0.0 - */ -function legacyRegexpStringParse( - faker: SimpleFaker, - string: string = '' -): string { - // Deal with range repeat `{min,max}` - const RANGE_REP_REG = /(.)\{(\d+),(\d+)\}/; - const REP_REG = /(.)\{(\d+)\}/; - const RANGE_REG = /\[(\d+)-(\d+)\]/; - let min: number; - let max: number; - let tmp: number; - let repetitions: number; - let token = RANGE_REP_REG.exec(string); - while (token != null) { - min = Number.parseInt(token[2]); - max = Number.parseInt(token[3]); - // switch min and max - if (min > max) { - tmp = max; - max = min; - min = tmp; - } - - repetitions = faker.number.int({ min, max }); - string = - string.slice(0, token.index) + - token[1].repeat(repetitions) + - string.slice(token.index + token[0].length); - token = RANGE_REP_REG.exec(string); - } - - // Deal with repeat `{num}` - token = REP_REG.exec(string); - while (token != null) { - repetitions = Number.parseInt(token[2]); - string = - string.slice(0, token.index) + - token[1].repeat(repetitions) + - string.slice(token.index + token[0].length); - token = REP_REG.exec(string); - } - // Deal with range `[min-max]` (only works with numbers for now) - - token = RANGE_REG.exec(string); - while (token != null) { - min = Number.parseInt(token[1]); // This time we are not capturing the char before `[]` - max = Number.parseInt(token[2]); - // switch min and max - if (min > max) { - tmp = max; - max = min; - min = tmp; - } - - string = - string.slice(0, token.index) + - faker.number.int({ min, max }).toString() + - string.slice(token.index + token[0].length); - token = RANGE_REG.exec(string); - } - - return string; -} - -/** - * Parses the given string symbol by symbol and replaces the placeholders with digits (`0` - `9`). - * `!` will be replaced by digits >=2 (`2` - `9`). - * - * Note: This method will be removed in v9. - * - * @internal - * - * @param faker The Faker instance to use. - * @param string The template string to parse. Defaults to `''`. - * @param symbol The symbol to replace with digits. Defaults to `'#'`. - * - * @example - * legacyReplaceSymbolWithNumber(faker) // '' - * legacyReplaceSymbolWithNumber(faker, '#####') // '04812' - * legacyReplaceSymbolWithNumber(faker, '!####') // '27378' - * legacyReplaceSymbolWithNumber(faker, 'Your pin is: !####') // '29841' - * - * @since 8.4.0 - */ -export function legacyReplaceSymbolWithNumber( - faker: SimpleFaker, - string: string = '', - symbol: string = '#' -): string { - let result = ''; - for (let i = 0; i < string.length; i++) { - if (string.charAt(i) === symbol) { - result += faker.number.int(9); - } else if (string.charAt(i) === '!') { - result += faker.number.int({ min: 2, max: 9 }); - } else { - result += string.charAt(i); - } - } - - return result; -} +import { fakeEval } from './_eval'; +import { arrayElement as helpersArrayElement } from './array-element'; +import { arrayElements as helpersArrayElements } from './array-elements'; +import { enumValue as helpersEnumValue } from './enum-value'; +import { fromRegExp as helpersFromRegExp } from './from-reg-exp'; +import { maybe as helpersMaybe } from './maybe'; +import { multiple as helpersMultiple } from './multiple'; +import { mustache as helpersMustache } from './mustache'; +import { objectEntry as helpersObjectEntry } from './object-entry'; +import { objectKey as helpersObjectKey } from './object-key'; +import { objectValue as helpersObjectValue } from './object-value'; +import { rangeToNumber as helpersRangeToNumber } from './range-to-number'; +import { replaceCreditCardSymbols as helpersReplaceCreditCardSymbols } from './replace-credit-card-symbols'; +import { replaceSymbols as helpersReplaceSymbols } from './replace-symbols'; +import { shuffle as helpersShuffle } from './shuffle'; +import { slugify as helpersSlugify } from './slugify'; +import { uniqueArray as helpersUniqueArray } from './unique-array'; +import { weightedArrayElement as helpersWeightedArrayElement } from './weighted-array-element'; /** * Module with various helper methods providing basic (seed-dependent) operations useful for implementing faker methods (without methods requiring localized data). @@ -216,11 +37,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 2.0.1 */ slugify(string: string = ''): string { - return string - .normalize('NFKD') //for example è decomposes to as e + ̀ - .replaceAll(/[\u0300-\u036F]/g, '') // removes combining marks - .replaceAll(' ', '-') // replaces spaces with hyphens - .replaceAll(/[^\w.-]+/g, ''); // removes all non-word characters except for dots and hyphens + return helpersSlugify(this.faker.fakerCore, string); } /** @@ -242,51 +59,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 3.0.0 */ replaceSymbols(string: string = ''): string { - const alpha = [ - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - 'H', - 'I', - 'J', - 'K', - 'L', - 'M', - 'N', - 'O', - 'P', - 'Q', - 'R', - 'S', - 'T', - 'U', - 'V', - 'W', - 'X', - 'Y', - 'Z', - ]; - let result = ''; - - for (let i = 0; i < string.length; i++) { - if (string.charAt(i) === '#') { - result += this.faker.number.int(9); - } else if (string.charAt(i) === '?') { - result += this.arrayElement(alpha); - } else if (string.charAt(i) === '*') { - result += this.faker.datatype.boolean() - ? this.arrayElement(alpha) - : this.faker.number.int(9); - } else { - result += string.charAt(i); - } - } - - return result; + return helpersReplaceSymbols(this.faker.fakerCore, string); } /** @@ -308,13 +81,11 @@ export class SimpleHelpersModule extends SimpleModuleBase { string: string = '6453-####-####-####-###L', symbol: string = '#' ): string { - // default values required for calling method without arguments - - string = legacyRegexpStringParse(this.faker, string); // replace [4-9] with a random number in range etc... - string = legacyReplaceSymbolWithNumber(this.faker, string, symbol); // replace ### with random numbers - - const checkNum = luhnCheckValue(string); - return string.replace('L', String(checkNum)); + return helpersReplaceCreditCardSymbols( + this.faker.fakerCore, + string, + symbol + ); } /** @@ -366,204 +137,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 8.0.0 */ fromRegExp(pattern: string | RegExp): string { - let isCaseInsensitive = false; - - if (pattern instanceof RegExp) { - isCaseInsensitive = pattern.flags.includes('i'); - pattern = pattern.source.replace(/^\^+/, '').replace(/\$+$/, ''); - } - - let min: number; - let max: number; - let repetitions: number; - - // Deal with single wildcards - const SINGLE_CHAR_REG = - /([.A-Za-z0-9])(?:\{(\d+)(?:,(\d+)|)\}|(\?|\*|\+))(?![^[]*]|[^{]*})/; - let token = SINGLE_CHAR_REG.exec(pattern); - while (token != null) { - const quantifierMin: string = token[2]; - const quantifierMax: string = token[3]; - const quantifierSymbol: string = token[4]; - - repetitions = getRepetitionsBasedOnQuantifierParameters( - this.faker, - quantifierSymbol, - quantifierMin, - quantifierMax - ); - - let replacement: string; - if (token[1] === '.') { - replacement = this.faker.string.alphanumeric(repetitions); - } else if (isCaseInsensitive) { - replacement = this.faker.string.fromCharacters( - [token[1].toLowerCase(), token[1].toUpperCase()], - repetitions - ); - } else { - replacement = token[1].repeat(repetitions); - } - - pattern = - pattern.slice(0, token.index) + - replacement + - pattern.slice(token.index + token[0].length); - token = SINGLE_CHAR_REG.exec(pattern); - } - - const SINGLE_RANGE_REG = /(\d-\d|\w-\w|\d|\w|[-!@#$&()`.+,/"])/; - const RANGE_ALPHANUMEMRIC_REG = - /\[(\^|)(-|)(.+?)\](?:\{(\d+)(?:,(\d+)|)\}|(\?|\*|\+)|)/; - // Deal with character classes with quantifiers `[a-z0-9]{min[, max]}` - token = RANGE_ALPHANUMEMRIC_REG.exec(pattern); - while (token != null) { - const isNegated = token[1] === '^'; - const includesDash: boolean = token[2] === '-'; - const quantifierMin: string = token[4]; - const quantifierMax: string = token[5]; - const quantifierSymbol: string = token[6]; - - const rangeCodes: number[] = []; - - let ranges = token[3]; - let range = SINGLE_RANGE_REG.exec(ranges); - - if (includesDash) { - // 45 is the ascii code for '-' - rangeCodes.push(45); - } - - while (range != null) { - if (range[0].includes('-')) { - // handle ranges - const rangeMinMax = range[0] - .split('-') - .map((x) => x.codePointAt(0) ?? Number.NaN); - min = rangeMinMax[0]; - max = rangeMinMax[1]; - // throw error if min larger than max - if (min > max) { - throw new FakerError('Character range provided is out of order.'); - } - - for (let i = min; i <= max; i++) { - if ( - isCaseInsensitive && - Number.isNaN(Number(String.fromCodePoint(i))) - ) { - const ch = String.fromCodePoint(i); - rangeCodes.push( - ch.toUpperCase().codePointAt(0) ?? Number.NaN, - ch.toLowerCase().codePointAt(0) ?? Number.NaN - ); - } else { - rangeCodes.push(i); - } - } - } else { - // handle non-ranges - if (isCaseInsensitive && Number.isNaN(Number(range[0]))) { - rangeCodes.push( - range[0].toUpperCase().codePointAt(0) ?? Number.NaN, - range[0].toLowerCase().codePointAt(0) ?? Number.NaN - ); - } else { - rangeCodes.push(range[0].codePointAt(0) ?? Number.NaN); - } - } - - ranges = ranges.substring(range[0].length); - range = SINGLE_RANGE_REG.exec(ranges); - } - - repetitions = getRepetitionsBasedOnQuantifierParameters( - this.faker, - quantifierSymbol, - quantifierMin, - quantifierMax - ); - - if (isNegated) { - let index; - // 0-9 - for (let i = 48; i <= 57; i++) { - index = rangeCodes.indexOf(i); - if (index > -1) { - rangeCodes.splice(index, 1); - continue; - } - - rangeCodes.push(i); - } - - // A-Z - for (let i = 65; i <= 90; i++) { - index = rangeCodes.indexOf(i); - if (index > -1) { - rangeCodes.splice(index, 1); - continue; - } - - rangeCodes.push(i); - } - - // a-z - for (let i = 97; i <= 122; i++) { - index = rangeCodes.indexOf(i); - if (index > -1) { - rangeCodes.splice(index, 1); - continue; - } - - rangeCodes.push(i); - } - } - - const generatedString = this.multiple( - () => String.fromCodePoint(this.arrayElement(rangeCodes)), - { count: repetitions } - ).join(''); - - pattern = - pattern.slice(0, token.index) + - generatedString + - pattern.slice(token.index + token[0].length); - token = RANGE_ALPHANUMEMRIC_REG.exec(pattern); - } - - const RANGE_REP_REG = /(.)\{(\d+),(\d+)\}/; - // Deal with quantifier ranges `{min,max}` - token = RANGE_REP_REG.exec(pattern); - while (token != null) { - min = Number.parseInt(token[2]); - max = Number.parseInt(token[3]); - // throw error if min larger than max - if (min > max) { - throw new FakerError('Numbers out of order in {} quantifier.'); - } - - repetitions = this.faker.number.int({ min, max }); - pattern = - pattern.slice(0, token.index) + - token[1].repeat(repetitions) + - pattern.slice(token.index + token[0].length); - token = RANGE_REP_REG.exec(pattern); - } - - const REP_REG = /(.)\{(\d+)\}/; - // Deal with repeat `{num}` - token = REP_REG.exec(pattern); - while (token != null) { - repetitions = Number.parseInt(token[2]); - pattern = - pattern.slice(0, token.index) + - token[1].repeat(repetitions) + - pattern.slice(token.index + token[0].length); - token = REP_REG.exec(pattern); - } - - return pattern; + return helpersFromRegExp(this.faker.fakerCore, pattern); } /** @@ -645,18 +219,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { } ): T[]; shuffle(list: T[], options: { inplace?: boolean } = {}): T[] { - const { inplace = false } = options; - - if (!inplace) { - list = [...list]; - } - - for (let i = list.length - 1; i > 0; --i) { - const j = this.faker.number.int(i); - [list[i], list[j]] = [list[j], list[i]]; - } - - return list; + return helpersShuffle(this.faker.fakerCore, list, options); } /** @@ -685,27 +248,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { source: ReadonlyArray | (() => T), length: number ): T[] { - if (Array.isArray(source)) { - const set = new Set(source); - const array = [...set]; - return this.shuffle(array).splice(0, length); - } - - const set = new Set(); - try { - if (typeof source === 'function') { - const maxAttempts = 1000 * length; - let attempts = 0; - while (set.size < length && attempts < maxAttempts) { - set.add(source()); - attempts++; - } - } - } catch { - // Ignore - } - - return [...set]; + return helpersUniqueArray(this.faker.fakerCore, source, length); } /** @@ -728,23 +271,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { text: string | undefined, data: Record[1]> ): string { - if (text == null) { - return ''; - } - - for (const p in data) { - const re = new RegExp(`{{${p}}}`, 'g'); - let value = data[p]; - if (typeof value === 'string') { - // escape $, source: https://stackoverflow.com/a/6969486/6897682 - value = value.replaceAll('$', '$$$$'); - text = text.replace(re, value); - } else { - text = text.replace(re, value); - } - } - - return text; + return helpersMustache(this.faker.fakerCore, text, data); } /** @@ -774,11 +301,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { probability?: number; } = {} ): TResult | undefined { - if (this.faker.datatype.boolean(options)) { - return callback(); - } - - return undefined; + return helpersMaybe(this.faker.fakerCore, callback, options); } /** @@ -796,8 +319,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 6.3.0 */ objectKey>(object: T): keyof T { - const array: Array = Object.keys(object); - return this.arrayElement(array); + return helpersObjectKey(this.faker.fakerCore, object); } /** @@ -815,8 +337,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 6.3.0 */ objectValue>(object: T): T[keyof T] { - const key = this.faker.helpers.objectKey(object); - return object[key]; + return helpersObjectValue(this.faker.fakerCore, object); } /** @@ -836,8 +357,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { objectEntry>( object: T ): [keyof T, T[keyof T]] { - const key = this.faker.helpers.objectKey(object); - return [key, object[key]]; + return helpersObjectEntry(this.faker.fakerCore, object); } /** @@ -855,14 +375,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { * @since 6.3.0 */ arrayElement(array: ReadonlyArray): T { - if (array.length === 0) { - throw new FakerError('Cannot get value from empty dataset.'); - } - - const index = - array.length > 1 ? this.faker.number.int({ max: array.length - 1 }) : 0; - - return array[index]; + return helpersArrayElement(this.faker.fakerCore, array); } /** @@ -896,34 +409,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { value: T; }> ): T { - if (array.length === 0) { - throw new FakerError( - 'weightedArrayElement expects an array with at least one element' - ); - } - - if (!array.every((elt) => elt.weight > 0)) { - throw new FakerError( - 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' - ); - } - - const total = array.reduce((sum, { weight }) => sum + weight, 0); - const random = this.faker.number.float({ - min: 0, - max: total, - }); - let current = 0; - for (const { weight, value } of array) { - current += weight; - if (random < current) { - return value; - } - } - - // In case of rounding errors, return the last element - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return array.at(-1)!.value; + return helpersWeightedArrayElement(this.faker.fakerCore, array); } /** @@ -958,35 +444,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { max: number; } ): T[] { - if (array.length === 0) { - return []; - } - - const numElements = this.rangeToNumber( - count ?? { min: 1, max: array.length } - ); - - if (numElements >= array.length) { - return this.shuffle(array); - } else if (numElements <= 0) { - return []; - } - - const arrayCopy = [...array]; - let i = array.length; - const min = i - numElements; - let temp: T; - let index: number; - - // Shuffle the last `count` elements of the array - while (i-- > min) { - index = this.faker.number.int(i); - temp = arrayCopy[index]; - arrayCopy[index] = arrayCopy[i]; - arrayCopy[i] = temp; - } - - return arrayCopy.slice(min); + return helpersArrayElements(this.faker.fakerCore, array, count); } /** @@ -1010,16 +468,10 @@ export class SimpleHelpersModule extends SimpleModuleBase { * * @since 8.0.0 */ - // This does not use `const T` because enums shouldn't be created on the spot. enumValue>( enumObject: T ): T[keyof T] { - // ignore numeric keys added by TypeScript - const keys: Array = Object.keys(enumObject).filter((key) => - Number.isNaN(Number(key)) - ); - const randomKey = this.arrayElement(keys); - return enumObject[randomKey]; + return helpersEnumValue(this.faker.fakerCore, enumObject); } /** @@ -1049,11 +501,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { max: number; } ): number { - if (typeof numberOrRange === 'number') { - return numberOrRange; - } - - return this.faker.number.int(numberOrRange); + return helpersRangeToNumber(this.faker.fakerCore, numberOrRange); } /** @@ -1095,12 +543,7 @@ export class SimpleHelpersModule extends SimpleModuleBase { }; } = {} ): TResult[] { - const count = this.rangeToNumber(options.count ?? 3); - if (count <= 0) { - return []; - } - - return Array.from({ length: count }, method); + return helpersMultiple(this.faker.fakerCore, method, options); } } diff --git a/src/modules/helpers/maybe.ts b/src/modules/helpers/maybe.ts new file mode 100644 index 00000000000..ce8136099aa --- /dev/null +++ b/src/modules/helpers/maybe.ts @@ -0,0 +1,38 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; + +/** + * Returns the result of the callback if the probability check was successful, otherwise `undefined`. + * + * @template TResult The type of result of the given callback. + * + * @param fakerCore The FakerCore to use. + * @param callback The callback that will be invoked if the probability check was successful. + * @param options The options to use. + * @param options.probability The probability (`[0.00, 1.00]`) of the callback being invoked. Defaults to `0.5`. + * + * @example + * maybe(fakerCore, () => 'Hello World!') // 'Hello World!' + * maybe(fakerCore, () => 'Hello World!', { probability: 0.1 }) // undefined + * maybe(fakerCore, () => 'Hello World!', { probability: 0.9 }) // 'Hello World!' + * + * @since 6.3.0 + */ +export function maybe( + fakerCore: FakerCore, + callback: () => TResult, + options: { + /** + * The probability (`[0.00, 1.00]`) of the callback being invoked. + * + * @default 0.5 + */ + probability?: number; + } = {} +): TResult | undefined { + if (boolean(fakerCore, options)) { + return callback(); + } + + return undefined; +} diff --git a/src/modules/helpers/multiple.ts b/src/modules/helpers/multiple.ts new file mode 100644 index 00000000000..ce0443e37b3 --- /dev/null +++ b/src/modules/helpers/multiple.ts @@ -0,0 +1,51 @@ +import type { FakerCore } from '../../core'; +import { rangeToNumber } from '../helpers/range-to-number'; + +/** + * Generates an array containing values returned by the given method. + * + * @template TResult The type of elements. + * + * @param fakerCore The FakerCore to use. + * @param method The method used to generate the values. + * The method will be called with `(_, index)`, to allow using the index in the generated value e.g. as id. + * @param options The optional options object. + * @param options.count The number or range of elements to generate. Defaults to `3`. + * + * @example + * multiple(fakerCore, () => personFirstName(fakerCore)) // [ 'Aniya', 'Norval', 'Dallin' ] + * multiple(fakerCore, () => personFirstName(fakerCore), { count: 3 }) // [ 'Santos', 'Lavinia', 'Lavinia' ] + * multiple(fakerCore, (_, i) => `${colorHuman(fakerCore)}-${i + 1}`) // [ 'orange-1', 'orchid-2', 'sky blue-3' ] + * + * @since 8.0.0 + */ +export function multiple( + fakerCore: FakerCore, + method: (v: unknown, index: number) => TResult, + options: { + /** + * The number or range of elements to generate. + * + * @default 3 + */ + count?: + | number + | { + /** + * The minimum value for the range. + */ + min: number; + /** + * The maximum value for the range. + */ + max: number; + }; + } = {} +): TResult[] { + const count = rangeToNumber(fakerCore, options.count ?? 3); + if (count <= 0) { + return []; + } + + return Array.from({ length: count }, method); +} diff --git a/src/modules/helpers/mustache.ts b/src/modules/helpers/mustache.ts new file mode 100644 index 00000000000..3ac3604d2c7 --- /dev/null +++ b/src/modules/helpers/mustache.ts @@ -0,0 +1,42 @@ +import type { FakerCore } from '../../core'; + +/** + * Replaces the `{{placeholder}}` patterns in the given string mustache style. + * + * @param fakerCore The FakerCore to use. + * @param text The template string to parse. + * @param data The data used to populate the placeholders. + * This is a record where the key is the template placeholder, + * whereas the value is either a string or a function suitable for `String.replace()`. + * + * @example + * mustache(fakerCore, 'I found {{count}} instances of "{{word}}".', { + * count: () => `${numberInt(fakerCore)}`, + * word: "this word", + * }) // 'I found 57591 instances of "this word".' + * + * @since 2.0.1 + */ +export function mustache( + fakerCore: FakerCore, + text: string | undefined, + data: Record[1]> +): string { + if (text == null) { + return ''; + } + + for (const p in data) { + const re = new RegExp(`{{${p}}}`, 'g'); + let value = data[p]; + if (typeof value === 'string') { + // escape $, source: https://stackoverflow.com/a/6969486/6897682 + value = value.replaceAll('$', '$$$$'); + text = text.replace(re, value); + } else { + text = text.replace(re, value); + } + } + + return text; +} diff --git a/src/modules/helpers/object-entry.ts b/src/modules/helpers/object-entry.ts new file mode 100644 index 00000000000..e3c398f5289 --- /dev/null +++ b/src/modules/helpers/object-entry.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { objectKey } from '../helpers/object-key'; + +/** + * Returns a random `[key, value]` pair from the given object. + * + * @template T The type of the object to select from. + * + * @param fakerCore The FakerCore to use. + * @param object The object to be used. + * + * @throws {FakerError} If the given object is empty. + * + * @example + * objectEntry(fakerCore, { Cheetah: 120, Falcon: 390, Snail: 0.03 }) // ['Snail', 0.03] + * + * @since 8.0.0 + */ +export function objectEntry>( + fakerCore: FakerCore, + object: T +): [keyof T, T[keyof T]] { + const key = objectKey(fakerCore, object); + return [key, object[key]]; +} diff --git a/src/modules/helpers/object-key.ts b/src/modules/helpers/object-key.ts new file mode 100644 index 00000000000..82b56b47703 --- /dev/null +++ b/src/modules/helpers/object-key.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random key from the given object. + * + * @template T The type of the object to select from. + * + * @param fakerCore The FakerCore to use. + * @param object The object to be used. + * + * @throws {FakerError} If the given object is empty. + * + * @example + * objectKey(fakerCore, { Cheetah: 120, Falcon: 390, Snail: 0.03 }) // 'Falcon' + * + * @since 6.3.0 + */ +export function objectKey>( + fakerCore: FakerCore, + object: T +): keyof T { + const array: Array = Object.keys(object); + return arrayElement(fakerCore, array); +} diff --git a/src/modules/helpers/object-value.ts b/src/modules/helpers/object-value.ts new file mode 100644 index 00000000000..ad66f2b2c88 --- /dev/null +++ b/src/modules/helpers/object-value.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { objectKey } from '../helpers/object-key'; + +/** + * Returns a random value from the given object. + * + * @template T The type of object to select from. + * + * @param fakerCore The FakerCore to use. + * @param object The object to be used. + * + * @throws {FakerError} If the given object is empty. + * + * @example + * objectValue(fakerCore, { Cheetah: 120, Falcon: 390, Snail: 0.03 }) // 390 + * + * @since 6.3.0 + */ +export function objectValue>( + fakerCore: FakerCore, + object: T +): T[keyof T] { + const key = objectKey(fakerCore, object); + return object[key]; +} diff --git a/src/modules/helpers/range-to-number.ts b/src/modules/helpers/range-to-number.ts new file mode 100644 index 00000000000..a2e0670fbaa --- /dev/null +++ b/src/modules/helpers/range-to-number.ts @@ -0,0 +1,38 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Helper method that converts the given number or range to a number. + * + * @param fakerCore The FakerCore to use. + * @param numberOrRange The number or range to convert. + * @param numberOrRange.min The minimum value for the range. + * @param numberOrRange.max The maximum value for the range. + * + * @example + * rangeToNumber(fakerCore, 1) // 1 + * rangeToNumber(fakerCore, { min: 1, max: 10 }) // 5 + * + * @since 8.0.0 + */ +export function rangeToNumber( + fakerCore: FakerCore, + numberOrRange: + | number + | { + /** + * The minimum value for the range. + */ + min: number; + /** + * The maximum value for the range. + */ + max: number; + } +): number { + if (typeof numberOrRange === 'number') { + return numberOrRange; + } + + return int(fakerCore, numberOrRange); +} diff --git a/src/modules/helpers/replace-credit-card-symbols.ts b/src/modules/helpers/replace-credit-card-symbols.ts new file mode 100644 index 00000000000..c0102774481 --- /dev/null +++ b/src/modules/helpers/replace-credit-card-symbols.ts @@ -0,0 +1,160 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; +import { luhnCheckValue } from './_luhn-check'; + +/** + * Replaces the regex like expressions in the given string with matching values. + * + * Note: This method will be removed in v9. + * + * Supported patterns: + * - `.{times}` => Repeat the character exactly `times` times. + * - `.{min,max}` => Repeat the character `min` to `max` times. + * - `[min-max]` => Generate a number between min and max (inclusive). + * + * @internal + * + * @param fakerCore The Faker instance to use. + * @param string The template string to parse. + * + * @example + * legacyRegexpStringParse(fakerCore) // '' + * legacyRegexpStringParse(fakerCore, '#{5}') // '#####' + * legacyRegexpStringParse(fakerCore, '#{2,9}') // '#######' + * legacyRegexpStringParse(fakerCore, '[500-15000]') // '8375' + * legacyRegexpStringParse(fakerCore, '#{3}test[1-5]') // '###test3' + * + * @since 5.0.0 + */ +function legacyRegexpStringParse( + fakerCore: FakerCore, + string: string = '' +): string { + // Deal with range repeat `{min,max}` + const RANGE_REP_REG = /(.)\{(\d+),(\d+)\}/; + const REP_REG = /(.)\{(\d+)\}/; + const RANGE_REG = /\[(\d+)-(\d+)\]/; + let min: number; + let max: number; + let tmp: number; + let repetitions: number; + let token = RANGE_REP_REG.exec(string); + while (token != null) { + min = Number.parseInt(token[2]); + max = Number.parseInt(token[3]); + // switch min and max + if (min > max) { + tmp = max; + max = min; + min = tmp; + } + + repetitions = int(fakerCore, { min, max }); + string = + string.slice(0, token.index) + + token[1].repeat(repetitions) + + string.slice(token.index + token[0].length); + token = RANGE_REP_REG.exec(string); + } + + // Deal with repeat `{num}` + token = REP_REG.exec(string); + while (token != null) { + repetitions = Number.parseInt(token[2]); + string = + string.slice(0, token.index) + + token[1].repeat(repetitions) + + string.slice(token.index + token[0].length); + token = REP_REG.exec(string); + } + // Deal with range `[min-max]` (only works with numbers for now) + + token = RANGE_REG.exec(string); + while (token != null) { + min = Number.parseInt(token[1]); // This time we are not capturing the char before `[]` + max = Number.parseInt(token[2]); + // switch min and max + if (min > max) { + tmp = max; + max = min; + min = tmp; + } + + string = + string.slice(0, token.index) + + int(fakerCore, { min, max }).toString() + + string.slice(token.index + token[0].length); + token = RANGE_REG.exec(string); + } + + return string; +} + +/** + * Parses the given string symbol by symbol and replaces the placeholders with digits (`0` - `9`). + * `!` will be replaced by digits >=2 (`2` - `9`). + * + * Note: This method will be removed in v9. + * + * @internal + * + * @param fakerCore The Faker instance to use. + * @param string The template string to parse. Defaults to `''`. + * @param symbol The symbol to replace with digits. Defaults to `'#'`. + * + * @example + * legacyReplaceSymbolWithNumber(fakerCore) // '' + * legacyReplaceSymbolWithNumber(fakerCore, '#####') // '04812' + * legacyReplaceSymbolWithNumber(fakerCore, '!####') // '27378' + * legacyReplaceSymbolWithNumber(fakerCore, 'Your pin is: !####') // '29841' + * + * @since 8.4.0 + */ +export function legacyReplaceSymbolWithNumber( + fakerCore: FakerCore, + string: string = '', + symbol: string = '#' +): string { + let result = ''; + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === symbol) { + result += int(fakerCore, 9); + } else if (string.charAt(i) === '!') { + result += int(fakerCore, { min: 2, max: 9 }); + } else { + result += string.charAt(i); + } + } + + return result; +} + +/** + * Replaces the symbols and patterns in a credit card schema including Luhn checksum. + * + * This method supports both range patterns `[4-9]` as well as the patterns used by `replaceSymbolWithNumber()`. + * `L` will be replaced with the appropriate Luhn checksum. + * + * @param fakerCore The FakerCore to use. + * @param string The credit card format pattern. Defaults to `'6453-####-####-####-###L'`. + * @param symbol The symbol to replace with a digit. Defaults to `'#'`. + * + * @example + * replaceCreditCardSymbols(fakerCore) // '6453-4876-8626-8995-3771' + * replaceCreditCardSymbols(fakerCore, '1234-[4-9]-##!!-L') // '1234-9-5298-2' + * + * @since 5.0.0 + */ +export function replaceCreditCardSymbols( + fakerCore: FakerCore, + string: string = '6453-####-####-####-###L', + symbol: string = '#' +): string { + // default values required for calling method without arguments + + string = legacyRegexpStringParse(fakerCore, string); // replace [4-9] with a random number in range etc... + string = legacyReplaceSymbolWithNumber(fakerCore, string, symbol); // replace ### with random numbers + + const checkNum = luhnCheckValue(string); + return string.replace('L', String(checkNum)); +} diff --git a/src/modules/helpers/replace-symbols.ts b/src/modules/helpers/replace-symbols.ts new file mode 100644 index 00000000000..ff7e60021eb --- /dev/null +++ b/src/modules/helpers/replace-symbols.ts @@ -0,0 +1,74 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; + +/** + * Parses the given string symbol by symbol and replaces the placeholder appropriately. + * + * - `#` will be replaced with a digit (`0` - `9`). + * - `?` will be replaced with an upper letter ('A' - 'Z') + * - and `*` will be replaced with either a digit or letter. + * + * @param fakerCore The FakerCore to use. + * @param string The template string to parse. Defaults to `''`. + * + * @example + * replaceSymbols(fakerCore) // '' + * replaceSymbols(fakerCore, '#####') // '98441' + * replaceSymbols(fakerCore, '?????') // 'ZYRQQ' + * replaceSymbols(fakerCore, '*****') // '4Z3P7' + * replaceSymbols(fakerCore, 'Your pin is: #?*#?*') // 'Your pin is: 0T85L1' + * + * @since 3.0.0 + */ +export function replaceSymbols( + fakerCore: FakerCore, + string: string = '' +): string { + const alpha = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + ]; + let result = ''; + + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === '#') { + result += int(fakerCore, 9); + } else if (string.charAt(i) === '?') { + result += arrayElement(fakerCore, alpha); + } else if (string.charAt(i) === '*') { + result += boolean(fakerCore) + ? arrayElement(fakerCore, alpha) + : int(fakerCore, 9); + } else { + result += string.charAt(i); + } + } + + return result; +} diff --git a/src/modules/helpers/shuffle.ts b/src/modules/helpers/shuffle.ts new file mode 100644 index 00000000000..8450190873f --- /dev/null +++ b/src/modules/helpers/shuffle.ts @@ -0,0 +1,106 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Takes an array and randomizes it in place then returns it. + * + * @template T The type of the elements to shuffle. + * + * @param fakerCore The FakerCore to use. + * @param list The array to shuffle. + * @param options The options to use when shuffling. + * @param options.inplace Whether to shuffle the array in place or return a new array. Defaults to `false`. + * + * @example + * shuffle(fakerCore, ['a', 'b', 'c'], { inplace: true }) // [ 'b', 'c', 'a' ] + * + * @since 8.0.0 + */ +export function shuffle( + fakerCore: FakerCore, + list: T[], + options: { + /** + * Whether to shuffle the array in place or return a new array. + * + * @default false + */ + inplace: true; + } +): T[]; +/** + * Returns a randomized version of the array. + * + * @template T The type of the elements to shuffle. + * + * @param fakerCore The FakerCore to use. + * @param list The array to shuffle. + * @param options The options to use when shuffling. + * @param options.inplace Whether to shuffle the array in place or return a new array. Defaults to `false`. + * + * @example + * shuffle(fakerCore, ['a', 'b', 'c']) // [ 'b', 'c', 'a' ] + * shuffle(fakerCore, ['a', 'b', 'c'], { inplace: false }) // [ 'b', 'c', 'a' ] + * + * @since 2.0.1 + */ +// @ts-expect-error TS2394 -- Implementation cannot fullfil the readonly array part, since it needs to comply with the inplace version of the function. +export function shuffle( + fakerCore: FakerCore, + list: ReadonlyArray, + options?: { + /** + * Whether to shuffle the array in place or return a new array. + * + * @default false + */ + inplace?: false; + } +): T[]; +/** + * Returns a randomized version of the array. + * + * @template T The type of the elements to shuffle. + * + * @param fakerCore The FakerCore to use. + * @param list The array to shuffle. + * @param options The options to use when shuffling. + * @param options.inplace Whether to shuffle the array in place or return a new array. Defaults to `false`. + * + * @example + * shuffle(fakerCore, ['a', 'b', 'c']) // [ 'b', 'c', 'a' ] + * shuffle(fakerCore, ['a', 'b', 'c'], { inplace: true }) // [ 'b', 'c', 'a' ] + * shuffle(fakerCore, ['a', 'b', 'c'], { inplace: false }) // [ 'b', 'c', 'a' ] + * + * @since 2.0.1 + */ +export function shuffle( + fakerCore: FakerCore, + list: T[], + options?: { + /** + * Whether to shuffle the array in place or return a new array. + * + * @default false + */ + inplace?: boolean; + } +): T[]; +export function shuffle( + fakerCore: FakerCore, + list: T[], + options: { inplace?: boolean } = {} +): T[] { + const { inplace = false } = options; + + if (!inplace) { + list = [...list]; + } + + for (let i = list.length - 1; i > 0; --i) { + const j = int(fakerCore, i); + [list[i], list[j]] = [list[j], list[i]]; + } + + return list; +} diff --git a/src/modules/helpers/slugify.ts b/src/modules/helpers/slugify.ts new file mode 100644 index 00000000000..eb5b31c51ed --- /dev/null +++ b/src/modules/helpers/slugify.ts @@ -0,0 +1,23 @@ +import type { FakerCore } from '../../core'; + +/** + * Slugifies the given string. + * For that all spaces (` `) are replaced by hyphens (`-`) + * and most non word characters except for dots and hyphens will be removed. + * + * @param fakerCore The FakerCore to use. + * @param string The input to slugify. Defaults to `''`. + * + * @example + * slugify(fakerCore) // '' + * slugify(fakerCore, "Hello world!") // 'Hello-world' + * + * @since 2.0.1 + */ +export function slugify(fakerCore: FakerCore, string: string = ''): string { + return string + .normalize('NFKD') //for example è decomposes to as e + ̀ + .replaceAll(/[\u0300-\u036F]/g, '') // removes combining marks + .replaceAll(' ', '-') // replaces spaces with hyphens + .replaceAll(/[^\w.-]+/g, ''); // removes all non-word characters except for dots and hyphens +} diff --git a/src/modules/helpers/unique-array.ts b/src/modules/helpers/unique-array.ts new file mode 100644 index 00000000000..7941f93cb1c --- /dev/null +++ b/src/modules/helpers/unique-array.ts @@ -0,0 +1,53 @@ +import type { FakerCore } from '../../core'; +import { shuffle } from '../helpers/shuffle'; + +/** + * Takes an array of strings or function that returns a string + * and outputs a unique array of strings based on that source. + * This method does not store the unique state between invocations. + * + * If there are not enough unique values to satisfy the length, if + * the source is an array, it will only return as many items as are + * in the array. If the source is a function, it will return after + * a maximum number of attempts has been reached. + * + * @template T The type of the elements. + * + * @param fakerCore The FakerCore to use. + * @param source The strings to choose from or a function that generates a string. + * @param length The number of elements to generate. + * + * @example + * uniqueArray(fakerCore, faker.word.sample, 3) // ['mob', 'junior', 'ripe'] + * uniqueArray(fakerCore, faker.definitions.person.first_name.generic, 6) // ['Silas', 'Montana', 'Lorenzo', 'Alayna', 'Aditya', 'Antone'] + * uniqueArray(fakerCore, ["Hello", "World", "Goodbye"], 2) // ['World', 'Goodbye'] + * + * @since 6.0.0 + */ +export function uniqueArray( + fakerCore: FakerCore, + source: ReadonlyArray | (() => T), + length: number +): T[] { + if (Array.isArray(source)) { + const set = new Set(source); + const array = [...set]; + return shuffle(fakerCore, array).splice(0, length); + } + + const set = new Set(); + try { + if (typeof source === 'function') { + const maxAttempts = 1000 * length; + let attempts = 0; + while (set.size < length && attempts < maxAttempts) { + set.add(source()); + attempts++; + } + } + } catch { + // Ignore + } + + return [...set]; +} diff --git a/src/modules/helpers/weighted-array-element.ts b/src/modules/helpers/weighted-array-element.ts new file mode 100644 index 00000000000..ad02a234ec9 --- /dev/null +++ b/src/modules/helpers/weighted-array-element.ts @@ -0,0 +1,66 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { float } from '../number/float'; + +/** + * Returns a weighted random element from the given array. Each element of the array should be an object with two keys `weight` and `value`. + * + * - Each `weight` key should be a number representing the probability of selecting the value, relative to the sum of the weights. Weights can be any positive float or integer. + * - Each `value` key should be the corresponding value. + * + * For example, if there are two values A and B, with weights 1 and 2 respectively, then the probability of picking A is 1/3 and the probability of picking B is 2/3. + * + * @template T The type of the elements to pick from. + * + * @param fakerCore The FakerCore to use. + * @param array Array to pick the value from. + * @param array[].weight The weight of the value. + * @param array[].value The value to pick. + * + * @example + * weightedArrayElement(fakerCore, [{ weight: 5, value: 'sunny' }, { weight: 4, value: 'rainy' }, { weight: 1, value: 'snowy' }]) // 'sunny', 50% of the time, 'rainy' 40% of the time, 'snowy' 10% of the time + * + * @since 8.0.0 + */ +export function weightedArrayElement( + fakerCore: FakerCore, + array: ReadonlyArray<{ + /** + * The weight of the value. + */ + weight: number; + /** + * The value to pick. + */ + value: T; + }> +): T { + if (array.length === 0) { + throw new FakerError( + 'weightedArrayElement expects an array with at least one element' + ); + } + + if (!array.every((elt) => elt.weight > 0)) { + throw new FakerError( + 'weightedArrayElement expects an array of { weight, value } objects where weight is a positive number' + ); + } + + const total = array.reduce((sum, { weight }) => sum + weight, 0); + const random = float(fakerCore, { + min: 0, + max: total, + }); + let current = 0; + for (const { weight, value } of array) { + current += weight; + if (random < current) { + return value; + } + } + + // In case of rounding errors, return the last element + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return array.at(-1)!.value; +} diff --git a/src/modules/number/big-int.ts b/src/modules/number/big-int.ts new file mode 100644 index 00000000000..b9e66bc97a5 --- /dev/null +++ b/src/modules/number/big-int.ts @@ -0,0 +1,102 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { numeric } from '../string/numeric'; + +/** + * Returns a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#bigint_type) number. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated bigint. Defaults to `0n`. + * @param options.max Upper bound for generated bigint. Defaults to `min + 999999999999999n`. + * @param options.multipleOf The generated bigint will be a multiple of this parameter. Defaults to `1n`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When there are no suitable bigint between `min` and `max`. + * @throws {FakerError} When `multipleOf` is not a positive bigint. + * + * @example + * bigInt(fakerCore) // 55422n + * bigInt(fakerCore, 100n) // 52n + * bigInt(fakerCore, { min: 1000000n }) // 431433n + * bigInt(fakerCore, { max: 100n }) // 42n + * bigInt(fakerCore, { multipleOf: 7n }) // 35n + * bigInt(fakerCore, { min: 10n, max: 100n }) // 36n + * + * @since 8.0.0 + */ +export function bigInt( + fakerCore: FakerCore, + options: + | bigint + | number + | string + | boolean + | { + /** + * Lower bound for generated bigint. + * + * @default 0n + */ + min?: bigint | number | string | boolean; + /** + * Upper bound for generated bigint. + * + * @default min + 999999999999999n + */ + max?: bigint | number | string | boolean; + /** + * The generated bigint will be a multiple of this parameter. + * + * @default 1n + */ + multipleOf?: bigint | number | string | boolean; + } = {} +): bigint { + if ( + typeof options === 'bigint' || + typeof options === 'number' || + typeof options === 'string' || + typeof options === 'boolean' + ) { + options = { + max: options, + }; + } + + const min = BigInt(options.min ?? 0); + const max = BigInt(options.max ?? min + BigInt(999999999999999)); + const multipleOf = BigInt(options.multipleOf ?? 1); + + if (max < min) { + throw new FakerError(`Max ${max} should be larger than min ${min}.`); + } + + if (multipleOf <= BigInt(0)) { + throw new FakerError(`multipleOf should be greater than 0.`); + } + + const effectiveMin = min / multipleOf + (min % multipleOf > 0n ? 1n : 0n); // Math.ceil(min / multipleOf) + const effectiveMax = max / multipleOf - (max % multipleOf < 0n ? 1n : 0n); // Math.floor(max / multipleOf) + + if (effectiveMin === effectiveMax) { + return effectiveMin * multipleOf; + } + + if (effectiveMax < effectiveMin) { + throw new FakerError( + `No suitable bigint value between ${min} and ${max} found.` + ); + } + + const delta = effectiveMax - effectiveMin + 1n; // +1 for inclusive max bounds and even distribution + const offset = + BigInt( + numeric(fakerCore, { + length: delta.toString(10).length, + allowLeadingZeros: true, + }) + ) % delta; + return (effectiveMin + offset) * multipleOf; +} diff --git a/src/modules/number/binary.ts b/src/modules/number/binary.ts new file mode 100644 index 00000000000..c7cb680ce54 --- /dev/null +++ b/src/modules/number/binary.ts @@ -0,0 +1,54 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Returns a [binary](https://en.wikipedia.org/wiki/Binary_number) number. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `1`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When there are no integers between `min` and `max`. + * + * @see stringBinary(fakerCore): For generating a `binary string` with a given length (range). + * + * @example + * binary(fakerCore) // '1' + * binary(fakerCore, 255) // '110101' + * binary(fakerCore, { min: 0, max: 65535 }) // '10110101' + * + * @since 8.0.0 + */ +export function binary( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number. + * + * @default 0 + */ + min?: number; + /** + * Upper bound for generated number. + * + * @default 1 + */ + max?: number; + } = {} +): string { + if (typeof options === 'number') { + options = { max: options }; + } + + const { min = 0, max = 1 } = options; + + return int(fakerCore, { + max, + min, + }).toString(2); +} diff --git a/src/modules/number/float.ts b/src/modules/number/float.ts new file mode 100644 index 00000000000..5e8a8a0a683 --- /dev/null +++ b/src/modules/number/float.ts @@ -0,0 +1,127 @@ +import type { FakerCore } from '../../core'; +import type { Distributor } from '../../distributors/distributor'; +import { uniformDistributor } from '../../distributors/uniform'; +import { FakerError } from '../../errors/faker-error'; +import { int as numberInt } from '../number/int'; + +/** + * Returns a single random floating-point number, by default between `0.0` and `1.0`. To change the range, pass a `min` and `max` value. To limit the number of decimal places, pass a `multipleOf` or `fractionDigits` parameter. + * + * @param fakerCore The FakerCore to use. + * @param options Upper bound or options object. + * @param options.min Lower bound for generated number, inclusive. Defaults to `0.0`. + * @param options.max Upper bound for generated number, exclusive, unless `multipleOf` or `fractionDigits` are passed. Defaults to `1.0`. + * @param options.multipleOf The generated number will be a multiple of this parameter. Only one of `multipleOf` or `fractionDigits` should be passed. + * @param options.fractionDigits The maximum number of digits to appear after the decimal point, for example `2` will round to 2 decimal points. Only one of `multipleOf` or `fractionDigits` should be passed. + * @param options.distributor A function to determine the distribution of generated values. Defaults to `uniformDistributor()`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When `multipleOf` is not a positive number. + * @throws {FakerError} When `fractionDigits` is negative. + * @throws {FakerError} When `fractionDigits` and `multipleOf` is passed in the same options object. + * + * @example + * float(fakerCore) // 0.5688541042618454 + * float(fakerCore, 3) // 2.367973240558058 + * float(fakerCore, { max: 100 }) // 17.3687307164073 + * float(fakerCore, { min: 20, max: 30 }) // 23.94764115102589 + * float(fakerCore, { multipleOf: 0.25, min: 0, max:10 }) // 7.75 + * float(fakerCore, { fractionDigits: 1 }) // 0.9 + * float(fakerCore, { min: 10, max: 100, multipleOf: 0.02 }) // 35.42 + * float(fakerCore, { min: 10, max: 100, fractionDigits: 3 }) // 65.716 + * float(fakerCore, { min: 10, max: 100, multipleOf: 0.001 }) // 65.716 - same as above + * + * @since 8.0.0 + */ +export function float( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number, inclusive. + * + * @default 0.0 + */ + min?: number; + /** + * Upper bound for generated number, exclusive, unless `multipleOf` or `fractionDigits` are passed. + * + * @default 1.0 + */ + max?: number; + /** + * 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. + */ + fractionDigits?: number; + /** + * 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') { + options = { + max: options, + }; + } + + const { + min = 0, + max = 1, + fractionDigits, + multipleOf: originalMultipleOf, + multipleOf = fractionDigits == null ? undefined : 10 ** -fractionDigits, + distributor = uniformDistributor(), + } = options; + + if (max < min) { + throw new FakerError(`Max ${max} should be greater than min ${min}.`); + } + + if (fractionDigits != null) { + if (originalMultipleOf != null) { + throw new FakerError( + 'multipleOf and fractionDigits cannot be set at the same time.' + ); + } + + if (!Number.isInteger(fractionDigits)) { + throw new FakerError('fractionDigits should be an integer.'); + } + + if (fractionDigits < 0) { + throw new FakerError( + 'fractionDigits should be greater than or equal to 0.' + ); + } + } + + if (multipleOf != null) { + if (multipleOf <= 0) { + throw new FakerError(`multipleOf should be greater than 0.`); + } + + const logPrecision = Math.log10(multipleOf); + // Workaround to get integer values for the inverse of all multiples of the form 10^-n + const factor = + multipleOf < 1 && Number.isInteger(logPrecision) + ? 10 ** -logPrecision + : 1 / multipleOf; + const int = numberInt(fakerCore, { + min: min * factor, + max: max * factor, + distributor, + }); + return int / factor; + } + + const real = distributor(fakerCore.randomizer); + return real * (max - min) + min; +} diff --git a/src/modules/number/hex.ts b/src/modules/number/hex.ts new file mode 100644 index 00000000000..322ef06c63e --- /dev/null +++ b/src/modules/number/hex.ts @@ -0,0 +1,52 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Returns a lowercase [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) number. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `15`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When there are no integers between `min` and `max`. + * + * @example + * hex(fakerCore) // 'b' + * hex(fakerCore, 255) // '9d' + * hex(fakerCore, { min: 0, max: 65535 }) // 'af17' + * + * @since 8.0.0 + */ +export function hex( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number. + * + * @default 0 + */ + min?: number; + /** + * Upper bound for generated number. + * + * @default 15 + */ + max?: number; + } = {} +): string { + if (typeof options === 'number') { + options = { max: options }; + } + + const { min = 0, max = 15 } = options; + + return int(fakerCore, { + max, + min, + }).toString(16); +} diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index a694c8e3d94..0ddf44a57cf 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -1,7 +1,12 @@ import type { Distributor } from '../../distributors/distributor'; -import { uniformDistributor } from '../../distributors/uniform'; -import { FakerError } from '../../errors/faker-error'; import { SimpleModuleBase } from '../../internal/module-base'; +import { bigInt as numberBigInt } from './big-int'; +import { binary as numberBinary } from './binary'; +import { float as numberFloat } from './float'; +import { hex as numberHex } from './hex'; +import { int as numberInt } from './int'; +import { octal as numberOctal } from './octal'; +import { romanNumeral as numberRomanNumeral } from './roman-numeral'; /** * Module to generate numbers of any kind. @@ -74,45 +79,7 @@ export class NumberModule extends SimpleModuleBase { distributor?: Distributor; } = {} ): number { - if (typeof options === 'number') { - options = { max: 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.`); - } - - if (multipleOf <= 0) { - throw new FakerError(`multipleOf should be greater than 0.`); - } - - const effectiveMin = Math.ceil(min / multipleOf); - const effectiveMax = Math.floor(max / multipleOf); - - if (effectiveMin === effectiveMax) { - return effectiveMin * multipleOf; - } - - if (effectiveMax < effectiveMin) { - if (max >= min) { - throw new FakerError( - `No suitable integer value between ${min} and ${max} found.` - ); - } - - throw new FakerError(`Max ${max} should be greater than min ${min}.`); - } - - const real = distributor(this.faker.fakerCore.randomizer); - const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution - return Math.floor(real * delta + effectiveMin) * multipleOf; + return numberInt(this.faker.fakerCore, options); } /** @@ -175,64 +142,7 @@ export class NumberModule extends SimpleModuleBase { distributor?: Distributor; } = {} ): number { - if (typeof options === 'number') { - options = { - max: options, - }; - } - - const { - min = 0, - max = 1, - fractionDigits, - multipleOf: originalMultipleOf, - multipleOf = fractionDigits == null ? undefined : 10 ** -fractionDigits, - distributor = uniformDistributor(), - } = options; - - if (max < min) { - throw new FakerError(`Max ${max} should be greater than min ${min}.`); - } - - if (fractionDigits != null) { - if (originalMultipleOf != null) { - throw new FakerError( - 'multipleOf and fractionDigits cannot be set at the same time.' - ); - } - - if (!Number.isInteger(fractionDigits)) { - throw new FakerError('fractionDigits should be an integer.'); - } - - if (fractionDigits < 0) { - throw new FakerError( - 'fractionDigits should be greater than or equal to 0.' - ); - } - } - - if (multipleOf != null) { - if (multipleOf <= 0) { - throw new FakerError(`multipleOf should be greater than 0.`); - } - - const logPrecision = Math.log10(multipleOf); - // Workaround to get integer values for the inverse of all multiples of the form 10^-n - const factor = - multipleOf < 1 && Number.isInteger(logPrecision) - ? 10 ** -logPrecision - : 1 / multipleOf; - const int = this.int({ - min: min * factor, - max: max * factor, - distributor, - }); - return int / factor; - } - - const real = distributor(this.faker.fakerCore.randomizer); - return real * (max - min) + min; + return numberFloat(this.faker.fakerCore, options); } /** @@ -273,16 +183,7 @@ export class NumberModule extends SimpleModuleBase { max?: number; } = {} ): string { - if (typeof options === 'number') { - options = { max: options }; - } - - const { min = 0, max = 1 } = options; - - return this.int({ - max, - min, - }).toString(2); + return numberBinary(this.faker.fakerCore, options); } /** @@ -323,16 +224,7 @@ export class NumberModule extends SimpleModuleBase { max?: number; } = {} ): string { - if (typeof options === 'number') { - options = { max: options }; - } - - const { min = 0, max = 7 } = options; - - return this.int({ - max, - min, - }).toString(8); + return numberOctal(this.faker.fakerCore, options); } /** @@ -371,16 +263,7 @@ export class NumberModule extends SimpleModuleBase { max?: number; } = {} ): string { - if (typeof options === 'number') { - options = { max: options }; - } - - const { min = 0, max = 15 } = options; - - return this.int({ - max, - min, - }).toString(16); + return numberHex(this.faker.fakerCore, options); } /** @@ -433,51 +316,7 @@ export class NumberModule extends SimpleModuleBase { multipleOf?: bigint | number | string | boolean; } = {} ): bigint { - if ( - typeof options === 'bigint' || - typeof options === 'number' || - typeof options === 'string' || - typeof options === 'boolean' - ) { - options = { - max: options, - }; - } - - const min = BigInt(options.min ?? 0); - const max = BigInt(options.max ?? min + BigInt(999999999999999)); - const multipleOf = BigInt(options.multipleOf ?? 1); - - if (max < min) { - throw new FakerError(`Max ${max} should be larger than min ${min}.`); - } - - if (multipleOf <= BigInt(0)) { - throw new FakerError(`multipleOf should be greater than 0.`); - } - - const effectiveMin = min / multipleOf + (min % multipleOf > 0n ? 1n : 0n); // Math.ceil(min / multipleOf) - const effectiveMax = max / multipleOf - (max % multipleOf < 0n ? 1n : 0n); // Math.floor(max / multipleOf) - - if (effectiveMin === effectiveMax) { - return effectiveMin * multipleOf; - } - - if (effectiveMax < effectiveMin) { - throw new FakerError( - `No suitable bigint value between ${min} and ${max} found.` - ); - } - - const delta = effectiveMax - effectiveMin + 1n; // +1 for inclusive max bounds and even distribution - const offset = - BigInt( - this.faker.string.numeric({ - length: delta.toString(10).length, - allowLeadingZeros: true, - }) - ) % delta; - return (effectiveMin + offset) * multipleOf; + return numberBigInt(this.faker.fakerCore, options); } /** @@ -520,54 +359,6 @@ export class NumberModule extends SimpleModuleBase { max?: number; } = {} ): string { - const DEFAULT_MIN = 1; - const DEFAULT_MAX = 3999; - - if (typeof options === 'number') { - options = { - max: options, - }; - } - - const { min = DEFAULT_MIN, max = DEFAULT_MAX } = options; - - if (min < DEFAULT_MIN) { - throw new FakerError( - `Min value ${min} should be ${DEFAULT_MIN} or greater.` - ); - } - - if (max > DEFAULT_MAX) { - throw new FakerError( - `Max value ${max} should be ${DEFAULT_MAX} or less.` - ); - } - - let num = this.int({ min, max }); - - const lookup: Array<[string, number]> = [ - ['M', 1000], - ['CM', 900], - ['D', 500], - ['CD', 400], - ['C', 100], - ['XC', 90], - ['L', 50], - ['XL', 40], - ['X', 10], - ['IX', 9], - ['V', 5], - ['IV', 4], - ['I', 1], - ]; - - let result = ''; - - for (const [k, v] of lookup) { - result += k.repeat(Math.floor(num / v)); - num %= v; - } - - return result; + return numberRomanNumeral(this.faker.fakerCore, options); } } diff --git a/src/modules/number/int.ts b/src/modules/number/int.ts new file mode 100644 index 00000000000..d6b8bc20562 --- /dev/null +++ b/src/modules/number/int.ts @@ -0,0 +1,103 @@ +import type { FakerCore } from '../../core'; +import type { Distributor } from '../../distributors/distributor'; +import { uniformDistributor } from '../../distributors/uniform'; +import { FakerError } from '../../errors/faker-error'; + +/** + * Returns a single random integer between zero and the given max value or the given range. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `Number.MAX_SAFE_INTEGER`. + * @param options.multipleOf Generated number will be a multiple of the given integer. Defaults to `1`. + * @param options.distributor A function to determine the distribution of generated values. Defaults to `uniformDistributor()`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When there are no suitable integers between `min` and `max`. + * @throws {FakerError} When `multipleOf` is not a positive integer. + * + * @see stringNumeric(fakerCore): For generating a `string` of digits with a given length (range). + * + * @example + * int(fakerCore) // 2900970162509863 + * int(fakerCore, 100) // 52 + * int(fakerCore, { min: 1000000 }) // 2900970162509863 + * int(fakerCore, { max: 100 }) // 42 + * int(fakerCore, { min: 10, max: 100 }) // 57 + * int(fakerCore, { min: 10, max: 100, multipleOf: 10 }) // 50 + * + * @since 8.0.0 + */ +export function int( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number. + * + * @default 0 + */ + min?: number; + /** + * Upper bound for generated number. + * + * @default Number.MAX_SAFE_INTEGER + */ + max?: number; + /** + * Generated number will be a multiple of the given integer. + * + * @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, + distributor = uniformDistributor(), + } = options; + + if (!Number.isInteger(multipleOf)) { + throw new FakerError(`multipleOf should be an integer.`); + } + + if (multipleOf <= 0) { + throw new FakerError(`multipleOf should be greater than 0.`); + } + + const effectiveMin = Math.ceil(min / multipleOf); + const effectiveMax = Math.floor(max / multipleOf); + + if (effectiveMin === effectiveMax) { + return effectiveMin * multipleOf; + } + + if (effectiveMax < effectiveMin) { + if (max >= min) { + throw new FakerError( + `No suitable integer value between ${min} and ${max} found.` + ); + } + + throw new FakerError(`Max ${max} should be greater than min ${min}.`); + } + + const real = distributor(fakerCore.randomizer); + const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution + return Math.floor(real * delta + effectiveMin) * multipleOf; +} diff --git a/src/modules/number/octal.ts b/src/modules/number/octal.ts new file mode 100644 index 00000000000..2179e4fb683 --- /dev/null +++ b/src/modules/number/octal.ts @@ -0,0 +1,54 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Returns an [octal](https://en.wikipedia.org/wiki/Octal) number. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `7`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When there are no integers between `min` and `max`. + * + * @see stringOctal(fakerCore): For generating an `octal string` with a given length (range). + * + * @example + * octal(fakerCore) // '5' + * octal(fakerCore, 255) // '377' + * octal(fakerCore, { min: 0, max: 65535 }) // '4766' + * + * @since 8.0.0 + */ +export function octal( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number. + * + * @default 0 + */ + min?: number; + /** + * Upper bound for generated number. + * + * @default 7 + */ + max?: number; + } = {} +): string { + if (typeof options === 'number') { + options = { max: options }; + } + + const { min = 0, max = 7 } = options; + + return int(fakerCore, { + max, + min, + }).toString(8); +} diff --git a/src/modules/number/roman-numeral.ts b/src/modules/number/roman-numeral.ts new file mode 100644 index 00000000000..257d4e826d2 --- /dev/null +++ b/src/modules/number/roman-numeral.ts @@ -0,0 +1,94 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { int } from '../number/int'; + +/** + * Returns a roman numeral in String format. + * The bounds are inclusive. + * + * @param fakerCore The FakerCore to use. + * @param options Maximum value or options object. + * @param options.min Lower bound for generated roman numerals. Defaults to `1`. + * @param options.max Upper bound for generated roman numerals. Defaults to `3999`. + * + * @throws {FakerError} When `min` is greater than `max`. + * @throws {FakerError} When `min`, `max` is not a number. + * @throws {FakerError} When `min` is less than `1`. + * @throws {FakerError} When `max` is greater than `3999`. + * + * @example + * romanNumeral(fakerCore) // "CMXCIII" + * romanNumeral(fakerCore, 5) // "III" + * romanNumeral(fakerCore, { min: 10 }) // "XCIX" + * romanNumeral(fakerCore, { max: 20 }) // "XVII" + * romanNumeral(fakerCore, { min: 5, max: 10 }) // "VII" + * + * @since 9.2.0 + */ +export function romanNumeral( + fakerCore: FakerCore, + options: + | number + | { + /** + * Lower bound for generated number. + * + * @default 1 + */ + min?: number; + /** + * Upper bound for generated number. + * + * @default 3999 + */ + max?: number; + } = {} +): string { + const DEFAULT_MIN = 1; + const DEFAULT_MAX = 3999; + + if (typeof options === 'number') { + options = { + max: options, + }; + } + + const { min = DEFAULT_MIN, max = DEFAULT_MAX } = options; + + if (min < DEFAULT_MIN) { + throw new FakerError( + `Min value ${min} should be ${DEFAULT_MIN} or greater.` + ); + } + + if (max > DEFAULT_MAX) { + throw new FakerError(`Max value ${max} should be ${DEFAULT_MAX} or less.`); + } + + let num = int(fakerCore, { min, max }); + + const lookup: Array<[string, number]> = [ + ['M', 1000], + ['CM', 900], + ['D', 500], + ['CD', 400], + ['C', 100], + ['XC', 90], + ['L', 50], + ['XL', 40], + ['X', 10], + ['IX', 9], + ['V', 5], + ['IV', 4], + ['I', 1], + ]; + + let result = ''; + + for (const [k, v] of lookup) { + result += k.repeat(Math.floor(num / v)); + num %= v; + } + + return result; +} diff --git a/src/modules/phone/index.ts b/src/modules/phone/index.ts index c55bfeed6e7..eb554265979 100644 --- a/src/modules/phone/index.ts +++ b/src/modules/phone/index.ts @@ -1,5 +1,5 @@ import { ModuleBase } from '../../internal/module-base'; -import { legacyReplaceSymbolWithNumber } from '../helpers'; +import { legacyReplaceSymbolWithNumber } from '../helpers/replace-credit-card-symbols'; /** * Module to generate phone-related data. @@ -50,7 +50,7 @@ export class PhoneModule extends ModuleBase { } const format = this.faker.helpers.arrayElement(definitions); - return legacyReplaceSymbolWithNumber(this.faker, format); + return legacyReplaceSymbolWithNumber(this.faker.fakerCore, format); } /** diff --git a/src/modules/string/_types.ts b/src/modules/string/_types.ts new file mode 100644 index 00000000000..fa839700051 --- /dev/null +++ b/src/modules/string/_types.ts @@ -0,0 +1,78 @@ +export const UPPER_CHARS: ReadonlyArray = [ + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', +]; +export const LOWER_CHARS: ReadonlyArray = [ + ...'abcdefghijklmnopqrstuvwxyz', +]; +export const DIGIT_CHARS: ReadonlyArray = [...'0123456789']; + +export type LowerAlphaChar = + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z'; + +export type UpperAlphaChar = + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z'; + +export type NumericChar = + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9'; + +export type AlphaChar = LowerAlphaChar | UpperAlphaChar; +export type AlphaNumericChar = AlphaChar | NumericChar; diff --git a/src/modules/string/alpha.ts b/src/modules/string/alpha.ts new file mode 100644 index 00000000000..c0dedf17788 --- /dev/null +++ b/src/modules/string/alpha.ts @@ -0,0 +1,103 @@ +import type { FakerCore } from '../../core'; +import type { LiteralUnion } from '../../internal/types'; +import type { Casing } from '../../utils/types'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { fromCharacters } from '../string/from-characters'; +import type { AlphaChar } from './_types'; +import { LOWER_CHARS, UPPER_CHARS } from './_types'; + +/** + * Generating a string consisting of letters in the English alphabet. + * + * @param fakerCore The FakerCore to use. + * @param options Either the length of the string to generate or the optional options object. + * @param options.length The length of the string to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.casing The casing of the characters. Defaults to `'mixed'`. + * @param options.exclude An array with characters which should be excluded in the generated string. Defaults to `[]`. + * + * @example + * alpha(fakerCore) // 'b' + * alpha(fakerCore, 10) // 'fEcAaCVbaR' + * alpha(fakerCore, { length: { min: 5, max: 10 } }) // 'HcVrCf' + * alpha(fakerCore, { casing: 'lower' }) // 'r' + * alpha(fakerCore, { exclude: ['W'] }) // 'Z' + * alpha(fakerCore, { length: 5, casing: 'upper', exclude: ['A'] }) // 'DTCIC' + * + * @since 8.0.0 + */ +export function alpha( + fakerCore: FakerCore, + options: + | number + | { + /** + * The length of the string to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + }; + /** + * The casing of the characters. + * + * @default 'mixed' + */ + casing?: Casing; + /** + * An array with characters which should be excluded in the generated string. + * + * @default [] + */ + exclude?: ReadonlyArray> | string; + } = {} +): string { + if (typeof options === 'number') { + options = { + length: options, + }; + } + + const length = rangeToNumber(fakerCore, options.length ?? 1); + if (length <= 0) { + return ''; + } + + const { casing = 'mixed' } = options; + let { exclude = [] } = options; + + if (typeof exclude === 'string') { + exclude = [...exclude]; + } + + let charsArray: string[]; + switch (casing) { + case 'upper': { + charsArray = [...UPPER_CHARS]; + break; + } + + case 'lower': { + charsArray = [...LOWER_CHARS]; + break; + } + + case 'mixed': { + charsArray = [...LOWER_CHARS, ...UPPER_CHARS]; + break; + } + } + + charsArray = charsArray.filter((elem) => !exclude.includes(elem)); + + return fromCharacters(fakerCore, charsArray, length); +} diff --git a/src/modules/string/alphanumeric.ts b/src/modules/string/alphanumeric.ts new file mode 100644 index 00000000000..a3c8b2f4d52 --- /dev/null +++ b/src/modules/string/alphanumeric.ts @@ -0,0 +1,104 @@ +import type { FakerCore } from '../../core'; +import type { LiteralUnion } from '../../internal/types'; +import type { Casing } from '../../utils/types'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { fromCharacters } from '../string/from-characters'; +import type { AlphaNumericChar } from './_types'; +import { DIGIT_CHARS, LOWER_CHARS, UPPER_CHARS } from './_types'; + +/** + * Generating a string consisting of alpha characters and digits. + * + * @param fakerCore The FakerCore to use. + * @param options Either the length of the string to generate or the optional options object. + * @param options.length The length of the string to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.casing The casing of the characters. Defaults to `'mixed'`. + * @param options.exclude An array of characters and digits which should be excluded in the generated string. Defaults to `[]`. + * + * @example + * alphanumeric(fakerCore) // '2' + * alphanumeric(fakerCore, 5) // '3e5V7' + * alphanumeric(fakerCore, { length: { min: 5, max: 10 } }) // 'muaApG' + * alphanumeric(fakerCore, { casing: 'upper' }) // 'A' + * alphanumeric(fakerCore, { exclude: ['W'] }) // 'r' + * alphanumeric(fakerCore, { length: 5, exclude: ["a"] }) // 'x1Z7f' + * + * @since 8.0.0 + */ +export function alphanumeric( + fakerCore: FakerCore, + options: + | number + | { + /** + * The length of the string to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + }; + /** + * The casing of the characters. + * + * @default 'mixed' + */ + casing?: Casing; + /** + * An array of characters and digits which should be excluded in the generated string. + * + * @default [] + */ + exclude?: ReadonlyArray> | string; + } = {} +): string { + if (typeof options === 'number') { + options = { + length: options, + }; + } + + const length = rangeToNumber(fakerCore, options.length ?? 1); + if (length <= 0) { + return ''; + } + + const { casing = 'mixed' } = options; + let { exclude = [] } = options; + + if (typeof exclude === 'string') { + exclude = [...exclude]; + } + + let charsArray = [...DIGIT_CHARS]; + + switch (casing) { + case 'upper': { + charsArray.push(...UPPER_CHARS); + break; + } + + case 'lower': { + charsArray.push(...LOWER_CHARS); + break; + } + + case 'mixed': { + charsArray.push(...LOWER_CHARS, ...UPPER_CHARS); + break; + } + } + + charsArray = charsArray.filter((elem) => !exclude.includes(elem)); + + return fromCharacters(fakerCore, charsArray, length); +} diff --git a/src/modules/string/binary.ts b/src/modules/string/binary.ts new file mode 100644 index 00000000000..91740ec2f19 --- /dev/null +++ b/src/modules/string/binary.ts @@ -0,0 +1,56 @@ +import type { FakerCore } from '../../core'; +import { fromCharacters } from '../string/from-characters'; + +/** + * Returns a [binary](https://en.wikipedia.org/wiki/Binary_number) string. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.length The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.prefix Prefix for the generated number. Defaults to `'0b'`. + * + * @see numberBinary(fakerCore): For generating a binary number (within a range). + * + * @example + * binary(fakerCore) // '0b1' + * binary(fakerCore, { length: 10 }) // '0b1101011011' + * binary(fakerCore, { length: { min: 5, max: 10 } }) // '0b11101011' + * binary(fakerCore, { prefix: '0b' }) // '0b1' + * binary(fakerCore, { length: 10, prefix: 'bin_' }) // 'bin_1101011011' + * + * @since 8.0.0 + */ +export function binary( + fakerCore: FakerCore, + options: { + /** + * The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string (excluding the prefix) to generate. + */ + min: number; + /** + * The maximum length of the string (excluding the prefix) to generate. + */ + max: number; + }; + /** + * Prefix for the generated number. + * + * @default '0b' + */ + prefix?: string; + } = {} +): string { + const { prefix = '0b', length = 1 } = options; + + let result = prefix; + result += fromCharacters(fakerCore, ['0', '1'], length); + return result; +} diff --git a/src/modules/string/from-characters.ts b/src/modules/string/from-characters.ts new file mode 100644 index 00000000000..d0092d374d9 --- /dev/null +++ b/src/modules/string/from-characters.ts @@ -0,0 +1,63 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { arrayElement } from '../helpers/array-element'; +import { multiple } from '../helpers/multiple'; +import { rangeToNumber } from '../helpers/range-to-number'; + +/** + * Generates a string from the given characters. + * + * @param fakerCore The FakerCore to use. + * @param characters The characters to use for the string. Can be a string or an array of characters. + * If it is an array, then each element is treated as a single character even if it is a string with multiple characters. + * @param length The length of the string to generate either as a fixed length or as a length range. Defaults to `1`. + * @param length.min The minimum length of the string to generate. + * @param length.max The maximum length of the string to generate. + * + * @example + * fromCharacters(fakerCore, 'abc') // 'c' + * fromCharacters(fakerCore, ['a', 'b', 'c']) // 'a' + * fromCharacters(fakerCore, 'abc', 10) // 'cbbbacbacb' + * fromCharacters(fakerCore, 'abc', { min: 5, max: 10 }) // 'abcaaaba' + * + * @since 8.0.0 + */ +export function fromCharacters( + fakerCore: FakerCore, + characters: string | ReadonlyArray, + length: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + } = 1 +): string { + length = rangeToNumber(fakerCore, length); + if (length <= 0) { + return ''; + } + + if (typeof characters === 'string') { + characters = [...characters]; + } + + if (characters.length === 0) { + throw new FakerError( + 'Unable to generate string: No characters to select from.' + ); + } + + return multiple( + fakerCore, + () => arrayElement(fakerCore, characters as string[]), + { + count: length, + } + ).join(''); +} diff --git a/src/modules/string/hexadecimal.ts b/src/modules/string/hexadecimal.ts new file mode 100644 index 00000000000..d36d57cab3a --- /dev/null +++ b/src/modules/string/hexadecimal.ts @@ -0,0 +1,102 @@ +import type { FakerCore } from '../../core'; +import type { Casing } from '../../utils/types'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { fromCharacters } from '../string/from-characters'; + +const HEX_CHARS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', +]; + +/** + * Returns a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) string. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.length The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.casing Casing of the generated number. Defaults to `'mixed'`. + * @param options.prefix Prefix for the generated number. Defaults to `'0x'`. + * + * @example + * hexadecimal(fakerCore) // '0xB' + * hexadecimal(fakerCore, { length: 10 }) // '0xaE13d044cB' + * hexadecimal(fakerCore, { length: { min: 5, max: 10 } }) // '0x7dEf7FCD' + * hexadecimal(fakerCore, { prefix: '0x' }) // '0xE' + * hexadecimal(fakerCore, { casing: 'lower' }) // '0xf' + * hexadecimal(fakerCore, { length: 10, prefix: '#' }) // '#f12a974eB1' + * hexadecimal(fakerCore, { length: 10, casing: 'upper' }) // '0xE3F38014FB' + * hexadecimal(fakerCore, { casing: 'lower', prefix: '' }) // 'd' + * hexadecimal(fakerCore, { length: 10, casing: 'mixed', prefix: '0x' }) // '0xAdE330a4D1' + * + * @since 8.0.0 + */ +export function hexadecimal( + fakerCore: FakerCore, + options: { + /** + * The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string (excluding the prefix) to generate. + */ + min: number; + /** + * The maximum length of the string (excluding the prefix) to generate. + */ + max: number; + }; + /** + * Casing of the generated number. + * + * @default 'mixed' + */ + casing?: Casing; + /** + * Prefix for the generated number. + * + * @default '0x' + */ + prefix?: string; + } = {} +): string { + const { casing = 'mixed', prefix = '0x' } = options; + const length = rangeToNumber(fakerCore, options.length ?? 1); + if (length <= 0) { + return prefix; + } + + let wholeString = fromCharacters(fakerCore, HEX_CHARS, length); + + if (casing === 'upper') { + wholeString = wholeString.toUpperCase(); + } else if (casing === 'lower') { + wholeString = wholeString.toLowerCase(); + } + + return `${prefix}${wholeString}`; +} diff --git a/src/modules/string/index.ts b/src/modules/string/index.ts index 0a07e6c086e..325f629af14 100644 --- a/src/modules/string/index.ts +++ b/src/modules/string/index.ts @@ -1,85 +1,21 @@ -import { FakerError } from '../../errors/faker-error'; -import { CROCKFORDS_BASE32, dateToBase32 } from '../../internal/base32'; -import { toDate } from '../../internal/date'; import { SimpleModuleBase } from '../../internal/module-base'; import type { LiteralUnion } from '../../internal/types'; import type { Casing } from '../../utils/types'; -import { uuidV4, uuidV7 } from './uuid'; - -const UPPER_CHARS: ReadonlyArray = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ']; -const LOWER_CHARS: ReadonlyArray = [...'abcdefghijklmnopqrstuvwxyz']; -const DIGIT_CHARS: ReadonlyArray = [...'0123456789']; - -export type LowerAlphaChar = - | 'a' - | 'b' - | 'c' - | 'd' - | 'e' - | 'f' - | 'g' - | 'h' - | 'i' - | 'j' - | 'k' - | 'l' - | 'm' - | 'n' - | 'o' - | 'p' - | 'q' - | 'r' - | 's' - | 't' - | 'u' - | 'v' - | 'w' - | 'x' - | 'y' - | 'z'; - -export type UpperAlphaChar = - | 'A' - | 'B' - | 'C' - | 'D' - | 'E' - | 'F' - | 'G' - | 'H' - | 'I' - | 'J' - | 'K' - | 'L' - | 'M' - | 'N' - | 'O' - | 'P' - | 'Q' - | 'R' - | 'S' - | 'T' - | 'U' - | 'V' - | 'W' - | 'X' - | 'Y' - | 'Z'; - -export type NumericChar = - | '0' - | '1' - | '2' - | '3' - | '4' - | '5' - | '6' - | '7' - | '8' - | '9'; - -export type AlphaChar = LowerAlphaChar | UpperAlphaChar; -export type AlphaNumericChar = AlphaChar | NumericChar; +import type { AlphaChar, AlphaNumericChar, NumericChar } from './_types'; +import { alpha as stringAlpha } from './alpha'; +import { alphanumeric as stringAlphanumeric } from './alphanumeric'; +import { binary as stringBinary } from './binary'; +import { fromCharacters as stringFromCharacters } from './from-characters'; +import { hexadecimal as stringHexadecimal } from './hexadecimal'; +import { nanoid as stringNanoid } from './nanoid'; +import { numeric as stringNumeric } from './numeric'; +import { octal as stringOctal } from './octal'; +import { sample as stringSample } from './sample'; +import { symbol as stringSymbol } from './symbol'; +import { ulid as stringUlid } from './ulid'; +import { uuid as stringUuid } from './uuid'; + +export type { AlphaChar, AlphaNumericChar, NumericChar } from './_types'; /** * Module to generate string related entries. @@ -130,26 +66,7 @@ export class StringModule extends SimpleModuleBase { max: number; } = 1 ): string { - length = this.faker.helpers.rangeToNumber(length); - if (length <= 0) { - return ''; - } - - if (typeof characters === 'string') { - characters = [...characters]; - } - - if (characters.length === 0) { - throw new FakerError( - 'Unable to generate string: No characters to select from.' - ); - } - - return this.faker.helpers - .multiple(() => this.faker.helpers.arrayElement(characters as string[]), { - count: length, - }) - .join(''); + return stringFromCharacters(this.faker.fakerCore, characters, length); } /** @@ -205,45 +122,7 @@ export class StringModule extends SimpleModuleBase { exclude?: ReadonlyArray> | string; } = {} ): string { - if (typeof options === 'number') { - options = { - length: options, - }; - } - - const length = this.faker.helpers.rangeToNumber(options.length ?? 1); - if (length <= 0) { - return ''; - } - - const { casing = 'mixed' } = options; - let { exclude = [] } = options; - - if (typeof exclude === 'string') { - exclude = [...exclude]; - } - - let charsArray: string[]; - switch (casing) { - case 'upper': { - charsArray = [...UPPER_CHARS]; - break; - } - - case 'lower': { - charsArray = [...LOWER_CHARS]; - break; - } - - case 'mixed': { - charsArray = [...LOWER_CHARS, ...UPPER_CHARS]; - break; - } - } - - charsArray = charsArray.filter((elem) => !exclude.includes(elem)); - - return this.fromCharacters(charsArray, length); + return stringAlpha(this.faker.fakerCore, options); } /** @@ -299,46 +178,7 @@ export class StringModule extends SimpleModuleBase { exclude?: ReadonlyArray> | string; } = {} ): string { - if (typeof options === 'number') { - options = { - length: options, - }; - } - - const length = this.faker.helpers.rangeToNumber(options.length ?? 1); - if (length <= 0) { - return ''; - } - - const { casing = 'mixed' } = options; - let { exclude = [] } = options; - - if (typeof exclude === 'string') { - exclude = [...exclude]; - } - - let charsArray = [...DIGIT_CHARS]; - - switch (casing) { - case 'upper': { - charsArray.push(...UPPER_CHARS); - break; - } - - case 'lower': { - charsArray.push(...LOWER_CHARS); - break; - } - - case 'mixed': { - charsArray.push(...LOWER_CHARS, ...UPPER_CHARS); - break; - } - } - - charsArray = charsArray.filter((elem) => !exclude.includes(elem)); - - return this.fromCharacters(charsArray, length); + return stringAlphanumeric(this.faker.fakerCore, options); } /** @@ -386,11 +226,7 @@ export class StringModule extends SimpleModuleBase { prefix?: string; } = {} ): string { - const { prefix = '0b' } = options; - - let result = prefix; - result += this.fromCharacters(['0', '1'], options.length ?? 1); - return result; + return stringBinary(this.faker.fakerCore, options); } /** @@ -438,14 +274,7 @@ export class StringModule extends SimpleModuleBase { prefix?: string; } = {} ): string { - const { prefix = '0o' } = options; - - let result = prefix; - result += this.fromCharacters( - ['0', '1', '2', '3', '4', '5', '6', '7'], - options.length ?? 1 - ); - return result; + return stringOctal(this.faker.fakerCore, options); } /** @@ -502,47 +331,7 @@ export class StringModule extends SimpleModuleBase { prefix?: string; } = {} ): string { - const { casing = 'mixed', prefix = '0x' } = options; - const length = this.faker.helpers.rangeToNumber(options.length ?? 1); - if (length <= 0) { - return prefix; - } - - let wholeString = this.fromCharacters( - [ - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - ], - length - ); - - if (casing === 'upper') { - wholeString = wholeString.toUpperCase(); - } else if (casing === 'lower') { - wholeString = wholeString.toLowerCase(); - } - - return `${prefix}${wholeString}`; + return stringHexadecimal(this.faker.fakerCore, options); } /** @@ -600,50 +389,7 @@ export class StringModule extends SimpleModuleBase { exclude?: ReadonlyArray> | string; } = {} ): string { - if (typeof options === 'number') { - options = { - length: options, - }; - } - - const length = this.faker.helpers.rangeToNumber(options.length ?? 1); - if (length <= 0) { - return ''; - } - - const { allowLeadingZeros = true } = options; - let { exclude = [] } = options; - - if (typeof exclude === 'string') { - exclude = [...exclude]; - } - - const allowedDigits = DIGIT_CHARS.filter( - (digit) => !exclude.includes(digit) - ); - - if ( - allowedDigits.length === 0 || - (allowedDigits.length === 1 && - !allowLeadingZeros && - allowedDigits[0] === '0') - ) { - throw new FakerError( - 'Unable to generate numeric string, because all possible digits are excluded.' - ); - } - - let result = ''; - - if (!allowLeadingZeros && !exclude.includes('0')) { - result += this.faker.helpers.arrayElement( - allowedDigits.filter((digit) => digit !== '0') - ); - } - - result += this.fromCharacters(allowedDigits, length - result.length); - - return result; + return stringNumeric(this.faker.fakerCore, options); } /** @@ -674,22 +420,7 @@ export class StringModule extends SimpleModuleBase { max: number; } = 10 ): string { - length = this.faker.helpers.rangeToNumber(length); - - const charCodeOption = { - min: 33, - max: 125, - }; - - let returnString = ''; - - while (returnString.length < length) { - returnString += String.fromCodePoint( - this.faker.number.int(charCodeOption) - ); - } - - return returnString; + return stringSample(this.faker.fakerCore, length); } /** @@ -725,7 +456,7 @@ export class StringModule extends SimpleModuleBase { * @param options.version The specific UUID version to use. * @param options.refDate The timestamp to encode into the uuid. * The encoded timestamp is represented by the first 12 characters of the result. - * Defaults to `faker.defaultRefDate()`. + * Defaults to `faker.getDefaultRefDate()`. * * @example * faker.string.uuid() // '019be2c5-58de-70fe-a693-2ccbff1f0780' @@ -752,7 +483,7 @@ export class StringModule extends SimpleModuleBase { * @param options.version The specific UUID version to use. Defaults to `4`. * @param options.refDate The timestamp to encode into the UUID. * This parameter is only relevant for UUID v7. - * Defaults to `faker.defaultRefDate()`. + * Defaults to `faker.getDefaultRefDate()`. * * @example * faker.string.uuid() // '4136cd0b-d90b-4af7-b485-5d1ded8db252' @@ -781,16 +512,7 @@ export class StringModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): string { - const { version = 4, refDate = this.faker.defaultRefDate() } = options; - switch (version) { - case 7: { - return uuidV7(this.faker, toDate(refDate)); - } - - default: { - return uuidV4(this.faker); - } - } + return stringUuid(this.faker.fakerCore, options); } /** @@ -799,7 +521,7 @@ export class StringModule extends SimpleModuleBase { * @param options The optional options object. * @param options.refDate The timestamp to encode into the ULID. * The encoded timestamp is represented by the first 10 characters of the result. - * Defaults to `faker.defaultRefDate()`. + * Defaults to `faker.getDefaultRefDate()`. * * @example * faker.string.ulid() // '01ARZ3NDEKTSV4RRFFQ69G5FAV' @@ -818,10 +540,7 @@ export class StringModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): string { - const { refDate = this.faker.defaultRefDate() } = options; - const date = toDate(refDate); - - return dateToBase32(date) + this.fromCharacters(CROCKFORDS_BASE32, 16); + return stringUlid(this.faker.fakerCore, options); } /** @@ -852,31 +571,7 @@ export class StringModule extends SimpleModuleBase { max: number; } = 21 ): string { - length = this.faker.helpers.rangeToNumber(length); - if (length <= 0) { - return ''; - } - - const generators = [ - { - value: () => this.alphanumeric(1), - // a-z is 26 characters - // this times 2 for upper & lower case is 52 - // add all numbers 0-9 (10 in total) you get 62 - weight: 62, - }, - { - value: () => this.faker.helpers.arrayElement(['_', '-']), - weight: 2, - }, - ]; - let result = ''; - while (result.length < length) { - const charGen = this.faker.helpers.weightedArrayElement(generators); - result += charGen(); - } - - return result; + return stringNanoid(this.faker.fakerCore, length); } /** @@ -911,42 +606,6 @@ export class StringModule extends SimpleModuleBase { max: number; } = 1 ): string { - return this.fromCharacters( - [ - '!', - '"', - '#', - '$', - '%', - '&', - "'", - '(', - ')', - '*', - '+', - ',', - '-', - '.', - '/', - ':', - ';', - '<', - '=', - '>', - '?', - '@', - '[', - '\\', - ']', - '^', - '_', - '`', - '{', - '|', - '}', - '~', - ], - length - ); + return stringSymbol(this.faker.fakerCore, length); } } diff --git a/src/modules/string/nanoid.ts b/src/modules/string/nanoid.ts new file mode 100644 index 00000000000..54a76a42323 --- /dev/null +++ b/src/modules/string/nanoid.ts @@ -0,0 +1,62 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { weightedArrayElement } from '../helpers/weighted-array-element'; +import { alphanumeric } from '../string/alphanumeric'; + +/** + * Generates a [Nano ID](https://github.com/ai/nanoid). + * + * @param fakerCore The FakerCore to use. + * @param length The length of the string to generate either as a fixed length or as a length range. Defaults to `21`. + * @param length.min The minimum length of the Nano ID to generate. + * @param length.max The maximum length of the Nano ID to generate. + * + * @example + * nanoid(fakerCore) // ptL0KpX_yRMI98JFr6B3n + * nanoid(fakerCore, 10) // VsvwSdm_Am + * nanoid(fakerCore, { min: 13, max: 37 }) // KIRsdEL9jxVgqhBDlm + * + * @since 8.0.0 + */ +export function nanoid( + fakerCore: FakerCore, + length: + | number + | { + /** + * The minimum length of the Nano ID to generate. + */ + min: number; + /** + * The maximum length of the Nano ID to generate. + */ + max: number; + } = 21 +): string { + length = rangeToNumber(fakerCore, length); + if (length <= 0) { + return ''; + } + + const generators = [ + { + value: () => alphanumeric(fakerCore, 1), + // a-z is 26 characters + // this times 2 for upper & lower case is 52 + // add all numbers 0-9 (10 in total) you get 62 + weight: 62, + }, + { + value: () => arrayElement(fakerCore, ['_', '-']), + weight: 2, + }, + ]; + let result = ''; + while (result.length < length) { + const charGen = weightedArrayElement(fakerCore, generators); + result += charGen(); + } + + return result; +} diff --git a/src/modules/string/numeric.ts b/src/modules/string/numeric.ts new file mode 100644 index 00000000000..f42d250a6ba --- /dev/null +++ b/src/modules/string/numeric.ts @@ -0,0 +1,110 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import type { LiteralUnion } from '../../internal/types'; +import { arrayElement } from '../helpers/array-element'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { fromCharacters } from '../string/from-characters'; +import type { NumericChar } from './_types'; +import { DIGIT_CHARS } from './_types'; + +/** + * Generates a given length string of digits. + * + * @param fakerCore The FakerCore to use. + * @param options Either the length of the string to generate or the optional options object. + * @param options.length The length of the string to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.allowLeadingZeros Whether leading zeros are allowed or not. Defaults to `true`. + * @param options.exclude An array of digits which should be excluded in the generated string. Defaults to `[]`. + * + * @see numberInt(fakerCore): For generating a number (within a range). + * + * @example + * numeric(fakerCore) // '2' + * numeric(fakerCore, 5) // '31507' + * numeric(fakerCore, 42) // '06434563150765416546479875435481513188548' + * numeric(fakerCore, { length: { min: 5, max: 10 } }) // '197089478' + * numeric(fakerCore, { length: 42, allowLeadingZeros: false }) // '72564846278453876543517840713421451546115' + * numeric(fakerCore, { length: 6, exclude: ['0'] }) // '943228' + * + * @since 8.0.0 + */ +export function numeric( + fakerCore: FakerCore, + options: + | number + | { + /** + * The length of the string to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + }; + /** + * Whether leading zeros are allowed or not. + * + * @default true + */ + allowLeadingZeros?: boolean; + /** + * An array of digits which should be excluded in the generated string. + * + * @default [] + */ + exclude?: ReadonlyArray> | string; + } = {} +): string { + if (typeof options === 'number') { + options = { + length: options, + }; + } + + const length = rangeToNumber(fakerCore, options.length ?? 1); + if (length <= 0) { + return ''; + } + + const { allowLeadingZeros = true } = options; + let { exclude = [] } = options; + + if (typeof exclude === 'string') { + exclude = [...exclude]; + } + + const allowedDigits = DIGIT_CHARS.filter((digit) => !exclude.includes(digit)); + + if ( + allowedDigits.length === 0 || + (allowedDigits.length === 1 && + !allowLeadingZeros && + allowedDigits[0] === '0') + ) { + throw new FakerError( + 'Unable to generate numeric string, because all possible digits are excluded.' + ); + } + + let result = ''; + + if (!allowLeadingZeros && !exclude.includes('0')) { + result += arrayElement( + fakerCore, + allowedDigits.filter((digit) => digit !== '0') + ); + } + + result += fromCharacters(fakerCore, allowedDigits, length - result.length); + + return result; +} diff --git a/src/modules/string/octal.ts b/src/modules/string/octal.ts new file mode 100644 index 00000000000..44ae5545007 --- /dev/null +++ b/src/modules/string/octal.ts @@ -0,0 +1,58 @@ +import type { FakerCore } from '../../core'; +import { fromCharacters } from '../string/from-characters'; + +const OCTAL_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7']; + +/** + * Returns an [octal](https://en.wikipedia.org/wiki/Octal) string. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.length The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. Defaults to `1`. + * @param options.prefix Prefix for the generated number. Defaults to `'0o'`. + * + * @see numberOctal(fakerCore): For generating an octal number (within a range). + * + * @example + * octal(fakerCore) // '0o3' + * octal(fakerCore, { length: 10 }) // '0o1526216210' + * octal(fakerCore, { length: { min: 5, max: 10 } }) // '0o15263214' + * octal(fakerCore, { prefix: '0o' }) // '0o7' + * octal(fakerCore, { length: 10, prefix: 'oct_' }) // 'oct_1542153414' + * + * @since 8.0.0 + */ +export function octal( + fakerCore: FakerCore, + options: { + /** + * The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the string (excluding the prefix) to generate. + */ + min: number; + /** + * The maximum length of the string (excluding the prefix) to generate. + */ + max: number; + }; + /** + * Prefix for the generated number. + * + * @default '0o' + */ + prefix?: string; + } = {} +): string { + const { prefix = '0o', length = 1 } = options; + + let result = prefix; + result += fromCharacters(fakerCore, OCTAL_CHARS, length); + return result; +} diff --git a/src/modules/string/sample.ts b/src/modules/string/sample.ts new file mode 100644 index 00000000000..b9665941998 --- /dev/null +++ b/src/modules/string/sample.ts @@ -0,0 +1,49 @@ +import type { FakerCore } from '../../core'; +import { rangeToNumber } from '../helpers/range-to-number'; +import { int } from '../number/int'; + +/** + * Returns a string containing UTF-16 chars between 33 and 125 (`!` to `}`). + * + * @param fakerCore The FakerCore to use. + * @param length The length of the string (excluding the prefix) to generate either as a fixed length or as a length range. Defaults to `10`. + * @param length.min The minimum length of the string to generate. + * @param length.max The maximum length of the string to generate. + * + * @example + * sample(fakerCore) // 'Zo!.:*e>wR' + * sample(fakerCore, 5) // '6Bye8' + * sample(fakerCore, { min: 5, max: 10 }) // 'FeKunG' + * + * @since 8.0.0 + */ +export function sample( + fakerCore: FakerCore, + length: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + } = 10 +): string { + length = rangeToNumber(fakerCore, length); + + const charCodeOption = { + min: 33, + max: 125, + }; + + let returnString = ''; + + while (returnString.length < length) { + returnString += String.fromCodePoint(int(fakerCore, charCodeOption)); + } + + return returnString; +} diff --git a/src/modules/string/symbol.ts b/src/modules/string/symbol.ts new file mode 100644 index 00000000000..6058c7a6afa --- /dev/null +++ b/src/modules/string/symbol.ts @@ -0,0 +1,74 @@ +import type { FakerCore } from '../../core'; +import { fromCharacters } from '../string/from-characters'; + +const SYMBOL_CHARS = [ + '!', + '"', + '#', + '$', + '%', + '&', + "'", + '(', + ')', + '*', + '+', + ',', + '-', + '.', + '/', + ':', + ';', + '<', + '=', + '>', + '?', + '@', + '[', + '\\', + ']', + '^', + '_', + '`', + '{', + '|', + '}', + '~', +]; + +/** + * Returns a string containing only special characters from the following list: + * + * ```txt + * ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ + * ``` + * + * @param fakerCore The FakerCore to use. + * @param length The length of the string to generate either as a fixed length or as a length range. Defaults to `1`. + * @param length.min The minimum length of the string to generate. + * @param length.max The maximum length of the string to generate. + * + * @example + * symbol(fakerCore) // '$' + * symbol(fakerCore, 5) // '#*!.~' + * symbol(fakerCore, { min: 5, max: 10 }) // ')|@*>^+' + * + * @since 8.0.0 + */ +export function symbol( + fakerCore: FakerCore, + length: + | number + | { + /** + * The minimum length of the string to generate. + */ + min: number; + /** + * The maximum length of the string to generate. + */ + max: number; + } = 1 +): string { + return fromCharacters(fakerCore, SYMBOL_CHARS, length); +} diff --git a/src/modules/string/ulid.ts b/src/modules/string/ulid.ts new file mode 100644 index 00000000000..eafe53d69e0 --- /dev/null +++ b/src/modules/string/ulid.ts @@ -0,0 +1,38 @@ +import type { FakerCore } from '../../core'; +import { CROCKFORDS_BASE32, dateToBase32 } from '../../internal/base32'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { fromCharacters } from '../string/from-characters'; + +/** + * Returns a ULID ([Universally Unique Lexicographically Sortable Identifier](https://github.com/ulid/spec)). + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.refDate The timestamp to encode into the ULID. + * The encoded timestamp is represented by the first 10 characters of the result. + * Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * ulid(fakerCore) // '01ARZ3NDEKTSV4RRFFQ69G5FAV' + * ulid(fakerCore, { refDate: '2020-01-01T00:00:00.000Z' }) // '01DXF6DT00CX9QNNW7PNXQ3YR8' + * + * @since 9.1.0 + */ +export function ulid( + fakerCore: FakerCore, + options: { + /** + * The date to use as reference point for the newly generated ULID encoded timestamp. + * The encoded timestamp is represented by the first 10 characters of the result. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): string { + const { refDate = getDefaultRefDate(fakerCore) } = options; + const date = toDate(refDate); + + return dateToBase32(date) + fromCharacters(fakerCore, CROCKFORDS_BASE32, 16); +} diff --git a/src/modules/string/uuid.ts b/src/modules/string/uuid.ts index f24561dd4f3..4c8fca30115 100644 --- a/src/modules/string/uuid.ts +++ b/src/modules/string/uuid.ts @@ -1,27 +1,142 @@ -import type { SimpleFaker } from '../../'; +import type { FakerCore } from '../../core'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { hex } from '../number/hex'; +/** + * Returns a UUID ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @param fakerCore The FakerCore to use. + * + * @example + * uuid(fakerCore) // '4136cd0b-d90b-4af7-b485-5d1ded8db252' + * + * @since 8.0.0 + */ +export function uuid(fakerCore: FakerCore): string; /** * Returns a UUID v4 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). * - * @internal + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.version The specific UUID version to use. + * + * @example + * uuid(fakerCore, { version: 4 }) // '4136cd0b-d90b-4af7-b485-5d1ded8db252' * - * @param faker The faker instance to use. + * @since 8.0.0 */ -export function uuidV4(faker: SimpleFaker): string { +export function uuid( + fakerCore: FakerCore, + options: { + /** + * The specific UUID version to use. + */ + version: 4; + } +): string; +/** + * Returns a UUID v7 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.version The specific UUID version to use. + * @param options.refDate The timestamp to encode into the uuid. + * The encoded timestamp is represented by the first 12 characters of the result. + * Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * uuid(fakerCore) // '019be2c5-58de-70fe-a693-2ccbff1f0780' + * + * @since 10.3.0 + */ +export function uuid( + fakerCore: FakerCore, + options: { + /** + * The specific UUID version to use. + */ + version: 7; + /** + * The timestamp to encode into the uuid. + * The encoded timestamp is represented by the first 12 characters of the result. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate: string | Date | number; + } +): string; +/** + * Returns a UUID ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @param fakerCore The FakerCore to use. + * @param options An optional options object. + * @param options.version The specific UUID version to use. Defaults to `4`. + * @param options.refDate The timestamp to encode into the UUID. + * This parameter is only relevant for UUID v7. + * Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * uuid(fakerCore) // '4136cd0b-d90b-4af7-b485-5d1ded8db252' + * uuid(fakerCore, { version: 4 }) // 'd5482c1f-c30d-4bbc-b151-d95145bae71b' + * uuid(fakerCore, { version: 7 }) // '01948b54-1b78-75fb-9922-0d9b0fd32248' + * uuid(fakerCore, { version: 7, refDate: '2020-01-01T00:00:00.000Z' }) // '016f5e66-e800-725e-b078-f413f23aaff0' + * + * @since 8.0.0 + */ +export function uuid( + fakerCore: FakerCore, + options?: { + /** + * The specific UUID version to use. + */ + version?: 4 | 7; + /** + * The timestamp to encode into the UUID. + * This parameter is only relevant for UUID v7. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } +): string; +export function uuid( + fakerCore: FakerCore, + options: { + version?: 4 | 7; + refDate?: string | Date | number; + } = {} +): string { + const { version = 4, refDate = getDefaultRefDate(fakerCore) } = options; + switch (version) { + case 7: { + return uuidV7(fakerCore, toDate(refDate)); + } + + default: { + return uuidV4(fakerCore); + } + } +} + +/** + * Returns a UUID v4 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @param fakerCore The FakerCore to use. + */ +function uuidV4(fakerCore: FakerCore): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - .replaceAll('x', () => faker.number.hex({ min: 0x0, max: 0xf })) - .replaceAll('y', () => faker.number.hex({ min: 0x8, max: 0xb })); + .replaceAll('x', () => hex(fakerCore, { min: 0x0, max: 0xf })) + .replaceAll('y', () => hex(fakerCore, { min: 0x8, max: 0xb })); } /** * Returns a UUID v7 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). * - * @internal - * - * @param faker The faker instance to use. + * @param fakerCore The FakerCore to use. * @param refDate The reference date to retrieve the unix timestamp from. */ -export function uuidV7(faker: SimpleFaker, refDate: Date): string { +function uuidV7(fakerCore: FakerCore, refDate: Date): string { const unixTimeMs = refDate.valueOf(); const unixTimeMsNormalized = Math.max(unixTimeMs, 0); const unixTimeMsHex = unixTimeMsNormalized @@ -35,8 +150,8 @@ export function uuidV7(faker: SimpleFaker, refDate: Date): string { ].join('-'); const randomPart = '7xxx-yxxx-xxxxxxxxxxxx' - .replaceAll('x', () => faker.number.hex({ min: 0x0, max: 0xf })) - .replaceAll('y', () => faker.number.hex({ min: 0x8, max: 0xb })); + .replaceAll('x', () => hex(fakerCore, { min: 0x0, max: 0xf })) + .replaceAll('y', () => hex(fakerCore, { min: 0x8, max: 0xb })); return `${unixTimePart}-${randomPart}`; } diff --git a/test/modules/finance.spec.ts b/test/modules/finance.spec.ts index 292e1719114..27cd20e91c7 100644 --- a/test/modules/finance.spec.ts +++ b/test/modules/finance.spec.ts @@ -9,7 +9,7 @@ import { BitcoinNetwork, } from '../../src/modules/finance/bitcoin'; import ibanLib from '../../src/modules/finance/iban'; -import { luhnCheck } from '../../src/modules/helpers/luhn-check'; +import { luhnCheck } from '../../src/modules/helpers/_luhn-check'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; diff --git a/test/modules/helpers-eval.spec.ts b/test/modules/helpers-eval.spec.ts index 21df9975735..b590eda8cf6 100644 --- a/test/modules/helpers-eval.spec.ts +++ b/test/modules/helpers-eval.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { FakerError, faker } from '../../src'; -import { fakeEval } from '../../src/modules/helpers/eval'; +import { fakeEval } from '../../src/modules/helpers/_eval'; describe('fakeEval()', () => { it('does not allow empty string input', () => { diff --git a/test/modules/helpers.spec.ts b/test/modules/helpers.spec.ts index aec8dd228ef..949119f243a 100644 --- a/test/modules/helpers.spec.ts +++ b/test/modules/helpers.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { FakerError, faker } from '../../src'; -import { luhnCheck } from '../../src/modules/helpers/luhn-check'; +import { luhnCheck } from '../../src/modules/helpers/_luhn-check'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 8ac8f572cb9..29d919902a1 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -1,6 +1,7 @@ import { isHexadecimal, isOctal } from 'validator'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { FakerError, SimpleFaker, faker } from '../../src'; +import * as numberIntModule from '../../src/modules/number/int'; import { seededTests } from '../support/seeded-runs'; import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils'; import { times } from './../support/times'; @@ -832,6 +833,10 @@ describe('number', () => { }); describe('romanNumeral', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should generate a Roman numeral within default range', () => { const roman = faker.number.romanNumeral(); expect(roman).toBeTypeOf('string'); @@ -866,7 +871,7 @@ describe('number', () => { )( 'should generate a Roman numeral %s for value %d', (expected: string, value: number) => { - const mock = vi.spyOn(faker.number, 'int'); + const mock = vi.spyOn(numberIntModule, 'int'); mock.mockReturnValue(value); const actual = faker.number.romanNumeral(); mock.mockRestore(); diff --git a/test/modules/phone.spec.ts b/test/modules/phone.spec.ts index d91975f38c9..43b41989d17 100644 --- a/test/modules/phone.spec.ts +++ b/test/modules/phone.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { faker, fakerEN_GB } from '../../src'; -import { luhnCheck } from '../../src/modules/helpers/luhn-check'; +import { luhnCheck } from '../../src/modules/helpers/_luhn-check'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times';