Skip to content

Commit 8516bfb

Browse files
authored
feat(helpers): add fromRegExp method (#1569)
1 parent 2a4f137 commit 8516bfb

File tree

3 files changed

+453
-0
lines changed

3 files changed

+453
-0
lines changed

src/modules/helpers/index.ts

+295
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,72 @@ import { luhnCheckValue } from './luhn-check';
44
import type { RecordKey } from './unique';
55
import * as uniqueExec from './unique';
66

7+
/**
8+
* Returns a number based on given RegEx-based quantifier symbol or quantifier values.
9+
*
10+
* @param faker Faker instance
11+
* @param quantifierSymbol Quantifier symbols can be either of these: `?`, `*`, `+`.
12+
* @param quantifierMin Quantifier minimum value. If given without a maximum, this will be used as the quantifier value.
13+
* @param quantifierMax Quantifier maximum value. Will randomly get a value between the minimum and maximum if both are provided.
14+
*
15+
* @returns a random number based on the given quantifier parameters.
16+
*
17+
* @example
18+
* getRepetitionsBasedOnQuantifierParameters(this.faker, '*', null, null) // 3
19+
* getRepetitionsBasedOnQuantifierParameters(this.faker, null, 10, null) // 10
20+
* getRepetitionsBasedOnQuantifierParameters(this.faker, null, 5, 8) // 6
21+
*
22+
* @since 8.0.0
23+
*/
24+
function getRepetitionsBasedOnQuantifierParameters(
25+
faker: Faker,
26+
quantifierSymbol: string,
27+
quantifierMin: string,
28+
quantifierMax: string
29+
) {
30+
let repetitions = 1;
31+
if (quantifierSymbol) {
32+
switch (quantifierSymbol) {
33+
case '?': {
34+
repetitions = faker.datatype.boolean() ? 0 : 1;
35+
break;
36+
}
37+
38+
case '*': {
39+
let limit = 1;
40+
while (faker.datatype.boolean()) {
41+
limit *= 2;
42+
}
43+
44+
repetitions = faker.number.int({ min: 0, max: limit });
45+
break;
46+
}
47+
48+
case '+': {
49+
let limit = 1;
50+
while (faker.datatype.boolean()) {
51+
limit *= 2;
52+
}
53+
54+
repetitions = faker.number.int({ min: 1, max: limit });
55+
break;
56+
}
57+
58+
default:
59+
throw new FakerError('Unknown quantifier symbol provided.');
60+
}
61+
} else if (quantifierMin != null && quantifierMax != null) {
62+
repetitions = faker.number.int({
63+
min: parseInt(quantifierMin),
64+
max: parseInt(quantifierMax),
65+
});
66+
} else if (quantifierMin != null && quantifierMax == null) {
67+
repetitions = parseInt(quantifierMin);
68+
}
69+
70+
return repetitions;
71+
}
72+
773
/**
874
* Module with various helper methods providing basic (seed-dependent) operations useful for implementing faker methods.
975
*/
@@ -247,6 +313,235 @@ export class HelpersModule {
247313
return string;
248314
}
249315

316+
/**
317+
* Generates a string matching the given regex like expressions.
318+
*
319+
* This function doesn't provide full support of actual `RegExp`.
320+
* Features such as grouping, anchors and character classes are not supported.
321+
* If you are looking for a library that randomly generates strings based on
322+
* `RegExp`s, see [randexp.js](https://github.com/fent/randexp.js)
323+
*
324+
* Supported patterns:
325+
* - `x{times}` => Repeat the `x` exactly `times` times.
326+
* - `x{min,max}` => Repeat the `x` `min` to `max` times.
327+
* - `[x-y]` => Randomly get a character between `x` and `y` (inclusive).
328+
* - `[x-y]{times}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `times` times.
329+
* - `[x-y]{min,max}` => Randomly get a character between `x` and `y` (inclusive) and repeat it `min` to `max` times.
330+
* - `[^...]` => 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).
331+
* - `[-...]` => 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).
332+
* - `/[x-y]/i` => Randomly gets an uppercase or lowercase character between `x` and `y` (inclusive).
333+
* - `x?` => Randomly decide to include or not include `x`.
334+
* - `[x-y]?` => Randomly decide to include or not include characters between `x` and `y` (inclusive).
335+
* - `x*` => Repeat `x` 0 or more times.
336+
* - `[x-y]*` => Repeat characters between `x` and `y` (inclusive) 0 or more times.
337+
* - `x+` => Repeat `x` 1 or more times.
338+
* - `[x-y]+` => Repeat characters between `x` and `y` (inclusive) 1 or more times.
339+
* - `.` => returns a wildcard ASCII character that can be any number, character or symbol. Can be combined with quantifiers as well.
340+
*
341+
* @param pattern The template string/RegExp to to generate a matching string for.
342+
*
343+
* @throws If min value is more than max value in quantifier. e.g. `#{10,5}`
344+
* @throws If invalid quantifier symbol is passed in.
345+
*
346+
* @example
347+
* faker.helpers.fromRegExp('#{5}') // '#####'
348+
* faker.helpers.fromRegExp('#{2,9}') // '#######'
349+
* faker.helpers.fromRegExp('[1-7]') // '5'
350+
* faker.helpers.fromRegExp('#{3}test[1-5]') // '###test3'
351+
* faker.helpers.fromRegExp('[0-9a-dmno]') // '5'
352+
* faker.helpers.fromRegExp('[^a-zA-Z0-8]') // '9'
353+
* faker.helpers.fromRegExp('[a-d0-6]{2,8}') // 'a0dc45b0'
354+
* faker.helpers.fromRegExp('[-a-z]{5}') // 'a-zab'
355+
* faker.helpers.fromRegExp(/[A-Z0-9]{4}-[A-Z0-9]{4}/) // 'BS4G-485H'
356+
* faker.helpers.fromRegExp(/[A-Z]{5}/i) // 'pDKfh'
357+
* faker.helpers.fromRegExp(/.{5}/) // '14(#B'
358+
* faker.helpers.fromRegExp(/Joh?n/) // 'Jon'
359+
* faker.helpers.fromRegExp(/ABC*DE/) // 'ABDE'
360+
* faker.helpers.fromRegExp(/bee+p/) // 'beeeeeeeep'
361+
*
362+
* @since 8.0.0
363+
*/
364+
fromRegExp(pattern: string | RegExp): string {
365+
let isCaseInsensitive = false;
366+
367+
if (pattern instanceof RegExp) {
368+
isCaseInsensitive = pattern.flags.includes('i');
369+
pattern = pattern.toString();
370+
pattern = pattern.match(/\/(.+?)\//)?.[1] ?? ''; // Remove frontslash from front and back of RegExp
371+
}
372+
373+
let min: number;
374+
let max: number;
375+
let repetitions: number;
376+
377+
// Deal with single wildcards
378+
const SINGLE_CHAR_REG =
379+
/([.A-Za-z0-9])(?:\{(\d+)(?:\,(\d+)|)\}|(\?|\*|\+))(?![^[]*]|[^{]*})/;
380+
let token = pattern.match(SINGLE_CHAR_REG);
381+
while (token != null) {
382+
const quantifierMin: string = token[2];
383+
const quantifierMax: string = token[3];
384+
const quantifierSymbol: string = token[4];
385+
386+
repetitions = getRepetitionsBasedOnQuantifierParameters(
387+
this.faker,
388+
quantifierSymbol,
389+
quantifierMin,
390+
quantifierMax
391+
);
392+
393+
pattern =
394+
pattern.slice(0, token.index) +
395+
token[1].repeat(repetitions) +
396+
pattern.slice(token.index + token[0].length);
397+
token = pattern.match(SINGLE_CHAR_REG);
398+
}
399+
400+
const SINGLE_RANGE_REG = /(\d-\d|\w-\w|\d|\w|[-!@#$&()`.+,/"])/;
401+
const RANGE_ALPHANUMEMRIC_REG =
402+
/\[(\^|)(-|)(.+?)\](?:\{(\d+)(?:\,(\d+)|)\}|(\?|\*|\+)|)/;
403+
// Deal with character classes with quantifiers `[a-z0-9]{min[, max]}`
404+
token = pattern.match(RANGE_ALPHANUMEMRIC_REG);
405+
while (token != null) {
406+
const isNegated = token[1] === '^';
407+
const includesDash: boolean = token[2] === '-';
408+
const quantifierMin: string = token[4];
409+
const quantifierMax: string = token[5];
410+
const quantifierSymbol: string = token[6];
411+
412+
const rangeCodes: number[] = [];
413+
414+
let ranges = token[3];
415+
let range = ranges.match(SINGLE_RANGE_REG);
416+
417+
if (includesDash) {
418+
// 45 is the ascii code for '-'
419+
rangeCodes.push(45);
420+
}
421+
422+
while (range != null) {
423+
if (range[0].indexOf('-') === -1) {
424+
// handle non-ranges
425+
if (isCaseInsensitive && isNaN(Number(range[0]))) {
426+
rangeCodes.push(range[0].toUpperCase().charCodeAt(0));
427+
rangeCodes.push(range[0].toLowerCase().charCodeAt(0));
428+
} else {
429+
rangeCodes.push(range[0].charCodeAt(0));
430+
}
431+
} else {
432+
// handle ranges
433+
const rangeMinMax = range[0].split('-').map((x) => x.charCodeAt(0));
434+
min = rangeMinMax[0];
435+
max = rangeMinMax[1];
436+
// throw error if min larger than max
437+
if (min > max) {
438+
throw new FakerError('Character range provided is out of order.');
439+
}
440+
441+
for (let i = min; i <= max; i++) {
442+
if (isCaseInsensitive && isNaN(Number(String.fromCharCode(i)))) {
443+
const ch = String.fromCharCode(i);
444+
rangeCodes.push(ch.toUpperCase().charCodeAt(0));
445+
rangeCodes.push(ch.toLowerCase().charCodeAt(0));
446+
} else {
447+
rangeCodes.push(i);
448+
}
449+
}
450+
}
451+
452+
ranges = ranges.substring(range[0].length);
453+
range = ranges.match(SINGLE_RANGE_REG);
454+
}
455+
456+
repetitions = getRepetitionsBasedOnQuantifierParameters(
457+
this.faker,
458+
quantifierSymbol,
459+
quantifierMin,
460+
quantifierMax
461+
);
462+
463+
if (isNegated) {
464+
let index = -1;
465+
// 0-9
466+
for (let i = 48; i <= 57; i++) {
467+
index = rangeCodes.indexOf(i);
468+
if (index > -1) {
469+
rangeCodes.splice(index, 1);
470+
continue;
471+
}
472+
473+
rangeCodes.push(i);
474+
}
475+
476+
// A-Z
477+
for (let i = 65; i <= 90; i++) {
478+
index = rangeCodes.indexOf(i);
479+
if (index > -1) {
480+
rangeCodes.splice(index, 1);
481+
continue;
482+
}
483+
484+
rangeCodes.push(i);
485+
}
486+
487+
// a-z
488+
for (let i = 97; i <= 122; i++) {
489+
index = rangeCodes.indexOf(i);
490+
if (index > -1) {
491+
rangeCodes.splice(index, 1);
492+
continue;
493+
}
494+
495+
rangeCodes.push(i);
496+
}
497+
}
498+
499+
const generatedString = this.multiple(
500+
() => String.fromCharCode(this.arrayElement(rangeCodes)),
501+
{ count: repetitions }
502+
).join('');
503+
504+
pattern =
505+
pattern.slice(0, token.index) +
506+
generatedString +
507+
pattern.slice(token.index + token[0].length);
508+
token = pattern.match(RANGE_ALPHANUMEMRIC_REG);
509+
}
510+
511+
const RANGE_REP_REG = /(.)\{(\d+)\,(\d+)\}/;
512+
// Deal with quantifier ranges `{min,max}`
513+
token = pattern.match(RANGE_REP_REG);
514+
while (token != null) {
515+
min = parseInt(token[2]);
516+
max = parseInt(token[3]);
517+
// throw error if min larger than max
518+
if (min > max) {
519+
throw new FakerError('Numbers out of order in {} quantifier.');
520+
}
521+
522+
repetitions = this.faker.number.int({ min, max });
523+
pattern =
524+
pattern.slice(0, token.index) +
525+
token[1].repeat(repetitions) +
526+
pattern.slice(token.index + token[0].length);
527+
token = pattern.match(RANGE_REP_REG);
528+
}
529+
530+
const REP_REG = /(.)\{(\d+)\}/;
531+
// Deal with repeat `{num}`
532+
token = pattern.match(REP_REG);
533+
while (token != null) {
534+
repetitions = parseInt(token[2]);
535+
pattern =
536+
pattern.slice(0, token.index) +
537+
token[1].repeat(repetitions) +
538+
pattern.slice(token.index + token[0].length);
539+
token = pattern.match(REP_REG);
540+
}
541+
542+
return pattern;
543+
}
544+
250545
/**
251546
* Takes an array and randomizes it in place then returns it.
252547
*

0 commit comments

Comments
 (0)