Skip to main content

Operators and Precedence

TransformScript provides operators for arithmetic, concatenation, comparison, logical tests, type checks and casts, null fallbacks, and conditionals. Each operator binds at a defined level of precedence that controls how an expression groups when you don't add parentheses. Operators that bind more tightly attach to their operands first. For example, multiplication binds more tightly than addition, so 2 + 3 * 4 is 14, not 20. When you want a different grouping, wrap the looser part in parentheses.

Precedence

The table below lists every band from the loosest-binding form to the tightest. A form lower in the table binds more tightly than a form above it, so it attaches to its operands first. Binary operators are left-associative unless noted.

Band (low to high)FormsAssociativity
using bindingsusing(a = …) bodyprefix block
conditional if/elseif c a else bright (nests via else)
defaulta default bleft
ternaryc ? a : bright (branches re-enter if/else)
type castexpr as Type { opts }left
matchsubject match { … }left
updatesubject update { … }left
logical ororleft
logical andandleft
logical not (prefix)not x, !xright (prefix)
comparison and type-check== != ~= < <= > >=, x is Typeleft
additive / shift / concat+ - << >> ++left
multiplicative* / % modleft
unary minus (prefix)-xright (prefix)
postfixselector chains (.field, […]), callsleft
primaryliterals, (…), identifiers, objects, arrays, lambdas, do

A few consequences are worth keeping in mind:

  • 2 + 3 * 4 is 14, not 20, because multiplicative binds more tightly than additive.
  • ++, <<, and >> sit in the same band as + and -, all tighter than every comparison. So 1 + 1 < 3 is true ((1+1) < 3), and 1 + 2 ++ "x" evaluates (1 + 2) ++ "x", which raises a runtime error because 3 ++ "x" mixes a number with a string.
  • Unary minus binds more tightly than *: -2 * 3 is -6, and -2 + 3 is 1.
  • default is looser than the ternary: null default false ? 1 : 2 parses as null default (false ? 1 : 2).
  • as is looser than ++: 5 as String ++ "x" casts first, so the ++ never sees a single combined expression. Write (5 as String) ++ "x" to concatenate, which gives "5x".

Arithmetic

The arithmetic operators are +, -, *, /, %, and mod. They coerce each operand to a number: a numeric string parses (a string with a . becomes a decimal, otherwise an integer), true becomes 1, and both false and null become 0. A non-numeric string raises Cannot convert '…' to a number.

OpMeaningNotes
+add / concatenate / pushOverloaded — see below
-subtract / removeOverloaded for arrays and objects — see below
*multiplyPure numeric
/divideAlways returns a decimal
%moduloInteger-style remainder; raises on divide-by-zero
modmodulo (word form)Identical to %; a soft keyword
2 * 3 + 4        // 10
10 - 2 * 3       // 4
7 mod 2          // 1

/ always produces a decimal, even when the result is whole: 6 / 2 is 3.0, and 7 / 2 is 3.5. Division (and % or mod) by zero raises Division by zero.

