Exercism - Roman Numerals
This post shows you how to get Roman Numerals exercise of Exercism.
Stevinator Preparation
Before we click on our next exercise, let’s see what concepts of DART we need to consider

So we need to use the following concepts.
Extension Methods
Extension methods allow you to add new functionality to existing types without modifying their source code. You can add methods to built-in types like int using extensions.
extension RomanNumerals on int {
String toRoman() {
// Convert integer to Roman numeral
return 'I'; // Placeholder
}
}
void main() {
int number = 5;
// Use extension method
String roman = number.toRoman();
print(roman); // 'I'
// Works on any int
int another = 10;
print(another.toRoman()); // 'I'
}
Records (Tuples)
Records allow you to group multiple values together. They’re useful for representing pairs of values like (value, numeral).
void main() {
// Record syntax: (value, numeral)
var conversion = (1000, 'M');
print(conversion); // (1000, M)
// Destructure records
var (value, numeral) = conversion;
print(value); // 1000
print(numeral); // M
// List of records
List<(int, String)> conversions = [
(1000, 'M'),
(500, 'D'),
(100, 'C'),
];
// Iterate with destructuring
for (var (val, num) in conversions) {
print('$val = $num');
}
}
Const Lists
Const lists are compile-time constants that cannot be modified. They’re perfect for lookup tables that never change.
void main() {
// Const list - cannot be modified
const conversions = [
(1000, 'M'),
(500, 'D'),
(100, 'C'),
];
// Access elements
print(conversions[0]); // (1000, M)
// Iterate
for (var conversion in conversions) {
print(conversion);
}
// Cannot modify
// conversions.add((50, 'L')); // Error: Cannot add to const list
}
For-In Loops with Destructuring
For-in loops can destructure records directly in the loop variable, making it easy to iterate through pairs of values.
void main() {
List<(int, String)> conversions = [
(1000, 'M'),
(500, 'D'),
(100, 'C'),
];
// Destructure in loop
for (var (value, numeral) in conversions) {
print('$value -> $numeral');
// 1000 -> M
// 500 -> D
// 100 -> C
}
}
While Loops
While loops repeatedly execute a block of code as long as a condition is true. They’re perfect for repeatedly subtracting values until a condition is met.
void main() {
int remaining = 15;
int value = 5;
String result = '';
// Repeat while condition is true
while (remaining >= value) {
result += 'V'; // Add numeral
remaining -= value; // Subtract value
}
print(result); // "VVV" (15 = 5 + 5 + 5)
print(remaining); // 0
}
String Concatenation
Strings can be built by concatenating characters or substrings. You can use the += operator to append to a string.
void main() {
String result = '';
// Build string by concatenation
result += 'M';
result += 'C';
result += 'M';
print(result); // "MCM"
// Use in loops
String roman = '';
for (int i = 0; i < 3; i++) {
roman += 'I';
}
print(roman); // "III"
}
Integer Subtraction
Integer subtraction is used to decrease the remaining value as we convert it to Roman numerals.
void main() {
int remaining = 15;
int value = 5;
// Subtract value
remaining -= value;
print(remaining); // 10
// Use in loop
while (remaining >= value) {
remaining -= value; // Decrease remaining
print(remaining); // 10, 5, 0
}
}
Comparison Operators
Comparison operators (>=) compare values and return boolean results. They’re used to check if we can still use a particular Roman numeral value.
void main() {
int remaining = 15;
int value = 5;
// Check if remaining is greater than or equal to value
bool canUse = remaining >= value;
print(canUse); // true
// Use in loop condition
while (remaining >= value) {
// Process...
remaining -= value;
}
}
The this Keyword
In extension methods, this refers to the instance the method is called on. For int extensions, this is the integer value.
extension RomanNumerals on int {
String toRoman() {
// 'this' refers to the integer value
var remaining = this;
print(remaining); // The number the method was called on
// Example: 5.toRoman()
// this = 5
return 'V';
}
}
void main() {
int number = 5;
String roman = number.toRoman(); // this = 5 inside toRoman()
print(roman); // 'V'
}
Introduction
Today, most people in the world use Arabic numerals (0–9). But if you travelled back two thousand years, you’d find that most Europeans were using Roman numerals instead.
To write a Roman numeral we use the following Latin letters, each of which has a value:
| Letter | Value |
|---|---|
| M | 1000 |
| D | 500 |
| C | 100 |
| L | 50 |
| X | 10 |
| V | 5 |
| I | 1 |
A Roman numeral is a sequence of these letters, and its value is the sum of the letters’ values. For example, XVIII has the value 18 (10 + 5 + 1 + 1 + 1 = 18).
There’s one rule that makes things trickier though, and that’s that the same letter cannot be used more than three times in succession. That means that we can’t express numbers such as 4 with the seemingly natural IIII. Instead, for those numbers, we use a subtraction method between two letters. So we think of 4 not as 1 + 1 + 1 + 1 but instead as 5 - 1. And slightly confusingly to our modern thinking, we write the smaller number first. This applies only in the following cases: 4 (IV), 9 (IX), 40 (XL), 90 (XC), 400 (CD) and 900 (CM).
Order matters in Roman numerals! Letters (and the special compounds above) must be ordered by decreasing value from left to right.
Examples
105 => CV
100 => C
+ 5 => V
106 => CVI
100 => C
+ 5 => V
+ 1 => I
104 => CIV
100 => C
+ 4 => IV
1996 => MCMXCVI
1000 => M
+ 900 => CM
+ 90 => XC
+ 5 => V
+ 1 => I
Instructions
Your task is to convert a number from Arabic numerals to Roman numerals.
For this exercise, we are only concerned about traditional Roman numerals, in which the largest number is MMMCMXCIX (or 3,999).
Note
There are lots of different ways to convert between Arabic and Roman numerals. We recommend taking a naive approach first to familiarise yourself with the concept of Roman numerals and then search for more efficient methods.
Dart Extension Methods
We’re implementing this exercise with an extension method on the int class. For details, see the Dart docs.
How can we convert to Roman numerals?
To convert a number to Roman numerals:
- Create conversion table: List all Roman numeral values in descending order, including subtraction cases (900, 400, 90, 40, 9, 4)
- Start with largest value: Begin with the largest Roman numeral value
- Subtract repeatedly: While the remaining number is greater than or equal to the current value:
- Add the corresponding Roman numeral to the result
- Subtract the value from the remaining number
- Move to next value: When the remaining number is less than the current value, move to the next smaller value
- Continue until zero: Repeat until the remaining number is zero
The key insight is using a greedy algorithm: always use the largest possible Roman numeral value first, which naturally handles the subtraction cases (IV, IX, XL, XC, CD, CM) when they’re in the conversion table.
For example, with number 1996:
- 1996 >= 1000 → Add ‘M’, remaining = 996
- 996 >= 900 → Add ‘CM’, remaining = 96
- 96 >= 90 → Add ‘XC’, remaining = 6
- 6 >= 5 → Add ‘V’, remaining = 1
- 1 >= 1 → Add ‘I’, remaining = 0
- Result: MCMXCVI
Solution
extension RomanNumerals on int {
String toRoman() {
const conversions = [
(1000, 'M'),
(900, 'CM'),
(500, 'D'),
(400, 'CD'),
(100, 'C'),
(90, 'XC'),
(50, 'L'),
(40, 'XL'),
(10, 'X'),
(9, 'IX'),
(5, 'V'),
(4, 'IV'),
(1, 'I'),
];
var result = '';
var remaining = this;
for (var (value, numeral) in conversions) {
while (remaining >= value) {
result += numeral;
remaining -= value;
}
}
return result;
}
}
Let’s break down the solution:
-
extension RomanNumerals on int- Extension on int type:- Adds a
toRoman()method to all integers - Allows calling
5.toRoman()ornumber.toRoman()
- Adds a
-
String toRoman()- Main conversion method:- Returns a string representing the Roman numeral
- Uses
thisto access the integer value
-
const conversions- Conversion table:- List of records
(value, numeral)pairs - Ordered from largest to smallest value
- Includes subtraction cases: 900 (CM), 400 (CD), 90 (XC), 40 (XL), 9 (IX), 4 (IV)
- Const means it’s a compile-time constant
- List of records
-
var result = ''- Initialize result string:- Will accumulate Roman numeral characters
- Starts empty
-
var remaining = this- Track remaining value:- Starts with the original integer value
- Will be decreased as we convert
-
for (var (value, numeral) in conversions)- Iterate through conversions:- Destructures each record into
valueandnumeral - Processes from largest to smallest value
- Example: (1000, ‘M’), then (900, ‘CM’), etc.
- Destructures each record into
-
while (remaining >= value)- Repeat while value fits:- Checks if remaining number is greater than or equal to current value
- Continues until remaining is less than current value
-
result += numeral- Add Roman numeral:- Appends the Roman numeral character(s) to result
- Example: ‘M’, ‘CM’, ‘XC’, etc.
-
remaining -= value- Subtract value:- Decreases remaining by the current value
- Example: remaining = 1996 - 1000 = 996
-
return result- Return final Roman numeral:- Returns the complete Roman numeral string
- Example: “MCMXCVI” for 1996
The solution efficiently converts integers to Roman numerals using a greedy algorithm that always uses the largest possible value first, naturally handling subtraction cases through the ordered conversion table.
A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉
Visit My YouTube Channel