Velocitip: the less than ﹤ and greater than ﹥ operators fail silently with Strings (VTL ain’t JavaScript!)

The first thing I teach in Velocity classes is that VTL (Velocity Template Language) is essentially a simpler way to write Java, and totally unrelated to JavaScript.

VTL frees you from many Java concerns. You don’t need a main method. You don’t need to think as much about classes and objects (though you’ll be a better developer if you do). And, key to any template language, you don’t need print: if it’s not inside a #directive() it’s assumed to be output.[1]

VTL is deliberately forgiving of runtime errors, prioritizing showing some output to the end user over failing completely. Some code — though far from all — that would throw a fatal error in pure Java is ignored by the Velocity engine. But that forgiving quality is also frustrating: the code still fails. You just don’t see it fail, so you may not catch the side effects.

Some broken code

I was looking at someone’s {{my.token}} code today and saw a structure like this:

#if( $LeadDate >= $StartDate && $LeadDate <= $EndDate )
Because you bought items between ${StartDate} and ${EndDate}, you’re eligible for a discount this year!
#end

At first take, I thought: OK, they have Strings in ISO 8601 format, like “2021-12-01” and “2021-12-31”. I’d use real date objects myself, but string comparison works for that particular format.

Then I did a double-take and realized I was thinking in JavaScript. This condition could never match in Velocity. In fact, not only could it never match, it couldn’t even compile! What actually happens is the Velocity engine logs a so-called “SEVERE” error under the hood:

SEVERE: Left side of '>=' operation is not a Number

But a SEVERE error isn’t the same as a FATAL error. You’d never see the SEVERE error in the Marketo UI (nor in any generated emails). But it sure as heck means the #if condition will never evaluate to true.

In JavaScript, sure!

One of the principal advantages of ISO 8601 date and datetime formats is that they can be sorted in what’s called lexicographical order and the result directly corresponds to chronological order.

If you run a simple string sort[2] on “2022-06-06”, “2022-02-02”, and “2022-12-12”, you’ll get the same order you see on the calendar. Likewise for “2022-06-06T12:34:56Z” and “2022-06-06T11:23:45Z”.

When you use the < and > (and  >= / <=) operators between strings, JavaScript kicks in lexicographic ordering, so this is fully valid in JS:

if( LeadDate >= StartDate && LeadDate <= EndDate )

But not in Java nor Velocity

In Java, the only operator valid for String comparison is ==. (Though == doesn’t check value equality so should almost never be used, but that’s a discussion for another day.)

In Velocity, similarly, only == can be used between Strings. And, similarly, you shouldn’t use it. It’s not as fundamentally wrong as in Java, but has a major flaw[3]. You’ve surely seen me tell people on Marketo Nation they should be using equals(), not ==. Trust me on that!

As the SEVERE message indicates, Velocity only supports < > >= <= for Numbers. It doesn’t treat Strings as sequences of 16-bit integers. It just fails. But it doesn’t tell you it failed (aside from whispering it into its logfile) because hey, why would you want to know your condition could never match? 😛

Doing it right

You can still do lexicographic comparison in Velocity and Java. You just can’t do it with < and >. Instead, you use the String.compareTo() method.

stringA.compareTo(stringB) returns a negative number if stringA comes lexicographically before stringB, the number 0 if they’re at the same position, or a positive number if stringA comes after stringB.

Note “a negative number” or “a positive number”: for legacy reasons, the exact number returned may change. You might get -99, -2 or -1 depending on context. The important thing is negative vs. 0 vs. positive.

So the correct way to use lexicographic order on ISO dates in Velocity is:

#if( $LeadDate.compareTo($StartDate) >= 0 && $LeadDate.compareTo($EndDate) <= 0 )
Because you bought items between ${StartDate} and ${EndDate}, you’re eligible for a discount this year!
#end

Still, while this works for a simple date range comparison, you get a lot more power if you create real Date objects from ISO strings, as my well-traveled post on days and times in Velocity shows.

Notes

[1] Simplifying somewhat by leaving out block #macros but you get the gist.

[2] That is, sorting using Unicode code unit order instead of locale/collation/case-aware logic. In general, code unit order rarely gives the results humans expect. But it does work fine with ISO 8601 formats, since they have such a limited character set.

[3] The flaw relates to unexpected string conversion but is off-topic for today.