mod is a soft keyword. It acts as the modulo operator only when it is not immediately followed by (, so a function named mod(…) is still callable.

The + Overload

+ is overloaded by operand type. The cases are checked in this order:

  1. Periods and temporals: if either side is a Period or temporal, it does temporal arithmetic. |P1D| + |PT3H| yields a combined Period of 1 day 3 hours. See Types and Coercion.
  2. Both arrays: concatenation, so [1] + [2] is [1, 2].
  3. Left is an array and right is not: push, so [1,2] + 3 is [1, 2, 3].
  4. Either side is a string: string concatenation, so 1 + "a" is "1a" and "a" + 1 is "a1".
  5. Otherwise: numeric addition.

The - Overload

- also specializes by the left operand's type:

  • Period or temporal subtraction, such as |2021-03-02T10:39:59| - |P1D|.
  • Left is an array: element removal, removing every matching element, so [1,2,3] - 2 is [1, 3].
  • Left is an object: key removal, so {a:1, b:2} - "a" is {b: 2}.
  • Otherwise: numeric subtraction.

Concatenation and Shift

The operators ++, <<, and >> live in the additive band but are type-strict: unlike +, they do not coerce. Each works on strings, arrays, or objects and raises if the operands don't fit.

++ — Strict Concatenation and Merge

++ joins two operands of the same kind:

  • string with string, producing a string
  • array with array, producing a concatenated array
  • object with object, producing a merged object (the right side wins on a key clash)
"a" ++ "b" ++ "c"     // "abc"  (left-associative)
[1,2] ++ [3,4]        // [1, 2, 3, 4]

Mixing kinds, or feeding it a number, raises ++ supports only strings, arrays, or objects, for example 1 ++ 2.

<< — Append / Merge Into Left

<< adds the right operand to the left:

  • String left: appends; the right operand must also be a String, otherwise << expects right operand to be a String.
  • Array left: pushes the right operand as one new element, so [1,2] << 3 is [1, 2, 3].
  • Object left: merges the right object in (the right side wins), so {a:1} << {b:2} is {a:1, b:2}; a non-object right operand raises.
"ab" << "c"      // "abc"

>> — Prepend / Merge Into Right

>> is the mirror image: the left operand is added to the front of the right.

  • String right: "c" >> "ab" is "cab" (the left must be a String).
  • Array right: 3 >> [1,2] is [3, 1, 2].
  • Object right: the left object's keys are laid down first, then the right object merged over them.

These are not bit shifts. TransformScript has no bitwise operators.

Comparison

All comparison operators share one band, tighter than the concatenation and additive band. They follow the equality and ordering rules in Types and Coercion.

OpMeaningCoercion
==strict equalitynone
!=strict inequalitynone
~=coercive equalitycoerces one operand toward the other's shape
< <= > >=orderingleft-type-driven; null on either side returns false
x is Typeruntime type checknone (reflection)
1 == 1.0       // true   (numerically equal)
"true" ~= true // true   (coercive)
"1" ~= 2       // false
3 > 2          // true

Strict comparison (== and !=) does no coercion. Coercive equality (~=) coerces one side toward the other's shape, recursing through arrays and objects, before comparing, so "1" ~= 1 is true but "1" ~= 2 is false.

Relational comparison (<, <=, >, >=) is left-type-driven and null-safe by returning false: if either operand is null the result is false (null > 2 and 2 > null are both false), otherwise the right operand is coerced to the left operand's type and compared; if it can't be coerced, the result is false.

x is Type — Type Check

is takes a bare type name on the right and produces a boolean. It binds looser than arithmetic, so 1 + 1 is Number is (1 + 1) is Number, which is true.

5 is Number      // true
"x" is String    // true

The valid type names are the runtime types in Types and Coercion.

Logical

The boolean operators are strict about booleans. They require actual Boolean operands and raise on anything else, which is stricter than the truthiness coercion used in conditions (see Types and Coercion).

OpMeaningOperands
orlogical ortwo Booleans
andlogical andtwo Booleans
notlogical not (prefix)one Boolean
!logical not (prefix)one Boolean (same as not)
true and false      // false
true or false       // true
not true            // false
!false              // true

1 and 2 raises Logical AND requires Boolean operands, and not 1 raises not requires Boolean operands. There is no coercion here, so feed these operators booleans.

Within this group, or is loosest, then and, then prefix not and ! (the tightest of the three). So not true and false is (not true) and false, which is false, whereas not (true and false) is true.

Type — is and as

is is covered above under comparison. The cast operator as sits in its own band, looser than match, logical, and comparison, but tighter than the ternary and default.

"42" as Number     // 42   (integer preserved)
42 as String       // "42"

The form is expr as Type { options }, where the type is a bare name and the optional { … } is an object of conversion options (such as a format strptime/strftime string, locale, or trueValues). A cast passes a value through unchanged when it already matches and no options are given. The per-type conversion rules — including how as Number preserves integers, how as Boolean raises on unrecognized strings, and how temporals format — are in Types and Coercion. Because as binds looser than ++, parenthesize when combining: (5 as String) ++ "x" is "5x".

Null Handling — default

a default b evaluates a; if it is null, it evaluates and returns b, otherwise it returns a. The test is strict null, not falsiness, so 0, "", and false are all returned as-is and do not trigger the fallback.

null default 5      // 5
3 default 5         // 3
0 default 9         // 0
false default 9     // false

default is left-associative, so it chains as a fallback ladder: a default b default c returns the first non-null value among a, b, and c. It binds looser than the ternary and the cast but tighter than if/else.

Conditionals — Ternary and if/else

TransformScript has two inline conditional forms (plus match, covered on the Syntax page).

The ternary form c ? a : b evaluates the condition for truthiness, then one branch:

true ? 1 : 2        // 1

The if c a else b keyword form requires else. There is no bare if and no then keyword. Parentheses around the predicate are optional.

if true 1 else 2    // 1

if/else is the loosest expression band, so a trailing default binds more tightly and lands inside the else branch: if false 1 else null default 9 is 9. The ternary's branches re-enter the full if/else grammar, which is why true ? 1 : 2 + 10 is 1 (the 2 + 10 is the false branch and is never evaluated).

Both conditional forms and filter selectors use truthiness, not boolean-strictness: a string is truthy only if it equals "true" (case-insensitive, trimmed), numbers are truthy when non-zero, and null and false are falsy. See Types and Coercion.

Unary Minus

A leading - is the unary-minus operator, not part of a numeric literal (TransformScript has no negative literals). It is equivalent to -1 * x, so it binds more tightly than *:

-5             // -5
-(2 + 3)       // -5
-2 * 3         // -6
2 - -3         // 5

A - that begins the lambda arrow -> (which is the two tokens - then >) is not read as unary minus.

Ranges in Selectors

Ranges are not a general operator. They exist only inside selector brackets and use the to keyword:

[1, 2, 3][1 to 2]    // [2, 3]   (inclusive, zero-based)

There is no a..b or [a:b] slice syntax; [1:3] is a parse error. See Selecting and Navigating Data for the full selector grammar.

Common Mistakes

  • | is not a pipe. It opens a temporal literal, so 1 | 2 and true | false are both errors. TransformScript has no pipe operator; thread values with the then function instead. See Functions and Lambdas.
  • . is not lambda shorthand. The implicit lambda parameters are $, $$, and $$$ (and _). . is selector and field access only.
  • There are no bitwise operators. << and >> are append and prepend, not shifts.
  • -> is two tokens. The lambda arrow is - then >.