Skip to content

Commit

Permalink
feat(number): add romanNumeral method (#3070)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmaanRS authored Oct 31, 2024
1 parent c02beea commit 72937de
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 1 deletion.
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"
*
* @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;

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

0 comments on commit 72937de

Please sign in to comment.