“I’m disappointed by bands left and right, every day.”
Henry Rollins
A wonderful achievement of agile software development is, that new features are implemented when they are needed and not before. It took a long time for the built-ins left_pad
and right_pad
for padding text to be implemented, but now they are finally here.
Some requirements for a template engine seem quite simple until you look at the details. An example of such a requirement is the following invoice.
RECHNUNG Datum: 16. Mai 2025 Artikel Anzahl Einzelpreis (€) Gesamt (€) -------------------------------------------------------------- Tomatensuppe 1 5,90 5,90 Bruschetta 1 6,50 6,50 Pizza Margherita 1 10,90 10,90 Spaghetti Carbonara 1 12,50 12,50 Mineralwasser (0,5l) 2 3,20 6,40 Cola (0,3l) 1 3,50 3,50 Bier (0,5l) 1 4,90 4,90 Tiramisu 1 6,90 6,90 -------------------------------------------------------------- Zwischensumme 57,60 Service (10 %) 5,76 -------------------------------------------------------------- GESAMTBETRAG 63,36
The individual pieces of information should be correctly formatted in the columns and the date should appear flush right at the top. The formatting of the amounts is the easy part, the number types in FreshMarker provide the format
built-in. This built-in uses the Java formatting options for numbers. It is more difficult for the names of the individual invoice items. In order for the table to be structured properly, all names must be made the same length by padding the right-hand side with spaces. Until now, there was only the option of creating a separate function for this type of formatting or somewhat ugly interpolation.
${(name + ' ')[0..<10]} ${(' ' + name)[name?length..*10]}
ens Kaiser Jens Kaise
The first interpolation concatenates ten spaces to the end of the name and then slices the first 10 characters from the string using the slice operator. The second interpolation is a bit more complicated, it requires the length of the name for the slice operator.
With the introduction of left_pad
and right_pad
you can create the following template for the example above.
RECHNUNG ${.now?string("'Datum:' dd. MMMM yyyy")?left_pad(53)} Artikel Anzahl Einzelpreis (€) Gesamt (€) ${''?right_pad(62,'-')} <#list items as item> ${item.name?right_pad(29)} ${item.count?c?left_pad(5)} ${item.price?format("%15.2f")} ${item.total?format("%10.2f")} </#list> ${''?right_pad(62,'-')} Zwischensumme ${subtotal?format("%5.2f")} Service (10 %) ${service?format("%5.2f")} ${''?right_pad(62,'-')} GESAMTBETRAG ${total?format("%5.2f")}
In the first line, the date is formatted with "'Date:' dd. MMMM yyyy"
and the generated string is filled with spaces on the left. Instead of writing a separator line as a long chain of minus signs in the template, it is created with the interpolation ${''?right_pad(62,'-')}
. The individual names are brought to the same length with ${item.name?right_pad(29)}
.
FreshMarkers Partial Template Reduction can help anyone who is afraid that the dynamic formatting of dates and separators will take up too much time if they want to create thousands of invoices every day.
Template invoiceTemplateToday = invoiceTemplate.reduce(Map.of());
In this example, the above invoiceTemplate
is reduced with an empty Map
. This resolves all interpolations that do not depend on values from the data model. In the new template invoiceTemplateToday
, the date and separators are already converted into static content, as the following text indicates.
RECHNUNG Datum: 16. Mai 2025 Artikel Anzahl Einzelpreis (€) Gesamt (€) -------------------------------------------------------------- -------------------------------------------------------------- Zwischensumme Service (10 %) -------------------------------------------------------------- GESAMTBETRAG
The two built-ins left_pad
and right_pad
are implemented as usual. They are registered for TemplateString
and obviously fill a given String
on the right or left with a sufficient number of characters.
register.add("left_pad", (x, y, e) -> padding((TemplateString) x, e, y, true)); register.add("right_pad", (x, y, e) -> padding((TemplateString) x, e, y, false));
Since the difference between the two built-ins is only the side on which they fill up the String
, they are implemented by the same method.
private static TemplateString padding(TemplateString value, ProcessContext context, List<TemplateObject> parameters, boolean left) { BuiltInHelper.checkParametersLength(parameters, 1, 2); int size = parameters.getFirst().evaluate(context, TemplateNumber.class).asInt(); String text = value.getValue(); if (text.length() >= size) { return value; } String paddingPattern = parameters.size() < 2 ? " " : parameters.get(1).evaluate(context, TemplateString.class).getValue(); String padding = padding(paddingPattern, size - text.length()); return new TemplateString(left ? padding + text : text + padding); }
First, the method checks whether the built-in has one or two parameters. The first parameter is the width of the resulting String
. The second optional parameter specifies the padding characters.
If the text is already wide enough or wider, the original value is returned. If the exact length is needed, the result of the padding method can still be sliced. In the other case, a String
is generated for the missing number of characters. This String
is then appended either to the left or right of the original text. The String
is generated from a space or the second parameter of the built-in.
private static String padding(String paddingPattern, int paddingSize) { if (paddingPattern.length() < 2) { return paddingPattern.repeat(paddingSize); } int paddingRest = paddingSize % paddingPattern.length(); String patternStart = paddingPattern.repeat(paddingSize / paddingPattern.length()); if (paddingRest == 0) { return patternStart; } return patternStart + paddingPattern.substring(0, paddingRest); }
The second parameter does not have to contain just one character. If it contains less than two characters, the repeat
method is called on this String
. Padding with an empty String
therefore produces a result with the original length. If the second parameter contains a String
with more than one character, they are appended together until the required width is reached.
[${''?right_pad(40)}] [${''?right_pad(40, '-')}] [${''?right_pad(40, '-=')}] [${''?right_pad(40, '•●⬤●')}] [${''?left_pad(40, '▲▼')}]
[ ] [----------------------------------------] [-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=] [•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤] [▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼]
The second parameter can therefore be used to produce not only simple dividing lines, but also very attractive decors. This concludes the implementation of the two new built-ins for FreshMarker and I hope you enjoy padding your own texts.