From 72937de55c892c011846bc2b67dc0df61fbdf5a2 Mon Sep 17 00:00:00 2001 From: Amaan Shaikh <91262696+AmaanRS@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:30:16 +0530 Subject: [PATCH] feat(number): add romanNumeral method (#3070) --- src/modules/number/index.ts | 91 +++++++++++++++++++ .../modules/__snapshots__/number.spec.ts.snap | 42 +++++++++ test/modules/number.spec.ts | 74 ++++++++++++++- 3 files changed, 206 insertions(+), 1 deletion(-) diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index be5e213f272..f5f5c27fab6 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -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; + } } diff --git a/test/modules/__snapshots__/number.spec.ts.snap b/test/modules/__snapshots__/number.spec.ts.snap index 8c3f5cccf2e..516abc9ffa8 100644 --- a/test/modules/__snapshots__/number.spec.ts.snap +++ b/test/modules/__snapshots__/number.spec.ts.snap @@ -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`; @@ -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`; @@ -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"`; diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index f9adf9ff07d..cfd50b205c0 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -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'; @@ -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()}`, () => { @@ -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', () => {