Skip to content

Commit

Permalink
feat(helpers): add fromRegExp method (#1569)
Browse files Browse the repository at this point in the history
  • Loading branch information
wooneusean committed Mar 27, 2023
1 parent 2a4f137 commit 8516bfb
Show file tree
Hide file tree
Showing 3 changed files with 453 additions and 0 deletions.
295 changes: 295 additions & 0 deletions src/modules/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,72 @@ import { luhnCheckValue } from './luhn-check';
import type { RecordKey } from './unique';
import * as uniqueExec from './unique';

/**
* Returns a number based on given RegEx-based quantifier symbol or quantifier values.
*
* @param faker Faker instance
* @param quantifierSymbol Quantifier symbols can be either of these: `?`, `*`, `+`.
* @param quantifierMin Quantifier minimum value. If given without a maximum, this will be used as the quantifier value.
* @param quantifierMax Quantifier maximum value. Will randomly get a value between the minimum and maximum if both are provided.
*
* @returns a random number based on the given quantifier parameters.
*
* @example
* getRepetitionsBasedOnQuantifierParameters(this.faker, '*', null, null) // 3
* getRepetitionsBasedOnQuantifierParameters(this.faker, null, 10, null) // 10
* getRepetitionsBasedOnQuantifierParameters(this.faker, null, 5, 8) // 6
*
* @since 8.0.0
*/
function getRepetitionsBasedOnQuantifierParameters(
faker: Faker,
quantifierSymbol: string,
quantifierMin: string,
quantifierMax: string
) {
let repetitions = 1;
if (quantifierSymbol) {
switch (quantifierSymbol) {
case '?': {
repetitions = faker.datatype.boolean() ? 0 : 1;
break;
}

case '*': {
let limit = 1;
while (faker.datatype.boolean()) {
limit *= 2;
}

repetitions = faker.number.int({ min: 0, max: limit });
break;
}

case '+': {
let limit = 1;
while (faker.datatype.boolean()) {
limit *= 2;
}

repetitions = faker.number.int({ min: 1, max: limit });
break;
}

default:
throw new FakerError('Unknown quantifier symbol provided.');
}
} else if (quantifierMin != null && quantifierMax != null) {
repetitions = faker.number.int({
min: parseInt(quantifierMin),
max: parseInt(quantifierMax),
});
} else if (quantifierMin != null && quantifierMax == null) {
repetitions = parseInt(quantifierMin);
}

return repetitions;
}

/**
* Module with various helper methods providing basic (seed-dependent) operations useful for implementing faker methods.
*/
Expand Down Expand Up @@ -247,6 +313,235 @@ export class HelpersModule {
return string;
}

/**
* Generates a string matching the given regex like expressions.
*
* This function doesn't provide full support of actual `RegExp`.
* Features such as grouping, anchors and character classes are not supported.
* If you are looking for a library that randomly generates strings based on
* `RegExp`s, see [randexp.js](https://github.com/fent/randexp.js)
*
* Supported patterns:
* - `x{times}` => Repeat the `x` exactly `times` times.
* - `x{min,max}` => Repeat the `x` `min` to `max` times.
* - `[x-y]` => Randomly get a character between `x` and `y` (inclusive).
* - `[x-y]{times}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `times` times.
* - `[x-y]{min,max}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `min` to `max` times.
* - `[^...]` => Randomly get an ASCII number or letter character that is not in the given range. (e.g. `[^0-9]` will get a random non-numeric character).
* - `[-...]` => Include dashes in the range. Must be placed after the negate character `^` and before any character sets if used (e.g. `[^-0-9]` will not get any numeric characters or dashes).
* - `/[x-y]/i` => Randomly gets an uppercase or lowercase character between `x` and `y` (inclusive).
* - `x?` => Randomly decide to include or not include `x`.
* - `[x-y]?` => Randomly decide to include or not include characters between `x` and `y` (inclusive).
* - `x*` => Repeat `x` 0 or more times.
* - `[x-y]*` => Repeat characters between `x` and `y` (inclusive) 0 or more times.
* - `x+` => Repeat `x` 1 or more times.
* - `[x-y]+` => Repeat characters between `x` and `y` (inclusive) 1 or more times.
* - `.` => returns a wildcard ASCII character that can be any number, character or symbol. Can be combined with quantifiers as well.
*
* @param pattern The template string/RegExp to to generate a matching string for.
*
* @throws If min value is more than max value in quantifier. e.g. `#{10,5}`
* @throws If invalid quantifier symbol is passed in.
*
* @example
* faker.helpers.fromRegExp('#{5}') // '#####'
* faker.helpers.fromRegExp('#{2,9}') // '#######'
* faker.helpers.fromRegExp('[1-7]') // '5'
* faker.helpers.fromRegExp('#{3}test[1-5]') // '###test3'
* faker.helpers.fromRegExp('[0-9a-dmno]') // '5'
* faker.helpers.fromRegExp('[^a-zA-Z0-8]') // '9'
* faker.helpers.fromRegExp('[a-d0-6]{2,8}') // 'a0dc45b0'
* faker.helpers.fromRegExp('[-a-z]{5}') // 'a-zab'
* faker.helpers.fromRegExp(/[A-Z0-9]{4}-[A-Z0-9]{4}/) // 'BS4G-485H'
* faker.helpers.fromRegExp(/[A-Z]{5}/i) // 'pDKfh'
* faker.helpers.fromRegExp(/.{5}/) // '14(#B'
* faker.helpers.fromRegExp(/Joh?n/) // 'Jon'
* faker.helpers.fromRegExp(/ABC*DE/) // 'ABDE'
* faker.helpers.fromRegExp(/bee+p/) // 'beeeeeeeep'
*
* @since 8.0.0
*/
fromRegExp(pattern: string | RegExp): string {
let isCaseInsensitive = false;

if (pattern instanceof RegExp) {
isCaseInsensitive = pattern.flags.includes('i');
pattern = pattern.toString();
pattern = pattern.match(/\/(.+?)\//)?.[1] ?? ''; // Remove frontslash from front and back of RegExp
}

let min: number;
let max: number;
let repetitions: number;

// Deal with single wildcards
const SINGLE_CHAR_REG =
/([.A-Za-z0-9])(?:\{(\d+)(?:\,(\d+)|)\}|(\?|\*|\+))(?![^[]*]|[^{]*})/;
let token = pattern.match(SINGLE_CHAR_REG);
while (token != null) {
const quantifierMin: string = token[2];
const quantifierMax: string = token[3];
const quantifierSymbol: string = token[4];

repetitions = getRepetitionsBasedOnQuantifierParameters(
this.faker,
quantifierSymbol,
quantifierMin,
quantifierMax
);

pattern =
pattern.slice(0, token.index) +
token[1].repeat(repetitions) +
pattern.slice(token.index + token[0].length);
token = pattern.match(SINGLE_CHAR_REG);
}

const SINGLE_RANGE_REG = /(\d-\d|\w-\w|\d|\w|[-!@#$&()`.+,/"])/;
const RANGE_ALPHANUMEMRIC_REG =
/\[(\^|)(-|)(.+?)\](?:\{(\d+)(?:\,(\d+)|)\}|(\?|\*|\+)|)/;
// Deal with character classes with quantifiers `[a-z0-9]{min[, max]}`
token = pattern.match(RANGE_ALPHANUMEMRIC_REG);
while (token != null) {
const isNegated = token[1] === '^';
const includesDash: boolean = token[2] === '-';
const quantifierMin: string = token[4];
const quantifierMax: string = token[5];
const quantifierSymbol: string = token[6];

const rangeCodes: number[] = [];

let ranges = token[3];
let range = ranges.match(SINGLE_RANGE_REG);

if (includesDash) {
// 45 is the ascii code for '-'
rangeCodes.push(45);
}

while (range != null) {
if (range[0].indexOf('-') === -1) {
// handle non-ranges
if (isCaseInsensitive && isNaN(Number(range[0]))) {
rangeCodes.push(range[0].toUpperCase().charCodeAt(0));
rangeCodes.push(range[0].toLowerCase().charCodeAt(0));
} else {
rangeCodes.push(range[0].charCodeAt(0));
}
} else {
// handle ranges
const rangeMinMax = range[0].split('-').map((x) => x.charCodeAt(0));
min = rangeMinMax[0];
max = rangeMinMax[1];
// throw error if min larger than max
if (min > max) {
throw new FakerError('Character range provided is out of order.');
}

for (let i = min; i <= max; i++) {
if (isCaseInsensitive && isNaN(Number(String.fromCharCode(i)))) {
const ch = String.fromCharCode(i);
rangeCodes.push(ch.toUpperCase().charCodeAt(0));
rangeCodes.push(ch.toLowerCase().charCodeAt(0));
} else {
rangeCodes.push(i);
}
}
}

ranges = ranges.substring(range[0].length);
range = ranges.match(SINGLE_RANGE_REG);
}

repetitions = getRepetitionsBasedOnQuantifierParameters(
this.faker,
quantifierSymbol,
quantifierMin,
quantifierMax
);

if (isNegated) {
let index = -1;
// 0-9
for (let i = 48; i <= 57; i++) {
index = rangeCodes.indexOf(i);
if (index > -1) {
rangeCodes.splice(index, 1);
continue;
}

rangeCodes.push(i);
}

// A-Z
for (let i = 65; i <= 90; i++) {
index = rangeCodes.indexOf(i);
if (index > -1) {
rangeCodes.splice(index, 1);
continue;
}

rangeCodes.push(i);
}

// a-z
for (let i = 97; i <= 122; i++) {
index = rangeCodes.indexOf(i);
if (index > -1) {
rangeCodes.splice(index, 1);
continue;
}

rangeCodes.push(i);
}
}

const generatedString = this.multiple(
() => String.fromCharCode(this.arrayElement(rangeCodes)),
{ count: repetitions }
).join('');

pattern =
pattern.slice(0, token.index) +
generatedString +
pattern.slice(token.index + token[0].length);
token = pattern.match(RANGE_ALPHANUMEMRIC_REG);
}

const RANGE_REP_REG = /(.)\{(\d+)\,(\d+)\}/;
// Deal with quantifier ranges `{min,max}`
token = pattern.match(RANGE_REP_REG);
while (token != null) {
min = parseInt(token[2]);
max = parseInt(token[3]);
// throw error if min larger than max
if (min > max) {
throw new FakerError('Numbers out of order in {} quantifier.');
}

repetitions = this.faker.number.int({ min, max });
pattern =
pattern.slice(0, token.index) +
token[1].repeat(repetitions) +
pattern.slice(token.index + token[0].length);
token = pattern.match(RANGE_REP_REG);
}

const REP_REG = /(.)\{(\d+)\}/;
// Deal with repeat `{num}`
token = pattern.match(REP_REG);
while (token != null) {
repetitions = parseInt(token[2]);
pattern =
pattern.slice(0, token.index) +
token[1].repeat(repetitions) +
pattern.slice(token.index + token[0].length);
token = pattern.match(REP_REG);
}

return pattern;
}

/**
* Takes an array and randomizes it in place then returns it.
*
Expand Down

0 comments on commit 8516bfb

Please sign in to comment.