Converting integers to Roman numerals with TypeScript

Josh Sherman
5 min read
Software Development JavaScript TypeScript

My buddy Robert has been seeking new opportunities in the software development realm, and recently ran into a coding challenge that I haven’t thought about in a good long time: converting an integer to Roman numeral.

Now, I actually hate these kinds of brain teasers in the context of interviewing and hiring, but I do love to mess around with them from time to time. This one was especially interesting to me because I have done it in the past and I remember it being a fairly shoddy implementation that fell apart with higher numbers.

That was then and this is now though. I had a rough idea in my head about how I would want to approach the problem. Take the integer and break it up into individual digits, then take those digits and convert them.

Previous attempts in the past (seriously, it’s been at least 10 or 15 years since I attempted this) were for lack of a better word, clever. Attempting to interrogate the numbers and attempt to “generate” a number. Take the number 8 for example, I remember at one point writing logic to determine that the number “contained” 5 (or V) and then take the remaining integer and converting that to III (3 * I).

Writing clever code is fun, but in my experience, rarely holds up. Fast forward quite a ways into my software engineering career, being clever is something I try to avoid unless it’s absolutely necessary. With this problem, I opted to hard code a mapping of values that could exist in each of the places in the number.

This resulted in an array of arrays broken out into 1-9, 10-90, 100-900 and 1,000-3,000.

I know what you’re thinking, “why only up to 3,000?” As it turns out, Roman numerals only go up to 3,999. This was as per what the original spec for the coding challenge said and I did take some liberties with looking it up on Wikipedia to confirm.

My suspicion with my previous attempts falling apart were due to attempting to convert values that were simply too large and should have been thrown own. Live and learn.

So yeah, I went ahead and limited the conversion method to 1 through 3,999 and went ahead and used TypeScript (which is my daily driver at this point) and omitted any sort of run-time sanity checks on the argument coming in, simply allowing TypeScript to do the type checking.

The only other liberty I took was in terms of fractions. Roman numerals do support another block of letters / symbols to represent the fractions, but for the sake of not over engineering, I didn’t venture down that path.

I did leave a note though. If this were in the context of me submitting a coding challenge, I would want the person reviewing my code to know that I did give that aspect of the conversion some thought, and consciously omitted it.

Here’s the code that I came up with:

const int2roman = (original: number): string => {
  if (original < 1 || original > 3999) {
    throw new Error('Error: Input integer limited to 1 through 3,999');

  const numerals = [
    ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'], // 1-9
    ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'], // 10-90
    ['C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM'], // 100-900
    ['M', 'MM', 'MMM'], // 1000-3000

  // TODO: Could expand to support fractions, simply rounding for now
  const digits = Math.round(original).toString().split('');
  let position = (digits.length - 1);

  return digits.reduce((roman, digit) => {
    if (digit !== '0') {
      roman += numerals[position][parseInt(digit) - 1];

    position -= 1;

    return roman;
  }, '');

How do I know it works? Glad you asked! I scoured the web and found a listing of Roman numeral values from 1 to 3,999 and I put together a quick test to run through every time value, to see if my code generated the expected value.

Everything married up perfectly, and without any additional tweaks. With age comes wisdom and all of that, and the lack of cleverness payed off.

Don’t get me wrong, I could have probably “golfed” the answer a bit to reduce it’s size, but it’s pretty easy to read. Similar to being too clever, I’ll take longer code that’s easier to read over cryptic one line solutions any day of the week.

Join the Conversation

Good stuff? Want more?

Weekly emails about technology, development, and sometimes sauerkraut.

100% Fresh, Grade A Content, Never Spam.

Related Articles