Back to blog

Play with Code: Converting Roman Numerals with Swift


Aasif Khan
By Aasif Khan | December 28, 2021 5:50 pm  | 4-min read

Roman numerals, such as MMXIX, are exciting! In this tutorial, we’re going to write some code that converts any integer number to a roman numeral. It’s a fun, short exercise that touches on many aspects of the Swift programming language. It’s perfect if you have a spare minute or two to play with code!

What Are Roman Numerals?

Before we start, let’s do a quick refresher on roman numerals. Roman numerals are a numeric system that originated in ancient Rome. With it, a number like 2019 is written as MMXIX, in letters of the Latin alphabet.

Roman numerals use 7 symbols, each with a fixed integer number:

SymbolIVXLCDM
Value1510501005001000

Roman numerals were used until the late Middle Ages (1500 CE), but in our modern world they still have their uses! You see roman numerals on clocks, paper currency, buildings, monuments, and even movie credits.

The roman numerals system is basically a decimal or “base 10” number system, written from left to right. Instead of each power of ten having its own decimal place, roman numerals “add up” symbols to a given number. It’s easiest to think of them as “tally marks.”

For example, the number 2019 is written as:

  • MM for 2 times a thousand
  • X for once a ten
  • IX for “one before 10”, i.e. 9

That last symbol, IX, is special. Instead of writing VIIII, for “5 + 4 = 9”, you write IX, for “10 – 1 = 9”. This is called “subtractive notation”. It’s quite useful, because it’ll save you from writing lots of symbols! IX is simply shorter than VIIII.

The same trick applies to other symbols too. For example, 40 is XL instead of XXXX, 90 is XC instead of LXXXX, 400 is CD, and 900 is CM. It doesn’t work for non-adjacent symbols, i.e. 999 is not IM but CMXCIX.

Setting Up The “roman(number:)” Function

We’re going to write a Swift function that can convert arbitrary integer numbers to roman numerals. Let’s get started!

First, let’s think about the input and output for this function. The function needs an Int value as input, and outputs a value of type String. So, our function declaration will be this:

func roman(number: Int) -> String
{

}

The roman(number:) function takes a value of type Int for the number parameter and outputs a value of type String. Perfect!

Next, inside the function, we’re going to define the roman numeral system, like this:

let decimals = [1000, 500, 100, 50, 10, 5, 1]
let numerals = [“M”, “D”, “C”, “L”, “X”, “V”, “I”]

The above code defines exactly the same as the table in the previous section. Every decimal value has a roman numeral, such as C = 100. Both arrays have exactly the same amount of items, so we can use identical index numbers to look up numerals by decimals.

Two things stand out here:

  1. We’ve sorted the arrays from largest-to-smallest. This is crucial for our implementation, as you’ll see later on.
  2. We haven’t yet added the IX, CM, etc. “subtractive notation” symbols, which we’ll do later.

Next, we’re coding this inside the function:

var result = “”
var number = number

The above code first defines a variable result of type String. We’re going to use this variable to build up the resulting roman numeral, i.e. the output string.

The local variable number is declared with the function parameter number (which is a bit ugly). Parameters are constants, so they can’t be changed. By “redeclaring” number as a variable, we’re now allowed to change number. We couldn’t have used the inout keyword here, because that would have produced the unintended side-effect of changing the function’s argument, as you’ll see later on.

Why don’t we use a dictionary for decimals and numerals? Well, dictionary items don’t have a sort order! The algorithm we’re about to write relies on a larger-to-smaller sort order, which won’t work with dictionaries. An alternative would be using tuples, or sorting the dictionary and decomposing its keys and values – but what we have now is much simpler.

Converting Numbers To Roman Numerals

Alright, we’re getting to the core of the algorithm. Before we start, it’s worthwhile to consider how we would convert a number to roman numerals with pen and paper. This is always a great idea if you need to design an algorithm.

Here’s how we would convert 1776:

  1. See that we need one M, for a thousand – which leaves 776
  2. See that we need DCC, for “500 + 100 + 100 = 700”, which leaves 76
  3. See that we need LXX, for “50 + 10 + 10 = 70”, which leaves 6
  4. Finally, we need VI for “5 + 1 = 6”, resulting in MDCCLXXVI

We could say that we’re subtracting numbers from 1776 until we reach zero. Each number we subtract results in a different symbol added to the final result. Would it be possible to put that workflow into an algorithm?

First, let’s start with a while loop. Like this:

while number > 0
{

}

This loop will keep iterating for as long as number is greater than 0. Differently said, it’ll stop if number is zero. We don’t know how many iterations we need, so we’re using while instead of a for loop.

Next, we want to loop over every item in the decimals array. We’re going to attempt to subtract this decimal number from number. That’s why we sorted the decimals array from largest-to-smallest, so we can see if a decimal would “fit into” the input number.

