Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(helpers): add fromRegExp method #1569

Merged
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8ae8530
feat: add character class regexp style string parse (close #1359)
wooneusean Nov 19, 2022
8f7c26a
feat: regexp style parsing add negation and dash (close #1359)
wooneusean Nov 19, 2022
fa46afe
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Nov 19, 2022
8c01f86
docs: update regexpStyleStringParse docs
wooneusean Nov 20, 2022
cef0d5c
test(helpers): added missing test for inverted ranges.
wooneusean Nov 20, 2022
6d25ea0
chore: merge next
wooneusean Dec 18, 2022
13ba7c9
feat: add fromRegExp function
wooneusean Dec 18, 2022
301bed7
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Dec 18, 2022
74a11db
fix: fixed negative index for splice function causing removal from ba…
wooneusean Dec 19, 2022
5094a9b
refactor: cleaned up fromRegExp function
wooneusean Dec 20, 2022
6c47f86
test: added non-seeded tests
wooneusean Jan 7, 2023
ec54405
fix: fix typescript errors
wooneusean Jan 7, 2023
bfe14e7
chore: reverted pnpm-lock.yaml
wooneusean Jan 9, 2023
5019f91
Merge remote-tracking branch 'upstream/next' into feat/helpers/regexp…
wooneusean Jan 9, 2023
2a63823
Merge remote-tracking branch 'upstream/next' into feat/helpers/regexp…
wooneusean Jan 11, 2023
ad88fbc
chore: addressed review and merged next
wooneusean Jan 11, 2023
5cd5408
Merge remote-tracking branch 'upstream/next' into feat/helpers/regexp…
wooneusean Jan 12, 2023
9b81969
chore: address test failure and merge next
wooneusean Jan 12, 2023
97e5a0e
Merge remote-tracking branch 'upstream/next' into feat/helpers/regexp…
wooneusean Jan 17, 2023
a8e9a42
docs: removed unneeded example for fromRegExp
wooneusean Jan 17, 2023
201720b
Merge remote-tracking branch 'upstream/next' into feat/helpers/regexp…
wooneusean Jan 18, 2023
ab948f1
fix: removed unnecessary null check
wooneusean Jan 18, 2023
c45ecd3
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Jan 21, 2023
ff685d8
fix: throw error for invalid quantifiers
wooneusean Jan 23, 2023
82a7129
chore: merge next
wooneusean Jan 23, 2023
dc35d9e
chore: merge next
wooneusean Jan 25, 2023
e31b512
test: rewrote tests for fromRegExp
wooneusean Feb 10, 2023
53ff47b
chore: merged next
wooneusean Feb 10, 2023
c13ef4d
feat: added more quantifier options and wildcard
wooneusean Feb 10, 2023
e16699b
docs: update fromRegExp examples
wooneusean Feb 10, 2023
3efaeb2
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Feb 11, 2023
98fcf9e
chore: merge next
wooneusean Feb 25, 2023
9b2b6a3
Merge branch 'feat/helpers/regexpStyleStringParse-letter-range' of ht…
wooneusean Feb 25, 2023
77e85f9
fix: addressed reviews
wooneusean Feb 25, 2023
81dfcd7
chore: merge next
wooneusean Mar 3, 2023
f9c8a2c
fix: resolved reviews and moved quantifer function out of class.
wooneusean Mar 3, 2023
7836043
test: updated snapshots
wooneusean Mar 3, 2023
3cd1a11
chore: merge next
wooneusean Mar 3, 2023
f3fba4c
refactor: moved duplicate code into one function
wooneusean Mar 3, 2023
3a9f7c7
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Mar 16, 2023
2983a8e
chore: merge next
wooneusean Mar 17, 2023
a232d3d
fix: address review
wooneusean Mar 17, 2023
00a838f
chore: merge next
wooneusean Mar 26, 2023
68cfc9b
fix: changed fallback repetition to 1 instead of 0
wooneusean Mar 26, 2023
d06bc55
fix: corrected typo and clarified character encoding in JSDoc
wooneusean Mar 26, 2023
6360860
Merge branch 'next' into feat/helpers/regexpStyleStringParse-letter-r…
ST-DDT Mar 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = 0;
wooneusean marked this conversation as resolved.
Show resolved Hide resolved
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:
wooneusean marked this conversation as resolved.
Show resolved Hide resolved
* - `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 ACSCII number or letter character that is not in the given range. (e.g. `[^0-9]` will get a random non-numeric character).
wooneusean marked this conversation as resolved.
Show resolved Hide resolved
* - `[-...]` => 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 character that can be any number, character or symbol. Can be combined with quantifiers as well.
wooneusean marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
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
Loading