More About Range Expressions

FreshMarker as a Template Engine supports a whole range of data types for its model values. This data type are usually a mapping of the corresponding Java types such as String, Boolean, Long, Beans, Records, List and Maps. In addition, FreshMarker has the data type Range, which describes intervals with a lower and an optional upper limit.

The first of the following three Ranges is a so called Right-Unlimited Range and the other two are Right-Limited Ranges.

1..
1..10
10..1

The first Range has a lower limit 1 on the left but no upper limit on the right. Hence the name Right-Unlimited Range. The missing upper limit makes this Range unbounded (this is currently effectively 2147483647 but may yet change to 9223372036854775807) but its use is limited.

The other two Ranges have the limits 1 and 10 but produce different results when evaluated as a sequence. The second Range produces a monotonically increasing sequence, while the third Range produces a monotonically decreasing sequence. The first Right-Limited Range is called a regular Range and the second Right-Limited Range is an inverse Range.

This already provides a first possibility for the use of Ranges. A Range can be used as a sequence in a List Directive.

<#list 10..1 as i>${i} </#list> 

The above template generates the output 10 9 8 7 6 5 4 3 2 1. The Right-Unlimited Range is prohibited in the List Directive, because it is by definition unlimited or, to put it another way, it cannot produce a finite sequence. But it can appear in a List Directive as part of an expression that generates a sequence.

Another use of Ranges is Slicing. The Slicing operation cuts pieces out of sequences, Strings and Ranges. A Range in square brackets specifies the lower and upper index of a Slice. For the previous implementation, the Slice was a regular positive Range, i.e. both limits were non-negative and the lower limit was smaller than the upper limit.

<#list list[0..9] as i>${i} </#list> 

In the example above, the first 10 elements with the index 0 to 9 are returned from the sequence as a new sequence.

However, an error occurs if the sequence has fewer than 10 elements. Slicing can also be applied to Strings and Right-Limited Ranges and then creates a new Range resp. String. If the upper bound of the Range or the length of the String is exceeded, an error occurs here too.

<#list (1..)[0..9] as i>${i} </#list> 

This restriction does not exist for Right-Unlimited Ranges. Slicing with a non-empty Range (more on this later) never result in an error. Instead, it always returns a valid Range. The previous Ranges implementation was a subset of the possibilities in Freemarker. Some things were left out because they could be formulated differently in FreshMarker or were considered unnecessary. Until now, the Exclusive Right-Limited Ranges and the Length-Limited Ranges were missing from the FreshMarker repertoire. Instead of explaining both types, I will contrast them with their alternative FreshMarker formulation.

FreemarkerFreshMarker
Right-Limited Exclusive Ranges1..<101..9
a..<ba..(b-1)
a..<aempty range did not exist until now
Length-Limited Ranges4..*44..7
a..*ba..(a+b-1)
a..*0empty range did not exist until now

With the exception of the empty ranges, both constructs can also be formulated in FreshMarker. However, as the formulation of the Exclusive Right-Limited Ranges and the Length-Limited Ranges looks more elegant, they should also be supported by FreshMarker.

To do this, the visit method for RangeExpression must be adapted.

@Override
public TemplateRange visit(RangeExpression expression, Object input) {
  TemplateObject left = expression.getFirst().accept(this, null);
  Token range = (Token)expression.get(1);
  if (expression.size() < 3) {
    return new TemplateRightUnlimitedRange(left);
  }
  TemplateObject right = expression.get(2).accept(this, null);
  return new TemplateRightLimitedRange(left, right);
}

Previously, it checked for the existence of the upper bound as the third token and created either a TemplateRightUnlimtedRange instance or a TemplateRightLimitedRange instance. The second token did not have to be checked because CongoCC only allowed the DOT_DOT token there. After extending the FreshMarker grammar with two additional tokens for RangeExpression, the extended visit method can be seen here.

@Override
public TemplateRange visit(RangeExpression expression, Object input) {
  TemplateObject left = expression.getFirst().accept(this, null);
  Token range = (Token)expression.get(1);
  if (expression.size() < 3) {
    if (range.getType() != TokenType.DOT_DOT) {
      throw new ParsingException("right unlimited does not support: " + range, expression);
    }
    return new TemplateRightUnlimitedRange(left);
  }
  return switch (range.getType()) {
    case DOT_DOT -> new TemplateRightLimitedRange(left, expression.get(2).accept(this, null), false);
    case DOT_DOT_EXCLUSIVE -> new TemplateRightLimitedRange(left, expression.get(2).accept(this, null), true);
    case DOT_DOT_LENGTH -> new TemplateLengthLimitedRange(left, expression.get(2).accept(this, null));
    default -> throw new ParsingException("right limited does not support: " + range, expression);
  };
}

As there are now two further token in addition to DOT_DOT, an error message is generated if ..< or ..* are used without an upper bound. The third parameter is evaluated differently for the Right-Limited and Length-Limited Ranges. A new model class TemplateLengthLimitedRange is used for Length-Limited Ranges. To ensure that the Inclusive and Exclusive Right-Limited Ranges are evaluated correctly, an additional boolean flag is passed to the TemplateRightLimitedRange instance.

A new type has been introduced for the Length-Limited Ranges because the initialization of the internal structure differs from that of the Right-Limited Ranges. As the size of the Range is specified and not the upper limit of the Range, the upper limit can only be determined when the Range is evaluated. If the evaluation for the size of the Range results in zero, then this Range is an Empty Range.

Empty Ranges have been included in FreshMarker for the sake of completeness. I personally don’t like them, so there is currently a restriction on their use. It is forbidden to slice with them. Otherwise, the Built-Ins join, size, upper and lower deliver the expected results. Empty Ranges can also be used in List Directives without generating any output.

You can try out these new features and much more in FreshMarker 1.6.4.

Leave a Comment