Thursday, August 16, 2018

Coding for fun: Roman Numeral Converter with unit tests

I was listening to an audiobook this morning about the evolution of communication technology. It's The Information by James Gleick. Good book. I should probably write a longer review later. For now though, I happened to hear a section where Claude Shannon amused himself by writing an early program that translates Roman numerals. I figured if it's good enough for Shannon, it's good enough for me, although I have better tools that make it easier. So anyway, just for fun:

Conversion class:

package com.russellglasser.romannumeral;
import java.util.HashMap;
import java.util.Map;

class RomanNumeralConverter {

   private final String[] romanDigits;
   private final Long[] arabicDigits;
   private final Map toArabicTable;
   private final Map toRomanTable;

   RomanNumeralConverter() {
      romanDigits = new String[]{"M", "CM",
            "D", "CD", "C", "XC",
            "L", "XL", "X", "IX",
            "V", "IV", "I"};
      arabicDigits = new Long[]{1000L, 900L,
            500L, 400L, 100L, 90L,
            50L, 40L, 10L, 9L,
            5L, 4L, 1L};
      toArabicTable = new HashMap<>();
      toRomanTable = new HashMap<>();
      for (int i = 0; i < romanDigits.length; i++) {
         toArabicTable.put(romanDigits[i], arabicDigits[i]);
         toRomanTable.put(arabicDigits[i], romanDigits[i]);
      }
   }

   String toRoman(Long arabic) {
      StringBuilder builder = new StringBuilder();
      Long remaining = arabic;
      for (Long digit : arabicDigits) {
         while (remaining >= digit) {
            builder.append(toRomanTable.get(digit));
            remaining -= digit;
         }
      }
      return builder.toString();
   }

   Long toArabic(String roman) {
      String remaining = roman;
      Long result = 0L;
      for (String digit : romanDigits) {
         while (remaining.startsWith(digit)) {
            result += toArabicTable.get(digit);
            remaining = remaining.replaceFirst(digit, "");
         }
      }
      return result;
   }
}
Unit test class:

package com.russellglasser.romannumeral;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class RomanNumeralConverterTest {

   private final RomanNumeralConverter converter = new RomanNumeralConverter();

   @Test   void toRoman1() {
      check(1L, "I");
   }

   @Test   void toRoman3() {
      check(3L, "III");
   }

   @Test   void toRoman4() {
      check(4L, "IV");
   }

   @Test   void toRoman8() {
      check(8L, "VIII");
   }

   @Test   void toRoman14() {
      check(14L, "XIV");
   }

   @Test   void toRoman18() {
      check(18L, "XVIII");
   }

   @Test   void toRoman1979() {
      check(1979L, "MCMLXXIX");
   }

   @Test   void toRoman2018() {
      check(2018L, "MMXVIII");
   }

   @Test   void toRoman9999() {
      check(9999L, "MMMMMMMMMCMXCIX");
   }

   private void check(Long arabic, String roman) {
      assertEquals(roman, converter.toRoman(arabic));
      assertEquals(arabic, converter.toArabic(roman));
   }
}

2 comments:

  1. That is a nice solution where the subtractive notation is treated as a single token.
    I wonder how the performance would differ using StringBuilder methods instead of String.sartsWith() and String.replaceFirst().

    ReplyDelete
  2. Things I should modify:
    1. Should use Integers instead of Longs, because let's face it, there is no conceivable situation where you want to convert more than a 32 bit number to a Roman numeral.
    2. toRoman should throw an exception if it is passed a number <= 0
    3. toArabic should throw an exception if it can't parse any more digits but the remaining string is not empty

    ReplyDelete