Skip to content

Expressions and operators

You write relations (expr) and conditions (applicable_when) in a small expression language. Tickmarks type-checks every expression when it compiles the schema, so adding a date to a currency amount, say, fails straight away as a compile error that points at the problem, rather than producing a wrong answer on some document months later.

This page is the complete reference. If you’re writing your first relation, Relations is a gentler start.

From lowest to highest precedence:

Precedence Operators Operand types Result
1 or bool bool
2 and bool bool
3 not bool bool
4 == != < <= > >= see comparisons bool
5 + - see arithmetic
6 * / see arithmetic
7 unary - int, decimal, money, duration same type
8 function calls, field names, literals, ( … )

A few things trip people up who are used to other languages. Logic is written in words, and, or and not, with no &&, || or !. Equality is ==, and a single = is a syntax error. Comparisons can’t be chained, so write a < b and b < c instead of a < b < c. Binary operators are left-associative: a - b - c means (a - b) - c.

Literal Example Type
Integer 12, 0 int (64-bit)
Decimal 1.21, 0.05 decimal (a digit is required after the point)
String 'terminated', 'it''s' string; double a quote to escape it
Date date'2027-03-01' date
Duration dur'P12M', dur'P1Y2M10D', dur'PT2H' duration (ISO 8601)
Boolean true, false bool
Null null only in field == null / field != null

There is no negative literal: -5 is unary minus applied to 5.

You don’t need to write a currency on a number used with money. A numeric literal next to a money value takes that value’s currency, so rent >= 0 and total == net + 25 work on a money field. A string literal compared with an enum must be one of the enum’s values.

Operands == != < <= > >=
int, decimal (mixed freely) yes yes
money, same currency yes yes
date with date, datetime with datetime yes yes
duration, same basis yes yes, see comparing durations
string with string yes no
bool with bool yes no
enum with its own labels or the same enum field yes no
field with null yes no

Equality is exact, with no tolerance: 1.50 == 1.5 is true, but 21.00 == 20.999 is false. If a document might print a rounded figure, round your side of the comparison with round.

Only == derives values. endDate == startDate + duration can compute any one of the three fields from the other two. <, <=, >, >= and != only check candidates.

Expression Result Notes
int + - * int int Overflow is an error, never a wrap.
int/decimal + - * int/decimal decimal Exact, never floating point.
money + - money money Same currency only.
money * int/decimal, int/decimal * money money
money * money error
number / number decimal Only inside round.
money / int/decimal money Only inside round.
money / money decimal Same currency; only inside round.
date + duration, duration + date, date - duration date Duration basis calendar, months or days.
date - date duration Takes the basis of the duration it is compared or combined with; calendar otherwise.
datetime + duration, datetime - duration datetime Duration basis exact.
duration + - duration duration Same basis.

Anything not in the table is refused. In particular, you can’t mix money with a plain number field (rent + units), combine different currencies, subtract one datetime from another, multiply a duration by a number, or do arithmetic on strings, bools or enums.

Rounds an int, decimal or money value to scale digits after the point. scale must be an integer literal from 0 to 28, and mode is written as a bare word, without quotes:

mode Rule 2.5 -2.5 2.1
half_even (default) nearest; ties to the even digit 2 -2 2
half_up nearest; ties away from zero 3 -3 2
down towards zero 2 -2 2
up away from zero 3 -3 3

Division is only allowed inside round. That’s because most divisions, such as 10 / 3, have no exact decimal result, and Tickmarks would otherwise have to pick a rounding for you. Inside round, the quotient is computed to 28 significant digits and rounded once:

vat == round(net * rate / 100, 2)
vat == round(net * rate / 100, 2, half_up)
monthly == round(annual / 12, 2)

rule must hold only when condition is true. If condition is false, or Tickmarks doesn’t know its value, the relation is satisfied. when must be the whole relation, or the rule of another when. It can’t appear inside not, and, a comparison or applicable_when.

when(status == 'terminated', terminationDate <= endDate)
when(hasDeposit == true, when(currencyKnown == true, deposit <= rent * 3))

Test whether a field has a value. The argument must be a field name. field == null is the same as is_null(field) and field != null the same as is_present(field).

when(status == 'terminated', is_present(terminationDate))

Refer to a field by its name; a name that isn’t a field is E-NAME-UNKNOWN. Array fields can’t be used in expressions at all (E-ARRAY-RELATION), and an applicable_when can’t refer to the field it belongs to.

When an expression fails to compile, the cause is usually one of these:

Expression Code Why
rate / 2 == x E-DIV-UNROUNDED Division outside round.
eurTotal == usdTotal E-CURRENCY Different currencies.
rent * rent == x E-TYPE Money times money.
status < 'draft' E-TYPE Enums have no order.
startDate < rate E-TYPE Date compared with a number.
amount > null E-TYPE null is only allowed with == and !=.
status == 'bogus' E-ENUM-VALUE Not a declared label.
monthsDur < daysDur E-BASIS Durations of different bases.
startDate + dur'PT2H' == endDate E-BASIS A date takes whole days only.
not when(a, b) E-WHEN-NESTED when must be outermost.
round(x, 40) E-ROUND-ARG Scale above 28.
startDate + duration E-REL-TYPE A relation must be true or false.
a < b < c E-EXPR-SYNTAX Comparisons cannot be chained.