UNPKG

romans

Version:

A small, no-dependency lib for converting to and from roman numerals

393 lines (347 loc) 15.9 kB
/* eslint-disable */ const romans = require('../romans') const { deromanize, romanize } = require('../romans') describe('Module Structure', () => { it('should export an object with all required methods and properties', () => { expect(typeof romans).toBe('object') expect(romans).toHaveProperty('romanize') expect(romans).toHaveProperty('deromanize') expect(romans).toHaveProperty('allChars') expect(romans).toHaveProperty('allNumerals') }) it('should export romanize as a function', () => { expect(typeof romanize).toBe('function') }) it('should export deromanize as a function', () => { expect(typeof deromanize).toBe('function') }) it('should export allChars as an array of strings', () => { expect(Array.isArray(romans.allChars)).toBe(true) expect(romans.allChars.every(char => typeof char === 'string')).toBe(true) }) it('should export allNumerals as an array of numbers', () => { expect(Array.isArray(romans.allNumerals)).toBe(true) expect(romans.allNumerals.every(num => typeof num === 'number')).toBe(true) }) }) describe('romanize() - Basic Functionality', () => { describe('Valid inputs', () => { const testCases = [ { input: 1, expected: 'I' }, { input: 2, expected: 'II' }, { input: 3, expected: 'III' }, { input: 4, expected: 'IV' }, { input: 5, expected: 'V' }, { input: 6, expected: 'VI' }, { input: 7, expected: 'VII' }, { input: 8, expected: 'VIII' }, { input: 9, expected: 'IX' }, { input: 10, expected: 'X' }, { input: 40, expected: 'XL' }, { input: 50, expected: 'L' }, { input: 90, expected: 'XC' }, { input: 100, expected: 'C' }, { input: 153, expected: 'CLIII' }, { input: 400, expected: 'CD' }, { input: 500, expected: 'D' }, { input: 900, expected: 'CM' }, { input: 1000, expected: 'M' }, { input: 1994, expected: 'MCMXCIV' }, { input: 2023, expected: 'MMXXIII' }, { input: 3999, expected: 'MMMCMXCIX' } ] testCases.forEach(({ input, expected }) => { it(`should convert ${input} to ${expected}`, () => { expect(romanize(input)).toBe(expected) }) }) }) describe('Edge cases with leading zeros', () => { it('should handle numbers with leading zeros as strings internally', () => { // The function converts to string internally and strips leading zeros expect(romanize(1)).toBe('I') expect(romanize(8)).toBe('VIII') expect(romanize(64)).toBe('LXIV') }) }) }) describe('romanize() - Error Handling', () => { describe('Invalid numeric inputs', () => { it('should throw error for zero', () => { expect(() => romanize(0)).toThrow('requires an unsigned integer') }) it('should throw error for negative integers', () => { expect(() => romanize(-1)).toThrow('requires an unsigned integer') expect(() => romanize(-100)).toThrow('requires an unsigned integer') expect(() => romanize(-999)).toThrow('requires an unsigned integer') }) it('should throw error for numbers >= 4000', () => { expect(() => romanize(4000)).toThrow('requires max value of less than 4000') expect(() => romanize(5000)).toThrow('requires max value of less than 4000') expect(() => romanize(10000)).toThrow('requires max value of less than 4000') }) it('should throw error for floating point numbers', () => { expect(() => romanize(1.5)).toThrow('requires an unsigned integer') expect(() => romanize(99.99)).toThrow('requires an unsigned integer') expect(() => romanize(567.789)).toThrow('requires an unsigned integer') }) it('should throw error for special numeric values', () => { expect(() => romanize(NaN)).toThrow('requires an unsigned integer') expect(() => romanize(Infinity)).toThrow('requires max value of less than 4000') expect(() => romanize(-Infinity)).toThrow('requires an unsigned integer') }) }) describe('Invalid non-numeric inputs', () => { it('should throw error for string inputs', () => { expect(() => romanize('1000')).toThrow('requires an unsigned integer') expect(() => romanize('abc')).toThrow('requires an unsigned integer') expect(() => romanize('')).toThrow('requires an unsigned integer') }) it('should throw error for null and undefined', () => { expect(() => romanize(null)).toThrow('requires an unsigned integer') expect(() => romanize(undefined)).toThrow('requires an unsigned integer') }) it('should throw error for boolean values', () => { expect(() => romanize(true)).toThrow('requires an unsigned integer') expect(() => romanize(false)).toThrow('requires an unsigned integer') }) it('should throw error for objects and arrays', () => { expect(() => romanize({})).toThrow('requires an unsigned integer') expect(() => romanize({ value: 1000 })).toThrow('requires an unsigned integer') expect(() => romanize([1000])).toThrow('requires an unsigned integer') expect(() => romanize([])).toThrow('requires an unsigned integer') }) }) }) describe('deromanize() - Basic Functionality', () => { describe('Valid inputs', () => { const testCases = [ { input: 'I', expected: 1 }, { input: 'II', expected: 2 }, { input: 'III', expected: 3 }, { input: 'IV', expected: 4 }, { input: 'V', expected: 5 }, { input: 'VI', expected: 6 }, { input: 'VII', expected: 7 }, { input: 'VIII', expected: 8 }, { input: 'IX', expected: 9 }, { input: 'X', expected: 10 }, { input: 'XL', expected: 40 }, { input: 'L', expected: 50 }, { input: 'XC', expected: 90 }, { input: 'C', expected: 100 }, { input: 'CLIII', expected: 153 }, { input: 'CD', expected: 400 }, { input: 'D', expected: 500 }, { input: 'CM', expected: 900 }, { input: 'M', expected: 1000 }, { input: 'MCMXCIV', expected: 1994 }, { input: 'MMXXIII', expected: 2023 }, { input: 'MMMCMXCIX', expected: 3999 } ] testCases.forEach(({ input, expected }) => { it(`should convert ${input} to ${expected}`, () => { expect(deromanize(input)).toBe(expected) }) }) }) describe('Complex valid combinations', () => { it('should handle all subtractive notation correctly', () => { expect(deromanize('CDXLIV')).toBe(444) // CD + XL + IV expect(deromanize('CMXCIX')).toBe(999) // CM + XC + IX expect(deromanize('MCMXCIX')).toBe(1999) // M + CM + XC + IX }) }) }) describe('deromanize() - Error Handling', () => { describe('Invalid characters', () => { it('should throw error for strings containing invalid characters', () => { expect(() => deromanize('ABC')).toThrow('requires valid roman numeral string') expect(() => deromanize('CIVIL')).toThrow('requires valid roman numeral string') expect(() => deromanize('MXQ')).toThrow('requires valid roman numeral string') expect(() => deromanize('123')).toThrow('requires valid roman numeral string') }) it('should throw error for special characters and symbols', () => { expect(() => deromanize('M@C')).toThrow('requires valid roman numeral string') expect(() => deromanize('X!I')).toThrow('requires valid roman numeral string') expect(() => deromanize('C#D')).toThrow('requires valid roman numeral string') }) it('should throw error for Unicode lookalikes', () => { expect(() => deromanize('ⅠⅤ')).toThrow('requires valid roman numeral string') expect(() => deromanize('МСМ')).toThrow('requires valid roman numeral string') // Cyrillic expect(() => deromanize('IV')).toThrow('requires valid roman numeral string') // Full-width }) it('should throw error for emojis mixed with valid characters', () => { expect(() => deromanize('M🏛️I')).toThrow('requires valid roman numeral string') expect(() => deromanize('X😀I')).toThrow('requires valid roman numeral string') }) }) describe('Case sensitivity', () => { it('should throw error for lowercase roman numerals', () => { expect(() => deromanize('mcmxciv')).toThrow('requires valid roman numeral string') expect(() => deromanize('iv')).toThrow('requires valid roman numeral string') expect(() => deromanize('xi')).toThrow('requires valid roman numeral string') }) it('should throw error for mixed case roman numerals', () => { expect(() => deromanize('mXvIi')).toThrow('requires valid roman numeral string') expect(() => deromanize('McmXCiv')).toThrow('requires valid roman numeral string') expect(() => deromanize('iV')).toThrow('requires valid roman numeral string') }) }) describe('Invalid sequences', () => { it('should throw error for more than 3 consecutive identical characters', () => { expect(() => deromanize('IIII')).toThrow('requires valid roman numeral string') expect(() => deromanize('XXXX')).toThrow('requires valid roman numeral string') expect(() => deromanize('CCCC')).toThrow('requires valid roman numeral string') expect(() => deromanize('MMMM')).toThrow('requires valid roman numeral string') }) it('should throw error for repeated characters that cannot repeat', () => { expect(() => deromanize('VV')).toThrow('requires valid roman numeral string') expect(() => deromanize('LL')).toThrow('requires valid roman numeral string') expect(() => deromanize('DD')).toThrow('requires valid roman numeral string') }) it('should throw error for invalid subtractive combinations', () => { expect(() => deromanize('IC')).toThrow('requires valid roman numeral string') expect(() => deromanize('IL')).toThrow('requires valid roman numeral string') expect(() => deromanize('XD')).toThrow('requires valid roman numeral string') expect(() => deromanize('XM')).toThrow('requires valid roman numeral string') expect(() => deromanize('VX')).toThrow('requires valid roman numeral string') expect(() => deromanize('VC')).toThrow('requires valid roman numeral string') }) it('should throw error for consecutive subtractive patterns', () => { expect(() => deromanize('IVIV')).toThrow('requires valid roman numeral string') expect(() => deromanize('IXIX')).toThrow('requires valid roman numeral string') expect(() => deromanize('XLXL')).toThrow('requires valid roman numeral string') expect(() => deromanize('XCXC')).toThrow('requires valid roman numeral string') expect(() => deromanize('CDCD')).toThrow('requires valid roman numeral string') expect(() => deromanize('CMCM')).toThrow('requires valid roman numeral string') }) }) describe('Whitespace handling', () => { it('should throw error for strings with leading whitespace', () => { expect(() => deromanize(' IV')).toThrow('requires valid roman numeral string') expect(() => deromanize('\tMC')).toThrow('requires valid roman numeral string') expect(() => deromanize('\nXI')).toThrow('requires valid roman numeral string') }) it('should throw error for strings with trailing whitespace', () => { expect(() => deromanize('IV ')).toThrow('requires valid roman numeral string') expect(() => deromanize('MC\t')).toThrow('requires valid roman numeral string') expect(() => deromanize('XI\n')).toThrow('requires valid roman numeral string') }) it('should throw error for strings with internal whitespace', () => { expect(() => deromanize('X I')).toThrow('requires valid roman numeral string') expect(() => deromanize('M C')).toThrow('requires valid roman numeral string') expect(() => deromanize('C\tD')).toThrow('requires valid roman numeral string') }) }) describe('Invalid input types', () => { it('should throw error for non-string inputs', () => { expect(() => deromanize(1000)).toThrow('requires valid roman numeral string') expect(() => deromanize(true)).toThrow('requires valid roman numeral string') expect(() => deromanize(false)).toThrow('requires valid roman numeral string') expect(() => deromanize(null)).toThrow('requires valid roman numeral string') expect(() => deromanize(undefined)).toThrow('requires valid roman numeral string') }) it('should throw error for objects and arrays', () => { expect(() => deromanize({})).toThrow('requires valid roman numeral string') expect(() => deromanize([])).toThrow('requires valid roman numeral string') expect(() => deromanize({ value: 'III' })).toThrow('requires valid roman numeral string') expect(() => deromanize(['M', 'C', 'M'])).toThrow('requires valid roman numeral string') }) it('should throw error for objects with toUpperCase method', () => { const fakeString = { toUpperCase: function() { return 'III' } } expect(() => deromanize(fakeString)).toThrow('requires valid roman numeral string') }) }) }) describe('Round-trip conversions', () => { it('should maintain value integrity for all valid numbers', () => { for (let i = 1; i < 4000; i++) { const roman = romanize(i) const backToDecimal = deromanize(roman) expect(backToDecimal).toBe(i) } }) it('should not mutate input values', () => { const originalNum = 1994 const originalRoman = 'MCMXCIV' const romanResult = romanize(originalNum) expect(originalNum).toBe(1994) const decimalResult = deromanize(originalRoman) expect(originalRoman).toBe('MCMXCIV') }) }) describe('Property-based tests', () => { const fc = require('fast-check') it('should always produce valid roman numerals for numbers 1-3999', () => { fc.assert( fc.property( fc.integer({ min: 1, max: 3999 }), (num) => { const roman = romanize(num) // Check that the result only contains valid characters return /^[MDCLXVI]+$/.test(roman) } ) ) }) it('should never produce more than 3 consecutive identical characters', () => { fc.assert( fc.property( fc.integer({ min: 1, max: 3999 }), (num) => { const roman = romanize(num) return !/([IVXLCDM])\1{3,}/.test(roman) } ) ) }) it('should maintain ordering for consecutive numbers', () => { fc.assert( fc.property( fc.integer({ min: 1, max: 3998 }), (num) => { const current = deromanize(romanize(num)) const next = deromanize(romanize(num + 1)) return next === current + 1 } ) ) }) it('should follow valid subtractive notation rules', () => { fc.assert( fc.property( fc.integer({ min: 1, max: 3999 }), (num) => { const roman = romanize(num) // This regex validates proper Roman numeral structure const validPattern = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/ return validPattern.test(roman) } ) ) }) it('should handle any valid roman numeral string correctly', () => { const validRomans = ['I', 'V', 'X', 'L', 'C', 'D', 'M'] fc.assert( fc.property( fc.array(fc.constantFrom(...validRomans), { minLength: 1, maxLength: 15 }), (charArray) => { const romanStr = charArray.join('') try { const result = deromanize(romanStr) // If it doesn't throw, the result should be a positive number return typeof result === 'number' && result > 0 && result < 4000 } catch (e) { // If it throws, that's also valid for invalid sequences return true } } ) ) }) })