So, we write this inside the while loop:

for (index, decimal) in decimals.enumerated()
{
if number – decimal >= 0 {

}
}

The for loop is used to iterate over the decimals array. We’re using the enumerated() function so we can access both the array index and the array value, with the index and decimal constants in the tuple.

Inside the for loop, we’re checking if number minus decimal is greater or equal to zero, with an if block. This is exactly what we did in our pen-and-paper workflow, earlier.

  • Take for example the 1000 = M for number = 1776. Is 1776 minus 1000 greater than zero? Yes it is! We now know that 1776 can accommodate one M for its thousand.
  • Another example: 500 = D for number = 99. Is 99 minus 500 greater than zero? No it’s not! We now know that the number 99 doesn’t include a D.

Let’s finish the code by writing the following inside the if block:

number -= decimal
result += numerals[index]
break

Here’s what happens on those 3 lines:

  1. decimal is subtracted from number. When we’ve found that a decimal “fits in” the input number, we’ve found a roman numeral, so that decimal can be subtracted from the input number.
  2. numerals[index] is appended to result, so the found roman numeral in the iteration is written down by using index. Remember that index corresponds to the index number of both the decimal and the roman numeral!
  3. break exits this iteration of the for loop, because we’ve found a roman literal, and thus the while loop continues with its next iteration

Finally, we return the resulting value in the function with:

return result
Here’s the complete code once more:

func roman(number: Int) -> String
{
let decimals = [1000, 500, 100, 50, 10, 5, 1]
let numerals = [“M”, “D”, “C”, “L”, “X”, “V”, “I”]

var result = “”
var number = number

while number > 0
{
for (index, decimal) in decimals.enumerated()
{
if number – decimal >= 0 {
number -= decimal
result += numerals[index]
break
}
}
}

return result
}

Let’s go over the core principles of the algorithm.

  • The while loop ensures that we’re iterating for as long as number is greater than 0. If number equals 0, the algorithm stops. The code that subtracts something from number, is inside the for loop: number -= decimal. When we’ve found a roman numeral, we’re subtracting its value, to move towards zero.
  • The for loop, and its inner if block, is what checks every decimal against the current number value. It’s as if it’s trying to “fit” a roman numeral inside the number. Because, if it fits, we’ve got a match!
  • When the algorithm finds a roman numeral, for example M for a 1000 in 1776, it’ll subtract that decimal from number and append the numeral to result. The loop also needs to be broken with break, because the loop needs to start over from the beginning.

You can see the algorithm do its work more clearly, by adding the following line inside the if block:

print(“Found \(numerals[index]) for \(decimal)”)

You can run the function with the following code:

print(roman(number: 2019))
print(roman(number: 1776))
print(roman(number: 1999))

For the year 2019, the output is:

Found M for 1000
Found M for 1000
Found X for 10
Found V for 5
Found I for 1
Found I for 1
Found I for 1
Found I for 1

But… what’s going on there!? Wasn’t 2019 supposed to be MMXIX?

Taking Numerals Like “IX” Into Account

We haven’t yet incorporated subtractive notation in our algorithm. We discussed before that some symbols, such as for 9, are written as IX or “one before 10”, instead of VIIII.

The algorithm we’ve coded so far works by attempting to subtract decimals from the input value, substituting them for roman numerals. We could adjust the algorithm to incorporate numerals like IX with some complicated code, but as it turns out, supporting subtractive notation is incredibly simple.

Replace the declaration at the top of the function with:

let decimals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
let numerals = [“M”, “CM”, “D”, “CD”, “C”, “XC”, “L”, “XL”, “X”, “IX”, “V”, “IV”, “I”]

Instead of only using the basic values M, D, C, L, X, V, and I, we’ve simply added the other symbols, like CM, to the array as well. For our algorithm, it doesn’t matter much if you subtract 900, or 500 and 4 times 100! As long as the decimals and numerals are in descending order, the subtraction mechanism works perfectly fine.

And that’s everything there’s to it! Here, try out the code for yourself with this Swift sandbox:

func roman(number: Int) -> String
{
let decimals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
let numerals = [“M”, “CM”, “D”, “CD”, “C”, “XC”, “L”, “XL”, “X”, “IX”, “V”, “IV”, “I”]

var result = “”
var number = number

while number > 0
{
for (index, decimal) in decimals.enumerated()
{
if number – decimal >= 0 {
number -= decimal
result += numerals[index]
break
}
}
}

return result
}

print(roman(number: 2019))
print(roman(number: 1776))
print(roman(number: 1999))

Further Reading

Awesome! We’ve converted any arbitrary decimal value to roman numerals. It’s a fun, short programming exercise that touches on many interesting parts of Swift syntax.

But… what about converting numerals back to integer values? We’ll leave that for another day! If you’re up for a challenge, however, try to find the longest roman numeral between 1 and 3000.


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts