exercism

Exercism - Roman Numerals

This post shows you how to get Roman Numerals exercise of Exercism.

Stevinator Stevinator
10 min read
exercism dart flutter roman-numerals

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

Roman Numerals Exercise

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:

LetterValue
M1000
D500
C100
L50
X10
V5
I1

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:

  1. Create conversion table: List all Roman numeral values in descending order, including subtraction cases (900, 400, 90, 40, 9, 4)
  2. Start with largest value: Begin with the largest Roman numeral value
  3. 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
  4. Move to next value: When the remaining number is less than the current value, move to the next smaller value
  5. 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:

  1. extension RomanNumerals on int - Extension on int type:

    • Adds a toRoman() method to all integers
    • Allows calling 5.toRoman() or number.toRoman()
  2. String toRoman() - Main conversion method:

    • Returns a string representing the Roman numeral
    • Uses this to access the integer value
  3. 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
  4. var result = '' - Initialize result string:

    • Will accumulate Roman numeral characters
    • Starts empty
  5. var remaining = this - Track remaining value:

    • Starts with the original integer value
    • Will be decreased as we convert
  6. for (var (value, numeral) in conversions) - Iterate through conversions:

    • Destructures each record into value and numeral
    • Processes from largest to smallest value
    • Example: (1000, ‘M’), then (900, ‘CM’), etc.
  7. 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
  8. result += numeral - Add Roman numeral:

    • Appends the Roman numeral character(s) to result
    • Example: ‘M’, ‘CM’, ‘XC’, etc.
  9. remaining -= value - Subtract value:

    • Decreases remaining by the current value
    • Example: remaining = 1996 - 1000 = 996
  10. 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
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.