Extending TimEL

One can extend TimEL's default language in three ways:

  • By adding variables
  • By adding functions
  • By adding types & conversions

You can find below an example of each.

To add a variable to an expression, you need to register it while compiling an expression via the TimEL.withVariable method.

A variable is implemented via the Variable interface, and it need to support both read and write operations.

If all your data is already in memory, a convenient way to supply a variable is via the TreeMapVariable which is a variable backed by a standard Java TreeMap whose keys are Intervals.

Be aware that variables can also be used as output, as in the following example:

// Setup the variable
TreeMapVariable<Integer> variable = new TreeMapVariable<>();
TreeMap<Interval, Integer> values = variable.getTreeMap();
// Compile "a = 1 + 1"
Expression<?> expression = TimEL
        .parse("a = 1 + 1")
        .withVariable("a", new IntegerType(), variable) // Declare 'a' as a int
// Evaluate
Instant now = Instant.now();
TimeIterator<?> output = TimEL
        .evaluate(expression, Interval.of(now, now.plus(1, ChronoUnit.MINUTES)));
output.next(); // Force the evaluation
// We'll have a sample with value=2

To declare a function you need to register it while compiling an expression via the TimEL.withFunction method.

A function is implemented via the Function interface which is used to model the behaviour of the function. The declaration of the function itself is instead provided via the FunctionProtoype annotation (or FunctionProtoypes if the same implementation matches more prototypes).

As an example, let's implement a function Integer length(String) that returns the length of a String passed as input:

@FunctionPrototype( // (1)
        returns = @Returns(type = IntegerType.class),
        name = "length",
        parameters = {
                @Parameter(type = StringType.class)
public class Length implements Function<Integer> {
    public UpscalableIterator<Integer> evaluate(Interval interval, ExecutorContext context, Upscaler<Integer> upscaler, Downscaler<Integer> downscaler, Evaluable<?>... arguments) {
        return new UpscalerIterator<>( // (4)
                new ValueAdapterTimeIterator<>( // (3)
                        ((Evaluable<String>) arguments[0]).evaluate(interval, context), // (2)

Note the following steps:

  1. We declare the length function, accepting a single String argument and returning an Integer;
  2. When evaluated, this function will at first invoke its argument (which we know being an Evaluable<String> due to the functions' prototype - hence the cast);
  3. We map each returned value (which is a string) to an integer via a lambda, in this case String::length;
  4. We make the outcome upscalable according to the return type - this is a common snippet for most functions.

Now, let's bind the function when compiling an expression:

Expression<?> expression = TimEL.parse("length(\"hello world\")")
        .withFunction(new Length())
Instant now = Instant.now();
TimeIterator<?> output = TimEL
        .evaluate(expression, Interval.of(now, now.plus(1, ChronoUnit.MINUTES)));

The expected output should be 11, that is the length of “hello world”.

Please note that function binding supports type variables, so that generic-functions (like when or coalesce) can accept any type. When dealing with template-types (like IntegralInteger<1>), TimEL itself is not equipped with a deduction mechanism that allows type parameters inferences, in this case programmatic deduction has to be provided overriding the default Function.resolveReturnType and Function.specializeVariableTemplate methods. An example of such a behaviour can be observed for the multiplication operator, as *(IntegralInteger<T>, IntegralInteger<U>) will return IntegralInteger<T + U> when T + U != 0, else Integer.

For more function examples, refer directly to the TimEL sources as all the standard functions are implemented in a similar way.


  • extensions.txt
  • Last modified: 2019/05/29 16:48
  • by a.leofreddi