RomanNumeral.java
package com.roxoft.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class RomanNumeral {
/** Roman notation for ZERO. */
public static final String ZERO = "NULLA";
/** Characters which cannot be repeated in a Roman numeral. */
private static final String UNREPEATABLES = "VLD";
/** Characters which can be used as a subtractive in a Roman numeral. */
private static final String SUBTRACTIVE = "IXC";
/** The Roman numeral String in question. */
private final String numeral;
/** A numerical representation of the Roman numeral in question. */
private final int value;
@SuppressWarnings("magicnumber")
private int characterValue(final String romanCharacter) {
return switch (romanCharacter) {
case "I" -> 1;
case "V" -> 5;
case "X" -> 10;
case "L" -> 50;
case "C" -> 100;
case "D" -> 500;
case "M" -> 1000;
default ->
throw new InvalidRomanNumeralException("'"
+ romanCharacter
+ "' is an unknown Roman numeral character"
);
};
}
private RomanNumeral(final String numeralString) {
if (numeralString.isEmpty()) {
value = 0;
this.numeral = ZERO;
return;
} else {
this.numeral = numeralString;
}
//get all the characters that are the same
final List<Integer> asIntegers = Arrays.stream(numeralString.split(""))
.map(this::characterValue)
.toList();
final List<Integer> calculatedValues = new ArrayList<>(List.of(asIntegers.get(0)));
for (int characterIndex = 1,
previousValue = calculatedValues.get(0),
calculatedValueIndex = 0,
characterRepetitionCount = 1;
characterIndex < asIntegers.size(); characterIndex++) {
int subject = asIntegers.get(characterIndex);
if (subject > previousValue) {
characterRepetitionCount = 1;
if (SUBTRACTIVE.contains(numeralString.substring(characterIndex - 1, characterIndex))) {
calculatedValues.set(calculatedValueIndex, subject - calculatedValues.get(calculatedValueIndex));
} else {
throw new InvalidRomanNumeralException("Use of '" + numeralString.charAt(characterIndex) + "' as subtractive character. Only I, X and C can be used as subtractive numerals.");
}
} else if (subject == previousValue) {
if (UNREPEATABLES.contains(numeralString.substring(characterIndex, characterIndex + 1))) {
throw new InvalidRomanNumeralException("Repeated instance of '" + numeralString.charAt(characterIndex) + "'. V, L or D cannot be repeated.");
}
if (++characterRepetitionCount > 3) {
throw new InvalidRomanNumeralException("Roman characters (in this case '" + numeralString.charAt(characterIndex) + "') cannot repeat more than 3 times");
}
calculatedValues.set(calculatedValueIndex, calculatedValues.get(calculatedValueIndex) + subject);
} else {
characterRepetitionCount = 1;
calculatedValues.add(subject);
calculatedValueIndex++;
}
previousValue = subject;
}
value = calculatedValues.stream().mapToInt(i -> i).sum();
}
/**
* @param numeralString a Roman numeral String.
* @return a {@link RomanNumeral} of the provided String.
*/
public static RomanNumeral of(final String numeralString) {
return new RomanNumeral(numeralString);
}
/** @return the numerical value of this Roman numeral. */
public int numericalValue() {
return value;
}
/** @return the String representation of this Roman numeral. */
public String romanNotation() {
return numeral;
}
}