A non-recursive no-loop solution with nested curried functions for fun:
const convert =
(base, sym, next) =>
(num, res = '') =>
next && num
? next(num % base, res + sym.repeat(num / base))
: res + sym.repeat(num);
const roman = convert(1000, 'M',
convert( 900, 'CM',
convert( 500, 'D',
convert( 400, 'CD',
convert( 100, 'C',
convert( 90, 'XC',
convert( 50, 'L',
convert( 40, 'XL',
convert( 10, 'X',
convert( 9, 'IX',
convert( 5, 'V',
convert( 4, 'IV',
convert( 1, 'I')))))))))))));
roman(1999); //> 'MCMXCIX'
How does it work?
We define a convert
function that takes a base number (base
), its Roman numeral (sym
) and an optional next
function that we use for the next conversion. It then returns a function that takes a number (num
) to convert and an optional string (res
) used to accumulate previous conversions.
Example:
const roman =
convert(1000, 'M', (num, res) => console.log(`num=${num}, res=${res}`));
// ^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// base sym next
roman(3999);
// LOG: num=999, res=MMM
Note that roman
is the function returned by convert
: it takes a number (num
) and an optional string res
. It is the same signature as the next
function…
This means we can use the function returned by convert
as a next
function!
const roman =
convert(1000, 'M',
convert(900, 'CM', (num, res) => console.log(`num=${num}, res=${res}`)));
roman(3999);
// LOG: num=99, res=MMMCM
So we can keep nesting convert
functions to cover the entire Roman numerals conversion table:
const roman =
convert(1000, 'M',
convert( 900, 'CM',
convert( 500, 'D',
convert( 400, 'CD',
convert( 100, 'C',
convert( 90, 'XC',
convert( 50, 'L',
convert( 40, 'XL',
convert( 10, 'X',
convert( 9, 'IX',
convert( 5, 'V',
convert( 4, 'IV',
convert( 1, 'I')))))))))))));
When next
is not defined it means that we reached the end of the conversion table: convert(1, 'I')
which is as simple as repeating 'I'
n times e.g. 3
-> 'I'.repeat(3)
-> 'III'
.
The num
check is an early exit condition for when there is nothing left to convert e.g. 3000
-> MMM
.
NaN
or throw instead of returningfalse
as discussed in that post. – Villus