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: add casing option #955

Merged
merged 9 commits into from
May 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
181 changes: 92 additions & 89 deletions src/modules/random/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { Faker } from '../..';
import { FakerError } from '../../errors/faker-error';
import { deprecated } from '../../internal/deprecated';

export type Casing = 'upper' | 'lower' | 'mixed';

const UPPER_CHARS: readonly string[] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const LOWER_CHARS: readonly string[] = 'abcdefghijklmnopqrstuvwxyz'.split('');
const DIGIT_CHARS: readonly string[] = '0123456789'.split('');

/**
* Method to reduce array of characters.
Expand Down Expand Up @@ -141,25 +148,30 @@ export class Random {
}

/**
* Generating a string consisting of lower/upper alpha characters based on count and upcase options.
* Generating a string consisting of letters in the English alphabet.
*
* @param options Either the number of characters or an options instance. Defaults to `{ count: 1, upcase: false, bannedChars: [] }`.
* @param options Either the number of characters or an options instance. Defaults to `{ count: 1, casing: 'lower', 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.casing The casing of the characters. Defaults to `'lower'`.
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
* @param options.upcase Deprecated, use `casing: 'upper'` instead.
* @param options.bannedChars An array with characters to exclude. Defaults to `[]`.
*
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
* @example
* faker.random.alpha() // 'b'
* faker.random.alpha(10) // 'qccrabobaf'
* faker.random.alpha({ count: 5, upcase: true, bannedChars: ['a'] }) // 'DTCIC'
* faker.random.alpha({ count: 5, casing: 'upper', bannedChars: ['A'] }) // 'DTCIC'
*/
// TODO @Shinigami92 2022-02-14: Tests covered `(count, options)`, but they were never typed like that
alpha(
options:
| number
| {
count?: number;
/**
* @deprecated Use `casing` instead.
*/
upcase?: boolean;
casing?: Casing;
bannedChars?: readonly string[];
} = {}
): string {
Expand All @@ -168,52 +180,59 @@ export class Random {
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',
];
const { count = 1, upcase, bannedChars = [] } = options;

if (count <= 0) {
return '';
}

const {
// Switch to 'mixed' with v8.0
casing = upcase ? 'upper' : 'lower',
} = options;

if (upcase != null) {
deprecated({
deprecated: 'faker.random.alpha({ upcase: true })',
proposed: "faker.random.alpha({ casing: 'upper' })",
since: 'v7.0',
until: 'v8.0',
});
}

let charsArray: string[];
switch (casing) {
case 'upper':
charsArray = [...UPPER_CHARS];
break;
case 'lower':
charsArray = [...LOWER_CHARS];
break;
case 'mixed':
default:
charsArray = [...LOWER_CHARS, ...UPPER_CHARS];
break;
}

charsArray = arrayRemove(charsArray, bannedChars);

let wholeString = '';
for (let i = 0; i < count; i++) {
wholeString += this.faker.helpers.arrayElement(charsArray);
if (charsArray.length === 0) {
throw new FakerError(
'Unable to generate string, because all possible characters are banned.'
);
}

return upcase ? wholeString.toUpperCase() : wholeString;
return Array.from({ length: count }, () =>
this.faker.helpers.arrayElement(charsArray)
).join('');
}

/**
* Generating a string consisting of lower/upper alpha characters and digits based on count and upcase options.
* Generating a string consisting of alpha characters and digits.
*
* @param count The number of characters and digits to generate. Defaults to `1`.
* @param options The options to use. Defaults to `{ bannedChars: [] }`.
* @param options.casing The casing of the characters. Defaults to `'lower'`.
* @param options.bannedChars An array of characters and digits which should be banned in the generated string. Defaults to `[]`.
*
* @example
Expand All @@ -223,48 +242,35 @@ export class Random {
*/
alphaNumeric(
count: number = 1,
options: { bannedChars?: readonly string[] } = {}
options: {
casing?: Casing;
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',
];
if (count <= 0) {
return '';
}

const {
// Switch to 'mixed' with v8.0
casing = 'lower',
bannedChars = [],
} = options;

let charsArray = [...DIGIT_CHARS];

switch (casing) {
case 'upper':
charsArray.push(...UPPER_CHARS);
break;
case 'lower':
charsArray.push(...LOWER_CHARS);
break;
case 'mixed':
default:
charsArray.push(...LOWER_CHARS, ...UPPER_CHARS);
break;
}

charsArray = arrayRemove(charsArray, bannedChars);

Expand All @@ -274,12 +280,9 @@ export class Random {
);
}

let wholeString = '';
for (let i = 0; i < count; i++) {
wholeString += this.faker.helpers.arrayElement(charsArray);
}

return wholeString;
return Array.from({ length: count }, () =>
this.faker.helpers.arrayElement(charsArray)
).join('');
}

