Skip to content

Commit 72937de

Browse files
authored
feat(number): add romanNumeral method (#3070)
1 parent c02beea commit 72937de

File tree

3 files changed

+206
-1
lines changed

3 files changed

+206
-1
lines changed

src/modules/number/index.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,95 @@ export class NumberModule extends SimpleModuleBase {
443443

444444
return min + offset;
445445
}
446+
447+
/**
448+
* Returns a roman numeral in String format.
449+
* The bounds are inclusive.
450+
*
451+
* @param options Maximum value or options object.
452+
* @param options.min Lower bound for generated roman numerals. Defaults to `1`.
453+
* @param options.max Upper bound for generated roman numerals. Defaults to `3999`.
454+
*
455+
* @throws When `min` is greater than `max`.
456+
* @throws When `min`, `max` is not a number.
457+
* @throws When `min` is less than `1`.
458+
* @throws When `max` is greater than `3999`.
459+
*
460+
* @example
461+
* faker.number.romanNumeral() // "CMXCIII"
462+
* faker.number.romanNumeral(5) // "III"
463+
* faker.number.romanNumeral({ min: 10 }) // "XCIX"
464+
* faker.number.romanNumeral({ max: 20 }) // "XVII"
465+
* faker.number.romanNumeral({ min: 5, max: 10 }) // "VII"
466+
*
467+
* @since 9.2.0
468+
*/
469+
romanNumeral(
470+
options:
471+
| number
472+
| {
473+
/**
474+
* Lower bound for generated number.
475+
*
476+
* @default 1
477+
*/
478+
min?: number;
479+
/**
480+
* Upper bound for generated number.
481+
*
482+
* @default 3999
483+
*/
484+
max?: number;
485+
} = {}
486+
): string {
487+
const DEFAULT_MIN = 1;
488+
const DEFAULT_MAX = 3999;
489+
490+
if (typeof options === 'number') {
491+
options = {
492+
max: options,
493+
};
494+
}
495+
496+
const { min = DEFAULT_MIN, max = DEFAULT_MAX } = options;
497+
498+
if (min < DEFAULT_MIN) {
499+
throw new FakerError(
500+
`Min value ${min} should be ${DEFAULT_MIN} or greater.`
501+
);
502+
}
503+
504+
if (max > DEFAULT_MAX) {
505+
throw new FakerError(
506+
`Max value ${max} should be ${DEFAULT_MAX} or less.`
507+
);
508+
}
509+
510+
let num = this.int({ min, max });
511+
512+
const lookup: Array<[string, number]> = [
513+
['M', 1000],
514+
['CM', 900],
515+
['D', 500],
516+
['CD', 400],
517+
['C', 100],
518+
['XC', 90],
519+
['L', 50],
520+
['XL', 40],
521+
['X', 10],
522+
['IX', 9],
523+
['V', 5],
524+
['IV', 4],
525+
['I', 1],
526+
];
527+
528+
let result = '';
529+
530+
for (const [k, v] of lookup) {
531+
result += k.repeat(Math.floor(num / v));
532+
num %= v;
533+
}
534+
535+
return result;
536+
}
446537
}

test/modules/__snapshots__/number.spec.ts.snap

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ exports[`number > 42 > octal > with options 1`] = `"4"`;
5050

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

53+
exports[`number > 42 > romanNumeral > noArgs 1`] = `"MCDXCVIII"`;
54+
55+
exports[`number > 42 > romanNumeral > with max as 3999 1`] = `"MCDXCVIII"`;
56+
57+
exports[`number > 42 > romanNumeral > with min and max 1`] = `"CCL"`;
58+
59+
exports[`number > 42 > romanNumeral > with min as 1 1`] = `"MCDXCVIII"`;
60+
61+
exports[`number > 42 > romanNumeral > with number value 1`] = `"CCCLXXV"`;
62+
63+
exports[`number > 42 > romanNumeral > with only max 1`] = `"LXII"`;
64+
65+
exports[`number > 42 > romanNumeral > with only min 1`] = `"MDI"`;
66+
5367
exports[`number > 1211 > bigInt > noArgs 1`] = `982966736876848n`;
5468

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

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

117+
exports[`number > 1211 > romanNumeral > noArgs 1`] = `"MMMDCCXIV"`;
118+
119+
exports[`number > 1211 > romanNumeral > with max as 3999 1`] = `"MMMDCCXIV"`;
120+
121+
exports[`number > 1211 > romanNumeral > with min and max 1`] = `"CDLXXIV"`;
122+
123+
exports[`number > 1211 > romanNumeral > with min as 1 1`] = `"MMMDCCXIV"`;
124+
125+
exports[`number > 1211 > romanNumeral > with number value 1`] = `"CMXXIX"`;
126+
127+
exports[`number > 1211 > romanNumeral > with only max 1`] = `"CLIV"`;
128+
129+
exports[`number > 1211 > romanNumeral > with only min 1`] = `"MMMDCCXIV"`;
130+
103131
exports[`number > 1337 > bigInt > noArgs 1`] = `212435297136194n`;
104132

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

