diff options
| author | Shinigami <[email protected]> | 2022-05-03 15:48:20 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2022-05-03 15:48:20 +0200 |
| commit | a2da7c496e9a3741d165ddfe6128b50837fec361 (patch) | |
| tree | 88d371bc19487bc8a34d9043035aed8e4fedd7d5 /src/modules | |
| parent | cc46a0c19af2752b6210c24b715fcce20197b6d9 (diff) | |
| download | faker-a2da7c496e9a3741d165ddfe6128b50837fec361.tar.xz faker-a2da7c496e9a3741d165ddfe6128b50837fec361.zip | |
refactor!: reorganize src folder (#909)
Diffstat (limited to 'src/modules')
32 files changed, 8866 insertions, 0 deletions
diff --git a/src/modules/address/index.ts b/src/modules/address/index.ts new file mode 100644 index 00000000..db82e01e --- /dev/null +++ b/src/modules/address/index.ts @@ -0,0 +1,488 @@ +import type { Faker } from '../..'; + +/** + * Module to generate addresses and locations. + */ +export class Address { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Address.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates random zip code from specified format. If format is not specified, + * the locale's zip format is used. + * + * @param format The optional format used to generate the the zip code. + * By default, a random format is used from the locale zip formats. + * + * @see faker.helpers.replaceSymbols() + * + * @example + * faker.address.zipCode() // '17839' + * faker.address.zipCode('####') // '6925' + * + */ + zipCode(format?: string): string { + // if zip format is not specified, use the zip format defined for the locale + if (format == null) { + const localeFormat = this.faker.definitions.address.postcode; + if (typeof localeFormat === 'string') { + format = localeFormat; + } else { + format = this.faker.helpers.arrayElement(localeFormat); + } + } + return this.faker.helpers.replaceSymbols(format); + } + + /** + * Generates random zip code from state abbreviation. If state abbreviation is + * not specified, a random zip code is generated according to the locale's zip format. + * Only works for locales with postcode_by_state definition. If a locale does not + * have a postcode_by_state definition, a random zip code is generated according + * to the locale's zip format. + * + * @param state The abbreviation of the state to generate the zip code for. + * + * @example + * fakerUS.address.zipCodeByState("AK") // '99595' + * fakerUS.address.zipCodeByState("??") // '47683-9880' + */ + zipCodeByState(state: string): string { + const zipRange = this.faker.definitions.address.postcode_by_state?.[state]; + if (zipRange) { + return String(this.faker.datatype.number(zipRange)); + } + return this.zipCode(); + } + + /** + * Generates a random localized city name. + * + * @param format The format to use. Can be either the index of the format to use or + * any method provided by faker wrapped in `{{}}`, e.g. `{{name.firstName}}` in + * order to build the city name. + * + * If no format string is provided one of the following is randomly used: + * + * - `{{address.cityPrefix}} {{name.firstName}}{{address.citySuffix}}` + * - `{{address.cityPrefix}} {{name.firstName}}` + * - `{{name.firstName}}{{address.citySuffix}}` + * - `{{name.lastName}}{{address.citySuffix}}` + * - `{{address.cityName}}` when city name is available + * + * @example + * faker.address.city() // 'Gleasonbury' + * faker.address.city(2) // 'Jadenshire' + */ + // TODO ST-DDT 2022-02-10: The string parameter doesn't work as expected. + city(format?: string | number): string { + const formats = [ + '{{address.cityPrefix}} {{name.firstName}}{{address.citySuffix}}', + '{{address.cityPrefix}} {{name.firstName}}', + '{{name.firstName}}{{address.citySuffix}}', + '{{name.lastName}}{{address.citySuffix}}', + ]; + + if (!format && this.faker.definitions.address.city_name) { + formats.push('{{address.cityName}}'); + } + + if (typeof format !== 'number') { + format = this.faker.datatype.number(formats.length - 1); + } + + return this.faker.fake(formats[format]); + } + + /** + * Returns a random localized city prefix. + * + * @example + * faker.address.cityPrefix() // 'East' + */ + cityPrefix(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.city_prefix + ); + } + + /** + * Returns a random localized city suffix. + * + * @example + * faker.address.citySuffix() // 'mouth' + */ + citySuffix(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.city_suffix + ); + } + + /** + * Returns a random localized city name. + * + * @example + * faker.address.cityName() // 'San Rafael' + */ + cityName(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.city_name + ); + } + + /** + * Generates a random building number. + * + * @example + * faker.address.buildingNumber() // '379' + */ + buildingNumber(): string { + const format = this.faker.helpers.arrayElement( + this.faker.definitions.address.building_number + ); + + return this.faker.helpers.replaceSymbolWithNumber(format); + } + + /** + * Generates a random localized street name. + * + * @example + * faker.address.streetName() // 'Kulas Roads' + */ + streetName(): string { + let result: string; + let suffix = this.streetSuffix(); + if (suffix !== '') { + suffix = ` ${suffix}`; + } + + switch (this.faker.datatype.number(1)) { + case 0: + result = this.faker.name.lastName() + suffix; + break; + case 1: + result = this.faker.name.firstName() + suffix; + break; + } + return result; + } + + /** + * Generates a random localized street address. + * + * @param useFullAddress When true this will generate a full address. + * Otherwise it will just generate a street address. + * + * @example + * faker.address.streetName() // '0917 O'Conner Estates' + * faker.address.streetAddress(true) // '3393 Ronny Way Apt. 742' + * faker.address.streetAddress(false) // '34830 Erdman Hollow' + */ + streetAddress(useFullAddress: boolean = false): string { + const formats = this.faker.definitions.address.street_address; + const format = formats[useFullAddress ? 'full' : 'normal']; + + return this.faker.fake(format); + } + + /** + * Returns a random localized street suffix. + * + * @example + * faker.address.streetSuffix() // 'Streets' + */ + streetSuffix(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.street_suffix + ); + } + + /** + * Returns a random localized street prefix. + * + * @example + * fakerGH.address.streetPrefix() // 'Boame' + */ + streetPrefix(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.street_prefix + ); + } + + /** + * Generates a random localized secondary address. This refers to a specific location at a given address + * such as an apartment or room number. + * + * @example + * faker.address.secondaryAddress() // 'Apt. 861' + */ + secondaryAddress(): string { + return this.faker.helpers.replaceSymbolWithNumber( + this.faker.helpers.arrayElement( + this.faker.definitions.address.secondary_address + ) + ); + } + + /** + * Returns a random localized county. + * + * @example + * faker.address.county() // 'Cambridgeshire' + */ + county(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.county + ); + } + + /** + * Returns a random country name. + * + * @example + * faker.address.country() // 'Greece' + */ + country(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.country + ); + } + + /** + * Returns a random country code. + * + * @param alphaCode The code to return. Can be either `'alpha-2'` (2 letter code) + * or `'alpha-3'` (three letter code). Defaults to `'alpha-2'`. + * + * @example + * faker.address.countryCode() // 'SJ' + * faker.address.countryCode('alpha-2') // 'GA' + * faker.address.countryCode('alpha-3') // 'TJK' + */ + countryCode(alphaCode: 'alpha-2' | 'alpha-3' = 'alpha-2'): string { + const key: keyof typeof this.faker.definitions.address = + alphaCode === 'alpha-3' ? 'country_code_alpha_3' : 'country_code'; + + return this.faker.helpers.arrayElement(this.faker.definitions.address[key]); + } + + /** + * Returns a random localized state from this country. + * + * @example + * faker.address.state() // 'Georgia' + */ + state(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.state + ); + } + + /** + * Returns a random localized state's abbreviated name from this country. + * + * @example + * faker.address.stateAbbr() // 'ND' + */ + stateAbbr(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.state_abbr + ); + } + + /** + * Generates a random latitude. + * + * @param max The upper bound for the latitude to generate. Defaults to `90`. + * @param min The lower bound for the latitude to generate. Defaults to `-90`. + * @param precision The number of decimal points of precision for the latitude. Defaults to `4`. + * + * @example + * faker.address.latitude() // '-30.9501' + * faker.address.latitude(10, -10, 5) // '2.68452' + */ + latitude(max: number = 90, min: number = -90, precision: number = 4): string { + return this.faker.datatype + .number({ + min, + max, + precision: parseFloat(`${(0.0).toPrecision(precision)}1`), + }) + .toFixed(precision); + } + + /** + * Generates a random longitude. + * + * @param max The upper bound for the longitude to generate. Defaults to `180`. + * @param min The lower bound for the longitude to generate. Defaults to `-180`. + * @param precision The number of decimal points of precision for the longitude. Defaults to `4`. + * + * @example + * faker.address.longitude() // '-154.0226' + * faker.address.longitude(10, -10, 5) // '-4.03620' + */ + longitude( + max: number = 180, + min: number = -180, + precision: number = 4 + ): string { + return this.faker.datatype + .number({ + max: max, + min: min, + precision: parseFloat(`${(0.0).toPrecision(precision)}1`), + }) + .toFixed(precision); + } + + /** + * Returns a random direction (cardinal and ordinal; northwest, east, etc). + * + * @param useAbbr If true this will return abbreviated directions (NW, E, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * faker.address.direction() // 'Northeast' + * faker.address.direction(false) // 'South' + * faker.address.direction(true) // 'NE' + */ + direction(useAbbr: boolean = false): string { + if (!useAbbr) { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction + ); + } + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction_abbr + ); + } + + /** + * Returns a random cardinal direction (north, east, south, west). + * + * @param useAbbr If true this will return abbreviated directions (N, E, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * faker.address.cardinalDirection() // 'North' + * faker.address.cardinalDirection(false) // 'South' + * faker.address.cardinalDirection(true) // 'N' + */ + cardinalDirection(useAbbr: boolean = false): string { + if (!useAbbr) { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction.slice(0, 4) + ); + } + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction_abbr.slice(0, 4) + ); + } + + /** + * Returns a random ordinal direction (northwest, southeast, etc). + * + * @param useAbbr If true this will return abbreviated directions (NW, SE, etc). + * Otherwise this will return the long name. Defaults to `false`. + * + * @example + * faker.address.ordinalDirection() // 'Northeast' + * faker.address.ordinalDirection(false) // 'Northwest' + * faker.address.ordinalDirection(true) // 'NE' + */ + ordinalDirection(useAbbr: boolean = false): string { + if (!useAbbr) { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction.slice(4, 8) + ); + } + return this.faker.helpers.arrayElement( + this.faker.definitions.address.direction_abbr.slice(4, 8) + ); + } + + /** + * Generates a random GPS coordinate within the specified radius from the given coordinate. + * + * @param coordinate The original coordinate to get a new coordinate close to. + * If no coordinate is given, a random one will be chosen. + * @param radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`. + * @param isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`. + * + * @example + * faker.address.nearbyGPSCoordinate() // [ '33.8475', '-170.5953' ] + * faker.address.nearbyGPSCoordinate([33, -170]) // [ '33.0165', '-170.0636' ] + * faker.address.nearbyGPSCoordinate([33, -170], 1000, true) // [ '37.9163', '-179.2408' ] + */ + // TODO ST-DDT 2022-02-10: Allow coordinate parameter to be [string, string]. + nearbyGPSCoordinate( + coordinate?: [latitude: number, longitude: number], + radius: number = 10, + isMetric: boolean = false + ): [latitude: string, longitude: string] { + // If there is no coordinate, the best we can do is return a random GPS coordinate. + if (coordinate === undefined) { + return [this.latitude(), this.longitude()]; + } + + const angleRadians = this.faker.datatype.float({ + min: 0, + max: 2 * Math.PI, + precision: 0.00001, + }); // in ° radians + + const radiusMetric = isMetric ? radius : radius * 1.60934; // in km + const errorCorrection = 0.995; // avoid float issues + const distanceInKm = + this.faker.datatype.float({ + min: 0, + max: radiusMetric, + precision: 0.001, + }) * errorCorrection; // in km + + /** + * The distance in km per degree for earth. + */ + // TODO @Shinigami92 2022-04-26: Provide an option property to provide custom circumferences. + const kmPerDegree = 40_000 / 360; // in km/° + + const distanceInDegree = distanceInKm / kmPerDegree; // in ° + + const newCoordinate: [latitude: number, longitude: number] = [ + coordinate[0] + Math.sin(angleRadians) * distanceInDegree, + coordinate[1] + Math.cos(angleRadians) * distanceInDegree, + ]; + + // Box latitude [-90°, 90°] + newCoordinate[0] = newCoordinate[0] % 180; + if (newCoordinate[0] < -90 || newCoordinate[0] > 90) { + newCoordinate[0] = Math.sign(newCoordinate[0]) * 180 - newCoordinate[0]; + newCoordinate[1] += 180; + } + // Box longitude [-180°, 180°] + newCoordinate[1] = (((newCoordinate[1] % 360) + 540) % 360) - 180; + + return [newCoordinate[0].toFixed(4), newCoordinate[1].toFixed(4)]; + } + + /** + * Returns a random time zone. + * + * @example + * faker.address.timeZone() // 'Pacific/Guam' + */ + timeZone(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.address.time_zone + ); + } +} diff --git a/src/modules/animal/index.ts b/src/modules/animal/index.ts new file mode 100644 index 00000000..416fe533 --- /dev/null +++ b/src/modules/animal/index.ts @@ -0,0 +1,164 @@ +import type { Faker } from '../..'; + +/** + * Module to generate animal related entries. + */ +export class Animal { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Animal.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random dog breed. + * + * @example + * faker.animal.dog() // 'Irish Water Spaniel' + */ + dog(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.dog); + } + + /** + * Returns a random cat breed. + * + * @example + * faker.animal.cat() // 'Singapura' + */ + cat(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.cat); + } + + /** + * Returns a random snake species. + * + * @example + * faker.animal.snake() // 'Eyelash viper' + */ + snake(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.snake); + } + + /** + * Returns a random bear species. + * + * @example + * faker.animal.bear() // 'Asian black bear' + */ + bear(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.bear); + } + + /** + * Returns a random lion species. + * + * @example + * faker.animal.lion() // 'Northeast Congo Lion' + */ + lion(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.lion); + } + + /** + * Returns a random cetacean species. + * + * @example + * faker.animal.cetacean() // 'Spinner Dolphin' + */ + cetacean(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.animal.cetacean + ); + } + + /** + * Returns a random horse breed. + * + * @example + * faker.animal.horse() // 'Swedish Warmblood' + */ + horse(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.horse); + } + + /** + * Returns a random bird species. + * + * @example + * faker.animal.bird() // 'Buller's Shearwater' + */ + bird(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.bird); + } + + /** + * Returns a random cow species. + * + * @example + * faker.animal.cow() // 'Brava' + */ + cow(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.cow); + } + + /** + * Returns a random fish species. + * + * @example + * faker.animal.fish() // 'Mandarin fish' + */ + fish(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.fish); + } + + /** + * Returns a random crocodilian species. + * + * @example + * faker.animal.crocodilia() // 'Philippine Crocodile' + */ + crocodilia(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.animal.crocodilia + ); + } + + /** + * Returns a random insect species. + * + * @example + * faker.animal.insect() // 'Pyramid ant' + */ + insect(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.animal.insect + ); + } + + /** + * Returns a random rabbit species. + * + * @example + * faker.animal.rabbit() // 'Florida White' + */ + rabbit(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.animal.rabbit + ); + } + + /** + * Returns a random animal type. + * + * @example + * faker.animal.type() // 'crocodilia' + */ + type(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.animal.type); + } +} diff --git a/src/modules/commerce/index.ts b/src/modules/commerce/index.ts new file mode 100644 index 00000000..71ce96d6 --- /dev/null +++ b/src/modules/commerce/index.ts @@ -0,0 +1,133 @@ +import type { Faker } from '../..'; + +/** + * Module to generate commerce and product related entries. + */ +export class Commerce { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Commerce.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a human readable color name. + * + * @example + * faker.commerce.color() // 'red' + */ + color(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.color + ); + } + + /** + * Returns a department inside a shop. + * + * @example + * faker.commerce.department() // 'Garden' + */ + department(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.department + ); + } + + /** + * Generates a random descriptive product name. + * + * @example + * faker.commerce.productName() // 'Incredible Soft Gloves' + */ + productName(): string { + return `${this.productAdjective()} ${this.productMaterial()} ${this.product()}`; + } + + /** + * Generates a price between min and max (inclusive). + * + * @param min The minimum price. Defaults to `1`. + * @param max The maximum price. Defaults to `1000`. + * @param dec The number of decimal places. Defaults to `2`. + * @param symbol The currency value to use. Defaults to `''`. + * + * @example + * faker.commerce.price() // 828.00 + * faker.commerce.price(100) // 904.00 + * faker.commerce.price(100, 200) // 154.00 + * faker.commerce.price(100, 200, 0) // 133 + * faker.commerce.price(100, 200, 0, '$') // $114 + */ + price( + min: number = 1, + max: number = 1000, + dec: number = 2, + symbol: string = '' + ): string { + if (min < 0 || max < 0) { + return `${symbol}${0.0}`; + } + + const randValue = this.faker.datatype.number({ max: max, min: min }); + + return ( + symbol + + (Math.round(randValue * Math.pow(10, dec)) / Math.pow(10, dec)).toFixed( + dec + ) + ); + } + + /** + * Returns an adjective describing a product. + * + * @example + * faker.commerce.productAdjective() // 'Handcrafted' + */ + productAdjective(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.product_name.adjective + ); + } + + /** + * Returns a material of a product. + * + * @example + * faker.commerce.productMaterial() // 'Rubber' + */ + productMaterial(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.product_name.material + ); + } + + /** + * Returns a short product name. + * + * @example + * faker.commerce.product() // 'Computer' + */ + product(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.product_name.product + ); + } + + /** + * Returns a product description. + * + * @example + * faker.commerce.productDescription() // 'Andy shoes are designed to keeping...' + */ + productDescription(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.commerce.product_description + ); + } +} diff --git a/src/modules/company/index.ts b/src/modules/company/index.ts new file mode 100644 index 00000000..fb5f2e66 --- /dev/null +++ b/src/modules/company/index.ts @@ -0,0 +1,153 @@ +import type { Faker } from '../..'; + +/** + * Module to generate company related entries. + */ +export class Company { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Company.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns an array with possible company name suffixes. + * + * @example + * faker.company.suffixes() // [ 'Inc', 'and Sons', 'LLC', 'Group' ] + */ + suffixes(): string[] { + // Don't want the source array exposed to modification, so return a copy + return this.faker.definitions.company.suffix.slice(0); + } + + /** + * Generates a random company name. + * + * @param format The optional format index used to select a format. + * + * @example + * faker.company.companyName() // 'Zieme, Hauck and McClure' + */ + companyName(format?: number): string { + const formats = [ + '{{name.lastName}} {{company.companySuffix}}', + '{{name.lastName}} - {{name.lastName}}', + '{{name.lastName}}, {{name.lastName}} and {{name.lastName}}', + ]; + + if (typeof format !== 'number') { + format = this.faker.datatype.number(formats.length - 1); + } + + return this.faker.fake(formats[format]); + } + + /** + * Returns a random company suffix. + * + * @example + * faker.company.companySuffix() // 'and Sons' + */ + companySuffix(): string { + return this.faker.helpers.arrayElement(this.suffixes()); + } + + /** + * Generates a random business catch phrase. + * + * @example + * faker.company.catchPhrase() // 'Upgradable systematic flexibility' + */ + catchPhrase(): string { + return this.faker.fake( + '{{company.catchPhraseAdjective}} {{company.catchPhraseDescriptor}} {{company.catchPhraseNoun}}' + ); + } + + /** + * Generates a random company bs phrase. + * + * @example + * faker.company.bs() // 'cultivate synergistic e-markets' + */ + bs(): string { + return this.faker.fake( + '{{company.bsBuzz}} {{company.bsAdjective}} {{company.bsNoun}}' + ); + } + + /** + * Returns a random catch phrase adjective. + * + * @example + * faker.company.catchPhraseAdjective() // 'Multi-tiered' + */ + catchPhraseAdjective(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.company.adjective + ); + } + + /** + * Returns a random catch phrase descriptor. + * + * @example + * faker.company.catchPhraseDescriptor() // 'composite' + */ + catchPhraseDescriptor(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.company.descriptor + ); + } + + /** + * Returns a random catch phrase noun. + * + * @example + * faker.company.catchPhraseNoun() // 'leverage' + */ + catchPhraseNoun(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.company.noun); + } + + /** + * Returns a random company bs adjective. + * + * @example + * faker.company.bsAdjective() // 'one-to-one' + */ + bsAdjective(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.company.bs_adjective + ); + } + + /** + * Returns a random company bs buzz word. + * + * @example + * faker.company.bsBuzz() // 'empower' + */ + bsBuzz(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.company.bs_verb + ); + } + + /** + * Returns a random company bs noun. + * + * @example + * faker.company.bsNoun() // 'paradigms' + */ + bsNoun(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.company.bs_noun + ); + } +} diff --git a/src/modules/database/index.ts b/src/modules/database/index.ts new file mode 100644 index 00000000..073d3f8a --- /dev/null +++ b/src/modules/database/index.ts @@ -0,0 +1,75 @@ +import type { Faker } from '../..'; + +/** + * Module to generate database related entries. + */ +export class Database { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Database.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random database column name. + * + * @example + * faker.database.column() // 'createdAt' + */ + column(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.database.column + ); + } + + /** + * Returns a random database column type. + * + * @example + * faker.database.type() // 'timestamp' + */ + type(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.database.type + ); + } + + /** + * Returns a random database collation. + * + * @example + * faker.database.collation() // 'utf8_unicode_ci' + */ + collation(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.database.collation + ); + } + + /** + * Returns a random database engine. + * + * @example + * faker.database.engine() // 'ARCHIVE' + */ + engine(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.database.engine + ); + } + + /** + * Returns a MongoDB [ObjectId](https://docs.mongodb.com/manual/reference/method/ObjectId/) string. + * + * @example + * faker.database.mongodbObjectId() // 'e175cac316a79afdd0ad3afb' + */ + mongodbObjectId(): string { + // strip the "0x" from the hexadecimal output + return this.faker.datatype.hexadecimal(24).replace('0x', '').toLowerCase(); + } +} diff --git a/src/modules/datatype/index.ts b/src/modules/datatype/index.ts new file mode 100644 index 00000000..653d4d3f --- /dev/null +++ b/src/modules/datatype/index.ts @@ -0,0 +1,305 @@ +import type { Faker } from '../..'; +import { FakerError } from '../../errors/faker-error'; +import { deprecated } from '../../internal/deprecated'; + +/** + * Module to generate various primitive values and data types. + */ +export class Datatype { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Datatype.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a single random number between zero and the given max value or the given range with the specified precision. + * The bounds are inclusive. + * + * @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 `min + 99999`. + * @param options.precision Precision of the generated number. Defaults to `1`. + * + * @throws When options define `max < min`. + * + * @example + * faker.datatype.number() // 55422 + * faker.datatype.number(100) // 52 + * faker.datatype.number({ min: 1000000 }) // 431433 + * faker.datatype.number({ max: 100 }) // 42 + * faker.datatype.number({ precision: 0.01 }) // 64246.18 + * faker.datatype.number({ min: 10, max: 100, precision: 0.01 }) // 36.94 + */ + number( + options: number | { min?: number; max?: number; precision?: number } = 99999 + ): number { + if (typeof options === 'number') { + options = { max: options }; + } + + const { min = 0, precision = 1 } = options; + const max = options.max ?? min + 99999; + + if (max === min) { + return min; + } + + if (max < min) { + throw new FakerError(`Max ${max} should be larger then min ${min}.`); + } + + const randomNumber = Math.floor( + this.faker.mersenne.rand(max / precision + 1, min / precision) + ); + + // Workaround problem in float point arithmetics for e.g. 6681493 / 0.01 + return randomNumber / (1 / precision); + } + + /** + * Returns a single random floating-point number for the given precision or range and precision. + * + * @param options Precision or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `99999`. + * @param options.precision Precision of the generated number. Defaults to `0.01`. + * + * @example + * faker.datatype.float() // 51696.36 + * faker.datatype.float(0.1) // 52023.2 + * faker.datatype.float({ min: 1000000 }) // 212859.76 + * faker.datatype.float({ max: 100 }) // 28.11 + * faker.datatype.float({ precision: 0.1 }) // 84055.3 + * faker.datatype.float({ min: 10, max: 100, precision: 0.001 }) // 57.315 + */ + float( + options?: number | { min?: number; max?: number; precision?: number } + ): number { + if (typeof options === 'number') { + options = { + precision: options, + }; + } + options = options || {}; + const opts: { precision?: number } = {}; + for (const p in options) { + opts[p] = options[p]; + } + if (opts.precision == null) { + opts.precision = 0.01; + } + return this.number(opts); + } + + /** + * Returns a Date object using a random number of milliseconds since + * the [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time) (1 January 1970 UTC). + * + * @param options Max number of milliseconds since unix epoch or options object. + * @param options.min Lower bound for milliseconds since base date. + * When not provided or smaller than `-8640000000000000`, `1990-01-01` is considered + * as minimum generated date. Defaults to `631152000000`. + * @param options.max Upper bound for milliseconds since base date. + * When not provided or larger than `8640000000000000`, `2100-01-01` is considered + * as maximum generated date. Defaults to `4102444800000`. + * + * @example + * faker.datatype.datetime() // '2089-04-17T18:03:24.956Z' + * faker.datatype.datetime(1893456000000) // '2022-03-28T07:00:56.876Z' + * faker.datatype.datetime({ min: 1577836800000, max: 1893456000000 }) // '2021-09-12T07:13:00.255Z' + */ + datetime(options: number | { min?: number; max?: number } = {}): Date { + const minMax = 8640000000000000; + + let min = typeof options === 'number' ? undefined : options.min; + let max = typeof options === 'number' ? options : options.max; + + if (min == null || min < minMax * -1) { + min = Date.UTC(1990, 0); + } + + if (max == null || max > minMax) { + max = Date.UTC(2100, 0); + } + + return new Date(this.number({ min, max })); + } + + /** + * Returns a string containing UTF-16 chars between 33 and 125 (`!` to `}`). + * + * @param length Length of the generated string. Max length is `2^20`. Defaults to `10`. + * + * @example + * faker.datatype.string() // 'Zo!.:*e>wR' + * faker.datatype.string(5) // '6Bye8' + */ + string(length = 10): string { + const maxLength = Math.pow(2, 20); + if (length >= maxLength) { + length = maxLength; + } + + const charCodeOption = { + min: 33, + max: 125, + }; + + let returnString = ''; + + for (let i = 0; i < length; i++) { + returnString += String.fromCharCode(this.number(charCodeOption)); + } + + return returnString; + } + + /** + * Returns a UUID v4 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @example + * faker.datatype.uuid() // '4136cd0b-d90b-4af7-b485-5d1ded8db252' + */ + uuid(): string { + const RFC4122_TEMPLATE = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + const replacePlaceholders = (placeholder) => { + const random = this.number({ min: 0, max: 15 }); + const value = placeholder === 'x' ? random : (random & 0x3) | 0x8; + return value.toString(16); + }; + return RFC4122_TEMPLATE.replace(/[xy]/g, replacePlaceholders); + } + + /** + * Returns the boolean value true or false. + * + * @example + * faker.datatype.boolean() // false + */ + boolean(): boolean { + return !!this.number(1); + } + + /** + * Returns a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) number. + * + * @param length Length of the generated number. Defaults to `1`. + * + * @see faker.datatype.hexadecimal() + * + * @example + * faker.datatype.hexaDecimal() // '0xb' + * faker.datatype.hexaDecimal(10) // '0xaE13F044fb' + * + * @deprecated + */ + hexaDecimal(length = 1): string { + deprecated({ + deprecated: 'faker.datatype.hexaDecimal()', + proposed: 'faker.datatype.hexadecimal()', + since: 'v6.1.2', + until: 'v7.0.0', + }); + + return this.hexadecimal(length); + } + + /** + * Returns a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) number. + * + * @param length Length of the generated number. Defaults to `1`. + * + * @example + * faker.datatype.hexadecimal() // '0xb' + * faker.datatype.hexadecimal(10) // '0xaE13F044fb' + */ + hexadecimal(length = 1): string { + let wholeString = ''; + + for (let i = 0; i < length; i++) { + wholeString += this.faker.helpers.arrayElement([ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + ]); + } + + return `0x${wholeString}`; + } + + /** + * Returns a string representing JSON object with 7 pre-defined properties. + * + * @example + * faker.datatype.json() // `{"foo":"mxz.v8ISij","bar":29154,"bike":8658,"a":"GxTlw$nuC:","b":40693,"name":"%'<FTou{7X","prop":"X(bd4iT>77"}` + */ + json(): string { + const properties = ['foo', 'bar', 'bike', 'a', 'b', 'name', 'prop']; + const returnObject: Record<string, string | number> = {}; + + properties.forEach((prop) => { + returnObject[prop] = this.boolean() ? this.string() : this.number(); + }); + + return JSON.stringify(returnObject); + } + + /** + * Returns an array with random strings and numbers. + * + * @param length Size of the returned array. Defaults to `10`. + * + * @example + * faker.datatype.array() // [ 94099, 85352, 'Hz%T.C\\l;8', '|#gmtw3otS', '2>:rJ|3$&d', 56864, 'Ss2-p0RXSI', 51084, 2039, 'mNEU[.r0Vf' ] + * faker.datatype.array(3) // [ 61845, 'SK7H$W3:d*', 'm[%7N8*GVK' ] + */ + array(length = 10): Array<string | number> { + return Array.from<string | number>({ length }).map(() => + this.boolean() ? this.string() : this.number() + ); + } + + /** + * Returns a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#bigint_type) number. + * + * @param value When provided, this method simply converts it to `BigInt` type. + * + * @example + * faker.datatype.bigInt() // 8507209999914928n + * faker.datatype.bigInt('156') // 156n + * faker.datatype.bigInt(30) // 30n + * faker.datatype.bigInt(3000n) // 3000n + * faker.datatype.bigInt(true) // 1n + */ + bigInt(value?: string | number | bigint | boolean): bigint { + if (value === undefined) { + value = Math.floor(this.number() * 99999999999) + 10000000000; + } + + return BigInt(value); + } +} diff --git a/src/modules/date/index.ts b/src/modules/date/index.ts new file mode 100644 index 00000000..09f608f0 --- /dev/null +++ b/src/modules/date/index.ts @@ -0,0 +1,256 @@ +import type { Faker } from '../..'; +import type { DateEntryDefinition } from '../../definitions'; + +/** + * Converts date passed as a string, number or Date to a Date object. + * If nothing or a non parseable value is passed, takes current date. + * + * @param date Date + */ +function toDate(date?: string | Date | number): Date { + date = new Date(date); + if (isNaN(date.valueOf())) { + date = new Date(); + } + + return date; +} + +/** + * Module to generate dates. + */ +export class _Date { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(_Date.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a random date in the past. + * + * @param years The range of years the date may be in the past. Defaults to `1`. + * @param refDate The date to use as reference point for the newly generated date. Defaults to now. + * + * @see faker.date.recent() + * + * @example + * faker.date.past() // '2021-12-03T05:40:44.408Z' + * faker.date.past(10) // '2017-10-25T21:34:19.488Z' + * faker.date.past(10, '2020-01-01T00:00:00.000Z') // '2017-08-18T02:59:12.350Z' + */ + past(years?: number, refDate?: string | Date | number): Date { + const date = toDate(refDate); + const range = { + min: 1000, + max: (years || 1) * 365 * 24 * 3600 * 1000, + }; + + let past = date.getTime(); + past -= this.faker.datatype.number(range); // some time from now to N years ago, in milliseconds + date.setTime(past); + + return date; + } + + /** + * Generates a random date in the future. + * + * @param years The range of years the date may be in the future. Defaults to `1`. + * @param refDate The date to use as reference point for the newly generated date. Defaults to now. + * + * @see faker.date.soon() + * + * @example + * faker.date.future() // '2022-11-19T05:52:49.100Z' + * faker.date.future(10) // '2030-11-23T09:38:28.710Z' + * faker.date.future(10, '2020-01-01T00:00:00.000Z') // '2020-12-13T22:45:10.252Z' + */ + future(years?: number, refDate?: string | Date | number): Date { + const date = toDate(refDate); + const range = { + min: 1000, + max: (years || 1) * 365 * 24 * 3600 * 1000, + }; + + let future = date.getTime(); + future += this.faker.datatype.number(range); // some time from now to N years later, in milliseconds + date.setTime(future); + + return date; + } + + /** + * Generates a random date between the given boundaries. + * + * @param from The early date boundary. + * @param to The late date boundary. + * + * @example + * faker.date.between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') // '2026-05-16T02:22:53.002Z' + */ + between(from: string | Date | number, to: string | Date | number): Date { + const fromMs = toDate(from).getTime(); + const toMs = toDate(to).getTime(); + const dateOffset = this.faker.datatype.number(toMs - fromMs); + + return new Date(fromMs + dateOffset); + } + + /** + * Generates n random dates between the given boundaries. + * + * @param from The early date boundary. + * @param to The late date boundary. + * @param num The number of dates to generate. Defaults to `3`. + * + * @example + * faker.date.betweens('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + * // [ + * // 2022-07-02T06:00:00.000Z, + * // 2024-12-31T12:00:00.000Z, + * // 2027-07-02T18:00:00.000Z + * // ] + * faker.date.betweens('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z', 2) + * // [ 2023-05-02T16:00:00.000Z, 2026-09-01T08:00:00.000Z ] + */ + betweens( + from: string | Date | number, + to: string | Date | number, + num: number = 3 + ): Date[] { + const dates: Date[] = []; + + while (dates.length < num) { + dates.push(this.between(from, to)); + } + + return dates.sort((a, b) => a.getTime() - b.getTime()); + } + + /** + * Generates a random date in the recent past. + * + * @param days The range of days the date may be in the past. Defaults to `1`. + * @param refDate The date to use as reference point for the newly generated date. Defaults to now. + * + * @see faker.date.past() + * + * @example + * faker.date.recent() // '2022-02-04T02:09:35.077Z' + * faker.date.recent(10) // '2022-01-29T06:12:12.829Z' + * faker.date.recent(10, '2020-01-01T00:00:00.000Z') // '2019-12-27T18:11:19.117Z' + */ + recent(days?: number, refDate?: string | Date | number): Date { + const date = toDate(refDate); + const range = { + min: 1000, + max: (days || 1) * 24 * 3600 * 1000, + }; + + let future = date.getTime(); + future -= this.faker.datatype.number(range); // some time from now to N days ago, in milliseconds + date.setTime(future); + + return date; + } + + /** + * Generates a random date in the near future. + * + * @param days The range of days the date may be in the future. Defaults to `1`. + * @param refDate The date to use as reference point for the newly generated date. Defaults to now. + * + * @see faker.date.future() + * + * @example + * faker.date.soon() // '2022-02-05T09:55:39.216Z' + * faker.date.soon(10) // '2022-02-11T05:14:39.138Z' + * faker.date.soon(10, '2020-01-01T00:00:00.000Z') // '2020-01-01T02:40:44.990Z' + */ + soon(days?: number, refDate?: string | Date | number): Date { + const date = toDate(refDate); + const range = { + min: 1000, + max: (days || 1) * 24 * 3600 * 1000, + }; + + let future = date.getTime(); + future += this.faker.datatype.number(range); // some time from now to N days later, in milliseconds + date.setTime(future); + + return date; + } + + /** + * Returns a random name of a month. + * + * @param options The optional options to use. + * @param options.abbr Whether to return an abbreviation. Defaults to `false`. + * @param options.context Whether to return the name of a month in a context. Defaults to `false`. + * + * @example + * faker.date.month() // 'October' + * faker.date.month({ abbr: true }) // 'Feb' + * faker.date.month({ context: true }) // 'June' + * faker.date.month({ abbr: true, context: true }) // 'Sep' + */ + month(options?: { abbr?: boolean; context?: boolean }): string { + const abbr = options?.abbr ?? false; + const context = options?.context ?? false; + + const source = this.faker.definitions.date.month; + let type: keyof DateEntryDefinition; + if (abbr) { + if (context && source['abbr_context'] != null) { + type = 'abbr_context'; + } else { + type = 'abbr'; + } + } else if (context && source['wide_context'] != null) { + type = 'wide_context'; + } else { + type = 'wide'; + } + + return this.faker.helpers.arrayElement(source[type]); + } + + /** + * Returns a random day of the week. + * + * @param options The optional options to use. + * @param options.abbr Whether to return an abbreviation. Defaults to `false`. + * @param options.context Whether to return the day of the week in a context. Defaults to `false`. + * + * @example + * faker.date.weekday() // 'Monday' + * faker.date.weekday({ abbr: true }) // 'Thu' + * faker.date.weekday({ context: true }) // 'Thursday' + * faker.date.weekday({ abbr: true, context: true }) // 'Fri' + */ + weekday(options?: { abbr?: boolean; context?: boolean }): string { + const abbr = options?.abbr ?? false; + const context = options?.context ?? false; + + const source = this.faker.definitions.date.weekday; + let type: keyof DateEntryDefinition; + if (abbr) { + if (context && source['abbr_context'] != null) { + type = 'abbr_context'; + } else { + type = 'abbr'; + } + } else if (context && source['wide_context'] != null) { + type = 'wide_context'; + } else { + type = 'wide'; + } + + return this.faker.helpers.arrayElement(source[type]); + } +} diff --git a/src/modules/fake/index.ts b/src/modules/fake/index.ts new file mode 100644 index 00000000..a9f2a22a --- /dev/null +++ b/src/modules/fake/index.ts @@ -0,0 +1,134 @@ +import type { Faker } from '../..'; +import { FakerError } from '../../errors/faker-error'; + +/** + * Generator method for combining faker methods based on string input. + */ +export class Fake { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Fake.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generator for combining faker methods based on a static string input. + * + * Note: We recommend using string template literals instead of `fake()`, + * which are faster and strongly typed (if you are using TypeScript), + * e.g. ``const address = `${faker.address.zipCode()} ${faker.address.city()}`;`` + * + * This method is useful if you have to build a random string from a static, non-executable source + * (e.g. string coming from a user, stored in a database or a file). + * + * It checks the given string for placeholders and replaces them by calling faker methods: + * + * ```js + * const hello = faker.fake('Hi, my name is {{name.firstName}} {{name.lastName}}!') + * ``` + * + * This would use the `faker.name.firstName()` and `faker.name.lastName()` method to resolve the placeholders respectively. + * + * It is also possible to provide parameters. At first, they will be parsed as json, + * and if that isn't possible, we will fall back to string: + * + * ```js + * const message = faker.fake(`You can call me at {{phone.phoneNumber(+!# !## #### #####!)}}.') + * ``` + * + * Currently it is not possible to set more than a single parameter. + * + * It is also NOT possible to use any non-faker methods or plain javascript in such templates. + * + * @param str The template string that will get interpolated. Must not be empty. + * + * @see faker.helpers.mustache() to use custom functions for resolution. + * + * @example + * faker.fake('{{name.lastName}}') // 'Barrows' + * faker.fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}') // 'Durgan, Noe MD' + * faker.fake('This is static test.') // 'This is static test.' + * faker.fake('Good Morning {{name.firstName}}!') // 'Good Morning Estelle!' + * faker.fake('You can call me at {{phone.phoneNumber(!## ### #####!)}}.') // 'You can call me at 202 555 973722.' + * faker.fake('I flipped the coin an got: {{helpers.arrayElement(["heads", "tails"])}}') // 'I flipped the coin an got: tails' + */ + fake(str: string): string { + // if incoming str parameter is not provided, return error message + if (typeof str !== 'string' || str.length === 0) { + throw new FakerError('string parameter is required!'); + } + + // find first matching {{ and }} + const start = str.search(/{{[a-z]/); + const end = str.indexOf('}}', start); + + // if no {{ and }} is found, we are done + if (start === -1 || end === -1) { + return str; + } + + // extract method name from between the {{ }} that we found + // for example: {{name.firstName}} + const token = str.substring(start + 2, end + 2); + let method = token.replace('}}', '').replace('{{', ''); + + // extract method parameters + const regExp = /\(([^)]+)\)/; + const matches = regExp.exec(method); + let parameters = ''; + if (matches) { + method = method.replace(regExp, ''); + parameters = matches[1]; + } + + // split the method into module and function + const parts = method.split('.'); + + if (this.faker[parts[0]] == null) { + throw new FakerError(`Invalid module: ${parts[0]}`); + } + + if (this.faker[parts[0]][parts[1]] == null) { + throw new FakerError(`Invalid method: ${parts[0]}.${parts[1]}`); + } + + // assign the function from the module.function namespace + let fn: (args?: unknown) => string = this.faker[parts[0]][parts[1]]; + fn = fn.bind(this); + + // If parameters are populated here, they are always going to be of string type + // since we might actually be dealing with an object or array, + // we always attempt to the parse the incoming parameters into JSON + let params: unknown; + // Note: we experience a small performance hit here due to JSON.parse try / catch + // If anyone actually needs to optimize this specific code path, please open a support issue on github + try { + params = JSON.parse(parameters); + } catch (err) { + // since JSON.parse threw an error, assume parameters was actually a string + params = parameters; + } + + let result: string; + if (typeof params === 'string' && params.length === 0) { + result = String(fn()); + } else { + result = String(fn(params)); + } + + // Replace the found tag with the returned fake value + // We cannot use string.replace here because the result might contain evaluated characters + const res = str.substring(0, start) + result + str.substring(end + 2); + + if (res === '') { + return ''; + } + + // return the response recursively until we are done finding all tags + return this.fake(res); + } +} diff --git a/src/modules/finance/iban.ts b/src/modules/finance/iban.ts new file mode 100644 index 00000000..9b4780c5 --- /dev/null +++ b/src/modules/finance/iban.ts @@ -0,0 +1,1414 @@ +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; +} + +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: 'c', + 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: '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: 'c', + 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: 'c', + 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 (let i = 0; i < digitStr.length; i++) { + m = (m * 10 + +digitStr[i]) % 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.replace(/[A-Z]/gi, (match) => + String(match.toUpperCase().charCodeAt(0) - 55) + ), +}; + +export default iban; diff --git a/src/modules/finance/index.ts b/src/modules/finance/index.ts new file mode 100644 index 00000000..a74dceba --- /dev/null +++ b/src/modules/finance/index.ts @@ -0,0 +1,436 @@ +import type { Faker } from '../..'; +import { FakerError } from '../../errors/faker-error'; +import iban from './iban'; + +/** + * Module to generate finance related entries. + */ +export class Finance { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Finance.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a random account number. + * + * @param length The length of the account number. Defaults to `8`. + * + * @example + * faker.finance.account() // 92842238 + * faker.finance.account(5) // 32564 + */ + account(length?: number): string { + length = length || 8; + let template = ''; + + for (let i = 0; i < length; i++) { + template += '#'; + } + length = null; + return this.faker.helpers.replaceSymbolWithNumber(template); + } + + /** + * Generates a random account name. + * + * @example + * faker.finance.accountName() // 'Personal Loan Account' + */ + accountName(): string { + return [ + this.faker.helpers.arrayElement( + this.faker.definitions.finance.account_type + ), + 'Account', + ].join(' '); + } + + /** + * Generates a random routing number. + * + * @example + * faker.finance.routingNumber() // '522814402' + */ + routingNumber(): string { + const routingNumber = + this.faker.helpers.replaceSymbolWithNumber('########'); + + // 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}`; + } + + /** + * Generates a random masked number. + * + * @param length The length of the unmasked number. Defaults to `4`. + * @param parens Whether to use surrounding parenthesis. Defaults to `true`. + * @param ellipsis Whether to prefix the numbers with an ellipsis. Defaults to `true`. + * + * @example + * faker.finance.mask() // '(...9711)' + * faker.finance.mask(3) // '(...342)' + * faker.finance.mask(3, false) // '...236' + * faker.finance.mask(3, false, false) // '298' + */ + mask(length?: number, parens?: boolean, ellipsis?: boolean): string { + // set defaults + length = length || 4; + parens = parens == null ? true : parens; + ellipsis = ellipsis == null ? true : ellipsis; + + // create a template for length + let template = ''; + + for (let i = 0; i < length; i++) { + template = `${template}#`; + } + + //prefix with ellipsis + template = ellipsis ? ['...', template].join('') : template; + + template = parens ? ['(', template, ')'].join('') : template; + + //generate random numbers + template = this.faker.helpers.replaceSymbolWithNumber(template); + + return template; + } + + /** + * Generates a random amount between the given bounds (inclusive). + * + * @param min The lower bound for the amount. Defaults to `0`. + * @param max The upper bound for the amount. Defaults to `1000`. + * @param dec The number of decimal places for the amount. Defaults to `2`. + * @param symbol The symbol used to prefix the amount. Defaults to `''`. + * @param autoFormat If true this method will use `Number.toLocaleString()`. Otherwise it will use `Number.toFixed()`. + * + * @example + * faker.finance.amount() // '617.87' + * faker.finance.amount(5, 10) // '5.53' + * faker.finance.amount(5, 10, 0) // '8' + * faker.finance.amount(5, 10, 2, '$') // '$5.85' + * faker.finance.amount(5, 10, 5, '', true) // '9,75067' + */ + amount( + min: number = 0, + max: number = 1000, + dec: number = 2, + symbol: string = '', + autoFormat?: boolean + ): string { + const randValue = this.faker.datatype.number({ + max, + min, + precision: Math.pow(10, -dec), + }); + + let formattedString: string; + if (autoFormat) { + formattedString = randValue.toLocaleString(undefined, { + minimumFractionDigits: dec, + }); + } else { + formattedString = randValue.toFixed(dec); + } + + return symbol + formattedString; + } + + /** + * Returns a random transaction type. + * + * @example + * faker.finance.transactionType() // 'payment' + */ + transactionType(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.finance.transaction_type + ); + } + + /** + * Returns a random currency code. + * (The short text/abbreviation for the currency (e.g. `US Dollar` -> `USD`)) + * + * @example + * faker.finance.currencyCode() // 'USD' + */ + currencyCode(): string { + return this.faker.helpers.objectValue( + this.faker.definitions.finance.currency + )['code']; + } + + /** + * Returns a random currency name. + * + * @example + * faker.finance.currencyName() // 'US Dollar' + */ + currencyName(): string { + return this.faker.helpers.objectKey( + this.faker.definitions.finance.currency + ) as string; + } + + /** + * Returns a random currency symbol. + * + * @example + * faker.finance.currencySymbol() // '$' + */ + currencySymbol(): string { + let symbol: string; + while (!symbol) { + symbol = this.faker.helpers.objectValue( + this.faker.definitions.finance.currency + )['symbol']; + } + return symbol; + } + + /** + * Generates a random bitcoin address. + * + * @example + * faker.finance.bitcoinAddress() // '3ySdvCkTLVy7gKD4j6JfSaf5d' + */ + bitcoinAddress(): string { + const addressLength = this.faker.datatype.number({ min: 25, max: 34 }); + + let address = this.faker.helpers.arrayElement(['1', '3']); + + for (let i = 0; i < addressLength - 1; i++) + address += this.faker.helpers.arrayElement( + '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'.split('') + ); + + return address; + } + + /** + * Generates a random litecoin address. + * + * @example + * faker.finance.litecoinAddress() // 'MoQaSTGWBRXkWfyxKbNKuPrAWGELzcW' + */ + litecoinAddress(): string { + const addressLength = this.faker.datatype.number({ min: 26, max: 33 }); + + let address = this.faker.helpers.arrayElement(['L', 'M', '3']); + + for (let i = 0; i < addressLength - 1; i++) + address += this.faker.helpers.arrayElement( + '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'.split('') + ); + + return address; + } + + /** + * Generates a random credit card number. + * + * @param issuer The name of the issuer (case insensitive) or the format used to generate one. + * + * @example + * faker.finance.creditCardNumber() // '4427163488668' + * faker.finance.creditCardNumber('visa') // '4882664999003' + * faker.finance.creditCardNumber('63[7-9]#-####-####-###L') // '6375-3265-4676-6644' + */ + creditCardNumber(issuer = ''): string { + 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.match(/#/)) { + // 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.replace(/\//g, ''); + return this.faker.helpers.replaceCreditCardSymbols(format); + } + + /** + * Generates a random credit card CVV. + * + * @example + * faker.finance.creditCardCVV() // '506' + */ + creditCardCVV(): string { + let cvv = ''; + for (let i = 0; i < 3; i++) { + cvv += this.faker.datatype.number({ max: 9 }).toString(); + } + return cvv; + } + + /** + * Returns a random credit card issuer. + * + * @example + * faker.finance.creditCardIssuer() // 'discover' + */ + creditCardIssuer(): string { + return this.faker.helpers.objectKey( + this.faker.definitions.finance.credit_card + ) as string; + } + + /** + * Generates a random PIN number. + * + * @param length The length of the PIN to generate. Defaults to `4`. + * @throws Will throw an error if length is less than 1. + * + * @example + * faker.finance.pin() // '5067' + * faker.finance.pin(6) // '213789' + */ + pin(length: number = 4): string { + if (length < 1) { + throw new FakerError('minimum length is 1'); + } + return Array.from({ length }, () => this.faker.datatype.number(9)).join(''); + } + + /** + * Generates a random ethereum Address. + * + * @example + * faker.finance.ethereumAddress() // '0xf03dfeecbafc5147241cc4c4ca20b3c9dfd04c4a' + */ + ethereumAddress(): string { + const address = this.faker.datatype.hexadecimal(40).toLowerCase(); + return address; + } + + /** + * Generates a random iban. + * + * @param formatted Return a formatted version of the generated IBAN. Defaults to `false`. + * @param countryCode The country code from which you want to generate an IBAN, if none is provided a random country will be used. + * @throws Will throw an error if the passed country code is not supported. + * + * @example + * faker.finance.iban() // 'TR736918640040966092800056' + * faker.finance.iban(true) // 'FR20 8008 2330 8984 74S3 Z620 224' + * faker.finance.iban(true, 'DE') // 'DE84 1022 7075 0900 1170 01' + */ + iban(formatted: boolean = false, countryCode?: string): string { + 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.number(100) < 80) { + s += this.faker.datatype.number(9); + } else { + s += this.faker.helpers.arrayElement(iban.alpha); + } + } else { + if (c >= 3 && this.faker.datatype.number(100) < 30) { + 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.datatype.number(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 ? result.match(/.{1,4}/g).join(' ') : result; + } + + /** + * Generates a random bic. + * + * @example + * faker.finance.bic() // 'WYAUPGX1432' + */ + bic(): string { + const vowels = ['A', 'E', 'I', 'O', 'U']; + const prob = this.faker.datatype.number(100); + + return [ + this.faker.helpers.replaceSymbols('???'), + this.faker.helpers.arrayElement(vowels), + this.faker.helpers.arrayElement(iban.iso3166), + this.faker.helpers.replaceSymbols('?'), + '1', + prob < 10 + ? this.faker.helpers.replaceSymbols( + `?${this.faker.helpers.arrayElement(vowels)}?` + ) + : prob < 40 + ? this.faker.helpers.replaceSymbols('###') + : '', + ].join(''); + } + + /** + * Generates a random transaction description. + * + * @example + * faker.finance.transactionDescription() + * // 'invoice transaction at Kilback - Durgan using card ending with ***(...4316) for UAH 783.82 in account ***16168663' + */ + transactionDescription(): string { + const transaction = this.faker.helpers.createTransaction(); + const account = transaction.account; + const amount = transaction.amount; + const transactionType = transaction.type; + const company = transaction.business; + const card = this.mask(); + const currency = this.currencyCode(); + + return `${transactionType} transaction at ${company} using card ending with ***${card} for ${currency} ${amount} in account ***${account}`; + } +} diff --git a/src/modules/git/index.ts b/src/modules/git/index.ts new file mode 100644 index 00000000..e86f6648 --- /dev/null +++ b/src/modules/git/index.ts @@ -0,0 +1,134 @@ +import type { Faker } from '../..'; + +/** + * Module to generate git related entries. + */ +export class Git { + private hexChars = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + ]; + + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Git.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a random branch name. + * + * @example + * faker.git.branch() // 'feed-parse' + */ + branch(): string { + const noun = this.faker.hacker.noun().replace(' ', '-'); + const verb = this.faker.hacker.verb().replace(' ', '-'); + return `${noun}-${verb}`; + } + + /** + * Generates a random commit entry. + * + * @param options Options for the commit entry. + * @param options.merge Set to `true` to generate a merge message line. + * @param options.eol Choose the end of line character to use. Defaults to 'CRLF'. + * 'LF' = '\n', + * 'CRLF' = '\r\n' + * + * @example + * faker.git.commitEntry() + * // commit fe8c38a965d13d9794eb36918cb24cebe49a45c2 + * // Author: Mable Harvey <[email protected]> + * // Date: Sat Feb 05 2022 15:09:18 GMT+0100 (Mitteleuropäische Normalzeit) + * // + * // copy primary system + */ + commitEntry( + options: { + merge?: boolean; + eol?: 'LF' | 'CRLF'; + } = {} + ): string { + const lines = [`commit ${this.faker.git.commitSha()}`]; + + if (options.merge || this.faker.datatype.number({ min: 0, max: 4 }) === 0) { + lines.push(`Merge: ${this.shortSha()} ${this.shortSha()}`); + } + + lines.push( + `Author: ${this.faker.name.firstName()} ${this.faker.name.lastName()} <${this.faker.internet.email()}>`, + `Date: ${this.faker.date.recent().toString()}`, + '', + `\xa0\xa0\xa0\xa0${this.commitMessage()}`, + // to end with a eol char + '' + ); + + const eolOption = options.eol ?? 'CRLF'; + const eolChar = eolOption === 'CRLF' ? '\r\n' : '\n'; + const entry = lines.join(eolChar); + + return entry; + } + + /** + * Generates a random commit message. + * + * @example + * faker.git.commitMessage() // 'reboot cross-platform driver' + */ + commitMessage(): string { + return `${this.faker.hacker.verb()} ${this.faker.hacker.adjective()} ${this.faker.hacker.noun()}`; + } + + /** + * Generates a random commit sha (full). + * + * @example + * faker.git.commitSha() // '2c6e3880fd94ddb7ef72d34e683cdc0c47bec6e6' + */ + commitSha(): string { + let commit = ''; + + for (let i = 0; i < 40; i++) { + commit += this.faker.helpers.arrayElement(this.hexChars); + } + + return commit; + } + + /** + * Generates a random commit sha (short). + * + * @example + * faker.git.shortSha() // '6155732' + */ + shortSha(): string { + let shortSha = ''; + + for (let i = 0; i < 7; i++) { + shortSha += this.faker.helpers.arrayElement(this.hexChars); + } + + return shortSha; + } +} diff --git a/src/modules/hacker/index.ts b/src/modules/hacker/index.ts new file mode 100644 index 00000000..ee1bbc5c --- /dev/null +++ b/src/modules/hacker/index.ts @@ -0,0 +1,94 @@ +import type { Faker } from '../..'; + +/** + * Module to generate hacker/IT words and phrases. + */ +export class Hacker { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Hacker.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random hacker/IT abbreviation. + * + * @example + * faker.hacker.abbreviation() // 'THX' + */ + abbreviation(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.hacker.abbreviation + ); + } + + /** + * Returns a random hacker/IT adjective. + * + * @example + * faker.hacker.adjective() // 'cross-platform' + */ + adjective(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.hacker.adjective + ); + } + + /** + * Returns a random hacker/IT noun. + * + * @example + * faker.hacker.noun() // 'system' + */ + noun(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.hacker.noun); + } + + /** + * Returns a random hacker/IT verb. + * + * @example + * faker.hacker.verb() // 'copy' + */ + verb(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.hacker.verb); + } + + /** + * Returns a random hacker/IT verb for continuous actions (en: ing suffix; e.g. hacking). + * + * @example + * faker.hacker.ingverb() // 'navigating' + */ + ingverb(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.hacker.ingverb + ); + } + + /** + * Generates a random hacker/IT phrase. + * + * @example + * faker.hacker.phrase() + * // 'If we override the card, we can get to the HDD feed through the back-end HDD sensor!' + */ + phrase(): string { + const data = { + abbreviation: this.abbreviation, + adjective: this.adjective, + ingverb: this.ingverb, + noun: this.noun, + verb: this.verb, + }; + + const phrase = this.faker.helpers.arrayElement( + this.faker.definitions.hacker.phrase + ); + return this.faker.helpers.mustache(phrase, data); + } +} diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts new file mode 100644 index 00000000..1b28ddfa --- /dev/null +++ b/src/modules/helpers/index.ts @@ -0,0 +1,810 @@ +import type { Faker } from '../..'; +import { deprecated } from '../../internal/deprecated'; + +/** + * A full card with various details. + */ +export interface Card { + name: string; + username: string; + email: string; + address: { + streetA: string; + streetB: string; + streetC: string; + streetD: string; + city: string; + state: string; + country: string; + zipcode: string; + geo: { + lat: string; + lng: string; + }; + }; + phone: string; + website: string; + company: { + name: string; + catchPhrase: string; + bs: string; + }; + posts: Array<{ + words: string; + sentence: string; + sentences: string; + paragraph: string; + }>; + accountHistory: Array<{ + amount: string; + date: Date; + business: string; + name: string; + type: string; + account: string; + }>; +} + +/** + * A persons card with various details attempting to use a consistent context. + */ +export interface ContextualCard { + name: string; + username: string; + avatar: string; + email: string; + dob: Date; + phone: string; + address: { + street: string; + suite: string; + city: string; + zipcode: string; + geo: { + lat: string; + lng: string; + }; + }; + website: string; + company: { + name: string; + catchPhrase: string; + bs: string; + }; +} + +/** + * A user card with various details. + */ +export interface UserCard { + name: string; + username: string; + email: string; + address: { + street: string; + suite: string; + city: string; + zipcode: string; + geo: { + lat: string; + lng: string; + }; + }; + phone: string; + website: string; + company: { + name: string; + catchPhrase: string; + bs: string; + }; +} + +/** + * A transaction info. + */ +export interface Transaction { + amount: string; + date: Date; + business: string; + name: string; + type: string; + account: string; +} + +/** + * Module with various helper methods that transform the method input rather than returning values from locales. + * The transformation process may call methods that use the locale data. + */ +export class Helpers { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Helpers.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Backward-compatibility. Use `faker.helpers.arrayElement()` instead. + * + * Takes an array and returns a random element of the array. + * + * @template T The type of the entries to pick from. + * @param array The array to select an element from. + * + * @see faker.helpers.arrayElement() + * + * @example + * faker.helpers.randomize() // 'c' + * faker.helpers.randomize([1, 2, 3]) // '2' + * + * @deprecated + */ + randomize<T = string>( + array: ReadonlyArray<T> = ['a', 'b', 'c'] as unknown as ReadonlyArray<T> + ): T { + deprecated({ + deprecated: 'faker.helpers.randomize()', + proposed: 'faker.helpers.arrayElement()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.arrayElement(array); + } + + /** + * 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 string The input to slugify. + * + * @example + * faker.helpers.slugify() // '' + * faker.helpers.slugify("Hello world!") // 'Hello-world' + */ + slugify(string: string = ''): string { + return string + .replace(/ /g, '-') + .replace(/[^\一-龠\ぁ-ゔ\ァ-ヴー\w\.\-]+/g, ''); + } + + /** + * Parses the given string symbol by symbol and replaces the placeholders with digits (`0` - `9`). + * `!` will be replaced by digits >=2 (`2` - `9`). + * + * @param string The template string to parse. + * @param symbol The symbol to replace with digits. Defaults to `'#'`. + * + * @example + * faker.helpers.replaceSymbolWithNumber() // '' + * faker.helpers.replaceSymbolWithNumber('#####') // '04812' + * faker.helpers.replaceSymbolWithNumber('!####') // '27378' + * faker.helpers.replaceSymbolWithNumber('Your pin is: !####') // '29841' + */ + replaceSymbolWithNumber(string: string = '', symbol: string = '#'): string { + let str = ''; + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === symbol) { + str += this.faker.datatype.number(9); + } else if (string.charAt(i) === '!') { + str += this.faker.datatype.number({ min: 2, max: 9 }); + } else { + str += string.charAt(i); + } + } + return str; + } + + /** + * Parses the given string symbol by symbols 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 string The template string to parse. + * + * @example + * faker.helpers.replaceSymbols() // '' + * faker.helpers.replaceSymbols('#####') // '98441' + * faker.helpers.replaceSymbols('?????') // 'ZYRQQ' + * faker.helpers.replaceSymbols('*****') // '4Z3P7' + * faker.helpers.replaceSymbols('Your pin is: #?*#?*') // '0T85L1' + */ + 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 str = ''; + + for (let i = 0; i < string.length; i++) { + if (string.charAt(i) === '#') { + str += this.faker.datatype.number(9); + } else if (string.charAt(i) === '?') { + str += this.arrayElement(alpha); + } else if (string.charAt(i) === '*') { + str += this.faker.datatype.boolean() + ? this.arrayElement(alpha) + : this.faker.datatype.number(9); + } else { + str += string.charAt(i); + } + } + return str; + } + + /** + * 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 string The credit card format pattern. Defaults to `6453-####-####-####-###L`. + * @param symbol The symbol to replace with a digit. + * + * @example + * faker.helpers.replaceCreditCardSymbols() // '6453-4876-8626-8995-3779' + * faker.helpers.replaceCreditCardSymbols('1234-[4-9]-##!!-L') // '1234-9-5298-2' + */ + replaceCreditCardSymbols( + string: string = '6453-####-####-####-###L', + symbol: string = '#' + ): string { + // default values required for calling method without arguments + + // Function calculating the Luhn checksum of a number string + const getCheckBit = (number: number[]) => { + number.reverse(); + number = number.map((num, index) => { + if (index % 2 === 0) { + num *= 2; + if (num > 9) { + num -= 9; + } + } + return num; + }); + const sum = number.reduce((prev, curr) => prev + curr); + return sum % 10; + }; + + string = this.regexpStyleStringParse(string); // replace [4-9] with a random number in range etc... + string = this.replaceSymbolWithNumber(string, symbol); // replace ### with random numbers + + const numberList = string + .replace(/\D/g, '') + .split('') + .map((num) => parseInt(num)); + const checkNum = getCheckBit(numberList); + return string.replace('L', String(checkNum)); + } + + /** + * Repeats the given string the given number of times. + * + * @param string The string to repeat. Defaults to `''`. + * @param num The number of times to repeat it. Defaults to `0`. + * + * @example + * faker.helpers.repeatString('Hello world! ') // '' + * faker.helpers.repeatString('Hello world! ', 1) // 'Hello world! ' + * faker.helpers.repeatString('Hello world! ', 2) // 'Hello world! Hello world! ' + */ + repeatString(string = '', num = 0): string { + let text = ''; + for (let i = 0; i < num; i++) { + text += string.toString(); + } + return text; + } + + /** + * Replaces the regex like expressions in the given string with matching values. + * + * 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). + * + * @param string The template string to to parse. + * + * @example + * faker.helpers.regexpStyleStringParse() // '' + * faker.helpers.regexpStyleStringParse('#{5}') // '#####' + * faker.helpers.regexpStyleStringParse('#{2,9}') // '#######' + * faker.helpers.regexpStyleStringParse('[500-15000]') // '8375' + * faker.helpers.regexpStyleStringParse('#{3}test[1-5]') // '###test3' + */ + regexpStyleStringParse(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 = string.match(RANGE_REP_REG); + while (token != null) { + min = parseInt(token[2]); + max = parseInt(token[3]); + // switch min and max + if (min > max) { + tmp = max; + max = min; + min = tmp; + } + repetitions = this.faker.datatype.number({ min: min, max: max }); + string = + string.slice(0, token.index) + + this.repeatString(token[1], repetitions) + + string.slice(token.index + token[0].length); + token = string.match(RANGE_REP_REG); + } + // Deal with repeat `{num}` + token = string.match(REP_REG); + while (token != null) { + repetitions = parseInt(token[2]); + string = + string.slice(0, token.index) + + this.repeatString(token[1], repetitions) + + string.slice(token.index + token[0].length); + token = string.match(REP_REG); + } + // Deal with range `[min-max]` (only works with numbers for now) + //TODO: implement for letters e.g. [0-9a-zA-Z] etc. + + token = string.match(RANGE_REG); + while (token != null) { + min = parseInt(token[1]); // This time we are not capturing the char before `[]` + max = parseInt(token[2]); + // switch min and max + if (min > max) { + tmp = max; + max = min; + min = tmp; + } + string = + string.slice(0, token.index) + + this.faker.datatype.number({ min: min, max: max }).toString() + + string.slice(token.index + token[0].length); + token = string.match(RANGE_REG); + } + return string; + } + + /** + * Takes an array and randomizes it in place then returns it. + * + * Uses the modern version of the Fisher–Yates algorithm. + * + * @template T The type of the entries to shuffle. + * @param o The array to shuffle. Defaults to `[]`. + * + * @example + * faker.helpers.shuffle() // [] + * faker.helpers.shuffle(['a', 'b', 'c']) // [ 'b', 'c', 'a' ] + */ + shuffle<T>(o?: T[]): T[] { + if (o == null || o.length === 0) { + return o || []; + } + + for (let i = o.length - 1; i > 0; --i) { + const j = this.faker.datatype.number(i); + const x = o[i]; + o[i] = o[j]; + o[j] = x; + } + return o; + } + + /** + * 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. + * + * @template T The type of the entries. + * @param source The strings to choose from or a function that generates a string. + * @param length The number of elements to generate. + * + * @example + * faker.helpers.uniqueArray(faker.random.word, 50) + * faker.helpers.uniqueArray(faker.definitions.name.first_name, 6) + * faker.helpers.uniqueArray(["Hello", "World", "Goodbye"], 2) + */ + uniqueArray<T>(source: readonly T[] | (() => T), length: number): T[] { + if (Array.isArray(source)) { + const set = new Set<T>(source); + const array = Array.from(set); + return this.shuffle(array).splice(0, length); + } + const set = new Set<T>(); + try { + if (typeof source === 'function') { + while (set.size < length) { + set.add(source()); + } + } + } catch { + // Ignore + } + return Array.from(set); + } + + /** + * Replaces the `{{placeholder}}` patterns in the given string mustache style. + * + * @param str 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 + * faker.helpers.mustache('I found {{count}} instances of "{{word}}".', { + * count: () => `${faker.datatype.number()}`, + * word: "this word", + * }) // 'I found 57591 instances of "this word".' + */ + mustache( + str: string | undefined, + data: Record<string, string | Parameters<string['replace']>[1]> + ): string { + if (str == null) { + return ''; + } + for (const p in data) { + const re = new RegExp(`{{${p}}}`, 'g'); + const value = data[p]; + if (typeof value === 'string') { + str = str.replace(re, value); + } else { + str = str.replace(re, value); + } + } + return str; + } + + /** + * Generates a full card with various random details. + * + * @example + * faker.helpers.createCard() + * // { + * // name: 'Maxine Abbott', + * // username: 'Idell_Kautzer60', + * // email: '[email protected]', + * // address: { + * // streetA: 'Drake Avenue', + * // ... + * @deprecated If you need some specific object you should create your own method. + */ + createCard(): Card { + deprecated({ + deprecated: 'helpers.createCard()', + proposed: 'a self-build function', + since: 'v6.1.0', + until: 'v7.0.0', + }); + return { + name: this.faker.name.findName(), + username: this.faker.internet.userName(), + email: this.faker.internet.email(), + address: { + streetA: this.faker.address.streetName(), + streetB: this.faker.address.streetAddress(), + streetC: this.faker.address.streetAddress(true), + streetD: this.faker.address.secondaryAddress(), + city: this.faker.address.city(), + state: this.faker.address.state(), + country: this.faker.address.country(), + zipcode: this.faker.address.zipCode(), + geo: { + lat: this.faker.address.latitude(), + lng: this.faker.address.longitude(), + }, + }, + phone: this.faker.phone.phoneNumber(), + website: this.faker.internet.domainName(), + company: { + name: this.faker.company.companyName(), + catchPhrase: this.faker.company.catchPhrase(), + bs: this.faker.company.bs(), + }, + posts: [ + { + words: this.faker.lorem.words(), + sentence: this.faker.lorem.sentence(), + sentences: this.faker.lorem.sentences(), + paragraph: this.faker.lorem.paragraph(), + }, + { + words: this.faker.lorem.words(), + sentence: this.faker.lorem.sentence(), + sentences: this.faker.lorem.sentences(), + paragraph: this.faker.lorem.paragraph(), + }, + { + words: this.faker.lorem.words(), + sentence: this.faker.lorem.sentence(), + sentences: this.faker.lorem.sentences(), + paragraph: this.faker.lorem.paragraph(), + }, + ], + accountHistory: [ + this.createTransaction(), + this.createTransaction(), + this.createTransaction(), + ], + }; + } + + /** + * Generates a persons card with various details attempting to use a consistent context. + * + * @example + * faker.helpers.contextualCard() + * // { + * // name: 'Eveline', + * // username: 'Eveline.Brekke56', + * // avatar: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/122.jpg', + * // email: '[email protected]', + * // dob: 1964-05-06T05:14:37.874Z, + * // ... + * @deprecated If you need some specific object you should create your own method. + */ + contextualCard(): ContextualCard { + deprecated({ + deprecated: 'helpers.contextualCard()', + proposed: 'a self-build function', + since: 'v6.1.0', + until: 'v7.0.0', + }); + const name = this.faker.name.firstName(); + const userName = this.faker.internet.userName(name); + return { + name: name, + username: userName, + avatar: this.faker.internet.avatar(), + email: this.faker.internet.email(userName), + dob: this.faker.date.past( + 50, + new Date('Sat Sep 20 1992 21:35:02 GMT+0200 (CEST)') + ), + phone: this.faker.phone.phoneNumber(), + address: { + street: this.faker.address.streetName(), + suite: this.faker.address.secondaryAddress(), + city: this.faker.address.city(), + zipcode: this.faker.address.zipCode(), + geo: { + lat: this.faker.address.latitude(), + lng: this.faker.address.longitude(), + }, + }, + website: this.faker.internet.domainName(), + company: { + name: this.faker.company.companyName(), + catchPhrase: this.faker.company.catchPhrase(), + bs: this.faker.company.bs(), + }, + }; + } + + /** + * Generates a user card with various details. + * + * @example + * faker.helpers.userCard() + * // { + * // name: 'Jodi Ferry', + * // username: 'Maybell.Kris', + * // email: '[email protected]', + * // address: { + * // street: 'McKenzie Estates', + * // .... + * @deprecated If you need some specific object you should create your own method. + */ + userCard(): UserCard { + deprecated({ + deprecated: 'helpers.userCard()', + proposed: 'a self-build function', + since: 'v6.1.0', + until: 'v7.0.0', + }); + return { + name: this.faker.name.findName(), + username: this.faker.internet.userName(), + email: this.faker.internet.email(), + address: { + street: this.faker.address.streetName(), + suite: this.faker.address.secondaryAddress(), + city: this.faker.address.city(), + zipcode: this.faker.address.zipCode(), + geo: { + lat: this.faker.address.latitude(), + lng: this.faker.address.longitude(), + }, + }, + phone: this.faker.phone.phoneNumber(), + website: this.faker.internet.domainName(), + company: { + name: this.faker.company.companyName(), + catchPhrase: this.faker.company.catchPhrase(), + bs: this.faker.company.bs(), + }, + }; + } + + /** + * Generates an example transaction. + * + * @example + * faker.helpers.createTransaction() + * // { + * // amount: '551.32', + * // date: 2012-02-01T23:00:00.000Z, + * // business: 'Will, Fisher and Marks', + * // name: 'Investment Account (...8755)', + * // type: 'invoice', + * // account: '41796240' + * // } + */ + createTransaction(): Transaction { + return { + amount: this.faker.finance.amount(), + date: new Date(2012, 1, 2), // TODO: add a ranged date method + business: this.faker.company.companyName(), + name: [this.faker.finance.accountName(), this.faker.finance.mask()].join( + ' ' + ), + type: this.arrayElement(this.faker.definitions.finance.transaction_type), + account: this.faker.finance.account(), + }; + } + + /** + * Returns the result of the callback if the probability check was successful, otherwise `undefined`. + * + * @template T The type of result of the given callback. + * @param callback The callback to that will be invoked if the probability check was successful. + * @param options The options to use. Defaults to `{}`. + * @param options.probability The probability (`[0.00, 1.00]`) of the callback being invoked. Defaults to `0.5`. + * + * @example + * faker.helpers.maybe(() => 'Hello World!') // 'Hello World!' + * faker.helpers.maybe(() => 'Hello World!', { probability: 0.1 }) // undefined + * faker.helpers.maybe(() => 'Hello World!', { probability: 0.9 }) // 'Hello World!' + */ + maybe<T>( + callback: () => T, + options: { probability?: number } = {} + ): T | undefined { + const { probability = 0.5 } = options; + if (this.faker.datatype.float({ min: 0, max: 1 }) < probability) { + return callback(); + } + return undefined; + } + + /** + * Returns a random key from given object or `undefined` if no key could be found. + * + * @param object The object to be used. + * + * @example + * faker.helpers.objectKey({ myProperty: 'myValue' }) // 'myProperty' + */ + objectKey<T extends Record<string, unknown>>(object: T): keyof T { + const array: Array<keyof T> = Object.keys(object); + return this.arrayElement(array); + } + + /** + * Returns a random value from given object or `undefined` if no key could be found. + * + * @param object The object to be used. + * + * @example + * faker.helpers.objectValue({ myProperty: 'myValue' }) // 'myValue' + */ + objectValue<T extends Record<string, unknown>>(object: T): T[keyof T] { + const key = this.faker.helpers.objectKey(object); + return object[key]; + } + + /** + * Returns random element from the given array. + * + * @template T The type of the entries to pick from. + * @param array Array to pick the value from. + * + * @example + * faker.helpers.arrayElement(['cat', 'dog', 'mouse']) // 'dog' + */ + arrayElement<T = string>( + // TODO @Shinigami92 2022-04-30: We want to remove this default value, but currently it's not possible because some definitions could be empty + // See https://github.com/faker-js/faker/issues/893 + array: ReadonlyArray<T> = ['a', 'b', 'c'] as unknown as ReadonlyArray<T> + ): T { + const index = + array.length > 1 + ? this.faker.datatype.number({ max: array.length - 1 }) + : 0; + + return array[index]; + } + + /** + * Returns a subset with random elements of the given array in random order. + * + * @template T The type of the entries to pick from. + * @param array Array to pick the value from. + * @param count Number 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 + * faker.helpers.arrayElements(['cat', 'dog', 'mouse']) // ['mouse', 'cat'] + * faker.helpers.arrayElements([1, 2, 3, 4, 5], 2) // [4, 2] + */ + arrayElements<T>( + // TODO @Shinigami92 2022-04-30: We want to remove this default value, but currently it's not possible because some definitions could be empty + // See https://github.com/faker-js/faker/issues/893 + array: ReadonlyArray<T> = ['a', 'b', 'c'] as unknown as ReadonlyArray<T>, + count?: number + ): T[] { + if (typeof count !== 'number') { + count = this.faker.datatype.number({ min: 1, max: array.length }); + } else if (count > array.length) { + count = array.length; + } else if (count < 0) { + count = 0; + } + + const arrayCopy = array.slice(0); + let i = array.length; + const min = i - count; + let temp: T; + let index: number; + + while (i-- > min) { + index = Math.floor( + (i + 1) * this.faker.datatype.float({ min: 0, max: 0.99 }) + ); + temp = arrayCopy[index]; + arrayCopy[index] = arrayCopy[i]; + arrayCopy[i] = temp; + } + + return arrayCopy.slice(min); + } +} diff --git a/src/modules/image/index.ts b/src/modules/image/index.ts new file mode 100644 index 00000000..9e8e1879 --- /dev/null +++ b/src/modules/image/index.ts @@ -0,0 +1,345 @@ +import type { Faker } from '../..'; +import type { MethodsOf } from '../../utils/types'; +import { LoremPicsum } from './providers/lorempicsum'; +import { Lorempixel } from './providers/lorempixel'; +import { Unsplash } from './providers/unsplash'; + +/** + * Module to generate placeholder images. + * + * Default provider is unsplash image provider. + */ +export class Image { + readonly lorempixel: Lorempixel; + readonly unsplash: Unsplash; + readonly lorempicsum: LoremPicsum; + + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Image.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + + this.lorempixel = new Lorempixel(this.faker); + this.unsplash = new Unsplash(this.faker); + this.lorempicsum = new LoremPicsum(this.faker); + } + + /** + * Generates a random image url from one of the supported categories. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.image() // 'http://loremflickr.com/640/480/city' + * faker.image.image(1234, 2345) // 'http://loremflickr.com/1234/2345/sports' + * faker.image.image(1234, 2345, true) // 'http://loremflickr.com/1234/2345/nature?56789' + */ + image(width?: number, height?: number, randomize?: boolean): string { + const categories: MethodsOf<Image, Image['image']> = [ + 'abstract', + 'animals', + 'business', + 'cats', + 'city', + 'food', + 'nightlife', + 'fashion', + 'people', + 'nature', + 'sports', + 'technics', + 'transport', + ]; + return this[this.faker.helpers.arrayElement(categories)]( + width, + height, + randomize + ); + } + + /** + * Generates a random avatar image url. + * + * @example + * faker.image.avatar() + * // 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/170.jpg' + */ + avatar(): string { + return this.faker.internet.avatar(); + } + + /** + * Generates a random image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param category The category of the image. By default, a random one will be selected. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * @param https When true, return a `https` url. Otherwise, return a `http` url. + * + * @example + * faker.image.imageUrl() // 'http://loremflickr.com/640/480' + * faker.image.imageUrl(1234, 2345) // 'http://loremflickr.com/1234/2345' + * faker.image.imageUrl(1234, 2345, 'cat') // 'http://loremflickr.com/1234/2345/cat' + * faker.image.imageUrl(1234, 2345, 'cat', true) // 'http://loremflickr.com/1234/2345/cat?6849' + * faker.image.imageUrl(1234, 2345, 'cat', true, true) // 'https://loremflickr.com/1234/2345/cat?56789' + */ + imageUrl( + width?: number, + height?: number, + category?: string, + randomize?: boolean, + https?: boolean + ): string { + width = width || 640; + height = height || 480; + let protocol = 'http://'; + if (https === true) { + protocol = 'https://'; + } + let url = `${protocol}loremflickr.com/${width}/${height}`; + if (category != null) { + url += `/${category}`; + } + + if (randomize) { + url += `?${this.faker.datatype.number()}`; + } + + return url; + } + + /** + * Generates a random abstract image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.abstract() // 'http://loremflickr.com/640/480/abstract' + * faker.image.abstract(1234, 2345) // 'http://loremflickr.com/1234/2345/abstract' + * faker.image.abstract(1234, 2345, true) // 'http://loremflickr.com/1234/2345/abstract?56789' + */ + abstract(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'abstract', randomize); + } + + /** + * Generates a random animal image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.animals() // 'http://loremflickr.com/640/480/animals' + * faker.image.animals(1234, 2345) // 'http://loremflickr.com/1234/2345/animals' + * faker.image.animals(1234, 2345, true) // 'http://loremflickr.com/1234/2345/animals?56789' + */ + animals(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'animals', randomize); + } + + /** + * Generates a random business image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.business() // 'http://loremflickr.com/640/480/business' + * faker.image.business(1234, 2345) // 'http://loremflickr.com/1234/2345/business' + * faker.image.business(1234, 2345, true) // 'http://loremflickr.com/1234/2345/business?56789' + */ + business(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'business', randomize); + } + + /** + * Generates a random cat image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.cats() // 'http://loremflickr.com/640/480/cats' + * faker.image.cats(1234, 2345) // 'http://loremflickr.com/1234/2345/cats' + * faker.image.cats(1234, 2345, true) // 'http://loremflickr.com/1234/2345/cats?56789' + */ + cats(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'cats', randomize); + } + + /** + * Generates a random city image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.city() // 'http://loremflickr.com/640/480/city' + * faker.image.city(1234, 2345) // 'http://loremflickr.com/1234/2345/city' + * faker.image.city(1234, 2345, true) // 'http://loremflickr.com/1234/2345/city?56789' + */ + city(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'city', randomize); + } + + /** + * Generates a random food image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.food() // 'http://loremflickr.com/640/480/food' + * faker.image.food(1234, 2345) // 'http://loremflickr.com/1234/2345/food' + * faker.image.food(1234, 2345, true) // 'http://loremflickr.com/1234/2345/food?56789' + */ + food(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'food', randomize); + } + + /** + * Generates a random nightlife image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.nightlife() // 'http://loremflickr.com/640/480/nightlife' + * faker.image.nightlife(1234, 2345) // 'http://loremflickr.com/1234/2345/nightlife' + * faker.image.nightlife(1234, 2345, true) // 'http://loremflickr.com/1234/2345/nightlife?56789' + */ + nightlife(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'nightlife', randomize); + } + + /** + * Generates a random fashion image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.fashion() // 'http://loremflickr.com/640/480/fashion' + * faker.image.fashion(1234, 2345) // 'http://loremflickr.com/1234/2345/fashion' + * faker.image.fashion(1234, 2345, true) // 'http://loremflickr.com/1234/2345/fashion?56789' + */ + fashion(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'fashion', randomize); + } + + /** + * Generates a random people image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.people() // 'http://loremflickr.com/640/480/people' + * faker.image.people(1234, 2345) // 'http://loremflickr.com/1234/2345/people' + * faker.image.people(1234, 2345, true) // 'http://loremflickr.com/1234/2345/people?56789' + */ + people(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'people', randomize); + } + + /** + * Generates a random nature image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.nature() // 'http://loremflickr.com/640/480/nature' + * faker.image.nature(1234, 2345) // 'http://loremflickr.com/1234/2345/nature' + * faker.image.nature(1234, 2345, true) // 'http://loremflickr.com/1234/2345/nature?56789' + */ + nature(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'nature', randomize); + } + + /** + * Generates a random sports image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.sports() // 'http://loremflickr.com/640/480/sports' + * faker.image.sports(1234, 2345) // 'http://loremflickr.com/1234/2345/sports' + * faker.image.sports(1234, 2345, true) // 'http://loremflickr.com/1234/2345/sports?56789' + */ + sports(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'sports', randomize); + } + + /** + * Generates a random technics image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.technics() // 'http://loremflickr.com/640/480/technics' + * faker.image.technics(1234, 2345) // 'http://loremflickr.com/1234/2345/technics' + * faker.image.technics(1234, 2345, true) // 'http://loremflickr.com/1234/2345/technics?56789' + */ + technics(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'technics', randomize); + } + + /** + * Generates a random transport image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to randomize the image or not. Defaults to `false`. + * + * @example + * faker.image.transport() // 'http://loremflickr.com/640/480/transport' + * faker.image.transport(1234, 2345) // 'http://loremflickr.com/1234/2345/transport' + * faker.image.transport(1234, 2345, true) // 'http://loremflickr.com/1234/2345/transport?56789' + */ + transport(width?: number, height?: number, randomize?: boolean): string { + return this.imageUrl(width, height, 'transport', randomize); + } + + /** + * Generates a random data uri containing an svg image. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param color The color to use. Defaults to `grey`. + * + * @example + * faker.image.dataUri() // 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http...' + */ + dataUri(width?: number, height?: number, color: string = 'grey'): string { + const svgString = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="${width}" height="${height}"><rect width="100%" height="100%" fill="${color}"/><text x="${ + width / 2 + }" y="${ + height / 2 + }" font-size="20" alignment-baseline="middle" text-anchor="middle" fill="white">${width}x${height}</text></svg>`; + const rawPrefix = 'data:image/svg+xml;charset=UTF-8,'; + return rawPrefix + encodeURIComponent(svgString); + } +} diff --git a/src/modules/image/providers/lorempicsum.ts b/src/modules/image/providers/lorempicsum.ts new file mode 100644 index 00000000..25869205 --- /dev/null +++ b/src/modules/image/providers/lorempicsum.ts @@ -0,0 +1,126 @@ +import type { Faker } from '../../..'; + +/** + * Module to generate links to random images on `https://picsum.photos/`. + */ +// TODO ST-DDT 2022-03-11: Rename to picsum? +export class LoremPicsum { + constructor(private readonly faker: Faker) {} + + /** + * Generates a new picsum image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param grayscale Whether to return a grayscale image. Default to `false`. + * @param blur The optional level of blur to apply. Supports `1` - `10`. + */ + image( + width?: number, + height?: number, + grayscale?: boolean, + blur?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + ): string { + return this.imageUrl(width, height, grayscale, blur); + } + + /** + * Generates a new picsum image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param grayscale Whether to return a grayscale image. Default to `false`. + */ + imageGrayscale(width?: number, height?: number, grayscale?: boolean): string { + return this.imageUrl(width, height, grayscale); + } + + /** + * Generates a new picsum image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param blur The optional level of blur to apply. Supports `1` - `10`. + */ + imageBlurred( + width?: number, + height?: number, + blur?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + ): string { + return this.imageUrl(width, height, undefined, blur); + } + + /** + * Generates a new picsum image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param grayscale Whether to return a grayscale image. Default to `false`. + * @param blur The optional level of blur to apply. Supports `1` - `10`. + * @param seed The optional seed to use. + */ + imageRandomSeeded( + width?: number, + height?: number, + grayscale?: boolean, + blur?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10, + seed?: string + ): string { + // TODO ST-DDT 2022-03-11: This method does the same as image url, maybe generate a seed, if it is missig? + return this.imageUrl(width, height, grayscale, blur, seed); + } + + /** + * Returns a random avatar url. + * + * @example + * faker.internet.avatar() + * // 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/315.jpg' + */ + // TODO ST-DDT 2022-03-11: Deprecate this method as it is duplicate and has nothing to do with lorempicsum. + avatar(): string { + return this.faker.internet.avatar(); + } + + /** + * Generates a new picsum image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param grayscale Whether to return a grayscale image. Default to `false`. + * @param blur The optional level of blur to apply. Supports `1` - `10`. + * @param seed The optional seed to use. + */ + imageUrl( + width?: number, + height?: number, + grayscale?: boolean, + blur?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10, + seed?: string + ): string { + width = width || 640; + height = height || 480; + + let url = 'https://picsum.photos'; + + if (seed) { + url += `/seed/${seed}`; + } + + url += `/${width}/${height}`; + + if (grayscale && blur) { + return `${url}?grayscale&blur=${blur}`; + } + + if (grayscale) { + return `${url}?grayscale`; + } + + if (blur) { + return `${url}?blur=${blur}`; + } + + return url; + } +} diff --git a/src/modules/image/providers/lorempixel.ts b/src/modules/image/providers/lorempixel.ts new file mode 100644 index 00000000..f1ab384b --- /dev/null +++ b/src/modules/image/providers/lorempixel.ts @@ -0,0 +1,288 @@ +import type { Faker } from '../../..'; +import type { MethodsOf } from '../../../utils/types'; + +/** + * Module to generate links to random images on `https://lorempixel.com/`. + */ +export class Lorempixel { + constructor(private readonly faker: Faker) {} + + /** + * Generates a new lorempixel image url for a random supported category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + image(width?: number, height?: number, randomize?: boolean): string { + const categories: MethodsOf<Lorempixel, Lorempixel['image']> = [ + 'abstract', + 'animals', + 'business', + 'cats', + 'city', + 'food', + 'nightlife', + 'fashion', + 'people', + 'nature', + 'sports', + 'technics', + 'transport', + ]; + return this[this.faker.helpers.arrayElement(categories)]( + width, + height, + randomize + ); + } + + /** + * Returns a random avatar url. + * + * @example + * faker.internet.avatar() + * // 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/315.jpg' + */ + // TODO ST-DDT 2022-03-11: Deprecate this method as it is duplicate and has nothing to do with lorempixel. + avatar(): string { + return this.faker.internet.avatar(); + } + + /** + * Generates a new lorempixel image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param category The category of the image to generate. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + imageUrl( + width?: number, + height?: number, + category?: string, + randomize?: boolean + ): string { + width = width || 640; + height = height || 480; + + let url = `https://lorempixel.com/${width}/${height}`; + if (category != null) { + url += `/${category}`; + } + + if (randomize) { + url += `?${this.faker.datatype.number()}`; + } + + return url; + } + + /** + * Generates a new lorempixel image url using the "abstract" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + abstract(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'abstract', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "animals" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + animals(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'animals', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "business" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + business(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'business', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "cats" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + cats(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'cats', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "city" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + city(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'city', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "food" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + food(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'food', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "nightlife" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + nightlife(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'nightlife', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "fashion" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + fashion(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'fashion', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "people" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + people(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'people', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "nature" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + nature(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'nature', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "sports" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + sports(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'sports', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "technics" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + technics(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'technics', + randomize + ); + } + + /** + * Generates a new lorempixel image url using the "transport" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param randomize Whether to append a seed to the url. Defaults to `false`. + */ + transport(width?: number, height?: number, randomize?: boolean): string { + return this.faker.image.lorempixel.imageUrl( + width, + height, + 'transport', + randomize + ); + } +} diff --git a/src/modules/image/providers/unsplash.ts b/src/modules/image/providers/unsplash.ts new file mode 100644 index 00000000..45bd815c --- /dev/null +++ b/src/modules/image/providers/unsplash.ts @@ -0,0 +1,157 @@ +import type { Faker } from '../../..'; + +/** + * Module to generate links to random images on `https://source.unsplash.com/`. + */ +export class Unsplash { + // TODO ST-DDT 2022-03-11: Remove unused(?) constant + categories = [ + 'food', + 'nature', + 'people', + 'technology', + 'objects', + 'buildings', + ]; + + constructor(private readonly faker: Faker) {} + + /** + * Generates a new unsplash image url for a random supported category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + image(width?: number, height?: number, keyword?: string): string { + return this.imageUrl(width, height, undefined, keyword); + } + + /** + * Returns a random avatar url. + * + * @example + * faker.internet.avatar() + * // 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/315.jpg' + */ + // TODO ST-DDT 2022-03-11: Deprecate this method as it is duplicate and has nothing to do with unsplash. + avatar(): string { + return this.faker.internet.avatar(); + } + + /** + * Generates a new unsplash image url. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param category The category of the image to generate. + * @param keyword The image keywords to use. + */ + imageUrl( + width?: number, + height?: number, + category?: string, + keyword?: string + ): string { + width = width || 640; + height = height || 480; + + let url = 'https://source.unsplash.com'; + + if (category != null) { + url += `/category/${category}`; + } + + url += `/${width}x${height}`; + + if (keyword != null) { + const keywordFormat = /^([A-Za-z0-9].+,[A-Za-z0-9]+)$|^([A-Za-z0-9]+)$/; + if (keywordFormat.test(keyword)) { + url += `?${keyword}`; + } + } + + return url; + } + + /** + * Generates a new unsplash image url using the "food" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + food(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl(width, height, 'food', keyword); + } + + /** + * Generates a new unsplash image url using the "people" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + people(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl(width, height, 'people', keyword); + } + + /** + * Generates a new unsplash image url using the "nature" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + nature(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl(width, height, 'nature', keyword); + } + + /** + * Generates a new unsplash image url using the "technology" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + technology(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl( + width, + height, + 'technology', + keyword + ); + } + + /** + * Generates a new unsplash image url using the "objects" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + objects(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl( + width, + height, + 'objects', + keyword + ); + } + + /** + * Generates a new unsplash image url using the "buildings" category. + * + * @param width The width of the image. Defaults to `640`. + * @param height The height of the image. Defaults to `480`. + * @param keyword The image keywords to use. + */ + buildings(width?: number, height?: number, keyword?: string): string { + return this.faker.image.unsplash.imageUrl( + width, + height, + 'buildings', + keyword + ); + } +} diff --git a/src/modules/internet/index.ts b/src/modules/internet/index.ts new file mode 100644 index 00000000..bcc5daf3 --- /dev/null +++ b/src/modules/internet/index.ts @@ -0,0 +1,458 @@ +import type { Faker } from '../..'; +import * as random_ua from './user-agent'; + +export type EmojiType = + | 'smiley' + | 'body' + | 'person' + | 'nature' + | 'food' + | 'travel' + | 'activity' + | 'object' + | 'symbol' + | 'flag'; + +/** + * Module to generate internet related entries. + */ +export class Internet { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Internet.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random avatar url. + * + * @example + * faker.internet.avatar() + * // 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/315.jpg' + */ + avatar(): string { + return `https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/${this.faker.datatype.number( + 1249 + )}.jpg`; + } + + /** + * Generates an email address using the given person's name as base. + * + * @param firstName The optional first name to use. If not specified, a random one will be chosen. + * @param lastName The optional last name to use. If not specified, a random one will be chosen. + * @param provider The mail provider domain to use. If not specified, a random free mail provider will be chosen. + * @param options The options to use. Defaults to `{ allowSpecialCharacters: false }`. + * @param options.allowSpecialCharacters Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included + * in the email address. Defaults to `false`. + * + * @example + * faker.internet.email() // '[email protected]' + * faker.internet.email('Jeanne', 'Doe') // '[email protected]' + * faker.internet.email('Jeanne', 'Doe', 'example.fakerjs.dev') // '[email protected]' + * faker.internet.email('Jeanne', 'Doe', 'example.fakerjs.dev', { allowSpecialCharacters: true }) // 'Jeanne%[email protected]' + */ + email( + firstName?: string, + lastName?: string, + provider?: string, + options?: { allowSpecialCharacters?: boolean } + ): string { + provider = + provider || + this.faker.helpers.arrayElement( + this.faker.definitions.internet.free_email + ); + + let localPart: string = this.faker.helpers.slugify( + this.userName(firstName, lastName) + ); + + if (options?.allowSpecialCharacters) { + const usernameChars: string[] = '._-'.split(''); + const specialChars: string[] = ".!#$%&'*+-/=?^_`{|}~".split(''); + localPart = localPart.replace( + this.faker.helpers.arrayElement(usernameChars), + this.faker.helpers.arrayElement(specialChars) + ); + } + + return `${localPart}@${provider}`; + } + + /** + * Generates an email address using an example mail provider using the given person's name as base. + * + * @param firstName The optional first name to use. If not specified, a random one will be chosen. + * @param lastName The optional last name to use. If not specified, a random one will be chosen. + * @param options The options to use. Defaults to `{ allowSpecialCharacters: false }`. + * @param options.allowSpecialCharacters Whether special characters such as ``.!#$%&'*+-/=?^_`{|}~`` should be included + * in the email address. Defaults to `false`. + * + * @example + * faker.internet.exampleEmail() // '[email protected]' + * faker.internet.exampleEmail('Jeanne', 'Doe') // '[email protected]' + * faker.internet.exampleEmail('Jeanne', 'Doe', { allowSpecialCharacters: true }) // 'Jeanne%[email protected]' + */ + exampleEmail( + firstName?: string, + lastName?: string, + options?: { allowSpecialCharacters?: boolean } + ): string { + const provider = this.faker.helpers.arrayElement( + this.faker.definitions.internet.example_email + ); + return this.email(firstName, lastName, provider, options); + } + + /** + * Generates a username using the given person's name as base. + * + * @param firstName The optional first name to use. If not specified, a random one will be chosen. + * @param lastName The optional last name to use. If not specified, a random one will be chosen. + * + * @example + * faker.internet.userName() // 'Nettie_Zboncak40' + * faker.internet.userName('Jeanne', 'Doe') // 'Jeanne98' + */ + userName(firstName?: string, lastName?: string): string { + let result: string; + firstName = firstName || this.faker.name.firstName(); + lastName = lastName || this.faker.name.lastName(); + switch (this.faker.datatype.number(2)) { + case 0: + result = `${firstName}${this.faker.datatype.number(99)}`; + break; + case 1: + result = + firstName + this.faker.helpers.arrayElement(['.', '_']) + lastName; + break; + case 2: + result = `${firstName}${this.faker.helpers.arrayElement([ + '.', + '_', + ])}${lastName}${this.faker.datatype.number(99)}`; + break; + } + result = result.toString().replace(/'/g, ''); + result = result.replace(/ /g, ''); + return result; + } + + /** + * Returns a random web protocol. Either `http` or `https`. + * + * @example + * faker.internet.protocol() // 'http' + * faker.internet.protocol() // 'https' + */ + protocol(): 'http' | 'https' { + const protocols: ['http', 'https'] = ['http', 'https']; + return this.faker.helpers.arrayElement(protocols); + } + + /** + * Returns a random http method. + * + * Can be either of the following: + * + * - `GET` + * - `POST` + * - `PUT` + * - `DELETE` + * - `PATCH` + * + * @example + * faker.internet.httpMethod() // 'PATCH' + */ + httpMethod(): 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' { + const httpMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + ]; + return this.faker.helpers.arrayElement(httpMethods); + } + + /** + * Generates a random url. + * + * @example + * faker.internet.url() // 'https://remarkable-hackwork.info' + */ + url(): string { + return `${this.protocol()}://${this.domainName()}`; + } + + /** + * Generates a random domain name. + * + * @example + * faker.internet.domainName() // 'slow-timer.info' + */ + domainName(): string { + return `${this.domainWord()}.${this.domainSuffix()}`; + } + + /** + * Returns a random domain suffix. + * + * @example + * faker.internet.domainSuffix() // 'com' + * faker.internet.domainSuffix() // 'name' + */ + domainSuffix(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.internet.domain_suffix + ); + } + + /** + * Generates a random domain word. + * + * @example + * faker.internet.domainWord() // 'close-reality' + * faker.internet.domainWord() // 'weird-cytoplasm' + */ + domainWord(): string { + return `${this.faker.word.adjective()}-${this.faker.word.noun()}` + .replace(/([\\~#&*{}/:<>?|\"'])/gi, '') + .replace(/\s/g, '-') + .replace(/-{2,}/g, '-') + .toLowerCase(); + } + + /** + * Generates a random IPv4 address. + * + * @example + * faker.internet.ip() // '245.108.222.0' + */ + ip(): string { + // TODO @Shinigami92 2022-03-21: We may want to return a IPv4 or IPv6 address here in a later major release + return this.ipv4(); + } + + /** + * Generates a random IPv4 address. + * + * @example + * faker.internet.ipv4() // '245.108.222.0' + */ + ipv4(): string { + const randNum = () => { + return this.faker.datatype.number(255).toFixed(0); + }; + + const result: string[] = []; + for (let i = 0; i < 4; i++) { + result[i] = randNum(); + } + + return result.join('.'); + } + + /** + * Generates a random IPv6 address. + * + * @example + * faker.internet.ipv6() // '269f:1230:73e3:318d:842b:daab:326d:897b' + */ + ipv6(): string { + const randHash = () => { + let result = ''; + for (let i = 0; i < 4; i++) { + result += this.faker.helpers.arrayElement([ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + ]); + } + return result; + }; + + const result: string[] = []; + for (let i = 0; i < 8; i++) { + result[i] = randHash(); + } + return result.join(':'); + } + + /** + * Generates a random port number. + * + * @example + * faker.internet.port() // '9414' + */ + port(): number { + return this.faker.datatype.number({ min: 0, max: 65535 }); + } + + /** + * Generates a random user agent string. + * + * @example + * faker.internet.userAgent() + * // 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8_8) AppleWebKit/536.0.2 (KHTML, like Gecko) Chrome/27.0.849.0 Safari/536.0.2' + */ + userAgent(): string { + return random_ua.generate(this.faker); + } + + /** + * Generates a random css hex color code in aesthetically pleasing color palette. + * + * Based on + * http://stackoverflow.com/questions/43044/algorithm-to-randomly-generate-an-aesthetically-pleasing-color-palette + * + * @param redBase The optional base red in range between `0` and `255`. Defaults to `0`. + * @param greenBase The optional base green in range between `0` and `255`. Defaults to `0`. + * @param blueBase The optional base blue in range between `0` and `255`. Defaults to `0`. + * + * @example + * faker.internet.color() // '#30686e' + * faker.internet.color(100, 100, 100) // '#4e5f8b' + */ + color( + redBase: number = 0, + greenBase: number = 0, + blueBase: number = 0 + ): string { + const colorFromBase = (base: number): string => + Math.floor((this.faker.datatype.number(256) + base) / 2) + .toString(16) + .padStart(2, '0'); + + const red = colorFromBase(redBase); + const green = colorFromBase(greenBase); + const blue = colorFromBase(blueBase); + + return `#${red}${green}${blue}`; + } + + /** + * Generates a random mac address. + * + * @param sep The optional separator to use. Can be either `':'`, `'-'` or `''`. Defaults to `':'`. + * + * @example + * faker.internet.mac() // '32:8e:2e:09:c6:05' + */ + mac(sep?: string): string { + let i: number; + let mac = ''; + let validSep = ':'; + + // if the client passed in a different separator than `:`, + // we will use it if it is in the list of acceptable separators (dash or no separator) + if (['-', ''].indexOf(sep) !== -1) { + validSep = sep; + } + + for (i = 0; i < 12; i++) { + mac += this.faker.datatype.number(15).toString(16); + if (i % 2 === 1 && i !== 11) { + mac += validSep; + } + } + return mac; + } + + /** + * Generates a random password. + * + * @param len The length of the password to generate. Defaults to `15`. + * @param memorable Whether the generated password should be memorable. Defaults to `false`. + * @param pattern The pattern that all chars should match should match. + * This option will be ignored, if `memorable` is `true`. Defaults to `/\w/`. + * @param prefix The prefix to use. Defaults to `''`. + * + * @example + * faker.internet.password() // '89G1wJuBLbGziIs' + * faker.internet.password(20) // 'aF55c_8O9kZaPOrysFB_' + * faker.internet.password(20, true) // 'lawetimufozujosodedi' + * faker.internet.password(20, true, /[A-Z]/) // 'HMAQDFFYLDDUTBKVNFVS' + * faker.internet.password(20, true, /[A-Z]/, 'Hello ') // 'Hello IREOXTDWPERQSB' + */ + password( + len: number = 15, + memorable: boolean = false, + pattern: RegExp = /\w/, + prefix: string = '' + ): string { + /* + * password-generator ( function ) + * Copyright(c) 2011-2013 Bermi Ferrer <[email protected]> + * 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) { + if (prefix.match(consonant)) { + pattern = vowel; + } else { + pattern = consonant; + } + } + const n = this.faker.datatype.number(94) + 33; + let char = String.fromCharCode(n); + if (memorable) { + char = char.toLowerCase(); + } + if (!char.match(pattern)) { + return _password(length, memorable, pattern, prefix); + } + return _password(length, memorable, pattern, prefix + char); + }; + return _password(len, memorable, pattern, prefix); + } + + /** + * Generates a random emoji. + * + * @param options Options object. + * @param options.types A list of the emoji types that should be used. + * @example + * faker.internet.emoji() // '🥰' + * faker.internet.emoji({ types: ['food', 'nature'] }) // '🥐' + */ + emoji(options: { types?: ReadonlyArray<EmojiType> } = {}): string { + const { + types = Object.keys( + this.faker.definitions.internet.emoji + ) as Array<EmojiType>, + } = options; + const emojiType = this.faker.helpers.arrayElement(types); + return this.faker.helpers.arrayElement( + this.faker.definitions.internet.emoji[emojiType] + ); + } +} diff --git a/src/modules/internet/user-agent.ts b/src/modules/internet/user-agent.ts new file mode 100644 index 00000000..046758d4 --- /dev/null +++ b/src/modules/internet/user-agent.ts @@ -0,0 +1,345 @@ +/** + * Copyright (c) 2022 Faker + * + * This is a version of the original code migrated to TypeScript and modified + * by the Faker team. + * + * Check LICENSE for more details about the copyright. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2012-2014 Jeffrey Mealo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ----------------------------------------------------------------------------- + * + * Based loosely on Luka Pusic's PHP Script: + * http://360percents.com/posts/php-random-user-agent-generator/ + * + * The license for that script is as follows: + * + * "THE BEER-WARE LICENSE" (Revision 42): + * + * <[email protected]> wrote this file. As long as you retain this notice you + * can do whatever you want with this stuff. If we meet some day, and you think + * this stuff is worth it, you can buy me a beer in return. Luka Pusic + */ + +import type { Faker } from '../..'; + +export type Arch = 'lin' | 'mac' | 'win'; + +export function generate(faker: Faker): string { + function rnd( + a?: string[] | number | Record<string, number>, + b?: number + ): string | number { + //calling rnd() with no arguments is identical to rnd(0, 100) + a = a || 0; + b = b || 100; + + if (typeof b === 'number' && typeof a === 'number') { + // 9/2018 - Added faker random to ensure mersenne and seed + return faker.datatype.number({ min: a, max: b }); + } + + if (Array.isArray(a)) { + //returns a random element from array (a), even weighting + return faker.helpers.arrayElement(a); + } + + if (a && typeof a === 'object') { + //returns a random key from the passed object; keys are weighted by the decimal probability in their value + return ((obj) => { + const rand = (rnd(0, 100) as number) / 100; + let min = 0; + let max = 0; + let return_val: string; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + max = obj[key] + min; + return_val = key; + if (rand >= min && rand <= max) { + break; + } + min = min + obj[key]; + } + } + + return return_val; + })(a); + } + + throw new TypeError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Invalid arguments passed to rnd. (${b ? `${a}, ${b}` : a})` + ); + } + + function randomLang(): string | number { + return rnd([ + 'AB', + 'AF', + 'AN', + 'AR', + 'AS', + 'AZ', + 'BE', + 'BG', + 'BN', + 'BO', + 'BR', + 'BS', + 'CA', + 'CE', + 'CO', + 'CS', + 'CU', + 'CY', + 'DA', + 'DE', + 'EL', + 'EN', + 'EO', + 'ES', + 'ET', + 'EU', + 'FA', + 'FI', + 'FJ', + 'FO', + 'FR', + 'FY', + 'GA', + 'GD', + 'GL', + 'GV', + 'HE', + 'HI', + 'HR', + 'HT', + 'HU', + 'HY', + 'ID', + 'IS', + 'IT', + 'JA', + 'JV', + 'KA', + 'KG', + 'KO', + 'KU', + 'KW', + 'KY', + 'LA', + 'LB', + 'LI', + 'LN', + 'LT', + 'LV', + 'MG', + 'MK', + 'MN', + 'MO', + 'MS', + 'MT', + 'MY', + 'NB', + 'NE', + 'NL', + 'NN', + 'NO', + 'OC', + 'PL', + 'PT', + 'RM', + 'RO', + 'RU', + 'SC', + 'SE', + 'SK', + 'SL', + 'SO', + 'SQ', + 'SR', + 'SV', + 'SW', + 'TK', + 'TR', + 'TY', + 'UK', + 'UR', + 'UZ', + 'VI', + 'VO', + 'YI', + 'ZH', + ]); + } + + function randomBrowserAndOS(): Array<string | number> { + const browser = rnd({ + chrome: 0.45132810566, + iexplorer: 0.27477061836, + firefox: 0.19384170608, + safari: 0.06186781118, + opera: 0.01574236955, + }); + const os = { + chrome: { win: 0.89, mac: 0.09, lin: 0.02 }, + firefox: { win: 0.83, mac: 0.16, lin: 0.01 }, + opera: { win: 0.91, mac: 0.03, lin: 0.06 }, + safari: { win: 0.04, mac: 0.96 }, + iexplorer: ['win'], + }; + + return [browser, rnd(os[browser])]; + } + + function randomProc(arch: Arch): string | number { + const procs = { + lin: ['i686', 'x86_64'], + mac: { Intel: 0.48, PPC: 0.01, 'U; Intel': 0.48, 'U; PPC': 0.01 }, + win: ['', 'WOW64', 'Win64; x64'], + }; + return rnd(procs[arch]); + } + + function randomRevision(dots: number): string { + let return_val = ''; + //generate a random revision + //dots = 2 returns .x.y where x & y are between 0 and 9 + for (let x = 0; x < dots; x++) { + return_val += `.${rnd(0, 9)}`; + } + return return_val; + } + + const version_string = { + net() { + return [rnd(1, 4), rnd(0, 9), rnd(10000, 99999), rnd(0, 9)].join('.'); + }, + nt() { + return `${rnd(5, 6)}.${rnd(0, 3)}`; + }, + ie() { + return rnd(7, 11); + }, + trident() { + return `${rnd(3, 7)}.${rnd(0, 1)}`; + }, + osx(delim?: string) { + return [10, rnd(5, 10), rnd(0, 9)].join(delim || '.'); + }, + chrome() { + return [rnd(13, 39), 0, rnd(800, 899), 0].join('.'); + }, + presto() { + return `2.9.${rnd(160, 190)}`; + }, + presto2() { + return `${rnd(10, 12)}.00`; + }, + safari() { + return `${rnd(531, 538)}.${rnd(0, 2)}.${rnd(0, 2)}`; + }, + }; + + const browser = { + firefox(arch: Arch): string { + //https://developer.mozilla.org/en-US/docs/Gecko_user_agent_string_reference + const firefox_ver = `${rnd(5, 15)}${randomRevision(2)}`, + gecko_ver = `Gecko/20100101 Firefox/${firefox_ver}`, + proc = randomProc(arch), + os_ver = + arch === 'win' + ? `(Windows NT ${version_string.nt()}${proc ? `; ${proc}` : ''}` + : arch === 'mac' + ? `(Macintosh; ${proc} Mac OS X ${version_string.osx()}` + : `(X11; Linux ${proc}`; + + return `Mozilla/5.0 ${os_ver}; rv:${firefox_ver.slice( + 0, + -2 + )}) ${gecko_ver}`; + }, + + iexplorer(): string { + const ver = version_string.ie(); + + if (ver >= 11) { + //http://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx + return `Mozilla/5.0 (Windows NT 6.${rnd(1, 3)}; Trident/7.0; ${rnd([ + 'Touch; ', + '', + ])}rv:11.0) like Gecko`; + } + + //http://msdn.microsoft.com/en-us/library/ie/ms537503(v=vs.85).aspx + return `Mozilla/5.0 (compatible; MSIE ${ver}.0; Windows NT ${version_string.nt()}; Trident/${version_string.trident()}${ + rnd(0, 1) === 1 ? `; .NET CLR ${version_string.net()}` : '' + })`; + }, + + opera(arch: Arch): string { + //http://www.opera.com/docs/history/ + const presto_ver = ` Presto/${version_string.presto()} Version/${version_string.presto2()})`, + os_ver = + arch === 'win' + ? `(Windows NT ${version_string.nt()}; U; ${randomLang()}${presto_ver}` + : arch === 'lin' + ? `(X11; Linux ${randomProc(arch)}; U; ${randomLang()}${presto_ver}` + : `(Macintosh; Intel Mac OS X ${version_string.osx()} U; ${randomLang()} Presto/${version_string.presto()} Version/${version_string.presto2()})`; + + return `Opera/${rnd(9, 14)}.${rnd(0, 99)} ${os_ver}`; + }, + + safari(arch: Arch): string { + const safari = version_string.safari(), + ver = `${rnd(4, 7)}.${rnd(0, 1)}.${rnd(0, 10)}`, + os_ver = + arch === 'mac' + ? `(Macintosh; ${randomProc('mac')} Mac OS X ${version_string.osx( + '_' + )} rv:${rnd(2, 6)}.0; ${randomLang()}) ` + : `(Windows; U; Windows NT ${version_string.nt()})`; + + return `Mozilla/5.0 ${os_ver}AppleWebKit/${safari} (KHTML, like Gecko) Version/${ver} Safari/${safari}`; + }, + + chrome(arch: Arch): string { + const safari = version_string.safari(), + os_ver = + arch === 'mac' + ? `(Macintosh; ${randomProc('mac')} Mac OS X ${version_string.osx( + '_' + )}) ` + : arch === 'win' + ? `(Windows; U; Windows NT ${version_string.nt()})` + : `(X11; Linux ${randomProc(arch)}`; + + return `Mozilla/5.0 ${os_ver} AppleWebKit/${safari} (KHTML, like Gecko) Chrome/${version_string.chrome()} Safari/${safari}`; + }, + }; + + const random = randomBrowserAndOS(); + return browser[random[0]](random[1]); +} diff --git a/src/modules/lorem/index.ts b/src/modules/lorem/index.ts new file mode 100644 index 00000000..5df39146 --- /dev/null +++ b/src/modules/lorem/index.ts @@ -0,0 +1,205 @@ +import type { Faker } from '../..'; + +/** + * Module to generate random texts and words. + */ +export class Lorem { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Lorem.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a word of a specified length. + * + * @param length length of the word that should be returned. Defaults to a random length. + * + * @example + * faker.lorem.word() // 'temporibus' + * faker.lorem.word(5) // 'velit' + */ + word(length?: number): string { + const hasRightLength = (word: string) => word.length === length; + let properLengthWords: readonly string[]; + if (length == null) { + properLengthWords = this.faker.definitions.lorem.words; + } else { + properLengthWords = + this.faker.definitions.lorem.words.filter(hasRightLength); + } + return this.faker.helpers.arrayElement(properLengthWords); + } + + /** + * Generates a space separated list of words. + * + * @param num The number of words to generate. Defaults to `3`. + * + * @example + * faker.lorem.words() // 'qui praesentium pariatur' + * faker.lorem.words(10) // 'debitis consectetur voluptatem non doloremque ipsum autem totam eum ratione' + */ + words(num: number = 3): string { + const words: string[] = []; + for (let i = 0; i < num; i++) { + words.push(this.word()); + } + return words.join(' '); + } + + /** + * Generates a space separated list of words beginning a capital letter and ending with a dot. + * + * @param wordCount The number of words, that should be in the sentence. Defaults to a random number between `3` and `10`. + * + * @example + * faker.lorem.sentence() // 'Voluptatum cupiditate suscipit autem eveniet aut dolorem aut officiis distinctio.' + * faker.lorem.sentence(5) // 'Laborum voluptatem officiis est et.' + */ + sentence(wordCount?: number): string { + if (wordCount == null) { + wordCount = this.faker.datatype.number({ min: 3, max: 10 }); + } + + const sentence = this.words(wordCount); + return `${sentence.charAt(0).toUpperCase() + sentence.slice(1)}.`; + } + + /** + * Generates a slugified text consisting of the given number of hyphen separated words. + * + * @param wordCount The number of words to generate. Defaults to `3`. + * + * @example + * faker.lorem.slug() // 'dolores-illo-est' + */ + slug(wordCount?: number): string { + const words = this.words(wordCount); + + return this.faker.helpers.slugify(words); + } + + /** + * Generates the given number of sentences. + * + * @param sentenceCount The number of sentences to generate. Defaults to a random number between `2` and `6`. + * @param separator The separator to add between sentences. Defaults to `' '`. + * + * @example + * faker.lorem.sentences() // 'Iste molestiae incidunt aliquam possimus reprehenderit eum corrupti. Deleniti modi voluptatem nostrum ut esse.' + * faker.lorem.sentences(2) // 'Maxime vel numquam quibusdam. Dignissimos ex molestias quos aut molestiae quam nihil occaecati maiores.' + * faker.lorem.sentences(2, '\n') + * // 'Et rerum a unde tempora magnam sit nisi. + * // Et perspiciatis ipsam omnis.' + */ + sentences(sentenceCount?: number, separator: string = ' '): string { + if (sentenceCount == null) { + sentenceCount = this.faker.datatype.number({ min: 2, max: 6 }); + } + const sentences: string[] = []; + for (sentenceCount; sentenceCount > 0; sentenceCount--) { + sentences.push(this.sentence()); + } + return sentences.join(separator); + } + + /** + * Generates a paragraph with at least the given number of sentences. + * + * @param sentenceCount The minim number of sentences to generate. Defaults to `3`. + * + * @example + * faker.lorem.paragraph() // 'Non architecto nam unde sint. Ex tenetur dolor facere optio aut consequatur. Ea laudantium reiciendis repellendus.' + * faker.lorem.paragraph() // 'Animi possimus nemo consequuntur ut ea et tempore unde qui. Quis corporis esse occaecati.' + */ + paragraph(sentenceCount: number = 3): string { + return this.sentences(sentenceCount + this.faker.datatype.number(3)); + } + + /** + * Generates the given number of paragraphs. + * + * @param paragraphCount The number of paragraphs to generate. Defaults to `3`. + * @param separator The separator to use. Defaults to `'\n'`. + * + * @example + * faker.lorem.paragraphs() + * // '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.' + * + * faker.lorem.paragraphs(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.' + * + * faker.lorem.paragraphs(2, '<br/>\n') + * // 'Eos magnam aut qui accusamus. Sapiente quas culpa totam excepturi. Blanditiis totam distinctio occaecati dignissimos cumque atque qui officiis.<br/> + * // 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(paragraphCount: number = 3, separator: string = '\n'): string { + const paragraphs: string[] = []; + for (paragraphCount; paragraphCount > 0; paragraphCount--) { + paragraphs.push(this.paragraph()); + } + return paragraphs.join(separator); + } + + /** + * Generates a random text based on a random lorem method. + * + * @example + * faker.lorem.text() // 'Doloribus autem non quis vero quia.' + * faker.lorem.text() + * // '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.' + */ + text(): string { + const methods: Array<keyof Lorem> = [ + 'word', + 'words', + 'sentence', + 'sentences', + 'paragraph', + 'paragraphs', + 'lines', + ]; + + const method = this.faker.helpers.arrayElement(methods); + + return `${this[method]()}`; + } + + /** + * Generates the given number lines of lorem separated by `'\n'`. + * + * @param lineCount The number of lines to generate. Defaults to a random number between `1` and `5`. + * + * @example + * faker.lorem.lines() + * // '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.' + * + * faker.lorem.lines() + * // 'Soluta deserunt eos quam reiciendis libero autem enim nam ut. + * // Voluptate aut aut.' + */ + lines(lineCount?: number): string { + if (lineCount == null) { + lineCount = this.faker.datatype.number({ min: 1, max: 5 }); + } + return this.sentences(lineCount, '\n'); + } +} diff --git a/src/modules/mersenne/index.ts b/src/modules/mersenne/index.ts new file mode 100644 index 00000000..33e610f8 --- /dev/null +++ b/src/modules/mersenne/index.ts @@ -0,0 +1,73 @@ +import { FakerError } from '../../errors/faker-error'; +import Gen from './twister'; + +/** + * Module to generate seed based random numbers. + */ +export class Mersenne { + private gen = new Gen(); + + constructor() { + this.gen.initGenrand(new Date().getTime() % 1000000000); + + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Mersenne.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a random number between `[min, max)`. + * + * @param max The maximum number. Defaults to `0`. + * @param min The minimum number. Defaults to `32768`. + * + * @example + * faker.mersenne.rand() // 15515 + * faker.mersenne.rand(500, 1000) // 578 + */ + rand(max = 32768, min = 0): number { + if (min > max) { + const temp = min; + min = max; + max = temp; + } + + return Math.floor(this.gen.genrandReal2() * (max - min) + min); + } + + /** + * Sets the seed to use. + * + * @param S The seed to use. + * @throws If the seed is not a `number`. + */ + seed(S: number): void { + if (typeof S !== 'number') { + throw new FakerError( + `seed(S) must take numeric argument; is ${typeof S}` + ); + } + + this.gen.initGenrand(S); + } + + /** + * Sets the seed to use. + * + * @param A The seed to use. + * @throws If the seed is not a `number[]`. + */ + seed_array(A: number[]): void { + if (typeof A !== 'object') { + throw new FakerError( + `seed_array(A) must take array of numbers; is ${typeof A}` + ); + } + + this.gen.initByArray(A, A.length); + } +} diff --git a/src/modules/mersenne/twister.ts b/src/modules/mersenne/twister.ts new file mode 100644 index 00000000..5859f3ae --- /dev/null +++ b/src/modules/mersenne/twister.ts @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2022 Faker + * + * This is a version of the original source code migrated to TypeScript and + * modified by the Faker team. + * + * Check LICENSE for more details on copyright. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2006 Y. Okada + * + * This program is a JavaScript version of Mersenne Twister, with concealment + * and encapsulation in class, an almost straight conversion from the original + * program, mt19937ar.c, translated by Y. Okada on July 17, 2006, and modified + * a little at July 20, 2006, but there are not any substantial differences. + * + * In this program, procedure descriptions and comments of original source code + * were not removed. + * + * Lines commented with //c// were originally descriptions of c procedure. + * And a few following lines are appropriate JavaScript descriptions. + * + * Lines commented with /* and *\/ are original comments. + * Lines commented with // are additional comments in this JavaScript version. + * + * Before using this version, create at least one instance of + * MersenneTwister19937 class, and initialize the each state, given below + * in C comments, of all the instances. + * + * ----------------------------------------------------------------------------- + * + * A C-program for MT19937, with initialization improved 2002/1/26. + * Coded by Takuji Nishimura and Makoto Matsumoto. + * + * Before using, initialize the state by using init_genrand(seed) + * or init_by_array(init_key, key_length). + * + * Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The names of its contributors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * Any feedback is very welcome. + * http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html + * email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) + */ + +export default class MersenneTwister19937 { + private readonly N = 624; + private readonly M = 397; + private readonly MATRIX_A = 0x9908b0df; // constant vector a + private readonly UPPER_MASK = 0x80000000; // most significant w-r bits + private readonly LOWER_MASK = 0x7fffffff; // least significant r bits + private mt: number[] = new Array(this.N); // the array for the state vector + private mti = this.N + 1; // mti==N+1 means mt[N] is not initialized + + /** + * Returns a 32-bits unsigned integer from an operand to which applied a bit + * operator. + * + * @param n1 number to unsign + */ + private unsigned32(n1: number): number { + return n1 < 0 ? (n1 ^ this.UPPER_MASK) + this.UPPER_MASK : n1; + } + + /** + * Emulates lowerflow of a c 32-bits unsigned integer variable, instead of + * the operator -. These both arguments must be non-negative integers + * expressible using unsigned 32 bits. + * + * @param n1 dividend + * @param n2 divisor + */ + private subtraction32(n1: number, n2: number): number { + return n1 < n2 + ? this.unsigned32((0x100000000 - (n2 - n1)) & 0xffffffff) + : n1 - n2; + } + + /** + * Emulates overflow of a c 32-bits unsigned integer variable, instead of the operator +. + * these both arguments must be non-negative integers expressible using unsigned 32 bits. + * + * @param n1 number one for addition + * @param n2 number two for addition + */ + private addition32(n1: number, n2: number): number { + return this.unsigned32((n1 + n2) & 0xffffffff); + } + + /** + * Emulates overflow of a c 32-bits unsigned integer variable, instead of the operator *. + * These both arguments must be non-negative integers expressible using unsigned 32 bits. + * + * @param n1 number one for multiplication + * @param n2 number two for multiplication + */ + private multiplication32(n1: number, n2: number): number { + let sum = 0; + for (let i = 0; i < 32; ++i) { + if ((n1 >>> i) & 0x1) { + sum = this.addition32(sum, this.unsigned32(n2 << i)); + } + } + return sum; + } + + /** + * Initializes mt[N] with a seed. + * + * @param seed the seed to use + */ + initGenrand(seed: number): void { + this.mt[0] = this.unsigned32(seed & 0xffffffff); + for (this.mti = 1; this.mti < this.N; this.mti++) { + this.mt[this.mti] = + //(1812433253 * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti); + this.addition32( + this.multiplication32( + 1812433253, + this.unsigned32( + this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30) + ) + ), + this.mti + ); + + /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ + /* In the previous versions, MSBs of the seed affect */ + /* only MSBs of the array mt[]. */ + /* 2002/01/09 modified by Makoto Matsumoto */ + this.mt[this.mti] = this.unsigned32(this.mt[this.mti] & 0xffffffff); + } + } + + /** + * Initialize by an array with array-length. + * + * @param initKey is the array for initializing keys + * @param keyLength is its length + */ + initByArray(initKey: number[], keyLength: number): void { + this.initGenrand(19650218); + let i = 1; + let j = 0; + let k = this.N > keyLength ? this.N : keyLength; + for (; k; k--) { + // mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1664525)) + init_key[j] + j; + this.mt[i] = this.addition32( + this.addition32( + this.unsigned32( + this.mt[i] ^ + this.multiplication32( + this.unsigned32(this.mt[i - 1] ^ (this.mt[i - 1] >>> 30)), + 1664525 + ) + ), + initKey[j] + ), + j + ); + // mt[i] &= 0xffffffff; for WORDSIZE > 32 machines + this.mt[i] = this.unsigned32(this.mt[i] & 0xffffffff); + i++; + j++; + if (i >= this.N) { + this.mt[0] = this.mt[this.N - 1]; + i = 1; + } + if (j >= keyLength) { + j = 0; + } + } + for (k = this.N - 1; k; k--) { + // mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1566083941)) - i + this.mt[i] = this.subtraction32( + this.unsigned32( + this.mt[i] ^ + this.multiplication32( + this.unsigned32(this.mt[i - 1] ^ (this.mt[i - 1] >>> 30)), + 1566083941 + ) + ), + i + ); + // mt[i] &= 0xffffffff; for WORDSIZE > 32 machines + this.mt[i] = this.unsigned32(this.mt[i] & 0xffffffff); + i++; + if (i >= this.N) { + this.mt[0] = this.mt[this.N - 1]; + i = 1; + } + } + this.mt[0] = 0x80000000; // MSB is 1; assuring non-zero initial array + } + + // moved outside of genrandInt32() by jwatte 2010-11-17; generate less garbage + private mag01 = [0x0, this.MATRIX_A]; + + /** + * Generates a random number on [0,2^32]-interval + */ + genrandInt32(): number { + let y: number; + + if (this.mti >= this.N) { + // generate N words at one time + let kk: number; + + // if initGenrand() has not been called a default initial seed is used + if (this.mti === this.N + 1) { + this.initGenrand(5489); + } + + for (kk = 0; kk < this.N - this.M; kk++) { + y = this.unsigned32( + (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK) + ); + + this.mt[kk] = this.unsigned32( + this.mt[kk + this.M] ^ (y >>> 1) ^ this.mag01[y & 0x1] + ); + } + + for (; kk < this.N - 1; kk++) { + y = this.unsigned32( + (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK) + ); + + this.mt[kk] = this.unsigned32( + this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ this.mag01[y & 0x1] + ); + } + + y = this.unsigned32( + (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK) + ); + + this.mt[this.N - 1] = this.unsigned32( + this.mt[this.M - 1] ^ (y >>> 1) ^ this.mag01[y & 0x1] + ); + + this.mti = 0; + } + + y = this.mt[this.mti++]; + + // Tempering + y = this.unsigned32(y ^ (y >>> 11)); + y = this.unsigned32(y ^ ((y << 7) & 0x9d2c5680)); + y = this.unsigned32(y ^ ((y << 15) & 0xefc60000)); + y = this.unsigned32(y ^ (y >>> 18)); + + return y; + } + + /** + * Generates a random number on [0,2^32]-interval + */ + genrandInt31(): number { + return this.genrandInt32() >>> 1; + } + + /** + * Generates a random number on [0,1]-real-interval + */ + genrandReal1(): number { + return this.genrandInt32() * (1.0 / 4294967295.0); // divided by 2^32-1 + } + + /** + * Generates a random number on [0,1)-real-interval + */ + genrandReal2(): number { + return this.genrandInt32() * (1.0 / 4294967296.0); // divided by 2^32 + } + + /** + * Generates a random number on (0,1)-real-interval + */ + genrandReal3(): number { + return (this.genrandInt32() + 0.5) * (1.0 / 4294967296.0); // divided by 2^32 + } + + /** + * Generates a random number on [0,1) with 53-bit resolution + */ + genrandRes53(): number { + const a = this.genrandInt32() >>> 5, + b = this.genrandInt32() >>> 6; + return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0); + } + // These real versions are due to Isaku Wada, 2002/01/09 +} diff --git a/src/modules/music/index.ts b/src/modules/music/index.ts new file mode 100644 index 00000000..a34a4bb8 --- /dev/null +++ b/src/modules/music/index.ts @@ -0,0 +1,26 @@ +import type { Faker } from '../..'; + +/** + * Module to generate music related entries. + */ +export class Music { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Music.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random music genre. + * + * @example + * faker.music.genre() // 'Reggae' + */ + genre(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.music.genre); + } +} diff --git a/src/modules/name/index.ts b/src/modules/name/index.ts new file mode 100644 index 00000000..c1ac22a7 --- /dev/null +++ b/src/modules/name/index.ts @@ -0,0 +1,350 @@ +import type { Faker } from '../..'; +import { deprecated } from '../../internal/deprecated'; + +export enum Gender { + female = 'female', + male = 'male', +} + +// TODO @Shinigami92 2022-03-21: Remove 0 and 1 in v7 +export type GenderType = 'female' | 'male' | 0 | 1; + +/** + * Normalize gender. + * + * @param gender Gender. + * @param functionName Temporary parameter for deprecation message. + * @returns Normalized gender. + */ +function normalizeGender( + gender?: GenderType, + functionName?: string +): Exclude<GenderType, number> | undefined { + if (gender == null || typeof gender === 'string') { + // TODO @Shinigami92 2022-03-21: Cast can be removed when we set `strict: true` + return gender as Exclude<GenderType, number>; + } + + const normalizedGender = gender === 0 ? 'male' : 'female'; + + deprecated({ + deprecated: `name.${functionName}(number)`, + proposed: "'female' or 'male'", + since: 'v6.1.0', + until: 'v7.0.0', + }); + + return normalizedGender; +} + +/** + * Select a definition based on given gender. + * + * @param faker Faker instance. + * @param gender Gender. + * @param param2 Definitions. + * @param param2.generic Non-gender definitions. + * @param param2.female Female definitions. + * @param param2.male Male definitions. + * @param functionName Temporary parameter for deprecation message. + * @returns Definition based on given gender. + */ +function selectDefinition( + faker: Faker, + gender: GenderType | undefined, + // TODO @Shinigami92 2022-03-21: Remove fallback empty object when `strict: true` + { + generic, + female, + male, + }: { generic?: string[]; female?: string[]; male?: string[] } = {}, + functionName?: string +) { + const normalizedGender = normalizeGender(gender, functionName); + + let values: string[] | undefined; + switch (normalizedGender) { + case 'female': + values = female; + break; + case 'male': + values = male; + break; + default: + values = generic; + break; + } + + if (values == null) { + if (female != null && male != null) { + values = faker.helpers.arrayElement([female, male]); + } else { + values = generic; + } + } + + return faker.helpers.arrayElement(values); +} + +/** + * Module to generate people's names and titles. + */ +export class Name { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Name.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random first name. + * + * @param gender The optional gender to use. + * Can be either `'female'` or `'male'`. + * + * @example + * faker.name.firstName() // 'Antwan' + * faker.name.firstName("female") // 'Victoria' + * faker.name.firstName("male") // 'Tom' + */ + firstName(gender?: GenderType): string { + const { first_name, female_first_name, male_first_name } = + this.faker.definitions.name; + + return selectDefinition( + this.faker, + gender, + { + generic: first_name, + female: female_first_name, + male: male_first_name, + }, + 'firstName' + ); + } + + /** + * Returns a random last name. + * + * @param gender The optional gender to use. + * Can be either `'female'` or `'male'`. + * + * @example + * faker.name.lastName() // 'Hauck' + * faker.name.lastName("female") // 'Grady' + * faker.name.lastName("male") // 'Barton' + */ + lastName(gender?: GenderType): string { + const { last_name, female_last_name, male_last_name } = + this.faker.definitions.name; + + return selectDefinition( + this.faker, + gender, + { + generic: last_name, + female: female_last_name, + male: male_last_name, + }, + 'lastName' + ); + } + + /** + * Returns a random middle name. + * + * @param gender The optional gender to use. + * Can be either `'female'` or `'male'`. + * + * @example + * faker.name.middleName() // 'Доброславівна' + * faker.name.middleName("female") // 'Анастасівна' + * faker.name.middleName("male") // 'Вікторович' + */ + middleName(gender?: GenderType): string { + const { middle_name, female_middle_name, male_middle_name } = + this.faker.definitions.name; + + return selectDefinition( + this.faker, + gender, + { + generic: middle_name, + female: female_middle_name, + male: male_middle_name, + }, + 'middleName' + ); + } + + /** + * Generates a random full name. + * + * @param firstName The optional first name to use. If not specified a random one will be chosen. + * @param lastName The optional last name to use. If not specified a random one will be chosen. + * @param gender The optional gender to use. + * Can be either `'female'` or `'male'`. + * + * @example + * faker.name.findName() // 'Allen Brown' + * faker.name.findName('Joann') // 'Joann Osinski' + * faker.name.findName('Marcella', '', 'female') // 'Mrs. Marcella Huels' + * faker.name.findName(undefined, 'Beer') // 'Mr. Alfonso Beer' + * faker.name.findName(undefined, undefined, 'male') // 'Fernando Schaefer' + */ + findName(firstName?: string, lastName?: string, gender?: GenderType): string { + const variant = this.faker.datatype.number(8); + let prefix = ''; + let suffix = ''; + + const normalizedGender: Exclude<GenderType, number> = + normalizeGender(gender, 'findName') ?? + this.faker.helpers.arrayElement(['female', 'male']); + + firstName = firstName || this.firstName(normalizedGender); + lastName = lastName || this.lastName(normalizedGender); + + switch (variant) { + // TODO @Shinigami92 2022-03-21: Add possibility to have a prefix together with a suffix + case 0: + prefix = this.prefix(gender); + if (prefix) { + return `${prefix} ${firstName} ${lastName}`; + } + // TODO @Shinigami92 2022-01-21: Not sure if this fallthrough is wanted + // eslint-disable-next-line no-fallthrough + case 1: + suffix = this.suffix(); + if (suffix) { + return `${firstName} ${lastName} ${suffix}`; + } + } + + return `${firstName} ${lastName}`; + } + + /** + * Return a random gender. + * + * @param binary Whether to return only binary gender names. Defaults to `false`. + * + * @example + * faker.name.gender() // 'Trans*Man' + * faker.name.gender(true) // 'Female' + */ + gender(binary?: boolean): string { + if (binary) { + return this.faker.helpers.arrayElement( + this.faker.definitions.name.binary_gender + ); + } + + return this.faker.helpers.arrayElement(this.faker.definitions.name.gender); + } + + /** + * Returns a random name prefix. + * + * @param gender The optional gender to use. + * Can be either `'female'` or `'male'`. + * + * @example + * faker.name.prefix() // 'Miss' + * faker.name.prefix('female') // 'Ms.' + * faker.name.prefix('male') // 'Mr.' + */ + prefix(gender?: GenderType): string { + const { prefix, female_prefix, male_prefix } = this.faker.definitions.name; + + return selectDefinition( + this.faker, + gender, + { + generic: prefix, + female: female_prefix, + male: male_prefix, + }, + 'prefix' + ); + } + + /** + * Returns a random name suffix. + * + * @example + * faker.name.suffix() // 'DDS' + */ + suffix(): string { + // TODO @Shinigami92 2022-03-21: Add female_suffix and male_suffix + return this.faker.helpers.arrayElement(this.faker.definitions.name.suffix); + } + + /** + * Generates a random job title. + * + * @example + * faker.name.title() // 'International Integration Manager' + * + * @deprecated + */ + title(): string { + deprecated({ + deprecated: 'faker.name.title()', + proposed: 'faker.name.jobTitle()', + since: 'v6.1.2', + until: 'v7.0.0', + }); + + return this.jobTitle(); + } + + /** + * Generates a random job title. + * + * @example + * faker.name.jobTitle() // 'Global Accounts Engineer' + */ + jobTitle(): string { + return `${this.jobDescriptor()} ${this.jobArea()} ${this.jobType()}`; + } + + /** + * Generates a random job descriptor. + * + * @example + * faker.name.jobDescriptor() // 'Customer' + */ + jobDescriptor(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.name.title.descriptor + ); + } + + /** + * Generates a random job area. + * + * @example + * faker.name.jobArea() // 'Brand' + */ + jobArea(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.name.title.level + ); + } + + /** + * Generates a random job type. + * + * @example + * faker.name.jobType() // 'Assistant' + */ + jobType(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.name.title.job + ); + } +} diff --git a/src/modules/phone/index.ts b/src/modules/phone/index.ts new file mode 100644 index 00000000..7e85915c --- /dev/null +++ b/src/modules/phone/index.ts @@ -0,0 +1,76 @@ +import type { Faker } from '../..'; + +/** + * Module to generate phone-related data. + */ +export class Phone { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Phone.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a random phone number. + * + * @param format Format of the phone number. Defaults to `faker.phone.phoneFormats()`. + * + * @example + * faker.phone.phoneNumber() // '961-770-7727' + * faker.phone.phoneNumber('501-###-###') // '501-039-841' + * faker.phone.phoneNumber('+48 91 ### ## ##') // '+48 91 463 61 70' + */ + // TODO @pkuczynski 2022-02-01: simplify name to `number()` + phoneNumber(format?: string): string { + return this.faker.helpers.replaceSymbolWithNumber( + format || this.phoneFormats() + ); + } + + /** + * Returns phone number in a format of the given index from `faker.definitions.phone_number.formats`. + * + * @param phoneFormatsArrayIndex Index in the `faker.definitions.phone_number.formats` array. Defaults to `0`. + * + * @example + * faker.phone.phoneNumberFormat() // '943-627-0355' + * faker.phone.phoneNumberFormat(3) // '282.652.3201' + */ + // FIXME @Shinigami 2022-01-14: this is strange passing in an array index + // TODO @pkuczynski 2022-02-01: discuss removing this method as it tightly couples with localisation + phoneNumberFormat(phoneFormatsArrayIndex = 0): string { + return this.faker.helpers.replaceSymbolWithNumber( + this.faker.definitions.phone_number.formats[phoneFormatsArrayIndex] + ); + } + + /** + * Returns a random phone number format. + * + * @example + * faker.phone.phoneFormats() // '!##.!##.####' + */ + // TODO @pkuczynski 2022-02-01: simplify name to `format()` + phoneFormats(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.phone_number.formats + ); + } + + /** + * Generates IMEI number. + * + * @example + * faker.phone.imei() // '13-850175-913761-7' + */ + imei(): string { + return this.faker.helpers.replaceCreditCardSymbols( + '##-######-######-L', + '#' + ); + } +} diff --git a/src/modules/random/index.ts b/src/modules/random/index.ts new file mode 100644 index 00000000..7939b298 --- /dev/null +++ b/src/modules/random/index.ts @@ -0,0 +1,648 @@ +import type { Faker } from '../..'; +import { FakerError } from '../../errors/faker-error'; +import { deprecated } from '../../internal/deprecated'; + +/** + * Method to reduce array of characters. + * + * @param arr existing array of characters + * @param values array of characters which should be removed + * @returns new array without banned characters + */ +function arrayRemove<T>(arr: T[], values: readonly T[]): T[] { + values.forEach((value) => { + arr = arr.filter((ele) => ele !== value); + }); + return arr; +} + +/** + * Generates random values of different kinds. Some methods are deprecated and have been moved to dedicated modules. + */ +export class Random { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Random.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a single random number between zero and the given max value or the given range with the specified precision. + * The bounds are inclusive. + * + * @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 `99999`. + * @param options.precision Precision of the generated number. Defaults to `1`. + * + * @see faker.datatype.number() + * + * @example + * faker.random.number() // 55422 + * faker.random.number(100) // 52 + * faker.random.number({ min: 1000000 }) // 431433 + * faker.random.number({ max: 100 }) // 42 + * faker.random.number({ precision: 0.01 }) // 64246.18 + * faker.random.number({ min: 10, max: 100, precision: 0.01 }) // 36.94 + * + * @deprecated + */ + number( + options?: number | { min?: number; max?: number; precision?: number } + ): number { + deprecated({ + deprecated: 'faker.random.number()', + proposed: 'faker.datatype.number()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.faker.datatype.number(options); + } + + /** + * Returns a single random floating-point number for the given precision or range and precision. + * + * @param options Precision or options object. + * @param options.min Lower bound for generated number. Defaults to `0`. + * @param options.max Upper bound for generated number. Defaults to `99999`. + * @param options.precision Precision of the generated number. Defaults to `0.01`. + * + * @see faker.datatype.float() + * + * @example + * faker.random.float() // 51696.36 + * faker.random.float(0.1) // 52023.2 + * faker.random.float({ min: 1000000 }) // 212859.76 + * faker.random.float({ max: 100 }) // 28.11 + * faker.random.float({ precision: 0.1 }) // 84055.3 + * faker.random.float({ min: 10, max: 100, precision: 0.001 }) // 57.315 + * + * @deprecated + */ + float( + options?: number | { min?: number; max?: number; precision?: number } + ): number { + deprecated({ + deprecated: 'faker.random.float()', + proposed: 'faker.datatype.float()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.faker.datatype.float(options); + } + + /** + * Returns random element from the given array. + * + * @template T The type of the entries to pick from. + * @param array Array to pick the value from. Defaults to `['a', 'b', 'c']`. + * + * @example + * faker.random.arrayElement() // 'b' + * faker.random.arrayElement(['cat', 'dog', 'mouse']) // 'dog' + * + * @deprecated + */ + arrayElement<T = string>( + array: ReadonlyArray<T> = ['a', 'b', 'c'] as unknown as ReadonlyArray<T> + ): T { + deprecated({ + deprecated: 'faker.random.arrayElement()', + proposed: 'faker.helpers.arrayElement()', + since: 'v6.3.0', + until: 'v7.0.0', + }); + return this.faker.helpers.arrayElement(array); + } + + /** + * Returns a subset with random elements of the given array in random order. + * + * @template T The type of the entries to pick from. + * @param array Array to pick the value from. Defaults to `['a', 'b', 'c']`. + * @param count Number 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 + * faker.random.arrayElements() // ['b', 'c'] + * faker.random.arrayElements(['cat', 'dog', 'mouse']) // ['mouse', 'cat'] + * faker.random.arrayElements([1, 2, 3, 4, 5], 2) // [4, 2] + * + * @deprecated + */ + arrayElements<T>( + array: ReadonlyArray<T> = ['a', 'b', 'c'] as unknown as ReadonlyArray<T>, + count?: number + ): T[] { + deprecated({ + deprecated: 'faker.random.arrayElements()', + proposed: 'faker.helpers.arrayElements()', + since: 'v6.3.0', + until: 'v7.0.0', + }); + return this.faker.helpers.arrayElements(array, count); + } + + /** + * Returns a random key from given object. + * + * @template T The type of `Record` to pick from. + * @template K The keys of `T`. + * @param object The object to get the keys from. + * @param field If this is set to `'key'`, this method will a return a random key of the given instance. + * + * @see faker.helpers.objectKey() + * + * @example + * const object = { keyA: 'valueA', keyB: 42 }; + * faker.random.objectElement(object, 'key') // 'keyB' + * + * @deprecated + */ + objectElement<T extends Record<string, unknown>, K extends keyof T>( + object: T, + field: 'key' + ): K; + /** + * Returns a random value from given object. + * + * @template T The type of `Record` to pick from. + * @template K The keys of `T`. + * @param object The object to get the values from. + * @param field If this is set to `'value'`, this method will a return a random value of the given instance. + * + * @see faker.helpers.objectValue() + * + * @example + * const object = { keyA: 'valueA', keyB: 42 }; + * faker.random.objectElement(object) // 42 + * faker.random.objectElement(object, 'value') // 'valueA' + * + * @deprecated + */ + objectElement<T extends Record<string, unknown>, K extends keyof T>( + object: T, + field?: unknown + ): T[K]; + /** + * Returns a random key or value from given object. + * + * @template T The type of `Record` to pick from. + * @template K The keys of `T`. + * @param object The object to get the keys or values from. + * @param field If this is set to `'key'`, this method will a return a random key of the given instance. + * If this is set to `'value'`, this method will a return a random value of the given instance. + * Defaults to `'value'`. + * + * @see faker.helpers.objectKey() + * @see faker.helpers.objectValue() + * + * @example + * const object = { keyA: 'valueA', keyB: 42 }; + * faker.random.objectElement(object) // 42 + * faker.random.objectElement(object, 'key') // 'keyB' + * faker.random.objectElement(object, 'value') // 'valueA' + * + * @deprecated + */ + objectElement<T extends Record<string, unknown>, K extends keyof T>( + object?: T, + field?: 'key' | 'value' + ): K | T[K]; + /** + * Returns a random key or value from given object. + * + * @template T The type of `Record` to pick from. + * @template K The keys of `T`. + * @param object The object to get the keys or values from. + * @param field If this is set to `'key'`, this method will a return a random key of the given instance. + * If this is set to `'value'`, this method will a return a random value of the given instance. + * Defaults to `'value'`. + * + * @see faker.helpers.objectKey() + * @see faker.helpers.objectValue() + * + * @example + * const object = { keyA: 'valueA', keyB: 42 }; + * faker.random.objectElement(object) // 42 + * faker.random.objectElement(object, 'key') // 'keyB' + * faker.random.objectElement(object, 'value') // 'valueA' + * + * @deprecated + */ + objectElement<T extends Record<string, unknown>, K extends keyof T>( + object: T = { foo: 'bar', too: 'car' } as unknown as T, + field: 'key' | 'value' = 'value' + ): K | T[K] { + const useKey = field === 'key'; + deprecated({ + deprecated: `faker.random.objectElement(${useKey ? "obj, 'key'" : ''})`, + proposed: `faker.helpers.object${useKey ? 'Key' : 'Value'}()`, + since: 'v6.3.0', + until: 'v7.0.0', + }); + return field === 'key' + ? (this.faker.helpers.objectKey(object) as K) + : (this.faker.helpers.objectValue(object) as T[K]); + } + + /** + * Returns a UUID v4 ([Universally Unique Identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier)). + * + * @see faker.datatype.uuid() + * + * @example + * faker.random.uuid() // '4136cd0b-d90b-4af7-b485-5d1ded8db252' + * + * @deprecated + */ + uuid(): string { + deprecated({ + deprecated: 'faker.random.uuid()', + proposed: 'faker.datatype.uuid()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.faker.datatype.uuid(); + } + + /** + * Returns the boolean value `true` or `false`. + * + * @see faker.datatype.boolean() + * + * @example + * faker.random.boolean() // false + * + * @deprecated + */ + boolean(): boolean { + deprecated({ + deprecated: 'faker.random.boolean()', + proposed: 'faker.datatype.boolean()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.faker.datatype.boolean(); + } + + /** + * Returns random word. + * + * @example + * faker.random.word() // 'Seamless' + */ + word(): string { + const wordMethods = [ + this.faker.commerce.department, + this.faker.commerce.productName, + this.faker.commerce.productAdjective, + this.faker.commerce.productMaterial, + this.faker.commerce.product, + this.faker.commerce.color, + + this.faker.company.catchPhraseAdjective, + this.faker.company.catchPhraseDescriptor, + this.faker.company.catchPhraseNoun, + this.faker.company.bsAdjective, + this.faker.company.bsBuzz, + this.faker.company.bsNoun, + this.faker.address.streetSuffix, + this.faker.address.county, + this.faker.address.country, + this.faker.address.state, + + this.faker.finance.accountName, + this.faker.finance.transactionType, + this.faker.finance.currencyName, + + this.faker.hacker.noun, + this.faker.hacker.verb, + this.faker.hacker.adjective, + this.faker.hacker.ingverb, + this.faker.hacker.abbreviation, + + this.faker.name.jobDescriptor, + this.faker.name.jobArea, + this.faker.name.jobType, + ]; + + const bannedChars = [ + '!', + '#', + '%', + '&', + '*', + ')', + '(', + '+', + '=', + '.', + '<', + '>', + '{', + '}', + '[', + ']', + ':', + ';', + "'", + '"', + '_', + '-', + ]; + let result: string; + + do { + // randomly pick from the many faker methods that can generate words + const randomWordMethod = this.faker.helpers.arrayElement(wordMethods); + + result = randomWordMethod(); + } while (!result || bannedChars.some((char) => result.includes(char))); + + return this.faker.helpers.arrayElement(result.split(' ')); + } + + /** + * Returns string with set of random words. + * + * @param count Number of words. Defaults to a random value between `1` and `3`. + * + * @example + * faker.random.words() // 'neural' + * faker.random.words(5) // 'copy Handcrafted bus client-server Point' + */ + words(count?: number): string { + const words: string[] = []; + + if (count == null) { + count = this.faker.datatype.number({ min: 1, max: 3 }); + } + + for (let i = 0; i < count; i++) { + words.push(this.word()); + } + + return words.join(' '); + } + + /** + * Returns a random image url. + * + * @see faker.random.image() + * + * @example + * faker.random.image() // 'http://placeimg.com/640/480/animals' + * + * @deprecated + */ + image(): string { + deprecated({ + deprecated: 'faker.random.image()', + proposed: 'faker.image.image()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + return this.faker.image.image(); + } + + /** + * Returns a random locale, that is available in this faker instance. + * You can use the returned locale with `faker.setLocale(result)`. + * + * @example + * faker.random.locale() // 'el' + */ + locale(): string { + return this.faker.helpers.arrayElement(Object.keys(this.faker.locales)); + } + + /** + * Generating a string consisting of lower/upper alpha characters based on count and upcase options. + * + * @param options Either the number of characters or an options instance. Defaults to `{ count: 1, upcase: false, bannedChars: [] }`. + * @param options.count The number of characters to generate. Defaults to `1`. + * @param options.upcase If true, the result will be uppercase. If false, it will be lowercase. Defaults to `false`. + * @param options.bannedChars An array with characters to exclude. Defaults to `[]`. + * + * @example + * faker.random.alpha() // 'b' + * faker.random.alpha(10) // 'qccrabobaf' + * faker.random.alpha({ count: 5, upcase: true, bannedChars: ['a'] }) // 'DTCIC' + */ + // TODO @Shinigami92 2022-02-14: Tests covered `(count, options)`, but they were never typed like that + alpha( + options: + | number + | { + count?: number; + upcase?: boolean; + bannedChars?: readonly string[]; + } = {} + ): string { + if (typeof options === 'number') { + options = { + count: options, + }; + } + const { count = 1, upcase = false, bannedChars = [] } = options; + + let charsArray = [ + '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', + ]; + + charsArray = arrayRemove(charsArray, bannedChars); + + let wholeString = ''; + for (let i = 0; i < count; i++) { + wholeString += this.faker.helpers.arrayElement(charsArray); + } + + return upcase ? wholeString.toUpperCase() : wholeString; + } + + /** + * Generating a string consisting of lower/upper alpha characters and digits based on count and upcase options. + * + * @param count The number of characters and digits to generate. Defaults to `1`. + * @param options The options to use. Defaults to `{ bannedChars: [] }`. + * @param options.bannedChars An array of characters and digits which should be banned in the generated string. Defaults to `[]`. + * + * @example + * faker.random.alphaNumeric() // '2' + * faker.random.alphaNumeric(5) // '3e5v7' + * faker.random.alphaNumeric(5, { bannedChars: ["a"] }) // 'xszlm' + */ + alphaNumeric( + count: number = 1, + options: { bannedChars?: readonly string[] } = {} + ): string { + const { bannedChars = [] } = options; + + let charsArray = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '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', + ]; + + charsArray = arrayRemove(charsArray, bannedChars); + + if (charsArray.length === 0) { + throw new FakerError( + 'Unable to generate string, because all possible characters are banned.' + ); + } + + let wholeString = ''; + for (let i = 0; i < count; i++) { + wholeString += this.faker.helpers.arrayElement(charsArray); + } + + return wholeString; + } + + /** + * Generates a given length string of digits. + * + * @param length The number of digits to generate. Defaults to `1`. + * @param options The options to use. Defaults to `{}`. + * @param options.allowLeadingZeros If true, leading zeros will be allowed. Defaults to `false`. + * @param options.bannedDigits An array of digits which should be banned in the generated string. Defaults to `[]`. + * + * @example + * faker.random.numeric() // '2' + * faker.random.numeric(5) // '31507' + * faker.random.numeric(42) // '56434563150765416546479875435481513188548' + * faker.random.numeric(42, { allowLeadingZeros: true }) // '00564846278453876543517840713421451546115' + * faker.random.numeric(6, { bannedDigits: ['0'] }) // '943228' + */ + numeric( + length: number = 1, + options: { + allowLeadingZeros?: boolean; + bannedDigits?: readonly string[]; + } = {} + ): string { + if (length <= 0) { + return ''; + } + + const { allowLeadingZeros = false, bannedDigits = [] } = options; + + const allowedDigits = '0123456789' + .split('') + .filter((digit) => !bannedDigits.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 banned.' + ); + } + + let result = ''; + + if (!allowLeadingZeros && !bannedDigits.includes('0')) { + result += this.faker.helpers.arrayElement( + allowedDigits.filter((digit) => digit !== '0') + ); + } + + while (result.length < length) { + result += this.faker.helpers.arrayElement(allowedDigits); + } + + return result; + } + + /** + * Returns a hexadecimal number. + * + * @param count Length of the generated number. Defaults to `1`. + * + * @see faker.datatype.hexadecimal() + * + * @example + * faker.random.hexaDecimal() // '0xb' + * faker.random.hexaDecimal(10) // '0xaE13F044fb' + * + * @deprecated + */ + hexaDecimal(count?: number): string { + deprecated({ + deprecated: 'faker.random.hexaDecimal()', + proposed: 'faker.datatype.hexadecimal()', + // since: 'v5.0.0', (?) + until: 'v7.0.0', + }); + + return this.faker.datatype.hexadecimal(count); + } +} diff --git a/src/modules/system/index.ts b/src/modules/system/index.ts new file mode 100644 index 00000000..250f1fb8 --- /dev/null +++ b/src/modules/system/index.ts @@ -0,0 +1,194 @@ +import type { Faker } from '../..'; + +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', +]; + +/** + * Converts the given set to an array. + * + * @param set The set to convert. + */ +// TODO ST-DDT 2022-03-11: Replace with Array.from(Set) +function setToArray<T>(set: Set<T>): T[] { + // shortcut if Array.from is available + if (Array.from) { + return Array.from(set); + } + + const array: T[] = []; + set.forEach((item) => { + array.push(item); + }); + return array; +} + +/** + * Generates fake data for many computer systems properties. + */ +export class System { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(System.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random file name with extension. + * + * @example + * faker.system.fileName() // 'self_enabling_accountability_toys.kpt' + */ + fileName(): string { + const str = this.faker.random.words().toLowerCase().replace(/\W/g, '_'); + + return `${str}.${this.fileExt()}`; + } + + /** + * Returns a random file name with a given extension or a commonly used extension. + * + * @param ext Extension. Empty string is considered to be not set. + * @example + * faker.system.commonFileName() // 'dollar.jpg' + * faker.system.commonFileName('txt') // 'global_borders_wyoming.txt' + */ + commonFileName(ext?: string): string { + const str = this.faker.random.words().toLowerCase().replace(/\W/g, '_'); + + return `${str}.${ext || this.commonFileExt()}`; + } + + /** + * Returns a mime-type. + * + * @example + * faker.system.mimeType() // 'video/vnd.vivo' + */ + mimeType(): string { + const mimeTypeKeys = Object.keys(this.faker.definitions.system.mimeTypes); + + return this.faker.helpers.arrayElement(mimeTypeKeys); + } + + /** + * Returns a commonly used file type. + * + * @example + * faker.system.commonFileType() // 'audio' + */ + commonFileType(): string { + return this.faker.helpers.arrayElement(commonFileTypes); + } + + /** + * Returns a commonly used file extension. + * + * @example + * faker.system.commonFileExt() // 'gif' + */ + commonFileExt(): string { + return this.fileExt(this.faker.helpers.arrayElement(commonMimeTypes)); + } + + /** + * Returns a file type. + * + * @example + * faker.system.fileType() // 'message' + */ + fileType(): string { + const typeSet = new Set<string>(); + const mimeTypes = this.faker.definitions.system.mimeTypes; + + Object.keys(mimeTypes).forEach((m) => { + const type = m.split('/')[0]; + + typeSet.add(type); + }); + + const types = setToArray(typeSet); + return this.faker.helpers.arrayElement(types); + } + + /** + * Returns a file extension. + * + * @param mimeType Valid [mime-type](https://github.com/jshttp/mime-db/blob/master/db.json) + * + * @example + * faker.system.fileExt() // 'emf' + * faker.system.fileExt('application/json') // 'json' + */ + fileExt(mimeType?: string): string { + if (typeof mimeType === 'string') { + const mimes = this.faker.definitions.system.mimeTypes; + return this.faker.helpers.arrayElement(mimes[mimeType].extensions); + } + + const mimeTypes = this.faker.definitions.system.mimeTypes; + const extensionSet = new Set<string>(); + + Object.keys(mimeTypes).forEach((m) => { + if (mimeTypes[m].extensions instanceof Array) { + mimeTypes[m].extensions.forEach((ext) => { + extensionSet.add(ext); + }); + } + }); + + const extensions = setToArray(extensionSet); + + return this.faker.helpers.arrayElement(extensions); + } + + /** + * Returns a directory path. + * + * @example + * faker.system.directoryPath() // '/etc/mail' + */ + directoryPath(): string { + const paths = this.faker.definitions.system.directoryPaths; + return this.faker.helpers.arrayElement(paths); + } + + /** + * Returns a file path. + * + * @example + * faker.system.filePath() // '/usr/local/src/money.dotx' + */ + // TODO @prisis 2022-01-25: add a parameter to have the possibility to have one or two ext on file. + filePath(): string { + return `${this.directoryPath()}/${this.fileName()}`; + } + + /** + * Returns a [semantic version](https://semver.org). + * + * @example + * faker.system.semver() // '1.1.2' + */ + semver(): string { + return [ + this.faker.datatype.number(9), + this.faker.datatype.number(9), + this.faker.datatype.number(9), + ].join('.'); + } +} diff --git a/src/modules/time/index.ts b/src/modules/time/index.ts new file mode 100644 index 00000000..021d75c8 --- /dev/null +++ b/src/modules/time/index.ts @@ -0,0 +1,57 @@ +import { deprecated } from '../../internal/deprecated'; +import type { LiteralUnion } from '../../utils/types'; + +/** + * Module to generate time of dates in various formats. + * + * @deprecated You should stop using this module, as it will be removed in the future. + */ +export class Time { + /** + * Returns recent time. + * + * @param format The format to use. + * + * - `'abbr'` Return a string with only the time. `Date.toLocaleTimeString`. + * - `'date'` Return a date instance. + * - `'wide'` Return a string with a long time. `Date.toTimeString()`. + * - `'unix'` Returns a unix timestamp. + * + * Defaults to `'unix'`. + * + * @example + * faker.time.recent() // 1643067231856 + * faker.time.recent('abbr') // '12:34:07 AM' + * faker.time.recent('date') // 2022-03-01T20:35:47.402Z + * faker.time.recent('wide') // '00:34:11 GMT+0100 (Central European Standard Time)' + * faker.time.recent('unix') // 1643067231856 + * + * @deprecated You should stop using this function, as it will be removed in the future. Use the native `new Date()` with one of the wanted functions directly. + */ + recent( + format: LiteralUnion<'abbr' | 'date' | 'wide' | 'unix'> = 'unix' + ): string | number | Date { + deprecated({ + deprecated: 'faker.time.recent()', + proposed: 'native `new Date()` and call the function you want on it', + since: 'v6.1.0', + until: 'v7.0.0', + }); + + let date: string | number | Date = new Date(); + + switch (format) { + case 'abbr': + date = date.toLocaleTimeString(); + break; + case 'wide': + date = date.toTimeString(); + break; + case 'unix': + date = date.getTime(); + break; + } + + return date; + } +} diff --git a/src/modules/unique/index.ts b/src/modules/unique/index.ts new file mode 100644 index 00000000..798fc29e --- /dev/null +++ b/src/modules/unique/index.ts @@ -0,0 +1,139 @@ +import { deprecated } from '../../internal/deprecated'; +import type { RecordKey } from './unique'; +import * as uniqueExec from './unique'; + +/** + * Module to generate unique entries. + */ +export class Unique { + /** + * Maximum time `unique.exec` will attempt to run before aborting. + * + * @deprecated Use options instead. + */ + private _maxTime = 10; + + /** + * Maximum time `unique.exec` will attempt to run before aborting. + * + * @deprecated Use options instead. + */ + get maxTime(): number { + deprecated({ + deprecated: 'faker.unique.maxTime', + proposed: 'Options', + since: 'v6.2.0', + until: 'v7.0.0', + }); + return this._maxTime; + } + + /** + * Maximum time `unique.exec` will attempt to run before aborting. + * + * @deprecated Use options instead. + */ + set maxTime(value: number) { + deprecated({ + deprecated: 'faker.unique.maxTime', + proposed: 'Options', + since: 'v6.2.0', + until: 'v7.0.0', + }); + this._maxTime = value; + } + + /** + * Maximum retries `unique.exec` will recurse before aborting (max loop depth). + * + * @deprecated Use options instead. + */ + private _maxRetries = 10; + + /** + * Maximum retries `unique.exec` will recurse before aborting (max loop depth). + * + * @deprecated Use options instead. + */ + get maxRetries(): number { + deprecated({ + deprecated: 'faker.unique.maxRetries', + proposed: 'Options', + since: 'v6.2.0', + until: 'v7.0.0', + }); + return this._maxRetries; + } + + /** + * Maximum retries `unique.exec` will recurse before aborting (max loop depth). + * + * @deprecated Use options instead. + */ + set maxRetries(value: number) { + deprecated({ + deprecated: 'faker.unique.maxRetries', + proposed: 'Options', + since: 'v6.2.0', + until: 'v7.0.0', + }); + this._maxRetries = value; + } + + constructor() { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Unique.prototype)) { + if ( + name === 'constructor' || + name === 'maxTime' || + name === 'maxRetries' || + typeof this[name] !== 'function' + ) { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Generates a unique result using the results of the given method. + * Used unique entries will be stored internally and filtered from subsequent calls. + * + * @template Method The type of the method to execute. + * @param method The method used to generate the values. + * @param args The arguments used to call the method. + * @param options The optional options used to configure this method. + * @param options.startTime This parameter does nothing. + * @param options.maxTime The time in milliseconds this method may take before throwing an error. Defaults to `50`. + * @param options.maxRetries The total number of attempts to try before throwing an error. Defaults to `50`. + * @param options.currentIterations This parameter does nothing. + * @param options.exclude The value or values that should be excluded/skipped. Defaults to `[]`. + * @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key. + * @param options.store The store of unique entries. Defaults to a global store. + * + * @example + * faker.unique(faker.name.firstName) // 'Corbin' + */ + unique<Method extends (...parameters) => RecordKey>( + method: Method, + args?: Parameters<Method>, + options: { + startTime?: number; + maxTime?: number; + maxRetries?: number; + currentIterations?: number; + exclude?: RecordKey | RecordKey[]; + compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1; + store?: Record<RecordKey, RecordKey>; + } = {} + ): ReturnType<Method> { + const { maxTime = this._maxTime, maxRetries = this._maxRetries } = options; + return uniqueExec.exec(method, args, { + ...options, + startTime: new Date().getTime(), + maxTime, + maxRetries, + currentIterations: 0, + }); + } +} diff --git a/src/modules/unique/unique.ts b/src/modules/unique/unique.ts new file mode 100644 index 00000000..70408faf --- /dev/null +++ b/src/modules/unique/unique.ts @@ -0,0 +1,158 @@ +import { FakerError } from '../../errors/faker-error'; + +export type RecordKey = string | number | symbol; + +/** + * Global store of unique values. + * This means that faker should *never* return duplicate values across all API methods when using `Faker.unique` without passing `options.store`. + */ +const GLOBAL_UNIQUE_STORE: Record<RecordKey, RecordKey> = {}; + +/** + * Global exclude list of results. + * Defaults to nothing excluded. + */ +const GLOBAL_UNIQUE_EXCLUDE: RecordKey[] = []; + +/** + * Uniqueness compare function. + * Default behavior is to check value as key against object hash. + * + * @param obj The object to check. + * @param key The key to check. + */ +function defaultCompare( + obj: Record<RecordKey, RecordKey>, + key: RecordKey +): 0 | -1 { + if (obj[key] === undefined) { + return -1; + } + return 0; +} + +/** + * Logs the given code as an error and throws it. + * Also logs a message for helping the user. + * + * @param startTime The time the execution started. + * @param now The current time. + * @param code The error code. + * @param store The store of unique entries. + * @param currentIterations Current iteration or retries of `unique.exec` (current loop depth). + * + * @throws The given error code with additional text. + */ +function errorMessage( + startTime: number, + now: number, + code: string, + store: Record<RecordKey, RecordKey>, + currentIterations: number +): never { + console.error('Error', code); + console.log( + `Found ${Object.keys(store).length} unique entries before throwing error. +retried: ${currentIterations} +total time: ${now - startTime}ms` + ); + throw new FakerError( + `${code} for uniqueness check. + +May not be able to generate any more unique values with current settings. +Try adjusting maxTime or maxRetries parameters for faker.unique().` + ); +} + +/** + * Generates a unique result using the results of the given method. + * Used unique entries will be stored internally and filtered from subsequent calls. + * + * @template Method The type of the method to execute. + * @param method The method used to generate the values. + * @param args The arguments used to call the method. + * @param options The optional options used to configure this method. + * @param options.startTime The time this execution stared. Defaults to `new Date().getTime()`. + * @param options.maxTime The time in milliseconds this method may take before throwing an error. Defaults to `50`. + * @param options.maxRetries The total number of attempts to try before throwing an error. Defaults to `50`. + * @param options.currentIterations The current attempt. Defaults to `0`. + * @param options.exclude The value or values that should be excluded/skipped. Defaults to `[]`. + * @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key. + * @param options.store The store of unique entries. Defaults to `GLOBAL_UNIQUE_STORE`. + */ +export function exec<Method extends (...parameters) => RecordKey>( + method: Method, + args: Parameters<Method>, + options: { + startTime?: number; + maxTime?: number; + maxRetries?: number; + currentIterations?: number; + exclude?: RecordKey | RecordKey[]; + compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1; + store?: Record<RecordKey, RecordKey>; + } = {} +): ReturnType<Method> { + const now = new Date().getTime(); + + const { + startTime = new Date().getTime(), + maxTime = 50, + maxRetries = 50, + compare = defaultCompare, + store = GLOBAL_UNIQUE_STORE, + } = options; + let { exclude = GLOBAL_UNIQUE_EXCLUDE } = options; + options.currentIterations = options.currentIterations ?? 0; + + // Support single exclude argument as string + if (!Array.isArray(exclude)) { + exclude = [exclude]; + } + + // if (options.currentIterations > 0) { + // console.log('iterating', options.currentIterations) + // } + + // console.log(now - startTime) + if (now - startTime >= maxTime) { + return errorMessage( + startTime, + now, + `Exceeded maxTime: ${maxTime}`, + store, + options.currentIterations + ); + } + + if (options.currentIterations >= maxRetries) { + return errorMessage( + startTime, + now, + `Exceeded maxRetries: ${maxRetries}`, + store, + options.currentIterations + ); + } + + // Execute the provided method to find a potential satisfied value. + const result: ReturnType<Method> = method.apply(this, args); + + // If the result has not been previously found, add it to the found array and return the value as it's unique. + if (compare(store, result) === -1 && exclude.indexOf(result) === -1) { + store[result] = result; + options.currentIterations = 0; + return result; + } else { + // console.log('conflict', result); + options.currentIterations++; + return exec(method, args, { + ...options, + startTime, + maxTime, + maxRetries, + compare, + exclude, + }); + } +} diff --git a/src/modules/vehicle/index.ts b/src/modules/vehicle/index.ts new file mode 100644 index 00000000..ec010af9 --- /dev/null +++ b/src/modules/vehicle/index.ts @@ -0,0 +1,131 @@ +import type { Faker } from '../..'; + +/** + * Module to generate vehicle related entries. + */ +export class Vehicle { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Vehicle.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns a random vehicle. + * + * @example + * faker.vehicle.vehicle() // 'BMW Explorer' + */ + vehicle(): string { + return `${this.manufacturer()} ${this.model()}`; + } + + /** + * Returns a manufacturer name. + * + * @example + * faker.vehicle.manufacturer() // 'Ford' + */ + manufacturer(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.vehicle.manufacturer + ); + } + + /** + * Returns a vehicle model. + * + * @example + * faker.vehicle.model() // 'Explorer' + */ + model(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.vehicle.model + ); + } + + /** + * Returns a vehicle type. + * + * @example + * faker.vehicle.type() // 'Coupe' + */ + type(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.vehicle.type); + } + + /** + * Returns a fuel type. + * + * @example + * faker.vehicle.fuel() // 'Electric' + */ + fuel(): string { + return this.faker.helpers.arrayElement(this.faker.definitions.vehicle.fuel); + } + + /** + * Returns a vehicle identification number (VIN). + * + * @example + * faker.vehicle.vin() // 'YV1MH682762184654' + */ + vin(): string { + const bannedChars = ['o', 'i', 'q']; + return `${this.faker.random.alphaNumeric(10, { + bannedChars, + })}${this.faker.random.alpha({ + count: 1, + upcase: true, + bannedChars, + })}${this.faker.random.alphaNumeric(1, { + bannedChars, + })}${this.faker.datatype.number({ min: 10000, max: 99999 })}` // return five digit # + .toUpperCase(); + } + + /** + * Returns a vehicle color. + * + * @example + * faker.vehicle.color() // 'red' + */ + color(): string { + return this.faker.commerce.color(); + } + + /** + * Returns a vehicle registration number (Vehicle Registration Mark - VRM) + * + * @example + * faker.vehicle.vrm() // 'MF56UPA' + */ + vrm(): string { + return `${this.faker.random.alpha({ + count: 2, + upcase: true, + })}${this.faker.datatype.number({ + min: 0, + max: 9, + })}${this.faker.datatype.number({ + min: 0, + max: 9, + })}${this.faker.random.alpha({ count: 3, upcase: true })}`.toUpperCase(); + } + + /** + * Returns a type of bicycle. + * + * @example + * faker.vehicle.bicycle() // 'Adventure Road Bicycle' + */ + bicycle(): string { + return this.faker.helpers.arrayElement( + this.faker.definitions.vehicle.bicycle_type + ); + } +} diff --git a/src/modules/word/index.ts b/src/modules/word/index.ts new file mode 100644 index 00000000..5968d225 --- /dev/null +++ b/src/modules/word/index.ts @@ -0,0 +1,174 @@ +import type { Faker } from '../..'; + +/** + * Filters a string array for values with a specific length. + * If length is not provided or no values with this length there found a copy of the original array is returned. + * + * @param options The options to provide + * @param options.wordList A list of word to filter + * @param options.length The exact length words should have + */ +function filterWordListByLength(options: { + wordList: string[]; + length?: number; +}): string[] { + if (!options.length) { + return options.wordList; + } + + const wordListWithLengthFilter = options.wordList.filter( + (word) => word.length === options.length + ); + + return wordListWithLengthFilter.length > 0 + ? wordListWithLengthFilter + : [...options.wordList]; +} + +/** + * Module to return various types of words. + */ +export class Word { + constructor(private readonly faker: Faker) { + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Word.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + } + + /** + * Returns an adjective of random or optionally specified length. + * + * @param length Expected adjective length. If specified length is unresolvable, returns adjective of a random length. + * + * @example + * faker.word.adjective() // 'pungent' + * faker.word.adjective(5) // 'slimy' + * faker.word.adjective(100) // 'complete' + */ + adjective(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.adjective, + length, + }) + ); + } + + /** + * Returns an adverb of random or optionally specified length. + * + * @param length Expected adverb length. If specified length is unresolvable, returns adverb of a random length. + * + * @example + * faker.word.adverb() // 'quarrelsomely' + * faker.word.adverb(5) // 'madly' + * faker.word.adverb(100) // 'sadly' + */ + adverb(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.adverb, + length, + }) + ); + } + + /** + * Returns a conjunction of random or optionally specified length. + * + * @param length Expected conjunction length. If specified length is unresolvable, returns conjunction of a random length. + * + * @example + * faker.word.conjunction() // 'in order that' + * faker.word.conjunction(5) // 'since' + * faker.word.conjunction(100) // 'as long as' + */ + conjunction(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.conjunction, + length, + }) + ); + } + + /** + * Returns an interjection of random or optionally specified length. + * + * @param length Expected interjection length. If specified length is unresolvable, returns interjection of a random length. + * + * @example + * faker.word.interjection() // 'gah' + * faker.word.interjection(5) // 'fooey' + * faker.word.interjection(100) // 'yowza' + */ + interjection(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.interjection, + length, + }) + ); + } + + /** + * Returns a noun of random or optionally specified length. + * + * @param length Expected noun length. If specified length is unresolvable, returns noun of a random length. + * + * @example + * faker.word.noun() // 'external' + * faker.word.noun(5) // 'front' + * faker.word.noun(100) // 'care' + */ + noun(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.noun, + length, + }) + ); + } + + /** + * Returns a preposition of random or optionally specified length. + * + * @param length Expected preposition length. If specified length is unresolvable, returns preposition of a random length. + * + * @example + * faker.word.preposition() // 'without' + * faker.word.preposition(5) // 'abaft' + * faker.word.preposition(100) // 'an' + */ + preposition(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.preposition, + length, + }) + ); + } + + /** + * Returns a verb of random or optionally specified length. + * + * @param length Expected verb length. If specified length is unresolvable, returns verb of a random length. + * + * @example + * faker.word.verb() // 'act' + * faker.word.verb(5) // 'tinge' + * faker.word.verb(100) // 'mess' + */ + verb(length?: number): string { + return this.faker.helpers.arrayElement( + filterWordListByLength({ + wordList: this.faker.definitions.word.verb, + length, + }) + ); + } +} |