/**
Expand Down Expand Up @@ -310,9 +313,9 @@ export class Random {

const { allowLeadingZeros = false, bannedDigits = [] } = options;

const allowedDigits = '0123456789'
.split('')
.filter((digit) => !bannedDigits.includes(digit));
const allowedDigits = DIGIT_CHARS.filter(
(digit) => !bannedDigits.includes(digit)
);

if (
allowedDigits.length === 0 ||
Expand Down
10 changes: 6 additions & 4 deletions src/modules/vehicle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ export class Vehicle {
* faker.vehicle.vin() // 'YV1MH682762184654'
*/
vin(): string {
const bannedChars = ['o', 'i', 'q'];
const bannedChars = ['o', 'i', 'q', 'O', 'I', 'Q'];
return `${this.faker.random.alphaNumeric(10, {
casing: 'upper',
bannedChars,
})}${this.faker.random.alpha({
count: 1,
upcase: true,
casing: 'upper',
bannedChars,
})}${this.faker.random.alphaNumeric(1, {
casing: 'upper',
bannedChars,
})}${this.faker.datatype.number({ min: 10000, max: 99999 })}` // return five digit #
.toUpperCase();
Expand All @@ -107,14 +109,14 @@ export class Vehicle {
vrm(): string {
return `${this.faker.random.alpha({
count: 2,
upcase: true,
casing: 'upper',
})}${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();
})}${this.faker.random.alpha({ count: 3, casing: 'upper' })}`.toUpperCase();
}

/**
Expand Down
61 changes: 55 additions & 6 deletions test/random.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,13 @@ describe('random', () => {
expect(actual).toMatch(/^[a-z]$/);
});

it('should return uppercase when upcase option is true', () => {
const actual = faker.random.alpha({ upcase: true });
expect(actual).toMatch(/^[A-Z]$/);
it.each([
['upper', /^[A-Z]{250}$/],
['lower', /^[a-z]{250}$/],
['mixed', /^[a-zA-Z]{250}$/],
] as const)('should return %s-case', (casing, pattern) => {
const actual = faker.random.alpha({ count: 250, casing });
expect(actual).toMatch(pattern);
});

it('should generate many random letters', () => {
Expand All @@ -190,6 +194,15 @@ describe('random', () => {
expect(actual).toHaveLength(5);
});

it.each([0, -1, -100])(
'should return empty string when length is <= 0',
(length) => {
const actual = faker.random.alpha(length);

expect(actual).toBe('');
}
);

it('should be able to ban some characters', () => {
const actual = faker.random.alpha({
count: 5,
Expand All @@ -210,14 +223,28 @@ describe('random', () => {
expect(alphaText).toMatch(/^[b-oq-z]{5}$/);
});

it('should throw if all possible characters being banned', () => {
const bannedChars = 'abcdefghijklmnopqrstuvwxyz'.split('');
expect(() =>
faker.random.alpha({
count: 5,
bannedChars,
})
).toThrowError(
new FakerError(
'Unable to generate string, because all possible characters are banned.'
)
);
});

it('should not mutate the input object', () => {
const input: {
count: number;
upcase: boolean;
casing: 'mixed';
bannedChars: string[];
} = Object.freeze({
count: 5,
upcase: true,
casing: 'mixed',
bannedChars: ['a', '%'],
});

Expand All @@ -233,12 +260,30 @@ describe('random', () => {
expect(actual).toHaveLength(1);
});

it.each([
['upper', /^[A-Z0-9]{250}$/],
['lower', /^[a-z0-9]{250}$/],
['mixed', /^[a-zA-Z0-9]{250}$/],
] as const)('should return %s-case', (casing, pattern) => {
const actual = faker.random.alphaNumeric(250, { casing });
expect(actual).toMatch(pattern);
});

it('should generate many random characters', () => {
const actual = faker.random.alphaNumeric(5);

expect(actual).toHaveLength(5);
});

it.each([0, -1, -100])(
'should return empty string when length is <= 0',
(length) => {
const actual = faker.random.alphaNumeric(length);

expect(actual).toBe('');
}
);

it('should be able to ban all alphabetic characters', () => {
const bannedChars = 'abcdefghijklmnopqrstuvwxyz'.split('');
const alphaText = faker.random.alphaNumeric(5, {
Expand Down Expand Up @@ -278,7 +323,11 @@ describe('random', () => {
faker.random.alphaNumeric(5, {
bannedChars,
})
).toThrowError();
).toThrowError(
new FakerError(
'Unable to generate string, because all possible characters are banned.'
)
);
});

it('should not mutate the input object', () => {
Expand Down