Wednesday, May 9th, 2018

Roman Numerals, Groovy Style

Your project is already late when you discover in the small print of the contract that the system in development "SHALL manage Roman digits in chapter numbers". You frantically search the 'Net and, yes there are solutions. Some of them seem complicated on first sight. Do you wish for less convoluted code? You've got it.

Here is an attempt at a Groovy solution, i.e. compact but not obscure code. The techniques are, of course, borrowed from other people on the 'Net. The parse method relies heavily on the Groovy switch statement. The print method uses floorKey, a method that GDK adds to the Java API. Both are recursive.

The main contribution may be that the parsing method does a fairly complete validation of its input.

GROOVY:

  1. package se.soderstrom.roman
  2.  
  3. import java.util.regex.Matcher
  4.  
  5. /**
  6. * Class for processing Roman numerals.
  7. */
  8. class Roman {
  9.  /**
  10.   * Convert a Roman numeral to an Integer value with decent validation.
  11.   * @param numeral must be a string of Roman digits,
  12.   * @throws NumberFormatException if the numeral is invalid.
  13.   */
  14. static Integer parse(String numeral) {
  15.   if (!numeral) return 0
  16.   String roman = numeral.toUpperCase()
  17.  
  18.   switch (roman) {
  19.   // Cases not detected by other regex.
  20.   case ~/XXXX[IVXLCDM]*/:
  21.   case ~/CCCC[IVXLCDM]*/:
  22.   case ~/MMMM[IVXLCDM]*/:
  23.     throw new NumberFormatException("Invalid Roman numeral: ${numeral}")
  24.  
  25.   case ~/M([IVXLCDM]*)/: grab(1000)
  26.      break
  27.   case ~/CM([IVXLC]*)/: grab(900)
  28.     break
  29.   case ~/D([IVXLC]*)/: grab(500)
  30.     break
  31.   case ~/CD([IVXLC]*)/: grab(400)
  32.     break
  33.   case ~/CCC([IVXL]*)/: grab(300)
  34.     break
  35.   case ~/C([IVXLC]*)/: grab(100)
  36.     break
  37.   case ~/XC([IVXL]*)/: grab(90)
  38.     break
  39.   case ~/L([IVX]*)/: grab(50)
  40.     break
  41.   case ~/XL([IVX]*)/: grab(40)
  42.     break
  43.   case ~/XXX([IV]*)/: grab(30)
  44.     break
  45.   case ~/X([IVX]*)/: grab(10)
  46.     break
  47.   case 'IX': 9
  48.     break
  49.   case ~/V(I*)/: grab(5)
  50.     break
  51.   case 'IV': 4
  52.     break
  53.   case 'III': 3
  54.     break
  55.   case 'II': 2
  56.     break
  57.   case 'I': 1
  58.     break
  59.   default: throw new NumberFormatException("Invalid Roman numeral: ${numeral}")
  60.   }
  61. }
  62.  
  63. /**
  64. * The recursive part of parsing.
  65. * @param value must be the integer value collected so far.
  66. * The method picks up the tail of the last regex match.
  67. */
  68. private static Integer grab(Integer value) {
  69.   value + parse(Matcher.lastMatcher[0][1])
  70. }
  71.  
  72. /**
  73. * Format an integer as a Roman numeral.
  74. * @param value must be the integer to format,
  75. * @throws IllegalArgumentException if the integer is out of range (1..3999).
  76. */
  77. static String print(Integer value) {
  78. if (value </>1 || value> />3999) throw new
  79. IllegalArgumentException("Integer out of range: ${value}")
  80.   Integer n = VAL.floorKey(value)
  81.   (value == n)? VAL[value] : VAL[n] + print(value - n)
  82. }
  83.  
  84. private static VAL =
  85.   new TreeMap([1000: 'M', 900: 'CM', 500: 'D', 400: 'CD',
  86.                100: 'C', 90: 'XC', 50: 'L', 40: 'XL',
  87.                10: 'X', 9: 'IX', 5: 'V', 4: 'IV', 1: 'I'])
  88. }

You may download a zipped archive here containing the above code (with better indenting) and also a Spock unit test.

Comments are closed.