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(number): add romanNumeral method #3070

Merged
merged 18 commits into from
Oct 31, 2024
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
91 changes: 91 additions & 0 deletions src/modules/number/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,95 @@ export class NumberModule extends SimpleModuleBase {

return min + offset;
}

/**
* Returns a roman numeral in String format.
* The bounds are inclusive.
*
* @param options Maximum value or options object.
* @param options.min Lower bound for generated roman numerals. Defaults to `1`.
* @param options.max Upper bound for generated roman numerals. Defaults to `3999`.
*
* @throws When `min` is greater than `max`.
* @throws When `min`, `max` is not a number.
* @throws When `min` is less than `1`.
* @throws When `max` is greater than `3999`.
*
* @example
* faker.number.romanNumeral() // "CMXCIII"
* faker.number.romanNumeral(5) // "III"
* faker.number.romanNumeral({ min: 10 }) // "XCIX"
* faker.number.romanNumeral({ max: 20 }) // "XVII"
* faker.number.romanNumeral({ min: 5, max: 10 }) // "VII"
*
matthewmayer marked this conversation as resolved.
Show resolved Hide resolved
* @since 9.2.0
*/
romanNumeral(
options:
| number
| {
/**
* Lower bound for generated number.
*
* @default 1
*/
min?: number;
/**
* Upper bound for generated number.
*
* @default 3999
*/
max?: number;
} = {}
): string {
const DEFAULT_MIN = 1;
const DEFAULT_MAX = 3999;
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved

if (typeof options === 'number') {
options = {
max: options,
};
}

const { min = DEFAULT_MIN, max = DEFAULT_MAX } = options;

if (min < DEFAULT_MIN) {
throw new FakerError(
`Min value ${min} should be ${DEFAULT_MIN} or greater.`
);
}

if (max > DEFAULT_MAX) {
throw new FakerError(
`Max value ${max} should be ${DEFAULT_MAX} or less.`
);
}

let num = this.int({ min, max });

const lookup: Array<[string, number]> = [
['M', 1000],
['CM', 900],
['D', 500],
['CD', 400],
['C', 100],
['XC', 90],
['L', 50],
['XL', 40],
['X', 10],
['IX', 9],
['V', 5],
['IV', 4],
['I', 1],
];

let result = '';

for (const [k, v] of lookup) {
result += k.repeat(Math.floor(num / v));
num %= v;
}

return result;
}
}
42 changes: 42 additions & 0 deletions test/modules/__snapshots__/number.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ exports[`number > 42 > octal > with options 1`] = `"4"`;

exports[`number > 42 > octal > with value 1`] = `"0"`;

exports[`number > 42 > romanNumeral > noArgs 1`] = `"MCDXCVIII"`;

exports[`number > 42 > romanNumeral > with max as 3999 1`] = `"MCDXCVIII"`;

exports[`number > 42 > romanNumeral > with min and max 1`] = `"CCL"`;

exports[`number > 42 > romanNumeral > with min as 1 1`] = `"MCDXCVIII"`;

exports[`number > 42 > romanNumeral > with number value 1`] = `"CCCLXXV"`;

exports[`number > 42 > romanNumeral > with only max 1`] = `"LXII"`;

exports[`number > 42 > romanNumeral > with only min 1`] = `"MDI"`;

exports[`number > 1211 > bigInt > noArgs 1`] = `982966736876848n`;

exports[`number > 1211 > bigInt > with big options 1`] = `25442250580110979794946298n`;
Expand Down Expand Up @@ -100,6 +114,20 @@ exports[`number > 1211 > octal > with options 1`] = `"12"`;

exports[`number > 1211 > octal > with value 1`] = `"1"`;

exports[`number > 1211 > romanNumeral > noArgs 1`] = `"MMMDCCXIV"`;

exports[`number > 1211 > romanNumeral > with max as 3999 1`] = `"MMMDCCXIV"`;

exports[`number > 1211 > romanNumeral > with min and max 1`] = `"CDLXXIV"`;

exports[`number > 1211 > romanNumeral > with min as 1 1`] = `"MMMDCCXIV"`;