151179
exports[`number > 1337 > octal > with value 1`] = `"0"`;
180+
181+
exports[`number > 1337 > romanNumeral > noArgs 1`] = `"MXLVIII"`;
182+
183+
exports[`number > 1337 > romanNumeral > with max as 3999 1`] = `"MXLVIII"`;
184+
185+
exports[`number > 1337 > romanNumeral > with min and max 1`] = `"CCV"`;
186+
187+
exports[`number > 1337 > romanNumeral > with min as 1 1`] = `"MXLVIII"`;
188+
189+
exports[`number > 1337 > romanNumeral > with number value 1`] = `"CCLXIII"`;
190+
191+
exports[`number > 1337 > romanNumeral > with only max 1`] = `"XLIV"`;
192+
193+
exports[`number > 1337 > romanNumeral > with only min 1`] = `"MLI"`;

test/modules/number.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import validator from 'validator';
2-
import { describe, expect, it } from 'vitest';
2+
import { describe, expect, it, vi } from 'vitest';
33
import { FakerError, SimpleFaker, faker } from '../../src';
44
import { seededTests } from '../support/seeded-runs';
55
import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils';
@@ -47,6 +47,16 @@ describe('number', () => {
4747
max: 32465761264574654845432354n,
4848
});
4949
});
50+
51+
t.describe('romanNumeral', (t) => {
52+
t.it('noArgs')
53+
.it('with number value', 1000)
54+
.it('with only min', { min: 5 })
55+
.it('with only max', { max: 165 })
56+
.it('with min as 1', { min: 1 })
57+
.it('with max as 3999', { max: 3999 })
58+
.it('with min and max', { min: 100, max: 502 });
59+
});
5060
});
5161

5262
describe(`random seeded tests for seed ${faker.seed()}`, () => {
@@ -625,6 +635,68 @@ describe('number', () => {
625635
);
626636
});
627637
});
638+
639+
describe('romanNumeral', () => {
640+
it('should generate a Roman numeral within default range', () => {
641+
const roman = faker.number.romanNumeral();
642+
expect(roman).toBeTypeOf('string');
643+
expect(roman).toMatch(/^[IVXLCDM]+$/);
644+
});
645+
646+
it('should generate a Roman numeral with max value of 1000', () => {
647+
const roman = faker.number.romanNumeral(1000);
648+
expect(roman).toMatch(/^[IVXLCDM]+$/);
649+
});
650+
651+
it.each(
652+
Object.entries({
653+
I: 1,
654+
IV: 4,
655+
IX: 9,
656+
X: 10,
657+
XXVII: 27,
658+
XC: 90,
659+
XCIX: 99,
660+
CCLXIII: 263,
661+
DXXXVI: 536,
662+
DCCXIX: 719,
663+
MDCCCLI: 1851,
664+
MDCCCXCII: 1892,
665+
MMCLXXXIII: 2183,
666+
MMCMXLIII: 2943,
667+
MMMDCCLXVI: 3766,
668+
MMMDCCLXXIV: 3774,
669+
MMMCMXCIX: 3999,
670+
})
671+
)(
672+
'should generate a Roman numeral %s for value %d',
673+
(expected: string, value: number) => {
674+
const mock = vi.spyOn(faker.number, 'int');
675+
mock.mockReturnValue(value);
676+
const actual = faker.number.romanNumeral();
677+
mock.mockRestore();
678+
expect(actual).toBe(expected);
679+
}
680+
);
681+
682+
it('should throw when min value is less than 1', () => {
683+
expect(() => {
684+
faker.number.romanNumeral({ min: 0 });
685+
}).toThrow(new FakerError('Min value 0 should be 1 or greater.'));
686+
});
687+
688+
it('should throw when max value is greater than 3999', () => {
689+
expect(() => {
690+
faker.number.romanNumeral({ max: 4000 });
691+
}).toThrow(new FakerError('Max value 4000 should be 3999 or less.'));
692+
});
693+
694+
it('should throw when max value is less than min value', () => {
695+
expect(() => {
696+
faker.number.romanNumeral({ min: 500, max: 100 });
697+
}).toThrow(new FakerError('Max 100 should be greater than min 500.'));
698+
});
699+
});
628700
});
629701

630702
describe('value range tests', () => {

0 commit comments

Comments
 (0)