New Operators

For the next version of FreshMarker, a number of new options will be created for the template engine. These new possibilities include the Spaceship Operator and a new String Concatenation Operator.

The Spaceship Operator is a comparison operator that is used in various programming languages to calculate three different comparison results in a single step. It is often used by the symbol <=>, which looks like a small spaceship, which gives the operator its name.

By using the Spaceship Operator, the coding of comparisons becomes shorter and more elegant, especially in situations where all three results (smaller, equal, larger) are required.

The Spaceship Operator is helpful for FreshMarker because it reduces the size of the templates.

<#if datum < .now?date>
Der Termin liegt in der Vergangeheit
<#elseif datum == .now?date>
Der Termin ist heute
<#else>
Der Termin liegt in der Zukunft
</#if>

This template produces different outputs depending on whether the date in the variable datum is in the past or in the future. To do this, the variable is compared twice with the current date in .now?date.

A variant with the Spaceship Operator is somewhat simpler.

<#switch datum <=> .now?date>
<#on -1>Der Termin liegt in der Vergangeheit
<#on 0>Der Termin ist heute
<#on 1>Der Termin liegt in der Zukunft
</#switch>

The Spaceship Operator is only called once and the result is used in a Switch Directive.

The Spaceship Operator is relatively easy to implement because, like the relational operators, it is based on the comparison properties of the base types. Most of them either implement the Comparable interface or have similiar functionalities.

protected TemplatePrimitive<?> compareValues(TokenType operator, int compare) {
  if (TokenType.COMPARE == operator) {
    return TemplateNumber.of(Integer.signum(compare));
  }
  return TemplateBoolean.from(switch (operator) {
    case LT -> compare < 0;
    case GT -> compare > 0;
    case LTE -> compare <= 0;
    case GTE, UNICODE_GTE -> compare >= 0;
    default -> throw new ProcessException("unsupported operation: " + operator);
  });
}

The TemplatePrimitive base class provides the compareValues method, which uses an operator and the compare value to generate the matching return value. For the relational operators, the compare value is compared with zero. For the COMPARE operator, our Spaceship Operator, the compare value is returned directly as a TemplateNumber.

@Override
public TemplatePrimitive<?> relational(TokenType operator, TemplatePrimitive<?> operand, ProcessContext context) {
  TemplateString rightValue = (TemplateString) operand;
  return compareValues(operator, getValue().compareTo(rightValue.getValue()));
}

The compareValue method is used by the relational methods in their subclasses. This example is the relational method of the TemlateString class. Here the compare value is calculated as the result of the String#compareTo method and passed to the compareValue method. With this implementation, all primitive FreshMarker types can use the Spaceship Operator if they also support the relational operators.

The second operator is a new String Concatenation Operator. The previous String Concatenation Operator + concatenates two strings, regardless of whether they are string literals or string values in variables.

${hello + ' ' + world}

In this example, the variables hello and world contain the corresponding strings and therefore the above interpolation results in the value Hello World. It happens relatively often, as in this example, that the strings are concatenated with spaces.

Of course, it would be nice if you could avoid the explicit concatenation with spaces. This is where the new String Concatenation Operator comes into play. This implicitly inserts a space between the strings to be concatenated.

${hello ~ world}

This example uses the new operator and you can see that no space needs to be inserted. This happens behind the scenes in the TemplateString class.

public TemplateString concat(TemplateString other, String seperator) {
  if (getValue().isEmpty()) {
    return other;
  }
  if (other.getValue().isEmpty()) {
    return this;
  }
  return new TemplateString(getValue() + seperator + other.getValue());
}

@Override
public TemplateObject operation(TokenType operator, TemplateObject operand, ProcessContext context) {
  return switch (operator) {
    case PLUS -> concat(operand.evaluate(context, TemplateString.class), "");
    case CONCAT -> concat(operand.evaluate(context, TemplateString.class), " ");
    default -> super.operation(operator, operand, context);
  };
}

The operation method is used for both operators. Both call the internal method concat. The difference between the two calls is the separator, which is once the empty string and once a blank character. This completes the implementation of the two new operators, and all that remains is to wait for FreshMarker 2.0.0.

Leave a Comment