Converting integers to Roman numerals with 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.

Josh Sherman - The Man, The Myth, The Avatar

About Josh

Husband. Father. Pug dad. Musician. Founder of Holiday API, Head of Engineering and Emoji Specialist at Mailshake, and author of the best damn Lorem Ipsum Library for PHP.


If you found this article helpful, please consider buying me a coffee.