exports[`number > 1211 > romanNumeral > with number value 1`] = `"CMXXIX"`;

exports[`number > 1211 > romanNumeral > with only max 1`] = `"CLIV"`;

exports[`number > 1211 > romanNumeral > with only min 1`] = `"MMMDCCXIV"`;

exports[`number > 1337 > bigInt > noArgs 1`] = `212435297136194n`;

exports[`number > 1337 > bigInt > with big options 1`] = `27379244885156992800029992n`;
Expand Down Expand Up @@ -149,3 +177,17 @@ exports[`number > 1337 > octal > noArgs 1`] = `"2"`;
exports[`number > 1337 > octal > with options 1`] = `"2"`;

exports[`number > 1337 > octal > with value 1`] = `"0"`;

exports[`number > 1337 > romanNumeral > noArgs 1`] = `"MXLVIII"`;

exports[`number > 1337 > romanNumeral > with max as 3999 1`] = `"MXLVIII"`;

exports[`number > 1337 > romanNumeral > with min and max 1`] = `"CCV"`;

exports[`number > 1337 > romanNumeral > with min as 1 1`] = `"MXLVIII"`;

exports[`number > 1337 > romanNumeral > with number value 1`] = `"CCLXIII"`;

exports[`number > 1337 > romanNumeral > with only max 1`] = `"XLIV"`;

exports[`number > 1337 > romanNumeral > with only min 1`] = `"MLI"`;
74 changes: 73 additions & 1 deletion test/modules/number.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import validator from 'validator';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { FakerError, SimpleFaker, faker } from '../../src';
import { seededTests } from '../support/seeded-runs';
import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils';
Expand Down Expand Up @@ -47,6 +47,16 @@ describe('number', () => {
max: 32465761264574654845432354n,
});
});

t.describe('romanNumeral', (t) => {
t.it('noArgs')
.it('with number value', 1000)
.it('with only min', { min: 5 })
.it('with only max', { max: 165 })
.it('with min as 1', { min: 1 })
.it('with max as 3999', { max: 3999 })
.it('with min and max', { min: 100, max: 502 });
});
});

describe(`random seeded tests for seed ${faker.seed()}`, () => {
Expand Down Expand Up @@ -625,6 +635,68 @@ describe('number', () => {
);
});
});

describe('romanNumeral', () => {
it('should generate a Roman numeral within default range', () => {
const roman = faker.number.romanNumeral();
expect(roman).toBeTypeOf('string');
expect(roman).toMatch(/^[IVXLCDM]+$/);
});

it('should generate a Roman numeral with max value of 1000', () => {
const roman = faker.number.romanNumeral(1000);
expect(roman).toMatch(/^[IVXLCDM]+$/);
});

it.each(
Object.entries({
I: 1,
IV: 4,
IX: 9,
X: 10,
XXVII: 27,
XC: 90,
XCIX: 99,
CCLXIII: 263,
DXXXVI: 536,
DCCXIX: 719,
MDCCCLI: 1851,
MDCCCXCII: 1892,
MMCLXXXIII: 2183,
MMCMXLIII: 2943,
MMMDCCLXVI: 3766,
MMMDCCLXXIV: 3774,
MMMCMXCIX: 3999,
})
)(
'should generate a Roman numeral %s for value %d',
(expected: string, value: number) => {
const mock = vi.spyOn(faker.number, 'int');
mock.mockReturnValue(value);
const actual = faker.number.romanNumeral();
mock.mockRestore();
expect(actual).toBe(expected);
}
);

it('should throw when min value is less than 1', () => {
expect(() => {
faker.number.romanNumeral({ min: 0 });
}).toThrow(new FakerError('Min value 0 should be 1 or greater.'));
});

it('should throw when max value is greater than 3999', () => {
expect(() => {
faker.number.romanNumeral({ max: 4000 });
}).toThrow(new FakerError('Max value 4000 should be 3999 or less.'));
});

it('should throw when max value is less than min value', () => {
expect(() => {
faker.number.romanNumeral({ min: 500, max: 100 });
}).toThrow(new FakerError('Max 100 should be greater than min 500.'));
});
});
});

describe('value range tests', () => {
Expand Down