diff --git a/scripts/generate-module-tree.ts b/scripts/generate-module-tree.ts new file mode 100644 index 00000000000..5c92ad5683c --- /dev/null +++ b/scripts/generate-module-tree.ts @@ -0,0 +1,207 @@ +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 } from './shared/character-case'; +import { formatTypescript } from './shared/format'; +import { FILE_PATH_SRC } from './shared/paths'; + +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(); + + 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/tranform-once.ts b/scripts/tranform-once.ts new file mode 100644 index 00000000000..60e627c3d0f --- /dev/null +++ b/scripts/tranform-once.ts @@ -0,0 +1,296 @@ +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'; + +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') + ) + ).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'); +} diff --git a/src/definitions/airline.ts b/src/definitions/airline.ts index e240d61b53c..b9eb671bb84 100644 --- a/src/definitions/airline.ts +++ b/src/definitions/airline.ts @@ -1,4 +1,6 @@ -import type { Airline, Airplane, Airport } from '../modules/airline'; +import type { Airline } from '../modules/airline/airline'; +import type { Airplane } from '../modules/airline/airplane'; +import type { Airport } from '../modules/airline/airport'; import type { LocaleEntry } from './definitions'; export type AirlineDefinition = LocaleEntry<{ diff --git a/src/definitions/finance.ts b/src/definitions/finance.ts index 8353cbfb126..1372b6b5dc4 100644 --- a/src/definitions/finance.ts +++ b/src/definitions/finance.ts @@ -1,4 +1,4 @@ -import type { Currency } from '../modules/finance'; +import type { Currency } from '../modules/finance/currency'; import type { LocaleEntry } from './definitions'; /** * The possible definitions related to finance. diff --git a/src/definitions/internet.ts b/src/definitions/internet.ts index dcd1d5ae09b..d1d4412a3c6 100644 --- a/src/definitions/internet.ts +++ b/src/definitions/internet.ts @@ -1,4 +1,5 @@ -import type { EmojiType, HTTPStatusCodeType } from '../modules/internet'; +import type { EmojiType } from '../modules/internet/emoji'; +import type { HTTPStatusCodeType } from '../modules/internet/http-status-code'; import type { LocaleEntry } from './definitions'; /** diff --git a/src/definitions/location.ts b/src/definitions/location.ts index 079bc22ed71..7d994c03a82 100644 --- a/src/definitions/location.ts +++ b/src/definitions/location.ts @@ -1,4 +1,4 @@ -import type { Language } from '../modules/location'; +import type { Language } from '../modules/location/language'; import type { LocaleEntry } from './definitions'; /** diff --git a/src/definitions/science.ts b/src/definitions/science.ts index 01f791f8a9d..9b21913d68a 100644 --- a/src/definitions/science.ts +++ b/src/definitions/science.ts @@ -1,4 +1,5 @@ -import type { ChemicalElement, Unit } from '../modules/science'; +import type { ChemicalElement } from '../modules/science/chemical-element'; +import type { Unit } from '../modules/science/unit'; import type { LocaleEntry } from './definitions'; /** diff --git a/src/index.ts b/src/index.ts index fa6cf275927..e5dd2538c9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,15 +57,13 @@ export type { CompanyModule } from './modules/company'; export type { DatabaseModule } from './modules/database'; export type { DatatypeModule } from './modules/datatype'; export type { DateModule, SimpleDateModule } from './modules/date'; -export type { Currency, FinanceModule } from './modules/finance'; -export { - BitcoinAddressFamily, - BitcoinNetwork, -} from './modules/finance/bitcoin'; +export { BitcoinAddressFamily, BitcoinNetwork } from './modules/finance'; export type { BitcoinAddressFamilyType, BitcoinNetworkType, -} from './modules/finance/bitcoin'; + Currency, + FinanceModule, +} from './modules/finance'; export type { FoodModule } from './modules/food'; export type { GitModule } from './modules/git'; export type { HackerModule } from './modules/hacker'; diff --git a/src/modules/airline/aircraft-type.ts b/src/modules/airline/aircraft-type.ts new file mode 100644 index 00000000000..0c255c2ea18 --- /dev/null +++ b/src/modules/airline/aircraft-type.ts @@ -0,0 +1,24 @@ +import type { FakerCore } from '../../core'; +import { enumValue } from '../helpers/enum-value'; + +export enum Aircraft { + Narrowbody = 'narrowbody', + Regional = 'regional', + Widebody = 'widebody', +} + +export type AircraftType = `${Aircraft}`; + +/** + * Returns a random aircraft type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * aircraftType(fakerCore) // 'narrowbody' + * + * @since 8.0.0 + */ +export function aircraftType(fakerCore: FakerCore): AircraftType { + return enumValue(fakerCore, Aircraft); +} diff --git a/src/modules/airline/airline.ts b/src/modules/airline/airline.ts new file mode 100644 index 00000000000..cf131f71e0f --- /dev/null +++ b/src/modules/airline/airline.ts @@ -0,0 +1,27 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export interface Airline { + /** + * The name of the airline (e.g. `'American Airlines'`). + */ + readonly name: string; + /** + * The 2 character IATA code of the airline (e.g. `'AA'`). + */ + readonly iataCode: string; +} + +/** + * Generates a random airline. + * + * @param fakerCore The FakerCore to use. + * + * @example + * airline(fakerCore) // { name: 'American Airlines', iataCode: 'AA' } + * + * @since 8.0.0 + */ +export function airline(fakerCore: FakerCore): Airline { + return arrayElement(fakerCore, fakerCore.locale.airline.airline); +} diff --git a/src/modules/airline/airplane.ts b/src/modules/airline/airplane.ts new file mode 100644 index 00000000000..bbbd30f1202 --- /dev/null +++ b/src/modules/airline/airplane.ts @@ -0,0 +1,27 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export interface Airplane { + /** + * The name of the airplane (e.g. `'Airbus A321'`). + */ + readonly name: string; + /** + * The IATA code of the airplane (e.g. `'321'`). + */ + readonly iataTypeCode: string; +} + +/** + * Generates a random airplane. + * + * @param fakerCore The FakerCore to use. + * + * @example + * airplane(fakerCore) // { name: 'Airbus A321neo', iataTypeCode: '32Q' } + * + * @since 8.0.0 + */ +export function airplane(fakerCore: FakerCore): Airplane { + return arrayElement(fakerCore, fakerCore.locale.airline.airplane); +} diff --git a/src/modules/airline/airport.ts b/src/modules/airline/airport.ts new file mode 100644 index 00000000000..cec3d0de163 --- /dev/null +++ b/src/modules/airline/airport.ts @@ -0,0 +1,27 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export interface Airport { + /** + * The name of the airport (e.g. `'Dallas Fort Worth International Airport'`). + */ + readonly name: string; + /** + * The IATA code of the airport (e.g. `'DFW'`). + */ + readonly iataCode: string; +} + +/** + * Generates a random airport. + * + * @param fakerCore The FakerCore to use. + * + * @example + * airport(fakerCore) // { name: 'Dallas Fort Worth International Airport', iataCode: 'DFW' } + * + * @since 8.0.0 + */ +export function airport(fakerCore: FakerCore): Airport { + return arrayElement(fakerCore, fakerCore.locale.airline.airport); +} diff --git a/src/modules/airline/flight-number.ts b/src/modules/airline/flight-number.ts new file mode 100644 index 00000000000..485b3f16c93 --- /dev/null +++ b/src/modules/airline/flight-number.ts @@ -0,0 +1,64 @@ +import type { FakerCore } from '../../core'; +import { numeric } from '../string/numeric'; + +/** + * Returns a random flight number. Flight numbers are always 1 to 4 digits long. Sometimes they are + * used without leading zeros (e.g.: `American Airlines flight 425`) and sometimes with leading + * zeros, often with the airline code prepended (e.g.: `AA0425`). + * + * To generate a flight number prepended with an airline code, combine this function with the + * `airline()` function and use template literals: + * ``` + * `${airline(fakerCore).iataCode}${flightNumber(fakerCore, { addLeadingZeros: true })}` // 'AA0798' + * ``` + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.length The number or range of digits to generate. Defaults to `{ min: 1, max: 4 }`. + * @param options.addLeadingZeros Whether to pad the flight number up to 4 digits with leading zeros. Defaults to `false`. + * + * @example + * flightNumber(fakerCore) // '2405' + * flightNumber(fakerCore, { addLeadingZeros: true }) // '0249' + * flightNumber(fakerCore, { addLeadingZeros: true, length: 2 }) // '0042' + * flightNumber(fakerCore, { addLeadingZeros: true, length: { min: 2, max: 3 } }) // '0624' + * flightNumber(fakerCore, { length: 3 }) // '425' + * flightNumber(fakerCore, { length: { min: 2, max: 3 } }) // '84' + * + * @since 8.0.0 + */ +export function flightNumber( + fakerCore: FakerCore, + options: { + /** + * The number or range of digits to generate. + * + * @default { min: 1, max: 4 } + */ + length?: + | number + | { + /** + * The minimum number of digits to generate. + */ + min: number; + /** + * The maximum number of digits to generate. + */ + max: number; + }; + /** + * Whether to pad the flight number up to 4 digits with leading zeros. + * + * @default false + */ + addLeadingZeros?: boolean; + } = {} +): string { + const { length = { min: 1, max: 4 }, addLeadingZeros = false } = options; + const flightNumber = numeric(fakerCore, { + length, + allowLeadingZeros: false, + }); + return addLeadingZeros ? flightNumber.padStart(4, '0') : flightNumber; +} diff --git a/src/modules/airline/index.ts b/src/modules/airline/index.ts index e8ac47c58c2..69fa181c795 100644 --- a/src/modules/airline/index.ts +++ b/src/modules/airline/index.ts @@ -5,60 +5,23 @@ * operations. */ import { ModuleBase } from '../../internal/module-base'; +import type { AircraftType } from './aircraft-type'; +import { aircraftType as airlineAircraftType } from './aircraft-type'; +import type { Airline } from './airline'; +import { airline as airlineAirline } from './airline'; +import type { Airplane } from './airplane'; +import { airplane as airlineAirplane } from './airplane'; +import type { Airport } from './airport'; +import { airport as airlineAirport } from './airport'; +import { flightNumber as airlineFlightNumber } from './flight-number'; +import { recordLocator as airlineRecordLocator } from './record-locator'; +import { seat as airlineSeat } from './seat'; -export enum Aircraft { - Narrowbody = 'narrowbody', - Regional = 'regional', - Widebody = 'widebody', -} - -export type AircraftType = `${Aircraft}`; - -export interface Airline { - /** - * The name of the airline (e.g. `'American Airlines'`). - */ - readonly name: string; - /** - * The 2 character IATA code of the airline (e.g. `'AA'`). - */ - readonly iataCode: string; -} - -export interface Airplane { - /** - * The name of the airplane (e.g. `'Airbus A321'`). - */ - readonly name: string; - /** - * The IATA code of the airplane (e.g. `'321'`). - */ - readonly iataTypeCode: string; -} - -export interface Airport { - /** - * The name of the airport (e.g. `'Dallas Fort Worth International Airport'`). - */ - readonly name: string; - /** - * The IATA code of the airport (e.g. `'DFW'`). - */ - readonly iataCode: string; -} - -const numerics = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; -const visuallySimilarCharacters = ['0', 'O', '1', 'I', 'L']; -const aircraftTypeMaxRows: Record = { - regional: 20, - narrowbody: 35, - widebody: 60, -}; -const aircraftTypeSeats: Record = { - regional: ['A', 'B', 'C', 'D'], - narrowbody: ['A', 'B', 'C', 'D', 'E', 'F'], - widebody: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K'], -}; +export { Aircraft } from './aircraft-type'; +export type { AircraftType } from './aircraft-type'; +export type { Airline } from './airline'; +export type { Airplane } from './airplane'; +export type { Airport } from './airport'; /** * Module to generate airline and airport related data. @@ -87,9 +50,7 @@ export class AirlineModule extends ModuleBase { * @since 8.0.0 */ airport(): Airport { - return this.faker.helpers.arrayElement( - this.faker.definitions.airline.airport - ); + return airlineAirport(this.faker.fakerCore); } /** @@ -101,9 +62,7 @@ export class AirlineModule extends ModuleBase { * @since 8.0.0 */ airline(): Airline { - return this.faker.helpers.arrayElement( - this.faker.definitions.airline.airline - ); + return airlineAirline(this.faker.fakerCore); } /** @@ -115,9 +74,7 @@ export class AirlineModule extends ModuleBase { * @since 8.0.0 */ airplane(): Airplane { - return this.faker.helpers.arrayElement( - this.faker.definitions.airline.airplane - ); + return airlineAirplane(this.faker.fakerCore); } /** @@ -153,22 +110,7 @@ export class AirlineModule extends ModuleBase { allowVisuallySimilarCharacters?: boolean; } = {} ): string { - const { allowNumerics = false, allowVisuallySimilarCharacters = false } = - options; - const excludedChars: string[] = []; - if (!allowNumerics) { - excludedChars.push(...numerics); - } - - if (!allowVisuallySimilarCharacters) { - excludedChars.push(...visuallySimilarCharacters); - } - - return this.faker.string.alphanumeric({ - length: 6, - casing: 'upper', - exclude: excludedChars, - }); + return airlineRecordLocator(this.faker.fakerCore, options); } /** @@ -194,12 +136,7 @@ export class AirlineModule extends ModuleBase { aircraftType?: AircraftType; } = {} ): string { - const { aircraftType = Aircraft.Narrowbody } = options; - const maxRow = aircraftTypeMaxRows[aircraftType]; - const allowedSeats = aircraftTypeSeats[aircraftType]; - const row = this.faker.number.int({ min: 1, max: maxRow }); - const seat = this.faker.helpers.arrayElement(allowedSeats); - return `${row}${seat}`; + return airlineSeat(this.faker.fakerCore, options); } /** @@ -211,7 +148,7 @@ export class AirlineModule extends ModuleBase { * @since 8.0.0 */ aircraftType(): AircraftType { - return this.faker.helpers.enumValue(Aircraft); + return airlineAircraftType(this.faker.fakerCore); } /** @@ -266,11 +203,6 @@ export class AirlineModule extends ModuleBase { addLeadingZeros?: boolean; } = {} ): string { - const { length = { min: 1, max: 4 }, addLeadingZeros = false } = options; - const flightNumber = this.faker.string.numeric({ - length, - allowLeadingZeros: false, - }); - return addLeadingZeros ? flightNumber.padStart(4, '0') : flightNumber; + return airlineFlightNumber(this.faker.fakerCore, options); } } diff --git a/src/modules/airline/record-locator.ts b/src/modules/airline/record-locator.ts new file mode 100644 index 00000000000..4af6366175c --- /dev/null +++ b/src/modules/airline/record-locator.ts @@ -0,0 +1,58 @@ +import type { FakerCore } from '../../core'; +import { alphanumeric } from '../string/alphanumeric'; + +const numerics = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +const visuallySimilarCharacters = ['0', 'O', '1', 'I', 'L']; + +/** + * Generates a random [record locator](https://en.wikipedia.org/wiki/Record_locator). Record locators + * are used by airlines to identify reservations. They're also known as booking reference numbers, + * locator codes, confirmation codes, or reservation codes. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.allowNumerics Whether to allow numeric characters. Defaults to `false`. + * @param options.allowVisuallySimilarCharacters Whether to allow visually similar characters such as '1' and 'I'. Defaults to `false`. + * + * @example + * recordLocator(fakerCore) // 'KIFRWE' + * recordLocator(fakerCore, { allowNumerics: true }) // 'E5TYEM' + * recordLocator(fakerCore, { allowVisuallySimilarCharacters: true }) // 'ANZNEI' + * recordLocator(fakerCore, { allowNumerics: true, allowVisuallySimilarCharacters: true }) // '1Z2Z3E' + * + * @since 8.0.0 + */ +export function recordLocator( + fakerCore: FakerCore, + options: { + /** + * Whether to allow numeric characters. + * + * @default false + */ + allowNumerics?: boolean; + /** + * Whether to allow visually similar characters such as '1' and 'I'. + * + * @default false + */ + allowVisuallySimilarCharacters?: boolean; + } = {} +): string { + const { allowNumerics = false, allowVisuallySimilarCharacters = false } = + options; + const excludedChars: string[] = []; + if (!allowNumerics) { + excludedChars.push(...numerics); + } + + if (!allowVisuallySimilarCharacters) { + excludedChars.push(...visuallySimilarCharacters); + } + + return alphanumeric(fakerCore, { + length: 6, + casing: 'upper', + exclude: excludedChars, + }); +} diff --git a/src/modules/airline/seat.ts b/src/modules/airline/seat.ts new file mode 100644 index 00000000000..fe12eb2b5e7 --- /dev/null +++ b/src/modules/airline/seat.ts @@ -0,0 +1,49 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; +import type { AircraftType } from './aircraft-type'; +import { Aircraft } from './aircraft-type'; + +const aircraftTypeMaxRows: Record = { + regional: 20, + narrowbody: 35, + widebody: 60, +}; +const aircraftTypeSeats: Record = { + regional: ['A', 'B', 'C', 'D'], + narrowbody: ['A', 'B', 'C', 'D', 'E', 'F'], + widebody: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K'], +}; + +/** + * Generates a random seat. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.aircraftType The aircraft type. Can be one of `narrowbody`, `regional`, `widebody`. Defaults to `narrowbody`. + * + * @example + * seat(fakerCore) // '22C' + * seat(fakerCore, { aircraftType: 'regional' }) // '7A' + * seat(fakerCore, { aircraftType: 'widebody' }) // '42K' + * + * @since 8.0.0 + */ +export function seat( + fakerCore: FakerCore, + options: { + /** + * The aircraft type. Can be one of `narrowbody`, `regional`, `widebody`. + * + * @default 'narrowbody' + */ + aircraftType?: AircraftType; + } = {} +): string { + const { aircraftType = Aircraft.Narrowbody } = options; + const maxRow = aircraftTypeMaxRows[aircraftType]; + const allowedSeats = aircraftTypeSeats[aircraftType]; + const row = int(fakerCore, { min: 1, max: maxRow }); + const seat = arrayElement(fakerCore, allowedSeats); + return `${row}${seat}`; +} diff --git a/src/modules/animal/bear.ts b/src/modules/animal/bear.ts new file mode 100644 index 00000000000..c48c7e81581 --- /dev/null +++ b/src/modules/animal/bear.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random bear species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * bear(fakerCore) // 'Asian black bear' + * + * @since 5.5.0 + */ +export function bear(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.bear); +} diff --git a/src/modules/animal/bird.ts b/src/modules/animal/bird.ts new file mode 100644 index 00000000000..6fdd0992b33 --- /dev/null +++ b/src/modules/animal/bird.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random bird species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * bird(fakerCore) // 'Buller's Shearwater' + * + * @since 5.5.0 + */ +export function bird(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.bird); +} diff --git a/src/modules/animal/cat.ts b/src/modules/animal/cat.ts new file mode 100644 index 00000000000..5f1a5262a07 --- /dev/null +++ b/src/modules/animal/cat.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random cat breed. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cat(fakerCore) // 'Singapura' + * + * @since 5.5.0 + */ +export function cat(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.cat); +} diff --git a/src/modules/animal/cetacean.ts b/src/modules/animal/cetacean.ts new file mode 100644 index 00000000000..01eea2b213d --- /dev/null +++ b/src/modules/animal/cetacean.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random cetacean species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cetacean(fakerCore) // 'Spinner Dolphin' + * + * @since 5.5.0 + */ +export function cetacean(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.cetacean); +} diff --git a/src/modules/animal/cow.ts b/src/modules/animal/cow.ts new file mode 100644 index 00000000000..1d6477c5e1f --- /dev/null +++ b/src/modules/animal/cow.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random cow species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cow(fakerCore) // 'Brava' + * + * @since 5.5.0 + */ +export function cow(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.cow); +} diff --git a/src/modules/animal/crocodilia.ts b/src/modules/animal/crocodilia.ts new file mode 100644 index 00000000000..001cf95cde9 --- /dev/null +++ b/src/modules/animal/crocodilia.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random crocodilian species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * crocodilia(fakerCore) // 'Philippine Crocodile' + * + * @since 5.5.0 + */ +export function crocodilia(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.crocodilia); +} diff --git a/src/modules/animal/dog.ts b/src/modules/animal/dog.ts new file mode 100644 index 00000000000..178d78cdf87 --- /dev/null +++ b/src/modules/animal/dog.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random dog breed. + * + * @param fakerCore The FakerCore to use. + * + * @example + * dog(fakerCore) // 'Irish Water Spaniel' + * + * @since 5.5.0 + */ +export function dog(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.dog); +} diff --git a/src/modules/animal/fish.ts b/src/modules/animal/fish.ts new file mode 100644 index 00000000000..f3aa462828a --- /dev/null +++ b/src/modules/animal/fish.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random fish species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * fish(fakerCore) // 'Mandarin fish' + * + * @since 5.5.0 + */ +export function fish(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.fish); +} diff --git a/src/modules/animal/horse.ts b/src/modules/animal/horse.ts new file mode 100644 index 00000000000..91896bce5e8 --- /dev/null +++ b/src/modules/animal/horse.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random horse breed. + * + * @param fakerCore The FakerCore to use. + * + * @example + * horse(fakerCore) // 'Swedish Warmblood' + * + * @since 5.5.0 + */ +export function horse(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.horse); +} diff --git a/src/modules/animal/index.ts b/src/modules/animal/index.ts index bb200a31c0a..8352e317bc6 100644 --- a/src/modules/animal/index.ts +++ b/src/modules/animal/index.ts @@ -1,4 +1,20 @@ import { ModuleBase } from '../../internal/module-base'; +import { bear as animalBear } from './bear'; +import { bird as animalBird } from './bird'; +import { cat as animalCat } from './cat'; +import { cetacean as animalCetacean } from './cetacean'; +import { cow as animalCow } from './cow'; +import { crocodilia as animalCrocodilia } from './crocodilia'; +import { dog as animalDog } from './dog'; +import { fish as animalFish } from './fish'; +import { horse as animalHorse } from './horse'; +import { insect as animalInsect } from './insect'; +import { lion as animalLion } from './lion'; +import { petName as animalPetName } from './pet-name'; +import { rabbit as animalRabbit } from './rabbit'; +import { rodent as animalRodent } from './rodent'; +import { snake as animalSnake } from './snake'; +import { type as animalType } from './type'; /** * Module to generate animal related entries. @@ -21,7 +37,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ dog(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.dog); + return animalDog(this.faker.fakerCore); } /** @@ -33,7 +49,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ cat(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.cat); + return animalCat(this.faker.fakerCore); } /** @@ -45,7 +61,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ snake(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.snake); + return animalSnake(this.faker.fakerCore); } /** @@ -57,7 +73,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ bear(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.bear); + return animalBear(this.faker.fakerCore); } /** @@ -69,7 +85,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ lion(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.lion); + return animalLion(this.faker.fakerCore); } /** @@ -81,9 +97,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ cetacean(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.cetacean - ); + return animalCetacean(this.faker.fakerCore); } /** @@ -95,7 +109,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ horse(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.horse); + return animalHorse(this.faker.fakerCore); } /** @@ -107,7 +121,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ bird(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.bird); + return animalBird(this.faker.fakerCore); } /** @@ -119,7 +133,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ cow(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.cow); + return animalCow(this.faker.fakerCore); } /** @@ -131,7 +145,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ fish(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.fish); + return animalFish(this.faker.fakerCore); } /** @@ -143,9 +157,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ crocodilia(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.crocodilia - ); + return animalCrocodilia(this.faker.fakerCore); } /** @@ -157,9 +169,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ insect(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.insect - ); + return animalInsect(this.faker.fakerCore); } /** @@ -171,9 +181,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ rabbit(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.rabbit - ); + return animalRabbit(this.faker.fakerCore); } /** @@ -185,9 +193,7 @@ export class AnimalModule extends ModuleBase { * @since 7.4.0 */ rodent(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.rodent - ); + return animalRodent(this.faker.fakerCore); } /** @@ -199,7 +205,7 @@ export class AnimalModule extends ModuleBase { * @since 5.5.0 */ type(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.animal.type); + return animalType(this.faker.fakerCore); } /** @@ -211,8 +217,6 @@ export class AnimalModule extends ModuleBase { * @since 9.2.0 */ petName(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.animal.pet_name - ); + return animalPetName(this.faker.fakerCore); } } diff --git a/src/modules/animal/insect.ts b/src/modules/animal/insect.ts new file mode 100644 index 00000000000..42953e04b2f --- /dev/null +++ b/src/modules/animal/insect.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random insect species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * insect(fakerCore) // 'Pyramid ant' + * + * @since 5.5.0 + */ +export function insect(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.insect); +} diff --git a/src/modules/animal/lion.ts b/src/modules/animal/lion.ts new file mode 100644 index 00000000000..810a4617333 --- /dev/null +++ b/src/modules/animal/lion.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random lion species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * lion(fakerCore) // 'Northeast Congo Lion' + * + * @since 5.5.0 + */ +export function lion(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.lion); +} diff --git a/src/modules/animal/pet-name.ts b/src/modules/animal/pet-name.ts new file mode 100644 index 00000000000..9fc771e81f3 --- /dev/null +++ b/src/modules/animal/pet-name.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random pet name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * petName(fakerCore) // 'Coco' + * + * @since 9.2.0 + */ +export function petName(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.pet_name); +} diff --git a/src/modules/animal/rabbit.ts b/src/modules/animal/rabbit.ts new file mode 100644 index 00000000000..a464ca8d2b1 --- /dev/null +++ b/src/modules/animal/rabbit.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random rabbit species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * rabbit(fakerCore) // 'Florida White' + * + * @since 5.5.0 + */ +export function rabbit(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.rabbit); +} diff --git a/src/modules/animal/rodent.ts b/src/modules/animal/rodent.ts new file mode 100644 index 00000000000..3e366fa6f3e --- /dev/null +++ b/src/modules/animal/rodent.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random rodent breed. + * + * @param fakerCore The FakerCore to use. + * + * @example + * rodent(fakerCore) // 'Cuscomys ashanika' + * + * @since 7.4.0 + */ +export function rodent(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.rodent); +} diff --git a/src/modules/animal/snake.ts b/src/modules/animal/snake.ts new file mode 100644 index 00000000000..e103ea8f21d --- /dev/null +++ b/src/modules/animal/snake.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random snake species. + * + * @param fakerCore The FakerCore to use. + * + * @example + * snake(fakerCore) // 'Eyelash viper' + * + * @since 5.5.0 + */ +export function snake(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.snake); +} diff --git a/src/modules/animal/type.ts b/src/modules/animal/type.ts new file mode 100644 index 00000000000..861706fc137 --- /dev/null +++ b/src/modules/animal/type.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random animal type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * type(fakerCore) // 'crocodile' + * + * @since 5.5.0 + */ +export function type(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.animal.type); +} diff --git a/src/modules/book/author.ts b/src/modules/book/author.ts new file mode 100644 index 00000000000..68dc07c00f4 --- /dev/null +++ b/src/modules/book/author.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random author name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * author(fakerCore) // 'William Shakespeare' + * + * @since 9.1.0 + */ +export function author(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.author); +} diff --git a/src/modules/book/format.ts b/src/modules/book/format.ts new file mode 100644 index 00000000000..7dacba9fe51 --- /dev/null +++ b/src/modules/book/format.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random book format. + * + * @param fakerCore The FakerCore to use. + * + * @example + * format(fakerCore) // 'Hardcover' + * + * @since 9.1.0 + */ +export function format(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.format); +} diff --git a/src/modules/book/genre.ts b/src/modules/book/genre.ts new file mode 100644 index 00000000000..333215e891a --- /dev/null +++ b/src/modules/book/genre.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random genre. + * + * @param fakerCore The FakerCore to use. + * + * @example + * genre(fakerCore) // 'Fantasy' + * + * @since 9.1.0 + */ +export function genre(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.genre); +} diff --git a/src/modules/book/index.ts b/src/modules/book/index.ts index 86e168d2ad4..f73ec91040f 100644 --- a/src/modules/book/index.ts +++ b/src/modules/book/index.ts @@ -1,4 +1,10 @@ import { ModuleBase } from '../../internal/module-base'; +import { author as bookAuthor } from './author'; +import { format as bookFormat } from './format'; +import { genre as bookGenre } from './genre'; +import { publisher as bookPublisher } from './publisher'; +import { series as bookSeries } from './series'; +import { title as bookTitle } from './title'; /** * Module to generate book related entries. @@ -26,7 +32,7 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ author(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.book.author); + return bookAuthor(this.faker.fakerCore); } /** @@ -38,7 +44,7 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ format(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.book.format); + return bookFormat(this.faker.fakerCore); } /** @@ -50,7 +56,7 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ genre(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.book.genre); + return bookGenre(this.faker.fakerCore); } /** @@ -62,9 +68,7 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ publisher(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.book.publisher - ); + return bookPublisher(this.faker.fakerCore); } /** @@ -76,7 +80,7 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ series(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.book.series); + return bookSeries(this.faker.fakerCore); } /** @@ -88,6 +92,6 @@ export class BookModule extends ModuleBase { * @since 9.1.0 */ title(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.book.title); + return bookTitle(this.faker.fakerCore); } } diff --git a/src/modules/book/publisher.ts b/src/modules/book/publisher.ts new file mode 100644 index 00000000000..0ce3e86cba7 --- /dev/null +++ b/src/modules/book/publisher.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random publisher. + * + * @param fakerCore The FakerCore to use. + * + * @example + * publisher(fakerCore) // 'Addison-Wesley' + * + * @since 9.1.0 + */ +export function publisher(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.publisher); +} diff --git a/src/modules/book/series.ts b/src/modules/book/series.ts new file mode 100644 index 00000000000..7dd42c032cb --- /dev/null +++ b/src/modules/book/series.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random series. + * + * @param fakerCore The FakerCore to use. + * + * @example + * series(fakerCore) // 'Harry Potter' + * + * @since 9.1.0 + */ +export function series(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.series); +} diff --git a/src/modules/book/title.ts b/src/modules/book/title.ts new file mode 100644 index 00000000000..108cae5bfb6 --- /dev/null +++ b/src/modules/book/title.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random title. + * + * @param fakerCore The FakerCore to use. + * + * @example + * title(fakerCore) // 'Romeo and Juliet' + * + * @since 9.1.0 + */ +export function title(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.book.title); +} diff --git a/src/modules/color/_format-hex-color.ts b/src/modules/color/_format-hex-color.ts new file mode 100644 index 00000000000..46f7208cd20 --- /dev/null +++ b/src/modules/color/_format-hex-color.ts @@ -0,0 +1,41 @@ +import type { Casing } from '../../utils/types'; + +/** + * Formats the hex format of a generated color string according + * to options specified by user. + * + * @param hexColor Hex color string to be formatted. + * @param options Options object. + * @param options.prefix Prefix of the generated hex color. + * @param options.casing Letter type case of the generated hex color. + */ +export function formatHexColor( + hexColor: string, + options: { + prefix: string; + casing: Casing; + } +): string { + const { prefix, casing } = options; + + switch (casing) { + case 'upper': { + hexColor = hexColor.toUpperCase(); + break; + } + + case 'lower': { + hexColor = hexColor.toLowerCase(); + break; + } + + case 'mixed': + // Do nothing + } + + if (prefix) { + hexColor = prefix + hexColor; + } + + return hexColor; +} diff --git a/src/modules/color/_to-color-format.ts b/src/modules/color/_to-color-format.ts new file mode 100644 index 00000000000..2cb4d6e9ff6 --- /dev/null +++ b/src/modules/color/_to-color-format.ts @@ -0,0 +1,120 @@ +import type { ColorFormat } from './_types'; +import type { CssFunctionType } from './css-supported-function'; +import type { CssSpaceType } from './css-supported-space'; + +/** + * Converts an array of color values to the specified color format. + * + * @param values Array of color values to be converted. + * @param format Format of generated RGB color. + * @param cssFunction CSS function to be generated for the color. Defaults to `'rgb'`. + * @param space Color space to format CSS color function with. Defaults to `'sRGB'`. + */ +export function toColorFormat( + values: number[], + format: ColorFormat, + cssFunction: CssFunctionType = 'rgb', + space: CssSpaceType = 'sRGB' +): string | number[] { + switch (format) { + case 'css': { + return toCSS(values, cssFunction, space); + } + + case 'binary': { + return toBinary(values); + } + + case 'decimal': { + return values; + } + } +} + +/** + * Converts an array of numbers into binary string format. + * + * @param values Array of values to be converted. + */ +function toBinary(values: number[]): string { + const binary: string[] = values.map((value) => { + const isFloat = value % 1 !== 0; + if (isFloat) { + const buffer = new ArrayBuffer(4); + new DataView(buffer).setFloat32(0, value); + const bytes = new Uint8Array(buffer); + return toBinary([...bytes]).replaceAll(' ', ''); + } + + return (value >>> 0).toString(2).padStart(8, '0'); + }); + return binary.join(' '); +} + +/** + * Converts the given value to a percentage (`round(value * 100)`). + * + * @param value The value to convert to a percentage. + */ +function toPercentage(value: number): number { + return Math.round(value * 100); +} + +/** + * Converts an array of numbers into CSS accepted format. + * + * @param values Array of values to be converted. + * @param cssFunction CSS function to be generated for the color. Defaults to `'rgb'`. + * @param space Color space to format CSS color function with. Defaults to `'sRGB'`. + */ +function toCSS( + values: number[], + cssFunction: CssFunctionType = 'rgb', + space: CssSpaceType = 'sRGB' +): string { + switch (cssFunction) { + case 'rgba': { + return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${values[3]})`; + } + + case 'color': { + return `color(${space} ${values[0]} ${values[1]} ${values[2]})`; + } + + case 'cmyk': { + return `cmyk(${toPercentage(values[0])}%, ${toPercentage( + values[1] + )}%, ${toPercentage(values[2])}%, ${toPercentage(values[3])}%)`; + } + + case 'hsl': { + return `hsl(${values[0]}deg ${toPercentage(values[1])}% ${toPercentage( + values[2] + )}%)`; + } + + case 'hsla': { + return `hsl(${values[0]}deg ${toPercentage(values[1])}% ${toPercentage( + values[2] + )}% / ${toPercentage(values[3])})`; + } + + case 'hwb': { + return `hwb(${values[0]} ${toPercentage(values[1])}% ${toPercentage( + values[2] + )}%)`; + } + + case 'lab': { + return `lab(${toPercentage(values[0])}% ${values[1]} ${values[2]})`; + } + + case 'lch': { + return `lch(${toPercentage(values[0])}% ${values[1]} ${values[2]})`; + } + + case 'rgb': { + return `rgb(${values[0]}, ${values[1]}, ${values[2]})`; + } + } +} diff --git a/src/modules/color/_types.ts b/src/modules/color/_types.ts new file mode 100644 index 00000000000..f58bdeba0d4 --- /dev/null +++ b/src/modules/color/_types.ts @@ -0,0 +1,3 @@ +export type StringColorFormat = 'css' | 'binary'; +export type NumberColorFormat = 'decimal'; +export type ColorFormat = StringColorFormat | NumberColorFormat; diff --git a/src/modules/color/cmyk.ts b/src/modules/color/cmyk.ts new file mode 100644 index 00000000000..10d9df91cdc --- /dev/null +++ b/src/modules/color/cmyk.ts @@ -0,0 +1,106 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; + +/** + * Returns a CMYK color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cmyk(fakerCore) // [0.31, 0.52, 0.32, 0.43] + * + * @since 7.0.0 + */ +export function cmyk(fakerCore: FakerCore): number[]; +/** + * Returns a CMYK color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated CMYK color. Defaults to `'decimal'`. + * + * @example + * cmyk(fakerCore) // [0.31, 0.52, 0.32, 0.43] + * cmyk(fakerCore, { format: 'css' }) // 'cmyk(35%, 39%, 68%, 60%)' + * cmyk(fakerCore, { format: 'binary' }) // (8-32 bits) x 4 + * + * @since 7.0.0 + */ +export function cmyk( + fakerCore: FakerCore, + options?: { + /** + * Format of generated CMYK color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + } +): string; +/** + * Returns a CMYK color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated CMYK color. Defaults to `'decimal'`. + * + * @example + * cmyk(fakerCore) // [0.31, 0.52, 0.32, 0.43] + * cmyk(fakerCore, { format: 'decimal' }) // [0.31, 0.52, 0.32, 0.43] + * + * @since 7.0.0 + */ +export function cmyk( + fakerCore: FakerCore, + options?: { + /** + * Format of generated CMYK color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + } +): number[]; +/** + * Returns a CMYK color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated CMYK color. Defaults to `'decimal'`. + * + * @example + * cmyk(fakerCore) // [0.31, 0.52, 0.32, 0.43] + * cmyk(fakerCore, { format: 'decimal' }) // [0.31, 0.52, 0.32, 0.43] + * cmyk(fakerCore, { format: 'css' }) // 'cmyk(35%, 39%, 68%, 60%)' + * cmyk(fakerCore, { format: 'binary' }) // (8-32 bits) x 4 + * + * @since 7.0.0 + */ +export function cmyk( + fakerCore: FakerCore, + options?: { + /** + * Format of generated CMYK color. + * + * @default 'decimal' + */ + format?: ColorFormat; + } +): string | number[]; + +export function cmyk( + fakerCore: FakerCore, + options: { format?: ColorFormat } = {} +): string | number[] { + const { format = 'decimal' } = options; + const color: string | number[] = Array.from({ length: 4 }, () => + float(fakerCore, { multipleOf: 0.01 }) + ); + return toColorFormat(color, format, 'cmyk'); +} diff --git a/src/modules/color/color-by-csscolor-space.ts b/src/modules/color/color-by-csscolor-space.ts new file mode 100644 index 00000000000..0934d8fb5e3 --- /dev/null +++ b/src/modules/color/color-by-csscolor-space.ts @@ -0,0 +1,132 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; +import type { CssSpaceType } from './css-supported-space'; + +/** + * Returns a random color based on CSS color space specified. + * + * @param fakerCore The FakerCore to use. + * + * @example + * colorByCSSColorSpace(fakerCore) // [0.93, 1, 0.82] + * + * @since 7.0.0 + */ +export function colorByCSSColorSpace(fakerCore: FakerCore): number[]; +/** + * Returns a random color based on CSS color space specified. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * @param options.space Color space to generate the color for. Defaults to `'sRGB'`. + * + * @example + * colorByCSSColorSpace(fakerCore) // [0.93, 1, 0.82] + * colorByCSSColorSpace(fakerCore, { format: 'css', space: 'display-p3' }) // color(display-p3 0.12 1 0.23) + * colorByCSSColorSpace(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function colorByCSSColorSpace( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + /** + * Color space to generate the color for. + * + * @default 'sRGB' + */ + space?: CssSpaceType; + } +): string; +/** + * Returns a random color based on CSS color space specified. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * @param options.space Color space to generate the color for. Defaults to `'sRGB'`. + * + * @example + * colorByCSSColorSpace(fakerCore) // [0.93, 1, 0.82] + * colorByCSSColorSpace(fakerCore, { format: 'decimal' }) // [0.12, 0.21, 0.31] + * + * @since 7.0.0 + */ +export function colorByCSSColorSpace( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + /** + * Color space to generate the color for. + * + * @default 'sRGB' + */ + space?: CssSpaceType; + } +): number[]; +/** + * Returns a random color based on CSS color space specified. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * @param options.space Color space to generate the color for. Defaults to `'sRGB'`. + * + * @example + * colorByCSSColorSpace(fakerCore) // [0.93, 1, 0.82] + * colorByCSSColorSpace(fakerCore, { format: 'decimal' }) // [0.12, 0.21, 0.31] + * colorByCSSColorSpace(fakerCore, { format: 'css', space: 'display-p3' }) // color(display-p3 0.12 1 0.23) + * colorByCSSColorSpace(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function colorByCSSColorSpace( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: ColorFormat; + /** + * Color space to generate the color for. + * + * @default 'sRGB' + */ + space?: CssSpaceType; + } +): string | number[]; + +export function colorByCSSColorSpace( + fakerCore: FakerCore, + options: { + format?: ColorFormat; + space?: CssSpaceType; + } = {} +): string | number[] { + const { format = 'decimal', space = 'sRGB' } = options; + + const color = Array.from({ length: 3 }, () => + float(fakerCore, { multipleOf: 0.0001 }) + ); + return toColorFormat(color, format, 'color', space); +} diff --git a/src/modules/color/css-supported-function.ts b/src/modules/color/css-supported-function.ts new file mode 100644 index 00000000000..3b2f39fafa3 --- /dev/null +++ b/src/modules/color/css-supported-function.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { enumValue } from '../helpers/enum-value'; + +/** + * Functions supported by CSS to produce color. + */ +export enum CssFunction { + RGB = 'rgb', + RGBA = 'rgba', + HSL = 'hsl', + HSLA = 'hsla', + HWB = 'hwb', + CMYK = 'cmyk', + LAB = 'lab', + LCH = 'lch', + COLOR = 'color', +} + +/** + * Functions supported by CSS to produce color. + */ +export type CssFunctionType = `${CssFunction}`; + +/** + * Returns a random CSS-supported color function name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cssSupportedFunction(fakerCore) // 'rgb' + * + * @since 7.0.0 + */ +export function cssSupportedFunction(fakerCore: FakerCore): CssFunctionType { + return enumValue(fakerCore, CssFunction); +} diff --git a/src/modules/color/css-supported-space.ts b/src/modules/color/css-supported-space.ts new file mode 100644 index 00000000000..562817f7d53 --- /dev/null +++ b/src/modules/color/css-supported-space.ts @@ -0,0 +1,32 @@ +import type { FakerCore } from '../../core'; +import { enumValue } from '../helpers/enum-value'; + +/** + * Color space names supported by CSS. + */ +export enum CssSpace { + SRGB = 'sRGB', + DisplayP3 = 'display-p3', + REC2020 = 'rec2020', + A98RGB = 'a98-rgb', + ProphotoRGB = 'prophoto-rgb', +} + +/** + * Color space names supported by CSS. + */ +export type CssSpaceType = `${CssSpace}`; + +/** + * Returns a random CSS-supported color space name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * cssSupportedSpace(fakerCore) // 'display-p3' + * + * @since 7.0.0 + */ +export function cssSupportedSpace(fakerCore: FakerCore): CssSpaceType { + return enumValue(fakerCore, CssSpace); +} diff --git a/src/modules/color/hsl.ts b/src/modules/color/hsl.ts new file mode 100644 index 00000000000..f4956ccb0e6 --- /dev/null +++ b/src/modules/color/hsl.ts @@ -0,0 +1,139 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { int } from '../number/int'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; + +/** + * Returns an HSL color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * hsl(fakerCore) // [201, 0.23, 0.32] + * + * @since 7.0.0 + */ +export function hsl(fakerCore: FakerCore): number[]; +/** + * Returns an HSL color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated HSL color. Defaults to `'decimal'`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * hsl(fakerCore) // [201, 0.23, 0.32] + * hsl(fakerCore, { format: 'css' }) // hsl(0deg, 100%, 80%) + * hsl(fakerCore, { format: 'css', includeAlpha: true }) // hsl(0deg 100% 50% / 0.5) + * hsl(fakerCore, { format: 'binary' }) // (8-32 bits) x 3 + * hsl(fakerCore, { format: 'binary', includeAlpha: true }) // (8-32 bits) x 4 + * + * @since 7.0.0 + */ +export function hsl( + fakerCore: FakerCore, + options?: { + /** + * Format of generated HSL color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): string; +/** + * Returns an HSL color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated HSL color. Defaults to `'decimal'`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * hsl(fakerCore) // [201, 0.23, 0.32] + * hsl(fakerCore, { format: 'decimal' }) // [300, 0.21, 0.52] + * hsl(fakerCore, { format: 'decimal', includeAlpha: true }) // [300, 0.21, 0.52, 0.28] + * + * @since 7.0.0 + */ +export function hsl( + fakerCore: FakerCore, + options?: { + /** + * Format of generated HSL color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): number[]; +/** + * Returns an HSL color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated HSL color. Defaults to `'decimal'`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * hsl(fakerCore) // [201, 0.23, 0.32] + * hsl(fakerCore, { format: 'decimal' }) // [300, 0.21, 0.52] + * hsl(fakerCore, { format: 'decimal', includeAlpha: true }) // [300, 0.21, 0.52, 0.28] + * hsl(fakerCore, { format: 'css' }) // hsl(0deg, 100%, 80%) + * hsl(fakerCore, { format: 'css', includeAlpha: true }) // hsl(0deg 100% 50% / 0.5) + * hsl(fakerCore, { format: 'binary' }) // (8-32 bits) x 3 + * hsl(fakerCore, { format: 'binary', includeAlpha: true }) // (8-32 bits) x 4 + * + * @since 7.0.0 + */ +export function hsl( + fakerCore: FakerCore, + options?: { + /** + * Format of generated HSL color. + * + * @default 'decimal' + */ + format?: ColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): string | number[]; + +export function hsl( + fakerCore: FakerCore, + options: { + format?: ColorFormat; + includeAlpha?: boolean; + } = {} +): string | number[] { + const { format = 'decimal', includeAlpha = false } = options; + const hsl: number[] = [int(fakerCore, 360)]; + for (let i = 0; i < (options?.includeAlpha ? 3 : 2); i++) { + hsl.push(float(fakerCore, { multipleOf: 0.01 })); + } + + return toColorFormat(hsl, format, includeAlpha ? 'hsla' : 'hsl'); +} diff --git a/src/modules/color/human.ts b/src/modules/color/human.ts new file mode 100644 index 00000000000..d1bd6d49c6a --- /dev/null +++ b/src/modules/color/human.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random human-readable color name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * human(fakerCore) // 'red' + * + * @since 7.0.0 + */ +export function human(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.color.human); +} diff --git a/src/modules/color/hwb.ts b/src/modules/color/hwb.ts new file mode 100644 index 00000000000..913a13f0cf8 --- /dev/null +++ b/src/modules/color/hwb.ts @@ -0,0 +1,130 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { int } from '../number/int'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; + +/** + * Returns an HWB color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * hwb(fakerCore) // [201, 0.21, 0.31] + * + * @since 7.0.0 + */ +export function hwb(fakerCore: FakerCore): number[]; +/** + * Returns an HWB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * hwb(fakerCore) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'css' }) // 'hwb(354 72% 41%)' + * hwb(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function hwb( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + } +): string; +/** + * Returns an HWB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * hwb(fakerCore) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'decimal' }) // [201, 0.21, 0.31] + * + * @since 7.0.0 + */ +export function hwb( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + } +): number[]; +/** + * Returns an HWB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * hwb(fakerCore) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'decimal' }) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'css' }) // 'hwb(354 72% 41%)' + * hwb(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function hwb( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: ColorFormat; + } +): string | number[]; +/** + * Returns an HWB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * hwb(fakerCore) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'decimal' }) // [201, 0.21, 0.31] + * hwb(fakerCore, { format: 'css' }) // 'hwb(354 72% 41%)' + * hwb(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function hwb( + fakerCore: FakerCore, + options: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: ColorFormat; + } = {} +): string | number[] { + const { format = 'decimal' } = options; + const hsl: number[] = [int(fakerCore, 360)]; + for (let i = 0; i < 2; i++) { + hsl.push(float(fakerCore, { multipleOf: 0.01 })); + } + + return toColorFormat(hsl, format, 'hwb'); +} diff --git a/src/modules/color/index.ts b/src/modules/color/index.ts index dec2df446bd..7b2c5ab681a 100644 --- a/src/modules/color/index.ts +++ b/src/modules/color/index.ts @@ -1,202 +1,33 @@ import { ModuleBase } from '../../internal/module-base'; import type { Casing } from '../../utils/types'; - -/** - * Color space names supported by CSS. - */ -export enum CssSpace { - SRGB = 'sRGB', - DisplayP3 = 'display-p3', - REC2020 = 'rec2020', - A98RGB = 'a98-rgb', - ProphotoRGB = 'prophoto-rgb', -} - -/** - * Color space names supported by CSS. - */ -export type CssSpaceType = `${CssSpace}`; - -/** - * Functions supported by CSS to produce color. - */ -export enum CssFunction { - RGB = 'rgb', - RGBA = 'rgba', - HSL = 'hsl', - HSLA = 'hsla', - HWB = 'hwb', - CMYK = 'cmyk', - LAB = 'lab', - LCH = 'lch', - COLOR = 'color', -} - -/** - * Functions supported by CSS to produce color. - */ -export type CssFunctionType = `${CssFunction}`; - -export type StringColorFormat = 'css' | 'binary'; -export type NumberColorFormat = 'decimal'; -export type ColorFormat = StringColorFormat | NumberColorFormat; - -/** - * Formats the hex format of a generated color string according - * to options specified by user. - * - * @param hexColor Hex color string to be formatted. - * @param options Options object. - * @param options.prefix Prefix of the generated hex color. - * @param options.casing Letter type case of the generated hex color. - */ -function formatHexColor( - hexColor: string, - options: { - prefix: string; - casing: Casing; - } -): string { - const { prefix, casing } = options; - - switch (casing) { - case 'upper': { - hexColor = hexColor.toUpperCase(); - break; - } - - case 'lower': { - hexColor = hexColor.toLowerCase(); - break; - } - - case 'mixed': - // Do nothing - } - - if (prefix) { - hexColor = prefix + hexColor; - } - - return hexColor; -} - -/** - * Converts an array of numbers into binary string format. - * - * @param values Array of values to be converted. - */ -function toBinary(values: number[]): string { - const binary: string[] = values.map((value) => { - const isFloat = value % 1 !== 0; - if (isFloat) { - const buffer = new ArrayBuffer(4); - new DataView(buffer).setFloat32(0, value); - const bytes = new Uint8Array(buffer); - return toBinary([...bytes]).replaceAll(' ', ''); - } - - return (value >>> 0).toString(2).padStart(8, '0'); - }); - return binary.join(' '); -} - -/** - * Converts the given value to a percentage (`round(value * 100)`). - * - * @param value The value to convert to a percentage. - */ -function toPercentage(value: number): number { - return Math.round(value * 100); -} - -/** - * Converts an array of numbers into CSS accepted format. - * - * @param values Array of values to be converted. - * @param cssFunction CSS function to be generated for the color. Defaults to `'rgb'`. - * @param space Color space to format CSS color function with. Defaults to `'sRGB'`. - */ -function toCSS( - values: number[], - cssFunction: CssFunctionType = 'rgb', - space: CssSpaceType = 'sRGB' -): string { - switch (cssFunction) { - case 'rgba': { - return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${values[3]})`; - } - - case 'color': { - return `color(${space} ${values[0]} ${values[1]} ${values[2]})`; - } - - case 'cmyk': { - return `cmyk(${toPercentage(values[0])}%, ${toPercentage( - values[1] - )}%, ${toPercentage(values[2])}%, ${toPercentage(values[3])}%)`; - } - - case 'hsl': { - return `hsl(${values[0]}deg ${toPercentage(values[1])}% ${toPercentage( - values[2] - )}%)`; - } - - case 'hsla': { - return `hsl(${values[0]}deg ${toPercentage(values[1])}% ${toPercentage( - values[2] - )}% / ${toPercentage(values[3])})`; - } - - case 'hwb': { - return `hwb(${values[0]} ${toPercentage(values[1])}% ${toPercentage( - values[2] - )}%)`; - } - - case 'lab': { - return `lab(${toPercentage(values[0])}% ${values[1]} ${values[2]})`; - } - - case 'lch': { - return `lch(${toPercentage(values[0])}% ${values[1]} ${values[2]})`; - } - - case 'rgb': { - return `rgb(${values[0]}, ${values[1]}, ${values[2]})`; - } - } -} - -/** - * Converts an array of color values to the specified color format. - * - * @param values Array of color values to be converted. - * @param format Format of generated RGB color. - * @param cssFunction CSS function to be generated for the color. Defaults to `'rgb'`. - * @param space Color space to format CSS color function with. Defaults to `'sRGB'`. - */ -function toColorFormat( - values: number[], - format: ColorFormat, - cssFunction: CssFunctionType = 'rgb', - space: CssSpaceType = 'sRGB' -): string | number[] { - switch (format) { - case 'css': { - return toCSS(values, cssFunction, space); - } - - case 'binary': { - return toBinary(values); - } - - case 'decimal': { - return values; - } - } -} +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; +import { cmyk as colorCmyk } from './cmyk'; +import { colorByCSSColorSpace as colorColorByCSSColorSpace } from './color-by-csscolor-space'; +import type { CssFunctionType } from './css-supported-function'; +import { cssSupportedFunction as colorCssSupportedFunction } from './css-supported-function'; +import type { CssSpaceType } from './css-supported-space'; +import { cssSupportedSpace as colorCssSupportedSpace } from './css-supported-space'; +import { hsl as colorHsl } from './hsl'; +import { human as colorHuman } from './human'; +import { hwb as colorHwb } from './hwb'; +import { lab as colorLab } from './lab'; +import { lch as colorLch } from './lch'; +import { rgb as colorRgb } from './rgb'; +import { space as colorSpace } from './space'; + +export type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; +export { CssFunction } from './css-supported-function'; +export type { CssFunctionType } from './css-supported-function'; +export { CssSpace } from './css-supported-space'; +export type { CssSpaceType } from './css-supported-space'; /** * Module to generate colors. @@ -217,7 +48,7 @@ export class ColorModule extends ModuleBase { * @since 7.0.0 */ human(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.color.human); + return colorHuman(this.faker.fakerCore); } /** @@ -230,7 +61,7 @@ export class ColorModule extends ModuleBase { * @since 7.0.0 */ space(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.color.space); + return colorSpace(this.faker.fakerCore); } /** @@ -242,7 +73,7 @@ export class ColorModule extends ModuleBase { * @since 7.0.0 */ cssSupportedFunction(): CssFunctionType { - return this.faker.helpers.enumValue(CssFunction); + return colorCssSupportedFunction(this.faker.fakerCore); } /** @@ -254,7 +85,7 @@ export class ColorModule extends ModuleBase { * @since 7.0.0 */ cssSupportedSpace(): CssSpaceType { - return this.faker.helpers.enumValue(CssSpace); + return colorCssSupportedSpace(this.faker.fakerCore); } /** @@ -402,30 +233,7 @@ export class ColorModule extends ModuleBase { includeAlpha?: boolean; } = {} ): string | number[] { - const { - format = 'hex', - includeAlpha = false, - prefix = '#', - casing = 'lower', - } = options; - let color: string | number[]; - let cssFunction: CssFunctionType = 'rgb'; - if (format === 'hex') { - color = this.faker.string.hexadecimal({ - length: includeAlpha ? 8 : 6, - prefix: '', - }); - color = formatHexColor(color, { prefix, casing }); - return color; - } - - color = Array.from({ length: 3 }, () => this.faker.number.int(255)); - if (includeAlpha) { - color.push(this.faker.number.float({ multipleOf: 0.01 })); - cssFunction = 'rgba'; - } - - return toColorFormat(color, format, cssFunction); + return colorRgb(this.faker.fakerCore, options); } /** @@ -501,11 +309,7 @@ export class ColorModule extends ModuleBase { format?: ColorFormat; }): string | number[]; cmyk(options: { format?: ColorFormat } = {}): string | number[] { - const { format = 'decimal' } = options; - const color: string | number[] = Array.from({ length: 4 }, () => - this.faker.number.float({ multipleOf: 0.01 }) - ); - return toColorFormat(color, format, 'cmyk'); + return colorCmyk(this.faker.fakerCore, options); } /** @@ -613,13 +417,7 @@ export class ColorModule extends ModuleBase { includeAlpha?: boolean; } = {} ): string | number[] { - const { format = 'decimal', includeAlpha = false } = options; - const hsl: number[] = [this.faker.number.int(360)]; - for (let i = 0; i < (options?.includeAlpha ? 3 : 2); i++) { - hsl.push(this.faker.number.float({ multipleOf: 0.01 })); - } - - return toColorFormat(hsl, format, includeAlpha ? 'hsla' : 'hsl'); + return colorHsl(this.faker.fakerCore, options); } /** @@ -718,13 +516,7 @@ export class ColorModule extends ModuleBase { format?: ColorFormat; } = {} ): string | number[] { - const { format = 'decimal' } = options; - const hsl: number[] = [this.faker.number.int(360)]; - for (let i = 0; i < 2; i++) { - hsl.push(this.faker.number.float({ multipleOf: 0.01 })); - } - - return toColorFormat(hsl, format, 'hwb'); + return colorHwb(this.faker.fakerCore, options); } /** @@ -800,15 +592,7 @@ export class ColorModule extends ModuleBase { format?: ColorFormat; }): string | number[]; lab(options: { format?: ColorFormat } = {}): string | number[] { - const { format = 'decimal' } = options; - const lab = [this.faker.number.float({ multipleOf: 0.000001 })]; - for (let i = 0; i < 2; i++) { - lab.push( - this.faker.number.float({ min: -100, max: 100, multipleOf: 0.0001 }) - ); - } - - return toColorFormat(lab, format, 'lab'); + return colorLab(this.faker.fakerCore, options); } /** @@ -896,13 +680,7 @@ export class ColorModule extends ModuleBase { format?: ColorFormat; }): string | number[]; lch(options: { format?: ColorFormat } = {}): string | number[] { - const { format = 'decimal' } = options; - const lch = [this.faker.number.float({ multipleOf: 0.000001 })]; - for (let i = 0; i < 2; i++) { - lch.push(this.faker.number.float({ max: 230, multipleOf: 0.1 })); - } - - return toColorFormat(lch, format, 'lch'); + return colorLch(this.faker.fakerCore, options); } /** @@ -1004,11 +782,6 @@ export class ColorModule extends ModuleBase { space?: CssSpaceType; } = {} ): string | number[] { - const { format = 'decimal', space = 'sRGB' } = options; - - const color = Array.from({ length: 3 }, () => - this.faker.number.float({ multipleOf: 0.0001 }) - ); - return toColorFormat(color, format, 'color', space); + return colorColorByCSSColorSpace(this.faker.fakerCore, options); } } diff --git a/src/modules/color/lab.ts b/src/modules/color/lab.ts new file mode 100644 index 00000000000..0ae4d106ec5 --- /dev/null +++ b/src/modules/color/lab.ts @@ -0,0 +1,108 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; + +/** + * Returns a LAB (CIELAB) color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * lab(fakerCore) // [0.832133, -80.3245, 100.1234] + * + * @since 7.0.0 + */ +export function lab(fakerCore: FakerCore): number[]; +/** + * Returns a LAB (CIELAB) color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lab(fakerCore) // [0.832133, -80.3245, 100.1234] + * lab(fakerCore, { format: 'css' }) // 'lab(29.2345% 39.3825 20.0664)' + * lab(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function lab( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + } +): string; +/** + * Returns a LAB (CIELAB) color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lab(fakerCore) // [0.832133, -80.3245, 100.1234] + * lab(fakerCore, { format: 'decimal' }) // [0.856773, -80.2345, 100.2341] + * + * @since 7.0.0 + */ +export function lab( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + } +): number[]; +/** + * Returns a LAB (CIELAB) color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lab(fakerCore) // [0.832133, -80.3245, 100.1234] + * lab(fakerCore, { format: 'decimal' }) // [0.856773, -80.2345, 100.2341] + * lab(fakerCore, { format: 'css' }) // 'lab(29.2345% 39.3825 20.0664)' + * lab(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function lab( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: ColorFormat; + } +): string | number[]; + +export function lab( + fakerCore: FakerCore, + options: { format?: ColorFormat } = {} +): string | number[] { + const { format = 'decimal' } = options; + const lab = [float(fakerCore, { multipleOf: 0.000001 })]; + for (let i = 0; i < 2; i++) { + lab.push(float(fakerCore, { min: -100, max: 100, multipleOf: 0.0001 })); + } + + return toColorFormat(lab, format, 'lab'); +} diff --git a/src/modules/color/lch.ts b/src/modules/color/lch.ts new file mode 100644 index 00000000000..67a7489e730 --- /dev/null +++ b/src/modules/color/lch.ts @@ -0,0 +1,120 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; + +/** + * Returns an LCH color. Even though upper bound of + * chroma in LCH color space is theoretically unbounded, + * it is bounded to 230 as anything above will not + * make a noticeable difference in the browser. + * + * @param fakerCore The FakerCore to use. + * + * @example + * lch(fakerCore) // [0.522345, 72.2, 56.2] + * + * @since 7.0.0 + */ +export function lch(fakerCore: FakerCore): number[]; +/** + * Returns an LCH color. Even though upper bound of + * chroma in LCH color space is theoretically unbounded, + * it is bounded to 230 as anything above will not + * make a noticeable difference in the browser. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lch(fakerCore) // [0.522345, 72.2, 56.2] + * lch(fakerCore, { format: 'css' }) // 'lch(52.2345% 72.2 56.2)' + * lch(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function lch( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: StringColorFormat; + } +): string; +/** + * Returns an LCH color. Even though upper bound of + * chroma in LCH color space is theoretically unbounded, + * it is bounded to 230 as anything above will not + * make a noticeable difference in the browser. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lch(fakerCore) // [0.522345, 72.2, 56.2] + * lch(fakerCore, { format: 'decimal' }) // [0.522345, 72.2, 56.2] + * + * @since 7.0.0 + */ +export function lch( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: NumberColorFormat; + } +): number[]; +/** + * Returns an LCH color. Even though upper bound of + * chroma in LCH color space is theoretically unbounded, + * it is bounded to 230 as anything above will not + * make a noticeable difference in the browser. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'decimal'`. + * + * @example + * lch(fakerCore) // [0.522345, 72.2, 56.2] + * lch(fakerCore, { format: 'decimal' }) // [0.522345, 72.2, 56.2] + * lch(fakerCore, { format: 'css' }) // 'lch(52.2345% 72.2 56.2)' + * lch(fakerCore, { format: 'binary' }) // (8-32 bits x 3) + * + * @since 7.0.0 + */ +export function lch( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'decimal' + */ + format?: ColorFormat; + } +): string | number[]; + +export function lch( + fakerCore: FakerCore, + options: { format?: ColorFormat } = {} +): string | number[] { + const { format = 'decimal' } = options; + const lch = [float(fakerCore, { multipleOf: 0.000001 })]; + for (let i = 0; i < 2; i++) { + lch.push(float(fakerCore, { max: 230, multipleOf: 0.1 })); + } + + return toColorFormat(lch, format, 'lch'); +} diff --git a/src/modules/color/rgb.ts b/src/modules/color/rgb.ts new file mode 100644 index 00000000000..7c1ea0b8427 --- /dev/null +++ b/src/modules/color/rgb.ts @@ -0,0 +1,200 @@ +import type { FakerCore } from '../../core'; +import type { Casing } from '../../utils/types'; +import { float } from '../number/float'; +import { int } from '../number/int'; +import { hexadecimal } from '../string/hexadecimal'; +import { formatHexColor } from './_format-hex-color'; +import { toColorFormat } from './_to-color-format'; +import type { + ColorFormat, + NumberColorFormat, + StringColorFormat, +} from './_types'; +import type { CssFunctionType } from './css-supported-function'; + +/** + * Returns an RGB color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * rgb(fakerCore) // '#8be4ab' + * + * @since 7.0.0 + */ +export function rgb(fakerCore: FakerCore): string; +/** + * Returns an RGB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.prefix Prefix of the generated hex color. Only applied when 'hex' format is used. Defaults to `'#'`. + * @param options.casing Letter type case of the generated hex color. Only applied when `'hex'` format is used. Defaults to `'lower'`. + * @param options.format Format of generated RGB color. Defaults to `hex`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * rgb(fakerCore) // '#0d7f26' + * rgb(fakerCore, { prefix: '0x' }) // '0x9ddc8b' + * rgb(fakerCore, { casing: 'upper' }) // '#B8A51E' + * rgb(fakerCore, { casing: 'lower' }) // '#b12f8b' + * rgb(fakerCore, { prefix: '#', casing: 'lower' }) // '#eb0c16' + * rgb(fakerCore, { format: 'hex', casing: 'lower' }) // '#bb9d17' + * rgb(fakerCore, { format: 'css' }) // 'rgb(216, 17, 192)' + * rgb(fakerCore, { format: 'binary' }) // '00110010 00001000 01110110' + * rgb(fakerCore, { includeAlpha: true }) // '#f96efb5e' + * rgb(fakerCore, { format: 'css', includeAlpha: true }) // 'rgba(180, 158, 24, 0.75)' + * + * @since 7.0.0 + */ +export function rgb( + fakerCore: FakerCore, + options?: { + /** + * Prefix of the generated hex color. Only applied when 'hex' format is used. + * + * @default '#' + */ + prefix?: string; + /** + * Letter type case of the generated hex color. Only applied when `'hex'` format is used. + * + * @default 'lower' + */ + casing?: Casing; + /** + * Format of generated RGB color. + * + * @default 'hex' + */ + format?: 'hex' | StringColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): string; +/** + * Returns an RGB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.format Format of generated RGB color. Defaults to `'hex'`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * rgb(fakerCore) // '0x8be4ab' + * rgb(fakerCore, { format: 'decimal' }) // [64, 192,174] + * rgb(fakerCore, { format: 'decimal', includeAlpha: true }) // [52, 250, 209, 0.21] + * + * @since 7.0.0 + */ +export function rgb( + fakerCore: FakerCore, + options?: { + /** + * Format of generated RGB color. + * + * @default 'hex' + */ + format?: NumberColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): number[]; +/** + * Returns an RGB color. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.prefix Prefix of the generated hex color. Only applied when `'hex'` format is used. Defaults to `'#'`. + * @param options.casing Letter type case of the generated hex color. Only applied when `'hex'` format is used. Defaults to `'lower'`. + * @param options.format Format of generated RGB color. Defaults to `'hex'`. + * @param options.includeAlpha Adds an alpha value to the color (RGBA). Defaults to `false`. + * + * @example + * rgb(fakerCore) // '#0d7f26' + * rgb(fakerCore, { prefix: '0x' }) // '0x9ddc8b' + * rgb(fakerCore, { casing: 'upper' }) // '#B8A51E' + * rgb(fakerCore, { casing: 'lower' }) // '#b12f8b' + * rgb(fakerCore, { prefix: '#', casing: 'lower' }) // '#eb0c16' + * rgb(fakerCore, { format: 'hex', casing: 'lower' }) // '#bb9d17' + * rgb(fakerCore, { format: 'decimal' }) // [64, 192,174] + * rgb(fakerCore, { format: 'css' }) // 'rgb(216, 17, 192)' + * rgb(fakerCore, { format: 'binary' }) // '00110010 00001000 01110110' + * rgb(fakerCore, { includeAlpha: true }) // '#f96efb5e' + * rgb(fakerCore, { format: 'css', includeAlpha: true }) // 'rgba(180, 158, 24, 0.75)' + * rgb(fakerCore, { format: 'decimal', includeAlpha: true }) // [52, 250, 209, 0.21] + * + * @since 7.0.0 + */ +export function rgb( + fakerCore: FakerCore, + options?: { + /** + * Prefix of the generated hex color. Only applied when `'hex'` format is used. + * + * @default '#' + */ + prefix?: string; + /** + * Letter type case of the generated hex color. Only applied when `'hex'` format is used. + * + * @default 'lower' + */ + casing?: Casing; + /** + * Format of generated RGB color. + * + * @default 'hex' + */ + format?: 'hex' | ColorFormat; + /** + * Adds an alpha value to the color (RGBA). + * + * @default false + */ + includeAlpha?: boolean; + } +): string | number[]; + +export function rgb( + fakerCore: FakerCore, + options: { + prefix?: string; + casing?: Casing; + format?: 'hex' | ColorFormat; + includeAlpha?: boolean; + } = {} +): string | number[] { + const { + format = 'hex', + includeAlpha = false, + prefix = '#', + casing = 'lower', + } = options; + let color: string | number[]; + let cssFunction: CssFunctionType = 'rgb'; + if (format === 'hex') { + color = hexadecimal(fakerCore, { + length: includeAlpha ? 8 : 6, + prefix: '', + }); + color = formatHexColor(color, { prefix, casing }); + return color; + } + + color = Array.from({ length: 3 }, () => int(fakerCore, 255)); + if (includeAlpha) { + color.push(float(fakerCore, { multipleOf: 0.01 })); + cssFunction = 'rgba'; + } + + return toColorFormat(color, format, cssFunction); +} diff --git a/src/modules/color/space.ts b/src/modules/color/space.ts new file mode 100644 index 00000000000..e4da8df6b6d --- /dev/null +++ b/src/modules/color/space.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random color space name from the worldwide accepted color spaces. + * Source: https://en.wikipedia.org/wiki/List_of_color_spaces_and_their_uses + * + * @param fakerCore The FakerCore to use. + * + * @example + * space(fakerCore) // 'sRGB' + * + * @since 7.0.0 + */ +export function space(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.color.space); +} diff --git a/src/modules/commerce/department.ts b/src/modules/commerce/department.ts new file mode 100644 index 00000000000..6ad8a4bd81f --- /dev/null +++ b/src/modules/commerce/department.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a department inside a shop. + * + * @param fakerCore The FakerCore to use. + * + * @example + * department(fakerCore) // 'Garden' + * + * @since 3.0.0 + */ +export function department(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.commerce.department); +} diff --git a/src/modules/commerce/index.ts b/src/modules/commerce/index.ts index ef27c161e2b..c0ae7ec8f31 100644 --- a/src/modules/commerce/index.ts +++ b/src/modules/commerce/index.ts @@ -1,79 +1,13 @@ -import { FakerError } from '../../errors/faker-error'; import { ModuleBase } from '../../internal/module-base'; -import { calculateUPCCheckDigit } from './upc-check-digit'; - -// Source for official prefixes: https://www.isbn-international.org/range_file_generation -const ISBN_LENGTH_RULES: Record< - string, - Array<[rangeMaximum: number, length: number]> -> = { - '0': [ - [1999999, 2], - [2279999, 3], - [2289999, 4], - [3689999, 3], - [3699999, 4], - [6389999, 3], - [6397999, 4], - [6399999, 7], - [6449999, 3], - [6459999, 7], - [6479999, 3], - [6489999, 7], - [6549999, 3], - [6559999, 4], - [6999999, 3], - [8499999, 4], - [8999999, 5], - [9499999, 6], - [9999999, 7], - ], - '1': [ - [99999, 3], - [299999, 2], - [349999, 3], - [399999, 4], - [499999, 3], - [699999, 2], - [999999, 4], - [3979999, 3], - [5499999, 4], - [6499999, 5], - [6799999, 4], - [6859999, 5], - [7139999, 4], - [7169999, 3], - [7319999, 4], - [7399999, 7], - [7749999, 5], - [7753999, 7], - [7763999, 5], - [7764999, 7], - [7769999, 5], - [7782999, 7], - [7899999, 5], - [7999999, 4], - [8004999, 5], - [8049999, 5], - [8379999, 5], - [8384999, 7], - [8671999, 5], - [8675999, 4], - [8697999, 5], - [9159999, 6], - [9165059, 7], - [9168699, 6], - [9169079, 7], - [9195999, 6], - [9196549, 7], - [9729999, 6], - [9877999, 4], - [9911499, 6], - [9911999, 7], - [9989899, 6], - [9999999, 7], - ], -}; +import { department as commerceDepartment } from './department'; +import { isbn as commerceIsbn } from './isbn'; +import { price as commercePrice } from './price'; +import { product as commerceProduct } from './product'; +import { productAdjective as commerceProductAdjective } from './product-adjective'; +import { productDescription as commerceProductDescription } from './product-description'; +import { productMaterial as commerceProductMaterial } from './product-material'; +import { productName as commerceProductName } from './product-name'; +import { upc as commerceUpc } from './upc'; /** * Module to generate commerce and product related entries. @@ -98,9 +32,7 @@ export class CommerceModule extends ModuleBase { * @since 3.0.0 */ department(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.commerce.department - ); + return commerceDepartment(this.faker.fakerCore); } /** @@ -112,8 +44,7 @@ export class CommerceModule extends ModuleBase { * @since 3.0.0 */ productName(): string { - const patterns = this.faker.definitions.commerce.product_name.pattern; - return this.faker.helpers.fake(patterns); + return commerceProductName(this.faker.fakerCore); } /** @@ -169,47 +100,7 @@ export class CommerceModule extends ModuleBase { symbol?: string; } = {} ): string { - const { dec = 2, max = 1000, min = 1, symbol = '' } = options; - - if (min < 0 || max < 0) { - return `${symbol}0`; - } - - if (min === max) { - return `${symbol}${min.toFixed(dec)}`; - } - - const generated = this.faker.number.float({ - min, - max, - fractionDigits: dec, - }); - - if (dec === 0) { - return `${symbol}${generated.toFixed(dec)}`; - } - - const oldLastDigit = (generated * 10 ** dec) % 10; - const newLastDigit = this.faker.helpers.weightedArrayElement([ - { weight: 5, value: 9 }, - { weight: 3, value: 5 }, - { weight: 1, value: 0 }, - { - weight: 1, - value: this.faker.number.int({ min: 0, max: 9 }), - }, - ]); - - const fraction = (1 / 10) ** dec; - const oldLastDigitValue = oldLastDigit * fraction; - const newLastDigitValue = newLastDigit * fraction; - const combined = generated - oldLastDigitValue + newLastDigitValue; - - if (min <= combined && combined <= max) { - return `${symbol}${combined.toFixed(dec)}`; - } - - return `${symbol}${generated.toFixed(dec)}`; + return commercePrice(this.faker.fakerCore, options); } /** @@ -221,9 +112,7 @@ export class CommerceModule extends ModuleBase { * @since 3.0.0 */ productAdjective(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.commerce.product_name.adjective - ); + return commerceProductAdjective(this.faker.fakerCore); } /** @@ -235,9 +124,7 @@ export class CommerceModule extends ModuleBase { * @since 3.0.0 */ productMaterial(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.commerce.product_name.material - ); + return commerceProductMaterial(this.faker.fakerCore); } /** @@ -249,9 +136,7 @@ export class CommerceModule extends ModuleBase { * @since 3.0.0 */ product(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.commerce.product_name.product - ); + return commerceProduct(this.faker.fakerCore); } /** @@ -263,9 +148,7 @@ export class CommerceModule extends ModuleBase { * @since 5.0.0 */ productDescription(): string { - return this.faker.helpers.fake( - this.faker.definitions.commerce.product_description - ); + return commerceProductDescription(this.faker.fakerCore); } /** @@ -308,50 +191,7 @@ export class CommerceModule extends ModuleBase { separator?: string; } = {} ): string { - if (typeof options === 'number') { - options = { variant: options }; - } - - const { variant = 13, separator = '-' } = options; - - const prefix = '978'; - const [group, groupRules] = - this.faker.helpers.objectEntry(ISBN_LENGTH_RULES); - const element = this.faker.string.numeric(8); - const elementValue = Number.parseInt(element.slice(0, -1)); - - const registrantLength = groupRules.find( - ([rangeMaximum]) => elementValue <= rangeMaximum - )?.[1]; - - if (!registrantLength) { - // This can only happen if the ISBN_LENGTH_RULES are corrupted - throw new FakerError( - `Unable to find a registrant length for the group ${group}` - ); - } - - const registrant = element.slice(0, registrantLength); - const publication = element.slice(registrantLength); - - const data = [prefix, group, registrant, publication]; - if (variant === 10) { - data.shift(); - } - - const isbn = data.join(''); - - let checksum = 0; - for (let i = 0; i < variant - 1; i++) { - const weight = variant === 10 ? i + 1 : i % 2 ? 3 : 1; - checksum += weight * Number.parseInt(isbn[i]); - } - - checksum = variant === 10 ? checksum % 11 : (10 - (checksum % 10)) % 10; - - data.push(checksum === 10 ? 'X' : checksum.toString()); - - return data.join(separator); + return commerceIsbn(this.faker.fakerCore, options); } /** @@ -381,23 +221,6 @@ export class CommerceModule extends ModuleBase { prefix?: string; } = {} ): string { - const { prefix = '' } = options; - if (prefix && /\D/.test(prefix)) { - throw new FakerError('Prefix must contain only numeric digits'); - } - - if (prefix.length > 11) { - throw new FakerError('Prefix must be at most 11 numeric digits'); - } - - const remaining = 11 - prefix.length; - const rand = this.faker.string.numeric({ - length: remaining, - allowLeadingZeros: true, - }); - - const body = `${prefix}${rand}`; // 11 digits - const check = calculateUPCCheckDigit(body); - return `${body}${check}`; // 12-digit UPC-A + return commerceUpc(this.faker.fakerCore, options); } } diff --git a/src/modules/commerce/isbn.ts b/src/modules/commerce/isbn.ts new file mode 100644 index 00000000000..93b8565ca95 --- /dev/null +++ b/src/modules/commerce/isbn.ts @@ -0,0 +1,164 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { objectEntry } from '../helpers/object-entry'; +import { numeric } from '../string/numeric'; + +// Source for official prefixes: https://www.isbn-international.org/range_file_generation +const ISBN_LENGTH_RULES: Record< + string, + Array<[rangeMaximum: number, length: number]> +> = { + '0': [ + [1999999, 2], + [2279999, 3], + [2289999, 4], + [3689999, 3], + [3699999, 4], + [6389999, 3], + [6397999, 4], + [6399999, 7], + [6449999, 3], + [6459999, 7], + [6479999, 3], + [6489999, 7], + [6549999, 3], + [6559999, 4], + [6999999, 3], + [8499999, 4], + [8999999, 5], + [9499999, 6], + [9999999, 7], + ], + '1': [ + [99999, 3], + [299999, 2], + [349999, 3], + [399999, 4], + [499999, 3], + [699999, 2], + [999999, 4], + [3979999, 3], + [5499999, 4], + [6499999, 5], + [6799999, 4], + [6859999, 5], + [7139999, 4], + [7169999, 3], + [7319999, 4], + [7399999, 7], + [7749999, 5], + [7753999, 7], + [7763999, 5], + [7764999, 7], + [7769999, 5], + [7782999, 7], + [7899999, 5], + [7999999, 4], + [8004999, 5], + [8049999, 5], + [8379999, 5], + [8384999, 7], + [8671999, 5], + [8675999, 4], + [8697999, 5], + [9159999, 6], + [9165059, 7], + [9168699, 6], + [9169079, 7], + [9195999, 6], + [9196549, 7], + [9729999, 6], + [9877999, 4], + [9911499, 6], + [9911999, 7], + [9989899, 6], + [9999999, 7], + ], +}; + +/** + * Returns a random [ISBN](https://en.wikipedia.org/wiki/ISBN) identifier. + * + * @param fakerCore The FakerCore to use. + * @param options The variant to return or an options object. + * @param options.variant The variant to return. Can be either `10` (10-digit format) + * or `13` (13-digit format). Defaults to `13`. + * @param options.separator The separator to use in the format. Defaults to `'-'`. + * + * @example + * isbn(fakerCore) // '978-0-692-82459-7' + * isbn(fakerCore, 10) // '1-155-36404-X' + * isbn(fakerCore, 13) // '978-1-60808-867-6' + * isbn(fakerCore, { separator: ' ' }) // '978 0 452 81498 1' + * isbn(fakerCore, { variant: 10, separator: ' ' }) // '0 940319 49 7' + * isbn(fakerCore, { variant: 13, separator: ' ' }) // '978 1 6618 9122 0' + * + * @since 8.1.0 + */ +export function isbn( + fakerCore: FakerCore, + options: + | 10 + | 13 + | { + /** + * The variant of the identifier to return. + * Can be either `10` (10-digit format) + * or `13` (13-digit format). + * + * @default 13 + */ + variant?: 10 | 13; + + /** + * The separator to use in the format. + * + * @default '-' + */ + separator?: string; + } = {} +): string { + if (typeof options === 'number') { + options = { variant: options }; + } + + const { variant = 13, separator = '-' } = options; + + const prefix = '978'; + const [group, groupRules] = objectEntry(fakerCore, ISBN_LENGTH_RULES); + const element = numeric(fakerCore, 8); + const elementValue = Number.parseInt(element.slice(0, -1)); + + const registrantLength = groupRules.find( + ([rangeMaximum]) => elementValue <= rangeMaximum + )?.[1]; + + if (!registrantLength) { + // This can only happen if the ISBN_LENGTH_RULES are corrupted + throw new FakerError( + `Unable to find a registrant length for the group ${group}` + ); + } + + const registrant = element.slice(0, registrantLength); + const publication = element.slice(registrantLength); + + const data = [prefix, group, registrant, publication]; + if (variant === 10) { + data.shift(); + } + + const isbn = data.join(''); + + let checksum = 0; + for (let i = 0; i < variant - 1; i++) { + const weight = variant === 10 ? i + 1 : i % 2 ? 3 : 1; + checksum += weight * Number.parseInt(isbn[i]); + } + + checksum = variant === 10 ? checksum % 11 : (10 - (checksum % 10)) % 10; + + data.push(checksum === 10 ? 'X' : checksum.toString()); + + return data.join(separator); +} diff --git a/src/modules/commerce/price.ts b/src/modules/commerce/price.ts new file mode 100644 index 00000000000..1414f8f6d4f --- /dev/null +++ b/src/modules/commerce/price.ts @@ -0,0 +1,102 @@ +import type { FakerCore } from '../../core'; +import { weightedArrayElement } from '../helpers/weighted-array-element'; +import { float } from '../number/float'; +import { int } from '../number/int'; + +/** + * Generates a price between min and max (inclusive). + * + * To better represent real-world prices, when `options.dec` is greater than `0`, the final decimal digit in the returned string will be generated as follows: + * + * - 50% of the time: `9` + * - 30% of the time: `5` + * - 10% of the time: `0` + * - 10% of the time: a random digit from `0` to `9` + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.min The minimum price. Defaults to `1`. + * @param options.max The maximum price. Defaults to `1000`. + * @param options.dec The number of decimal places. Defaults to `2`. + * @param options.symbol The currency value to use. Defaults to `''`. + * + * @example + * price(fakerCore) // '828.07' + * price(fakerCore, { min: 100 }) // '904.19' + * price(fakerCore, { min: 100, max: 200 }) // '154.55' + * price(fakerCore, { min: 100, max: 200, dec: 0 }) // '133' + * price(fakerCore, { min: 100, max: 200, dec: 0, symbol: '$' }) // '$114' + * + * @since 3.0.0 + */ +export function price( + fakerCore: FakerCore, + options: { + /** + * The minimum price. + * + * @default 1 + */ + min?: number; + /** + * The maximum price. + * + * @default 1000 + */ + max?: number; + /** + * The number of decimal places. + * + * @default 2 + */ + dec?: number; + /** + * The currency value to use. + * + * @default '' + */ + symbol?: string; + } = {} +): string { + const { dec = 2, max = 1000, min = 1, symbol = '' } = options; + + if (min < 0 || max < 0) { + return `${symbol}0`; + } + + if (min === max) { + return `${symbol}${min.toFixed(dec)}`; + } + + const generated = float(fakerCore, { + min, + max, + fractionDigits: dec, + }); + + if (dec === 0) { + return `${symbol}${generated.toFixed(dec)}`; + } + + const oldLastDigit = (generated * 10 ** dec) % 10; + const newLastDigit = weightedArrayElement(fakerCore, [ + { weight: 5, value: 9 }, + { weight: 3, value: 5 }, + { weight: 1, value: 0 }, + { + weight: 1, + value: int(fakerCore, { min: 0, max: 9 }), + }, + ]); + + const fraction = (1 / 10) ** dec; + const oldLastDigitValue = oldLastDigit * fraction; + const newLastDigitValue = newLastDigit * fraction; + const combined = generated - oldLastDigitValue + newLastDigitValue; + + if (min <= combined && combined <= max) { + return `${symbol}${combined.toFixed(dec)}`; + } + + return `${symbol}${generated.toFixed(dec)}`; +} diff --git a/src/modules/commerce/product-adjective.ts b/src/modules/commerce/product-adjective.ts new file mode 100644 index 00000000000..6a8be23a450 --- /dev/null +++ b/src/modules/commerce/product-adjective.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns an adjective describing a product. + * + * @param fakerCore The FakerCore to use. + * + * @example + * productAdjective(fakerCore) // 'Handcrafted' + * + * @since 3.0.0 + */ +export function productAdjective(fakerCore: FakerCore): string { + return arrayElement( + fakerCore, + fakerCore.locale.commerce.product_name.adjective + ); +} diff --git a/src/modules/commerce/product-description.ts b/src/modules/commerce/product-description.ts new file mode 100644 index 00000000000..793359b6322 --- /dev/null +++ b/src/modules/commerce/product-description.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Returns a product description. + * + * @param fakerCore The FakerCore to use. + * + * @example + * productDescription(fakerCore) // 'Featuring Phosphorus-enhanced technology, our Fish offers unparalleled Modern performance' + * + * @since 5.0.0 + */ +export function productDescription(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.commerce.product_description + ); +} diff --git a/src/modules/commerce/product-material.ts b/src/modules/commerce/product-material.ts new file mode 100644 index 00000000000..fa9239eb6fd --- /dev/null +++ b/src/modules/commerce/product-material.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a material of a product. + * + * @param fakerCore The FakerCore to use. + * + * @example + * productMaterial(fakerCore) // 'Rubber' + * + * @since 3.0.0 + */ +export function productMaterial(fakerCore: FakerCore): string { + return arrayElement( + fakerCore, + fakerCore.locale.commerce.product_name.material + ); +} diff --git a/src/modules/commerce/product-name.ts b/src/modules/commerce/product-name.ts new file mode 100644 index 00000000000..8eb75590257 --- /dev/null +++ b/src/modules/commerce/product-name.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random descriptive product name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * productName(fakerCore) // 'Incredible Soft Gloves' + * + * @since 3.0.0 + */ +export function productName(fakerCore: FakerCore): string { + const patterns = fakerCore.locale.commerce.product_name.pattern; + return new Faker(fakerCore).helpers.fake(patterns); +} diff --git a/src/modules/commerce/product.ts b/src/modules/commerce/product.ts new file mode 100644 index 00000000000..454c30d2dcb --- /dev/null +++ b/src/modules/commerce/product.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a short product name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * product(fakerCore) // 'Computer' + * + * @since 3.0.0 + */ +export function product(fakerCore: FakerCore): string { + return arrayElement( + fakerCore, + fakerCore.locale.commerce.product_name.product + ); +} diff --git a/src/modules/commerce/upc-check-digit.ts b/src/modules/commerce/upc-check-digit.ts deleted file mode 100644 index a3911d15a33..00000000000 --- a/src/modules/commerce/upc-check-digit.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FakerError } from '../../errors/faker-error'; - -/** - * Calculates the check digit for a UPC‑A using the Modulo 10 algorithm. - * - * @param digits The first 11 digits (UPC body) as a numeric string. - * - * @returns The check digit (0–9). - * - * @throws {FakerError} If `digits` is not exactly 11 numeric characters. - * - * @see upc - * - * @since 10.2.0 - */ -export function calculateUPCCheckDigit(digits: string): number { - if (!/^\d{11}$/.test(digits)) { - throw new FakerError( - 'calculateUPCCheckDigit expects exactly 11 numeric digits' - ); - } - - let sum = 0; - let idx = 0; - for (const digit of digits) { - const n = Number.parseInt(digit, 10); - sum += n * (idx % 2 === 0 ? 3 : 1); - idx++; - } - - return (10 - (sum % 10)) % 10; -} diff --git a/src/modules/commerce/upc.ts b/src/modules/commerce/upc.ts new file mode 100644 index 00000000000..e30ccd76db9 --- /dev/null +++ b/src/modules/commerce/upc.ts @@ -0,0 +1,83 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { numeric } from '../string/numeric'; + +/** + * Calculates the check digit for a UPC‑A using the Modulo 10 algorithm. + * + * @param digits The first 11 digits (UPC body) as a numeric string. + * + * @returns The check digit (0–9). + * + * @throws {FakerError} If `digits` is not exactly 11 numeric characters. + * + * @see upc + * + * @since 10.2.0 + */ +function calculateUPCCheckDigit(digits: string): number { + if (!/^\d{11}$/.test(digits)) { + throw new FakerError( + 'calculateUPCCheckDigit expects exactly 11 numeric digits' + ); + } + + let sum = 0; + let idx = 0; + for (const digit of digits) { + const n = Number.parseInt(digit, 10); + sum += n * (idx % 2 === 0 ? 3 : 1); + idx++; + } + + return (10 - (sum % 10)) % 10; +} + +/** + * Returns a valid [UPC‑A](https://en.wikipedia.org/wiki/Universal_Product_Code) (12 digits). + * + * When a `prefix` is provided, it is padded with random digits so that the body + * has 11 digits. The 12th digit (check digit) is computed using the Modulo 10 algorithm. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.prefix Optional numeric prefix for the UPC body (0–11 digits). + * + * @returns A 12‑digit UPC‑A string. + * + * @throws {FakerError} If `prefix` contains non-digit characters or more than 11 digits. + * + * @example + * upc(fakerCore) // '036000291452' + * upc(fakerCore, { prefix: '01234' }) // '012345678905' + * + * @since 10.2.0 + */ +export function upc( + fakerCore: FakerCore, + options: { + /** + * Optional numeric prefix for the UPC body (0–11 digits). + */ + prefix?: string; + } = {} +): string { + const { prefix = '' } = options; + if (prefix && /\D/.test(prefix)) { + throw new FakerError('Prefix must contain only numeric digits'); + } + + if (prefix.length > 11) { + throw new FakerError('Prefix must be at most 11 numeric digits'); + } + + const remaining = 11 - prefix.length; + const rand = numeric(fakerCore, { + length: remaining, + allowLeadingZeros: true, + }); + + const body = `${prefix}${rand}`; // 11 digits + const check = calculateUPCCheckDigit(body); + return `${body}${check}`; // 12-digit UPC-A +} diff --git a/src/modules/company/buzz-adjective.ts b/src/modules/company/buzz-adjective.ts new file mode 100644 index 00000000000..714516982b2 --- /dev/null +++ b/src/modules/company/buzz-adjective.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random buzz adjective that can be used to demonstrate data being viewed by a manager. + * + * @param fakerCore The FakerCore to use. + * + * @example + * buzzAdjective(fakerCore) // 'one-to-one' + * + * @since 8.0.0 + */ +export function buzzAdjective(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.buzz_adjective); +} diff --git a/src/modules/company/buzz-noun.ts b/src/modules/company/buzz-noun.ts new file mode 100644 index 00000000000..c8821e006bb --- /dev/null +++ b/src/modules/company/buzz-noun.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random buzz noun that can be used to demonstrate data being viewed by a manager. + * + * @param fakerCore The FakerCore to use. + * + * @example + * buzzNoun(fakerCore) // 'paradigms' + * + * @since 8.0.0 + */ +export function buzzNoun(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.buzz_noun); +} diff --git a/src/modules/company/buzz-phrase.ts b/src/modules/company/buzz-phrase.ts new file mode 100644 index 00000000000..bcfdfe5917f --- /dev/null +++ b/src/modules/company/buzz-phrase.ts @@ -0,0 +1,22 @@ +import type { FakerCore } from '../../core'; +import { buzzAdjective } from '../company/buzz-adjective'; +import { buzzNoun } from '../company/buzz-noun'; +import { buzzVerb } from '../company/buzz-verb'; + +/** + * Generates a random buzz phrase that can be used to demonstrate data being viewed by a manager. + * + * @param fakerCore The FakerCore to use. + * + * @example + * buzzPhrase(fakerCore) // 'cultivate synergistic e-markets' + * + * @since 8.0.0 + */ +export function buzzPhrase(fakerCore: FakerCore): string { + return [ + buzzVerb(fakerCore), + buzzAdjective(fakerCore), + buzzNoun(fakerCore), + ].join(' '); +} diff --git a/src/modules/company/buzz-verb.ts b/src/modules/company/buzz-verb.ts new file mode 100644 index 00000000000..f14bdc7f451 --- /dev/null +++ b/src/modules/company/buzz-verb.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random buzz verb that can be used to demonstrate data being viewed by a manager. + * + * @param fakerCore The FakerCore to use. + * + * @example + * buzzVerb(fakerCore) // 'empower' + * + * @since 8.0.0 + */ +export function buzzVerb(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.buzz_verb); +} diff --git a/src/modules/company/catch-phrase-adjective.ts b/src/modules/company/catch-phrase-adjective.ts new file mode 100644 index 00000000000..24d6e4a2b94 --- /dev/null +++ b/src/modules/company/catch-phrase-adjective.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random catch phrase adjective that can be displayed to an end user. + * + * @param fakerCore The FakerCore to use. + * + * @example + * catchPhraseAdjective(fakerCore) // 'Multi-tiered' + * + * @since 2.0.1 + */ +export function catchPhraseAdjective(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.adjective); +} diff --git a/src/modules/company/catch-phrase-descriptor.ts b/src/modules/company/catch-phrase-descriptor.ts new file mode 100644 index 00000000000..dc0f30b0dfc --- /dev/null +++ b/src/modules/company/catch-phrase-descriptor.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random catch phrase descriptor that can be displayed to an end user. + * + * @param fakerCore The FakerCore to use. + * + * @example + * catchPhraseDescriptor(fakerCore) // 'composite' + * + * @since 2.0.1 + */ +export function catchPhraseDescriptor(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.descriptor); +} diff --git a/src/modules/company/catch-phrase-noun.ts b/src/modules/company/catch-phrase-noun.ts new file mode 100644 index 00000000000..c97b9c36474 --- /dev/null +++ b/src/modules/company/catch-phrase-noun.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random catch phrase noun that can be displayed to an end user. + * + * @param fakerCore The FakerCore to use. + * + * @example + * catchPhraseNoun(fakerCore) // 'leverage' + * + * @since 2.0.1 + */ +export function catchPhraseNoun(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.company.noun); +} diff --git a/src/modules/company/catch-phrase.ts b/src/modules/company/catch-phrase.ts new file mode 100644 index 00000000000..45b159deb5e --- /dev/null +++ b/src/modules/company/catch-phrase.ts @@ -0,0 +1,22 @@ +import type { FakerCore } from '../../core'; +import { catchPhraseAdjective } from '../company/catch-phrase-adjective'; +import { catchPhraseDescriptor } from '../company/catch-phrase-descriptor'; +import { catchPhraseNoun } from '../company/catch-phrase-noun'; + +/** + * Generates a random catch phrase that can be displayed to an end user. + * + * @param fakerCore The FakerCore to use. + * + * @example + * catchPhrase(fakerCore) // 'Upgradable systematic flexibility' + * + * @since 2.0.1 + */ +export function catchPhrase(fakerCore: FakerCore): string { + return [ + catchPhraseAdjective(fakerCore), + catchPhraseDescriptor(fakerCore), + catchPhraseNoun(fakerCore), + ].join(' '); +} diff --git a/src/modules/company/index.ts b/src/modules/company/index.ts index 8548ba498d8..4ecd186661b 100644 --- a/src/modules/company/index.ts +++ b/src/modules/company/index.ts @@ -1,4 +1,13 @@ import { ModuleBase } from '../../internal/module-base'; +import { buzzAdjective as companyBuzzAdjective } from './buzz-adjective'; +import { buzzNoun as companyBuzzNoun } from './buzz-noun'; +import { buzzPhrase as companyBuzzPhrase } from './buzz-phrase'; +import { buzzVerb as companyBuzzVerb } from './buzz-verb'; +import { catchPhrase as companyCatchPhrase } from './catch-phrase'; +import { catchPhraseAdjective as companyCatchPhraseAdjective } from './catch-phrase-adjective'; +import { catchPhraseDescriptor as companyCatchPhraseDescriptor } from './catch-phrase-descriptor'; +import { catchPhraseNoun as companyCatchPhraseNoun } from './catch-phrase-noun'; +import { name as companyName } from './name'; /** * Module to generate company related entries. @@ -24,7 +33,7 @@ export class CompanyModule extends ModuleBase { * @since 7.4.0 */ name(): string { - return this.faker.helpers.fake(this.faker.definitions.company.name_pattern); + return companyName(this.faker.fakerCore); } /** @@ -36,11 +45,7 @@ export class CompanyModule extends ModuleBase { * @since 2.0.1 */ catchPhrase(): string { - return [ - this.catchPhraseAdjective(), - this.catchPhraseDescriptor(), - this.catchPhraseNoun(), - ].join(' '); + return companyCatchPhrase(this.faker.fakerCore); } /** @@ -52,7 +57,7 @@ export class CompanyModule extends ModuleBase { * @since 8.0.0 */ buzzPhrase(): string { - return [this.buzzVerb(), this.buzzAdjective(), this.buzzNoun()].join(' '); + return companyBuzzPhrase(this.faker.fakerCore); } /** @@ -64,9 +69,7 @@ export class CompanyModule extends ModuleBase { * @since 2.0.1 */ catchPhraseAdjective(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.company.adjective - ); + return companyCatchPhraseAdjective(this.faker.fakerCore); } /** @@ -78,9 +81,7 @@ export class CompanyModule extends ModuleBase { * @since 2.0.1 */ catchPhraseDescriptor(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.company.descriptor - ); + return companyCatchPhraseDescriptor(this.faker.fakerCore); } /** @@ -92,7 +93,7 @@ export class CompanyModule extends ModuleBase { * @since 2.0.1 */ catchPhraseNoun(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.company.noun); + return companyCatchPhraseNoun(this.faker.fakerCore); } /** @@ -104,9 +105,7 @@ export class CompanyModule extends ModuleBase { * @since 8.0.0 */ buzzAdjective(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.company.buzz_adjective - ); + return companyBuzzAdjective(this.faker.fakerCore); } /** @@ -118,9 +117,7 @@ export class CompanyModule extends ModuleBase { * @since 8.0.0 */ buzzVerb(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.company.buzz_verb - ); + return companyBuzzVerb(this.faker.fakerCore); } /** @@ -132,8 +129,6 @@ export class CompanyModule extends ModuleBase { * @since 8.0.0 */ buzzNoun(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.company.buzz_noun - ); + return companyBuzzNoun(this.faker.fakerCore); } } diff --git a/src/modules/company/name.ts b/src/modules/company/name.ts new file mode 100644 index 00000000000..df60a6ce3a2 --- /dev/null +++ b/src/modules/company/name.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random company name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * name(fakerCore) // 'Zieme, Hauck and McClure' + * + * @since 7.4.0 + */ +export function name(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.company.name_pattern + ); +} diff --git a/src/modules/database/collation.ts b/src/modules/database/collation.ts new file mode 100644 index 00000000000..8e940f3ca42 --- /dev/null +++ b/src/modules/database/collation.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random database collation. + * + * @param fakerCore The FakerCore to use. + * + * @example + * collation(fakerCore) // 'utf8_unicode_ci' + * + * @since 4.0.0 + */ +export function collation(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.database.collation); +} diff --git a/src/modules/database/column.ts b/src/modules/database/column.ts new file mode 100644 index 00000000000..8f10b56eb9f --- /dev/null +++ b/src/modules/database/column.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random database column name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * column(fakerCore) // 'createdAt' + * + * @since 4.0.0 + */ +export function column(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.database.column); +} diff --git a/src/modules/database/engine.ts b/src/modules/database/engine.ts new file mode 100644 index 00000000000..44e85429cbc --- /dev/null +++ b/src/modules/database/engine.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random database engine. + * + * @param fakerCore The FakerCore to use. + * + * @example + * engine(fakerCore) // 'ARCHIVE' + * + * @since 4.0.0 + */ +export function engine(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.database.engine); +} diff --git a/src/modules/database/index.ts b/src/modules/database/index.ts index 6c3595cc4fc..07b7776871a 100644 --- a/src/modules/database/index.ts +++ b/src/modules/database/index.ts @@ -1,4 +1,9 @@ import { ModuleBase } from '../../internal/module-base'; +import { collation as databaseCollation } from './collation'; +import { column as databaseColumn } from './column'; +import { engine as databaseEngine } from './engine'; +import { mongodbObjectId as databaseMongodbObjectId } from './mongodb-object-id'; +import { type as databaseType } from './type'; /** * Module to generate database related entries. @@ -19,9 +24,7 @@ export class DatabaseModule extends ModuleBase { * @since 4.0.0 */ column(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.database.column - ); + return databaseColumn(this.faker.fakerCore); } /** @@ -33,9 +36,7 @@ export class DatabaseModule extends ModuleBase { * @since 4.0.0 */ type(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.database.type - ); + return databaseType(this.faker.fakerCore); } /** @@ -47,9 +48,7 @@ export class DatabaseModule extends ModuleBase { * @since 4.0.0 */ collation(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.database.collation - ); + return databaseCollation(this.faker.fakerCore); } /** @@ -61,9 +60,7 @@ export class DatabaseModule extends ModuleBase { * @since 4.0.0 */ engine(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.database.engine - ); + return databaseEngine(this.faker.fakerCore); } /** @@ -75,10 +72,6 @@ export class DatabaseModule extends ModuleBase { * @since 6.2.0 */ mongodbObjectId(): string { - return this.faker.string.hexadecimal({ - length: 24, - casing: 'lower', - prefix: '', - }); + return databaseMongodbObjectId(this.faker.fakerCore); } } diff --git a/src/modules/database/mongodb-object-id.ts b/src/modules/database/mongodb-object-id.ts new file mode 100644 index 00000000000..236bf14ad7f --- /dev/null +++ b/src/modules/database/mongodb-object-id.ts @@ -0,0 +1,20 @@ +import type { FakerCore } from '../../core'; +import { hexadecimal } from '../string/hexadecimal'; + +/** + * Returns a MongoDB [ObjectId](https://docs.mongodb.com/manual/reference/method/ObjectId/) string. + * + * @param fakerCore The FakerCore to use. + * + * @example + * mongodbObjectId(fakerCore) // 'e175cac316a79afdd0ad3afb' + * + * @since 6.2.0 + */ +export function mongodbObjectId(fakerCore: FakerCore): string { + return hexadecimal(fakerCore, { + length: 24, + casing: 'lower', + prefix: '', + }); +} diff --git a/src/modules/database/type.ts b/src/modules/database/type.ts new file mode 100644 index 00000000000..6c6740f7dd1 --- /dev/null +++ b/src/modules/database/type.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random database column type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * type(fakerCore) // 'timestamp' + * + * @since 4.0.0 + */ +export function type(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.database.type); +} 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/date/_convert.ts b/src/modules/date/_convert.ts new file mode 100644 index 00000000000..ed31ba59baf --- /dev/null +++ b/src/modules/date/_convert.ts @@ -0,0 +1,8 @@ +/** + * Small helper function to convert a number of years to an amount of milliseconds. + * + * @param years The number of years to convert to milliseconds. + */ +export function yearsToMs(years: number): number { + return years * 365 * 24 * 3600 * 1000; +} diff --git a/src/modules/date/anytime.ts b/src/modules/date/anytime.ts new file mode 100644 index 00000000000..624b62a93b4 --- /dev/null +++ b/src/modules/date/anytime.ts @@ -0,0 +1,40 @@ +import type { FakerCore } from '../../core'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Generates a random date that can be either in the past or in the future. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @see between(fakerCore): For generating dates in a specific range. + * @see past(fakerCore): For generating dates explicitly in the past. + * @see future(fakerCore): For generating dates explicitly in the future. + * + * @example + * anytime(fakerCore) // '2022-07-31T01:33:29.567Z' + * + * @since 8.0.0 + */ +export function anytime( + fakerCore: FakerCore, + options: { + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): Date { + const { refDate = getDefaultRefDate(fakerCore) } = options; + const time = toDate(refDate).getTime(); + + return between(fakerCore, { + from: time - 1000 * 60 * 60 * 24 * 365, + to: time + 1000 * 60 * 60 * 24 * 365, + }); +} diff --git a/src/modules/date/between.ts b/src/modules/date/between.ts new file mode 100644 index 00000000000..2f04a005890 --- /dev/null +++ b/src/modules/date/between.ts @@ -0,0 +1,44 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { int } from '../number/int'; + +/** + * Generates a random date between the given boundaries. + * + * @param fakerCore The FakerCore to use. + * @param options The options object. + * @param options.from The early date boundary. + * @param options.to The late date boundary. + * + * @throws {FakerError} If `from` or `to` are not provided. + * @throws {FakerError} If `from` is after `to`. + * + * @example + * between(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z' }) // '2026-05-16T02:22:53.002Z' + * + * @since 8.0.0 + */ +export function between( + fakerCore: FakerCore, + options: { + /** + * The early date boundary. + */ + from: string | Date | number; + /** + * The late date boundary. + */ + to: string | Date | number; + } +): Date { + const { from, to } = options; + + const fromMs = toDate(from, 'from').getTime(); + const toMs = toDate(to, 'to').getTime(); + if (fromMs > toMs) { + throw new FakerError('`from` date must be before `to` date.'); + } + + return new Date(int(fakerCore, { min: fromMs, max: toMs })); +} diff --git a/src/modules/date/betweens.ts b/src/modules/date/betweens.ts new file mode 100644 index 00000000000..7106ffcf74f --- /dev/null +++ b/src/modules/date/betweens.ts @@ -0,0 +1,69 @@ +import type { FakerCore } from '../../core'; +import { between } from '../date/between'; +import { multiple } from '../helpers/multiple'; + +/** + * Generates random dates between the given boundaries. The dates will be returned in an array sorted in chronological order. + * + * @param fakerCore The FakerCore to use. + * @param options The options object. + * @param options.from The early date boundary. + * @param options.to The late date boundary. + * @param options.count The number of dates to generate. Defaults to `3`. + * + * @throws {FakerError} If `from` or `to` are not provided. + * @throws {FakerError} If `from` is after `to`. + * + * @example + * betweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z' }) + * // [ + * // '2022-07-02T06:00:00.000Z', + * // '2024-12-31T12:00:00.000Z', + * // '2027-07-02T18:00:00.000Z' + * // ] + * betweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: 2 }) + * // [ '2023-05-02T16:00:00.000Z', '2026-09-01T08:00:00.000Z' ] + * betweens(fakerCore, { from: '2020-01-01T00:00:00.000Z', to: '2030-01-01T00:00:00.000Z', count: { min: 2, max: 5 }}) + * // [ + * // 2021-12-19T06:35:40.191Z, + * // 2022-09-10T08:03:51.351Z, + * // 2023-04-19T11:41:17.501Z + * // ] + * + * @since 8.0.0 + */ +export function betweens( + fakerCore: FakerCore, + options: { + /** + * The early date boundary. + */ + from: string | Date | number; + /** + * The late date boundary. + */ + to: string | Date | number; + /** + * The number of dates to generate. + * + * @default 3 + */ + count?: + | number + | { + /** + * The minimum number of dates to generate. + */ + min: number; + /** + * The maximum number of dates to generate. + */ + max: number; + }; + } +): Date[] { + const { from, to, count = 3 } = options; + return multiple(fakerCore, () => between(fakerCore, { from, to }), { + count, + }).toSorted((a, b) => a.getTime() - b.getTime()); +} diff --git a/src/modules/date/birthdate.ts b/src/modules/date/birthdate.ts new file mode 100644 index 00000000000..2ecd0ff3921 --- /dev/null +++ b/src/modules/date/birthdate.ts @@ -0,0 +1,206 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Returns a random birthdate. By default, the birthdate is generated for an adult between 18 and 80 years old. + * But you can customize the `'age'` range or the `'year'` range to generate a more specific birthdate. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use to generate the birthdate. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * birthdate(fakerCore) // '1977-07-10T01:37:30.719Z' + * + * @since 7.0.0 + */ +export function birthdate( + fakerCore: FakerCore, + options?: { + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } +): Date; +/** + * Returns a random birthdate for a given age range. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use to generate the birthdate. + * @param options.mode `'age'` to generate a birthdate based on the age range. It is also possible to generate a birthdate based on a `'year'` range. + * @param options.min The minimum age to generate a birthdate for. + * @param options.max The maximum age to generate a birthdate for. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * birthdate(fakerCore, { mode: 'age', min: 18, max: 65 }) // '2003-11-02T20:03:20.116Z' + * + * @since 7.0.0 + */ +export function birthdate( + fakerCore: FakerCore, + options: { + /** + * `'age'` to generate a birthdate based on the age range. + * It is also possible to generate a birthdate based on a `'year'` range. + */ + mode: 'age'; + /** + * The minimum age to generate a birthdate for. + */ + min: number; + /** + * The maximum age to generate a birthdate for. + */ + max: number; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } +): Date; +/** + * Returns a random birthdate in the given range of years. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use to generate the birthdate. + * @param options.mode `'year'` to generate a birthdate based on the year range. It is also possible to generate a birthdate based on a `'age'` range. + * @param options.min The minimum year to generate a birthdate in. + * @param options.max The maximum year to generate a birthdate in. + * + * @example + * birthdate(fakerCore, { mode: 'year', min: 1900, max: 2000 }) // '1940-08-20T08:53:07.538Z' + * + * @since 7.0.0 + */ +export function birthdate( + fakerCore: FakerCore, + options: { + /** + * `'year'` to generate a birthdate based on the year range. + * It is also possible to generate a birthdate based on an `'age'` range. + */ + mode: 'year'; + /** + * The minimum year to generate a birthdate in. + */ + min: number; + /** + * The maximum year to generate a birthdate in. + */ + max: number; + } +): Date; +/** + * Returns a random birthdate. By default, the birthdate is generated for an adult between 18 and 80 years old. + * But you can customize the `'age'` range or the `'year'` range to generate a more specific birthdate. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use to generate the birthdate. + * @param options.mode Either `'age'` or `'year'` to generate a birthdate based on the age or year range. + * @param options.min The minimum age or year to generate a birthdate in. + * @param options.max The maximum age or year to generate a birthdate in. + * @param options.refDate The date to use as reference point for the newly generated date. + * Only used when `mode` is `'age'`. + * Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * birthdate(fakerCore) // '1977-07-10T01:37:30.719Z' + * birthdate(fakerCore, { mode: 'age', min: 18, max: 65 }) // '2003-11-02T20:03:20.116Z' + * birthdate(fakerCore, { mode: 'year', min: 1900, max: 2000 }) // '1940-08-20T08:53:07.538Z' + * + * @since 7.0.0 + */ +export function birthdate( + fakerCore: FakerCore, + options?: + | { + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } + | { + /** + * Either `'age'` or `'year'` to generate a birthdate based on the age or year range. + */ + mode: 'age' | 'year'; + /** + * The minimum age/year to generate a birthdate for/in. + */ + min: number; + /** + * The maximum age/year to generate a birthdate for/in. + */ + max: number; + /** + * The date to use as reference point for the newly generated date. + * Only used when `mode` is `'age'`. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } +): Date; + +export function birthdate( + fakerCore: FakerCore, + options: { + mode?: 'age' | 'year'; + min?: number; + max?: number; + refDate?: string | Date | number; + } = {} +): Date { + const { + mode = 'age', + min = 18, + max = 80, + refDate: rawRefDate = getDefaultRefDate(fakerCore), + } = options; + + const refDate = toDate(rawRefDate); + const refYear = refDate.getUTCFullYear(); + + switch (mode) { + case 'age': { + // Add one day to the `from` date to avoid generating the same date as the reference date. + const oneDay = 24 * 60 * 60 * 1000; + const from = new Date(refDate).setUTCFullYear(refYear - max - 1) + oneDay; + const to = new Date(refDate).setUTCFullYear(refYear - min); + + if (from > to) { + throw new FakerError( + `Max age ${max} should be greater than or equal to min age ${min}.` + ); + } + + return between(fakerCore, { from, to }); + } + + case 'year': { + // Avoid generating dates on the first and last date of the year + // to avoid running into other years depending on the timezone. + const from = new Date(Date.UTC(0, 0, 2)).setUTCFullYear(min); + const to = new Date(Date.UTC(0, 11, 30)).setUTCFullYear(max); + + if (from > to) { + throw new FakerError( + `Max year ${max} should be greater than or equal to min year ${min}.` + ); + } + + return between(fakerCore, { from, to }); + } + } +} diff --git a/src/modules/date/future.ts b/src/modules/date/future.ts new file mode 100644 index 00000000000..b61934fa5be --- /dev/null +++ b/src/modules/date/future.ts @@ -0,0 +1,86 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Generates a random date in the future. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.years The range of years the date may be in the future. Either as a fixed amount of years or as a year range. Defaults to `1`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @throws {FakerError} If `years.max` is less than 0. + * @throws {FakerError} If `years.min` is greater than or equal to `years.max`. + * + * @see soon(fakerCore): For generating dates in the near future (days instead of years). + * + * @example + * future(fakerCore) // '2022-11-19T05:52:49.100Z' + * future(fakerCore, { years: 10 }) // '2030-11-23T09:38:28.710Z' + * future(fakerCore, { years: { min: 4, max: 7 } }) // '2031-05-21T05:49:21.116Z' + * future(fakerCore, { years: 10, refDate: '2020-01-01T00:00:00.000Z' }) // '2020-12-13T22:45:10.252Z' + * + * @since 8.0.0 + */ +export function future( + fakerCore: FakerCore, + options: { + /** + * The range of years the date may be in the future. + * + * @default 1 + */ + years?: + | number + | { + /** + * The minimum amount of years the date should be in the future. + * + * @default 0 + */ + min: number; + /** + * The maximum amount of years the date should be in the future. + * + * @default 1 + */ + max: number; + }; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): Date { + const { refDate = getDefaultRefDate(fakerCore) } = options; + let { years = 1 } = options; + if (typeof years === 'number') { + years = { min: 0, max: years }; + } + + if (years.max <= 0) { + throw new FakerError('Years must be greater than 0.'); + } + + if (years.min >= years.max) { + throw new FakerError( + 'The maximum amount of years must be greater than the minimum amount of years.' + ); + } + + const time = toDate(refDate); + const from = new Date(time); + from.setUTCFullYear(from.getUTCFullYear() + years.min); + const to = new Date(time); + to.setUTCFullYear(to.getUTCFullYear() + years.max); + + return between(fakerCore, { + from: from.getTime() + 1000, + to, + }); +} diff --git a/src/modules/date/index.ts b/src/modules/date/index.ts index 0fcae7f885a..d03b9da24de 100644 --- a/src/modules/date/index.ts +++ b/src/modules/date/index.ts @@ -1,9 +1,16 @@ -import type { DateEntryDefinition } from '../../definitions'; -import { FakerError } from '../../errors/faker-error'; import type { Faker } from '../../faker'; -import { toDate } from '../../internal/date'; -import { assertLocaleData } from '../../internal/locale-proxy'; import { SimpleModuleBase } from '../../internal/module-base'; +import { anytime as dateAnytime } from './anytime'; +import { between as dateBetween } from './between'; +import { betweens as dateBetweens } from './betweens'; +import { birthdate as dateBirthdate } from './birthdate'; +import { future as dateFuture } from './future'; +import { month as dateMonth } from './month'; +import { past as datePast } from './past'; +import { recent as dateRecent } from './recent'; +import { soon as dateSoon } from './soon'; +import { timeZone as dateTimeZone } from './time-zone'; +import { weekday as dateWeekday } from './weekday'; /** * Module to generate dates (without methods requiring localized data). @@ -13,7 +20,7 @@ export class SimpleDateModule extends SimpleModuleBase { * Generates a random date that can be either in the past or in the future. * * @param options The optional options object. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @see faker.date.between(): For generating dates in a specific range. * @see faker.date.past(): For generating dates explicitly in the past. @@ -34,13 +41,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { refDate = this.faker.defaultRefDate() } = options; - const time = toDate(refDate).getTime(); - - return this.between({ - from: time - 1000 * 60 * 60 * 24 * 365, - to: time + 1000 * 60 * 60 * 24 * 365, - }); + return dateAnytime(this.faker.fakerCore, options); } /** @@ -48,7 +49,7 @@ export class SimpleDateModule extends SimpleModuleBase { * * @param options The optional options object. * @param options.years The range of years the date may be in the past. Either as a fixed amount of years or as a year range. Defaults to `1`. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @throws {FakerError} If `years.max` is less than 0. * @throws {FakerError} If `years.min` is greater than or equal to `years.max`. @@ -94,32 +95,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { refDate = this.faker.defaultRefDate() } = options; - let { years = 1 } = options; - if (typeof years === 'number') { - years = { min: 0, max: years }; - } - - if (years.max <= 0) { - throw new FakerError('Years must be greater than 0.'); - } - - if (years.min >= years.max) { - throw new FakerError( - 'The maximum amount of years must be greater than the minimum amount of years.' - ); - } - - const time = toDate(refDate); - const from = new Date(time); - from.setUTCFullYear(from.getUTCFullYear() - years.max); - const to = new Date(time); - to.setUTCFullYear(to.getUTCFullYear() - years.min); - - return this.between({ - from, - to: to.getTime() - 1000, - }); + return datePast(this.faker.fakerCore, options); } /** @@ -127,7 +103,7 @@ export class SimpleDateModule extends SimpleModuleBase { * * @param options The optional options object. * @param options.years The range of years the date may be in the future. Either as a fixed amount of years or as a year range. Defaults to `1`. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @throws {FakerError} If `years.max` is less than 0. * @throws {FakerError} If `years.min` is greater than or equal to `years.max`. @@ -173,32 +149,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { refDate = this.faker.defaultRefDate() } = options; - let { years = 1 } = options; - if (typeof years === 'number') { - years = { min: 0, max: years }; - } - - if (years.max <= 0) { - throw new FakerError('Years must be greater than 0.'); - } - - if (years.min >= years.max) { - throw new FakerError( - 'The maximum amount of years must be greater than the minimum amount of years.' - ); - } - - const time = toDate(refDate); - const from = new Date(time); - from.setUTCFullYear(from.getUTCFullYear() + years.min); - const to = new Date(time); - to.setUTCFullYear(to.getUTCFullYear() + years.max); - - return this.between({ - from: from.getTime() + 1000, - to, - }); + return dateFuture(this.faker.fakerCore, options); } /** @@ -226,15 +177,7 @@ export class SimpleDateModule extends SimpleModuleBase { */ to: string | Date | number; }): Date { - const { from, to } = options; - - const fromMs = toDate(from, 'from').getTime(); - const toMs = toDate(to, 'to').getTime(); - if (fromMs > toMs) { - throw new FakerError('`from` date must be before `to` date.'); - } - - return new Date(this.faker.number.int({ min: fromMs, max: toMs })); + return dateBetween(this.faker.fakerCore, options); } /** @@ -293,10 +236,7 @@ export class SimpleDateModule extends SimpleModuleBase { max: number; }; }): Date[] { - const { from, to, count = 3 } = options; - return this.faker.helpers - .multiple(() => this.between({ from, to }), { count }) - .toSorted((a, b) => a.getTime() - b.getTime()); + return dateBetweens(this.faker.fakerCore, options); } /** @@ -304,7 +244,7 @@ export class SimpleDateModule extends SimpleModuleBase { * * @param options The optional options object. * @param options.days The range of days the date may be in the past. Either as a fixed amount of days or as a day range. Defaults to `1`. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @throws {FakerError} If `days.max` is less than 0. * @throws {FakerError} If `days.min` is greater than or equal to `days.max`. @@ -350,32 +290,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { refDate = this.faker.defaultRefDate() } = options; - let { days = 1 } = options; - if (typeof days === 'number') { - days = { min: 0, max: days }; - } - - if (days.max <= 0) { - throw new FakerError('Days must be greater than 0.'); - } - - if (days.min >= days.max) { - throw new FakerError( - 'The maximum amount of days must be greater than the minimum amount of days.' - ); - } - - const time = toDate(refDate); - const from = new Date(time); - from.setUTCDate(from.getUTCDate() - days.max); - const to = new Date(time); - to.setUTCDate(to.getUTCDate() - days.min); - - return this.between({ - from, - to: to.getTime() - 1000, - }); + return dateRecent(this.faker.fakerCore, options); } /** @@ -383,7 +298,7 @@ export class SimpleDateModule extends SimpleModuleBase { * * @param options The optional options object. * @param options.days The range of days the date may be in the future. Either as a fixed amount of days or as a day range. Defaults to `1`. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @throws {FakerError} If `days.max` is less than 0. * @throws {FakerError} If `days.min` is greater than or equal to `days.max`. @@ -429,32 +344,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { refDate = this.faker.defaultRefDate() } = options; - let { days = 1 } = options; - if (typeof days === 'number') { - days = { min: 0, max: days }; - } - - if (days.max <= 0) { - throw new FakerError('Days must be greater than 0.'); - } - - if (days.min >= days.max) { - throw new FakerError( - 'The maximum amount of days must be greater than the minimum amount of days.' - ); - } - - const time = toDate(refDate); - const from = new Date(time); - from.setUTCDate(from.getUTCDate() + days.min); - const to = new Date(time); - to.setUTCDate(to.getUTCDate() + days.max); - - return this.between({ - from: from.getTime() + 1000, - to, - }); + return dateSoon(this.faker.fakerCore, options); } /** @@ -462,7 +352,7 @@ export class SimpleDateModule extends SimpleModuleBase { * But you can customize the `'age'` range or the `'year'` range to generate a more specific birthdate. * * @param options The options to use to generate the birthdate. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @example * faker.date.birthdate() // '1977-07-10T01:37:30.719Z' @@ -484,7 +374,7 @@ export class SimpleDateModule extends SimpleModuleBase { * @param options.mode `'age'` to generate a birthdate based on the age range. It is also possible to generate a birthdate based on a `'year'` range. * @param options.min The minimum age to generate a birthdate for. * @param options.max The maximum age to generate a birthdate for. - * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `faker.getDefaultRefDate()`. * * @example * faker.date.birthdate({ mode: 'age', min: 18, max: 65 }) // '2003-11-02T20:03:20.116Z' @@ -550,7 +440,7 @@ export class SimpleDateModule extends SimpleModuleBase { * @param options.max The maximum age or year to generate a birthdate in. * @param options.refDate The date to use as reference point for the newly generated date. * Only used when `mode` is `'age'`. - * Defaults to `faker.defaultRefDate()`. + * Defaults to `faker.getDefaultRefDate()`. * * @example * faker.date.birthdate() // '1977-07-10T01:37:30.719Z' @@ -599,48 +489,7 @@ export class SimpleDateModule extends SimpleModuleBase { refDate?: string | Date | number; } = {} ): Date { - const { - mode = 'age', - min = 18, - max = 80, - refDate: rawRefDate = this.faker.defaultRefDate(), - } = options; - - const refDate = toDate(rawRefDate); - const refYear = refDate.getUTCFullYear(); - - switch (mode) { - case 'age': { - // Add one day to the `from` date to avoid generating the same date as the reference date. - const oneDay = 24 * 60 * 60 * 1000; - const from = - new Date(refDate).setUTCFullYear(refYear - max - 1) + oneDay; - const to = new Date(refDate).setUTCFullYear(refYear - min); - - if (from > to) { - throw new FakerError( - `Max age ${max} should be greater than or equal to min age ${min}.` - ); - } - - return this.between({ from, to }); - } - - case 'year': { - // Avoid generating dates on the first and last date of the year - // to avoid running into other years depending on the timezone. - const from = new Date(Date.UTC(0, 0, 2)).setUTCFullYear(min); - const to = new Date(Date.UTC(0, 11, 30)).setUTCFullYear(max); - - if (from > to) { - throw new FakerError( - `Max year ${max} should be greater than or equal to min year ${min}.` - ); - } - - return this.between({ from, to }); - } - } + return dateBirthdate(this.faker.fakerCore, options); } } @@ -710,21 +559,7 @@ export class DateModule extends SimpleDateModule { context?: boolean; } = {} ): string { - const { abbreviated = false, context = false } = options; - - const source = this.faker.definitions.date.month; - let type: keyof DateEntryDefinition; - if (abbreviated) { - const useContext = context && source['abbr_context'] != null; - type = useContext ? 'abbr_context' : 'abbr'; - } else { - const useContext = context && source['wide_context'] != null; - type = useContext ? 'wide_context' : 'wide'; - } - - const values = source[type]; - assertLocaleData(values, 'date.month', type); - return this.faker.helpers.arrayElement(values); + return dateMonth(this.faker.fakerCore, options); } /** @@ -762,21 +597,7 @@ export class DateModule extends SimpleDateModule { context?: boolean; } = {} ): string { - const { abbreviated = false, context = false } = options; - - const source = this.faker.definitions.date.weekday; - let type: keyof DateEntryDefinition; - if (abbreviated) { - const useContext = context && source['abbr_context'] != null; - type = useContext ? 'abbr_context' : 'abbr'; - } else { - const useContext = context && source['wide_context'] != null; - type = useContext ? 'wide_context' : 'wide'; - } - - const values = source[type]; - assertLocaleData(values, 'date.weekday', type); - return this.faker.helpers.arrayElement(values); + return dateWeekday(this.faker.fakerCore, options); } /** @@ -793,8 +614,6 @@ export class DateModule extends SimpleDateModule { * @since 9.0.0 */ timeZone(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.date.time_zone - ); + return dateTimeZone(this.faker.fakerCore); } } diff --git a/src/modules/date/month.ts b/src/modules/date/month.ts new file mode 100644 index 00000000000..14d34693905 --- /dev/null +++ b/src/modules/date/month.ts @@ -0,0 +1,58 @@ +import type { FakerCore } from '../../core'; +import type { DateEntryDefinition } from '../../definitions'; +import { assertLocaleData } from '../../internal/locale-proxy'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random name of a month. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options to use. + * @param options.abbreviated Whether to return an abbreviation. Defaults to `false`. + * @param options.context Whether to return the name of a month in the context of a date. In the default `en` locale this has no effect, however, in other locales like `fr` or `ru`, this may affect grammar or capitalization, for example `'январь'` with `{ context: false }` and `'января'` with `{ context: true }` in `ru`. Defaults to `false`. + * + * @example + * month(fakerCore) // 'October' + * month(fakerCore, { abbreviated: true }) // 'Feb' + * month(fakerCore, { context: true }) // 'June' + * month(fakerCore, { abbreviated: true, context: true }) // 'Sep' + * + * @since 3.0.1 + */ +export function month( + fakerCore: FakerCore, + options: { + /** + * Whether to return an abbreviation. + * + * @default false + */ + abbreviated?: boolean; + /** + * Whether to return the name of a month in the context of a date. + * + * In the default `en` locale this has no effect, + * however, in other locales like `fr` or `ru`, this may affect grammar or capitalization, + * for example `'январь'` with `{ context: false }` and `'января'` with `{ context: true }` in `ru`. + * + * @default false + */ + context?: boolean; + } = {} +): string { + const { abbreviated = false, context = false } = options; + + const source = fakerCore.locale.date.month; + let type: keyof DateEntryDefinition; + if (abbreviated) { + const useContext = context && source['abbr_context'] != null; + type = useContext ? 'abbr_context' : 'abbr'; + } else { + const useContext = context && source['wide_context'] != null; + type = useContext ? 'wide_context' : 'wide'; + } + + const values = source[type]; + assertLocaleData(values, 'date.month', type); + return arrayElement(fakerCore, values); +} diff --git a/src/modules/date/past.ts b/src/modules/date/past.ts new file mode 100644 index 00000000000..5a93072a700 --- /dev/null +++ b/src/modules/date/past.ts @@ -0,0 +1,86 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Generates a random date in the past. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.years The range of years the date may be in the past. Either as a fixed amount of years or as a year range. Defaults to `1`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @throws {FakerError} If `years.max` is less than 0. + * @throws {FakerError} If `years.min` is greater than or equal to `years.max`. + * + * @see recent(fakerCore): For generating dates in the recent past (days instead of years). + * + * @example + * past(fakerCore) // '2021-12-03T05:40:44.408Z' + * past(fakerCore, { years: 10 }) // '2017-10-25T21:34:19.488Z' + * past(fakerCore, { years: { min: 4, max: 7 } }) // '2022-12-12T03:43:16.434Z' + * past(fakerCore, { years: 10, refDate: '2020-01-01T00:00:00.000Z' }) // '2017-08-18T02:59:12.350Z' + * + * @since 8.0.0 + */ +export function past( + fakerCore: FakerCore, + options: { + /** + * The range of years the date may be in the past. + * + * @default 1 + */ + years?: + | number + | { + /** + * The minimum amount of years the date should be in the past. + * + * @default 0 + */ + min: number; + /** + * The maximum amount of years the date should be in the past. + * + * @default 1 + */ + max: number; + }; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): Date { + const { refDate = getDefaultRefDate(fakerCore) } = options; + let { years = 1 } = options; + if (typeof years === 'number') { + years = { min: 0, max: years }; + } + + if (years.max <= 0) { + throw new FakerError('Years must be greater than 0.'); + } + + if (years.min >= years.max) { + throw new FakerError( + 'The maximum amount of years must be greater than the minimum amount of years.' + ); + } + + const time = toDate(refDate); + const from = new Date(time); + from.setUTCFullYear(from.getUTCFullYear() - years.max); + const to = new Date(time); + to.setUTCFullYear(to.getUTCFullYear() - years.min); + + return between(fakerCore, { + from, + to: to.getTime() - 1000, + }); +} diff --git a/src/modules/date/recent.ts b/src/modules/date/recent.ts new file mode 100644 index 00000000000..b2cbce54564 --- /dev/null +++ b/src/modules/date/recent.ts @@ -0,0 +1,86 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Generates a random date in the recent past. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.days The range of days the date may be in the past. Either as a fixed amount of days or as a day range. Defaults to `1`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @throws {FakerError} If `days.max` is less than 0. + * @throws {FakerError} If `days.min` is greater than or equal to `days.max`. + * + * @see past(fakerCore): For generating dates further back in time (years instead of days). + * + * @example + * recent(fakerCore) // '2022-02-04T02:09:35.077Z' + * recent(fakerCore, { days: 10 }) // '2022-01-29T06:12:12.829Z' + * recent(fakerCore, { days: { min: 4, max: 7 } }) // '2022-02-02T17:54:01.818Z' + * recent(fakerCore, { days: 10, refDate: '2020-01-01T00:00:00.000Z' }) // '2019-12-27T18:11:19.117Z' + * + * @since 8.0.0 + */ +export function recent( + fakerCore: FakerCore, + options: { + /** + * The range of days the date may be in the past. + * + * @default 1 + */ + days?: + | number + | { + /** + * The minimum amount of days the date should be in the past. + * + * @default 0 + */ + min: number; + /** + * The maximum amount of days the date should be in the past. + * + * @default 1 + */ + max: number; + }; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): Date { + const { refDate = getDefaultRefDate(fakerCore) } = options; + let { days = 1 } = options; + if (typeof days === 'number') { + days = { min: 0, max: days }; + } + + if (days.max <= 0) { + throw new FakerError('Days must be greater than 0.'); + } + + if (days.min >= days.max) { + throw new FakerError( + 'The maximum amount of days must be greater than the minimum amount of days.' + ); + } + + const time = toDate(refDate); + const from = new Date(time); + from.setUTCDate(from.getUTCDate() - days.max); + const to = new Date(time); + to.setUTCDate(to.getUTCDate() - days.min); + + return between(fakerCore, { + from, + to: to.getTime() - 1000, + }); +} diff --git a/src/modules/date/soon.ts b/src/modules/date/soon.ts new file mode 100644 index 00000000000..9c6b266cd6f --- /dev/null +++ b/src/modules/date/soon.ts @@ -0,0 +1,86 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { toDate } from '../../internal/date'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { between } from '../date/between'; + +/** + * Generates a random date in the near future. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.days The range of days the date may be in the future. Either as a fixed amount of days or as a day range. Defaults to `1`. + * @param options.refDate The date to use as reference point for the newly generated date. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @throws {FakerError} If `days.max` is less than 0. + * @throws {FakerError} If `days.min` is greater than or equal to `days.max`. + * + * @see future(fakerCore): For generating dates further in the future (years instead of days). + * + * @example + * soon(fakerCore) // '2022-02-05T09:55:39.216Z' + * soon(fakerCore, { days: 10 }) // '2022-02-11T05:14:39.138Z' + * soon(fakerCore, { days: { min: 4, max: 7 } }) // '2022-02-09T17:54:01.818Z' + * soon(fakerCore, { days: 10, refDate: '2020-01-01T00:00:00.000Z' }) // '2020-01-01T02:40:44.990Z' + * + * @since 8.0.0 + */ +export function soon( + fakerCore: FakerCore, + options: { + /** + * The range of days the date may be in the future. + * + * @default 1 + */ + days?: + | number + | { + /** + * The minimum amount of days the date should be in the future. + * + * @default 0 + */ + min: number; + /** + * The maximum amount of days the date should be in the future. + * + * @default 1 + */ + max: number; + }; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): Date { + const { refDate = getDefaultRefDate(fakerCore) } = options; + let { days = 1 } = options; + if (typeof days === 'number') { + days = { min: 0, max: days }; + } + + if (days.max <= 0) { + throw new FakerError('Days must be greater than 0.'); + } + + if (days.min >= days.max) { + throw new FakerError( + 'The maximum amount of days must be greater than the minimum amount of days.' + ); + } + + const time = toDate(refDate); + const from = new Date(time); + from.setUTCDate(from.getUTCDate() + days.min); + const to = new Date(time); + to.setUTCDate(to.getUTCDate() + days.max); + + return between(fakerCore, { + from: from.getTime() + 1000, + to, + }); +} diff --git a/src/modules/date/time-zone.ts b/src/modules/date/time-zone.ts new file mode 100644 index 00000000000..4147c59ad41 --- /dev/null +++ b/src/modules/date/time-zone.ts @@ -0,0 +1,21 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random IANA time zone name. + * + * The returned time zone is not tied to the current locale. + * + * @param fakerCore The FakerCore to use. + * + * @see [IANA Time Zone Database](https://www.iana.org/time-zones) + * @see locationTimeZone(fakerCore): For generating a timezone based on the current locale. + * + * @example + * locationTimeZone(fakerCore) // 'Pacific/Guam' + * + * @since 9.0.0 + */ +export function timeZone(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.date.time_zone); +} diff --git a/src/modules/date/weekday.ts b/src/modules/date/weekday.ts new file mode 100644 index 00000000000..bcf06c5279e --- /dev/null +++ b/src/modules/date/weekday.ts @@ -0,0 +1,58 @@ +import type { FakerCore } from '../../core'; +import type { DateEntryDefinition } from '../../definitions'; +import { assertLocaleData } from '../../internal/locale-proxy'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random day of the week. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options to use. + * @param options.abbreviated Whether to return an abbreviation. Defaults to `false`. + * @param options.context Whether to return the day of the week in the context of a date. In the default `en` locale this has no effect, however, in other locales like `fr` or `ru`, this may affect grammar or capitalization, for example `'Lundi'` with `{ context: false }` and `'lundi'` with `{ context: true }` in `fr`. Defaults to `false`. + * + * @example + * weekday(fakerCore) // 'Monday' + * weekday(fakerCore, { abbreviated: true }) // 'Thu' + * weekday(fakerCore, { context: true }) // 'Thursday' + * weekday(fakerCore, { abbreviated: true, context: true }) // 'Fri' + * + * @since 3.0.1 + */ +export function weekday( + fakerCore: FakerCore, + options: { + /** + * Whether to return an abbreviation. + * + * @default false + */ + abbreviated?: boolean; + /** + * Whether to return the day of the week in the context of a date. + * + * In the default `en` locale this has no effect, + * however, in other locales like `fr` or `ru`, this may affect grammar or capitalization, + * for example `'Lundi'` with `{ context: false }` and `'lundi'` with `{ context: true }` in `fr`. + * + * @default false + */ + context?: boolean; + } = {} +): string { + const { abbreviated = false, context = false } = options; + + const source = fakerCore.locale.date.weekday; + let type: keyof DateEntryDefinition; + if (abbreviated) { + const useContext = context && source['abbr_context'] != null; + type = useContext ? 'abbr_context' : 'abbr'; + } else { + const useContext = context && source['wide_context'] != null; + type = useContext ? 'wide_context' : 'wide'; + } + + const values = source[type]; + assertLocaleData(values, 'date.weekday', type); + return arrayElement(fakerCore, values); +} diff --git a/src/modules/finance/_iban-lib.ts b/src/modules/finance/_iban-lib.ts new file mode 100644 index 00000000000..b897f80bf42 --- /dev/null +++ b/src/modules/finance/_iban-lib.ts @@ -0,0 +1,1429 @@ +interface Iban { + alpha: string[]; + formats: Array<{ + bban: Array<{ type: string; count: number }>; + country: string; + format?: string; + total?: number; + }>; + iso3166: string[]; + mod97: (digitStr: string) => number; + pattern10: string[]; + pattern100: string[]; + toDigitString: (str: string) => string; +} + +/** + * The internal data required to generate IBANs. + * + * @internal + */ +export const ibanLib: Iban = { + 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', + ], + formats: [ + { + country: 'AL', + total: 28, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'c', + count: 16, + }, + ], + format: 'ALkk bbbs sssx cccc cccc cccc cccc', + }, + { + country: 'AD', + total: 24, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'c', + count: 12, + }, + ], + format: 'ADkk bbbb ssss cccc cccc cccc', + }, + { + country: 'AT', + total: 20, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'n', + count: 11, + }, + ], + format: 'ATkk bbbb bccc cccc cccc', + }, + { + // Azerbaijan + // https://transferwise.com/fr/iban/azerbaijan + // Length 28 + // BBAN 2c,16n + // GEkk bbbb cccc cccc cccc cccc cccc + // b = National bank code (alpha) + // c = Account number + // example IBAN AZ21 NABZ 0000 0000 1370 1000 1944 + country: 'AZ', + total: 28, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 20, + }, + ], + format: 'AZkk bbbb cccc cccc cccc cccc cccc', + }, + { + country: 'BH', + total: 22, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 14, + }, + ], + format: 'BHkk bbbb cccc cccc cccc cc', + }, + { + country: 'BE', + total: 16, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 9, + }, + ], + format: 'BEkk bbbc cccc ccxx', + }, + { + country: 'BA', + total: 20, + bban: [ + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'BAkk bbbs sscc cccc ccxx', + }, + { + country: 'BR', + total: 29, + bban: [ + { + type: 'n', + count: 13, + }, + { + type: 'n', + count: 10, + }, + { + type: 'a', + count: 1, + }, + { + type: 'c', + count: 1, + }, + ], + format: 'BRkk bbbb bbbb ssss sccc cccc ccct n', + }, + { + country: 'BG', + total: 22, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 6, + }, + { + type: 'c', + count: 8, + }, + ], + format: 'BGkk bbbb ssss ddcc cccc cc', + }, + { + country: 'CR', + total: 22, + bban: [ + { + type: 'n', + count: 1, + }, + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 14, + }, + ], + format: 'CRkk xbbb cccc cccc cccc cc', + }, + { + country: 'HR', + total: 21, + bban: [ + { + type: 'n', + count: 7, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'HRkk bbbb bbbc cccc cccc c', + }, + { + country: 'CY', + total: 28, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'c', + count: 16, + }, + ], + format: 'CYkk bbbs ssss cccc cccc cccc cccc', + }, + { + country: 'CZ', + total: 24, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'CZkk bbbb ssss sscc cccc cccc', + }, + { + country: 'DK', + total: 18, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'DKkk bbbb cccc cccc cc', + }, + { + country: 'DO', + total: 28, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 20, + }, + ], + format: 'DOkk bbbb cccc cccc cccc cccc cccc', + }, + { + country: 'TL', + total: 23, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'TLkk bbbc cccc cccc cccc cxx', + }, + { + country: 'EE', + total: 20, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 12, + }, + ], + format: 'EEkk bbss cccc cccc cccx', + }, + { + country: 'FO', + total: 18, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'FOkk bbbb cccc cccc cx', + }, + { + country: 'FI', + total: 18, + bban: [ + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 8, + }, + ], + format: 'FIkk bbbb bbcc cccc cx', + }, + { + country: 'FR', + total: 27, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'c', + count: 11, + }, + { + type: 'n', + count: 2, + }, + ], + format: 'FRkk bbbb bggg ggcc cccc cccc cxx', + }, + { + country: 'GE', + total: 22, + bban: [ + { + type: 'a', + count: 2, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'GEkk bbcc cccc cccc cccc cc', + }, + { + country: 'DE', + total: 22, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'DEkk bbbb bbbb cccc cccc cc', + }, + { + country: 'GI', + total: 23, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 15, + }, + ], + format: 'GIkk bbbb cccc cccc cccc ccc', + }, + { + country: 'GR', + total: 27, + bban: [ + { + type: 'n', + count: 7, + }, + { + type: 'c', + count: 16, + }, + ], + format: 'GRkk bbbs sssc cccc cccc cccc ccc', + }, + { + country: 'GL', + total: 18, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'GLkk bbbb cccc cccc cc', + }, + { + country: 'GT', + total: 28, + bban: [ + { + type: 'c', + count: 4, + }, + { + type: 'c', + count: 4, + }, + { + type: 'c', + count: 16, + }, + ], + format: 'GTkk bbbb mmtt cccc cccc cccc cccc', + }, + { + country: 'HU', + total: 28, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'HUkk bbbs sssk cccc cccc cccc cccx', + }, + { + country: 'IS', + total: 26, + bban: [ + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'ISkk bbbb sscc cccc iiii iiii ii', + }, + { + country: 'IE', + total: 22, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 8, + }, + ], + format: 'IEkk aaaa bbbb bbcc cccc cc', + }, + { + country: 'IL', + total: 23, + bban: [ + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 13, + }, + ], + format: 'ILkk bbbn nncc cccc cccc ccc', + }, + { + country: 'IR', + total: 26, + bban: [ + { + type: 'n', + count: 22, + }, + ], + format: 'IRkk bbbb cccc cccc cccc cccc cc', + }, + { + country: 'IT', + total: 27, + bban: [ + { + type: 'a', + count: 1, + }, + { + type: 'n', + count: 10, + }, + { + type: 'c', + count: 12, + }, + ], + format: 'ITkk xaaa aabb bbbc cccc cccc ccc', + }, + { + country: 'JO', + total: 30, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 18, + }, + ], + format: 'JOkk bbbb nnnn cccc cccc cccc cccc cc', + }, + { + country: 'KZ', + total: 20, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'c', + count: 13, + }, + ], + format: 'KZkk bbbc cccc cccc cccc', + }, + { + country: 'XK', + total: 20, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 12, + }, + ], + format: 'XKkk bbbb cccc cccc cccc', + }, + { + country: 'KW', + total: 30, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 22, + }, + ], + format: 'KWkk bbbb cccc cccc cccc cccc cccc cc', + }, + { + country: 'LV', + total: 21, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 13, + }, + ], + format: 'LVkk bbbb cccc cccc cccc c', + }, + { + country: 'LB', + total: 28, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'c', + count: 20, + }, + ], + format: 'LBkk bbbb cccc cccc cccc cccc cccc', + }, + { + country: 'LI', + total: 21, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'c', + count: 12, + }, + ], + format: 'LIkk bbbb bccc cccc cccc c', + }, + { + country: 'LT', + total: 20, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'n', + count: 11, + }, + ], + format: 'LTkk bbbb bccc cccc cccc', + }, + { + country: 'LU', + total: 20, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'c', + count: 13, + }, + ], + format: 'LUkk bbbc cccc cccc cccc', + }, + { + country: 'MK', + total: 19, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'c', + count: 10, + }, + { + type: 'n', + count: 2, + }, + ], + format: 'MKkk bbbc cccc cccc cxx', + }, + { + country: 'MT', + total: 31, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 5, + }, + { + type: 'c', + count: 18, + }, + ], + format: 'MTkk bbbb ssss sccc cccc cccc cccc ccc', + }, + { + country: 'MR', + total: 27, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'n', + count: 13, + }, + ], + format: 'MRkk bbbb bsss sscc cccc cccc cxx', + }, + { + country: 'MU', + total: 30, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 15, + }, + { + type: 'a', + count: 3, + }, + ], + format: 'MUkk bbbb bbss cccc cccc cccc 000d dd', + }, + { + country: 'MC', + total: 27, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'c', + count: 11, + }, + { + type: 'n', + count: 2, + }, + ], + format: 'MCkk bbbb bsss sscc cccc cccc cxx', + }, + { + country: 'MD', + total: 24, + bban: [ + { + type: 'c', + count: 2, + }, + { + type: 'c', + count: 18, + }, + ], + format: 'MDkk bbcc cccc cccc cccc cccc', + }, + { + country: 'ME', + total: 22, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 15, + }, + ], + format: 'MEkk bbbc cccc cccc cccc xx', + }, + { + country: 'NL', + total: 18, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'NLkk bbbb cccc cccc cc', + }, + { + country: 'NO', + total: 15, + bban: [ + { + type: 'n', + count: 4, + }, + { + type: 'n', + count: 7, + }, + ], + format: 'NOkk bbbb cccc ccx', + }, + { + country: 'PK', + total: 24, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'PKkk bbbb cccc cccc cccc cccc', + }, + { + country: 'PS', + total: 29, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 9, + }, + { + type: 'n', + count: 12, + }, + ], + format: 'PSkk bbbb xxxx xxxx xccc cccc cccc c', + }, + { + country: 'PL', + total: 28, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'PLkk bbbs sssx cccc cccc cccc cccc', + }, + { + country: 'PT', + total: 25, + bban: [ + { + type: 'n', + count: 8, + }, + { + type: 'n', + count: 13, + }, + ], + format: 'PTkk bbbb ssss cccc cccc cccx x', + }, + { + country: 'QA', + total: 29, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 21, + }, + ], + format: 'QAkk bbbb cccc cccc cccc cccc cccc c', + }, + { + country: 'RO', + total: 24, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'c', + count: 16, + }, + ], + format: 'ROkk bbbb cccc cccc cccc cccc', + }, + { + country: 'SM', + total: 27, + bban: [ + { + type: 'a', + count: 1, + }, + { + type: 'n', + count: 10, + }, + { + type: 'c', + count: 12, + }, + ], + format: 'SMkk xaaa aabb bbbc cccc cccc ccc', + }, + { + country: 'SA', + total: 24, + bban: [ + { + type: 'n', + count: 2, + }, + { + type: 'c', + count: 18, + }, + ], + format: 'SAkk bbcc cccc cccc cccc cccc', + }, + { + country: 'RS', + total: 22, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 15, + }, + ], + format: 'RSkk bbbc cccc cccc cccc xx', + }, + { + country: 'SK', + total: 24, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'SKkk bbbb ssss sscc cccc cccc', + }, + { + country: 'SI', + total: 19, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'SIkk bbss sccc cccc cxx', + }, + { + country: 'ES', + total: 24, + bban: [ + { + type: 'n', + count: 10, + }, + { + type: 'n', + count: 10, + }, + ], + format: 'ESkk bbbb gggg xxcc cccc cccc', + }, + { + country: 'SE', + total: 24, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 17, + }, + ], + format: 'SEkk bbbc cccc cccc cccc cccc', + }, + { + country: 'CH', + total: 21, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'c', + count: 12, + }, + ], + format: 'CHkk bbbb bccc cccc cccc c', + }, + { + country: 'TN', + total: 24, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'n', + count: 15, + }, + ], + format: 'TNkk bbss sccc cccc cccc cccc', + }, + { + country: 'TR', + total: 26, + bban: [ + { + type: 'n', + count: 5, + }, + { + type: 'n', + count: 1, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'TRkk bbbb bxcc cccc cccc cccc cc', + }, + { + country: 'AE', + total: 23, + bban: [ + { + type: 'n', + count: 3, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'AEkk bbbc cccc cccc cccc ccc', + }, + { + country: 'GB', + total: 22, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 6, + }, + { + type: 'n', + count: 8, + }, + ], + format: 'GBkk bbbb ssss sscc cccc cc', + }, + { + country: 'VG', + total: 24, + bban: [ + { + type: 'a', + count: 4, + }, + { + type: 'n', + count: 16, + }, + ], + format: 'VGkk bbbb cccc cccc cccc cccc', + }, + ], + iso3166: [ + 'AD', + 'AE', + 'AF', + 'AG', + 'AI', + 'AL', + 'AM', + 'AO', + 'AQ', + 'AR', + 'AS', + 'AT', + 'AU', + 'AW', + 'AX', + 'AZ', + 'BA', + 'BB', + 'BD', + 'BE', + 'BF', + 'BG', + 'BH', + 'BI', + 'BJ', + 'BL', + 'BM', + 'BN', + 'BO', + 'BQ', + 'BR', + 'BS', + 'BT', + 'BV', + 'BW', + 'BY', + 'BZ', + 'CA', + 'CC', + 'CD', + 'CF', + 'CG', + 'CH', + 'CI', + 'CK', + 'CL', + 'CM', + 'CN', + 'CO', + 'CR', + 'CU', + 'CV', + 'CW', + 'CX', + 'CY', + 'CZ', + 'DE', + 'DJ', + 'DK', + 'DM', + 'DO', + 'DZ', + 'EC', + 'EE', + 'EG', + 'EH', + 'ER', + 'ES', + 'ET', + 'FI', + 'FJ', + 'FK', + 'FM', + 'FO', + 'FR', + 'GA', + 'GB', + 'GD', + 'GE', + 'GF', + 'GG', + 'GH', + 'GI', + 'GL', + 'GM', + 'GN', + 'GP', + 'GQ', + 'GR', + 'GS', + 'GT', + 'GU', + 'GW', + 'GY', + 'HK', + 'HM', + 'HN', + 'HR', + 'HT', + 'HU', + 'ID', + 'IE', + 'IL', + 'IM', + 'IN', + 'IO', + 'IQ', + 'IR', + 'IS', + 'IT', + 'JE', + 'JM', + 'JO', + 'JP', + 'KE', + 'KG', + 'KH', + 'KI', + 'KM', + 'KN', + 'KP', + 'KR', + 'KW', + 'KY', + 'KZ', + 'LA', + 'LB', + 'LC', + 'LI', + 'LK', + 'LR', + 'LS', + 'LT', + 'LU', + 'LV', + 'LY', + 'MA', + 'MC', + 'MD', + 'ME', + 'MF', + 'MG', + 'MH', + 'MK', + 'ML', + 'MM', + 'MN', + 'MO', + 'MP', + 'MQ', + 'MR', + 'MS', + 'MT', + 'MU', + 'MV', + 'MW', + 'MX', + 'MY', + 'MZ', + 'NA', + 'NC', + 'NE', + 'NF', + 'NG', + 'NI', + 'NL', + 'NO', + 'NP', + 'NR', + 'NU', + 'NZ', + 'OM', + 'PA', + 'PE', + 'PF', + 'PG', + 'PH', + 'PK', + 'PL', + 'PM', + 'PN', + 'PR', + 'PS', + 'PT', + 'PW', + 'PY', + 'QA', + 'RE', + 'RO', + 'RS', + 'RU', + 'RW', + 'SA', + 'SB', + 'SC', + 'SD', + 'SE', + 'SG', + 'SH', + 'SI', + 'SJ', + 'SK', + 'SL', + 'SM', + 'SN', + 'SO', + 'SR', + 'SS', + 'ST', + 'SV', + 'SX', + 'SY', + 'SZ', + 'TC', + 'TD', + 'TF', + 'TG', + 'TH', + 'TJ', + 'TK', + 'TL', + 'TM', + 'TN', + 'TO', + 'TR', + 'TT', + 'TV', + 'TW', + 'TZ', + 'UA', + 'UG', + 'UM', + 'US', + 'UY', + 'UZ', + 'VA', + 'VC', + 'VE', + 'VG', + 'VI', + 'VN', + 'VU', + 'WF', + 'WS', + 'XK', + 'YE', + 'YT', + 'ZA', + 'ZM', + 'ZW', + ], + mod97: (digitStr) => { + let m = 0; + for (const element of digitStr) { + m = (m * 10 + +element) % 97; + } + + return m; + }, + pattern10: ['01', '02', '03', '04', '05', '06', '07', '08', '09'], + pattern100: ['001', '002', '003', '004', '005', '006', '007', '008', '009'], + toDigitString: (str) => + str.replaceAll(/[A-Z]/gi, (match) => + String((match.toUpperCase().codePointAt(0) ?? Number.NaN) - 55) + ), +}; diff --git a/src/modules/finance/account-name.ts b/src/modules/finance/account-name.ts new file mode 100644 index 00000000000..86d58e22830 --- /dev/null +++ b/src/modules/finance/account-name.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random account name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * accountName(fakerCore) // 'Personal Loan Account' + * + * @since 2.0.1 + */ +export function accountName(fakerCore: FakerCore): string { + return [ + arrayElement(fakerCore, fakerCore.locale.finance.account_type), + 'Account', + ].join(' '); +} diff --git a/src/modules/finance/account-number.ts b/src/modules/finance/account-number.ts new file mode 100644 index 00000000000..0552d9d6ac5 --- /dev/null +++ b/src/modules/finance/account-number.ts @@ -0,0 +1,110 @@ +import type { FakerCore } from '../../core'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random account number. + * + * @param fakerCore The FakerCore to use. + * @param length The length of the account number. Defaults to `8`. + * + * @see stringNumeric(fakerCore): For generating the number with greater control. + * + * @example + * accountNumber(fakerCore) // '92842238' + * accountNumber(fakerCore, 5) // '32564' + * + * @since 8.0.0 + */ +export function accountNumber(fakerCore: FakerCore, length?: number): string; +/** + * Generates a random account number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.length The length of the account number. Defaults to `8`. + * + * @see stringNumeric(fakerCore): For generating the number with greater control. + * + * @example + * accountNumber(fakerCore) // '92842238' + * accountNumber(fakerCore, { length: 5 }) // '32564' + * + * @since 8.0.0 + */ +export function accountNumber( + fakerCore: FakerCore, + options?: { + /** + * The length of the account number. + * + * @default 8 + */ + length?: number; + } +): string; +/** + * Generates a random account number. + * + * @param fakerCore The FakerCore to use. + * @param optionsOrLength An options object or the length of the account number. + * @param optionsOrLength.length The length of the account number. Defaults to `8`. + * + * @see stringNumeric(fakerCore): For generating the number with greater control. + * + * @example + * accountNumber(fakerCore) // '92842238' + * accountNumber(fakerCore, 5) // '28736' + * accountNumber(fakerCore, { length: 5 }) // '32564' + * + * @since 8.0.0 + */ +export function accountNumber( + fakerCore: FakerCore, + optionsOrLength?: + | number + | { + /** + * The length of the account number. + * + * @default 8 + */ + length?: number; + } +): string; +/** + * Generates a random account number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object or the length of the account number. + * @param options.length The length of the account number. Defaults to `8`. + * + * @see stringNumeric(fakerCore): For generating the number with greater control. + * + * @example + * accountNumber(fakerCore) // '92842238' + * accountNumber(fakerCore, 5) // '28736' + * accountNumber(fakerCore, { length: 5 }) // '32564' + * + * @since 8.0.0 + */ +export function accountNumber( + fakerCore: FakerCore, + options: + | number + | { + /** + * The length of the account number. + * + * @default 8 + */ + length?: number; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + const { length = 8 } = options; + + return numeric(fakerCore, { length, allowLeadingZeros: true }); +} diff --git a/src/modules/finance/amount.ts b/src/modules/finance/amount.ts new file mode 100644 index 00000000000..8aea42c225c --- /dev/null +++ b/src/modules/finance/amount.ts @@ -0,0 +1,80 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; + +/** + * Generates a random amount between the given bounds (inclusive). + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.min The lower bound for the amount. Defaults to `0`. + * @param options.max The upper bound for the amount. Defaults to `1000`. + * @param options.dec The number of decimal places for the amount. Defaults to `2`. + * @param options.symbol The symbol used to prefix the amount. Defaults to `''`. + * @param options.autoFormat If true this method will use `Number.toLocaleString()`. Otherwise it will use `Number.toFixed()`. + * + * @see numberFloat(fakerCore): For generating the amount with greater control. + * + * @example + * amount(fakerCore) // '617.87' + * amount(fakerCore, { min: 5, max: 10 }) // '5.53' + * amount(fakerCore, { min: 5, max: 10, dec: 0 }) // '8' + * amount(fakerCore, { min: 5, max: 10, dec: 2, symbol: '$' }) // '$5.85' + * amount(fakerCore, { min: 5, max: 10, dec: 5, symbol: '', autoFormat: true }) // '9,75067' + * + * @since 2.0.1 + */ +export function amount( + fakerCore: FakerCore, + options: { + /** + * The lower bound for the amount. + * + * @default 0 + */ + min?: number; + /** + * The upper bound for the amount. + * + * @default 1000 + */ + max?: number; + /** + * The number of decimal places for the amount. + * + * @default 2 + */ + dec?: number; + /** + * The symbol used to prefix the amount. + * + * @default '' + */ + symbol?: string; + /** + * If true this method will use `Number.toLocaleString()`. Otherwise it will use `Number.toFixed()`. + * + * @default false + */ + autoFormat?: boolean; + } = {} +): string { + const { + autoFormat = false, + dec = 2, + max = 1000, + min = 0, + symbol = '', + } = options; + + const randValue = float(fakerCore, { + max, + min, + fractionDigits: dec, + }); + + const formattedString = autoFormat + ? randValue.toLocaleString(undefined, { minimumFractionDigits: dec }) + : randValue.toFixed(dec); + + return symbol + formattedString; +} diff --git a/src/modules/finance/bic.ts b/src/modules/finance/bic.ts new file mode 100644 index 00000000000..503980fc1de --- /dev/null +++ b/src/modules/finance/bic.ts @@ -0,0 +1,51 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; +import { alpha } from '../string/alpha'; +import { alphanumeric } from '../string/alphanumeric'; +import { ibanLib } from './_iban-lib'; + +/** + * Generates a random SWIFT/BIC code based on the [ISO-9362](https://en.wikipedia.org/wiki/ISO_9362) format. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.includeBranchCode Whether to include a three-digit branch code at the end of the generated code. Defaults to a random boolean value. + * + * @example + * bic(fakerCore) // 'WYAUPGX1' + * bic(fakerCore, { includeBranchCode: true }) // 'KCAUPGR1432' + * bic(fakerCore, { includeBranchCode: false }) // 'XDAFQGT7' + * + * @since 4.0.0 + */ +export function bic( + fakerCore: FakerCore, + options: { + /** + * Whether to include a three-digit branch code at the end of the generated code. + * + * @default datatypeBoolean(fakerCore) + */ + includeBranchCode?: boolean; + } = {} +): string { + const { includeBranchCode = boolean(fakerCore) } = options; + + const bankIdentifier = alpha(fakerCore, { + length: 4, + casing: 'upper', + }); + const countryCode = arrayElement(fakerCore, ibanLib.iso3166); + const locationCode = alphanumeric(fakerCore, { + length: 2, + casing: 'upper', + }); + const branchCode = includeBranchCode + ? boolean(fakerCore) + ? alphanumeric(fakerCore, { length: 3, casing: 'upper' }) + : 'XXX' + : ''; + + return `${bankIdentifier}${countryCode}${locationCode}${branchCode}`; +} diff --git a/src/modules/finance/bitcoin-address.ts b/src/modules/finance/bitcoin-address.ts new file mode 100644 index 00000000000..64dab1b6db0 --- /dev/null +++ b/src/modules/finance/bitcoin-address.ts @@ -0,0 +1,125 @@ +import type { FakerCore } from '../../core'; +import type { Casing } from '../../utils/types'; +import { enumValue } from '../helpers/enum-value'; +import { int } from '../number/int'; +import { alphanumeric } from '../string/alphanumeric'; + +/** + * The bitcoin address families. + */ +export enum BitcoinAddressFamily { + Legacy = 'legacy', + Segwit = 'segwit', + Bech32 = 'bech32', + Taproot = 'taproot', +} + +/** + * The bitcoin address families. + */ +export type BitcoinAddressFamilyType = `${BitcoinAddressFamily}`; + +/** + * The different bitcoin networks. + */ +export enum BitcoinNetwork { + Mainnet = 'mainnet', + Testnet = 'testnet', +} + +/** + * The different bitcoin networks. + */ +export type BitcoinNetworkType = `${BitcoinNetwork}`; + +type BitcoinAddressOptions = { + prefix: Record; + length: { min: number; max: number }; + casing: Casing; + exclude: string; +}; + +export const BitcoinAddressSpecs: Record< + BitcoinAddressFamilyType, + BitcoinAddressOptions +> = { + [BitcoinAddressFamily.Legacy]: { + prefix: { [BitcoinNetwork.Mainnet]: '1', [BitcoinNetwork.Testnet]: 'm' }, + length: { min: 26, max: 34 }, + casing: 'mixed', + exclude: '0OIl', + }, + [BitcoinAddressFamily.Segwit]: { + prefix: { [BitcoinNetwork.Mainnet]: '3', [BitcoinNetwork.Testnet]: '2' }, + length: { min: 26, max: 34 }, + casing: 'mixed', + exclude: '0OIl', + }, + [BitcoinAddressFamily.Bech32]: { + prefix: { + [BitcoinNetwork.Mainnet]: 'bc1', + [BitcoinNetwork.Testnet]: 'tb1', + }, + length: { min: 42, max: 42 }, + casing: 'lower', + exclude: '1bBiIoO', + }, + [BitcoinAddressFamily.Taproot]: { + prefix: { + [BitcoinNetwork.Mainnet]: 'bc1p', + [BitcoinNetwork.Testnet]: 'tb1p', + }, + length: { min: 62, max: 62 }, + casing: 'lower', + exclude: '1bBiIoO', + }, +}; + +/** + * Generates a random Bitcoin address. + * + * @param fakerCore The FakerCore to use. + * @param options An optional options object. + * @param options.type The bitcoin address type (`'legacy'`, `'segwit'`, `'bech32'` or `'taproot'`). Defaults to a random address type. + * @param options.network The bitcoin network (`'mainnet'` or `'testnet'`). Defaults to `'mainnet'`. + * + * @example + * bitcoinAddress(fakerCore) // '1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog' + * bitcoinAddress(fakerCore, { type: 'bech32' }) // 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' + * bitcoinAddress(fakerCore, { type: 'bech32', network: 'testnet' }) // 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx' + * + * @since 3.1.0 + */ +export function bitcoinAddress( + fakerCore: FakerCore, + options: { + /** + * The bitcoin address type (`'legacy'`, `'segwit'`, `'bech32'` or `'taproot'`). + * + * @default helpersEnumValue(fakerCore, BitcoinAddressFamily) + */ + type?: BitcoinAddressFamilyType; + /** + * The bitcoin network (`'mainnet'` or `'testnet'`). + * + * @default 'mainnet' + */ + network?: BitcoinNetworkType; + } = {} +): string { + const { + type = enumValue(fakerCore, BitcoinAddressFamily), + network = BitcoinNetwork.Mainnet, + } = options; + const addressSpec = BitcoinAddressSpecs[type]; + const addressPrefix = addressSpec.prefix[network]; + const addressLength = int(fakerCore, addressSpec.length); + + const address = alphanumeric(fakerCore, { + length: addressLength - addressPrefix.length, + casing: addressSpec.casing, + exclude: addressSpec.exclude, + }); + + return addressPrefix + address; +} diff --git a/src/modules/finance/bitcoin.ts b/src/modules/finance/bitcoin.ts deleted file mode 100644 index 08d035a787b..00000000000 --- a/src/modules/finance/bitcoin.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Casing } from '../../utils/types'; - -/** - * The bitcoin address families. - */ -export enum BitcoinAddressFamily { - Legacy = 'legacy', - Segwit = 'segwit', - Bech32 = 'bech32', - Taproot = 'taproot', -} - -/** - * The bitcoin address families. - */ -export type BitcoinAddressFamilyType = `${BitcoinAddressFamily}`; - -/** - * The different bitcoin networks. - */ -export enum BitcoinNetwork { - Mainnet = 'mainnet', - Testnet = 'testnet', -} - -/** - * The different bitcoin networks. - */ -export type BitcoinNetworkType = `${BitcoinNetwork}`; - -type BitcoinAddressOptions = { - prefix: Record; - length: { min: number; max: number }; - casing: Casing; - exclude: string; -}; - -export const BitcoinAddressSpecs: Record< - BitcoinAddressFamilyType, - BitcoinAddressOptions -> = { - [BitcoinAddressFamily.Legacy]: { - prefix: { [BitcoinNetwork.Mainnet]: '1', [BitcoinNetwork.Testnet]: 'm' }, - length: { min: 26, max: 34 }, - casing: 'mixed', - exclude: '0OIl', - }, - [BitcoinAddressFamily.Segwit]: { - prefix: { [BitcoinNetwork.Mainnet]: '3', [BitcoinNetwork.Testnet]: '2' }, - length: { min: 26, max: 34 }, - casing: 'mixed', - exclude: '0OIl', - }, - [BitcoinAddressFamily.Bech32]: { - prefix: { - [BitcoinNetwork.Mainnet]: 'bc1', - [BitcoinNetwork.Testnet]: 'tb1', - }, - length: { min: 42, max: 42 }, - casing: 'lower', - exclude: '1bBiIoO', - }, - [BitcoinAddressFamily.Taproot]: { - prefix: { - [BitcoinNetwork.Mainnet]: 'bc1p', - [BitcoinNetwork.Testnet]: 'tb1p', - }, - length: { min: 62, max: 62 }, - casing: 'lower', - exclude: '1bBiIoO', - }, -}; diff --git a/src/modules/finance/credit-card-cvv.ts b/src/modules/finance/credit-card-cvv.ts new file mode 100644 index 00000000000..8d7b6b460e7 --- /dev/null +++ b/src/modules/finance/credit-card-cvv.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random credit card CVV. + * + * @param fakerCore The FakerCore to use. + * + * @example + * creditCardCVV(fakerCore) // '506' + * + * @since 5.0.0 + */ +export function creditCardCVV(fakerCore: FakerCore): string { + return numeric(fakerCore, { length: 3, allowLeadingZeros: true }); +} diff --git a/src/modules/finance/credit-card-issuer.ts b/src/modules/finance/credit-card-issuer.ts new file mode 100644 index 00000000000..ff98d60d389 --- /dev/null +++ b/src/modules/finance/credit-card-issuer.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { objectKey } from '../helpers/object-key'; + +/** + * Returns a random credit card issuer. + * + * @param fakerCore The FakerCore to use. + * + * @example + * creditCardIssuer(fakerCore) // 'discover' + * + * @since 6.3.0 + */ +export function creditCardIssuer(fakerCore: FakerCore): string { + return objectKey(fakerCore, fakerCore.locale.finance.credit_card) as string; +} diff --git a/src/modules/finance/credit-card-number.ts b/src/modules/finance/credit-card-number.ts new file mode 100644 index 00000000000..2e775b3385e --- /dev/null +++ b/src/modules/finance/credit-card-number.ts @@ -0,0 +1,124 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { objectValue } from '../helpers/object-value'; +import { replaceCreditCardSymbols } from '../helpers/replace-credit-card-symbols'; + +/** + * Generates a random credit card number. + * + * @param fakerCore The FakerCore to use. + * @param issuer The name of the issuer (case-insensitive) or the format used to generate one. + * + * @example + * creditCardNumber(fakerCore) // '4427163488662' + * creditCardNumber(fakerCore, 'visa') // '4882664999007' + * creditCardNumber(fakerCore, '63[7-9]#-####-####-###L') // '6375-3265-4676-6646' + * + * @since 5.0.0 + */ +export function creditCardNumber(fakerCore: FakerCore, issuer?: string): string; +/** + * Generates a random credit card number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.issuer The name of the issuer (case-insensitive) or the format used to generate one. Defaults to `''`. + * + * @example + * creditCardNumber(fakerCore) // '4427163488662' + * creditCardNumber(fakerCore, { issuer: 'visa' }) // '4882664999007' + * creditCardNumber(fakerCore, { issuer: '63[7-9]#-####-####-###L' }) // '6375-3265-4676-6646' + * + * @since 5.0.0 + */ +export function creditCardNumber( + fakerCore: FakerCore, + options?: { + /** + * The name of the issuer (case-insensitive) or the format used to generate one. + * + * @default '' + */ + issuer?: string; + } +): string; +/** + * Generates a random credit card number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object, the issuer or a custom format. + * @param options.issuer The name of the issuer (case-insensitive) or the format used to generate one. Defaults to `''`. + * + * @example + * creditCardNumber(fakerCore) // '4427163488662' + * creditCardNumber(fakerCore, { issuer: 'visa' }) // '4882664999007' + * creditCardNumber(fakerCore, { issuer: '63[7-9]#-####-####-###L' }) // '6375-3265-4676-6646' + * creditCardNumber(fakerCore, 'visa') // '1226423499765' + * + * @since 5.0.0 + */ +export function creditCardNumber( + fakerCore: FakerCore, + options?: + | string + | { + /** + * The name of the issuer (case-insensitive) or the format used to generate one. + * + * @default '' + */ + issuer?: string; + } +): string; +/** + * Generates a random credit card number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object, the issuer or a custom format. + * @param options.issuer The name of the issuer (case-insensitive) or the format used to generate one. + * + * @example + * creditCardNumber(fakerCore) // '4427163488662' + * creditCardNumber(fakerCore, { issuer: 'visa' }) // '4882664999007' + * creditCardNumber(fakerCore, { issuer: '63[7-9]#-####-####-###L' }) // '6375-3265-4676-6646' + * creditCardNumber(fakerCore, 'visa') // '1226423499765' + * + * @since 5.0.0 + */ +export function creditCardNumber( + fakerCore: FakerCore, + options: + | string + | { + /** + * The name of the issuer (case-insensitive) or the format used to generate one. + * + * @default '' + */ + issuer?: string; + } = {} +): string { + if (typeof options === 'string') { + options = { issuer: options }; + } + + const { issuer = '' } = options; + + let format: string; + const localeFormat = fakerCore.locale.finance.credit_card; + const normalizedIssuer = issuer.toLowerCase(); + if (normalizedIssuer in localeFormat) { + format = arrayElement(fakerCore, localeFormat[normalizedIssuer]); + } else if (issuer.includes('#')) { + // The user chose an optional scheme + format = issuer; + } else { + // Choose a random issuer + // Credit cards are in an object structure + const formats = objectValue(fakerCore, localeFormat); // There could be multiple formats + format = arrayElement(fakerCore, formats); + } + + format = format.replaceAll('/', ''); + return replaceCreditCardSymbols(fakerCore, format); +} diff --git a/src/modules/finance/currency-code.ts b/src/modules/finance/currency-code.ts new file mode 100644 index 00000000000..7ab764a8e88 --- /dev/null +++ b/src/modules/finance/currency-code.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { currency } from '../finance/currency'; + +/** + * Returns a random currency code. + * (The short text/abbreviation for the currency (e.g. `US Dollar` -> `USD`)) + * + * @param fakerCore The FakerCore to use. + * + * @example + * currencyCode(fakerCore) // 'USD' + * + * @since 2.0.1 + */ +export function currencyCode(fakerCore: FakerCore): string { + return currency(fakerCore).code; +} diff --git a/src/modules/finance/currency-name.ts b/src/modules/finance/currency-name.ts new file mode 100644 index 00000000000..b9c5eb2fccf --- /dev/null +++ b/src/modules/finance/currency-name.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { currency } from '../finance/currency'; + +/** + * Returns a random currency name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * currencyName(fakerCore) // 'US Dollar' + * + * @since 2.0.1 + */ +export function currencyName(fakerCore: FakerCore): string { + return currency(fakerCore).name; +} diff --git a/src/modules/finance/currency-numeric-code.ts b/src/modules/finance/currency-numeric-code.ts new file mode 100644 index 00000000000..c3ff22d36e6 --- /dev/null +++ b/src/modules/finance/currency-numeric-code.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { currency } from '../finance/currency'; + +/** + * Returns a random currency numeric code. + * (The ISO 4217 numerical code for a currency (e.g. `US Dollar` -> `840` )) + * + * @param fakerCore The FakerCore to use. + * + * @example + * currencyNumericCode(fakerCore) // '840' + * + * @since 9.6.0 + */ +export function currencyNumericCode(fakerCore: FakerCore): string { + return currency(fakerCore).numericCode; +} diff --git a/src/modules/finance/currency-symbol.ts b/src/modules/finance/currency-symbol.ts new file mode 100644 index 00000000000..e4e890ee5b8 --- /dev/null +++ b/src/modules/finance/currency-symbol.ts @@ -0,0 +1,21 @@ +import type { FakerCore } from '../../core'; +import { currency } from '../finance/currency'; + +/** + * Returns a random currency symbol. + * + * @param fakerCore The FakerCore to use. + * + * @example + * currencySymbol(fakerCore) // '$' + * + * @since 2.0.1 + */ +export function currencySymbol(fakerCore: FakerCore): string { + let symbol: string; + do { + symbol = currency(fakerCore).symbol; + } while (symbol.length === 0); + + return symbol; +} diff --git a/src/modules/finance/currency.ts b/src/modules/finance/currency.ts new file mode 100644 index 00000000000..82c50a259e6 --- /dev/null +++ b/src/modules/finance/currency.ts @@ -0,0 +1,46 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * The possible definitions related to currency entries. + */ +export interface Currency { + /** + * The full name for the currency (e.g. `US Dollar`). + */ + name: string; + + /** + * The code/short text/abbreviation for the currency (e.g. `USD`). + */ + code: string; + + /** + * The symbol for the currency (e.g. `$`). + */ + symbol: string; + + /** + * The ISO 4217 numeric code for the currency (e.g. `840`). + */ + numericCode: string; +} + +/** + * Returns a random currency object, containing `code`, `name`, `symbol`, and `numericCode` properties. + * + * @param fakerCore The FakerCore to use. + * + * @see currencyCode(fakerCore): For generating specifically the currency code. + * @see currencyName(fakerCore): For generating specifically the currency name. + * @see currencySymbol(fakerCore): For generating specifically the currency symbol. + * @see currencyNumericCode(fakerCore): For generating specifically the currency numeric code. + * + * @example + * currency(fakerCore) // { code: 'USD', name: 'US Dollar', symbol: '$', numericCode: '840' } + * + * @since 8.0.0 + */ +export function currency(fakerCore: FakerCore): Currency { + return arrayElement(fakerCore, fakerCore.locale.finance.currency); +} diff --git a/src/modules/finance/ethereum-address.ts b/src/modules/finance/ethereum-address.ts new file mode 100644 index 00000000000..9f030ca67fe --- /dev/null +++ b/src/modules/finance/ethereum-address.ts @@ -0,0 +1,22 @@ +import type { FakerCore } from '../../core'; +import { hexadecimal } from '../string/hexadecimal'; + +/** + * Creates a random, non-checksum Ethereum address. + * + * To generate a checksummed Ethereum address (with specific per character casing), wrap this method in a custom method and use third-party libraries to transform the result. + * + * @param fakerCore The FakerCore to use. + * + * @example + * ethereumAddress(fakerCore) // '0xf03dfeecbafc5147241cc4c4ca20b3c9dfd04c4a' + * + * @since 5.0.0 + */ +export function ethereumAddress(fakerCore: FakerCore): string { + const address = hexadecimal(fakerCore, { + length: 40, + casing: 'lower', + }); + return address; +} diff --git a/src/modules/finance/iban.ts b/src/modules/finance/iban.ts index c4ca0e7f1fc..417eb60e680 100644 --- a/src/modules/finance/iban.ts +++ b/src/modules/finance/iban.ts @@ -1,1426 +1,113 @@ -interface Iban { - alpha: string[]; - formats: Array<{ - bban: Array<{ type: string; count: number }>; - country: string; - format?: string; - total?: number; - }>; - iso3166: string[]; - mod97: (digitStr: string) => number; - pattern10: string[]; - pattern100: string[]; - toDigitString: (str: string) => string; +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; +import { ibanLib } from './_iban-lib'; + +/** + * Puts a space after every 4 characters. + * + * @internal + * + * @param iban The iban to pretty print. + */ +export function prettyPrintIban(iban: string): string { + let pretty = ''; + for (let i = 0; i < iban.length; i += 4) { + pretty += `${iban.substring(i, i + 4)} `; + } + + return pretty.trimEnd(); } -const iban: Iban = { - 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', - ], - formats: [ - { - country: 'AL', - total: 28, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'c', - count: 16, - }, - ], - format: 'ALkk bbbs sssx cccc cccc cccc cccc', - }, - { - country: 'AD', - total: 24, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'c', - count: 12, - }, - ], - format: 'ADkk bbbb ssss cccc cccc cccc', - }, - { - country: 'AT', - total: 20, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'n', - count: 11, - }, - ], - format: 'ATkk bbbb bccc cccc cccc', - }, - { - // Azerbaijan - // https://transferwise.com/fr/iban/azerbaijan - // Length 28 - // BBAN 2c,16n - // GEkk bbbb cccc cccc cccc cccc cccc - // b = National bank code (alpha) - // c = Account number - // example IBAN AZ21 NABZ 0000 0000 1370 1000 1944 - country: 'AZ', - total: 28, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 20, - }, - ], - format: 'AZkk bbbb cccc cccc cccc cccc cccc', - }, - { - country: 'BH', - total: 22, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 14, - }, - ], - format: 'BHkk bbbb cccc cccc cccc cc', - }, - { - country: 'BE', - total: 16, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 9, - }, - ], - format: 'BEkk bbbc cccc ccxx', - }, - { - country: 'BA', - total: 20, - bban: [ - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'BAkk bbbs sscc cccc ccxx', - }, - { - country: 'BR', - total: 29, - bban: [ - { - type: 'n', - count: 13, - }, - { - type: 'n', - count: 10, - }, - { - type: 'a', - count: 1, - }, - { - type: 'c', - count: 1, - }, - ], - format: 'BRkk bbbb bbbb ssss sccc cccc ccct n', - }, - { - country: 'BG', - total: 22, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 6, - }, - { - type: 'c', - count: 8, - }, - ], - format: 'BGkk bbbb ssss ddcc cccc cc', - }, - { - country: 'CR', - total: 22, - bban: [ - { - type: 'n', - count: 1, - }, - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 14, - }, - ], - format: 'CRkk xbbb cccc cccc cccc cc', - }, - { - country: 'HR', - total: 21, - bban: [ - { - type: 'n', - count: 7, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'HRkk bbbb bbbc cccc cccc c', - }, - { - country: 'CY', - total: 28, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'c', - count: 16, - }, - ], - format: 'CYkk bbbs ssss cccc cccc cccc cccc', - }, - { - country: 'CZ', - total: 24, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'CZkk bbbb ssss sscc cccc cccc', - }, - { - country: 'DK', - total: 18, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'DKkk bbbb cccc cccc cc', - }, - { - country: 'DO', - total: 28, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 20, - }, - ], - format: 'DOkk bbbb cccc cccc cccc cccc cccc', - }, - { - country: 'TL', - total: 23, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'TLkk bbbc cccc cccc cccc cxx', - }, - { - country: 'EE', - total: 20, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 12, - }, - ], - format: 'EEkk bbss cccc cccc cccx', - }, - { - country: 'FO', - total: 18, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'FOkk bbbb cccc cccc cx', - }, - { - country: 'FI', - total: 18, - bban: [ - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 8, - }, - ], - format: 'FIkk bbbb bbcc cccc cx', - }, - { - country: 'FR', - total: 27, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'c', - count: 11, - }, - { - type: 'n', - count: 2, - }, - ], - format: 'FRkk bbbb bggg ggcc cccc cccc cxx', - }, - { - country: 'GE', - total: 22, - bban: [ - { - type: 'a', - count: 2, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'GEkk bbcc cccc cccc cccc cc', - }, - { - country: 'DE', - total: 22, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'DEkk bbbb bbbb cccc cccc cc', - }, - { - country: 'GI', - total: 23, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 15, - }, - ], - format: 'GIkk bbbb cccc cccc cccc ccc', - }, - { - country: 'GR', - total: 27, - bban: [ - { - type: 'n', - count: 7, - }, - { - type: 'c', - count: 16, - }, - ], - format: 'GRkk bbbs sssc cccc cccc cccc ccc', - }, - { - country: 'GL', - total: 18, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'GLkk bbbb cccc cccc cc', - }, - { - country: 'GT', - total: 28, - bban: [ - { - type: 'c', - count: 4, - }, - { - type: 'c', - count: 4, - }, - { - type: 'c', - count: 16, - }, - ], - format: 'GTkk bbbb mmtt cccc cccc cccc cccc', - }, - { - country: 'HU', - total: 28, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'HUkk bbbs sssk cccc cccc cccc cccx', - }, - { - country: 'IS', - total: 26, - bban: [ - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'ISkk bbbb sscc cccc iiii iiii ii', - }, - { - country: 'IE', - total: 22, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 8, - }, - ], - format: 'IEkk aaaa bbbb bbcc cccc cc', - }, - { - country: 'IL', - total: 23, - bban: [ - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 13, - }, - ], - format: 'ILkk bbbn nncc cccc cccc ccc', - }, - { - country: 'IR', - total: 26, - bban: [ - { - type: 'n', - count: 22, - }, - ], - format: 'IRkk bbbb cccc cccc cccc cccc cc', - }, - { - country: 'IT', - total: 27, - bban: [ - { - type: 'a', - count: 1, - }, - { - type: 'n', - count: 10, - }, - { - type: 'c', - count: 12, - }, - ], - format: 'ITkk xaaa aabb bbbc cccc cccc ccc', - }, - { - country: 'JO', - total: 30, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 18, - }, - ], - format: 'JOkk bbbb nnnn cccc cccc cccc cccc cc', - }, - { - country: 'KZ', - total: 20, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'c', - count: 13, - }, - ], - format: 'KZkk bbbc cccc cccc cccc', - }, - { - country: 'XK', - total: 20, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 12, - }, - ], - format: 'XKkk bbbb cccc cccc cccc', - }, - { - country: 'KW', - total: 30, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 22, - }, - ], - format: 'KWkk bbbb cccc cccc cccc cccc cccc cc', - }, - { - country: 'LV', - total: 21, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 13, - }, - ], - format: 'LVkk bbbb cccc cccc cccc c', - }, - { - country: 'LB', - total: 28, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'c', - count: 20, - }, - ], - format: 'LBkk bbbb cccc cccc cccc cccc cccc', - }, - { - country: 'LI', - total: 21, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'c', - count: 12, - }, - ], - format: 'LIkk bbbb bccc cccc cccc c', - }, - { - country: 'LT', - total: 20, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'n', - count: 11, - }, - ], - format: 'LTkk bbbb bccc cccc cccc', - }, - { - country: 'LU', - total: 20, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'c', - count: 13, - }, - ], - format: 'LUkk bbbc cccc cccc cccc', - }, - { - country: 'MK', - total: 19, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'c', - count: 10, - }, - { - type: 'n', - count: 2, - }, - ], - format: 'MKkk bbbc cccc cccc cxx', - }, - { - country: 'MT', - total: 31, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 5, - }, - { - type: 'c', - count: 18, - }, - ], - format: 'MTkk bbbb ssss sccc cccc cccc cccc ccc', - }, - { - country: 'MR', - total: 27, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'n', - count: 13, - }, - ], - format: 'MRkk bbbb bsss sscc cccc cccc cxx', - }, - { - country: 'MU', - total: 30, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 15, - }, - { - type: 'a', - count: 3, - }, - ], - format: 'MUkk bbbb bbss cccc cccc cccc 000d dd', - }, - { - country: 'MC', - total: 27, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'c', - count: 11, - }, - { - type: 'n', - count: 2, - }, - ], - format: 'MCkk bbbb bsss sscc cccc cccc cxx', - }, - { - country: 'MD', - total: 24, - bban: [ - { - type: 'c', - count: 2, - }, - { - type: 'c', - count: 18, - }, - ], - format: 'MDkk bbcc cccc cccc cccc cccc', - }, - { - country: 'ME', - total: 22, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 15, - }, - ], - format: 'MEkk bbbc cccc cccc cccc xx', - }, - { - country: 'NL', - total: 18, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'NLkk bbbb cccc cccc cc', - }, - { - country: 'NO', - total: 15, - bban: [ - { - type: 'n', - count: 4, - }, - { - type: 'n', - count: 7, - }, - ], - format: 'NOkk bbbb cccc ccx', - }, - { - country: 'PK', - total: 24, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'PKkk bbbb cccc cccc cccc cccc', - }, - { - country: 'PS', - total: 29, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 9, - }, - { - type: 'n', - count: 12, - }, - ], - format: 'PSkk bbbb xxxx xxxx xccc cccc cccc c', - }, - { - country: 'PL', - total: 28, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'PLkk bbbs sssx cccc cccc cccc cccc', - }, - { - country: 'PT', - total: 25, - bban: [ - { - type: 'n', - count: 8, - }, - { - type: 'n', - count: 13, - }, - ], - format: 'PTkk bbbb ssss cccc cccc cccx x', - }, - { - country: 'QA', - total: 29, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 21, - }, - ], - format: 'QAkk bbbb cccc cccc cccc cccc cccc c', - }, - { - country: 'RO', - total: 24, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'c', - count: 16, - }, - ], - format: 'ROkk bbbb cccc cccc cccc cccc', - }, - { - country: 'SM', - total: 27, - bban: [ - { - type: 'a', - count: 1, - }, - { - type: 'n', - count: 10, - }, - { - type: 'c', - count: 12, - }, - ], - format: 'SMkk xaaa aabb bbbc cccc cccc ccc', - }, - { - country: 'SA', - total: 24, - bban: [ - { - type: 'n', - count: 2, - }, - { - type: 'c', - count: 18, - }, - ], - format: 'SAkk bbcc cccc cccc cccc cccc', - }, - { - country: 'RS', - total: 22, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 15, - }, - ], - format: 'RSkk bbbc cccc cccc cccc xx', - }, - { - country: 'SK', - total: 24, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'SKkk bbbb ssss sscc cccc cccc', - }, - { - country: 'SI', - total: 19, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'SIkk bbss sccc cccc cxx', - }, - { - country: 'ES', - total: 24, - bban: [ - { - type: 'n', - count: 10, - }, - { - type: 'n', - count: 10, - }, - ], - format: 'ESkk bbbb gggg xxcc cccc cccc', - }, - { - country: 'SE', - total: 24, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 17, - }, - ], - format: 'SEkk bbbc cccc cccc cccc cccc', - }, - { - country: 'CH', - total: 21, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'c', - count: 12, - }, - ], - format: 'CHkk bbbb bccc cccc cccc c', - }, - { - country: 'TN', - total: 24, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'n', - count: 15, - }, - ], - format: 'TNkk bbss sccc cccc cccc cccc', - }, - { - country: 'TR', - total: 26, - bban: [ - { - type: 'n', - count: 5, - }, - { - type: 'n', - count: 1, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'TRkk bbbb bxcc cccc cccc cccc cc', - }, - { - country: 'AE', - total: 23, - bban: [ - { - type: 'n', - count: 3, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'AEkk bbbc cccc cccc cccc ccc', - }, - { - country: 'GB', - total: 22, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 6, - }, - { - type: 'n', - count: 8, - }, - ], - format: 'GBkk bbbb ssss sscc cccc cc', - }, - { - country: 'VG', - total: 24, - bban: [ - { - type: 'a', - count: 4, - }, - { - type: 'n', - count: 16, - }, - ], - format: 'VGkk bbbb cccc cccc cccc cccc', - }, - ], - iso3166: [ - 'AD', - 'AE', - 'AF', - 'AG', - 'AI', - 'AL', - 'AM', - 'AO', - 'AQ', - 'AR', - 'AS', - 'AT', - 'AU', - 'AW', - 'AX', - 'AZ', - 'BA', - 'BB', - 'BD', - 'BE', - 'BF', - 'BG', - 'BH', - 'BI', - 'BJ', - 'BL', - 'BM', - 'BN', - 'BO', - 'BQ', - 'BR', - 'BS', - 'BT', - 'BV', - 'BW', - 'BY', - 'BZ', - 'CA', - 'CC', - 'CD', - 'CF', - 'CG', - 'CH', - 'CI', - 'CK', - 'CL', - 'CM', - 'CN', - 'CO', - 'CR', - 'CU', - 'CV', - 'CW', - 'CX', - 'CY', - 'CZ', - 'DE', - 'DJ', - 'DK', - 'DM', - 'DO', - 'DZ', - 'EC', - 'EE', - 'EG', - 'EH', - 'ER', - 'ES', - 'ET', - 'FI', - 'FJ', - 'FK', - 'FM', - 'FO', - 'FR', - 'GA', - 'GB', - 'GD', - 'GE', - 'GF', - 'GG', - 'GH', - 'GI', - 'GL', - 'GM', - 'GN', - 'GP', - 'GQ', - 'GR', - 'GS', - 'GT', - 'GU', - 'GW', - 'GY', - 'HK', - 'HM', - 'HN', - 'HR', - 'HT', - 'HU', - 'ID', - 'IE', - 'IL', - 'IM', - 'IN', - 'IO', - 'IQ', - 'IR', - 'IS', - 'IT', - 'JE', - 'JM', - 'JO', - 'JP', - 'KE', - 'KG', - 'KH', - 'KI', - 'KM', - 'KN', - 'KP', - 'KR', - 'KW', - 'KY', - 'KZ', - 'LA', - 'LB', - 'LC', - 'LI', - 'LK', - 'LR', - 'LS', - 'LT', - 'LU', - 'LV', - 'LY', - 'MA', - 'MC', - 'MD', - 'ME', - 'MF', - 'MG', - 'MH', - 'MK', - 'ML', - 'MM', - 'MN', - 'MO', - 'MP', - 'MQ', - 'MR', - 'MS', - 'MT', - 'MU', - 'MV', - 'MW', - 'MX', - 'MY', - 'MZ', - 'NA', - 'NC', - 'NE', - 'NF', - 'NG', - 'NI', - 'NL', - 'NO', - 'NP', - 'NR', - 'NU', - 'NZ', - 'OM', - 'PA', - 'PE', - 'PF', - 'PG', - 'PH', - 'PK', - 'PL', - 'PM', - 'PN', - 'PR', - 'PS', - 'PT', - 'PW', - 'PY', - 'QA', - 'RE', - 'RO', - 'RS', - 'RU', - 'RW', - 'SA', - 'SB', - 'SC', - 'SD', - 'SE', - 'SG', - 'SH', - 'SI', - 'SJ', - 'SK', - 'SL', - 'SM', - 'SN', - 'SO', - 'SR', - 'SS', - 'ST', - 'SV', - 'SX', - 'SY', - 'SZ', - 'TC', - 'TD', - 'TF', - 'TG', - 'TH', - 'TJ', - 'TK', - 'TL', - 'TM', - 'TN', - 'TO', - 'TR', - 'TT', - 'TV', - 'TW', - 'TZ', - 'UA', - 'UG', - 'UM', - 'US', - 'UY', - 'UZ', - 'VA', - 'VC', - 'VE', - 'VG', - 'VI', - 'VN', - 'VU', - 'WF', - 'WS', - 'XK', - 'YE', - 'YT', - 'ZA', - 'ZM', - 'ZW', - ], - mod97: (digitStr) => { - let m = 0; - for (const element of digitStr) { - m = (m * 10 + +element) % 97; +/** + * Generates a random IBAN. + * + * Please note that the generated IBAN might be invalid due to randomly generated bank codes/other country specific validation rules. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.formatted Return a formatted version of the generated IBAN. Defaults to `false`. + * @param options.countryCode The country code from which you want to generate an IBAN, if none is provided a random country will be used. + * + * @throws {FakerError} Will throw an error if the passed country code is not supported. + * + * @example + * iban(fakerCore) // 'TR736918640040966092800056' + * iban(fakerCore, { formatted: true }) // 'FR20 8008 2330 8984 74S3 Z620 224' + * iban(fakerCore, { formatted: true, countryCode: 'DE' }) // 'DE84 1022 7075 0900 1170 01' + * + * @since 4.0.0 + */ +export function iban( + fakerCore: FakerCore, + options: { + /** + * Return a formatted version of the generated IBAN. + * + * @default false + */ + formatted?: boolean; + /** + * The country code from which you want to generate an IBAN, + * if none is provided a random country will be used. + */ + countryCode?: string; + } = {} +): string { + const { countryCode, formatted = false } = options; + + const ibanFormat = countryCode + ? ibanLib.formats.find((f) => f.country === countryCode) + : arrayElement(fakerCore, ibanLib.formats); + + if (!ibanFormat) { + throw new FakerError(`Country code ${countryCode} not supported.`); + } + + let s = ''; + let count = 0; + for (const bban of ibanFormat.bban) { + let c = bban.count; + count += bban.count; + while (c > 0) { + if (bban.type === 'a') { + s += arrayElement(fakerCore, ibanLib.alpha); + } else if (bban.type === 'c') { + if (boolean(fakerCore, 0.8)) { + s += int(fakerCore, 9); + } else { + s += arrayElement(fakerCore, ibanLib.alpha); + } + } else { + if (c >= 3 && boolean(fakerCore, 0.3)) { + if (boolean(fakerCore)) { + s += arrayElement(fakerCore, ibanLib.pattern100); + c -= 2; + } else { + s += arrayElement(fakerCore, ibanLib.pattern10); + c--; + } + } else { + s += int(fakerCore, 9); + } + } + + c--; } - return m; - }, - pattern10: ['01', '02', '03', '04', '05', '06', '07', '08', '09'], - pattern100: ['001', '002', '003', '004', '005', '006', '007', '008', '009'], - toDigitString: (str) => - str.replaceAll(/[A-Z]/gi, (match) => - String((match.toUpperCase().codePointAt(0) ?? Number.NaN) - 55) - ), -}; + s = s.substring(0, count); + } + + let checksum: string | number = + 98 - ibanLib.mod97(ibanLib.toDigitString(`${s}${ibanFormat.country}00`)); + + if (checksum < 10) { + checksum = `0${checksum}`; + } -export default iban; + const result = `${ibanFormat.country}${checksum}${s}`; + + return formatted ? prettyPrintIban(result) : result; +} diff --git a/src/modules/finance/index.ts b/src/modules/finance/index.ts index 646ea8ae6f1..544093bacce 100644 --- a/src/modules/finance/index.ts +++ b/src/modules/finance/index.ts @@ -1,53 +1,36 @@ -import { FakerError } from '../../errors/faker-error'; import { ModuleBase } from '../../internal/module-base'; -import type { BitcoinAddressFamilyType, BitcoinNetworkType } from './bitcoin'; -import { - BitcoinAddressFamily, - BitcoinAddressSpecs, - BitcoinNetwork, -} from './bitcoin'; -import iban from './iban'; - -/** - * The possible definitions related to currency entries. - */ -export interface Currency { - /** - * The full name for the currency (e.g. `US Dollar`). - */ - name: string; - - /** - * The code/short text/abbreviation for the currency (e.g. `USD`). - */ - code: string; - - /** - * The symbol for the currency (e.g. `$`). - */ - symbol: string; - - /** - * The ISO 4217 numeric code for the currency (e.g. `840`). - */ - numericCode: string; -} - -/** - * Puts a space after every 4 characters. - * - * @internal - * - * @param iban The iban to pretty print. - */ -export function prettyPrintIban(iban: string): string { - let pretty = ''; - for (let i = 0; i < iban.length; i += 4) { - pretty += `${iban.substring(i, i + 4)} `; - } - - return pretty.trimEnd(); -} +import { accountName as financeAccountName } from './account-name'; +import { accountNumber as financeAccountNumber } from './account-number'; +import { amount as financeAmount } from './amount'; +import { bic as financeBic } from './bic'; +import type { + BitcoinAddressFamilyType, + BitcoinNetworkType, +} from './bitcoin-address'; +import { bitcoinAddress as financeBitcoinAddress } from './bitcoin-address'; +import { creditCardCVV as financeCreditCardCVV } from './credit-card-cvv'; +import { creditCardIssuer as financeCreditCardIssuer } from './credit-card-issuer'; +import { creditCardNumber as financeCreditCardNumber } from './credit-card-number'; +import type { Currency } from './currency'; +import { currency as financeCurrency } from './currency'; +import { currencyCode as financeCurrencyCode } from './currency-code'; +import { currencyName as financeCurrencyName } from './currency-name'; +import { currencyNumericCode as financeCurrencyNumericCode } from './currency-numeric-code'; +import { currencySymbol as financeCurrencySymbol } from './currency-symbol'; +import { ethereumAddress as financeEthereumAddress } from './ethereum-address'; +import { iban as financeIban } from './iban'; +import { litecoinAddress as financeLitecoinAddress } from './litecoin-address'; +import { pin as financePin } from './pin'; +import { routingNumber as financeRoutingNumber } from './routing-number'; +import { transactionDescription as financeTransactionDescription } from './transaction-description'; +import { transactionType as financeTransactionType } from './transaction-type'; + +export { BitcoinAddressFamily, BitcoinNetwork } from './bitcoin-address'; +export type { + BitcoinAddressFamilyType, + BitcoinNetworkType, +} from './bitcoin-address'; +export type { Currency } from './currency'; /** * Module to generate finance and money related entries. @@ -153,13 +136,7 @@ export class FinanceModule extends ModuleBase { length?: number; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - const { length = 8 } = options; - - return this.faker.string.numeric({ length, allowLeadingZeros: true }); + return financeAccountNumber(this.faker.fakerCore, options); } /** @@ -171,12 +148,7 @@ export class FinanceModule extends ModuleBase { * @since 2.0.1 */ accountName(): string { - return [ - this.faker.helpers.arrayElement( - this.faker.definitions.finance.account_type - ), - 'Account', - ].join(' '); + return financeAccountName(this.faker.fakerCore); } /** @@ -188,27 +160,7 @@ export class FinanceModule extends ModuleBase { * @since 5.0.0 */ routingNumber(): string { - const federalReserveRoutingSymbol = this.faker.helpers.arrayElement( - this.faker.definitions.finance.federal_reserve_routing_symbol - ); - - const institutionIdentifier = this.faker.string.numeric({ - length: 4, - allowLeadingZeros: true, - }); - - const routingNumber = federalReserveRoutingSymbol + institutionIdentifier; - - // Modules 10 straight summation. - let sum = 0; - - for (let i = 0; i < routingNumber.length; i += 3) { - sum += Number(routingNumber[i]) * 3; - sum += Number(routingNumber[i + 1]) * 7; - sum += Number(routingNumber[i + 2]) || 0; - } - - return `${routingNumber}${Math.ceil(sum / 10) * 10 - sum}`; + return financeRoutingNumber(this.faker.fakerCore); } /** @@ -266,25 +218,7 @@ export class FinanceModule extends ModuleBase { autoFormat?: boolean; } = {} ): string { - const { - autoFormat = false, - dec = 2, - max = 1000, - min = 0, - symbol = '', - } = options; - - const randValue = this.faker.number.float({ - max, - min, - fractionDigits: dec, - }); - - const formattedString = autoFormat - ? randValue.toLocaleString(undefined, { minimumFractionDigits: dec }) - : randValue.toFixed(dec); - - return symbol + formattedString; + return financeAmount(this.faker.fakerCore, options); } /** @@ -296,9 +230,7 @@ export class FinanceModule extends ModuleBase { * @since 2.0.1 */ transactionType(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.finance.transaction_type - ); + return financeTransactionType(this.faker.fakerCore); } /** @@ -315,9 +247,7 @@ export class FinanceModule extends ModuleBase { * @since 8.0.0 */ currency(): Currency { - return this.faker.helpers.arrayElement( - this.faker.definitions.finance.currency - ); + return financeCurrency(this.faker.fakerCore); } /** @@ -330,7 +260,7 @@ export class FinanceModule extends ModuleBase { * @since 2.0.1 */ currencyCode(): string { - return this.currency().code; + return financeCurrencyCode(this.faker.fakerCore); } /** @@ -342,7 +272,7 @@ export class FinanceModule extends ModuleBase { * @since 2.0.1 */ currencyName(): string { - return this.currency().name; + return financeCurrencyName(this.faker.fakerCore); } /** @@ -354,12 +284,7 @@ export class FinanceModule extends ModuleBase { * @since 2.0.1 */ currencySymbol(): string { - let symbol: string; - do { - symbol = this.currency().symbol; - } while (symbol.length === 0); - - return symbol; + return financeCurrencySymbol(this.faker.fakerCore); } /** @@ -372,7 +297,7 @@ export class FinanceModule extends ModuleBase { * @since 9.6.0 */ currencyNumericCode(): string { - return this.currency().numericCode; + return financeCurrencyNumericCode(this.faker.fakerCore); } /** @@ -405,21 +330,7 @@ export class FinanceModule extends ModuleBase { network?: BitcoinNetworkType; } = {} ): string { - const { - type = this.faker.helpers.enumValue(BitcoinAddressFamily), - network = BitcoinNetwork.Mainnet, - } = options; - const addressSpec = BitcoinAddressSpecs[type]; - const addressPrefix = addressSpec.prefix[network]; - const addressLength = this.faker.number.int(addressSpec.length); - - const address = this.faker.string.alphanumeric({ - length: addressLength - addressPrefix.length, - casing: addressSpec.casing, - exclude: addressSpec.exclude, - }); - - return addressPrefix + address; + return financeBitcoinAddress(this.faker.fakerCore, options); } /** @@ -431,16 +342,7 @@ export class FinanceModule extends ModuleBase { * @since 5.0.0 */ litecoinAddress(): string { - const addressLength = this.faker.number.int({ min: 26, max: 33 }); - - const address = - this.faker.string.fromCharacters('LM3') + - this.faker.string.fromCharacters( - '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', - addressLength - 1 - ); - - return address; + return financeLitecoinAddress(this.faker.fakerCore); } /** @@ -529,29 +431,7 @@ export class FinanceModule extends ModuleBase { issuer?: string; } = {} ): string { - if (typeof options === 'string') { - options = { issuer: options }; - } - - const { issuer = '' } = options; - - let format: string; - const localeFormat = this.faker.definitions.finance.credit_card; - const normalizedIssuer = issuer.toLowerCase(); - if (normalizedIssuer in localeFormat) { - format = this.faker.helpers.arrayElement(localeFormat[normalizedIssuer]); - } else if (issuer.includes('#')) { - // The user chose an optional scheme - format = issuer; - } else { - // Choose a random issuer - // Credit cards are in an object structure - const formats = this.faker.helpers.objectValue(localeFormat); // There could be multiple formats - format = this.faker.helpers.arrayElement(formats); - } - - format = format.replaceAll('/', ''); - return this.faker.helpers.replaceCreditCardSymbols(format); + return financeCreditCardNumber(this.faker.fakerCore, options); } /** @@ -563,7 +443,7 @@ export class FinanceModule extends ModuleBase { * @since 5.0.0 */ creditCardCVV(): string { - return this.faker.string.numeric({ length: 3, allowLeadingZeros: true }); + return financeCreditCardCVV(this.faker.fakerCore); } /** @@ -575,9 +455,7 @@ export class FinanceModule extends ModuleBase { * @since 6.3.0 */ creditCardIssuer(): string { - return this.faker.helpers.objectKey( - this.faker.definitions.finance.credit_card - ) as string; + return financeCreditCardIssuer(this.faker.fakerCore); } /** @@ -678,17 +556,7 @@ export class FinanceModule extends ModuleBase { length?: number; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - const { length = 4 } = options; - - if (length < 1) { - throw new FakerError('minimum length is 1'); - } - - return this.faker.string.numeric({ length, allowLeadingZeros: true }); + return financePin(this.faker.fakerCore, options); } /** @@ -702,11 +570,7 @@ export class FinanceModule extends ModuleBase { * @since 5.0.0 */ ethereumAddress(): string { - const address = this.faker.string.hexadecimal({ - length: 40, - casing: 'lower', - }); - return address; + return financeEthereumAddress(this.faker.fakerCore); } /** @@ -742,60 +606,7 @@ export class FinanceModule extends ModuleBase { countryCode?: string; } = {} ): string { - const { countryCode, formatted = false } = options; - - const ibanFormat = countryCode - ? iban.formats.find((f) => f.country === countryCode) - : this.faker.helpers.arrayElement(iban.formats); - - if (!ibanFormat) { - throw new FakerError(`Country code ${countryCode} not supported.`); - } - - let s = ''; - let count = 0; - for (const bban of ibanFormat.bban) { - let c = bban.count; - count += bban.count; - while (c > 0) { - if (bban.type === 'a') { - s += this.faker.helpers.arrayElement(iban.alpha); - } else if (bban.type === 'c') { - if (this.faker.datatype.boolean(0.8)) { - s += this.faker.number.int(9); - } else { - s += this.faker.helpers.arrayElement(iban.alpha); - } - } else { - if (c >= 3 && this.faker.datatype.boolean(0.3)) { - if (this.faker.datatype.boolean()) { - s += this.faker.helpers.arrayElement(iban.pattern100); - c -= 2; - } else { - s += this.faker.helpers.arrayElement(iban.pattern10); - c--; - } - } else { - s += this.faker.number.int(9); - } - } - - c--; - } - - s = s.substring(0, count); - } - - let checksum: string | number = - 98 - iban.mod97(iban.toDigitString(`${s}${ibanFormat.country}00`)); - - if (checksum < 10) { - checksum = `0${checksum}`; - } - - const result = `${ibanFormat.country}${checksum}${s}`; - - return formatted ? prettyPrintIban(result) : result; + return financeIban(this.faker.fakerCore, options); } /** @@ -821,24 +632,7 @@ export class FinanceModule extends ModuleBase { includeBranchCode?: boolean; } = {} ): string { - const { includeBranchCode = this.faker.datatype.boolean() } = options; - - const bankIdentifier = this.faker.string.alpha({ - length: 4, - casing: 'upper', - }); - const countryCode = this.faker.helpers.arrayElement(iban.iso3166); - const locationCode = this.faker.string.alphanumeric({ - length: 2, - casing: 'upper', - }); - const branchCode = includeBranchCode - ? this.faker.datatype.boolean() - ? this.faker.string.alphanumeric({ length: 3, casing: 'upper' }) - : 'XXX' - : ''; - - return `${bankIdentifier}${countryCode}${locationCode}${branchCode}`; + return financeBic(this.faker.fakerCore, options); } /** @@ -851,8 +645,6 @@ export class FinanceModule extends ModuleBase { * @since 5.1.0 */ transactionDescription(): string { - return this.faker.helpers.fake( - this.faker.definitions.finance.transaction_description_pattern - ); + return financeTransactionDescription(this.faker.fakerCore); } } diff --git a/src/modules/finance/litecoin-address.ts b/src/modules/finance/litecoin-address.ts new file mode 100644 index 00000000000..0ac8836f6a1 --- /dev/null +++ b/src/modules/finance/litecoin-address.ts @@ -0,0 +1,27 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; +import { fromCharacters } from '../string/from-characters'; + +/** + * Generates a random Litecoin address. + * + * @param fakerCore The FakerCore to use. + * + * @example + * litecoinAddress(fakerCore) // 'MoQaSTGWBRXkWfyxKbNKuPrAWGELzcW' + * + * @since 5.0.0 + */ +export function litecoinAddress(fakerCore: FakerCore): string { + const addressLength = int(fakerCore, { min: 26, max: 33 }); + + const address = + fromCharacters(fakerCore, 'LM3') + + fromCharacters( + fakerCore, + '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', + addressLength - 1 + ); + + return address; +} diff --git a/src/modules/finance/pin.ts b/src/modules/finance/pin.ts new file mode 100644 index 00000000000..d2014648d5f --- /dev/null +++ b/src/modules/finance/pin.ts @@ -0,0 +1,123 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random PIN number. + * + * @param fakerCore The FakerCore to use. + * @param length The length of the PIN to generate. Defaults to `4`. + * + * @throws {FakerError} Will throw an error if length is less than 1. + * + * @see stringNumeric(fakerCore): For generating the pin with greater control. + * + * @example + * pin(fakerCore) // '5067' + * pin(fakerCore, 6) // '213789' + * + * @since 6.2.0 + */ +export function pin(fakerCore: FakerCore, length?: number): string; +/** + * Generates a random PIN number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.length The length of the PIN to generate. Defaults to `4`. + * + * @throws {FakerError} Will throw an error if length is less than 1. + * + * @see stringNumeric(fakerCore): For generating the pin with greater control. + * + * @example + * pin(fakerCore) // '5067' + * pin(fakerCore, { length: 6 }) // '213789' + * + * @since 6.2.0 + */ +export function pin( + fakerCore: FakerCore, + options?: { + /** + * The length of the PIN to generate. + * + * @default 4 + */ + length?: number; + } +): string; +/** + * Generates a random PIN number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object or the length of the PIN. + * @param options.length The length of the PIN to generate. Defaults to `4`. + * + * @throws {FakerError} Will throw an error if length is less than 1. + * + * @see stringNumeric(fakerCore): For generating the pin with greater control. + * + * @example + * pin(fakerCore) // '5067' + * pin(fakerCore, { length: 6 }) // '213789' + * pin(fakerCore, 6) // '213789' + * + * @since 6.2.0 + */ +export function pin( + fakerCore: FakerCore, + options?: + | number + | { + /** + * The length of the PIN to generate. + * + * @default 4 + */ + length?: number; + } +): string; +/** + * Generates a random PIN number. + * + * @param fakerCore The FakerCore to use. + * @param options An options object or the length of the PIN. + * @param options.length The length of the PIN to generate. Defaults to `4`. + * + * @throws {FakerError} Will throw an error if length is less than 1. + * + * @see stringNumeric(fakerCore): For generating the pin with greater control. + * + * @example + * pin(fakerCore) // '5067' + * pin(fakerCore, { length: 6 }) // '213789' + * pin(fakerCore, 6) // '213789' + * + * @since 6.2.0 + */ +export function pin( + fakerCore: FakerCore, + options: + | number + | { + /** + * The length of the PIN to generate. + * + * @default 4 + */ + length?: number; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + const { length = 4 } = options; + + if (length < 1) { + throw new FakerError('minimum length is 1'); + } + + return numeric(fakerCore, { length, allowLeadingZeros: true }); +} diff --git a/src/modules/finance/routing-number.ts b/src/modules/finance/routing-number.ts new file mode 100644 index 00000000000..06b37a3bdf6 --- /dev/null +++ b/src/modules/finance/routing-number.ts @@ -0,0 +1,38 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random [ABA routing number](https://en.wikipedia.org/wiki/ABA_routing_transit_number). + * + * @param fakerCore The FakerCore to use. + * + * @example + * routingNumber(fakerCore) // '062197511' + * + * @since 5.0.0 + */ +export function routingNumber(fakerCore: FakerCore): string { + const federalReserveRoutingSymbol = arrayElement( + fakerCore, + fakerCore.locale.finance.federal_reserve_routing_symbol + ); + + const institutionIdentifier = numeric(fakerCore, { + length: 4, + allowLeadingZeros: true, + }); + + const routingNumber = federalReserveRoutingSymbol + institutionIdentifier; + + // Modules 10 straight summation. + let sum = 0; + + for (let i = 0; i < routingNumber.length; i += 3) { + sum += Number(routingNumber[i]) * 3; + sum += Number(routingNumber[i + 1]) * 7; + sum += Number(routingNumber[i + 2]) || 0; + } + + return `${routingNumber}${Math.ceil(sum / 10) * 10 - sum}`; +} diff --git a/src/modules/finance/transaction-description.ts b/src/modules/finance/transaction-description.ts new file mode 100644 index 00000000000..1a186e408a0 --- /dev/null +++ b/src/modules/finance/transaction-description.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random transaction description. + * + * @param fakerCore The FakerCore to use. + * + * @example + * transactionDescription(fakerCore) + * // 'payment transaction at Emard LLC using card ending with ****9187 for HNL 506.57 in account ***2584.' + * + * @since 5.1.0 + */ +export function transactionDescription(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.finance.transaction_description_pattern + ); +} diff --git a/src/modules/finance/transaction-type.ts b/src/modules/finance/transaction-type.ts new file mode 100644 index 00000000000..532bd69c2c6 --- /dev/null +++ b/src/modules/finance/transaction-type.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random transaction type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * transactionType(fakerCore) // 'payment' + * + * @since 2.0.1 + */ +export function transactionType(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.finance.transaction_type); +} diff --git a/src/modules/food/adjective.ts b/src/modules/food/adjective.ts new file mode 100644 index 00000000000..94cebe042ac --- /dev/null +++ b/src/modules/food/adjective.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random dish adjective. + * + * @param fakerCore The FakerCore to use. + * + * @example + * adjective(fakerCore) // 'crispy' + * + * @since 9.0.0 + */ +export function adjective(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.adjective); +} diff --git a/src/modules/food/description.ts b/src/modules/food/description.ts new file mode 100644 index 00000000000..9cb8296409e --- /dev/null +++ b/src/modules/food/description.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random dish description. + * + * @param fakerCore The FakerCore to use. + * + * @example + * description(fakerCore) // 'An exquisite ostrich roast, infused with the essence of longan, slow-roasted to bring out its natural flavors and served with a side of creamy red cabbage' + * + * @since 9.0.0 + */ +export function description(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.food.description_pattern + ); +} diff --git a/src/modules/food/dish.ts b/src/modules/food/dish.ts new file mode 100644 index 00000000000..926b3860841 --- /dev/null +++ b/src/modules/food/dish.ts @@ -0,0 +1,37 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Converts the given string to title case. + * + * @param text The text to convert. + */ +function toTitleCase(text: string): string { + return text + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +/** + * Generates a random dish name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * dish(fakerCore) // 'Tagine-Rubbed Venison Salad' + * + * @since 9.0.0 + */ +export function dish(fakerCore: FakerCore): string { + // A 50/50 mix of specific dishes and dish_patterns + if (boolean(fakerCore)) { + return toTitleCase( + new Faker(fakerCore).helpers.fake(fakerCore.locale.food.dish_pattern) + ); + } + + return toTitleCase(arrayElement(fakerCore, fakerCore.locale.food.dish)); +} diff --git a/src/modules/food/ethnic-category.ts b/src/modules/food/ethnic-category.ts new file mode 100644 index 00000000000..4824e6a0ff1 --- /dev/null +++ b/src/modules/food/ethnic-category.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random food's ethnic category. + * + * @param fakerCore The FakerCore to use. + * + * @example + * ethnicCategory(fakerCore) // 'Italian' + * + * @since 9.0.0 + */ +export function ethnicCategory(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.ethnic_category); +} diff --git a/src/modules/food/fruit.ts b/src/modules/food/fruit.ts new file mode 100644 index 00000000000..c5af0f653ba --- /dev/null +++ b/src/modules/food/fruit.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random fruit name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * fruit(fakerCore) // 'lemon' + * + * @since 9.0.0 + */ +export function fruit(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.fruit); +} diff --git a/src/modules/food/index.ts b/src/modules/food/index.ts index 930c738254a..f34e65c4015 100644 --- a/src/modules/food/index.ts +++ b/src/modules/food/index.ts @@ -1,16 +1,13 @@ import { ModuleBase } from '../../internal/module-base'; - -/** - * Converts the given string to title case. - * - * @param text The text to convert. - */ -function toTitleCase(text: string): string { - return text - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -} +import { adjective as foodAdjective } from './adjective'; +import { description as foodDescription } from './description'; +import { dish as foodDish } from './dish'; +import { ethnicCategory as foodEthnicCategory } from './ethnic-category'; +import { fruit as foodFruit } from './fruit'; +import { ingredient as foodIngredient } from './ingredient'; +import { meat as foodMeat } from './meat'; +import { spice as foodSpice } from './spice'; +import { vegetable as foodVegetable } from './vegetable'; /** * Module for generating food-related data. @@ -31,9 +28,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ adjective(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.food.adjective - ); + return foodAdjective(this.faker.fakerCore); } /** @@ -45,9 +40,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ description(): string { - return this.faker.helpers.fake( - this.faker.definitions.food.description_pattern - ); + return foodDescription(this.faker.fakerCore); } /** @@ -59,16 +52,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ dish(): string { - // A 50/50 mix of specific dishes and dish_patterns - if (this.faker.datatype.boolean()) { - return toTitleCase( - this.faker.helpers.fake(this.faker.definitions.food.dish_pattern) - ); - } - - return toTitleCase( - this.faker.helpers.arrayElement(this.faker.definitions.food.dish) - ); + return foodDish(this.faker.fakerCore); } /** @@ -80,9 +64,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ ethnicCategory(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.food.ethnic_category - ); + return foodEthnicCategory(this.faker.fakerCore); } /** @@ -94,7 +76,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ fruit(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.food.fruit); + return foodFruit(this.faker.fakerCore); } /** @@ -106,9 +88,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ ingredient(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.food.ingredient - ); + return foodIngredient(this.faker.fakerCore); } /** @@ -120,7 +100,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ meat(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.food.meat); + return foodMeat(this.faker.fakerCore); } /** @@ -132,7 +112,7 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ spice(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.food.spice); + return foodSpice(this.faker.fakerCore); } /** @@ -144,8 +124,6 @@ export class FoodModule extends ModuleBase { * @since 9.0.0 */ vegetable(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.food.vegetable - ); + return foodVegetable(this.faker.fakerCore); } } diff --git a/src/modules/food/ingredient.ts b/src/modules/food/ingredient.ts new file mode 100644 index 00000000000..313c94643b6 --- /dev/null +++ b/src/modules/food/ingredient.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random ingredient name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * ingredient(fakerCore) // 'butter' + * + * @since 9.0.0 + */ +export function ingredient(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.ingredient); +} diff --git a/src/modules/food/meat.ts b/src/modules/food/meat.ts new file mode 100644 index 00000000000..8e5042f1a80 --- /dev/null +++ b/src/modules/food/meat.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random meat. + * + * @param fakerCore The FakerCore to use. + * + * @example + * meat(fakerCore) // 'venison' + * + * @since 9.0.0 + */ +export function meat(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.meat); +} diff --git a/src/modules/food/spice.ts b/src/modules/food/spice.ts new file mode 100644 index 00000000000..ee98cfecc87 --- /dev/null +++ b/src/modules/food/spice.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random spice name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * spice(fakerCore) // 'chilli' + * + * @since 9.0.0 + */ +export function spice(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.spice); +} diff --git a/src/modules/food/vegetable.ts b/src/modules/food/vegetable.ts new file mode 100644 index 00000000000..dd7d28acbb7 --- /dev/null +++ b/src/modules/food/vegetable.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random vegetable name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * vegetable(fakerCore) // 'broccoli' + * + * @since 9.0.0 + */ +export function vegetable(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.food.vegetable); +} diff --git a/src/modules/git/branch.ts b/src/modules/git/branch.ts new file mode 100644 index 00000000000..339df27fb71 --- /dev/null +++ b/src/modules/git/branch.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { noun as hackerNoun } from '../hacker/noun'; +import { verb as hackerVerb } from '../hacker/verb'; + +/** + * Generates a random branch name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * branch(fakerCore) // 'feed-parse' + * + * @since 5.0.0 + */ +export function branch(fakerCore: FakerCore): string { + const noun = hackerNoun(fakerCore).replace(' ', '-'); + const verb = hackerVerb(fakerCore).replace(' ', '-'); + return `${noun}-${verb}`; +} diff --git a/src/modules/git/commit-date.ts b/src/modules/git/commit-date.ts new file mode 100644 index 00000000000..c5d0d506194 --- /dev/null +++ b/src/modules/git/commit-date.ts @@ -0,0 +1,63 @@ +import type { FakerCore } from '../../core'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { recent } from '../date/recent'; +import { int } from '../number/int'; + +/** + * Generates a date string for a git commit using the same format as `git log`. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.refDate The date to use as reference point for the commit. Defaults to `getDefaultRefDate(fakerCore)`. + * + * @example + * commitDate(fakerCore) // 'Mon Nov 7 14:40:58 2022 +0600' + * commitDate(fakerCore, { refDate: '2020-01-01' }) // 'Tue Dec 31 05:40:59 2019 -0400' + * + * @since 8.0.0 + */ +export function commitDate( + fakerCore: FakerCore, + options: { + /** + * The date to use as reference point for the commit. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): string { + const { refDate = getDefaultRefDate(fakerCore) } = options; + // Git uses a non-standard date format for commits by default per https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-log.html + // --date=default is the default format, and is based on ctime(3) output. It shows a single line with three-letter day of the week, three-letter month, day-of-month, hour-minute-seconds in "HH:MM:SS" format, followed by 4-digit year, plus timezone information, unless the local time zone is used, e.g. Thu Jan 1 00:00:00 1970 +0000. + // To avoid relying on the Intl global which may not be available in all environments, we implement a custom date format using built-in Javascript date functions. + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + const date = recent(fakerCore, { days: 1, refDate }); + const day = days[date.getUTCDay()]; + const month = months[date.getUTCMonth()]; + const dayOfMonth = date.getUTCDate(); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + const year = date.getUTCFullYear(); + const timezone = int(fakerCore, { min: -11, max: 12 }); + const timezoneHours = Math.abs(timezone).toString().padStart(2, '0'); + const timezoneMinutes = '00'; + const timezoneSign = timezone >= 0 ? '+' : '-'; + return `${day} ${month} ${dayOfMonth} ${hours}:${minutes}:${seconds} ${year} ${timezoneSign}${timezoneHours}${timezoneMinutes}`; +} diff --git a/src/modules/git/commit-entry.ts b/src/modules/git/commit-entry.ts new file mode 100644 index 00000000000..15ffad4ba11 --- /dev/null +++ b/src/modules/git/commit-entry.ts @@ -0,0 +1,101 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { commitDate } from '../git/commit-date'; +import { commitMessage } from '../git/commit-message'; +import { commitSha } from '../git/commit-sha'; +import { arrayElement } from '../helpers/array-element'; +import { email as internetEmail } from '../internet/email'; +import { username as internetUsername } from '../internet/username'; +import { firstName as personFirstName } from '../person/first-name'; +import { fullName as personFullName } from '../person/full-name'; +import { lastName as personLastName } from '../person/last-name'; + +const nbsp = '\u00A0'; + +/** + * Generates a random commit entry as printed by `git log`. + * + * @param fakerCore The FakerCore to use. + * @param options Options for the commit entry. + * @param options.merge Whether to generate a merge message line. Defaults to 20% `true` and 80% `false`. + * @param options.eol Choose the end of line character to use. Defaults to `'CRLF'`. + * 'LF' = '\n', + * 'CRLF' = '\r\n' + * @param options.refDate The date to use as reference point for the commit. Defaults to `new Date()`. + * + * @example + * commitEntry(fakerCore) + * // commit fe8c38a965d13d9794eb36918cb24cebe49a45c2 + * // Author: Marion Becker + * // Date: Mon Nov 7 05:38:37 2022 -0600 + * // + * // generate open-source system + * + * @since 5.0.0 + */ +export function commitEntry( + fakerCore: FakerCore, + options: { + /** + * Set to `true` to generate a merge message line. + * + * @default datatypeBoolean(fakerCore, { probability: 0.2 }) + */ + merge?: boolean; + /** + * Choose the end of line character to use. + * + * - 'LF' = '\n', + * - 'CRLF' = '\r\n' + * + * @default 'CRLF' + */ + eol?: 'LF' | 'CRLF'; + /** + * The date to use as reference point for the commit. + * + * @default new Date() + */ + refDate?: string | Date | number; + } = {} +): string { + const { + merge = boolean(fakerCore, { probability: 0.2 }), + eol = 'CRLF', + refDate, + } = options; + + const lines = [`commit ${commitSha(fakerCore)}`]; + + if (merge) { + lines.push( + `Merge: ${commitSha(fakerCore, { length: 7 })} ${commitSha(fakerCore, { + length: 7, + })}` + ); + } + + const firstName = personFirstName(fakerCore); + const lastName = personLastName(fakerCore); + const fullName = personFullName(fakerCore, { firstName, lastName }); + const username = internetUsername(fakerCore, { firstName, lastName }); + let user = arrayElement(fakerCore, [fullName, username]); + const email = internetEmail(fakerCore, { firstName, lastName }); + + // Normalize user according to https://github.com/libgit2/libgit2/issues/5342 + user = user.replaceAll(/^[.,:;"\\']|[<>\n]|[.,:;"\\']$/g, ''); + + lines.push( + `Author: ${user} <${email}>`, + `Date: ${commitDate(fakerCore, { refDate })}`, + '', + `${nbsp.repeat(4)}${commitMessage(fakerCore)}`, + // to end with a eol char + '' + ); + + const eolChar = eol === 'CRLF' ? '\r\n' : '\n'; + const entry = lines.join(eolChar); + + return entry; +} diff --git a/src/modules/git/commit-message.ts b/src/modules/git/commit-message.ts new file mode 100644 index 00000000000..ab831f37c5e --- /dev/null +++ b/src/modules/git/commit-message.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { adjective } from '../hacker/adjective'; +import { noun } from '../hacker/noun'; +import { verb } from '../hacker/verb'; + +/** + * Generates a random commit message. + * + * @param fakerCore The FakerCore to use. + * + * @example + * commitMessage(fakerCore) // 'reboot cross-platform driver' + * + * @since 5.0.0 + */ +export function commitMessage(fakerCore: FakerCore): string { + return `${verb(fakerCore)} ${adjective(fakerCore)} ${noun(fakerCore)}`; +} diff --git a/src/modules/git/commit-sha.ts b/src/modules/git/commit-sha.ts new file mode 100644 index 00000000000..9900a8cd89d --- /dev/null +++ b/src/modules/git/commit-sha.ts @@ -0,0 +1,43 @@ +import type { FakerCore } from '../../core'; +import { hexadecimal } from '../string/hexadecimal'; + +/** + * Generates a random commit sha. + * + * By default, the length of the commit sha is 40 characters. + * + * For a shorter commit sha, use the `length` option. + * + * Usual short commit sha length is: + * - 7 for GitHub + * - 8 for GitLab + * + * @param fakerCore The FakerCore to use. + * @param options Options for the commit sha. + * @param options.length The length of the commit sha. Defaults to `40`. + * + * @example + * commitSha(fakerCore) // '2c6e3880fd94ddb7ef72d34e683cdc0c47bec6e6' + * commitSha(fakerCore, { length: 7 }) // 'dbee57b' + * commitSha(fakerCore, { length: 8 }) // '0e52376a' + * + * @since 5.0.0 + */ +export function commitSha( + fakerCore: FakerCore, + options: { + /** + * The length of the commit sha. + * + * @default 40 + */ + length?: number; + } = {} +): string { + const { length = 40 } = options; + return hexadecimal(fakerCore, { + length, + casing: 'lower', + prefix: '', + }); +} diff --git a/src/modules/git/index.ts b/src/modules/git/index.ts index 22c848b8828..18dfa530730 100644 --- a/src/modules/git/index.ts +++ b/src/modules/git/index.ts @@ -1,6 +1,9 @@ import { ModuleBase } from '../../internal/module-base'; - -const nbsp = '\u00A0'; +import { branch as gitBranch } from './branch'; +import { commitDate as gitCommitDate } from './commit-date'; +import { commitEntry as gitCommitEntry } from './commit-entry'; +import { commitMessage as gitCommitMessage } from './commit-message'; +import { commitSha as gitCommitSha } from './commit-sha'; /** * Module to generate git related entries. @@ -19,9 +22,7 @@ export class GitModule extends ModuleBase { * @since 5.0.0 */ branch(): string { - const noun = this.faker.hacker.noun().replace(' ', '-'); - const verb = this.faker.hacker.verb().replace(' ', '-'); - return `${noun}-${verb}`; + return gitBranch(this.faker.fakerCore); } /** @@ -69,45 +70,7 @@ export class GitModule extends ModuleBase { refDate?: string | Date | number; } = {} ): string { - const { - merge = this.faker.datatype.boolean({ probability: 0.2 }), - eol = 'CRLF', - refDate, - } = options; - - const lines = [`commit ${this.faker.git.commitSha()}`]; - - if (merge) { - lines.push( - `Merge: ${this.commitSha({ length: 7 })} ${this.commitSha({ - length: 7, - })}` - ); - } - - const firstName = this.faker.person.firstName(); - const lastName = this.faker.person.lastName(); - const fullName = this.faker.person.fullName({ firstName, lastName }); - const username = this.faker.internet.username({ firstName, lastName }); - let user = this.faker.helpers.arrayElement([fullName, username]); - const email = this.faker.internet.email({ firstName, lastName }); - - // Normalize user according to https://github.com/libgit2/libgit2/issues/5342 - user = user.replaceAll(/^[.,:;"\\']|[<>\n]|[.,:;"\\']$/g, ''); - - lines.push( - `Author: ${user} <${email}>`, - `Date: ${this.commitDate({ refDate })}`, - '', - `${nbsp.repeat(4)}${this.commitMessage()}`, - // to end with a eol char - '' - ); - - const eolChar = eol === 'CRLF' ? '\r\n' : '\n'; - const entry = lines.join(eolChar); - - return entry; + return gitCommitEntry(this.faker.fakerCore, options); } /** @@ -119,14 +82,14 @@ export class GitModule extends ModuleBase { * @since 5.0.0 */ commitMessage(): string { - return `${this.faker.hacker.verb()} ${this.faker.hacker.adjective()} ${this.faker.hacker.noun()}`; + return gitCommitMessage(this.faker.fakerCore); } /** * Generates a date string for a git commit using the same format as `git log`. * * @param options The optional options object. - * @param options.refDate The date to use as reference point for the commit. Defaults to `faker.defaultRefDate()`. + * @param options.refDate The date to use as reference point for the commit. Defaults to `faker.getDefaultRefDate()`. * * @example * faker.git.commitDate() // 'Mon Nov 7 14:40:58 2022 +0600' @@ -144,39 +107,7 @@ export class GitModule extends ModuleBase { refDate?: string | Date | number; } = {} ): string { - const { refDate = this.faker.defaultRefDate() } = options; - // Git uses a non-standard date format for commits by default per https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-log.html - // --date=default is the default format, and is based on ctime(3) output. It shows a single line with three-letter day of the week, three-letter month, day-of-month, hour-minute-seconds in "HH:MM:SS" format, followed by 4-digit year, plus timezone information, unless the local time zone is used, e.g. Thu Jan 1 00:00:00 1970 +0000. - // To avoid relying on the Intl global which may not be available in all environments, we implement a custom date format using built-in Javascript date functions. - const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - - const date = this.faker.date.recent({ days: 1, refDate }); - const day = days[date.getUTCDay()]; - const month = months[date.getUTCMonth()]; - const dayOfMonth = date.getUTCDate(); - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); - const year = date.getUTCFullYear(); - const timezone = this.faker.number.int({ min: -11, max: 12 }); - const timezoneHours = Math.abs(timezone).toString().padStart(2, '0'); - const timezoneMinutes = '00'; - const timezoneSign = timezone >= 0 ? '+' : '-'; - return `${day} ${month} ${dayOfMonth} ${hours}:${minutes}:${seconds} ${year} ${timezoneSign}${timezoneHours}${timezoneMinutes}`; + return gitCommitDate(this.faker.fakerCore, options); } /** @@ -210,11 +141,6 @@ export class GitModule extends ModuleBase { length?: number; } = {} ): string { - const { length = 40 } = options; - return this.faker.string.hexadecimal({ - length, - casing: 'lower', - prefix: '', - }); + return gitCommitSha(this.faker.fakerCore, options); } } diff --git a/src/modules/hacker/abbreviation.ts b/src/modules/hacker/abbreviation.ts new file mode 100644 index 00000000000..6b4e6ad08c2 --- /dev/null +++ b/src/modules/hacker/abbreviation.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random hacker/IT abbreviation. + * + * @param fakerCore The FakerCore to use. + * + * @example + * abbreviation(fakerCore) // 'THX' + * + * @since 2.0.1 + */ +export function abbreviation(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.hacker.abbreviation); +} diff --git a/src/modules/hacker/adjective.ts b/src/modules/hacker/adjective.ts new file mode 100644 index 00000000000..843b447c93b --- /dev/null +++ b/src/modules/hacker/adjective.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random hacker/IT adjective. + * + * @param fakerCore The FakerCore to use. + * + * @example + * adjective(fakerCore) // 'cross-platform' + * + * @since 2.0.1 + */ +export function adjective(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.hacker.adjective); +} diff --git a/src/modules/hacker/index.ts b/src/modules/hacker/index.ts index 500f2f907c8..7ceb5c41761 100644 --- a/src/modules/hacker/index.ts +++ b/src/modules/hacker/index.ts @@ -1,4 +1,10 @@ import { ModuleBase } from '../../internal/module-base'; +import { abbreviation as hackerAbbreviation } from './abbreviation'; +import { adjective as hackerAdjective } from './adjective'; +import { ingverb as hackerIngverb } from './ingverb'; +import { noun as hackerNoun } from './noun'; +import { phrase as hackerPhrase } from './phrase'; +import { verb as hackerVerb } from './verb'; /** * Module to generate hacker/IT words and phrases. @@ -25,9 +31,7 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ abbreviation(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.hacker.abbreviation - ); + return hackerAbbreviation(this.faker.fakerCore); } /** @@ -39,9 +43,7 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ adjective(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.hacker.adjective - ); + return hackerAdjective(this.faker.fakerCore); } /** @@ -53,7 +55,7 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ noun(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.hacker.noun); + return hackerNoun(this.faker.fakerCore); } /** @@ -65,7 +67,7 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ verb(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.hacker.verb); + return hackerVerb(this.faker.fakerCore); } /** @@ -77,9 +79,7 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ ingverb(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.hacker.ingverb - ); + return hackerIngverb(this.faker.fakerCore); } /** @@ -92,6 +92,6 @@ export class HackerModule extends ModuleBase { * @since 2.0.1 */ phrase(): string { - return this.faker.helpers.fake(this.faker.definitions.hacker.phrase); + return hackerPhrase(this.faker.fakerCore); } } diff --git a/src/modules/hacker/ingverb.ts b/src/modules/hacker/ingverb.ts new file mode 100644 index 00000000000..2dc0ec5acf3 --- /dev/null +++ b/src/modules/hacker/ingverb.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random hacker/IT verb for continuous actions (en: ing suffix; e.g. hacking). + * + * @param fakerCore The FakerCore to use. + * + * @example + * ingverb(fakerCore) // 'navigating' + * + * @since 2.0.1 + */ +export function ingverb(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.hacker.ingverb); +} diff --git a/src/modules/hacker/noun.ts b/src/modules/hacker/noun.ts new file mode 100644 index 00000000000..5a57983f1a7 --- /dev/null +++ b/src/modules/hacker/noun.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random hacker/IT noun. + * + * @param fakerCore The FakerCore to use. + * + * @example + * noun(fakerCore) // 'system' + * + * @since 2.0.1 + */ +export function noun(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.hacker.noun); +} diff --git a/src/modules/hacker/phrase.ts b/src/modules/hacker/phrase.ts new file mode 100644 index 00000000000..8a16cdea30a --- /dev/null +++ b/src/modules/hacker/phrase.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random hacker/IT phrase. + * + * @param fakerCore The FakerCore to use. + * + * @example + * phrase(fakerCore) + * // 'If we override the card, we can get to the HDD feed through the back-end HDD sensor!' + * + * @since 2.0.1 + */ +export function phrase(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake(fakerCore.locale.hacker.phrase); +} diff --git a/src/modules/hacker/verb.ts b/src/modules/hacker/verb.ts new file mode 100644 index 00000000000..619d1a7d55d --- /dev/null +++ b/src/modules/hacker/verb.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random hacker/IT verb. + * + * @param fakerCore The FakerCore to use. + * + * @example + * verb(fakerCore) // 'copy' + * + * @since 2.0.1 + */ +export function verb(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.hacker.verb); +} 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/image/avatar-git-hub.ts b/src/modules/image/avatar-git-hub.ts new file mode 100644 index 00000000000..968f10e2aac --- /dev/null +++ b/src/modules/image/avatar-git-hub.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Generates a random avatar from GitHub. + * + * @remark This method generates a random string representing an URL from GitHub by using a random user ID. Faker is not responsible for the content of the image or the service providing it. + * + * @param fakerCore The FakerCore to use. + * + * @example + * avatarGitHub(fakerCore) + * // 'https://avatars.githubusercontent.com/u/97165289' + * + * @since 8.0.0 + */ +export function avatarGitHub(fakerCore: FakerCore): string { + return `https://avatars.githubusercontent.com/u/${int(fakerCore, 100000000)}`; +} diff --git a/src/modules/image/avatar.ts b/src/modules/image/avatar.ts new file mode 100644 index 00000000000..70cf7528504 --- /dev/null +++ b/src/modules/image/avatar.ts @@ -0,0 +1,23 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { avatarGitHub } from './avatar-git-hub'; +import { personPortrait } from './person-portrait'; + +/** + * Generates a random avatar image url. + * + * @remark This method sometimes generates a random string representing an URL from GitHub by using a random user ID. Faker is not responsible for the content of the image or the service providing it. + * + * @param fakerCore The FakerCore to use. + * + * @example + * avatar(fakerCore) + * // 'https://avatars.githubusercontent.com/u/97165289' + * + * @since 2.0.1 + */ +export function avatar(fakerCore: FakerCore): string { + // Add new avatar providers here, when adding a new one. + const avatarMethod = arrayElement(fakerCore, [personPortrait, avatarGitHub]); + return avatarMethod(fakerCore); +} diff --git a/src/modules/image/data-uri.ts b/src/modules/image/data-uri.ts new file mode 100644 index 00000000000..c110ef45724 --- /dev/null +++ b/src/modules/image/data-uri.ts @@ -0,0 +1,69 @@ +import type { FakerCore } from '../../core'; +import { toBase64 } from '../../internal/base64'; +import { rgb } from '../color/rgb'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; + +/** + * Generates a random data uri containing an URL-encoded SVG image or a Base64-encoded SVG image. + * + * @param fakerCore The FakerCore to use. + * @param options Options for generating a data uri. + * @param options.width The width of the image. Defaults to a random integer between `1` and `3999`. + * @param options.height The height of the image. Defaults to a random integer between `1` and `3999`. + * @param options.color The color of the image. Must be a color supported by svg. Defaults to a random color. + * @param options.type The type of the image. Defaults to a random type. + * + * @example + * dataUri(fakerCore) // 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http...' + * dataUri(fakerCore, { type: 'svg-base64' }) // 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3...' + * + * @since 4.0.0 + */ +export function dataUri( + fakerCore: FakerCore, + options: { + /** + * The width of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + width?: number; + /** + * The height of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + height?: number; + /** + * The color of the image. Must be a color supported by svg. + * + * @default colorRgb(fakerCore) + */ + color?: string; + /** + * The type of the image to return. Consisting of + * the file extension and the used encoding. + * + * @default helpersArrayElement(fakerCore, ['svg-uri', 'svg-base64']) + */ + type?: 'svg-uri' | 'svg-base64'; + } = {} +): string { + const { + width = int(fakerCore, { min: 1, max: 3999 }), + height = int(fakerCore, { min: 1, max: 3999 }), + color = rgb(fakerCore), + type = arrayElement(fakerCore, ['svg-uri', 'svg-base64']), + } = options; + + const svgString = `${width}x${height}`; + + return type === 'svg-uri' + ? `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgString)}` + : `data:image/svg+xml;base64,${toBase64(svgString)}`; +} diff --git a/src/modules/image/index.ts b/src/modules/image/index.ts index 327854205df..19771bfa296 100644 --- a/src/modules/image/index.ts +++ b/src/modules/image/index.ts @@ -1,7 +1,12 @@ -import { toBase64 } from '../../internal/base64'; -import { deprecated } from '../../internal/deprecated'; import { ModuleBase } from '../../internal/module-base'; import type { SexType } from '../person'; +import { avatar as imageAvatar } from './avatar'; +import { avatarGitHub as imageAvatarGitHub } from './avatar-git-hub'; +import { dataUri as imageDataUri } from './data-uri'; +import { personPortrait as imagePersonPortrait } from './person-portrait'; +import { url as imageUrl } from './url'; +import { urlLoremFlickr as imageUrlLoremFlickr } from './url-lorem-flickr'; +import { urlPicsumPhotos as imageUrlPicsumPhotos } from './url-picsum-photos'; /** * Module to generate images. @@ -32,12 +37,7 @@ export class ImageModule extends ModuleBase { * @since 2.0.1 */ avatar(): string { - // Add new avatar providers here, when adding a new one. - const avatarMethod = this.faker.helpers.arrayElement([ - this.personPortrait, - this.avatarGitHub, - ]); - return avatarMethod(); + return imageAvatar(this.faker.fakerCore); } /** @@ -52,9 +52,7 @@ export class ImageModule extends ModuleBase { * @since 8.0.0 */ avatarGitHub(): string { - return `https://avatars.githubusercontent.com/u/${this.faker.number.int( - 100000000 - )}`; + return imageAvatarGitHub(this.faker.fakerCore); } /** @@ -90,16 +88,7 @@ export class ImageModule extends ModuleBase { size?: 512 | 256 | 128 | 64 | 32; } = {} ): string { - const { size = 512 } = options; - let { sex = this.faker.person.sexType() } = options; - - if (sex === 'generic') { - sex = this.faker.person.sexType(); - } - - const baseURL = - 'https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait'; - return `${baseURL}/${sex}/${size}/${this.faker.number.int({ min: 0, max: 99 })}.jpg`; + return imagePersonPortrait(this.faker.fakerCore, options); } /** @@ -132,18 +121,7 @@ export class ImageModule extends ModuleBase { height?: number; } = {} ): string { - const { - width = this.faker.number.int({ min: 1, max: 3999 }), - height = this.faker.number.int({ min: 1, max: 3999 }), - } = options; - - const urlMethod = this.faker.helpers.arrayElement([ - ({ width, height }: { width?: number; height?: number }) => - this.urlPicsumPhotos({ width, height, grayscale: false, blur: 0 }), - // Other providers may be added back here in future versions. - ]); - - return urlMethod({ width, height }); + return imageUrl(this.faker.fakerCore, options); } /** @@ -186,22 +164,8 @@ export class ImageModule extends ModuleBase { category?: string; } = {} ): string { - deprecated({ - deprecated: 'faker.image.urlLoremFlickr()', - proposed: 'faker.image.url()', - since: '10.1.0', - until: '11.0.0', - }); - - const { - width = this.faker.number.int({ min: 1, max: 3999 }), - height = this.faker.number.int({ min: 1, max: 3999 }), - category, - } = options; - - return `https://loremflickr.com/${width}/${height}${ - category == null ? '' : `/${category}` - }?lock=${this.faker.number.int()}`; + // eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal call + return imageUrlLoremFlickr(this.faker.fakerCore, options); } /** @@ -253,36 +217,7 @@ export class ImageModule extends ModuleBase { blur?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; } = {} ): string { - const { - width = this.faker.number.int({ min: 1, max: 3999 }), - height = this.faker.number.int({ min: 1, max: 3999 }), - grayscale = this.faker.datatype.boolean(), - blur = this.faker.number.int({ max: 10 }), - } = options; - - let url = `https://picsum.photos/seed/${this.faker.string.alphanumeric({ - length: { min: 5, max: 10 }, - })}/${width}/${height}`; - - const hasValidBlur = typeof blur === 'number' && blur >= 1 && blur <= 10; - - if (grayscale || hasValidBlur) { - url += '?'; - - if (grayscale) { - url += `grayscale`; - } - - if (grayscale && hasValidBlur) { - url += '&'; - } - - if (hasValidBlur) { - url += `blur=${blur}`; - } - } - - return url; + return imageUrlPicsumPhotos(this.faker.fakerCore, options); } /** @@ -329,21 +264,6 @@ export class ImageModule extends ModuleBase { type?: 'svg-uri' | 'svg-base64'; } = {} ): string { - const { - width = this.faker.number.int({ min: 1, max: 3999 }), - height = this.faker.number.int({ min: 1, max: 3999 }), - color = this.faker.color.rgb(), - type = this.faker.helpers.arrayElement(['svg-uri', 'svg-base64']), - } = options; - - const svgString = `${width}x${height}`; - - return type === 'svg-uri' - ? `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgString)}` - : `data:image/svg+xml;base64,${toBase64(svgString)}`; + return imageDataUri(this.faker.fakerCore, options); } } diff --git a/src/modules/image/person-portrait.ts b/src/modules/image/person-portrait.ts new file mode 100644 index 00000000000..b2b7d05f913 --- /dev/null +++ b/src/modules/image/person-portrait.ts @@ -0,0 +1,50 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; +import type { SexType } from '../person'; +import { sexType } from '../person/sex-type'; + +/** + * Generates a random square portrait (avatar) of a person. + * These are static images of fictional people created by an AI, Stable Diffusion 3. + * The image URLs are served via the JSDelivr CDN and subject to their [terms of use](https://www.jsdelivr.com/terms). + * + * @param fakerCore The FakerCore to use. + * @param options Options for generating an AI avatar. + * @param options.sex The sex of the person for the avatar. Can be `'female'` or `'male'`. If not provided or `'generic'`, defaults to a random selection. + * @param options.size The size of the image. Can be `512`, `256`, `128`, `64` or `32`. If not provided, defaults to `512`. + * + * @example + * personPortrait(fakerCore) // 'https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait/female/512/57.jpg' + * personPortrait(fakerCore, { sex: 'male', size: '128' }) // 'https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait/male/128/27.jpg' + * + * @since 9.5.0 + */ +export function personPortrait( + fakerCore: FakerCore, + options: { + /** + * The sex of the person for the avatar. + * Can be `'female'` or `'male'`. `'generic'` uses a random selection. + * + * @default personSexType(fakerCore) + */ + sex?: SexType; + /** + * The size of the image. + * Can be `512`, `256`, `128`, `64` or `32`. + * + * @default 512 + */ + size?: 512 | 256 | 128 | 64 | 32; + } = {} +): string { + const { size = 512 } = options; + let { sex = sexType(fakerCore) } = options; + + if (sex === 'generic') { + sex = sexType(fakerCore); + } + + const baseURL = 'https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait'; + return `${baseURL}/${sex}/${size}/${int(fakerCore, { min: 0, max: 99 })}.jpg`; +} diff --git a/src/modules/image/url-lorem-flickr.ts b/src/modules/image/url-lorem-flickr.ts new file mode 100644 index 00000000000..95ffee615c7 --- /dev/null +++ b/src/modules/image/url-lorem-flickr.ts @@ -0,0 +1,63 @@ +import type { FakerCore } from '../../core'; +import { deprecated } from '../../internal/deprecated'; +import { int } from '../number/int'; + +/** + * Generates a random image url provided via https://loremflickr.com. + * + * @remark This method generates a random string representing an URL from loremflickr. Faker is not responsible for the content of the image or the service providing it. + * + * @param fakerCore The FakerCore to use. + * @param options Options for generating a URL for an image. + * @param options.width The width of the image. Defaults to a random integer between `1` and `3999`. + * @param options.height The height of the image. Defaults to a random integer between `1` and `3999`. + * @param options.category Category to use for the image. + * + * @example + * urlLoremFlickr(fakerCore) // 'https://loremflickr.com/640/480?lock=1234' + * urlLoremFlickr(fakerCore, { width: 128 }) // 'https://loremflickr.com/128/480?lock=1234' + * urlLoremFlickr(fakerCore, { height: 128 }) // 'https://loremflickr.com/640/128?lock=1234' + * urlLoremFlickr(fakerCore, { category: 'nature' }) // 'https://loremflickr.com/640/480/nature?lock=1234' + * + * @since 8.0.0 + * + * @deprecated LoremFlickr is no longer available, and image links will be broken. Use `url(fakerCore)` instead. + */ +export function urlLoremFlickr( + fakerCore: FakerCore, + options: { + /** + * The width of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + width?: number; + /** + * The height of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + height?: number; + /** + * Category to use for the image. + */ + category?: string; + } = {} +): string { + deprecated({ + deprecated: 'urlLoremFlickr(fakerCore)', + proposed: 'url(fakerCore)', + since: '10.1.0', + until: '11.0.0', + }); + + const { + width = int(fakerCore, { min: 1, max: 3999 }), + height = int(fakerCore, { min: 1, max: 3999 }), + category, + } = options; + + return `https://loremflickr.com/${width}/${height}${ + category == null ? '' : `/${category}` + }?lock=${int(fakerCore)}`; +} diff --git a/src/modules/image/url-picsum-photos.ts b/src/modules/image/url-picsum-photos.ts new file mode 100644 index 00000000000..318502a3569 --- /dev/null +++ b/src/modules/image/url-picsum-photos.ts @@ -0,0 +1,87 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { int } from '../number/int'; +import { alphanumeric } from '../string/alphanumeric'; + +/** + * Generates a random image url provided via https://picsum.photos. + * + * @remark This method generates a random string representing an URL from picsum.photos. Faker is not responsible for the content of the image or the service providing it. + * + * @param fakerCore The FakerCore to use. + * @param options Options for generating a URL for an image. + * @param options.width The width of the image. Defaults to a random integer between `1` and `3999`. + * @param options.height The height of the image. Defaults to a random integer between `1` and `3999`. + * @param options.grayscale Whether the image should be grayscale. Defaults to a random boolean value. + * @param options.blur Whether the image should be blurred. `0` disables the blur. Defaults to a random integer between `0` and `10`. + * + * @example + * urlPicsumPhotos(fakerCore) // 'https://picsum.photos/seed/NWbJM2B/640/480' + * urlPicsumPhotos(fakerCore, { width: 128 }) // 'https://picsum.photos/seed/NWbJM2B/128/480' + * urlPicsumPhotos(fakerCore, { height: 128 }) // 'https://picsum.photos/seed/NWbJM2B/640/128' + * urlPicsumPhotos(fakerCore, { grayscale: true }) // 'https://picsum.photos/seed/NWbJM2B/640/480?grayscale' + * urlPicsumPhotos(fakerCore, { blur: 4 }) // 'https://picsum.photos/seed/NWbJM2B/640/480?blur=4' + * urlPicsumPhotos(fakerCore, { blur: 4, grayscale: true }) // 'https://picsum.photos/seed/NWbJM2B/640/480?grayscale&blur=4' + * + * @since 8.0.0 + */ +export function urlPicsumPhotos( + fakerCore: FakerCore, + options: { + /** + * The width of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + width?: number; + /** + * The height of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + height?: number; + /** + * Whether the image should be grayscale. + * + * @default datatypeBoolean(fakerCore) + */ + grayscale?: boolean; + /** + * Whether the image should be blurred. `0` disables the blur. + * + * @default numberInt(fakerCore, { max: 10 }) + */ + blur?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; + } = {} +): string { + const { + width = int(fakerCore, { min: 1, max: 3999 }), + height = int(fakerCore, { min: 1, max: 3999 }), + grayscale = boolean(fakerCore), + blur = int(fakerCore, { max: 10 }), + } = options; + + let url = `https://picsum.photos/seed/${alphanumeric(fakerCore, { + length: { min: 5, max: 10 }, + })}/${width}/${height}`; + + const hasValidBlur = typeof blur === 'number' && blur >= 1 && blur <= 10; + + if (grayscale || hasValidBlur) { + url += '?'; + + if (grayscale) { + url += `grayscale`; + } + + if (grayscale && hasValidBlur) { + url += '&'; + } + + if (hasValidBlur) { + url += `blur=${blur}`; + } + } + + return url; +} diff --git a/src/modules/image/url.ts b/src/modules/image/url.ts new file mode 100644 index 00000000000..4e31e854fbe --- /dev/null +++ b/src/modules/image/url.ts @@ -0,0 +1,50 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { urlPicsumPhotos } from '../image/url-picsum-photos'; +import { int } from '../number/int'; + +/** + * Generates a random image url. + * + * @remark This method generates a random string representing an URL from an external provider. Faker is not responsible for the content of the image or the service providing it. + * + * @param fakerCore The FakerCore to use. + * @param options Options for generating a URL for an image. + * @param options.width The width of the image. Defaults to a random integer between `1` and `3999`. + * @param options.height The height of the image. Defaults to a random integer between `1` and `3999`. + * + * @example + * url(fakerCore) // 'https://picsum.photos/seed/NWbJM2B/640/480' + * + * @since 8.0.0 + */ +export function url( + fakerCore: FakerCore, + options: { + /** + * The width of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + width?: number; + /** + * The height of the image. + * + * @default numberInt(fakerCore, { min: 1, max: 3999 }) + */ + height?: number; + } = {} +): string { + const { + width = int(fakerCore, { min: 1, max: 3999 }), + height = int(fakerCore, { min: 1, max: 3999 }), + } = options; + + const urlMethod = arrayElement(fakerCore, [ + ({ width, height }: { width?: number; height?: number }) => + urlPicsumPhotos(fakerCore, { width, height, grayscale: false, blur: 0 }), + // Other providers may be added back here in future versions. + ]); + + return urlMethod({ width, height }); +} diff --git a/src/modules/internet/char-mappings.ts b/src/modules/internet/_char-mappings.ts similarity index 100% rename from src/modules/internet/char-mappings.ts rename to src/modules/internet/_char-mappings.ts diff --git a/src/modules/internet/display-name.ts b/src/modules/internet/display-name.ts new file mode 100644 index 00000000000..59964b074e8 --- /dev/null +++ b/src/modules/internet/display-name.ts @@ -0,0 +1,64 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; +import { firstName as personFirstName } from '../person/first-name'; +import { lastName as personLastName } from '../person/last-name'; + +/** + * Generates a display name using the given person's name as base. + * The resulting display name may use one or both of the provided names. + * If the input names include Unicode characters, the resulting display name will contain Unicode characters. + * It will not contain spaces. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.firstName The optional first name to use. If not specified, a random one will be chosen. + * @param options.lastName The optional last name to use. If not specified, a random one will be chosen. + * + * @see username(fakerCore): For generating a plain ASCII username. + * + * @example + * displayName(fakerCore) // 'Nettie_Zboncak40' + * displayName(fakerCore, { firstName: 'Jeanne', lastName: 'Doe' }) // 'Jeanne98' - note surname not used. + * displayName(fakerCore, { firstName: 'John', lastName: 'Doe' }) // 'John.Doe' + * displayName(fakerCore, { firstName: 'Hélene', lastName: 'Müller' }) // 'Hélene_Müller11' + * displayName(fakerCore, { firstName: 'Фёдор', lastName: 'Достоевский' }) // 'Фёдор.Достоевский50' + * displayName(fakerCore, { firstName: '大羽', lastName: '陳' }) // '大羽.陳' + * + * @since 8.0.0 + */ +export function displayName( + fakerCore: FakerCore, + options: { + /** + * The optional first name to use. + * + * @default personFirstName(fakerCore) + */ + firstName?: string; + /** + * The optional last name to use. + * + * @default personLastName(fakerCore) + */ + lastName?: string; + } = {} +): string { + const { + firstName = personFirstName(fakerCore), + lastName = personLastName(fakerCore), + } = options; + + const separator = arrayElement(fakerCore, ['.', '_']); + const disambiguator = int(fakerCore, 99); + const strategies: Array<() => string> = [ + () => `${firstName}${disambiguator}`, + () => `${firstName}${separator}${lastName}`, + () => `${firstName}${separator}${lastName}${disambiguator}`, + ]; + + let result = arrayElement(fakerCore, strategies)(); + result = result.replaceAll("'", ''); + result = result.replaceAll(' ', ''); + return result; +} diff --git a/src/modules/internet/domain-name.ts b/src/modules/internet/domain-name.ts new file mode 100644 index 00000000000..59466c5141e --- /dev/null +++ b/src/modules/internet/domain-name.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { domainSuffix } from '../internet/domain-suffix'; +import { domainWord } from '../internet/domain-word'; + +/** + * Generates a random domain name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * domainName(fakerCore) // 'slow-timer.info' + * + * @since 2.0.1 + */ +export function domainName(fakerCore: FakerCore): string { + return `${domainWord(fakerCore)}.${domainSuffix(fakerCore)}`; +} diff --git a/src/modules/internet/domain-suffix.ts b/src/modules/internet/domain-suffix.ts new file mode 100644 index 00000000000..ce1ccb6aba2 --- /dev/null +++ b/src/modules/internet/domain-suffix.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random domain suffix. + * + * @param fakerCore The FakerCore to use. + * + * @example + * domainSuffix(fakerCore) // 'com' + * domainSuffix(fakerCore) // 'name' + * + * @since 2.0.1 + */ +export function domainSuffix(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.internet.domain_suffix); +} diff --git a/src/modules/internet/domain-word.ts b/src/modules/internet/domain-word.ts new file mode 100644 index 00000000000..a395c513116 --- /dev/null +++ b/src/modules/internet/domain-word.ts @@ -0,0 +1,58 @@ +import type { FakerCore } from '../../core'; +import { slugify } from '../helpers/slugify'; +import { word as loremWord } from '../lorem/word'; +import { alpha } from '../string/alpha'; +import { adjective } from '../word/adjective'; +import { noun } from '../word/noun'; + +/** + * Checks whether the given string is a valid slug for `domainWord`s. + * + * @param slug The slug to check. + */ +function isValidDomainWordSlug(slug: string): boolean { + return /^[a-z][a-z-]*[a-z]$/i.exec(slug) !== null; +} + +/** + * Tries various ways to produce a valid domain word slug, falling back to a random string if needed. + * + * @param fakerCore The FakerCore to use. + * @param word The initial word to slugify. + */ +function makeValidDomainWordSlug(fakerCore: FakerCore, word: string): string { + const slug1 = slugify(fakerCore, word); + if (isValidDomainWordSlug(slug1)) { + return slug1; + } + + const slug2 = slugify(fakerCore, loremWord(fakerCore)); + if (isValidDomainWordSlug(slug2)) { + return slug2; + } + + return alpha(fakerCore, { + casing: 'lower', + length: { min: 4, max: 8 }, + }); +} + +/** + * Generates a random domain word. + * + * @param fakerCore The FakerCore to use. + * + * @example + * domainWord(fakerCore) // 'close-reality' + * domainWord(fakerCore) // 'weird-cytoplasm' + * + * @since 2.0.1 + */ +export function domainWord(fakerCore: FakerCore): string { + // Generate an ASCII "word" in the form `noun-adjective` + // For locales with non-ASCII characters, we fall back to lorem words, or a random string + + const word1 = makeValidDomainWordSlug(fakerCore, adjective(fakerCore)); + const word2 = makeValidDomainWordSlug(fakerCore, noun(fakerCore)); + return `${word1}-${word2}`.toLowerCase(); +} diff --git a/src/modules/internet/email.ts b/src/modules/internet/email.ts new file mode 100644 index 00000000000..8c5f27c8e5b --- /dev/null +++ b/src/modules/internet/email.ts @@ -0,0 +1,85 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { username } from '../internet/username'; + +/** + * Generates an email address using the given person's name as base. + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.firstName The optional first name to use. If not specified, a random one will be chosen. + * @param options.lastName The optional last name to use. If not specified, a random one will be chosen. + * @param options.provider The mail provider domain to use. If not specified, a random free mail provider will be chosen. + * @param options.allowSpecialCharacters Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included + * in the email address. Defaults to `false`. + * + * @example + * email(fakerCore) // 'Kassandra4@hotmail.com' + * email(fakerCore, { firstName: 'Jeanne'}) // 'Jeanne63@yahoo.com' + * email(fakerCore, { firstName: 'Jeanne'}) // 'Jeanne_Smith63@yahoo.com' + * email(fakerCore, { firstName: 'Jeanne', lastName: 'Doe' }) // 'Jeanne.Doe63@yahoo.com' + * email(fakerCore, { firstName: 'Jeanne', lastName: 'Doe', provider: 'example.fakerjs.dev' }) // 'Jeanne_Doe88@example.fakerjs.dev' + * email(fakerCore, { firstName: 'Jeanne', lastName: 'Doe', provider: 'example.fakerjs.dev', allowSpecialCharacters: true }) // 'Jeanne%Doe88@example.fakerjs.dev' + * + * @since 2.0.1 + */ +export function email( + fakerCore: FakerCore, + options: { + /** + * The optional first name to use. + * + * @default personFirstName(fakerCore) + */ + firstName?: string; + /** + * The optional last name to use. + * + * @default personLastName(fakerCore) + */ + lastName?: string; + /** + * The mail provider domain to use. If not specified, a random free mail provider will be chosen. + */ + provider?: string; + /** + * Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included in the email address. + * + * @default false + */ + allowSpecialCharacters?: boolean; + } = {} +): string { + const { + firstName, + lastName, + provider = arrayElement(fakerCore, fakerCore.locale.internet.free_email), + allowSpecialCharacters = false, + } = options; + + let localPart: string = username(fakerCore, { firstName, lastName }); + // Strip any special characters from the local part of the email address + // This could happen if invalid chars are passed in manually in the firstName/lastName + localPart = localPart.replaceAll(/[^A-Za-z0-9._+-]+/g, ''); + + // The local part of an email address is limited to 64 chars per RFC 3696 + // We limit to 50 chars to be more realistic + localPart = localPart.substring(0, 50); + if (allowSpecialCharacters) { + const usernameChars: string[] = [...'._-']; + const specialChars: string[] = [...".!#$%&'*+-/=?^_`{|}~"]; + localPart = localPart.replace( + arrayElement(fakerCore, usernameChars), + arrayElement(fakerCore, specialChars) + ); + } + + // local parts may not contain two or more consecutive . characters + localPart = localPart.replaceAll(/\.{2,}/g, '.'); + + // local parts may not start with or end with a . character + localPart = localPart.replace(/^\./, ''); + localPart = localPart.replace(/\.$/, ''); + + return `${localPart}@${provider}`; +} diff --git a/src/modules/internet/emoji.ts b/src/modules/internet/emoji.ts new file mode 100644 index 00000000000..3b133725278 --- /dev/null +++ b/src/modules/internet/emoji.ts @@ -0,0 +1,45 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export type EmojiType = + | 'smiley' + | 'body' + | 'person' + | 'nature' + | 'food' + | 'travel' + | 'activity' + | 'object' + | 'symbol' + | 'flag'; + +/** + * Generates a random emoji. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.types A list of the emoji types that should be included. Possible values are `'smiley'`, `'body'`, `'person'`, `'nature'`, `'food'`, `'travel'`, `'activity'`, `'object'`, `'symbol'`, `'flag'`. By default, emojis from any type will be included. + * + * @example + * emoji(fakerCore) // '🥰' + * emoji(fakerCore, { types: ['food', 'nature'] }) // '🥐' + * + * @since 6.2.0 + */ +export function emoji( + fakerCore: FakerCore, + options: { + /** + * A list of the emoji types that should be used. + * + * @default Object.keys(faker.definitions.internet.emoji) + */ + types?: ReadonlyArray; + } = {} +): string { + const { + types = Object.keys(fakerCore.locale.internet.emoji) as EmojiType[], + } = options; + const emojiType = arrayElement(fakerCore, types); + return arrayElement(fakerCore, fakerCore.locale.internet.emoji[emojiType]); +} diff --git a/src/modules/internet/example-email.ts b/src/modules/internet/example-email.ts new file mode 100644 index 00000000000..95b111b9d10 --- /dev/null +++ b/src/modules/internet/example-email.ts @@ -0,0 +1,60 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { email } from '../internet/email'; + +/** + * Generates an email address using an example mail provider using the given person's name as base. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.firstName The optional first name to use. If not specified, a random one will be chosen. + * @param options.lastName The optional last name to use. If not specified, a random one will be chosen. + * @param options.allowSpecialCharacters Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included + * in the email address. Defaults to `false`. + * + * @example + * exampleEmail(fakerCore) // 'Helmer.Graham23@example.com' + * exampleEmail(fakerCore, { firstName: 'Jeanne' }) // 'Jeanne96@example.net' + * exampleEmail(fakerCore, { firstName: 'Jeanne' }) // 'Jeanne.Smith96@example.net' + * exampleEmail(fakerCore, { firstName: 'Jeanne', lastName: 'Doe' }) // 'Jeanne_Doe96@example.net' + * exampleEmail(fakerCore, { firstName: 'Jeanne', lastName: 'Doe', allowSpecialCharacters: true }) // 'Jeanne%Doe88@example.com' + * + * @since 3.1.0 + */ +export function exampleEmail( + fakerCore: FakerCore, + options: { + /** + * The optional first name to use. + * + * @default personFirstName(fakerCore) + */ + firstName?: string; + /** + * The optional last name to use. + * + * @default personLastName(fakerCore) + */ + lastName?: string; + /** + * Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included in the email address. + * + * @default false + */ + allowSpecialCharacters?: boolean; + } = {} +): string { + const { firstName, lastName, allowSpecialCharacters = false } = options; + + const provider = arrayElement( + fakerCore, + fakerCore.locale.internet.example_email + ); + + return email(fakerCore, { + firstName, + lastName, + provider, + allowSpecialCharacters, + }); +} diff --git a/src/modules/internet/http-method.ts b/src/modules/internet/http-method.ts new file mode 100644 index 00000000000..2c5b1c9f423 --- /dev/null +++ b/src/modules/internet/http-method.ts @@ -0,0 +1,33 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random http method. + * + * Can be either of the following: + * + * - `GET` + * - `POST` + * - `PUT` + * - `DELETE` + * - `PATCH` + * + * @param fakerCore The FakerCore to use. + * + * @example + * httpMethod(fakerCore) // 'PATCH' + * + * @since 5.4.0 + */ +export function httpMethod( + fakerCore: FakerCore +): 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' { + const httpMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + ]; + return arrayElement(fakerCore, httpMethods); +} diff --git a/src/modules/internet/http-status-code.ts b/src/modules/internet/http-status-code.ts new file mode 100644 index 00000000000..6f86759671e --- /dev/null +++ b/src/modules/internet/http-status-code.ts @@ -0,0 +1,45 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export type HTTPStatusCodeType = + | 'informational' + | 'success' + | 'clientError' + | 'serverError' + | 'redirection'; + +/** + * Generates a random HTTP status code. + * + * @param fakerCore The FakerCore to use. + * @param options Options object. + * @param options.types A list of the HTTP status code types that should be used. + * + * @example + * httpStatusCode(fakerCore) // 200 + * httpStatusCode(fakerCore, { types: ['success', 'serverError'] }) // 500 + * + * @since 7.0.0 + */ +export function httpStatusCode( + fakerCore: FakerCore, + options: { + /** + * A list of the HTTP status code types that should be used. + * + * @default Object.keys(fakerCore.locale.internet.http_status_code) + */ + types?: ReadonlyArray; + } = {} +): number { + const { + types = Object.keys( + fakerCore.locale.internet.http_status_code + ) as HTTPStatusCodeType[], + } = options; + const httpStatusCodeType = arrayElement(fakerCore, types); + return arrayElement( + fakerCore, + fakerCore.locale.internet.http_status_code[httpStatusCodeType] + ); +} diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts index 8cd69fadb42..78d67c24ed7 100644 --- a/src/modules/internet/index.ts +++ b/src/modules/internet/index.ts @@ -1,137 +1,35 @@ -import { FakerError } from '../../errors/faker-error'; -import type { Faker } from '../../faker'; -import { toBase64Url } from '../../internal/base64'; import { ModuleBase } from '../../internal/module-base'; -import { charMapping } from './char-mappings'; - -export type EmojiType = - | 'smiley' - | 'body' - | 'person' - | 'nature' - | 'food' - | 'travel' - | 'activity' - | 'object' - | 'symbol' - | 'flag'; - -export type HTTPStatusCodeType = - | 'informational' - | 'success' - | 'clientError' - | 'serverError' - | 'redirection'; - -export type HTTPProtocolType = 'http' | 'https'; - -export enum IPv4Network { - /** - * Equivalent to: `0.0.0.0/0` - */ - Any = 'any', - /** - * Equivalent to: `127.0.0.0/8` - * - * @see [RFC1122](https://www.rfc-editor.org/rfc/rfc1122) - */ - Loopback = 'loopback', - /** - * Equivalent to: `10.0.0.0/8` - * - * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) - */ - PrivateA = 'private-a', - /** - * Equivalent to: `172.16.0.0/12` - * - * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) - */ - PrivateB = 'private-b', - /** - * Equivalent to: `192.168.0.0/16` - * - * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) - */ - PrivateC = 'private-c', - /** - * Equivalent to: `192.0.2.0/24` - * - * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) - */ - TestNet1 = 'test-net-1', - /** - * Equivalent to: `198.51.100.0/24` - * - * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) - */ - TestNet2 = 'test-net-2', - /** - * Equivalent to: `203.0.113.0/24` - * - * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) - */ - TestNet3 = 'test-net-3', - /** - * Equivalent to: `169.254.0.0/16` - * - * @see [RFC3927](https://www.rfc-editor.org/rfc/rfc3927) - */ - LinkLocal = 'link-local', - /** - * Equivalent to: `224.0.0.0/4` - * - * @see [RFC5771](https://www.rfc-editor.org/rfc/rfc5771) - */ - Multicast = 'multicast', -} - -export type IPv4NetworkType = `${IPv4Network}`; - -const ipv4Networks: Record = { - [IPv4Network.Any]: '0.0.0.0/0', - [IPv4Network.Loopback]: '127.0.0.0/8', - [IPv4Network.PrivateA]: '10.0.0.0/8', - [IPv4Network.PrivateB]: '172.16.0.0/12', - [IPv4Network.PrivateC]: '192.168.0.0/16', - [IPv4Network.TestNet1]: '192.0.2.0/24', - [IPv4Network.TestNet2]: '198.51.100.0/24', - [IPv4Network.TestNet3]: '203.0.113.0/24', - [IPv4Network.LinkLocal]: '169.254.0.0/16', - [IPv4Network.Multicast]: '224.0.0.0/4', -}; - -/** - * Checks whether the given string is a valid slug for `domainWord`s. - * - * @param slug The slug to check. - */ -function isValidDomainWordSlug(slug: string): boolean { - return /^[a-z][a-z-]*[a-z]$/i.exec(slug) !== null; -} - -/** - * Tries various ways to produce a valid domain word slug, falling back to a random string if needed. - * - * @param faker The faker instance to use. - * @param word The initial word to slugify. - */ -function makeValidDomainWordSlug(faker: Faker, word: string): string { - const slug1 = faker.helpers.slugify(word); - if (isValidDomainWordSlug(slug1)) { - return slug1; - } - - const slug2 = faker.helpers.slugify(faker.lorem.word()); - if (isValidDomainWordSlug(slug2)) { - return slug2; - } - - return faker.string.alpha({ - casing: 'lower', - length: faker.number.int({ min: 4, max: 8 }), - }); -} +import { displayName as internetDisplayName } from './display-name'; +import { domainName as internetDomainName } from './domain-name'; +import { domainSuffix as internetDomainSuffix } from './domain-suffix'; +import { domainWord as internetDomainWord } from './domain-word'; +import { email as internetEmail } from './email'; +import type { EmojiType } from './emoji'; +import { emoji as internetEmoji } from './emoji'; +import { exampleEmail as internetExampleEmail } from './example-email'; +import { httpMethod as internetHttpMethod } from './http-method'; +import type { HTTPStatusCodeType } from './http-status-code'; +import { httpStatusCode as internetHttpStatusCode } from './http-status-code'; +import { ip as internetIp } from './ip'; +import type { IPv4NetworkType } from './ipv4'; +import { ipv4 as internetIpv4 } from './ipv4'; +import { ipv6 as internetIpv6 } from './ipv6'; +import { jwt as internetJwt } from './jwt'; +import { jwtAlgorithm as internetJwtAlgorithm } from './jwt-algorithm'; +import { mac as internetMac } from './mac'; +import { password as internetPassword } from './password'; +import { port as internetPort } from './port'; +import { protocol as internetProtocol } from './protocol'; +import type { HTTPProtocolType } from './url'; +import { url as internetUrl } from './url'; +import { userAgent as internetUserAgent } from './user-agent'; +import { username as internetUsername } from './username'; + +export type { EmojiType } from './emoji'; +export type { HTTPStatusCodeType } from './http-status-code'; +export { IPv4Network } from './ipv4'; +export type { IPv4NetworkType } from './ipv4'; +export type { HTTPProtocolType } from './url'; /** * Module to generate internet related entries. @@ -193,40 +91,7 @@ export class InternetModule extends ModuleBase { allowSpecialCharacters?: boolean; } = {} ): string { - const { - firstName, - lastName, - provider = this.faker.helpers.arrayElement( - this.faker.definitions.internet.free_email - ), - allowSpecialCharacters = false, - } = options; - - let localPart: string = this.username({ firstName, lastName }); - // Strip any special characters from the local part of the email address - // This could happen if invalid chars are passed in manually in the firstName/lastName - localPart = localPart.replaceAll(/[^A-Za-z0-9._+-]+/g, ''); - - // The local part of an email address is limited to 64 chars per RFC 3696 - // We limit to 50 chars to be more realistic - localPart = localPart.substring(0, 50); - if (allowSpecialCharacters) { - const usernameChars: string[] = [...'._-']; - const specialChars: string[] = [...".!#$%&'*+-/=?^_`{|}~"]; - localPart = localPart.replace( - this.faker.helpers.arrayElement(usernameChars), - this.faker.helpers.arrayElement(specialChars) - ); - } - - // local parts may not contain two or more consecutive . characters - localPart = localPart.replaceAll(/\.{2,}/g, '.'); - - // local parts may not start with or end with a . character - localPart = localPart.replace(/^\./, ''); - localPart = localPart.replace(/\.$/, ''); - - return `${localPart}@${provider}`; + return internetEmail(this.faker.fakerCore, options); } /** @@ -269,18 +134,7 @@ export class InternetModule extends ModuleBase { allowSpecialCharacters?: boolean; } = {} ): string { - const { firstName, lastName, allowSpecialCharacters = false } = options; - - const provider = this.faker.helpers.arrayElement( - this.faker.definitions.internet.example_email - ); - - return this.email({ - firstName, - lastName, - provider, - allowSpecialCharacters, - }); + return internetExampleEmail(this.faker.fakerCore, options); } /** @@ -323,52 +177,7 @@ export class InternetModule extends ModuleBase { lastName?: string; } = {} ): string { - const { - firstName = this.faker.person.firstName(), - lastName = this.faker.person.lastName(), - lastName: hasLastName, - } = options; - - const separator = this.faker.helpers.arrayElement(['.', '_']); - const disambiguator = this.faker.number.int(99); - const strategies: Array<() => string> = [ - () => `${firstName}${separator}${lastName}${disambiguator}`, - () => `${firstName}${separator}${lastName}`, - ]; - if (!hasLastName) { - strategies.push(() => `${firstName}${disambiguator}`); - } - - let result = this.faker.helpers.arrayElement(strategies)(); - - // There may still be non-ascii characters in the result. - // First remove simple accents etc - result = result - .normalize('NFKD') //for example è decomposes to as e + ̀ - .replaceAll(/[\u0300-\u036F]/g, ''); // removes combining marks - - result = [...result] - .map((char) => { - // If we have a mapping for this character, (for Cyrillic, Greek etc) use it - if (charMapping[char]) { - return charMapping[char]; - } - - const charCode = char.codePointAt(0) ?? Number.NaN; - - if (charCode < 0x80) { - // Keep ASCII characters - return char; - } - - // Final fallback return the Unicode char code value for Chinese, Japanese, Korean etc, base-36 encoded - return charCode.toString(36); - }) - .join(''); - result = result.replaceAll("'", ''); - result = result.replaceAll(' ', ''); - - return result; + return internetUsername(this.faker.fakerCore, options); } /** @@ -409,23 +218,7 @@ export class InternetModule extends ModuleBase { lastName?: string; } = {} ): string { - const { - firstName = this.faker.person.firstName(), - lastName = this.faker.person.lastName(), - } = options; - - const separator = this.faker.helpers.arrayElement(['.', '_']); - const disambiguator = this.faker.number.int(99); - const strategies: Array<() => string> = [ - () => `${firstName}${disambiguator}`, - () => `${firstName}${separator}${lastName}`, - () => `${firstName}${separator}${lastName}${disambiguator}`, - ]; - - let result = this.faker.helpers.arrayElement(strategies)(); - result = result.replaceAll("'", ''); - result = result.replaceAll(' ', ''); - return result; + return internetDisplayName(this.faker.fakerCore, options); } /** @@ -437,8 +230,7 @@ export class InternetModule extends ModuleBase { * @since 2.1.5 */ protocol(): 'http' | 'https' { - const protocols: ['http', 'https'] = ['http', 'https']; - return this.faker.helpers.arrayElement(protocols); + return internetProtocol(this.faker.fakerCore); } /** @@ -458,14 +250,7 @@ export class InternetModule extends ModuleBase { * @since 5.4.0 */ httpMethod(): 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' { - const httpMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] = [ - 'GET', - 'POST', - 'PUT', - 'DELETE', - 'PATCH', - ]; - return this.faker.helpers.arrayElement(httpMethods); + return internetHttpMethod(this.faker.fakerCore); } /** @@ -485,20 +270,12 @@ export class InternetModule extends ModuleBase { /** * A list of the HTTP status code types that should be used. * - * @default Object.keys(faker.definitions.internet.http_status_code) + * @default Object.keys(fakerCore.locale.internet.http_status_code) */ types?: ReadonlyArray; } = {} ): number { - const { - types = Object.keys( - this.faker.definitions.internet.http_status_code - ) as HTTPStatusCodeType[], - } = options; - const httpStatusCodeType = this.faker.helpers.arrayElement(types); - return this.faker.helpers.arrayElement( - this.faker.definitions.internet.http_status_code[httpStatusCodeType] - ); + return internetHttpStatusCode(this.faker.fakerCore, options); } /** @@ -531,9 +308,7 @@ export class InternetModule extends ModuleBase { protocol?: HTTPProtocolType; } = {} ): string { - const { appendSlash = this.faker.datatype.boolean(), protocol = 'https' } = - options; - return `${protocol}://${this.domainName()}${appendSlash ? '/' : ''}`; + return internetUrl(this.faker.fakerCore, options); } /** @@ -545,7 +320,7 @@ export class InternetModule extends ModuleBase { * @since 2.0.1 */ domainName(): string { - return `${this.domainWord()}.${this.domainSuffix()}`; + return internetDomainName(this.faker.fakerCore); } /** @@ -558,9 +333,7 @@ export class InternetModule extends ModuleBase { * @since 2.0.1 */ domainSuffix(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.internet.domain_suffix - ); + return internetDomainSuffix(this.faker.fakerCore); } /** @@ -573,15 +346,7 @@ export class InternetModule extends ModuleBase { * @since 2.0.1 */ domainWord(): string { - // Generate an ASCII "word" in the form `noun-adjective` - // For locales with non-ASCII characters, we fall back to lorem words, or a random string - - const word1 = makeValidDomainWordSlug( - this.faker, - this.faker.word.adjective() - ); - const word2 = makeValidDomainWordSlug(this.faker, this.faker.word.noun()); - return `${word1}-${word2}`.toLowerCase(); + return internetDomainWord(this.faker.fakerCore); } /** @@ -594,7 +359,7 @@ export class InternetModule extends ModuleBase { * @since 2.0.1 */ ip(): string { - return this.faker.datatype.boolean() ? this.ipv4() : this.ipv6(); + return internetIp(this.faker.fakerCore); } /** @@ -673,27 +438,7 @@ export class InternetModule extends ModuleBase { ipv4( options: { cidrBlock?: string; network?: IPv4NetworkType } = {} ): string { - const { network = 'any', cidrBlock = ipv4Networks[network] } = options; - - if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/.test(cidrBlock)) { - throw new FakerError( - `Invalid CIDR block provided: ${cidrBlock}. Must be in the format x.x.x.x/y.` - ); - } - - const [ipText, subnet] = cidrBlock.split('/'); - const subnetMask = 0xffffffff >>> Number.parseInt(subnet); - const [rawIp1, rawIp2, rawIp3, rawIp4] = ipText.split('.').map(Number); - const rawIp = (rawIp1 << 24) | (rawIp2 << 16) | (rawIp3 << 8) | rawIp4; - const networkIp = rawIp & ~subnetMask; - const hostOffset = this.faker.number.int(subnetMask); - const ip = networkIp | hostOffset; - return [ - (ip >>> 24) & 0xff, - (ip >>> 16) & 0xff, - (ip >>> 8) & 0xff, - ip & 0xff, - ].join('.'); + return internetIpv4(this.faker.fakerCore, options); } /** @@ -705,13 +450,7 @@ export class InternetModule extends ModuleBase { * @since 4.0.0 */ ipv6(): string { - return Array.from({ length: 8 }, () => - this.faker.string.hexadecimal({ - length: 4, - casing: 'lower', - prefix: '', - }) - ).join(':'); + return internetIpv6(this.faker.fakerCore); } /** @@ -723,7 +462,7 @@ export class InternetModule extends ModuleBase { * @since 5.4.0 */ port(): number { - return this.faker.number.int(65535); + return internetPort(this.faker.fakerCore); } /** @@ -736,9 +475,7 @@ export class InternetModule extends ModuleBase { * @since 2.0.1 */ userAgent(): string { - return this.faker.helpers.fake( - this.faker.definitions.internet.user_agent_pattern - ); + return internetUserAgent(this.faker.fakerCore); } /** @@ -806,28 +543,7 @@ export class InternetModule extends ModuleBase { separator?: string; } = {} ): string { - if (typeof options === 'string') { - options = { separator: options }; - } - - let { separator = ':' } = options; - - let i: number; - let mac = ''; - - const acceptableSeparators = [':', '-', '']; - if (!acceptableSeparators.includes(separator)) { - separator = ':'; - } - - for (i = 0; i < 12; i++) { - mac += this.faker.number.hex(15); - if (i % 2 === 1 && i !== 11) { - mac += separator; - } - } - - return mac; + return internetMac(this.faker.fakerCore, options); } /** @@ -879,48 +595,7 @@ export class InternetModule extends ModuleBase { prefix?: string; } = {} ): string { - /* - * password-generator ( function ) - * Copyright(c) 2011-2013 Bermi Ferrer - * MIT Licensed - */ - const vowel = /[aeiouAEIOU]$/; - const consonant = /[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]$/; - const _password = ( - length: number, - memorable: boolean, - pattern: RegExp, - prefix: string - ): string => { - if (prefix.length >= length) { - return prefix; - } - - if (memorable) { - pattern = consonant.test(prefix) ? vowel : consonant; - } - - const n = this.faker.number.int(94) + 33; - let char = String.fromCodePoint(n); - if (memorable) { - char = char.toLowerCase(); - } - - if (!pattern.test(char)) { - return _password(length, memorable, pattern, prefix); - } - - return _password(length, memorable, pattern, prefix + char); - }; - - const { - length = 15, - memorable = false, - pattern = /\w/, - prefix = '', - } = options; - - return _password(length, memorable, pattern, prefix); + return internetPassword(this.faker.fakerCore, options); } /** @@ -945,13 +620,7 @@ export class InternetModule extends ModuleBase { types?: ReadonlyArray; } = {} ): string { - const { - types = Object.keys(this.faker.definitions.internet.emoji) as EmojiType[], - } = options; - const emojiType = this.faker.helpers.arrayElement(types); - return this.faker.helpers.arrayElement( - this.faker.definitions.internet.emoji[emojiType] - ); + return internetEmoji(this.faker.fakerCore, options); } /** @@ -966,9 +635,7 @@ export class InternetModule extends ModuleBase { * @since 9.1.0 */ jwtAlgorithm(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.internet.jwt_algorithm - ); + return internetJwtAlgorithm(this.faker.fakerCore); } /** @@ -1027,32 +694,6 @@ export class InternetModule extends ModuleBase { refDate?: string | Date | number; } = {} ): string { - const { refDate = this.faker.defaultRefDate() } = options; - - const iatDefault = this.faker.date.recent({ refDate }); - - const { - header = { - alg: this.jwtAlgorithm(), - typ: 'JWT', - }, - payload = { - iat: Math.round(iatDefault.valueOf() / 1000), - exp: Math.round( - this.faker.date.soon({ refDate: iatDefault }).valueOf() / 1000 - ), - nbf: Math.round(this.faker.date.anytime({ refDate }).valueOf() / 1000), - iss: this.faker.company.name(), - sub: this.faker.string.uuid(), - aud: this.faker.string.uuid(), - jti: this.faker.string.uuid(), - }, - } = options; - - const encodedHeader = toBase64Url(JSON.stringify(header)); - const encodedPayload = toBase64Url(JSON.stringify(payload)); - const signature = this.faker.string.alphanumeric(64); - - return `${encodedHeader}.${encodedPayload}.${signature}`; + return internetJwt(this.faker.fakerCore, options); } } diff --git a/src/modules/internet/ip.ts b/src/modules/internet/ip.ts new file mode 100644 index 00000000000..dea4576e46e --- /dev/null +++ b/src/modules/internet/ip.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { ipv4 } from '../internet/ipv4'; +import { ipv6 } from '../internet/ipv6'; + +/** + * Generates a random IPv4 or IPv6 address. + * + * @param fakerCore The FakerCore to use. + * + * @example + * ip(fakerCore) // '245.108.222.0' + * ip(fakerCore) // '4e5:f9c5:4337:abfd:9caf:1135:41ad:d8d3' + * + * @since 2.0.1 + */ +export function ip(fakerCore: FakerCore): string { + return boolean(fakerCore) ? ipv4(fakerCore) : ipv6(fakerCore); +} diff --git a/src/modules/internet/ipv4.ts b/src/modules/internet/ipv4.ts new file mode 100644 index 00000000000..4ca22e0ab26 --- /dev/null +++ b/src/modules/internet/ipv4.ts @@ -0,0 +1,190 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { int } from '../number/int'; + +export enum IPv4Network { + /** + * Equivalent to: `0.0.0.0/0` + */ + Any = 'any', + /** + * Equivalent to: `127.0.0.0/8` + * + * @see [RFC1122](https://www.rfc-editor.org/rfc/rfc1122) + */ + Loopback = 'loopback', + /** + * Equivalent to: `10.0.0.0/8` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateA = 'private-a', + /** + * Equivalent to: `172.16.0.0/12` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateB = 'private-b', + /** + * Equivalent to: `192.168.0.0/16` + * + * @see [RFC1918](https://www.rfc-editor.org/rfc/rfc1918) + */ + PrivateC = 'private-c', + /** + * Equivalent to: `192.0.2.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet1 = 'test-net-1', + /** + * Equivalent to: `198.51.100.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet2 = 'test-net-2', + /** + * Equivalent to: `203.0.113.0/24` + * + * @see [RFC5737](https://www.rfc-editor.org/rfc/rfc5737) + */ + TestNet3 = 'test-net-3', + /** + * Equivalent to: `169.254.0.0/16` + * + * @see [RFC3927](https://www.rfc-editor.org/rfc/rfc3927) + */ + LinkLocal = 'link-local', + /** + * Equivalent to: `224.0.0.0/4` + * + * @see [RFC5771](https://www.rfc-editor.org/rfc/rfc5771) + */ + Multicast = 'multicast', +} + +export type IPv4NetworkType = `${IPv4Network}`; + +const ipv4Networks: Record = { + [IPv4Network.Any]: '0.0.0.0/0', + [IPv4Network.Loopback]: '127.0.0.0/8', + [IPv4Network.PrivateA]: '10.0.0.0/8', + [IPv4Network.PrivateB]: '172.16.0.0/12', + [IPv4Network.PrivateC]: '192.168.0.0/16', + [IPv4Network.TestNet1]: '192.0.2.0/24', + [IPv4Network.TestNet2]: '198.51.100.0/24', + [IPv4Network.TestNet3]: '203.0.113.0/24', + [IPv4Network.LinkLocal]: '169.254.0.0/16', + [IPv4Network.Multicast]: '224.0.0.0/4', +}; + +/** + * Generates a random IPv4 address. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.cidrBlock The optional CIDR block to use. Must be in the format `x.x.x.x/y`. Defaults to `'0.0.0.0/0'`. + * + * @example + * ipv4(fakerCore) // '245.108.222.0' + * ipv4(fakerCore, { cidrBlock: '192.168.0.0/16' }) // '192.168.215.224' + * + * @since 6.1.1 + */ +export function ipv4( + fakerCore: FakerCore, + options?: { + /** + * The optional CIDR block to use. Must be in the format `x.x.x.x/y`. + * + * @default '0.0.0.0/0' + */ + cidrBlock?: string; + } +): string; +/** + * Generates a random IPv4 address. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.network The optional network to use. This is intended as an alias for well-known `cidrBlock`s. Defaults to `'any'`. + * + * @example + * ipv4(fakerCore) // '245.108.222.0' + * ipv4(fakerCore, { network: 'private-a' }) // '10.199.154.205' + * + * @since 6.1.1 + */ +export function ipv4( + fakerCore: FakerCore, + options?: { + /** + * The optional network to use. This is intended as an alias for well-known `cidrBlock`s. + * + * @default 'any' + */ + network?: IPv4NetworkType; + } +): string; +/** + * Generates a random IPv4 address. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.cidrBlock The optional CIDR block to use. Must be in the format `x.x.x.x/y`. Defaults to `'0.0.0.0/0'`. + * @param options.network The optional network to use. This is intended as an alias for well-known `cidrBlock`s. Defaults to `'any'`. + * + * @example + * ipv4(fakerCore) // '245.108.222.0' + * ipv4(fakerCore, { cidrBlock: '192.168.0.0/16' }) // '192.168.215.224' + * ipv4(fakerCore, { network: 'private-a' }) // '10.199.154.205' + * + * @since 6.1.1 + */ +export function ipv4( + fakerCore: FakerCore, + options?: + | { + /** + * The optional CIDR block to use. Must be in the format `x.x.x.x/y`. + * + * @default '0.0.0.0/0' + */ + cidrBlock?: string; + } + | { + /** + * The optional network to use. This is intended as an alias for well-known `cidrBlock`s. + * + * @default 'any' + */ + network?: IPv4NetworkType; + } +): string; + +export function ipv4( + fakerCore: FakerCore, + options: { cidrBlock?: string; network?: IPv4NetworkType } = {} +): string { + const { network = 'any', cidrBlock = ipv4Networks[network] } = options; + + if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/.test(cidrBlock)) { + throw new FakerError( + `Invalid CIDR block provided: ${cidrBlock}. Must be in the format x.x.x.x/y.` + ); + } + + const [ipText, subnet] = cidrBlock.split('/'); + const subnetMask = 0xffffffff >>> Number.parseInt(subnet); + const [rawIp1, rawIp2, rawIp3, rawIp4] = ipText.split('.').map(Number); + const rawIp = (rawIp1 << 24) | (rawIp2 << 16) | (rawIp3 << 8) | rawIp4; + const networkIp = rawIp & ~subnetMask; + const hostOffset = int(fakerCore, subnetMask); + const ip = networkIp | hostOffset; + return [ + (ip >>> 24) & 0xff, + (ip >>> 16) & 0xff, + (ip >>> 8) & 0xff, + ip & 0xff, + ].join('.'); +} diff --git a/src/modules/internet/ipv6.ts b/src/modules/internet/ipv6.ts new file mode 100644 index 00000000000..d87c293fe3c --- /dev/null +++ b/src/modules/internet/ipv6.ts @@ -0,0 +1,22 @@ +import type { FakerCore } from '../../core'; +import { hexadecimal } from '../string/hexadecimal'; + +/** + * Generates a random IPv6 address. + * + * @param fakerCore The FakerCore to use. + * + * @example + * ipv6(fakerCore) // '269f:1230:73e3:318d:842b:daab:326d:897b' + * + * @since 4.0.0 + */ +export function ipv6(fakerCore: FakerCore): string { + return Array.from({ length: 8 }, () => + hexadecimal(fakerCore, { + length: 4, + casing: 'lower', + prefix: '', + }) + ).join(':'); +} diff --git a/src/modules/internet/jwt-algorithm.ts b/src/modules/internet/jwt-algorithm.ts new file mode 100644 index 00000000000..a708f4dabb7 --- /dev/null +++ b/src/modules/internet/jwt-algorithm.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random JWT (JSON Web Token) Algorithm. + * + * @param fakerCore The FakerCore to use. + * + * @see jwt(fakerCore): For generating random JWT (JSON Web Token). + * + * @example + * jwtAlgorithm(fakerCore) // 'HS256' + * jwtAlgorithm(fakerCore) // 'RS512' + * + * @since 9.1.0 + */ +export function jwtAlgorithm(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.internet.jwt_algorithm); +} diff --git a/src/modules/internet/jwt.ts b/src/modules/internet/jwt.ts new file mode 100644 index 00000000000..8ef137ad853 --- /dev/null +++ b/src/modules/internet/jwt.ts @@ -0,0 +1,97 @@ +import type { FakerCore } from '../../core'; +import { toBase64Url } from '../../internal/base64'; +import { getDefaultRefDate } from '../../utils/get-default-ref-date'; +import { name } from '../company/name'; +import { anytime } from '../date/anytime'; +import { recent } from '../date/recent'; +import { soon } from '../date/soon'; +import { jwtAlgorithm } from '../internet/jwt-algorithm'; +import { alphanumeric } from '../string/alphanumeric'; +import { uuid } from '../string/uuid'; + +/** + * Generates a random JWT (JSON Web Token). + * + * Please note that this method generates a random signature instead of a valid one. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.header The Header to use for the token. Defaults to a random object with the following fields: `alg` and `typ`. + * @param options.payload The Payload to use for the token. Defaults to a random object with the following fields: `iat`, `exp`, `nbf`, `iss`, `sub`, `aud`, and `jti`. + * @param options.refDate The date to use as reference point for the newly generated date. + * + * @see https://datatracker.ietf.org/doc/html/rfc7519 + * @see jwtAlgorithm(fakerCore): For generating random JWT (JSON Web Token) Algorithm. + * + * @example + * jwt(fakerCore) // 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzI2MzgxMDYsImV4cCI6MTczMjY5MjUwOSwibmJmIjoxNzA1MDgxNjQ4LCJpc3MiOiJHdXRrb3dza2kgYW5kIFNvbnMiLCJzdWIiOiJlMzQxZjMwNS0yM2I2LTRkYmQtOTY2ZC1iNDRiZmM0ZGIzMGUiLCJhdWQiOiI0YzMwZGE3Yi0zZDUzLTQ4OGUtYTAyZC0zOWI2MDZiZmYxMTciLCJqdGkiOiJiMGZmOTMzOC04ODMwLTRmNDgtYjA3Ny1kNDNmMjU2OGZlYzAifQ.oDLVR73M0u5SjMPlc1aruxbdK7l2titXSeo9J5M1JUd65a1X9MhCz7FOobtX8eaj' + * jwt(fakerCore, { header: { alg: 'HS256' }}) // 'eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTg2MTM3MTIsImV4cCI6MTcxODYzMzY3OSwibmJmIjoxNjk3MjYzNjMwLCJpc3MiOiJEb3lsZSBhbmQgU29ucyIsInN1YiI6IjYxYWRkYWFmLWY4MjktNDkzZS1iNTI1LTJjMGJkNjkzOTdjNyIsImF1ZCI6IjczNjcyMjVjLWIwMWMtNGE1My1hYzQyLTYwOWJkZmI1MzBiOCIsImp0aSI6IjU2Y2ZkZjAxLWRhMzMtNGUxNi04MzJiLTFlYTk3ZGY1MTQ2YSJ9.5iUgaCaFVPZ8d1QD0xMjoeJbmPVyUfKfoRQ6Njzm5MLp5F4UMh5REbPCrW70fAkr' + * jwt(fakerCore, { payload: { iss: 'Acme' }}) // 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBY21lIn0.syUt0GBukNac8Cn1AGKFq2SWAXWy1YIfl0uOYiwg6TZ3omAW0c7FGWY6bC7ZOFSt' + * jwt(fakerCore, { refDate: '2020-01-01T00:00:00.000Z' }) // 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1Nzc4MDY4NDUsImV4cCI6MTU3Nzg0NjI4MCwibmJmIjoxNTgxNTQyMDYwLCJpc3MiOiJLcmVpZ2VyLCBBbHRlbndlcnRoIGFuZCBQYXVjZWsiLCJzdWIiOiI5NzVjMjMyOS02MDlhLTRjYTYtYjBkZi05ZmY4MGZiNDUwN2QiLCJhdWQiOiI0ODQxZWYwNi01OWYwLTQzMWEtYmFmZi0xMjkxZmRhZDdhNjgiLCJqdGkiOiJmNDBjZTJiYi00ZWYyLTQ1MjMtOGIxMy1kN2Q4NTA5N2M2ZTUifQ.cuClEZQ0CyPIMVS5uxrMwWXz0wcqFFdt0oNne3PMryyly0jghkxVurss2TapMC3C' + * + * @since 9.1.0 + */ +export function jwt( + fakerCore: FakerCore, + options: { + /** + * The header to use for the token. If present, it will replace any default values. + * + * @default + * { + * alg: jwtAlgorithm(fakerCore), + * typ: 'JWT' + * } + */ + header?: Record; + /** + * The payload to use for the token. If present, it will replace any default values. + * + * @default + * { + * iat: dateRecent(fakerCore), + * exp: dateSoon(fakerCore), + * nbf: dateAnytime(fakerCore), + * iss: companyName(fakerCore), + * sub: stringUuid(fakerCore), + * aud: stringUuid(fakerCore), + * jti: stringUuid(fakerCore) + * } + */ + payload?: Record; + /** + * The date to use as reference point for the newly generated date. + * + * @default getDefaultRefDate(fakerCore) + */ + refDate?: string | Date | number; + } = {} +): string { + const { refDate = getDefaultRefDate(fakerCore) } = options; + + const iatDefault = recent(fakerCore, { refDate }); + + const { + header = { + alg: jwtAlgorithm(fakerCore), + typ: 'JWT', + }, + payload = { + iat: Math.round(iatDefault.valueOf() / 1000), + exp: Math.round( + soon(fakerCore, { refDate: iatDefault }).valueOf() / 1000 + ), + nbf: Math.round(anytime(fakerCore, { refDate }).valueOf() / 1000), + iss: name(fakerCore), + sub: uuid(fakerCore), + aud: uuid(fakerCore), + jti: uuid(fakerCore), + }, + } = options; + + const encodedHeader = toBase64Url(JSON.stringify(header)); + const encodedPayload = toBase64Url(JSON.stringify(payload)); + const signature = alphanumeric(fakerCore, 64); + + return `${encodedHeader}.${encodedPayload}.${signature}`; +} diff --git a/src/modules/internet/mac.ts b/src/modules/internet/mac.ts new file mode 100644 index 00000000000..b5edcfd43e6 --- /dev/null +++ b/src/modules/internet/mac.ts @@ -0,0 +1,99 @@ +import type { FakerCore } from '../../core'; +import { hex } from '../number/hex'; + +/** + * Generates a random mac address. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.separator The optional separator to use. Can be either `':'`, `'-'` or `''`. Defaults to `':'`. + * + * @example + * mac(fakerCore) // '32:8e:2e:09:c6:05' + * + * @since 3.0.0 + */ +export function mac( + fakerCore: FakerCore, + options?: { + /** + * The optional separator to use. Can be either `':'`, `'-'` or `''`. + * + * @default ':' + */ + separator?: string; + } +): string; +/** + * Generates a random mac address. + * + * @param fakerCore The FakerCore to use. + * @param separator The optional separator to use. Can be either `':'`, `'-'` or `''`. Defaults to `':'`. + * + * @example + * mac(fakerCore) // '32:8e:2e:09:c6:05' + * + * @since 3.0.0 + */ +export function mac(fakerCore: FakerCore, separator?: string): string; +/** + * Generates a random mac address. + * + * @param fakerCore The FakerCore to use. + * @param options The optional separator or an options object. + * @param options.separator The optional separator to use. Can be either `':'`, `'-'` or `''`. Defaults to `':'`. + * + * @example + * mac(fakerCore) // '32:8e:2e:09:c6:05' + * + * @since 3.0.0 + */ +export function mac( + fakerCore: FakerCore, + options?: + | string + | { + /** + * The optional separator to use. Can be either `':'`, `'-'` or `''`. + * + * @default ':' + */ + separator?: string; + } +): string; +export function mac( + fakerCore: FakerCore, + options: + | string + | { + /** + * The optional separator to use. Can be either `':'`, `'-'` or `''`. + * + * @default ':' + */ + separator?: string; + } = {} +): string { + if (typeof options === 'string') { + options = { separator: options }; + } + + let { separator = ':' } = options; + + let i: number; + let mac = ''; + + const acceptableSeparators = [':', '-', '']; + if (!acceptableSeparators.includes(separator)) { + separator = ':'; + } + + for (i = 0; i < 12; i++) { + mac += hex(fakerCore, 15); + if (i % 2 === 1 && i !== 11) { + mac += separator; + } + } + + return mac; +} diff --git a/src/modules/internet/password.ts b/src/modules/internet/password.ts new file mode 100644 index 00000000000..175221c3863 --- /dev/null +++ b/src/modules/internet/password.ts @@ -0,0 +1,97 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Generates a random password-like string. Do not use this method for generating actual passwords for users. + * Since the source of the randomness is not cryptographically secure, neither is this generator. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.length The length of the password to generate. Defaults to `15`. + * @param options.memorable Whether the generated password should be memorable. Defaults to `false`. + * @param options.pattern The pattern that all chars should match. + * This option will be ignored, if `memorable` is `true`. Defaults to `/\w/`. + * @param options.prefix The prefix to use. Defaults to `''`. + * + * @example + * password(fakerCore) // '89G1wJuBLbGziIs' + * password(fakerCore, { length: 20 }) // 'aF55c_8O9kZaPOrysFB_' + * password(fakerCore, { length: 20, memorable: true }) // 'lawetimufozujosodedi' + * password(fakerCore, { length: 20, memorable: true, pattern: /[A-Z]/ }) // 'HMAQDFFYLDDUTBKVNFVS' + * password(fakerCore, { length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Hello ' }) // 'Hello IREOXTDWPERQSB' + * + * @since 2.0.1 + */ +export function password( + fakerCore: FakerCore, + options: { + /** + * The length of the password to generate. + * + * @default 15 + */ + length?: number; + /** + * Whether the generated password should be memorable. + * + * @default false + */ + memorable?: boolean; + /** + * The pattern that all chars should match. + * This option will be ignored, if `memorable` is `true`. + * + * @default /\w/ + */ + pattern?: RegExp; + /** + * The prefix to use. + * + * @default '' + */ + prefix?: string; + } = {} +): string { + /* + * password-generator ( function ) + * Copyright(c) 2011-2013 Bermi Ferrer + * MIT Licensed + */ + const vowel = /[aeiouAEIOU]$/; + const consonant = /[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]$/; + const _password = ( + length: number, + memorable: boolean, + pattern: RegExp, + prefix: string + ): string => { + if (prefix.length >= length) { + return prefix; + } + + if (memorable) { + pattern = consonant.test(prefix) ? vowel : consonant; + } + + const n = int(fakerCore, 94) + 33; + let char = String.fromCodePoint(n); + if (memorable) { + char = char.toLowerCase(); + } + + if (!pattern.test(char)) { + return _password(length, memorable, pattern, prefix); + } + + return _password(length, memorable, pattern, prefix + char); + }; + + const { + length = 15, + memorable = false, + pattern = /\w/, + prefix = '', + } = options; + + return _password(length, memorable, pattern, prefix); +} diff --git a/src/modules/internet/port.ts b/src/modules/internet/port.ts new file mode 100644 index 00000000000..1547821587f --- /dev/null +++ b/src/modules/internet/port.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Generates a random port number. + * + * @param fakerCore The FakerCore to use. + * + * @example + * port(fakerCore) // 9414 + * + * @since 5.4.0 + */ +export function port(fakerCore: FakerCore): number { + return int(fakerCore, 65535); +} diff --git a/src/modules/internet/protocol.ts b/src/modules/internet/protocol.ts new file mode 100644 index 00000000000..41331474a56 --- /dev/null +++ b/src/modules/internet/protocol.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random web protocol. Either `http` or `https`. + * + * @param fakerCore The FakerCore to use. + * + * @example + * protocol(fakerCore) // 'http' + * + * @since 2.1.5 + */ +export function protocol(fakerCore: FakerCore): 'http' | 'https' { + const protocols: ['http', 'https'] = ['http', 'https']; + return arrayElement(fakerCore, protocols); +} diff --git a/src/modules/internet/url.ts b/src/modules/internet/url.ts new file mode 100644 index 00000000000..cbbd9181b08 --- /dev/null +++ b/src/modules/internet/url.ts @@ -0,0 +1,41 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { domainName } from '../internet/domain-name'; + +export type HTTPProtocolType = 'http' | 'https'; + +/** + * Generates a random http(s) url. + * + * @param fakerCore The FakerCore to use. + * @param options Optional options object. + * @param options.appendSlash Whether to append a slash to the end of the url (path). Defaults to a random boolean value. + * @param options.protocol The protocol to use. Defaults to `'https'`. + * + * @example + * url(fakerCore) // 'https://remarkable-hackwork.info' + * url(fakerCore, { appendSlash: true }) // 'https://slow-timer.info/' + * url(fakerCore, { protocol: 'http', appendSlash: false }) // 'http://www.terrible-idea.com' + * + * @since 2.1.5 + */ +export function url( + fakerCore: FakerCore, + options: { + /** + * Whether to append a slash to the end of the url (path). + * + * @default datatypeBoolean(fakerCore) + */ + appendSlash?: boolean; + /** + * The protocol to use. + * + * @default 'https' + */ + protocol?: HTTPProtocolType; + } = {} +): string { + const { appendSlash = boolean(fakerCore), protocol = 'https' } = options; + return `${protocol}://${domainName(fakerCore)}${appendSlash ? '/' : ''}`; +} diff --git a/src/modules/internet/user-agent.ts b/src/modules/internet/user-agent.ts new file mode 100644 index 00000000000..75d301581c5 --- /dev/null +++ b/src/modules/internet/user-agent.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random user agent string. + * + * @param fakerCore The FakerCore to use. + * + * @example + * userAgent(fakerCore) + * // 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1 like Mac OS X) AppleWebKit/537.19.86 (KHTML, like Gecko) Version/18_3 Mobile/15E148 Safari/598.43' + * + * @since 2.0.1 + */ +export function userAgent(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.internet.user_agent_pattern + ); +} diff --git a/src/modules/internet/username.ts b/src/modules/internet/username.ts new file mode 100644 index 00000000000..b164921e009 --- /dev/null +++ b/src/modules/internet/username.ts @@ -0,0 +1,96 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; +import { firstName as personFirstName } from '../person/first-name'; +import { lastName as personLastName } from '../person/last-name'; +import { charMapping } from './_char-mappings'; + +/** + * Generates a username using the given person's name as base. + * The resulting username may use neither, one or both of the names provided. + * This will always return a plain ASCII string. + * Some basic stripping of accents and transliteration of characters will be done. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.firstName The optional first name to use. If not specified, a random one will be chosen. + * @param options.lastName The optional last name to use. If not specified, a random one will be chosen. + * + * @see displayName(fakerCore): For generating an Unicode display name. + * + * @example + * username(fakerCore) // 'Nettie_Zboncak40' + * username(fakerCore, { firstName: 'Jeanne' }) // 'Jeanne98' + * username(fakerCore, { firstName: 'Jeanne' }) // 'Jeanne.Smith98' + * username(fakerCore, { firstName: 'Jeanne', lastName: 'Doe'}) // 'Jeanne_Doe98' + * username(fakerCore, { firstName: 'John', lastName: 'Doe' }) // 'John.Doe' + * username(fakerCore, { firstName: 'Hélene', lastName: 'Müller' }) // 'Helene_Muller11' + * username(fakerCore, { firstName: 'Фёдор', lastName: 'Достоевский' }) // 'Fedor.Dostoevskii50' + * username(fakerCore, { firstName: '大羽', lastName: '陳' }) // 'hlzp8d.tpv45' - note neither name is used + * + * @since 9.1.0 + */ +export function username( + fakerCore: FakerCore, + options: { + /** + * The optional first name to use. + * + * @default personFirstName(fakerCore) + */ + firstName?: string; + /** + * The optional last name to use. + * + * @default personLastName(fakerCore) + */ + lastName?: string; + } = {} +): string { + const { + firstName = personFirstName(fakerCore), + lastName = personLastName(fakerCore), + lastName: hasLastName, + } = options; + + const separator = arrayElement(fakerCore, ['.', '_']); + const disambiguator = int(fakerCore, 99); + const strategies: Array<() => string> = [ + () => `${firstName}${separator}${lastName}${disambiguator}`, + () => `${firstName}${separator}${lastName}`, + ]; + if (!hasLastName) { + strategies.push(() => `${firstName}${disambiguator}`); + } + + let result = arrayElement(fakerCore, strategies)(); + + // There may still be non-ascii characters in the result. + // First remove simple accents etc + result = result + .normalize('NFKD') //for example è decomposes to as e + ̀ + .replaceAll(/[\u0300-\u036F]/g, ''); // removes combining marks + + result = [...result] + .map((char) => { + // If we have a mapping for this character, (for Cyrillic, Greek etc) use it + if (charMapping[char]) { + return charMapping[char]; + } + + const charCode = char.codePointAt(0) ?? Number.NaN; + + if (charCode < 0x80) { + // Keep ASCII characters + return char; + } + + // Final fallback return the Unicode char code value for Chinese, Japanese, Korean etc, base-36 encoded + return charCode.toString(36); + }) + .join(''); + result = result.replaceAll("'", ''); + result = result.replaceAll(' ', ''); + + return result; +} diff --git a/src/modules/location/building-number.ts b/src/modules/location/building-number.ts new file mode 100644 index 00000000000..261136b6074 --- /dev/null +++ b/src/modules/location/building-number.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random building number. + * + * @param fakerCore The FakerCore to use. + * + * @example + * buildingNumber(fakerCore) // '379' + * + * @since 8.0.0 + */ +export function buildingNumber(fakerCore: FakerCore): string { + return arrayElement( + fakerCore, + fakerCore.locale.location.building_number + ).replaceAll(/#+/g, (m) => + numeric(fakerCore, { + length: m.length, + allowLeadingZeros: false, + }) + ); +} diff --git a/src/modules/location/cardinal-direction.ts b/src/modules/location/cardinal-direction.ts new file mode 100644 index 00000000000..ffbb8042788 --- /dev/null +++ b/src/modules/location/cardinal-direction.ts @@ -0,0 +1,35 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random cardinal direction (north, east, south, west). + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.abbreviated If true this will return abbreviated directions (N, E, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * cardinalDirection(fakerCore) // 'North' + * cardinalDirection(fakerCore, { abbreviated: true }) // 'W' + * + * @since 8.0.0 + */ +export function cardinalDirection( + fakerCore: FakerCore, + options: { + /** + * If true this will return abbreviated directions (N, E, etc). + * Otherwise this will return the long name. + * + * @default false + */ + abbreviated?: boolean; + } = {} +): string { + const { abbreviated = false } = options; + const direction = fakerCore.locale.location.direction; + const data = abbreviated ? direction.cardinal_abbr : direction.cardinal; + + return arrayElement(fakerCore, data); +} diff --git a/src/modules/location/city.ts b/src/modules/location/city.ts new file mode 100644 index 00000000000..8f0f57696c7 --- /dev/null +++ b/src/modules/location/city.ts @@ -0,0 +1,19 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random localized city name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * city(fakerCore) // 'East Jarretmouth' + * fakerDE.location.city() // 'Bad Lilianadorf' + * + * @since 8.0.0 + */ +export function city(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.location.city_pattern + ); +} diff --git a/src/modules/location/continent.ts b/src/modules/location/continent.ts new file mode 100644 index 00000000000..da1a17f9c42 --- /dev/null +++ b/src/modules/location/continent.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random continent name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * continent(fakerCore) // 'Asia' + * + * @since 9.1.0 + */ +export function continent(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.location.continent); +} diff --git a/src/modules/location/country-code.ts b/src/modules/location/country-code.ts new file mode 100644 index 00000000000..4974fdb75e0 --- /dev/null +++ b/src/modules/location/country-code.ts @@ -0,0 +1,65 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random [ISO_3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) country code. + * + * @param fakerCore The FakerCore to use. + * @param options The code to return or an options object. + * @param options.variant The variant to return. Can be one of: + * + * - `'alpha-2'` (two-letter code) + * - `'alpha-3'` (three-letter code) + * - `'numeric'` (numeric code) + * + * Defaults to `'alpha-2'`. + * + * @example + * countryCode(fakerCore) // 'SJ' + * countryCode(fakerCore, 'alpha-2') // 'GA' + * countryCode(fakerCore, 'alpha-3') // 'TJK' + * countryCode(fakerCore, 'numeric') // '528' + * + * @since 8.0.0 + */ +export function countryCode( + fakerCore: FakerCore, + options: + | 'alpha-2' + | 'alpha-3' + | 'numeric' + | { + /** + * The code to return. + * Can be either `'alpha-2'` (two-letter code), + * `'alpha-3'` (three-letter code) + * or `'numeric'` (numeric code). + * + * @default 'alpha-2' + */ + variant?: 'alpha-2' | 'alpha-3' | 'numeric'; + } = {} +): string { + if (typeof options === 'string') { + options = { variant: options }; + } + + const { variant = 'alpha-2' } = options; + const key = (() => { + switch (variant) { + case 'numeric': { + return 'numeric'; + } + + case 'alpha-3': { + return 'alpha3'; + } + + case 'alpha-2': { + return 'alpha2'; + } + } + })(); + + return arrayElement(fakerCore, fakerCore.locale.location.country_code)[key]; +} diff --git a/src/modules/location/country.ts b/src/modules/location/country.ts new file mode 100644 index 00000000000..7cfa7da2a7a --- /dev/null +++ b/src/modules/location/country.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random country name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * country(fakerCore) // 'Greece' + * + * @since 8.0.0 + */ +export function country(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.location.country); +} diff --git a/src/modules/location/county.ts b/src/modules/location/county.ts new file mode 100644 index 00000000000..bfd341e1ccf --- /dev/null +++ b/src/modules/location/county.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random localized county, or other equivalent second-level administrative entity for the locale's country such as a district or department. + * + * @param fakerCore The FakerCore to use. + * + * @example + * fakerEN_GB.location.county() // 'Cambridgeshire' + * fakerEN_US.location.county() // 'Monroe County' + * + * @since 8.0.0 + */ +export function county(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.location.county); +} diff --git a/src/modules/location/direction.ts b/src/modules/location/direction.ts new file mode 100644 index 00000000000..cab1224aef5 --- /dev/null +++ b/src/modules/location/direction.ts @@ -0,0 +1,37 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random direction (cardinal and ordinal; northwest, east, etc). + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.abbreviated If true this will return abbreviated directions (NW, E, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * direction(fakerCore) // 'Northeast' + * direction(fakerCore, { abbreviated: true }) // 'SW' + * + * @since 8.0.0 + */ +export function direction( + fakerCore: FakerCore, + options: { + /** + * If true this will return abbreviated directions (NW, E, etc). + * Otherwise this will return the long name. + * + * @default false + */ + abbreviated?: boolean; + } = {} +): string { + const { abbreviated = false } = options; + const direction = fakerCore.locale.location.direction; + const data = abbreviated + ? [...direction.cardinal_abbr, ...direction.ordinal_abbr] + : [...direction.cardinal, ...direction.ordinal]; + + return arrayElement(fakerCore, data); +} diff --git a/src/modules/location/index.ts b/src/modules/location/index.ts index 98bd7879c6c..1f341221d71 100644 --- a/src/modules/location/index.ts +++ b/src/modules/location/index.ts @@ -1,26 +1,28 @@ -import { FakerError } from '../../errors/faker-error'; import type { Faker } from '../../faker'; import { SimpleModuleBase } from '../../internal/module-base'; - -/** - * Represents a language with its full name, 2 character ISO 639-1 code, and 3 character ISO 639-2 code. - */ -export interface Language { - /** - * The full name for the language (e.g. `English`). - */ - name: string; - - /** - * The 2 character [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) code. - */ - alpha2: string; - - /** - * The 3 character [ISO 639-2](https://en.wikipedia.org/wiki/ISO_639-2) code. - */ - alpha3: string; -} +import { buildingNumber as locationBuildingNumber } from './building-number'; +import { cardinalDirection as locationCardinalDirection } from './cardinal-direction'; +import { city as locationCity } from './city'; +import { continent as locationContinent } from './continent'; +import { country as locationCountry } from './country'; +import { countryCode as locationCountryCode } from './country-code'; +import { county as locationCounty } from './county'; +import { direction as locationDirection } from './direction'; +import type { Language } from './language'; +import { language as locationLanguage } from './language'; +import { latitude as locationLatitude } from './latitude'; +import { longitude as locationLongitude } from './longitude'; +import { nearbyGPSCoordinate as locationNearbyGPSCoordinate } from './nearby-gpscoordinate'; +import { ordinalDirection as locationOrdinalDirection } from './ordinal-direction'; +import { postalAddress as locationPostalAddress } from './postal-address'; +import { secondaryAddress as locationSecondaryAddress } from './secondary-address'; +import { state as locationState } from './state'; +import { street as locationStreet } from './street'; +import { streetAddress as locationStreetAddress } from './street-address'; +import { timeZone as locationTimeZone } from './time-zone'; +import { zipCode as locationZipCode } from './zip-code'; + +export type { Language } from './language'; /** * Module with location functions that don't require localized data @@ -64,9 +66,7 @@ export class SimpleLocationModule extends SimpleModuleBase { precision?: number; } = {} ): number { - const { max = 90, min = -90, precision = 4 } = options; - - return this.faker.number.float({ min, max, fractionDigits: precision }); + return locationLatitude(this.faker.fakerCore, options); } /** @@ -107,9 +107,7 @@ export class SimpleLocationModule extends SimpleModuleBase { precision?: number; } = {} ): number { - const { max = 180, min = -180, precision = 4 } = options; - - return this.faker.number.float({ max, min, fractionDigits: precision }); + return locationLongitude(this.faker.fakerCore, options); } /** @@ -148,49 +146,7 @@ export class SimpleLocationModule extends SimpleModuleBase { isMetric?: boolean; } = {} ): [latitude: number, longitude: number] { - const { origin, radius = 10, isMetric = false } = options; - - // If there is no origin, the best we can do is return a random GPS coordinate. - if (origin == null) { - return [this.latitude(), this.longitude()]; - } - - const angleRadians = this.faker.number.float({ - max: 2 * Math.PI, - fractionDigits: 5, - }); // in ° radians - - const radiusMetric = isMetric ? radius : radius * 1.60934; // in km - const errorCorrection = 0.995; // avoid float issues - const distanceInKm = - this.faker.number.float({ - max: radiusMetric, - fractionDigits: 3, - }) * errorCorrection; // in km - - /** - * The distance in km per degree for earth. - */ - const kmPerDegree = 40_000 / 360; // in km/° - - const distanceInDegree = distanceInKm / kmPerDegree; // in ° - - const coordinate: [latitude: number, longitude: number] = [ - origin[0] + Math.sin(angleRadians) * distanceInDegree, - origin[1] + Math.cos(angleRadians) * distanceInDegree, - ]; - - // Box latitude [-90°, 90°] - coordinate[0] = coordinate[0] % 180; - if (coordinate[0] < -90 || coordinate[0] > 90) { - coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0]; - coordinate[1] += 180; - } - - // Box longitude [-180°, 180°] - coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180; - - return [coordinate[0], coordinate[1]]; + return locationNearbyGPSCoordinate(this.faker.fakerCore, options); } } @@ -249,33 +205,7 @@ export class LocationModule extends SimpleLocationModule { format?: string; } = {} ): string { - if (typeof options === 'string') { - options = { format: options }; - } - - const { state } = options; - - if (state != null) { - const zipPattern = - this.faker.definitions.location.postcode_by_state[state]; - - if (zipPattern == null) { - throw new FakerError( - `No zip code definition found for state "${state}"` - ); - } - - return this.faker.helpers.fake(zipPattern); - } - - let { format = this.faker.definitions.location.postcode } = options; - if (typeof format === 'string') { - format = [format]; - } - - format = this.faker.helpers.arrayElement(format); - - return this.faker.helpers.replaceSymbols(format); + return locationZipCode(this.faker.fakerCore, options); } /** @@ -288,9 +218,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ city(): string { - return this.faker.helpers.fake( - this.faker.definitions.location.city_pattern - ); + return locationCity(this.faker.fakerCore); } /** @@ -302,14 +230,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ buildingNumber(): string { - return this.faker.helpers - .arrayElement(this.faker.definitions.location.building_number) - .replaceAll(/#+/g, (m) => - this.faker.string.numeric({ - length: m.length, - allowLeadingZeros: false, - }) - ); + return locationBuildingNumber(this.faker.fakerCore); } /** @@ -321,9 +242,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ street(): string { - return this.faker.helpers.fake( - this.faker.definitions.location.street_pattern - ); + return locationStreet(this.faker.fakerCore); } /** @@ -352,16 +271,7 @@ export class LocationModule extends SimpleLocationModule { useFullAddress?: boolean; } = {} ): string { - if (typeof options === 'boolean') { - options = { useFullAddress: options }; - } - - const { useFullAddress } = options; - - const formats = this.faker.definitions.location.street_address; - const format = formats[useFullAddress ? 'full' : 'normal']; - - return this.faker.helpers.fake(format); + return locationStreetAddress(this.faker.fakerCore, options); } /** @@ -391,9 +301,7 @@ export class LocationModule extends SimpleLocationModule { * @since 10.5.0 */ postalAddress(): string { - return this.faker.helpers.fake( - this.faker.definitions.location.postal_address - ); + return locationPostalAddress(this.faker.fakerCore); } /** @@ -406,14 +314,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ secondaryAddress(): string { - return this.faker.helpers - .fake(this.faker.definitions.location.secondary_address) - .replaceAll(/#+/g, (m) => - this.faker.string.numeric({ - length: m.length, - allowLeadingZeros: false, - }) - ); + return locationSecondaryAddress(this.faker.fakerCore); } /** @@ -426,9 +327,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ county(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.location.county - ); + return locationCounty(this.faker.fakerCore); } /** @@ -440,9 +339,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ country(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.location.country - ); + return locationCountry(this.faker.fakerCore); } /** @@ -454,9 +351,7 @@ export class LocationModule extends SimpleLocationModule { * @since 9.1.0 */ continent(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.location.continent - ); + return locationContinent(this.faker.fakerCore); } /** @@ -496,30 +391,7 @@ export class LocationModule extends SimpleLocationModule { variant?: 'alpha-2' | 'alpha-3' | 'numeric'; } = {} ): string { - if (typeof options === 'string') { - options = { variant: options }; - } - - const { variant = 'alpha-2' } = options; - const key = (() => { - switch (variant) { - case 'numeric': { - return 'numeric'; - } - - case 'alpha-3': { - return 'alpha3'; - } - - case 'alpha-2': { - return 'alpha2'; - } - } - })(); - - return this.faker.helpers.arrayElement( - this.faker.definitions.location.country_code - )[key]; + return locationCountryCode(this.faker.fakerCore, options); } /** @@ -551,12 +423,7 @@ export class LocationModule extends SimpleLocationModule { abbreviated?: boolean; } = {} ): string { - const { abbreviated = false } = options; - const data = abbreviated - ? this.faker.definitions.location.state_abbr - : this.faker.definitions.location.state; - - return this.faker.helpers.arrayElement(data); + return locationState(this.faker.fakerCore, options); } /** @@ -583,13 +450,7 @@ export class LocationModule extends SimpleLocationModule { abbreviated?: boolean; } = {} ): string { - const { abbreviated = false } = options; - const direction = this.faker.definitions.location.direction; - const data = abbreviated - ? [...direction.cardinal_abbr, ...direction.ordinal_abbr] - : [...direction.cardinal, ...direction.ordinal]; - - return this.faker.helpers.arrayElement(data); + return locationDirection(this.faker.fakerCore, options); } /** @@ -616,11 +477,7 @@ export class LocationModule extends SimpleLocationModule { abbreviated?: boolean; } = {} ): string { - const { abbreviated = false } = options; - const direction = this.faker.definitions.location.direction; - const data = abbreviated ? direction.cardinal_abbr : direction.cardinal; - - return this.faker.helpers.arrayElement(data); + return locationCardinalDirection(this.faker.fakerCore, options); } /** @@ -647,11 +504,7 @@ export class LocationModule extends SimpleLocationModule { abbreviated?: boolean; } = {} ): string { - const { abbreviated = false } = options; - const direction = this.faker.definitions.location.direction; - const data = abbreviated ? direction.ordinal_abbr : direction.ordinal; - - return this.faker.helpers.arrayElement(data); + return locationOrdinalDirection(this.faker.fakerCore, options); } /** @@ -668,9 +521,7 @@ export class LocationModule extends SimpleLocationModule { * @since 8.0.0 */ timeZone(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.location.time_zone - ); + return locationTimeZone(this.faker.fakerCore); } /** @@ -689,8 +540,6 @@ export class LocationModule extends SimpleLocationModule { * @since 9.4.0 */ language(): Language { - return this.faker.helpers.arrayElement( - this.faker.definitions.location.language - ); + return locationLanguage(this.faker.fakerCore); } } diff --git a/src/modules/location/language.ts b/src/modules/location/language.ts new file mode 100644 index 00000000000..0b79b180f34 --- /dev/null +++ b/src/modules/location/language.ts @@ -0,0 +1,43 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Represents a language with its full name, 2 character ISO 639-1 code, and 3 character ISO 639-2 code. + */ +export interface Language { + /** + * The full name for the language (e.g. `English`). + */ + name: string; + + /** + * The 2 character [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) code. + */ + alpha2: string; + + /** + * The 3 character [ISO 639-2](https://en.wikipedia.org/wiki/ISO_639-2) code. + */ + alpha3: string; +} + +/** + * Returns a random spoken language. + * + * @param fakerCore The FakerCore to use. + * + * @see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) + * @see [ISO 639-2](https://en.wikipedia.org/wiki/ISO_639-2) + * @see [ISO 639-2 Language Code List](https://www.loc.gov/standards/iso639-2/php/code_list.php) + * + * @example + * language(fakerCore) // { alpha2: 'de', alpha3: 'deu', name: 'German' } + * language(fakerCore).name // German + * language(fakerCore).alpha2 // de + * language(fakerCore).alpha3 // deu + * + * @since 9.4.0 + */ +export function language(fakerCore: FakerCore): Language { + return arrayElement(fakerCore, fakerCore.locale.location.language); +} diff --git a/src/modules/location/latitude.ts b/src/modules/location/latitude.ts new file mode 100644 index 00000000000..4b1efa13d34 --- /dev/null +++ b/src/modules/location/latitude.ts @@ -0,0 +1,47 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; + +/** + * Generates a random latitude. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.max The upper bound for the latitude to generate. Defaults to `90`. + * @param options.min The lower bound for the latitude to generate. Defaults to `-90`. + * @param options.precision The number of decimal points of precision for the latitude. Defaults to `4`. + * + * @example + * latitude(fakerCore) // -30.9501 + * latitude(fakerCore, { max: 10 }) // 5.7225 + * latitude(fakerCore, { max: 10, min: -10 }) // -9.6273 + * latitude(fakerCore, { max: 10, min: -10, precision: 5 }) // 2.68452 + * + * @since 8.0.0 + */ +export function latitude( + fakerCore: FakerCore, + options: { + /** + * The upper bound for the latitude to generate. + * + * @default 90 + */ + max?: number; + /** + * The lower bound for the latitude to generate. + * + * @default -90 + */ + min?: number; + /** + * The number of decimal points of precision for the latitude. + * + * @default 4 + */ + precision?: number; + } = {} +): number { + const { max = 90, min = -90, precision = 4 } = options; + + return float(fakerCore, { min, max, fractionDigits: precision }); +} diff --git a/src/modules/location/longitude.ts b/src/modules/location/longitude.ts new file mode 100644 index 00000000000..de3c4991134 --- /dev/null +++ b/src/modules/location/longitude.ts @@ -0,0 +1,47 @@ +import type { FakerCore } from '../../core'; +import { float } from '../number/float'; + +/** + * Generates a random longitude. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.max The upper bound for the longitude to generate. Defaults to `180`. + * @param options.min The lower bound for the longitude to generate. Defaults to `-180`. + * @param options.precision The number of decimal points of precision for the longitude. Defaults to `4`. + * + * @example + * longitude(fakerCore) // -30.9501 + * longitude(fakerCore, { max: 10 }) // 5.7225 + * longitude(fakerCore, { max: 10, min: -10 }) // -9.6273 + * longitude(fakerCore, { max: 10, min: -10, precision: 5 }) // 2.68452 + * + * @since 8.0.0 + */ +export function longitude( + fakerCore: FakerCore, + options: { + /** + * The upper bound for the longitude to generate. + * + * @default 180 + */ + max?: number; + /** + * The lower bound for the longitude to generate. + * + * @default -180 + */ + min?: number; + /** + * The number of decimal points of precision for the longitude. + * + * @default 4 + */ + precision?: number; + } = {} +): number { + const { max = 180, min = -180, precision = 4 } = options; + + return float(fakerCore, { max, min, fractionDigits: precision }); +} diff --git a/src/modules/location/nearby-gpscoordinate.ts b/src/modules/location/nearby-gpscoordinate.ts new file mode 100644 index 00000000000..42191c1f621 --- /dev/null +++ b/src/modules/location/nearby-gpscoordinate.ts @@ -0,0 +1,87 @@ +import type { FakerCore } from '../../core'; +import { latitude } from '../location/latitude'; +import { longitude } from '../location/longitude'; +import { float } from '../number/float'; + +/** + * Generates a random GPS coordinate within the specified radius from the given coordinate. + * + * @param fakerCore The FakerCore to use. + * @param options The options for generating a GPS coordinate. + * @param options.origin The original coordinate to get a new coordinate close to. + * If no coordinate is given, a random one will be chosen. + * @param options.radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`. + * @param options.isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`. + * + * @example + * nearbyGPSCoordinate(fakerCore) // [ 33.8475, -170.5953 ] + * nearbyGPSCoordinate(fakerCore, { origin: [33, -170] }) // [ 33.0165, -170.0636 ] + * nearbyGPSCoordinate(fakerCore, { origin: [33, -170], radius: 1000, isMetric: true }) // [ 37.9163, -179.2408 ] + * + * @since 8.0.0 + */ +export function nearbyGPSCoordinate( + fakerCore: FakerCore, + options: { + /** + * The original coordinate to get a new coordinate close to. + */ + origin?: [latitude: number, longitude: number]; + /** + * The maximum distance from the given coordinate to the new coordinate. + * + * @default 10 + */ + radius?: number; + /** + * If `true` assume the radius to be in kilometers. If `false` for miles. + * + * @default false + */ + isMetric?: boolean; + } = {} +): [latitude: number, longitude: number] { + const { origin, radius = 10, isMetric = false } = options; + + // If there is no origin, the best we can do is return a random GPS coordinate. + if (origin == null) { + return [latitude(fakerCore), longitude(fakerCore)]; + } + + const angleRadians = float(fakerCore, { + max: 2 * Math.PI, + fractionDigits: 5, + }); // in ° radians + + const radiusMetric = isMetric ? radius : radius * 1.60934; // in km + const errorCorrection = 0.995; // avoid float issues + const distanceInKm = + float(fakerCore, { + max: radiusMetric, + fractionDigits: 3, + }) * errorCorrection; // in km + + /** + * The distance in km per degree for earth. + */ + const kmPerDegree = 40_000 / 360; // in km/° + + const distanceInDegree = distanceInKm / kmPerDegree; // in ° + + const coordinate: [latitude: number, longitude: number] = [ + origin[0] + Math.sin(angleRadians) * distanceInDegree, + origin[1] + Math.cos(angleRadians) * distanceInDegree, + ]; + + // Box latitude [-90°, 90°] + coordinate[0] = coordinate[0] % 180; + if (coordinate[0] < -90 || coordinate[0] > 90) { + coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0]; + coordinate[1] += 180; + } + + // Box longitude [-180°, 180°] + coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180; + + return [coordinate[0], coordinate[1]]; +} diff --git a/src/modules/location/ordinal-direction.ts b/src/modules/location/ordinal-direction.ts new file mode 100644 index 00000000000..efe0cc1f30c --- /dev/null +++ b/src/modules/location/ordinal-direction.ts @@ -0,0 +1,35 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random ordinal direction (northwest, southeast, etc). + * + * @param fakerCore The FakerCore to use. + * @param options Whether to use abbreviated or an options object. + * @param options.abbreviated If true this will return abbreviated directions (NW, SE, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * ordinalDirection(fakerCore) // 'Northeast' + * ordinalDirection(fakerCore, { abbreviated: true }) // 'SW' + * + * @since 8.0.0 + */ +export function ordinalDirection( + fakerCore: FakerCore, + options: { + /** + * If true this will return abbreviated directions (NW, SE, etc). + * Otherwise this will return the long name. + * + * @default false + */ + abbreviated?: boolean; + } = {} +): string { + const { abbreviated = false } = options; + const direction = fakerCore.locale.location.direction; + const data = abbreviated ? direction.ordinal_abbr : direction.ordinal; + + return arrayElement(fakerCore, data); +} diff --git a/src/modules/location/postal-address.ts b/src/modules/location/postal-address.ts new file mode 100644 index 00000000000..4285178d972 --- /dev/null +++ b/src/modules/location/postal-address.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random localized full postal address, which may include a street address, secondary address, city, state, and zip code. To ensure you get locale-specific address formats, use a localized Faker instance. + * + * @param fakerCore The FakerCore to use. + * + * @example + * postalAddress(fakerCore) + * // 'Apt. 980 + * // 0917 O'Conner Estates + * // West Shannonview + * // Michigan + * // 82180' + * + * fakerEN_US.location.postalAddress() + * // '0917 O'Conner Estates, Apt. 980 + * // West Shannonview, MI 82180' + * + * fakerEN_GB.location.postalAddress() + * // '79 Bogan Corner + * // Castle Zemlakborough + * // Dumfries and Galloway + * // ZH17 2SD' + * + * fakerZH_CN.location.postalAddress() + * // '广东省贵原市门路19号' + * + * @since 10.5.0 + */ +export function postalAddress(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.location.postal_address + ); +} diff --git a/src/modules/location/secondary-address.ts b/src/modules/location/secondary-address.ts new file mode 100644 index 00000000000..32f9bdd2de4 --- /dev/null +++ b/src/modules/location/secondary-address.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; +import { numeric } from '../string/numeric'; + +/** + * Generates a random localized secondary address. This refers to a specific location at a given address + * such as an apartment or room number. + * + * @param fakerCore The FakerCore to use. + * + * @example + * secondaryAddress(fakerCore) // 'Apt. 861' + * + * @since 8.0.0 + */ +export function secondaryAddress(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers + .fake(fakerCore.locale.location.secondary_address) + .replaceAll(/#+/g, (m) => + numeric(fakerCore, { + length: m.length, + allowLeadingZeros: false, + }) + ); +} diff --git a/src/modules/location/state.ts b/src/modules/location/state.ts new file mode 100644 index 00000000000..4052c59e74d --- /dev/null +++ b/src/modules/location/state.ts @@ -0,0 +1,41 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random localized state, or other equivalent first-level administrative entity for the locale's country such as a province or region. + * Generally, these are the ISO 3166-2 subdivisions for a country. + * If a locale doesn't correspond to one specific country, the method may return ISO 3166-2 subdivisions from one or more countries that uses that language. For example, the `ar` locale includes subdivisions from Arabic-speaking countries, such as Tunisia, Algeria, Syria, Lebanon, etc. + * For historical compatibility reasons, the default `en` locale only includes states in the United States (identical to `en_US`). However, you can use other English locales, such as `en_IN`, `en_GB`, and `en_AU`, if needed. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.abbreviated If true this will return abbreviated first-level administrative entity names. + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * state(fakerCore) // 'Mississippi' + * fakerEN_CA.location.state() // 'Saskatchewan' + * fakerDE.location.state() // 'Nordrhein-Westfalen' + * state(fakerCore, { abbreviated: true }) // 'LA' + * + * @since 8.0.0 + */ +export function state( + fakerCore: FakerCore, + options: { + /** + * If true this will return abbreviated first-level administrative entity names. + * Otherwise this will return the long name. + * + * @default false + */ + abbreviated?: boolean; + } = {} +): string { + const { abbreviated = false } = options; + const data = abbreviated + ? fakerCore.locale.location.state_abbr + : fakerCore.locale.location.state; + + return arrayElement(fakerCore, data); +} diff --git a/src/modules/location/street-address.ts b/src/modules/location/street-address.ts new file mode 100644 index 00000000000..5a78cc94997 --- /dev/null +++ b/src/modules/location/street-address.ts @@ -0,0 +1,42 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random localized street address. + * + * @param fakerCore The FakerCore to use. + * @param options Whether to use a full address or an options object. + * @param options.useFullAddress When true this will generate a full address. + * Otherwise it will just generate a street address. + * + * @example + * streetAddress(fakerCore) // '0917 O'Conner Estates' + * streetAddress(fakerCore, false) // '34830 Erdman Hollow' + * streetAddress(fakerCore, true) // '3393 Ronny Way Apt. 742' + * streetAddress(fakerCore, { useFullAddress: true }) // '7917 Miller Park Apt. 410' + * + * @since 8.0.0 + */ +export function streetAddress( + fakerCore: FakerCore, + options: + | boolean + | { + /** + * When true this will generate a full address. + * Otherwise it will just generate a street address. + */ + useFullAddress?: boolean; + } = {} +): string { + if (typeof options === 'boolean') { + options = { useFullAddress: options }; + } + + const { useFullAddress } = options; + + const formats = fakerCore.locale.location.street_address; + const format = formats[useFullAddress ? 'full' : 'normal']; + + return new Faker(fakerCore).helpers.fake(format); +} diff --git a/src/modules/location/street.ts b/src/modules/location/street.ts new file mode 100644 index 00000000000..e626782d561 --- /dev/null +++ b/src/modules/location/street.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random localized street name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * street(fakerCore) // 'Schroeder Isle' + * + * @since 8.0.0 + */ +export function street(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.location.street_pattern + ); +} diff --git a/src/modules/location/time-zone.ts b/src/modules/location/time-zone.ts new file mode 100644 index 00000000000..3dc1b417853 --- /dev/null +++ b/src/modules/location/time-zone.ts @@ -0,0 +1,21 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random IANA time zone relevant to this locale. + * + * The returned time zone is tied to the current locale. + * + * @param fakerCore The FakerCore to use. + * + * @see [IANA Time Zone Database](https://www.iana.org/time-zones) + * @see dateTimeZone(fakerCore): For generating a random time zone from all available time zones. + * + * @example + * timeZone(fakerCore) // 'Pacific/Guam' + * + * @since 8.0.0 + */ +export function timeZone(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.location.time_zone); +} diff --git a/src/modules/location/zip-code.ts b/src/modules/location/zip-code.ts new file mode 100644 index 00000000000..f0fa03c7c75 --- /dev/null +++ b/src/modules/location/zip-code.ts @@ -0,0 +1,72 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { Faker } from '../../faker'; +import { arrayElement } from '../helpers/array-element'; +import { replaceSymbols } from '../helpers/replace-symbols'; + +/** + * Generates random zip code from specified format. If format is not specified, + * the locale's zip format is used. + * + * @param fakerCore The FakerCore to use. + * @param options The format used to generate the zip code or an options object. + * @param options.state The state to generate the zip code for. + * If the current locale does not have a corresponding `postcode_by_state` definition, an error is thrown. + * @param options.format The optional format used to generate the zip code. + * By default, a random format is used from the locale zip formats. + * This won't be used if the state option is specified. + * + * @see helpersReplaceSymbols(fakerCore): For more information about how the pattern is used. + * + * @example + * zipCode(fakerCore) // '17839' + * zipCode(fakerCore, '####') // '6925' + * + * @since 8.0.0 + */ +export function zipCode( + fakerCore: FakerCore, + options: + | string + | { + /** + * The state to generate the zip code for. + * + * If the current locale does not have a corresponding `postcode_by_state` definition, an error is thrown. + */ + state?: string; + /** + * The optional format used to generate the zip code. + * + * This won't be used if the state option is specified. + * + * @default faker.definitions.location.postcode + */ + format?: string; + } = {} +): string { + if (typeof options === 'string') { + options = { format: options }; + } + + const { state } = options; + + if (state != null) { + const zipPattern = fakerCore.locale.location.postcode_by_state[state]; + + if (zipPattern == null) { + throw new FakerError(`No zip code definition found for state "${state}"`); + } + + return new Faker(fakerCore).helpers.fake(zipPattern); + } + + let { format = fakerCore.locale.location.postcode } = options; + if (typeof format === 'string') { + format = [format]; + } + + format = arrayElement(fakerCore, format); + + return replaceSymbols(fakerCore, format); +} diff --git a/src/modules/lorem/index.ts b/src/modules/lorem/index.ts index 880fb1a61c6..5643a4b482b 100644 --- a/src/modules/lorem/index.ts +++ b/src/modules/lorem/index.ts @@ -1,5 +1,13 @@ import { ModuleBase } from '../../internal/module-base'; -import { filterWordListByLength } from '../word/filter-word-list-by-length'; +import { lines as loremLines } from './lines'; +import { paragraph as loremParagraph } from './paragraph'; +import { paragraphs as loremParagraphs } from './paragraphs'; +import { sentence as loremSentence } from './sentence'; +import { sentences as loremSentences } from './sentences'; +import { slug as loremSlug } from './slug'; +import { text as loremText } from './text'; +import { word as loremWord } from './word'; +import { words as loremWords } from './words'; /** * Module to generate random texts and words. @@ -75,16 +83,7 @@ export class LoremModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.lorem.word, - }) - ); + return loremWord(this.faker.fakerCore, options); } /** @@ -115,9 +114,7 @@ export class LoremModule extends ModuleBase { max: number; } = 3 ): string { - return this.faker.helpers - .multiple(() => this.word(), { count: wordCount }) - .join(' '); + return loremWords(this.faker.fakerCore, wordCount); } /** @@ -148,8 +145,7 @@ export class LoremModule extends ModuleBase { max: number; } = { min: 3, max: 10 } ): string { - const sentence = this.words(wordCount); - return `${sentence.charAt(0).toUpperCase() + sentence.substring(1)}.`; + return loremSentence(this.faker.fakerCore, wordCount); } /** @@ -180,8 +176,7 @@ export class LoremModule extends ModuleBase { max: number; } = 3 ): string { - const words = this.words(wordCount); - return this.faker.helpers.slugify(words); + return loremSlug(this.faker.fakerCore, wordCount); } /** @@ -217,9 +212,7 @@ export class LoremModule extends ModuleBase { } = { min: 2, max: 6 }, separator: string = ' ' ): string { - return this.faker.helpers - .multiple(() => this.sentence(), { count: sentenceCount }) - .join(separator); + return loremSentences(this.faker.fakerCore, sentenceCount, separator); } /** @@ -250,7 +243,7 @@ export class LoremModule extends ModuleBase { max: number; } = 3 ): string { - return this.sentences(sentenceCount); + return loremParagraph(this.faker.fakerCore, sentenceCount); } /** @@ -300,9 +293,7 @@ export class LoremModule extends ModuleBase { } = 3, separator: string = '\n' ): string { - return this.faker.helpers - .multiple(() => this.paragraph(), { count: paragraphCount }) - .join(separator); + return loremParagraphs(this.faker.fakerCore, paragraphCount, separator); } /** @@ -320,17 +311,7 @@ export class LoremModule extends ModuleBase { * @since 3.1.0 */ text(): string { - const methods: Array = [ - 'sentence', - 'sentences', - 'paragraph', - 'paragraphs', - 'lines', - ]; - - const method = this.faker.helpers.arrayElement(methods); - - return this[method](); + return loremText(this.faker.fakerCore); } /** @@ -374,6 +355,6 @@ export class LoremModule extends ModuleBase { max: number; } = { min: 1, max: 5 } ): string { - return this.sentences(lineCount, '\n'); + return loremLines(this.faker.fakerCore, lineCount); } } diff --git a/src/modules/lorem/lines.ts b/src/modules/lorem/lines.ts new file mode 100644 index 00000000000..6da0ed15b72 --- /dev/null +++ b/src/modules/lorem/lines.ts @@ -0,0 +1,48 @@ +import type { FakerCore } from '../../core'; +import { sentences } from '../lorem/sentences'; + +/** + * Generates the given number lines of lorem separated by `'\n'`. + * + * @param fakerCore The FakerCore to use. + * @param lineCount The number of lines to generate. Defaults to a random number between `1` and `5`. + * @param lineCount.min The minimum number of lines to generate. Defaults to `1`. + * @param lineCount.max The maximum number of lines to generate. Defaults to `5`. + * + * @example + * lines(fakerCore) + * // 'Rerum quia aliquam pariatur explicabo sint minima eos. + * // Voluptatem repellat consequatur deleniti qui quibusdam harum cumque. + * // Enim eveniet a qui. + * // Consectetur velit eligendi animi nostrum veritatis.' + * + * lines(fakerCore) + * // 'Soluta deserunt eos quam reiciendis libero autem enim nam ut. + * // Voluptate aut aut.' + * + * lines(fakerCore, 2) + * // 'Quod quas nam quis impedit aut consequuntur. + * // Animi dolores aspernatur.' + * + * lines(fakerCore, { min: 1, max: 3 }) + * // 'Error dolorem natus quos eum consequatur necessitatibus.' + * + * @since 3.1.0 + */ +export function lines( + fakerCore: FakerCore, + lineCount: + | number + | { + /** + * The minimum number of lines to generate. + */ + min: number; + /** + * The maximum number of lines to generate. + */ + max: number; + } = { min: 1, max: 5 } +): string { + return sentences(fakerCore, lineCount, '\n'); +} diff --git a/src/modules/lorem/paragraph.ts b/src/modules/lorem/paragraph.ts new file mode 100644 index 00000000000..f35933beb85 --- /dev/null +++ b/src/modules/lorem/paragraph.ts @@ -0,0 +1,35 @@ +import type { FakerCore } from '../../core'; +import { sentences } from '../lorem/sentences'; + +/** + * Generates a paragraph with the given number of sentences. + * + * @param fakerCore The FakerCore to use. + * @param sentenceCount The number of sentences to generate. Defaults to `3`. + * @param sentenceCount.min The minimum number of sentences to generate. + * @param sentenceCount.max The maximum number of sentences to generate. + * + * @example + * paragraph(fakerCore) // 'Non architecto nam unde sint. Ex tenetur dolor facere optio aut consequatur. Ea laudantium reiciendis repellendus.' + * paragraph(fakerCore, 2) // 'Animi possimus nemo consequuntur ut ea et tempore unde qui. Quis corporis esse occaecati.' + * paragraph(fakerCore, { min: 1, max: 3 }) // 'Quis doloribus necessitatibus sint. Rerum accusamus impedit corporis porro.' + * + * @since 2.0.1 + */ +export function paragraph( + fakerCore: FakerCore, + sentenceCount: + | number + | { + /** + * The minimum number of sentences to generate. + */ + min: number; + /** + * The maximum number of sentences to generate. + */ + max: number; + } = 3 +): string { + return sentences(fakerCore, sentenceCount); +} diff --git a/src/modules/lorem/paragraphs.ts b/src/modules/lorem/paragraphs.ts new file mode 100644 index 00000000000..021b11221ba --- /dev/null +++ b/src/modules/lorem/paragraphs.ts @@ -0,0 +1,57 @@ +import type { FakerCore } from '../../core'; +import { multiple } from '../helpers/multiple'; +import { paragraph } from '../lorem/paragraph'; + +/** + * Generates the given number of paragraphs. + * + * @param fakerCore The FakerCore to use. + * @param paragraphCount The number of paragraphs to generate. Defaults to `3`. + * @param paragraphCount.min The minimum number of paragraphs to generate. + * @param paragraphCount.max The maximum number of paragraphs to generate. + * @param separator The separator to use. Defaults to `'\n'`. + * + * @example + * paragraphs(fakerCore) + * // 'Beatae voluptatem dicta et assumenda fugit eaque quidem consequatur. Fuga unde provident. Id reprehenderit soluta facilis est laborum laborum. Illum aut non ut. Est nulla rem ipsa. + * // Voluptatibus quo pariatur est. Temporibus deleniti occaecati pariatur nemo est molestias voluptas. Doloribus commodi et et exercitationem vel et. Omnis inventore cum aut amet. + * // Sapiente deleniti et. Ducimus maiores eum. Rem dolorem itaque aliquam.' + * + * paragraphs(fakerCore, 5) + * // 'Quia hic sunt ducimus expedita quo impedit soluta. Quam impedit et ipsum optio. Unde dolores nulla nobis vero et aspernatur officiis. + * // Aliquam dolorem temporibus dolores voluptatem voluptatem qui nostrum quia. Sit hic facilis rerum eius. Beatae doloribus nesciunt iste ipsum. + * // Natus nam eum nulla voluptas molestiae fuga libero nihil voluptatibus. Sed quam numquam eum ipsam temporibus eaque ut et. Enim quas debitis quasi quis. Vitae et vitae. + * // Repellat voluptatem est laborum illo harum sed reprehenderit aut. Quo sit et. Exercitationem blanditiis totam velit ad dicta placeat. + * // Rerum non eum incidunt amet quo. Eaque laborum ut. Recusandae illo ab distinctio veritatis. Cum quis architecto ad maxime a.' + * + * paragraphs(fakerCore, 2, '
\n') + * // 'Eos magnam aut qui accusamus. Sapiente quas culpa totam excepturi. Blanditiis totam distinctio occaecati dignissimos cumque atque qui officiis.
+ * // Nihil quis vel consequatur. Blanditiis commodi deserunt sunt animi dolorum. A optio porro hic dolorum fugit aut et sint voluptas. Minima ad sed ipsa est non dolores.' + * + * paragraphs(fakerCore, { min: 1, max: 3 }) + * // 'Eum nam fugiat laudantium. + * // Dignissimos tempore porro necessitatibus commodi nam. + * // Veniam at commodi iste perferendis totam dolorum corporis ipsam.' + * + * @since 2.0.1 + */ +export function paragraphs( + fakerCore: FakerCore, + paragraphCount: + | number + | { + /** + * The minimum number of paragraphs to generate. + */ + min: number; + /** + * The maximum number of paragraphs to generate. + */ + max: number; + } = 3, + separator: string = '\n' +): string { + return multiple(fakerCore, () => paragraph(fakerCore), { + count: paragraphCount, + }).join(separator); +} diff --git a/src/modules/lorem/sentence.ts b/src/modules/lorem/sentence.ts new file mode 100644 index 00000000000..adb2c3feb80 --- /dev/null +++ b/src/modules/lorem/sentence.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { words } from '../lorem/words'; + +/** + * Generates a space separated list of words beginning with a capital letter and ending with a period. + * + * @param fakerCore The FakerCore to use. + * @param wordCount The number of words, that should be in the sentence. Defaults to a random number between `3` and `10`. + * @param wordCount.min The minimum number of words to generate. Defaults to `3`. + * @param wordCount.max The maximum number of words to generate. Defaults to `10`. + * + * @example + * sentence(fakerCore) // 'Voluptatum cupiditate suscipit autem eveniet aut dolorem aut officiis distinctio.' + * sentence(fakerCore, 5) // 'Laborum voluptatem officiis est et.' + * sentence(fakerCore, { min: 3, max: 5 }) // 'Fugiat repellendus nisi.' + * + * @since 2.0.1 + */ +export function sentence( + fakerCore: FakerCore, + wordCount: + | number + | { + /** + * The minimum number of words to generate. + */ + min: number; + /** + * The maximum number of words to generate. + */ + max: number; + } = { min: 3, max: 10 } +): string { + const sentence = words(fakerCore, wordCount); + return `${sentence.charAt(0).toUpperCase() + sentence.substring(1)}.`; +} diff --git a/src/modules/lorem/sentences.ts b/src/modules/lorem/sentences.ts new file mode 100644 index 00000000000..e7105677111 --- /dev/null +++ b/src/modules/lorem/sentences.ts @@ -0,0 +1,43 @@ +import type { FakerCore } from '../../core'; +import { multiple } from '../helpers/multiple'; +import { sentence } from '../lorem/sentence'; + +/** + * Generates the given number of sentences. + * + * @param fakerCore The FakerCore to use. + * @param sentenceCount The number of sentences to generate. Defaults to a random number between `2` and `6`. + * @param sentenceCount.min The minimum number of sentences to generate. Defaults to `2`. + * @param sentenceCount.max The maximum number of sentences to generate. Defaults to `6`. + * @param separator The separator to add between sentences. Defaults to `' '`. + * + * @example + * sentences(fakerCore) // 'Iste molestiae incidunt aliquam possimus reprehenderit eum corrupti. Deleniti modi voluptatem nostrum ut esse.' + * sentences(fakerCore, 2) // 'Maxime vel numquam quibusdam. Dignissimos ex molestias quos aut molestiae quam nihil occaecati maiores.' + * sentences(fakerCore, 2, '\n') + * // 'Et rerum a unde tempora magnam sit nisi. + * // Et perspiciatis ipsam omnis.' + * sentences(fakerCore, { min: 1, max: 3 }) // 'Placeat ex natus tenetur repellendus repellendus iste. Optio nostrum veritatis.' + * + * @since 2.0.1 + */ +export function sentences( + fakerCore: FakerCore, + sentenceCount: + | number + | { + /** + * The minimum number of sentences to generate. + */ + min: number; + /** + * The maximum number of sentences to generate. + */ + max: number; + } = { min: 2, max: 6 }, + separator: string = ' ' +): string { + return multiple(fakerCore, () => sentence(fakerCore), { + count: sentenceCount, + }).join(separator); +} diff --git a/src/modules/lorem/slug.ts b/src/modules/lorem/slug.ts new file mode 100644 index 00000000000..c2f4ce51de5 --- /dev/null +++ b/src/modules/lorem/slug.ts @@ -0,0 +1,37 @@ +import type { FakerCore } from '../../core'; +import { slugify } from '../helpers/slugify'; +import { words as loremWords } from '../lorem/words'; + +/** + * Generates a slugified text consisting of the given number of hyphen separated words. + * + * @param fakerCore The FakerCore to use. + * @param wordCount The number of words to generate. Defaults to `3`. + * @param wordCount.min The minimum number of words to generate. + * @param wordCount.max The maximum number of words to generate. + * + * @example + * slug(fakerCore) // 'dolores-illo-est' + * slug(fakerCore, 5) // 'delectus-totam-iusto-itaque-placeat' + * slug(fakerCore, { min: 1, max: 3 }) // 'illo-ratione' + * + * @since 4.0.0 + */ +export function slug( + fakerCore: FakerCore, + wordCount: + | number + | { + /** + * The minimum number of words to generate. + */ + min: number; + /** + * The maximum number of words to generate. + */ + max: number; + } = 3 +): string { + const words = loremWords(fakerCore, wordCount); + return slugify(fakerCore, words); +} diff --git a/src/modules/lorem/text.ts b/src/modules/lorem/text.ts new file mode 100644 index 00000000000..6e9b355ef5f --- /dev/null +++ b/src/modules/lorem/text.ts @@ -0,0 +1,35 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { lines } from './lines'; +import { paragraph } from './paragraph'; +import { paragraphs } from './paragraphs'; +import { sentence } from './sentence'; +import { sentences } from './sentences'; + +/** + * Generates a random text based on a random lorem method. + * + * @param fakerCore The FakerCore to use. + * + * @example + * text(fakerCore) // 'Doloribus autem non quis vero quia.' + * text(fakerCore) + * // 'Rerum eum reiciendis id ipsa hic dolore aut laborum provident. + * // Quis beatae quis corporis veritatis corrupti ratione delectus sapiente ut. + * // Quis ut dolor dolores facilis possimus tempore voluptates. + * // Iure nam officia optio cumque. + * // Dolor tempora iusto.' + * + * @since 3.1.0 + */ +export function text(fakerCore: FakerCore): string { + const method = arrayElement(fakerCore, [ + sentence, + sentences, + paragraph, + paragraphs, + lines, + ]); + + return method(fakerCore); +} diff --git a/src/modules/lorem/word.ts b/src/modules/lorem/word.ts new file mode 100644 index 00000000000..20080a18fdf --- /dev/null +++ b/src/modules/lorem/word.ts @@ -0,0 +1,80 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from '../word/_filter-word-list-by-length'; + +/** + * Generates a word of a specified length. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'any-length'`. + * + * @example + * word(fakerCore) // 'temporibus' + * word(fakerCore, 5) // 'velit' + * word(fakerCore, { strategy: 'shortest' }) // 'a' + * word(fakerCore, { length: { min: 5, max: 7 }, strategy: 'fail' }) // 'quaerat' + * + * @since 3.1.0 + */ +export function word( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + * + * @default 1 + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'any-length' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.lorem.word, + }) + ); +} diff --git a/src/modules/lorem/words.ts b/src/modules/lorem/words.ts new file mode 100644 index 00000000000..b94e01afe5f --- /dev/null +++ b/src/modules/lorem/words.ts @@ -0,0 +1,38 @@ +import type { FakerCore } from '../../core'; +import { multiple } from '../helpers/multiple'; +import { word } from '../lorem/word'; + +/** + * Generates a space separated list of words. + * + * @param fakerCore The FakerCore to use. + * @param wordCount The number of words to generate. Defaults to `3`. + * @param wordCount.min The minimum number of words to generate. + * @param wordCount.max The maximum number of words to generate. + * + * @example + * words(fakerCore) // 'qui praesentium pariatur' + * words(fakerCore, 10) // 'debitis consectetur voluptatem non doloremque ipsum autem totam eum ratione' + * words(fakerCore, { min: 1, max: 3 }) // 'tenetur error cum' + * + * @since 2.0.1 + */ +export function words( + fakerCore: FakerCore, + wordCount: + | number + | { + /** + * The minimum number of words to generate. + */ + min: number; + /** + * The maximum number of words to generate. + */ + max: number; + } = 3 +): string { + return multiple(fakerCore, () => word(fakerCore), { count: wordCount }).join( + ' ' + ); +} diff --git a/src/modules/music/album.ts b/src/modules/music/album.ts new file mode 100644 index 00000000000..aa4b7f7f938 --- /dev/null +++ b/src/modules/music/album.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random album name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * album(fakerCore) // '1989' + * + * @since 9.0.0 + */ +export function album(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.music.album); +} diff --git a/src/modules/music/artist.ts b/src/modules/music/artist.ts new file mode 100644 index 00000000000..0ebd0bcf508 --- /dev/null +++ b/src/modules/music/artist.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random artist name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * artist(fakerCore) // 'The Beatles' + * + * @since 9.0.0 + */ +export function artist(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.music.artist); +} diff --git a/src/modules/music/genre.ts b/src/modules/music/genre.ts new file mode 100644 index 00000000000..fb48577ed8d --- /dev/null +++ b/src/modules/music/genre.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random music genre. + * + * @param fakerCore The FakerCore to use. + * + * @example + * genre(fakerCore) // 'Reggae' + * + * @since 5.2.0 + */ +export function genre(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.music.genre); +} diff --git a/src/modules/music/index.ts b/src/modules/music/index.ts index e3d56715b1c..0d291bc663f 100644 --- a/src/modules/music/index.ts +++ b/src/modules/music/index.ts @@ -1,4 +1,8 @@ import { ModuleBase } from '../../internal/module-base'; +import { album as musicAlbum } from './album'; +import { artist as musicArtist } from './artist'; +import { genre as musicGenre } from './genre'; +import { songName as musicSongName } from './song-name'; /** * Module to generate music related entries. @@ -27,7 +31,7 @@ export class MusicModule extends ModuleBase { * @since 9.0.0 */ album(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.music.album); + return musicAlbum(this.faker.fakerCore); } /** @@ -39,7 +43,7 @@ export class MusicModule extends ModuleBase { * @since 9.0.0 */ artist(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.music.artist); + return musicArtist(this.faker.fakerCore); } /** @@ -51,7 +55,7 @@ export class MusicModule extends ModuleBase { * @since 5.2.0 */ genre(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.music.genre); + return musicGenre(this.faker.fakerCore); } /** @@ -63,8 +67,6 @@ export class MusicModule extends ModuleBase { * @since 7.1.0 */ songName(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.music.song_name - ); + return musicSongName(this.faker.fakerCore); } } diff --git a/src/modules/music/song-name.ts b/src/modules/music/song-name.ts new file mode 100644 index 00000000000..8e9268cddeb --- /dev/null +++ b/src/modules/music/song-name.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random song name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * songName(fakerCore) // 'White Christmas' + * + * @since 7.1.0 + */ +export function songName(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.music.song_name); +} 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/person/_select-definition.ts b/src/modules/person/_select-definition.ts new file mode 100644 index 00000000000..e1e3bb2082f --- /dev/null +++ b/src/modules/person/_select-definition.ts @@ -0,0 +1,61 @@ +import type { FakerCore } from '../../core'; +import type { PersonEntryDefinition } from '../../definitions'; +import { arrayElement } from '../helpers/array-element'; +import { weightedArrayElement } from '../helpers/weighted-array-element'; +import type { SexType } from './sex-type'; +import { sexType } from './sex-type'; + +/** + * Select a definition based on given sex. + * + * @param fakerCore The FakerCore to use. + * @param sex The sex to select the definition for. + * @param personEntry The definitions to select from. + * + * @returns Definition based on given sex. + */ +export function selectDefinition( + fakerCore: FakerCore, + sex: SexType = sexType(fakerCore), + personEntry: PersonEntryDefinition +): T[] { + const { generic, female, male } = personEntry; + + if (sex === 'generic') { + return ( + generic ?? + arrayElement(fakerCore, [female, male]) ?? + // The last statement should never happen at run time. At this point in time, + // the entry will satisfy at least (generic || (female && male)). + // TS is not able to infer the type correctly. + [] + ); + } + + const binary = sex === 'female' ? female : male; + + if (binary != null) { + if (generic != null) { + return weightedArrayElement(fakerCore, [ + { + weight: 3 * Math.sqrt(binary.length), + value: binary, + }, + { + weight: Math.sqrt(generic.length), + value: generic, + }, + ]); + } + + return binary; + } + + return ( + generic ?? + // The last statement should never happen at run time. At this point in time, + // the entry will satisfy at least (generic || (female && male)). + // TS is not able to infer the type correctly. + [] + ); +} diff --git a/src/modules/person/bio.ts b/src/modules/person/bio.ts new file mode 100644 index 00000000000..5c1ebe0fcdf --- /dev/null +++ b/src/modules/person/bio.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Returns a random short biography + * + * @param fakerCore The FakerCore to use. + * + * @example + * bio(fakerCore) // 'oatmeal advocate, veteran 🐠' + * + * @since 8.0.0 + */ +export function bio(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake(fakerCore.locale.person.bio_pattern); +} diff --git a/src/modules/person/first-name.ts b/src/modules/person/first-name.ts new file mode 100644 index 00000000000..34fa6b02465 --- /dev/null +++ b/src/modules/person/first-name.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { selectDefinition } from './_select-definition'; +import type { SexType } from './sex-type'; + +/** + * Returns a random first name. + * + * @param fakerCore The FakerCore to use. + * @param sex The optional sex to use. + * Can be either `'female'` or `'male'`. + * + * @example + * firstName(fakerCore) // 'Antwan' + * firstName(fakerCore, 'female') // 'Victoria' + * firstName(fakerCore, 'male') // 'Tom' + * + * @since 8.0.0 + */ +export function firstName(fakerCore: FakerCore, sex?: SexType): string { + return arrayElement( + fakerCore, + selectDefinition(fakerCore, sex, fakerCore.locale.person.first_name) + ); +} diff --git a/src/modules/person/full-name.ts b/src/modules/person/full-name.ts new file mode 100644 index 00000000000..e5ee496a682 --- /dev/null +++ b/src/modules/person/full-name.ts @@ -0,0 +1,73 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { mustache } from '../helpers/mustache'; +import { weightedArrayElement } from '../helpers/weighted-array-element'; +import { firstName as personFirstName } from '../person/first-name'; +import { lastName as personLastName } from '../person/last-name'; +import { middleName } from '../person/middle-name'; +import { prefix } from '../person/prefix'; +import { suffix } from '../person/suffix'; +import type { SexType } from './sex-type'; +import { Sex } from './sex-type'; + +/** + * Generates a random full name. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.firstName The optional first name to use. If not specified a random one will be chosen. + * @param options.lastName The optional last name to use. If not specified a random one will be chosen. + * @param options.sex The optional sex to use. Can be either `'female'` or `'male'`. + * + * @example + * fullName(fakerCore) // 'Allen Brown' + * fullName(fakerCore, { firstName: 'Joann' }) // 'Joann Osinski' + * fullName(fakerCore, { firstName: 'Marcella', sex: 'female' }) // 'Mrs. Marcella Huels' + * fullName(fakerCore, { lastName: 'Beer' }) // 'Mr. Alfonso Beer' + * fullName(fakerCore, { sex: 'male' }) // 'Fernando Schaefer' + * + * @since 8.0.0 + */ +export function fullName( + fakerCore: FakerCore, + options: { + /** + * The optional first name to use. If not specified a random one will be chosen. + * + * @default firstName(fakerCore, sex) + */ + firstName?: string; + /** + * The optional last name to use. If not specified a random one will be chosen. + * + * @default lastName(fakerCore, sex) + */ + lastName?: string; + /** + * The optional sex to use. Can be either `'female'` or `'male'`. + * + * @default helpersArrayElement(fakerCore, [Sex.Female, Sex.Male]) + */ + sex?: SexType; + } = {} +): string { + const { + sex = arrayElement(fakerCore, [Sex.Female, Sex.Male]), + firstName = personFirstName(fakerCore, sex), + lastName = personLastName(fakerCore, sex), + } = options; + + const fullNamePattern: string = weightedArrayElement( + fakerCore, + fakerCore.locale.person.name + ); + + const fullName = mustache(fakerCore, fullNamePattern, { + 'person.prefix': () => prefix(fakerCore, sex), + 'person.firstName': () => firstName, + 'person.middleName': () => middleName(fakerCore, sex), + 'person.lastName': () => lastName, + 'person.suffix': () => suffix(fakerCore), + }); + return fullName; +} diff --git a/src/modules/person/gender.ts b/src/modules/person/gender.ts new file mode 100644 index 00000000000..5efb40951d7 --- /dev/null +++ b/src/modules/person/gender.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random gender. + * + * @param fakerCore The FakerCore to use. + * + * @see sex(fakerCore): For generating a binary-gender value. + * + * @example + * gender(fakerCore) // 'Trans*Man' + * + * @since 8.0.0 + */ +export function gender(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.gender); +} diff --git a/src/modules/person/index.ts b/src/modules/person/index.ts index ef5b0274bbc..15355e1eabf 100644 --- a/src/modules/person/index.ts +++ b/src/modules/person/index.ts @@ -1,84 +1,23 @@ -import type { PersonEntryDefinition } from '../../definitions/person'; -import type { Faker } from '../../faker'; import { ModuleBase } from '../../internal/module-base'; +import { bio as personBio } from './bio'; +import { firstName as personFirstName } from './first-name'; +import { fullName as personFullName } from './full-name'; +import { gender as personGender } from './gender'; +import { jobArea as personJobArea } from './job-area'; +import { jobDescriptor as personJobDescriptor } from './job-descriptor'; +import { jobTitle as personJobTitle } from './job-title'; +import { jobType as personJobType } from './job-type'; +import { lastName as personLastName } from './last-name'; +import { middleName as personMiddleName } from './middle-name'; +import { prefix as personPrefix } from './prefix'; +import { sex as personSex } from './sex'; +import type { SexType } from './sex-type'; +import { sexType as personSexType } from './sex-type'; +import { suffix as personSuffix } from './suffix'; +import { zodiacSign as personZodiacSign } from './zodiac-sign'; -/** - * The enum for values corresponding to a person's sex. - */ -export enum Sex { - /** - * Is used for values that are primarily attributable to only females. - */ - Female = 'female', - /** - * Is used for values that cannot clearly be attributed to a specific sex or are used for both sexes. - */ - Generic = 'generic', - /** - * Is used for values that are primarily attributable to only males. - */ - Male = 'male', -} - -/** - * The parameter type for values corresponding to a person's sex. - */ -export type SexType = `${Sex}`; - -/** - * Select a definition based on given sex. - * - * @param faker Faker instance. - * @param sex Sex. - * @param personEntry Definitions. - * - * @returns Definition based on given sex. - */ -function selectDefinition( - faker: Faker, - sex: SexType = faker.person.sexType(), - personEntry: PersonEntryDefinition -): T[] { - const { generic, female, male } = personEntry; - - if (sex === 'generic') { - return ( - generic ?? - faker.helpers.arrayElement([female, male]) ?? - // The last statement should never happen at run time. At this point in time, - // the entry will satisfy at least (generic || (female && male)). - // TS is not able to infer the type correctly. - [] - ); - } - - const binary = sex === 'female' ? female : male; - - if (binary != null) { - if (generic != null) { - return faker.helpers.weightedArrayElement([ - { - weight: 3 * Math.sqrt(binary.length), - value: binary, - }, - { - weight: Math.sqrt(generic.length), - value: generic, - }, - ]); - } - - return binary; - } - - return ( - generic ?? - // The last statement should never happen at run time. At this point in time, - // the entry will satisfy at least (generic || (female && male)). - // TS is not able to infer the type correctly. - [] - ); -} +export { Sex } from './sex-type'; +export type { SexType } from './sex-type'; /** * Module to generate people's personal information such as names and job titles. Prior to Faker 8.0.0, this module was known as `faker.name`. @@ -114,13 +53,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ firstName(sex?: SexType): string { - return this.faker.helpers.arrayElement( - selectDefinition( - this.faker, - sex, - this.faker.definitions.person.first_name - ) - ); + return personFirstName(this.faker.fakerCore, sex); } /** @@ -137,17 +70,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ lastName(sex?: SexType): string { - const patterns = this.faker.definitions.raw.person?.last_name_pattern; - if (patterns != null) { - const pattern = this.faker.helpers.weightedArrayElement( - selectDefinition(this.faker, sex, patterns) - ); - return this.faker.helpers.fake(pattern); - } - - return this.faker.helpers.arrayElement( - selectDefinition(this.faker, sex, this.faker.definitions.person.last_name) - ); + return personLastName(this.faker.fakerCore, sex); } /** @@ -164,13 +87,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ middleName(sex?: SexType): string { - return this.faker.helpers.arrayElement( - selectDefinition( - this.faker, - sex, - this.faker.definitions.person.middle_name - ) - ); + return personMiddleName(this.faker.fakerCore, sex); } /** @@ -207,29 +124,12 @@ export class PersonModule extends ModuleBase { /** * The optional sex to use. Can be either `'female'` or `'male'`. * - * @default faker.helpers.arrayElement(['female', 'male']) + * @default faker.helpers.arrayElement([Sex.Female, Sex.Male]) */ sex?: SexType; } = {} ): string { - const { - sex = this.faker.helpers.arrayElement([Sex.Female, Sex.Male]), - firstName = this.firstName(sex), - lastName = this.lastName(sex), - } = options; - - const fullNamePattern: string = this.faker.helpers.weightedArrayElement( - this.faker.definitions.person.name - ); - - const fullName = this.faker.helpers.mustache(fullNamePattern, { - 'person.prefix': () => this.prefix(sex), - 'person.firstName': () => firstName, - 'person.middleName': () => this.middleName(sex), - 'person.lastName': () => lastName, - 'person.suffix': () => this.suffix(), - }); - return fullName; + return personFullName(this.faker.fakerCore, options); } /** @@ -243,9 +143,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ gender(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.person.gender - ); + return personGender(this.faker.fakerCore); } /** @@ -263,7 +161,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ sex(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.person.sex); + return personSex(this.faker.fakerCore); } /** @@ -294,13 +192,7 @@ export class PersonModule extends ModuleBase { includeGeneric?: boolean; } = {} ): SexType { - const { includeGeneric = false } = options; - - if (includeGeneric) { - return this.faker.helpers.enumValue(Sex); - } - - return this.faker.helpers.arrayElement([Sex.Female, Sex.Male]); + return personSexType(this.faker.fakerCore, options); } /** @@ -312,7 +204,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ bio(): string { - return this.faker.helpers.fake(this.faker.definitions.person.bio_pattern); + return personBio(this.faker.fakerCore); } /** @@ -328,9 +220,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ prefix(sex?: SexType): string { - return this.faker.helpers.arrayElement( - selectDefinition(this.faker, sex, this.faker.definitions.person.prefix) - ); + return personPrefix(this.faker.fakerCore, sex); } /** @@ -342,10 +232,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ suffix(): string { - // TODO @Shinigami92 2022-03-21: Add female_suffix and male_suffix - return this.faker.helpers.arrayElement( - this.faker.definitions.person.suffix - ); + return personSuffix(this.faker.fakerCore); } /** @@ -357,9 +244,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ jobTitle(): string { - return this.faker.helpers.fake( - this.faker.definitions.person.job_title_pattern - ); + return personJobTitle(this.faker.fakerCore); } /** @@ -371,9 +256,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ jobDescriptor(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.person.job_descriptor - ); + return personJobDescriptor(this.faker.fakerCore); } /** @@ -385,9 +268,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ jobArea(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.person.job_area - ); + return personJobArea(this.faker.fakerCore); } /** @@ -399,9 +280,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ jobType(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.person.job_type - ); + return personJobType(this.faker.fakerCore); } /** @@ -413,8 +292,6 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ zodiacSign(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.person.western_zodiac_sign - ); + return personZodiacSign(this.faker.fakerCore); } } diff --git a/src/modules/person/job-area.ts b/src/modules/person/job-area.ts new file mode 100644 index 00000000000..bc7cb8d65d0 --- /dev/null +++ b/src/modules/person/job-area.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random job area. + * + * @param fakerCore The FakerCore to use. + * + * @example + * jobArea(fakerCore) // 'Brand' + * + * @since 8.0.0 + */ +export function jobArea(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.job_area); +} diff --git a/src/modules/person/job-descriptor.ts b/src/modules/person/job-descriptor.ts new file mode 100644 index 00000000000..4098271aa4e --- /dev/null +++ b/src/modules/person/job-descriptor.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random job descriptor. + * + * @param fakerCore The FakerCore to use. + * + * @example + * jobDescriptor(fakerCore) // 'Customer' + * + * @since 8.0.0 + */ +export function jobDescriptor(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.job_descriptor); +} diff --git a/src/modules/person/job-title.ts b/src/modules/person/job-title.ts new file mode 100644 index 00000000000..661a96f6d58 --- /dev/null +++ b/src/modules/person/job-title.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; + +/** + * Generates a random job title. + * + * @param fakerCore The FakerCore to use. + * + * @example + * jobTitle(fakerCore) // 'Global Accounts Engineer' + * + * @since 8.0.0 + */ +export function jobTitle(fakerCore: FakerCore): string { + return new Faker(fakerCore).helpers.fake( + fakerCore.locale.person.job_title_pattern + ); +} diff --git a/src/modules/person/job-type.ts b/src/modules/person/job-type.ts new file mode 100644 index 00000000000..2a6dec8a348 --- /dev/null +++ b/src/modules/person/job-type.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Generates a random job type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * jobType(fakerCore) // 'Assistant' + * + * @since 8.0.0 + */ +export function jobType(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.job_type); +} diff --git a/src/modules/person/last-name.ts b/src/modules/person/last-name.ts new file mode 100644 index 00000000000..5a6470cf627 --- /dev/null +++ b/src/modules/person/last-name.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { Faker } from '../../faker'; +import { arrayElement } from '../helpers/array-element'; +import { weightedArrayElement } from '../helpers/weighted-array-element'; +import { selectDefinition } from './_select-definition'; +import type { SexType } from './sex-type'; + +/** + * Returns a random last name. + * + * @param fakerCore The FakerCore to use. + * @param sex The optional sex to use. + * Can be either `'female'` or `'male'`. + * + * @example + * lastName(fakerCore) // 'Hauck' + * lastName(fakerCore, 'female') // 'Grady' + * lastName(fakerCore, 'male') // 'Barton' + * + * @since 8.0.0 + */ +export function lastName(fakerCore: FakerCore, sex?: SexType): string { + const patterns = fakerCore.locale.raw.person?.last_name_pattern; + if (patterns != null) { + const pattern = weightedArrayElement( + fakerCore, + selectDefinition(fakerCore, sex, patterns) + ); + return new Faker(fakerCore).helpers.fake(pattern); + } + + return arrayElement( + fakerCore, + selectDefinition(fakerCore, sex, fakerCore.locale.person.last_name) + ); +} diff --git a/src/modules/person/middle-name.ts b/src/modules/person/middle-name.ts new file mode 100644 index 00000000000..c2b5f2fc05c --- /dev/null +++ b/src/modules/person/middle-name.ts @@ -0,0 +1,25 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { selectDefinition } from './_select-definition'; +import type { SexType } from './sex-type'; + +/** + * Returns a random middle name. + * + * @param fakerCore The FakerCore to use. + * @param sex The optional sex to use. + * Can be either `'female'` or `'male'`. + * + * @example + * middleName(fakerCore) // 'James' + * middleName(fakerCore, 'female') // 'Eloise' + * middleName(fakerCore, 'male') // 'Asher' + * + * @since 8.0.0 + */ +export function middleName(fakerCore: FakerCore, sex?: SexType): string { + return arrayElement( + fakerCore, + selectDefinition(fakerCore, sex, fakerCore.locale.person.middle_name) + ); +} diff --git a/src/modules/person/prefix.ts b/src/modules/person/prefix.ts new file mode 100644 index 00000000000..6301ea4ab26 --- /dev/null +++ b/src/modules/person/prefix.ts @@ -0,0 +1,24 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { selectDefinition } from './_select-definition'; +import type { SexType } from './sex-type'; + +/** + * Returns a random person prefix. + * + * @param fakerCore The FakerCore to use. + * @param sex The optional sex to use. Can be either `'female'` or `'male'`. + * + * @example + * prefix(fakerCore) // 'Miss' + * prefix(fakerCore, 'female') // 'Ms.' + * prefix(fakerCore, 'male') // 'Mr.' + * + * @since 8.0.0 + */ +export function prefix(fakerCore: FakerCore, sex?: SexType): string { + return arrayElement( + fakerCore, + selectDefinition(fakerCore, sex, fakerCore.locale.person.prefix) + ); +} diff --git a/src/modules/person/sex-type.ts b/src/modules/person/sex-type.ts new file mode 100644 index 00000000000..bf733818122 --- /dev/null +++ b/src/modules/person/sex-type.ts @@ -0,0 +1,65 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { enumValue } from '../helpers/enum-value'; + +/** + * The enum for values corresponding to a person's sex. + */ +export enum Sex { + /** + * Is used for values that are primarily attributable to only females. + */ + Female = 'female', + /** + * Is used for values that cannot clearly be attributed to a specific sex or are used for both sexes. + */ + Generic = 'generic', + /** + * Is used for values that are primarily attributable to only males. + */ + Male = 'male', +} + +/** + * The parameter type for values corresponding to a person's sex. + */ +export type SexType = `${Sex}`; + +/** + * Returns a random sex type. The `SexType` is intended to be used in parameters and conditions. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object. + * @param options.includeGeneric Whether `'generic'` should be included in the potential outputs. + * If `false`, this method only returns `'female'` and `'male'`. + * Default is `false`. + * + * @see gender(fakerCore): For generating a gender related value in forms. + * @see sex(fakerCore): For generating a binary-gender value in forms. + * + * @example + * sexType(fakerCore) // Sex.Female + * sexType(fakerCore, { includeGeneric: true }) // Sex.Generic + * + * @since 8.0.0 + */ +export function sexType( + fakerCore: FakerCore, + options: { + /** + * Whether `'generic'` should be included in the potential outputs. + * If `false`, this method only returns `'female'` and `'male'`. + * + * @default false + */ + includeGeneric?: boolean; + } = {} +): SexType { + const { includeGeneric = false } = options; + + if (includeGeneric) { + return enumValue(fakerCore, Sex); + } + + return arrayElement(fakerCore, [Sex.Female, Sex.Male]); +} diff --git a/src/modules/person/sex.ts b/src/modules/person/sex.ts new file mode 100644 index 00000000000..3926b24ef67 --- /dev/null +++ b/src/modules/person/sex.ts @@ -0,0 +1,22 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random sex. + * + * Output of this method is localised, so it should not be used to fill the parameter `sex` + * available in some other modules for example `firstName(fakerCore)`. + * + * @param fakerCore The FakerCore to use. + * + * @see gender(fakerCore): For generating a gender related value. + * @see sexType(fakerCore): For generating a sex value to be used as a parameter. + * + * @example + * sex(fakerCore) // 'female' + * + * @since 8.0.0 + */ +export function sex(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.sex); +} diff --git a/src/modules/person/suffix.ts b/src/modules/person/suffix.ts new file mode 100644 index 00000000000..78fe49fd810 --- /dev/null +++ b/src/modules/person/suffix.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random person suffix. + * + * @param fakerCore The FakerCore to use. + * + * @example + * suffix(fakerCore) // 'DDS' + * + * @since 8.0.0 + */ +export function suffix(fakerCore: FakerCore): string { + // TODO @Shinigami92 2022-03-21: Add female_suffix and male_suffix + return arrayElement(fakerCore, fakerCore.locale.person.suffix); +} diff --git a/src/modules/person/zodiac-sign.ts b/src/modules/person/zodiac-sign.ts new file mode 100644 index 00000000000..82789e958c4 --- /dev/null +++ b/src/modules/person/zodiac-sign.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a random zodiac sign. + * + * @param fakerCore The FakerCore to use. + * + * @example + * zodiacSign(fakerCore) // 'Pisces' + * + * @since 8.0.0 + */ +export function zodiacSign(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.person.western_zodiac_sign); +} diff --git a/src/modules/phone/imei.ts b/src/modules/phone/imei.ts new file mode 100644 index 00000000000..6a512fa7052 --- /dev/null +++ b/src/modules/phone/imei.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { replaceCreditCardSymbols } from '../helpers/replace-credit-card-symbols'; + +/** + * Generates IMEI number. + * + * @param fakerCore The FakerCore to use. + * + * @example + * imei(fakerCore) // '13-850175-913761-7' + * + * @since 6.2.0 + */ +export function imei(fakerCore: FakerCore): string { + return replaceCreditCardSymbols(fakerCore, '##-######-######-L', '#'); +} diff --git a/src/modules/phone/index.ts b/src/modules/phone/index.ts index c55bfeed6e7..2816a753ae5 100644 --- a/src/modules/phone/index.ts +++ b/src/modules/phone/index.ts @@ -1,5 +1,6 @@ import { ModuleBase } from '../../internal/module-base'; -import { legacyReplaceSymbolWithNumber } from '../helpers'; +import { imei as phoneImei } from './imei'; +import { number as phoneNumber } from './number'; /** * Module to generate phone-related data. @@ -41,16 +42,7 @@ export class PhoneModule extends ModuleBase { style?: 'human' | 'national' | 'international' | 'mobile'; } = {} ): string { - const { style = 'human' } = options; - const formats = this.faker.definitions.phone_number.format; - - const definitions = formats[style]; - if (!definitions) { - throw new Error(`No definitions for ${style} in this locale`); - } - - const format = this.faker.helpers.arrayElement(definitions); - return legacyReplaceSymbolWithNumber(this.faker, format); + return phoneNumber(this.faker.fakerCore, options); } /** @@ -62,9 +54,6 @@ export class PhoneModule extends ModuleBase { * @since 6.2.0 */ imei(): string { - return this.faker.helpers.replaceCreditCardSymbols( - '##-######-######-L', - '#' - ); + return phoneImei(this.faker.fakerCore); } } diff --git a/src/modules/phone/number.ts b/src/modules/phone/number.ts new file mode 100644 index 00000000000..ae84c17e326 --- /dev/null +++ b/src/modules/phone/number.ts @@ -0,0 +1,45 @@ +import type { FakerCore } from '../../core'; +import { assertLocaleData } from '../../internal/locale-proxy'; +import { arrayElement } from '../helpers/array-element'; +import { legacyReplaceSymbolWithNumber } from '../helpers/replace-credit-card-symbols'; + +/** + * Generates a random phone number. + * + * @param fakerCore The FakerCore to use. + * @param options Options object + * @param options.style Style of the phone number. Defaults to `'human'`. + * + * @see stringNumeric(fakerCore): For generating a random string of numbers. + * @see helpersFromRegExp(fakerCore): For generating a phone number matching a regular expression. + * + * @example + * number(fakerCore) // '961-770-7727' + * number(fakerCore, { style: 'human' }) // '555.770.7727 x1234' + * number(fakerCore, { style: 'national' }) // '(961) 770-7727' + * number(fakerCore, { style: 'international' }) // '+15551234567' + * fakerEN_GB.phone.number({ style: 'mobile' }) // '07123456789' + * + * @since 7.3.0 + */ +export function number( + fakerCore: FakerCore, + options: { + /** + * Style of the generated phone number: + * - `'human'`: (default) A human-input phone number, e.g. `555-770-7727` or `555.770.7727 x1234` + * - `'national'`: A phone number in a standardized national format, e.g. `(555) 123-4567`. + * - `'international'`: A phone number in the E.123 international format, e.g. `+15551234567` + * - `'mobile'`: In selected locales, provides a number used for mobile phones, e.g. `07123456789` in en_GB. + * + * @default 'human' + */ + style?: 'human' | 'national' | 'international' | 'mobile'; + } = {} +): string { + const { style = 'human' } = options; + const formats = fakerCore.locale.phone_number.format[style]; + assertLocaleData(formats, 'phone_number.format', style); + const format = arrayElement(fakerCore, formats); + return legacyReplaceSymbolWithNumber(fakerCore, format); +} diff --git a/src/modules/science/chemical-element.ts b/src/modules/science/chemical-element.ts new file mode 100644 index 00000000000..630c7c15384 --- /dev/null +++ b/src/modules/science/chemical-element.ts @@ -0,0 +1,36 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * The possible definitions related to elements. + */ +export interface ChemicalElement { + /** + * The symbol for the element (e.g. `'He'`). + */ + symbol: string; + /** + * The name for the element (e.g. `'Cerium'`). + */ + name: string; + /** + * The atomic number for the element (e.g. `52`). + */ + atomicNumber: number; +} + +/** + * Returns a random periodic table element. + * + * @param fakerCore The FakerCore to use. + * + * @example + * chemicalElement(fakerCore) // { symbol: 'H', name: 'Hydrogen', atomicNumber: 1 } + * chemicalElement(fakerCore) // { symbol: 'Xe', name: 'Xenon', atomicNumber: 54 } + * chemicalElement(fakerCore) // { symbol: 'Ce', name: 'Cerium', atomicNumber: 58 } + * + * @since 7.2.0 + */ +export function chemicalElement(fakerCore: FakerCore): ChemicalElement { + return arrayElement(fakerCore, fakerCore.locale.science.chemical_element); +} diff --git a/src/modules/science/index.ts b/src/modules/science/index.ts index 525ff8bf0cc..85e5df12b70 100644 --- a/src/modules/science/index.ts +++ b/src/modules/science/index.ts @@ -1,33 +1,11 @@ import { ModuleBase } from '../../internal/module-base'; +import type { ChemicalElement } from './chemical-element'; +import { chemicalElement as scienceChemicalElement } from './chemical-element'; +import type { Unit } from './unit'; +import { unit as scienceUnit } from './unit'; -/** - * The possible definitions related to elements. - */ -export interface ChemicalElement { - /** - * The symbol for the element (e.g. `'He'`). - */ - symbol: string; - /** - * The name for the element (e.g. `'Cerium'`). - */ - name: string; - /** - * The atomic number for the element (e.g. `52`). - */ - atomicNumber: number; -} - -export interface Unit { - /** - * The long version of the unit (e.g. `meter`). - */ - name: string; - /** - * The short version/abbreviation of the element (e.g. `Pa`). - */ - symbol: string; -} +export type { ChemicalElement } from './chemical-element'; +export type { Unit } from './unit'; /** * Module to generate science related entries. @@ -48,9 +26,7 @@ export class ScienceModule extends ModuleBase { * @since 7.2.0 */ chemicalElement(): ChemicalElement { - return this.faker.helpers.arrayElement( - this.faker.definitions.science.chemical_element - ); + return scienceChemicalElement(this.faker.fakerCore); } /** @@ -64,6 +40,6 @@ export class ScienceModule extends ModuleBase { * @since 7.2.0 */ unit(): Unit { - return this.faker.helpers.arrayElement(this.faker.definitions.science.unit); + return scienceUnit(this.faker.fakerCore); } } diff --git a/src/modules/science/unit.ts b/src/modules/science/unit.ts new file mode 100644 index 00000000000..bbc2c03acb2 --- /dev/null +++ b/src/modules/science/unit.ts @@ -0,0 +1,29 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +export interface Unit { + /** + * The long version of the unit (e.g. `meter`). + */ + name: string; + /** + * The short version/abbreviation of the element (e.g. `Pa`). + */ + symbol: string; +} + +/** + * Returns a random scientific unit. + * + * @param fakerCore The FakerCore to use. + * + * @example + * unit(fakerCore) // { name: 'meter', symbol: 'm' } + * unit(fakerCore) // { name: 'second', symbol: 's' } + * unit(fakerCore) // { name: 'mole', symbol: 'mol' } + * + * @since 7.2.0 + */ +export function unit(fakerCore: FakerCore): Unit { + return arrayElement(fakerCore, fakerCore.locale.science.unit); +} 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/src/modules/system/common-file-ext.ts b/src/modules/system/common-file-ext.ts new file mode 100644 index 00000000000..8c61eba6a27 --- /dev/null +++ b/src/modules/system/common-file-ext.ts @@ -0,0 +1,29 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { fileExt } from '../system/file-ext'; + +const commonMimeTypes = [ + 'application/pdf', + 'audio/mpeg', + 'audio/wav', + 'image/png', + 'image/jpeg', + 'image/gif', + 'video/mp4', + 'video/mpeg', + 'text/html', +]; + +/** + * Returns a commonly used file extension. + * + * @param fakerCore The FakerCore to use. + * + * @example + * commonFileExt(fakerCore) // 'gif' + * + * @since 3.1.0 + */ +export function commonFileExt(fakerCore: FakerCore): string { + return fileExt(fakerCore, arrayElement(fakerCore, commonMimeTypes)); +} diff --git a/src/modules/system/common-file-name.ts b/src/modules/system/common-file-name.ts new file mode 100644 index 00000000000..bc9c5c52ac5 --- /dev/null +++ b/src/modules/system/common-file-name.ts @@ -0,0 +1,24 @@ +import type { FakerCore } from '../../core'; +import { commonFileExt } from '../system/common-file-ext'; +import { fileName as systemFileName } from '../system/file-name'; + +/** + * Returns a random file name with a given extension or a commonly used extension. + * + * @param fakerCore The FakerCore to use. + * @param extension The file extension to use. Empty string is considered to be not set. + * + * @example + * commonFileName(fakerCore) // 'dollar.jpg' + * commonFileName(fakerCore, 'txt') // 'global_borders_wyoming.txt' + * + * @since 3.1.0 + */ +export function commonFileName( + fakerCore: FakerCore, + extension?: string +): string { + const fileName = systemFileName(fakerCore, { extensionCount: 0 }); + + return `${fileName}.${extension || commonFileExt(fakerCore)}`; +} diff --git a/src/modules/system/common-file-type.ts b/src/modules/system/common-file-type.ts new file mode 100644 index 00000000000..370df0f3217 --- /dev/null +++ b/src/modules/system/common-file-type.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +const commonFileTypes = ['video', 'audio', 'image', 'text', 'application']; + +/** + * Returns a commonly used file type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * commonFileType(fakerCore) // 'audio' + * + * @since 3.1.0 + */ +export function commonFileType(fakerCore: FakerCore): string { + return arrayElement(fakerCore, commonFileTypes); +} diff --git a/src/modules/system/cron.ts b/src/modules/system/cron.ts new file mode 100644 index 00000000000..c0bf957160d --- /dev/null +++ b/src/modules/system/cron.ts @@ -0,0 +1,91 @@ +import type { FakerCore } from '../../core'; +import { boolean } from '../datatype/boolean'; +import { arrayElement } from '../helpers/array-element'; +import { int } from '../number/int'; + +const CRON_DAY_OF_WEEK = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', +] as const; + +/** + * Returns a random cron expression. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options to use. + * @param options.includeYear Whether to include a year in the generated expression. Defaults to `false`. + * @param options.includeNonStandard Whether to include a `@yearly`, `@monthly`, `@daily`, etc text labels in the generated expression. Defaults to `false`. + * + * @example + * cron(fakerCore) // '45 23 * * 6' + * cron(fakerCore, { includeYear: true }) // '45 23 * * 6 2067' + * cron(fakerCore, { includeYear: false }) // '45 23 * * 6' + * cron(fakerCore, { includeNonStandard: false }) // '45 23 * * 6' + * cron(fakerCore, { includeNonStandard: true }) // '@yearly' + * + * @since 7.5.0 + */ +export function cron( + fakerCore: FakerCore, + options: { + /** + * Whether to include a year in the generated expression. + * + * @default false + */ + includeYear?: boolean; + /** + * Whether to include a `@yearly`, `@monthly`, `@daily`, etc text labels in the generated expression. + * + * @default false + */ + includeNonStandard?: boolean; + } = {} +): string { + const { includeYear = false, includeNonStandard = false } = options; + + // create the arrays to hold the available values for each component of the expression + const minutes = [int(fakerCore, 59), '*']; + const hours = [int(fakerCore, 23), '*']; + const days = [int(fakerCore, { min: 1, max: 31 }), '*', '?']; + const months = [int(fakerCore, { min: 1, max: 12 }), '*']; + const daysOfWeek = [ + int(fakerCore, 6), + arrayElement(fakerCore, CRON_DAY_OF_WEEK), + '*', + '?', + ]; + const years = [int(fakerCore, { min: 1970, max: 2099 }), '*']; + + const minute = arrayElement(fakerCore, minutes); + const hour = arrayElement(fakerCore, hours); + const day = arrayElement(fakerCore, days); + const month = arrayElement(fakerCore, months); + const dayOfWeek = arrayElement(fakerCore, daysOfWeek); + const year = arrayElement(fakerCore, years); + + // create and return the cron expression string + let standardExpression = `${minute} ${hour} ${day} ${month} ${dayOfWeek}`; + if (includeYear) { + standardExpression += ` ${year}`; + } + + const nonStandardExpressions = [ + '@annually', + '@daily', + '@hourly', + '@monthly', + '@reboot', + '@weekly', + '@yearly', + ]; + + return !includeNonStandard || boolean(fakerCore) + ? standardExpression + : arrayElement(fakerCore, nonStandardExpressions); +} diff --git a/src/modules/system/directory-path.ts b/src/modules/system/directory-path.ts new file mode 100644 index 00000000000..f004ab73c70 --- /dev/null +++ b/src/modules/system/directory-path.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a directory path. + * + * @param fakerCore The FakerCore to use. + * + * @example + * directoryPath(fakerCore) // '/etc/mail' + * + * @since 3.1.0 + */ +export function directoryPath(fakerCore: FakerCore): string { + const paths = fakerCore.locale.system.directory_path; + return arrayElement(fakerCore, paths); +} diff --git a/src/modules/system/file-ext.ts b/src/modules/system/file-ext.ts new file mode 100644 index 00000000000..0ba061457d0 --- /dev/null +++ b/src/modules/system/file-ext.ts @@ -0,0 +1,27 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a file extension. + * + * @param fakerCore The FakerCore to use. + * @param mimeType Valid [mime-type](https://github.com/jshttp/mime-db/blob/master/db.json) + * + * @example + * fileExt(fakerCore) // 'emf' + * fileExt(fakerCore, 'application/json') // 'json' + * + * @since 3.1.0 + */ +export function fileExt(fakerCore: FakerCore, mimeType?: string): string { + const mimeTypes = fakerCore.locale.system.mime_type; + + if (typeof mimeType === 'string') { + return arrayElement(fakerCore, mimeTypes[mimeType].extensions); + } + + const extensionSet = new Set( + Object.values(mimeTypes).flatMap(({ extensions }) => extensions) + ); + return arrayElement(fakerCore, [...extensionSet]); +} diff --git a/src/modules/system/file-name.ts b/src/modules/system/file-name.ts new file mode 100644 index 00000000000..2c6a8315567 --- /dev/null +++ b/src/modules/system/file-name.ts @@ -0,0 +1,55 @@ +import type { FakerCore } from '../../core'; +import { multiple } from '../helpers/multiple'; +import { fileExt } from '../system/file-ext'; +import { words } from '../word/words'; + +/** + * Returns a random file name with extension. + * + * @param fakerCore The FakerCore to use. + * @param options An options object. + * @param options.extensionCount Define how many extensions the file name should have. Defaults to `1`. + * + * @example + * fileName(fakerCore) // 'faithfully_calculating.u8mdn' + * fileName(fakerCore, { extensionCount: 2 }) // 'times_after.swf.ntf' + * fileName(fakerCore, { extensionCount: { min: 1, max: 2 } }) // 'jaywalk_like_ill.osfpvg' + * + * @since 3.1.0 + */ +export function fileName( + fakerCore: FakerCore, + options: { + /** + * Define how many extensions the file name should have. + * + * @default 1 + */ + extensionCount?: + | number + | { + /** + * Minimum number of extensions. + */ + min: number; + /** + * Maximum number of extensions. + */ + max: number; + }; + } = {} +): string { + const { extensionCount = 1 } = options; + + const baseName = words(fakerCore).toLowerCase().replaceAll(/\W/g, '_'); + + const extensionsSuffix = multiple(fakerCore, () => fileExt(fakerCore), { + count: extensionCount, + }).join('.'); + + if (extensionsSuffix.length === 0) { + return baseName; + } + + return `${baseName}.${extensionsSuffix}`; +} diff --git a/src/modules/system/file-path.ts b/src/modules/system/file-path.ts new file mode 100644 index 00000000000..5718b9bdc0a --- /dev/null +++ b/src/modules/system/file-path.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { directoryPath } from '../system/directory-path'; +import { fileName } from '../system/file-name'; + +/** + * Returns a file path. + * + * @param fakerCore The FakerCore to use. + * + * @example + * filePath(fakerCore) // '/usr/local/src/money.dotx' + * + * @since 3.1.0 + */ +export function filePath(fakerCore: FakerCore): string { + return `${directoryPath(fakerCore)}/${fileName(fakerCore)}`; +} diff --git a/src/modules/system/file-type.ts b/src/modules/system/file-type.ts new file mode 100644 index 00000000000..570a0561259 --- /dev/null +++ b/src/modules/system/file-type.ts @@ -0,0 +1,21 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a file type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * fileType(fakerCore) // 'message' + * + * @since 3.1.0 + */ +export function fileType(fakerCore: FakerCore): string { + const mimeTypes = fakerCore.locale.system.mime_type; + + const typeSet = new Set( + Object.keys(mimeTypes).map((key) => key.split('/')[0]) + ); + return arrayElement(fakerCore, [...typeSet]); +} diff --git a/src/modules/system/index.ts b/src/modules/system/index.ts index cfa9c04e6b6..90e3655af2a 100644 --- a/src/modules/system/index.ts +++ b/src/modules/system/index.ts @@ -1,36 +1,20 @@ import { ModuleBase } from '../../internal/module-base'; - -const commonFileTypes = ['video', 'audio', 'image', 'text', 'application']; - -const commonMimeTypes = [ - 'application/pdf', - 'audio/mpeg', - 'audio/wav', - 'image/png', - 'image/jpeg', - 'image/gif', - 'video/mp4', - 'video/mpeg', - 'text/html', -]; - -const commonInterfaceTypes = ['en', 'wl', 'ww'] as const; -const commonInterfaceSchemas = { - index: 'o', - slot: 's', - mac: 'x', - pci: 'p', -} as const; - -const CRON_DAY_OF_WEEK = [ - 'SUN', - 'MON', - 'TUE', - 'WED', - 'THU', - 'FRI', - 'SAT', -] as const; +import { commonFileExt as systemCommonFileExt } from './common-file-ext'; +import { commonFileName as systemCommonFileName } from './common-file-name'; +import { commonFileType as systemCommonFileType } from './common-file-type'; +import { cron as systemCron } from './cron'; +import { directoryPath as systemDirectoryPath } from './directory-path'; +import { fileExt as systemFileExt } from './file-ext'; +import { fileName as systemFileName } from './file-name'; +import { filePath as systemFilePath } from './file-path'; +import { fileType as systemFileType } from './file-type'; +import { mimeType as systemMimeType } from './mime-type'; +import type { + CommonInterfaceSchema, + CommonInterfaceType, +} from './network-interface'; +import { networkInterface as systemNetworkInterface } from './network-interface'; +import { semver as systemSemver } from './semver'; /** * Generates fake data for many computer systems properties. @@ -70,22 +54,7 @@ export class SystemModule extends ModuleBase { }; } = {} ): string { - const { extensionCount = 1 } = options; - - const baseName = this.faker.word - .words() - .toLowerCase() - .replaceAll(/\W/g, '_'); - - const extensionsSuffix = this.faker.helpers - .multiple(() => this.fileExt(), { count: extensionCount }) - .join('.'); - - if (extensionsSuffix.length === 0) { - return baseName; - } - - return `${baseName}.${extensionsSuffix}`; + return systemFileName(this.faker.fakerCore, options); } /** @@ -100,9 +69,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ commonFileName(extension?: string): string { - const fileName = this.fileName({ extensionCount: 0 }); - - return `${fileName}.${extension || this.commonFileExt()}`; + return systemCommonFileName(this.faker.fakerCore, extension); } /** @@ -114,9 +81,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ mimeType(): string { - const mimeTypeKeys = Object.keys(this.faker.definitions.system.mime_type); - - return this.faker.helpers.arrayElement(mimeTypeKeys); + return systemMimeType(this.faker.fakerCore); } /** @@ -128,7 +93,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ commonFileType(): string { - return this.faker.helpers.arrayElement(commonFileTypes); + return systemCommonFileType(this.faker.fakerCore); } /** @@ -140,7 +105,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ commonFileExt(): string { - return this.fileExt(this.faker.helpers.arrayElement(commonMimeTypes)); + return systemCommonFileExt(this.faker.fakerCore); } /** @@ -152,12 +117,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ fileType(): string { - const mimeTypes = this.faker.definitions.system.mime_type; - - const typeSet = new Set( - Object.keys(mimeTypes).map((key) => key.split('/')[0]) - ); - return this.faker.helpers.arrayElement([...typeSet]); + return systemFileType(this.faker.fakerCore); } /** @@ -172,16 +132,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ fileExt(mimeType?: string): string { - const mimeTypes = this.faker.definitions.system.mime_type; - - if (typeof mimeType === 'string') { - return this.faker.helpers.arrayElement(mimeTypes[mimeType].extensions); - } - - const extensionSet = new Set( - Object.values(mimeTypes).flatMap(({ extensions }) => extensions) - ); - return this.faker.helpers.arrayElement([...extensionSet]); + return systemFileExt(this.faker.fakerCore, mimeType); } /** @@ -193,8 +144,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ directoryPath(): string { - const paths = this.faker.definitions.system.directory_path; - return this.faker.helpers.arrayElement(paths); + return systemDirectoryPath(this.faker.fakerCore); } /** @@ -206,7 +156,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ filePath(): string { - return `${this.directoryPath()}/${this.fileName()}`; + return systemFilePath(this.faker.fakerCore); } /** @@ -218,11 +168,7 @@ export class SystemModule extends ModuleBase { * @since 3.1.0 */ semver(): string { - return [ - this.faker.number.int(9), - this.faker.number.int(20), - this.faker.number.int(20), - ].join('.'); + return systemSemver(this.faker.fakerCore); } /** @@ -247,54 +193,16 @@ export class SystemModule extends ModuleBase { * * @default faker.helpers.arrayElement(['en', 'wl', 'ww']) */ - interfaceType?: (typeof commonInterfaceTypes)[number]; + interfaceType?: CommonInterfaceType; /** * The interface schema. Can be one of `index`, `slot`, `mac`, `pci`. * * @default faker.helpers.objectKey(['index' | 'slot' | 'mac' | 'pci']) */ - interfaceSchema?: keyof typeof commonInterfaceSchemas; + interfaceSchema?: CommonInterfaceSchema; } = {} ): string { - const { - interfaceType = this.faker.helpers.arrayElement(commonInterfaceTypes), - interfaceSchema = this.faker.helpers.objectKey(commonInterfaceSchemas), - } = options; - - let suffix: string; - let prefix = ''; - switch (interfaceSchema) { - case 'index': { - suffix = this.faker.string.numeric(); - break; - } - - case 'slot': { - suffix = `${this.faker.string.numeric()}${ - this.faker.helpers.maybe(() => `f${this.faker.string.numeric()}`) ?? - '' - }${this.faker.helpers.maybe(() => `d${this.faker.string.numeric()}`) ?? ''}`; - break; - } - - case 'mac': { - suffix = this.faker.internet.mac(''); - break; - } - - case 'pci': { - prefix = - this.faker.helpers.maybe(() => `P${this.faker.string.numeric()}`) ?? - ''; - suffix = `${this.faker.string.numeric()}s${this.faker.string.numeric()}${ - this.faker.helpers.maybe(() => `f${this.faker.string.numeric()}`) ?? - '' - }${this.faker.helpers.maybe(() => `d${this.faker.string.numeric()}`) ?? ''}`; - break; - } - } - - return `${prefix}${interfaceType}${commonInterfaceSchemas[interfaceSchema]}${suffix}`; + return systemNetworkInterface(this.faker.fakerCore, options); } /** @@ -329,46 +237,6 @@ export class SystemModule extends ModuleBase { includeNonStandard?: boolean; } = {} ): string { - const { includeYear = false, includeNonStandard = false } = options; - - // create the arrays to hold the available values for each component of the expression - const minutes = [this.faker.number.int(59), '*']; - const hours = [this.faker.number.int(23), '*']; - const days = [this.faker.number.int({ min: 1, max: 31 }), '*', '?']; - const months = [this.faker.number.int({ min: 1, max: 12 }), '*']; - const daysOfWeek = [ - this.faker.number.int(6), - this.faker.helpers.arrayElement(CRON_DAY_OF_WEEK), - '*', - '?', - ]; - const years = [this.faker.number.int({ min: 1970, max: 2099 }), '*']; - - const minute = this.faker.helpers.arrayElement(minutes); - const hour = this.faker.helpers.arrayElement(hours); - const day = this.faker.helpers.arrayElement(days); - const month = this.faker.helpers.arrayElement(months); - const dayOfWeek = this.faker.helpers.arrayElement(daysOfWeek); - const year = this.faker.helpers.arrayElement(years); - - // create and return the cron expression string - let standardExpression = `${minute} ${hour} ${day} ${month} ${dayOfWeek}`; - if (includeYear) { - standardExpression += ` ${year}`; - } - - const nonStandardExpressions = [ - '@annually', - '@daily', - '@hourly', - '@monthly', - '@reboot', - '@weekly', - '@yearly', - ]; - - return !includeNonStandard || this.faker.datatype.boolean() - ? standardExpression - : this.faker.helpers.arrayElement(nonStandardExpressions); + return systemCron(this.faker.fakerCore, options); } } diff --git a/src/modules/system/mime-type.ts b/src/modules/system/mime-type.ts new file mode 100644 index 00000000000..8b4ef2607b2 --- /dev/null +++ b/src/modules/system/mime-type.ts @@ -0,0 +1,18 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a mime-type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * mimeType(fakerCore) // 'video/vnd.vivo' + * + * @since 3.1.0 + */ +export function mimeType(fakerCore: FakerCore): string { + const mimeTypeKeys = Object.keys(fakerCore.locale.system.mime_type); + + return arrayElement(fakerCore, mimeTypeKeys); +} diff --git a/src/modules/system/network-interface.ts b/src/modules/system/network-interface.ts new file mode 100644 index 00000000000..46bf2f0d1fa --- /dev/null +++ b/src/modules/system/network-interface.ts @@ -0,0 +1,86 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { maybe } from '../helpers/maybe'; +import { objectKey } from '../helpers/object-key'; +import { mac } from '../internet/mac'; +import { numeric } from '../string/numeric'; + +const commonInterfaceTypes = ['en', 'wl', 'ww'] as const; +export type CommonInterfaceType = (typeof commonInterfaceTypes)[number]; +const commonInterfaceSchemas = { + index: 'o', + slot: 's', + mac: 'x', + pci: 'p', +} as const; +export type CommonInterfaceSchema = keyof typeof commonInterfaceSchemas; + +/** + * Returns a random [network interface](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/networking_guide/sec-understanding_the_predictable_network_interface_device_names). + * + * @param fakerCore The FakerCore to use. + * @param options The options to use. + * @param options.interfaceType The interface type. Can be one of `en`, `wl`, `ww`. + * @param options.interfaceSchema The interface schema. Can be one of `index`, `slot`, `mac`, `pci`. + * + * @example + * networkInterface(fakerCore) // 'enp0s3' + * networkInterface(fakerCore, { interfaceType: 'wl' }) // 'wlo1' + * networkInterface(fakerCore, { interfaceSchema: 'mac' }) // 'enx000c29c00000' + * networkInterface(fakerCore, { interfaceType: 'en', interfaceSchema: 'pci' }) // 'enp5s0f1d0' + * + * @since 7.4.0 + */ +export function networkInterface( + fakerCore: FakerCore, + options: { + /** + * The interface type. Can be one of `en`, `wl`, `ww`. + * + * @default helpersArrayElement(fakerCore, ['en', 'wl', 'ww']) + */ + interfaceType?: CommonInterfaceType; + /** + * The interface schema. Can be one of `index`, `slot`, `mac`, `pci`. + * + * @default helpersObjectKey(fakerCore, ['index' | 'slot' | 'mac' | 'pci']) + */ + interfaceSchema?: CommonInterfaceSchema; + } = {} +): string { + const { + interfaceType = arrayElement(fakerCore, commonInterfaceTypes), + interfaceSchema = objectKey(fakerCore, commonInterfaceSchemas), + } = options; + + let suffix: string; + let prefix = ''; + switch (interfaceSchema) { + case 'index': { + suffix = numeric(fakerCore); + break; + } + + case 'slot': { + suffix = `${numeric(fakerCore)}${ + maybe(fakerCore, () => `f${numeric(fakerCore)}`) ?? '' + }${maybe(fakerCore, () => `d${numeric(fakerCore)}`) ?? ''}`; + break; + } + + case 'mac': { + suffix = mac(fakerCore, ''); + break; + } + + case 'pci': { + prefix = maybe(fakerCore, () => `P${numeric(fakerCore)}`) ?? ''; + suffix = `${numeric(fakerCore)}s${numeric(fakerCore)}${ + maybe(fakerCore, () => `f${numeric(fakerCore)}`) ?? '' + }${maybe(fakerCore, () => `d${numeric(fakerCore)}`) ?? ''}`; + break; + } + } + + return `${prefix}${interfaceType}${commonInterfaceSchemas[interfaceSchema]}${suffix}`; +} diff --git a/src/modules/system/semver.ts b/src/modules/system/semver.ts new file mode 100644 index 00000000000..11d23782895 --- /dev/null +++ b/src/modules/system/semver.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { int } from '../number/int'; + +/** + * Returns a [semantic version](https://semver.org). + * + * @param fakerCore The FakerCore to use. + * + * @example + * semver(fakerCore) // '1.15.2' + * + * @since 3.1.0 + */ +export function semver(fakerCore: FakerCore): string { + return [int(fakerCore, 9), int(fakerCore, 20), int(fakerCore, 20)].join('.'); +} diff --git a/src/modules/vehicle/bicycle.ts b/src/modules/vehicle/bicycle.ts new file mode 100644 index 00000000000..17ce07722d7 --- /dev/null +++ b/src/modules/vehicle/bicycle.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a type of bicycle. + * + * @param fakerCore The FakerCore to use. + * + * @example + * bicycle(fakerCore) // 'Adventure Road Bicycle' + * + * @since 5.5.0 + */ +export function bicycle(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.vehicle.bicycle_type); +} diff --git a/src/modules/vehicle/color.ts b/src/modules/vehicle/color.ts new file mode 100644 index 00000000000..6ec2e32eaeb --- /dev/null +++ b/src/modules/vehicle/color.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { human } from '../color/human'; + +/** + * Returns a vehicle color. + * + * @param fakerCore The FakerCore to use. + * + * @example + * color(fakerCore) // 'red' + * + * @since 5.0.0 + */ +export function color(fakerCore: FakerCore): string { + return human(fakerCore); +} diff --git a/src/modules/vehicle/fuel.ts b/src/modules/vehicle/fuel.ts new file mode 100644 index 00000000000..a52de322a1d --- /dev/null +++ b/src/modules/vehicle/fuel.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a fuel type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * fuel(fakerCore) // 'Electric' + * + * @since 5.0.0 + */ +export function fuel(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.vehicle.fuel); +} diff --git a/src/modules/vehicle/index.ts b/src/modules/vehicle/index.ts index d032465a42d..64d761683a8 100644 --- a/src/modules/vehicle/index.ts +++ b/src/modules/vehicle/index.ts @@ -1,4 +1,13 @@ import { ModuleBase } from '../../internal/module-base'; +import { bicycle as vehicleBicycle } from './bicycle'; +import { color as vehicleColor } from './color'; +import { fuel as vehicleFuel } from './fuel'; +import { manufacturer as vehicleManufacturer } from './manufacturer'; +import { model as vehicleModel } from './model'; +import { type as vehicleType } from './type'; +import { vehicle as vehicleVehicle } from './vehicle'; +import { vin as vehicleVin } from './vin'; +import { vrm as vehicleVrm } from './vrm'; /** * Module to generate vehicle related entries. @@ -19,7 +28,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ vehicle(): string { - return `${this.manufacturer()} ${this.model()}`; + return vehicleVehicle(this.faker.fakerCore); } /** @@ -31,9 +40,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ manufacturer(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.vehicle.manufacturer - ); + return vehicleManufacturer(this.faker.fakerCore); } /** @@ -45,9 +52,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ model(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.vehicle.model - ); + return vehicleModel(this.faker.fakerCore); } /** @@ -59,7 +64,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ type(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.vehicle.type); + return vehicleType(this.faker.fakerCore); } /** @@ -71,7 +76,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ fuel(): string { - return this.faker.helpers.arrayElement(this.faker.definitions.vehicle.fuel); + return vehicleFuel(this.faker.fakerCore); } /** @@ -83,20 +88,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ vin(): string { - const exclude = ['o', 'i', 'q', 'O', 'I', 'Q']; - return `${this.faker.string.alphanumeric({ - length: 10, - casing: 'upper', - exclude, - })}${this.faker.string.alpha({ - length: 1, - casing: 'upper', - exclude, - })}${this.faker.string.alphanumeric({ - length: 1, - casing: 'upper', - exclude, - })}${this.faker.string.numeric({ length: 5, allowLeadingZeros: true })}`; + return vehicleVin(this.faker.fakerCore); } /** @@ -108,7 +100,7 @@ export class VehicleModule extends ModuleBase { * @since 5.0.0 */ color(): string { - return this.faker.color.human(); + return vehicleColor(this.faker.fakerCore); } /** @@ -120,16 +112,7 @@ export class VehicleModule extends ModuleBase { * @since 5.4.0 */ vrm(): string { - return `${this.faker.string.alpha({ - length: 2, - casing: 'upper', - })}${this.faker.string.numeric({ - length: 2, - allowLeadingZeros: true, - })}${this.faker.string.alpha({ - length: 3, - casing: 'upper', - })}`; + return vehicleVrm(this.faker.fakerCore); } /** @@ -141,8 +124,6 @@ export class VehicleModule extends ModuleBase { * @since 5.5.0 */ bicycle(): string { - return this.faker.helpers.arrayElement( - this.faker.definitions.vehicle.bicycle_type - ); + return vehicleBicycle(this.faker.fakerCore); } } diff --git a/src/modules/vehicle/manufacturer.ts b/src/modules/vehicle/manufacturer.ts new file mode 100644 index 00000000000..926fe8a8c1d --- /dev/null +++ b/src/modules/vehicle/manufacturer.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a manufacturer name. + * + * @param fakerCore The FakerCore to use. + * + * @example + * manufacturer(fakerCore) // 'Ford' + * + * @since 5.0.0 + */ +export function manufacturer(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.vehicle.manufacturer); +} diff --git a/src/modules/vehicle/model.ts b/src/modules/vehicle/model.ts new file mode 100644 index 00000000000..363f9e37e6f --- /dev/null +++ b/src/modules/vehicle/model.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a vehicle model. + * + * @param fakerCore The FakerCore to use. + * + * @example + * model(fakerCore) // 'Explorer' + * + * @since 5.0.0 + */ +export function model(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.vehicle.model); +} diff --git a/src/modules/vehicle/type.ts b/src/modules/vehicle/type.ts new file mode 100644 index 00000000000..66a9a32194f --- /dev/null +++ b/src/modules/vehicle/type.ts @@ -0,0 +1,16 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; + +/** + * Returns a vehicle type. + * + * @param fakerCore The FakerCore to use. + * + * @example + * type(fakerCore) // 'Coupe' + * + * @since 5.0.0 + */ +export function type(fakerCore: FakerCore): string { + return arrayElement(fakerCore, fakerCore.locale.vehicle.type); +} diff --git a/src/modules/vehicle/vehicle.ts b/src/modules/vehicle/vehicle.ts new file mode 100644 index 00000000000..dda27ee5526 --- /dev/null +++ b/src/modules/vehicle/vehicle.ts @@ -0,0 +1,17 @@ +import type { FakerCore } from '../../core'; +import { manufacturer } from '../vehicle/manufacturer'; +import { model } from '../vehicle/model'; + +/** + * Returns a random vehicle. + * + * @param fakerCore The FakerCore to use. + * + * @example + * vehicle(fakerCore) // 'BMW Explorer' + * + * @since 5.0.0 + */ +export function vehicle(fakerCore: FakerCore): string { + return `${manufacturer(fakerCore)} ${model(fakerCore)}`; +} diff --git a/src/modules/vehicle/vin.ts b/src/modules/vehicle/vin.ts new file mode 100644 index 00000000000..dfdf2acb9b5 --- /dev/null +++ b/src/modules/vehicle/vin.ts @@ -0,0 +1,31 @@ +import type { FakerCore } from '../../core'; +import { alpha } from '../string/alpha'; +import { alphanumeric } from '../string/alphanumeric'; +import { numeric } from '../string/numeric'; + +/** + * Returns a vehicle identification number (VIN). + * + * @param fakerCore The FakerCore to use. + * + * @example + * vin(fakerCore) // 'YV1MH682762184654' + * + * @since 5.0.0 + */ +export function vin(fakerCore: FakerCore): string { + const exclude = ['o', 'i', 'q', 'O', 'I', 'Q']; + return `${alphanumeric(fakerCore, { + length: 10, + casing: 'upper', + exclude, + })}${alpha(fakerCore, { + length: 1, + casing: 'upper', + exclude, + })}${alphanumeric(fakerCore, { + length: 1, + casing: 'upper', + exclude, + })}${numeric(fakerCore, { length: 5, allowLeadingZeros: true })}`; +} diff --git a/src/modules/vehicle/vrm.ts b/src/modules/vehicle/vrm.ts new file mode 100644 index 00000000000..84d5ebd501f --- /dev/null +++ b/src/modules/vehicle/vrm.ts @@ -0,0 +1,26 @@ +import type { FakerCore } from '../../core'; +import { alpha } from '../string/alpha'; +import { numeric } from '../string/numeric'; + +/** + * Returns a vehicle registration number (Vehicle Registration Mark - VRM) + * + * @param fakerCore The FakerCore to use. + * + * @example + * vrm(fakerCore) // 'MF56UPA' + * + * @since 5.4.0 + */ +export function vrm(fakerCore: FakerCore): string { + return `${alpha(fakerCore, { + length: 2, + casing: 'upper', + })}${numeric(fakerCore, { + length: 2, + allowLeadingZeros: true, + })}${alpha(fakerCore, { + length: 3, + casing: 'upper', + })}`; +} diff --git a/src/modules/word/filter-word-list-by-length.ts b/src/modules/word/_filter-word-list-by-length.ts similarity index 100% rename from src/modules/word/filter-word-list-by-length.ts rename to src/modules/word/_filter-word-list-by-length.ts diff --git a/src/modules/word/adjective.ts b/src/modules/word/adjective.ts new file mode 100644 index 00000000000..d7c137ce8c0 --- /dev/null +++ b/src/modules/word/adjective.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random adjective. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * adjective(fakerCore) // 'pungent' + * adjective(fakerCore, 5) // 'slimy' + * adjective(fakerCore, { strategy: 'shortest' }) // 'icy' + * adjective(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'distant' + * + * @since 6.0.0 + */ +export function adjective( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.adjective, + }) + ); +} diff --git a/src/modules/word/adverb.ts b/src/modules/word/adverb.ts new file mode 100644 index 00000000000..20bd36f654d --- /dev/null +++ b/src/modules/word/adverb.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random adverb. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * adverb(fakerCore) // 'quarrelsomely' + * adverb(fakerCore, 5) // 'madly' + * adverb(fakerCore, { strategy: 'shortest' }) // 'too' + * adverb(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'sweetly' + * + * @since 6.0.0 + */ +export function adverb( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.adverb, + }) + ); +} diff --git a/src/modules/word/conjunction.ts b/src/modules/word/conjunction.ts new file mode 100644 index 00000000000..55c005f430d --- /dev/null +++ b/src/modules/word/conjunction.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random conjunction. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * conjunction(fakerCore) // 'in order that' + * conjunction(fakerCore, 5) // 'since' + * conjunction(fakerCore, { strategy: 'shortest' }) // 'or' + * conjunction(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'hence' + * + * @since 6.0.0 + */ +export function conjunction( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.conjunction, + }) + ); +} diff --git a/src/modules/word/index.ts b/src/modules/word/index.ts index cf5719b23d6..2e42660fa10 100644 --- a/src/modules/word/index.ts +++ b/src/modules/word/index.ts @@ -1,6 +1,13 @@ -import { FakerError } from '../../errors/faker-error'; import { ModuleBase } from '../../internal/module-base'; -import { filterWordListByLength } from './filter-word-list-by-length'; +import { adjective as wordAdjective } from './adjective'; +import { adverb as wordAdverb } from './adverb'; +import { conjunction as wordConjunction } from './conjunction'; +import { interjection as wordInterjection } from './interjection'; +import { noun as wordNoun } from './noun'; +import { preposition as wordPreposition } from './preposition'; +import { sample as wordSample } from './sample'; +import { verb as wordVerb } from './verb'; +import { words as wordWords } from './words'; /** * Module to return various types of words. @@ -66,16 +73,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.adjective, - }) - ); + return wordAdjective(this.faker.fakerCore, options); } /** @@ -138,16 +136,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.adverb, - }) - ); + return wordAdverb(this.faker.fakerCore, options); } /** @@ -210,16 +199,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.conjunction, - }) - ); + return wordConjunction(this.faker.fakerCore, options); } /** @@ -282,16 +262,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.interjection, - }) - ); + return wordInterjection(this.faker.fakerCore, options); } /** @@ -354,16 +325,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.noun, - }) - ); + return wordNoun(this.faker.fakerCore, options); } /** @@ -426,16 +388,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.preposition, - }) - ); + return wordPreposition(this.faker.fakerCore, options); } /** @@ -498,16 +451,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - if (typeof options === 'number') { - options = { length: options }; - } - - return this.faker.helpers.arrayElement( - filterWordListByLength({ - ...options, - wordList: this.faker.definitions.word.verb, - }) - ); + return wordVerb(this.faker.fakerCore, options); } /** @@ -568,28 +512,7 @@ export class WordModule extends ModuleBase { strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; } = {} ): string { - const wordMethods = this.faker.helpers.shuffle([ - this.adjective, - this.adverb, - this.conjunction, - this.interjection, - this.noun, - this.preposition, - this.verb, - ]); - - for (const randomWordMethod of wordMethods) { - try { - return randomWordMethod(options); - } catch { - // catch missing locale data potentially required by randomWordMethod - continue; - } - } - - throw new FakerError( - 'No matching word data available for the current locale' - ); + return wordSample(this.faker.fakerCore, options); } /** @@ -629,14 +552,6 @@ export class WordModule extends ModuleBase { }; } = {} ): string { - if (typeof options === 'number') { - options = { count: options }; - } - - const { count = { min: 1, max: 3 } } = options; - - return this.faker.helpers - .multiple(() => this.sample(), { count }) - .join(' '); + return wordWords(this.faker.fakerCore, options); } } diff --git a/src/modules/word/interjection.ts b/src/modules/word/interjection.ts new file mode 100644 index 00000000000..2f3cf985cb2 --- /dev/null +++ b/src/modules/word/interjection.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random interjection. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * interjection(fakerCore) // 'gah' + * interjection(fakerCore, 5) // 'fooey' + * interjection(fakerCore, { strategy: 'shortest' }) // 'hm' + * interjection(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'boohoo' + * + * @since 6.0.0 + */ +export function interjection( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.interjection, + }) + ); +} diff --git a/src/modules/word/noun.ts b/src/modules/word/noun.ts new file mode 100644 index 00000000000..d31fa7a81f2 --- /dev/null +++ b/src/modules/word/noun.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random noun. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * noun(fakerCore) // 'external' + * noun(fakerCore, 5) // 'front' + * noun(fakerCore, { strategy: 'shortest' }) // 'ad' + * noun(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'average' + * + * @since 6.0.0 + */ +export function noun( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.noun, + }) + ); +} diff --git a/src/modules/word/preposition.ts b/src/modules/word/preposition.ts new file mode 100644 index 00000000000..ab383fac14b --- /dev/null +++ b/src/modules/word/preposition.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random preposition. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * preposition(fakerCore) // 'without' + * preposition(fakerCore, 5) // 'abaft' + * preposition(fakerCore, { strategy: 'shortest' }) // 'a' + * preposition(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'given' + * + * @since 6.0.0 + */ +export function preposition( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.preposition, + }) + ); +} diff --git a/src/modules/word/sample.ts b/src/modules/word/sample.ts new file mode 100644 index 00000000000..022c607270d --- /dev/null +++ b/src/modules/word/sample.ts @@ -0,0 +1,94 @@ +import type { FakerCore } from '../../core'; +import { FakerError } from '../../errors/faker-error'; +import { shuffle } from '../helpers/shuffle'; +import { adjective } from './adjective'; +import { adverb } from './adverb'; +import { conjunction } from './conjunction'; +import { interjection } from './interjection'; +import { noun } from './noun'; +import { preposition } from './preposition'; +import { verb } from './verb'; + +/** + * Returns a random word, that can be an adjective, adverb, conjunction, interjection, noun, preposition, or verb. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * sample(fakerCore) // 'incidentally' + * sample(fakerCore, 5) // 'fruit' + * + * @since 8.0.0 + */ +export function sample( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + const wordMethods = shuffle(fakerCore, [ + adjective, + adverb, + conjunction, + interjection, + noun, + preposition, + verb, + ] satisfies Array); + + for (const randomWordMethod of wordMethods) { + try { + return randomWordMethod(fakerCore, options); + } catch { + // catch missing locale data potentially required by randomWordMethod + continue; + } + } + + throw new FakerError( + 'No matching word data available for the current locale' + ); +} diff --git a/src/modules/word/verb.ts b/src/modules/word/verb.ts new file mode 100644 index 00000000000..a7d74b61f6b --- /dev/null +++ b/src/modules/word/verb.ts @@ -0,0 +1,78 @@ +import type { FakerCore } from '../../core'; +import { arrayElement } from '../helpers/array-element'; +import { filterWordListByLength } from './_filter-word-list-by-length'; + +/** + * Returns a random verb. + * + * @param fakerCore The FakerCore to use. + * @param options The expected length of the word or the options to use. + * @param options.length The expected length of the word. + * @param options.strategy The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * Defaults to `'fail'`. + * + * @example + * verb(fakerCore) // 'act' + * verb(fakerCore, 5) // 'tinge' + * verb(fakerCore, { strategy: 'shortest' }) // 'do' + * verb(fakerCore, { length: { min: 5, max: 7 }, strategy: "fail" }) // 'vault' + * + * @since 6.0.0 + */ +export function verb( + fakerCore: FakerCore, + options: + | number + | { + /** + * The expected length of the word. + */ + length?: + | number + | { + /** + * The minimum length of the word. + */ + min: number; + /** + * The maximum length of the word. + */ + max: number; + }; + /** + * The strategy to apply when no words with a matching length are found. + * + * Available error handling strategies: + * + * - `fail`: Throws an error if no words with the given length are found. + * - `shortest`: Returns any of the shortest words. + * - `closest`: Returns any of the words closest to the given length. + * - `longest`: Returns any of the longest words. + * - `any-length`: Returns a word with any length. + * + * @default 'fail' + */ + strategy?: 'fail' | 'closest' | 'shortest' | 'longest' | 'any-length'; + } = {} +): string { + if (typeof options === 'number') { + options = { length: options }; + } + + return arrayElement( + fakerCore, + filterWordListByLength({ + ...options, + wordList: fakerCore.locale.word.verb, + }) + ); +} diff --git a/src/modules/word/words.ts b/src/modules/word/words.ts new file mode 100644 index 00000000000..6249361d3bb --- /dev/null +++ b/src/modules/word/words.ts @@ -0,0 +1,51 @@ +import type { FakerCore } from '../../core'; +import { multiple } from '../helpers/multiple'; +import { sample } from '../word/sample'; + +/** + * Returns a random string containing some words separated by spaces. + * + * @param fakerCore The FakerCore to use. + * @param options The optional options object or the number of words to return. + * @param options.count The number of words to return. Defaults to a random value between `1` and `3`. + * + * @example + * words(fakerCore) // 'almost' + * words(fakerCore, 5) // 'before hourly patiently dribble equal' + * words(fakerCore, { count: 5 }) // 'whoever edible um kissingly faraway' + * words(fakerCore, { count: { min: 5, max: 10 } }) // 'vice buoyant through apropos poised total wary boohoo' + * + * @since 8.0.0 + */ +export function words( + fakerCore: FakerCore, + options: + | number + | { + /** + * The number of words to return. + * + * @default { min: 1, max: 3 } + */ + count?: + | number + | { + /** + * The minimum number of words to return. + */ + min: number; + /** + * The maximum number of words to return. + */ + max: number; + }; + } = {} +): string { + if (typeof options === 'number') { + options = { count: options }; + } + + const { count = { min: 1, max: 3 } } = options; + + return multiple(fakerCore, () => sample(fakerCore), { count }).join(' '); +} diff --git a/test/modules/finance-iban.spec.ts b/test/modules/finance-iban.spec.ts index cb22583a86c..4a830af9525 100644 --- a/test/modules/finance-iban.spec.ts +++ b/test/modules/finance-iban.spec.ts @@ -1,8 +1,8 @@ import { isIBAN } from 'validator'; import { describe, expect, it } from 'vitest'; import { faker } from '../../src'; -import { prettyPrintIban } from '../../src/modules/finance'; -import ibanLib from '../../src/modules/finance/iban'; +import { ibanLib } from '../../src/modules/finance/_iban-lib'; +import { prettyPrintIban } from '../../src/modules/finance/iban'; import { times } from '../support/times'; const NON_SEEDED_BASED_RUN = 25; diff --git a/test/modules/finance.spec.ts b/test/modules/finance.spec.ts index 292e1719114..d835bf7070a 100644 --- a/test/modules/finance.spec.ts +++ b/test/modules/finance.spec.ts @@ -7,9 +7,9 @@ import { FakerError } from '../../src/errors/faker-error'; import { BitcoinAddressFamily, BitcoinNetwork, -} from '../../src/modules/finance/bitcoin'; -import ibanLib from '../../src/modules/finance/iban'; -import { luhnCheck } from '../../src/modules/helpers/luhn-check'; +} from '../../src/modules/finance'; +import { ibanLib } from '../../src/modules/finance/_iban-lib'; +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..1780cb95d5d 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 { FakerError, SimpleFaker, faker } from '../../src'; +import * as numberIntAll 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'; @@ -866,7 +867,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(numberIntAll, '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'; diff --git a/test/modules/word.spec.ts b/test/modules/word.spec.ts index 646bbc069f9..6b8ee5221f6 100644 --- a/test/modules/word.spec.ts +++ b/test/modules/word.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { faker } from '../../src'; -import { filterWordListByLength } from '../../src/modules/word/filter-word-list-by-length'; +import { filterWordListByLength } from '../../src/modules/word/_filter-word-list-by-length'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; diff --git a/test/scripts/apidocs/verify-jsdoc-tags.spec.ts b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts index 6187b2fd882..09c8659b678 100644 --- a/test/scripts/apidocs/verify-jsdoc-tags.spec.ts +++ b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts @@ -184,8 +184,6 @@ ${examples}`; const content = []; const hasFakerCore = examples.includes('fakerCore'); - // TODO @ST-DDT 2026-04-19: Remove when past is migrated to SMF - const hasTempPast = examples.includes('past(fakerCore'); if (hasFakerCore) { rootImports.delete('fakerCore'); @@ -193,21 +191,14 @@ ${examples}`; rootImports.add('en'); } - if (hasTempPast) { - rootImports.add('SimpleFaker'); - } - if (functionImports.length > 0) { content.push( '// functionImports', - ...functionImports - // TODO @ST-DDT 2026-04-19: Remove when past is migrated to SMF - .filter(([functionName]) => functionName !== 'past') - .map( - ([functionName, importPath]) => - `import { ${functionName} } from '${importPath}';` - ) + ...functionImports.map( + ([functionName, importPath]) => + `import { ${functionName} } from '${importPath}';` + ) ); } @@ -226,12 +217,6 @@ ${examples}`; ); } - if (hasTempPast) { - content.push( - 'const past = new SimpleFaker(fakerCore).date.past;' - ); - } - content.push('', '// Examples', examples); examples = content.join('\n');