FreshMarkers Roman Numbers

“Potius sero quam numquam”

Titus

FreshMarker is quite a small library, about 475 KB in size. Therefore there is always room to make a few small additions.

Some ideas arise by looking over the fence to the text processors. When generating lists in AsciiDoc, \LaTeX or Lout, the user can not only choose between unnumbered and numbered lists, but also the type of numbering. Arabic and Roman numerals are common here.

A list like the one in the following example is currently not so easy to create with FreshMarker.

Ⅰ. Lorem ipsum…
Ⅱ. Lorem ipsum…
Ⅲ. Lorem ipsum… 
Ⅳ. Lorem ipsum… 
Ⅴ. Lorem ipsum… 

For short lists, you can take the detour via the BuiltIn item_cycle.

<#list (1..5) as s with l>
${l?item_cycle('Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ')}. Lorem ipsum…
</#list>

For longer lists, however, the use is not elegant and numbers cannot be displayed in Roman numerals with this approach.

In order to be able to display Roman numbers, they must first be calculated. There is a whole range of algorithms for the calculation; a simple representative was selected for FreshMarker.

private static final String[] ONES = new String[]{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};
private static final String[] TENS = new String[]{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
private static final String[] HUNDREDS = new String[]{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};
private static final String[] THOUSANDS = new String[]{"", "M", "MM", "MMM"};

private static int checkRomanNumber(TemplateNumber value) {
  int number = value.asInt();
  if (number < 1 || number > 3999) {
    throw new IllegalArgumentException("roman numerals only between 1 and 3999");
  }
  return number;
}

private static String toRoman(int number) {
  return THOUSANDS[number / 1000] + HUNDREDS[(number % 1000) / 100] + TENS[(number % 100) / 10] + ONES[number % 10];
}

The usual Roman numerals Ⅰ, Ⅴ, Ⅹ, Ⅼ, Ⅽ, Ⅾ and Ⅿ (we ignore here ↁ, ↂ, ↇ and ↈ) with the formation rules for Roman numbers give a value range from 1 to 3999. This range of values is checked by the checkRomanNumber and the value of the TemplateNumber is returned as an int.

The toRoman method returns a String containing a roman number. For each digit of the decimal number, the corresponding representation is taken from a table and the results are concatenated.

The corresponding built-in is still missing so that a number can be converted into its Roman representation in an interpolation.

@BuiltInMethod
public static TemplateString roman(TemplateNumber value) {
  return new TemplateString(toRoman(checkRomanNumber(value)));
}

The Built-In method roman in the NumberPluginProvider uses checkRomanNumber and toRoman to generate a TemplateString from the TemplateNumber parameter.

With ${42?roman} an interpolation now produces XLII. If a lower case Roman number is required, the interpolation can be extended to ${42?roman?lower_case}.

In addition to the built-in roman, there is also utf_roman, which uses the corresponding Unicode characters for roman numerals instead of the letters I, V, X, L, D and M.

If you take a look at the Unicode table, you will also find Unicode characters for all Roman numbers between 1 and 12. These characters look better in a text file for short lists, so we also implement a built-in clock_roman that uses these twelve characters.

@BuiltInMethod("clock_roman")
public static TemplateString clockRoman(TemplateNumber value) {
  int number = value.asInt();
  if (number < 1 || number > 12) {
    throw new IllegalArgumentException("roman clock numerals only between 1 and 12");
  }
  return new TemplateString(String.valueOf((char) (number + 0x215F)));
}

The value range of this built-in is limited to 12 because mixing this characters with other looks ugly. This implementation does not require much calculation. Only the number is added to the character code before the character Ⅰ and the character is converted into a string. Thanks to the good Unicode support in Java, we also get the correct small roman number ⅺ with ${11?clock_roman?lower_case}.

With these three built-ins, numbers can now be converted to their Roman representation. But not yet the loop variable. We still need ${l?counter?roman} to obtain a Roman representation for the loop variable. Three corresponding Built-In methods in the LooperPlugInProvider generate the desired built-ins.

@BuiltInMethod("roman")
public static TemplateString romanCounter(TemplateSequenceLooper value) {
  return NumberPluginProvider.uppercaseRoman(value.getCounter());
}

The implementation in the LooperPlugInProvider delegates to the implementation in the NumberPluginProvider. Now the initial example can be produced with the following three lines.

<#list (1..5) as s with l>
${l?clock_roman}. Lorem ipsum…
</#list>

Ceterum censeo Cartaginem esse delendam!”

Cato the Elder

Leave a Comment