zplCore.comzplCore.com

Documentation · Label model

Expressions
functions, formats, cultures.

An expression computes the text of a label element from the record: Format([price], "C", [culture]), If([qty] > 10, "BULK", "SINGLE"), GS1("01", [gtin], "10", [lot]). This page describes every function with parameters, results and errors, plus operators, conversions, cultures and formats – every example on it is evaluated by a test against the real engine.

Open the expression builder Function reference

01Where expressions are used

Every element of the label model has the property Expression (JSON "expression"): text elements, barcodes (Code) and RFID elements (Data). When an expression is set it wins over Text/Code and over DataField. The result is converted to text with the engine culture and then printed; {t:key} placeholders in the result are translated like in fixed texts.

{ "type": "text", "x": 4, "y": 4, "expression": "Upper([name]) + \", \" + [customer.city]" }
{ "type": "barcode", "x": 4, "y": 20, "symbology": "Gs1_128", "expression": "GS1(\"01\", [gtin], \"10\", [lot])" }

The engine is ZplCore.Labels.ExpressionEngine; every Label has one (label.Expressions) for custom functions, variables and the culture. Expressions are evaluated per record when the label is bound (label.For<T>(), JSON records, DataRow …) and once for label.ToZpl() without data.

About the examples. Unless a table says otherwise, the examples read the sample record below, run in the first record of a job with 3 records on a 100 × 50 mm label, use the default (invariant) engine culture, and Now() is 22 October 2026, 14:30:05. The column “Result” shows the printed text. Click an expression to try it in the expression builder.
{
  "price": 1234.5,
  "oldPrice": 1499,
  "culture": "de-DE",
  "name": "Müller",
  "first": "Anna",
  "last": "Schmidt",
  "sku": "ab-123",
  "qty": 3,
  "weight": 0.4567,
  "date": "2026-10-22",
  "deDate": "22.10.2026",
  "gtin": "09521234543213",
  "lot": "LOT42",
  "serial": "42",
  "textPrice": "19.99",
  "textOld": "9.99",
  "empty": "",
  "nothing": null,
  "inStock": true,
  "Preis [EUR]": 4.5,
  "Preis.netto": 3.1,
  "customer": { "name": "ACME GmbH", "city": "Graz", "zip": "8010" },
  "items": [ { "name": "Bolt" }, { "name": "Nut" } ]
}

02Quick start: a price tag in several cultures

Two text elements: the old price, struck out, only when it is higher than the current price, and the current price as currency. The culture comes from the record ([culture]), so one label prints German, US or Japanese prices:

{ "type": "text", "expression": "If([oldPrice] > [price], \"instead of \" + Format([oldPrice], \"C\", [culture]), \"\")", "strikeout": true },
{ "type": "text", "expression": "Format([price], \"C\", [culture])", "fontSize": 22, "bold": true }
ExpressionRecordResult
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "")sample (de-DE)instead of 1.499,00 €
Format([price], "C", [culture])sample (de-DE)1.234,50 €
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "")en-USinstead of $1,499.00
Format([price], "C", [culture])en-US$1,234.50
Format([price], "C", [culture])ja-JP¥1,234
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "")old price lower(empty)
Format([price], "C", [culture])no culture field¤1,234.50
  • The culture argument is a culture name such as "de-DE", written as a literal or read from a field. Missing or empty → the engine culture (default: invariant, currency sign ¤). See Cultures.
  • The comparison [oldPrice] > [price] is numeric – also when prices arrive as text (CSV, JSON strings): two texts that are numbers are compared as numbers. See Types.
  • An empty result prints nothing; the struck-out element then simply stays empty.

03Syntax

ElementWritten asNotes
Field[price], [customer.city], [items[0].name], [Preis [EUR]]Value of the record (or of another element, see Fields). Brackets inside a name nest. Names ignore case when no exact match exists.
Name without bracketsprice, LABEL_NUMBER, SHIFTLetters, digits, _. Looked up as variable first, then as field.
Number42, 1.5, .5Digits with . as decimal point, always invariant. No exponent (1e3 is an error), no thousands separator; negative numbers use unary -.
Text"instead of ", "He said ""hi"""Double quotes, closed at the end (an open text is an error); a quote inside is doubled. No backslash escapes. In label JSON the quotes are escaped once more: "expression": "Upper(\"x\")".
Booleantrue, falseAny case. Printed as True/False.
Function callFormat([price], "C")Function names ignore case. Arguments are expressions separated by ,.
Parentheses([a] + [b]) * 2Grouping.
Member accessMath.Max(1, [qty])Only for types registered with AddTypeReference (C#), see C# API.
ExpressionResult
"He said ""hi"""He said "hi"
.5 + 11.5
0077
price * 22469
[PRICE]1234.5
TRUETrue
1e3Unexpected 'e3'

04Operators & precedence

From lowest to highest precedence; operators of the same level are evaluated left to right.

LevelOperatorsMeaning
1or ||Logical or (both sides are evaluated)
2and &&Logical and (both sides are evaluated)
3= == <> != < <= > >=Comparison: numeric when one side is a number or both sides are texts that are numbers, otherwise text ignoring case
4+ - &+ adds two numbers and joins everything else as text; & always joins text; - subtracts
5* / %Multiply, divide, remainder (always numeric)
6- not !Unary minus, logical not
7( ), calls, .Grouping, function calls, member access
ExpressionResultWhy
1 + 2 * 37* before +
(1 + 2) * 39grouping
10 / 42.5division is never integer division
10 % 31remainder
-7 % 3-1sign of the left side
10 / 0Infinityno error; guard with If
2 + "3"23text on one side → joined
"2" * "3"6* always converts to numbers
"2" - "3"-1- always converts to numbers
1 + 2 & 333same level, left to right: (1 + 2) & 3
[qty] & " pcs"3 pcs& joins
"a" = "A"Truetext comparison ignores case
"01" = 1Truenumber on one side → numeric
"01" = "1"Trueboth texts are numbers → numeric
"10" < "9"Falseboth texts are numbers → 10 < 9
"a10" < "a9"Truetext comparison, character by character
3 > 2 > 1False(3 > 2) = True, then True > 1 → 1 > 1
[qty] > 2 and [inStock]True
not [qty] = 3Falsenot binds first: (not 3) = 3
not ([qty] = 3)Falsewith parentheses
[qty] <> 3 || [qty] != 4True

05Types & conversion

Values are numbers (double), text, booleans, dates (DateTime) or empty (null). Integer and decimal values from records (C# int, long, decimal, JSON numbers) become numbers; everything else keeps its type. Conversions happen where an operator or function needs another type:

ConversionRule
→ textempty → ""; booleans → True/False; numbers and dates with the engine culture (invariant: 1234.5, 10/22/2026 14:30:05).
→ numberempty → 0; true → 1, false → 0; text is read with the engine culture, then invariant, else 0 (NumberStyles.Float: sign, decimal point, exponent – no thousands separators).
→ booleanempty → false; text → true unless "", "0" or "false" (any case); numbers → true unless 0.
→ datea date stays; text is read with the engine culture, then invariant, else 0001-01-01. Use ToDate(text, format) for fixed formats.

+ adds only when both sides are numbers. A number from a text field is text: [textPrice] + 1 joins. Comparisons are numeric when one side is a number or both sides are texts that are numbers (read with the engine culture, then invariant); other text is compared as text. Convert with Num() (or ToNumber(text, culture)) for arithmetic:

ExpressionResultWhy
[qty] + 14JSON number
[textPrice] + 119.991text + number → joined
Num([textPrice]) + 120.99converted first
[textOld] > [textPrice]Falseboth are number texts: 9.99 > 19.99
Num([textOld]) > Num([textPrice])False9.99 > 19.99 as numbers
[textPrice] * 239.98* converts
true + 1True1a boolean is not a number for +
1 / 30.3333333333333333full precision; round with Round or Format
0.1 + 0.20.30000000000000004binary floating point
Format(0.1 + 0.2, "0.00")0.30format for printing
Today()10/22/2026 00:00:00date → text (invariant)

With the engine culture de-DE (C#: label.Expressions.Culture) the same conversions use German conventions:

Expression (engine culture de-DE)Result
[price]1234,5
[price] + " EUR"1234,5 EUR
Num("19,99") * 239,98
Num("19.99") * 239,98
Format(ToDate([deDate]), "yyyy-MM-dd")2026-10-22
Today()22.10.2026 00:00:00

06Empty & missing values

A field that is missing, JSON null or a C# null is empty. Without data (label.ToZpl()) every field is empty. Empty values never throw: they print as "", count as 0 and as false.

ExpressionResultWhy
[missing](empty)unknown field
Upper([nothing]) & "|"|empty text
[nothing] * 20empty → 0
[missing] + 11empty is not a number → joined: "" + "1"
[missing] + 1 + 111"1" + 1 → joined again
Num([missing]) + 1 + 12convert first
If([nothing], "yes", "no")noempty → false
IsEmpty([empty])True""
IsEmpty([nothing])Truenull
IsEmpty(" ")Falsea space is text
Coalesce([empty], [nothing], "n/a")n/afirst non-empty value
If(IsEmpty([lot]), "", "LOT " + [lot])LOT LOT42text only when there is a value
Format([nothing], "C", "de-DE")(empty)nothing to format

07Fields & binding

[name] reads a value of the current record. Records can be C# objects (properties and fields), dictionaries, ExpandoObject, JsonElement/JsonNode, DataRow or LiteDB documents; LabelBinding.Map("name", r => …) adds computed fields. The lookup:

  1. A name with . or [n] is a path: [customer.city], [items[0].name]. A column whose name is exactly that text wins over the path (imported tables often have columns like Preis.netto).
  2. Exact name first, then ignoring case.
  3. Names of other elements: [Items.Total] or [Items.Total.Text] is the printed text of the element named Total – also when it comes later on the label (circular references give ""). A bare [Total] finds an element only after it has been printed and only when no record field has that name.
ExpressionResult
[customer.city]Graz
[items[1].name]Nut
[items[5].name](empty)
[Preis [EUR]] * 29
[Preis.netto]3.1
[CUSTOMER.CITY]Graz
[customer.zip] + " " + [customer.city]8010 Graz
[inStock]True
var label = new Label(LabelUnit.Mm, 100, 50)
    .Add(new TextItem { Name = "Net", Y = 4, Expression = "Format([price] / 1.2, \"0.00\")" })
    .Add(new TextItem { Y = 12, Expression = "\"net \" + [Items.Net] + \" / gross \" + Format([price], \"0.00\")" });
// with { price = 12 }: "net 10.00 / gross 12.00"

08Job variables

Variables are written without brackets (with brackets only the binding variables marked ✓ work).

VariableValue[…]
LABEL_INDEXZero-based number of the label (record) in the job–
LABEL_NUMBEROne-based: LABEL_INDEX + 1–
REPEAT_INDEXZero-based element index inside a RepeaterItem–
RECORD_INDEXZero-based record index (bindings)✓
PAGE_NUMBEROne-based record number (bindings)✓
TOTAL_PAGESNumber of records when known (collections, WithRecordCount), else empty✓
LABEL_WIDTH, LABEL_HEIGHTLabel size in label units (bindings)✓
own, e.g. SHIFTlabel.Expressions.Variables["SHIFT"] = "B" or SetCustomVariable–

The names are not reserved: a record field called PAGE_NUMBER (or one of the other binding variables) wins over the variable. LABEL_INDEX, LABEL_NUMBER and REPEAT_INDEX win over fields of the same name when written without brackets. Without a binding (label.ToZpl()) the binding variables are empty.

ExpressionResult
"Label " + LABEL_NUMBERLabel 1
"Page " + PAGE_NUMBER + " of " + TOTAL_PAGESPage 1 of 3
RECORD_INDEX0
[LABEL_WIDTH] + " x " + LABEL_HEIGHT + " mm"100 x 50 mm
[LABEL_NUMBER](empty)
"Page " + PAGE_NUMBERPage A-7

09Cultures

A culture decides decimal and thousands separators, currency, percent sign, date order, month and day names – and for some cultures even the calendar. zplCore uses .NET cultures (ICU data of the operating system). Two places set it:

WhereApplies toDefault
Culture argument of Format/ToString (3rd), ToNumber (2nd), IsNumeric (2nd), ToDate (3rd)only this callthe engine culture
Engine culture label.Expressions.Culture (C#) or engine.Cultureevery conversion of the label: numbers and dates as text, reading numbers and dates, Upper/Lower, Format without cultureCultureInfo.InvariantCulture
  • The default is invariant so labels never change with the server's regional settings: 1234.5, 10/22/2026, currency sign ¤.
  • Label JSON has no engine culture (Label.Expressions is not serialized). In JSON templates pass the culture as argument – literal "de-DE" or from the record [culture]. Label.Culture is something else: it selects the language of {t:key} translations.
  • The argument is a culture name: "de-DE", "en-US", "fr-CH", a neutral language such as "de", or "". A value that is not text (number, empty field) or empty text means the engine culture.
  • Unknown names throw FormatException: Unknown culture '…': only cultures that .NET knows (predefined in the ICU data) are accepted – "!!", "xx-YY" or "foo" fail.
  • Data-bound elements without expression use DataFieldFormat and TextItem.CultureName instead (no culture: invariant with a format); expressions do not read CultureName. The standard calendar and the plain spaces described below apply to them as well.
ExpressionResultCulture from
Format([price], "C", "de-DE")1.234,50 €literal
Format([price], "C", [culture])1.234,50 €field
Format([price], "C", If([customer.city] = "Graz", "de-AT", "en-US"))€ 1.234,50computed
Format([price], "C")¤1,234.50engine (invariant)
Format([price], "C", "")¤1,234.50engine
Format([price], "C", [missing])¤1,234.50engine
Format([price], "N2", "de")1.234,50neutral culture
Format([price], "C", "!!")Unknown culture '!!'malformed
Format([price], "C", "xx-YY")Unknown culture 'xx-YY'unknown
Upper("istanbul")ISTANBULengine (invariant)

The engine culture changes the conversions without arguments (here tr-TR, where the upper case of i is İ):

Expression (engine culture tr-TR)Result
Upper("istanbul")İSTANBUL
Format([price], "C")₺1.234,50
Format([price], "C", "en-US")$1,234.50
var label = Label.FromJson(json);                         // JSON: no engine culture
label.Expressions.Culture = CultureInfo.GetCultureInfo("de-DE");   // C#: for the whole label
// Format([price], "C") → "1.234,50 €", [price] + " EUR" → "1234,5 EUR"
// Format([price], "C", "en-US") → "$1,234.50" (the argument wins)

Results per culture

The same values in common cultures. The engine prints them the same on Windows, Linux and macOS: special spaces become plain spaces – the no-break and narrow no-break spaces of fr-FR, pl-PL or sv-SE thousands and of times such as 2:30 PM (U+00A0, U+202F, U+2000–U+200A, U+205F), which ICU versions set differently and printer fonts often cannot print – and dates use the culture's standard calendar (below). Other characters still come from the ICU data of the operating system, e.g. the apostrophe of de-CH (’ or ') and the direction marks of Arabic texts; the tests compare those normalized.

ExpressionResult
Format(1234.5, "C", "de-DE")1.234,50 €
Format(1234.5, "C", "de-AT")€ 1.234,50
Format(1234.5, "C", "de-CH")CHF 1’234.50
Format(1234.5, "C", "en-US")$1,234.50
Format(1234.5, "C", "en-GB")£1,234.50
Format(1234.5, "C", "fr-FR")1 234,50 €
Format(1234.5, "C", "fr-CH")1 234.50 CHF
Format(1234.5, "C", "it-IT")1.234,50 €
Format(1234.5, "C", "es-ES")1.234,50 €
Format(1234.5, "C", "nl-NL")€ 1.234,50
Format(1234.5, "C", "pl-PL")1 234,50 zł
Format(1234.5, "C", "sv-SE")1 234,50 kr
Format(1234.5, "C", "ja-JP")¥1,234
Format(1234.5, "C", "zh-CN")¥1,234.50
Format(1234.5, "C", "ar-SA")1٬234٫50 ر.س.‏
Format(1234.5, "C", "ar-AE")1,234.50 د.إ.‏
Expressionde-DEen-USen-GBfr-FRfr-CHja-JPar-SA
Format(1234.5, "N2", c) 1.234,50 1,234.50 1,234.50 1 234,50 1 234,50 1,234.50 1٬234٫50
Format(0.125, "P1", c) 12,5 % 12.5% 12.5% 12,5 % 12,5% 12.5% 12٫5٪؜
Format(1234.5, "#,##0.00", c) 1.234,50 1,234.50 1,234.50 1 234,50 1 234,50 1,234.50 1٬234٫50
Format(ToDate([date]), "d", c) 22.10.2026 10/22/2026 22/10/2026 22/10/2026 22.10.2026 2026/10/22 11/05/1448
Format(ToDate([date]), "D", c) Donnerstag, 22. Oktober 2026 Thursday, October 22, 2026 Thursday, 22 October 2026 jeudi 22 octobre 2026 jeudi, 22 octobre 2026 2026年10月22日木曜日 جمادى الأولى
Calendars. Every culture formats and reads dates in its standard calendar from a fixed table (ExpressionEngine.StandardCalendar), the same on every operating system: ar-SA Islamic Umm al-Qura (22 Oct 2026 = 11 Jumada al-Ula 1448, the last column above shows the pattern dd/MM/yyyy and the month name), th/th-TH Thai Buddhist (year 2569), Persian for fa, fa-IR, fa-AF, ps, ps-AF, ckb-IR, lrc, mzn and uz-Arab (year 1405); all other cultures Gregorian. Without the table the result depended on the server: Linux formatted ar-SA in the Gregorian calendar, Windows in Umm al-Qura. For Gregorian dates with Arabic texts use e.g. ar-AE or ar-EG, or a fixed pattern with an invariant culture: Format(ToDate([date]), "dd.MM.yyyy").
ExpressionResult
Format(ToDate([date]), "d", "th-TH")22/10/2569
Format(ToDate([date]), "yyyy/MM/dd", "fa-IR")1405/07/30
Format(ToDate([date]), "d", "ar-AE")22‏/10‏/2026
Format(1234567.891, "N", "en-IN")12,34,567.891
Format(1234567.891, "N", "de-CH")1’234’567.891

10Number formats

The 2nd argument of Format(value, format, culture?) is a .NET format string. For numbers:

FormatMeaningPrecision
C, C0…C99Currency of the culture (sign, position, decimals)decimals; default from the culture
N, N2Number with thousands separatordecimals; default from the culture (mostly 2 or 3)
F, F2Fixed point, no thousands separatordecimals
P, P1Percent: value × 100 with the culture's percent signdecimals
E, E2Exponential (4.57E-001)decimals
G, RGeneral / round-trip (shortest exact text)significant digits
D, D5, X, X4Decimal digits with leading zeros / hexadecimal – for whole numbers; a number with decimals throws an error.minimum digits
Custom: 0 # . , % ; 'text'0 digit or zero, # digit if present, . decimal point, , thousands separator (or ×1000 at the end), % percent, ; sections positive;negative;zero, quoted text literallyas written

Rounding: standard formats with fewer decimals than the value (N0, F0, C0, P0 …) round an exact half to the even digit – 1234.5 → 1234, 2.5 → 2, 3.5 → 4. Custom formats ("0", "#,##0") and Round() round half away from zero (12.5 → 13). For prices round first: Format(Round([price]), "C0", …).

Separators in a custom format are placeholders: . prints the decimal separator of the culture, , its thousands separator. Put unit texts in single quotes ("0.00 'kg'"): letters such as E would otherwise be read as format characters. Double quotes inside the expression's text would end it.

ExpressionResult
Format([price], "N2", "en-US")1,234.50
Format([price], "N0", "de-DE")1.234
Format([price], "N", "de-DE")1.234,500
Format([price], "F", "de-DE")1234,500
Format([price], "F0")1234
Format(2.5, "F0") + " " + Format(3.5, "F0")2 4
Format(12.5, "0") + " " + Format(13.5, "0")13 14
Format([price], "C0", "de-DE")1.234 €
Format(Round([price]), "C0", "de-DE")1.235 €
Format([price], "C3", "en-US")$1,234.500
Format(-1234.5, "C", "en-US")-$1,234.50
Format([weight], "P1", "en-US")45.7%
Format([weight], "P0", "de-DE")46 %
Format([weight], "E2", "en-US")4.57E-001
Format([price], "R")1234.5
Format([price], "0.00", "de-DE")1234,50
Format([price], "#,##0.00", "de-DE")1.234,50
Format([weight], "0.000 'kg'", "de-DE")0,457 kg
Format([weight] * 1000, "#,##0 'g'")457 g
Format([qty], "000")003
Format(-1234.5, "#,##0.00;(#,##0.00)", "en-US")(1,234.50)
Format(0, "#,##0.00;-#,##0.00;'free'")free
Format(1234567, "#,##0,'k'")1,235k
Format([qty], "D5")00003
Format(255, "X4")00FF
Format(2.5, "D3")Format specifier was invalid.
Format([price], "dd.MM.yyyy")dd1235MMyyyy

Numbers as text ("19.99" from CSV or a JSON string) are formatted like numbers when a format is given: the text is read with the engine culture; text that is not a number prints unchanged.

ExpressionResult
Format([textPrice], "C", "de-DE")19,99 €
Format("12.5", "0.00", "fr-FR")12,50
Format([sku], "0.00")ab-123
Format([textPrice])19.99

11Date formats

Format formats dates – values from Now(), Today(), AddDays…, ToDate(…) or DateTime fields of C# records. Dates in JSON or CSV are text: convert them with ToDate first, otherwise Format prints the text unchanged.

FormatMeaningde-DE / en-US
dShort date22.10.2026 / 10/22/2026
DLong dateDonnerstag, 22. Oktober 2026 / Thursday, October 22, 2026
t, TShort / long time14:30 / 2:30 PM
g, GDate + time22.10.2026 14:30
o, u, sISO 8601 (culture independent)2026-10-22T14:30:05.0000000
dd d / ddd ddddDay 01–31 / 1–31, day name short / longDo / Donnerstag
MM M / MMM MMMMMonth 01–12 / 1–12, month name short / longOkt. / Oktober
yyyy yyYear 4 / 2 digits2026 / 26
HH hh mm ss ttHour 00–23 / 01–12, minute, second, AM/PM
'text', \xLiteral text

There is no week number in .NET date formats. / and : in a pattern print the culture's date and time separators.

ExpressionResult
Format(Today(), "dd.MM.yyyy")22.10.2026
Format(Now(), "yyyy-MM-dd HH:mm")2026-10-22 14:30
Format(Now(), "t", "en-US")2:30 PM
Format(Now(), "hh:mm tt", "en-US")02:30 PM
Format(Today(), "dddd, d. MMMM yyyy", "de-DE")Donnerstag, 22. Oktober 2026
Format(Today(), "MMM yyyy", "en-US")Oct 2026
Format(Today(), "ddd", "fr-FR")jeu.
Format(Today(), "yyMMdd")261022
Format(Today(), "o")2026-10-22T00:00:00.0000000
Format(Today(), "dd/MM/yyyy")22/10/2026
Format(Today(), "dd/MM/yyyy", "de-DE")22.10.2026
Format(Today(), "'KW' ww")KW ww
Format([date], "dd.MM.yyyy")2026-10-22
Format(ToDate([date]), "dd.MM.yyyy")22.10.2026

12Function reference

All 47 built-in functions. Names ignore case. Missing arguments are empty (null); extra arguments are ignored unless noted. Arguments are converted as described in Types: “text” arguments go through text conversion, “number” arguments through number conversion (non-numbers → 0), counts are truncated to whole numbers.

Text

Upper(text) · Lower(text)
Upper / lower case with the engine culture. Returns text.
Len(text)
Number of UTF-16 characters. Returns a number (empty → 0).
Left(text, n) · Right(text, n)
The first / last n characters; n is clamped to 0…length (never an error).
Mid(text, start, n?)
1-based start (1 = first character, values below 1 count as 1), n characters or the rest.
Substring(text, start, n?)
0-based start (clamped), n characters or the rest.
Trim(text, chars?) · TrimStart(text) · TrimEnd(text)
Removes white space at both ends / the start / the end. Trim with a 2nd argument removes those characters instead (each character of the text is one to remove).
Replace(text, find, with)
Replaces every occurrence (case-sensitive). An empty find returns the text unchanged.
PadLeft(text, width, char?) · PadRight(text, width, char?)
Fills up to width characters with the first character of char (default space). Longer text stays unchanged.
Contains(text, part) · StartsWith(text, part) · EndsWith(text, part)
True/false, ignoring case.
IndexOf(text, part)
0-based position of part ignoring case, −1 when missing.
Concat(value, …)
Joins any number of values as text (empty values add nothing).
Repeat(text, n)
The text n times; n is clamped to 0…10,000.
ExpressionResult
Upper([name])MÜLLER
Lower("ÄÖÜ")äöü
Len([name])6
Left([name], 3)Mül
Left([name], 99)Müller
Right([name], 2)er
Mid([name], 2, 3)üll
Mid([name], 2)üller
Substring([name], 1, 3)üll
Trim(" x ") & "|"x|
Trim("--x--", "-")x
"|" & TrimStart(" x ") & "|"|x |
"|" & TrimEnd(" x ") & "|"| x|
Replace([name], "ü", "ue")Mueller
Replace([sku], "-", "")ab123
PadLeft([serial], 6, "0")000042
PadRight("42", 5, "*")42***
PadLeft("123456", 3, "0")123456
Contains([name], "MÜL")True
StartsWith([sku], "AB")True
EndsWith([name], "x")False
IndexOf([name], "L")2
IndexOf([name], "z")-1
Concat([first], " ", [last], [nothing])Anna Schmidt
Repeat("*", 5)*****
Left([first], 1) + ". " + Upper([last])A. SCHMIDT

Conditions

If(condition, then, else?)
Returns then when the condition is true (see boolean conversion), else else (missing → empty). Both branches are evaluated before the choice, so a branch must not throw (e.g. GS1 with an empty value) – choose the arguments instead.
IsEmpty(value)
True for empty values and ""; " " and 0 are not empty.
Coalesce(value, …)
The first value that is neither empty nor ""; empty when there is none.
ExpressionResult
If([qty] > 2, "BULK", "SINGLE")BULK
If([inStock], "in stock", "sold out")in stock
If([qty] > 5, "BULK")(empty)
If("0", "yes", "no")no
If("false", "yes", "no")no
If("abc", "yes", "no")yes
If([qty] = 1, "1 piece", [qty] + " pieces")3 pieces
If([weight] < 1, Format([weight] * 1000, "0 'g'"), Format([weight], "0.0 'kg'"))457 g
Coalesce([empty], [customer.name])ACME GmbH
Coalesce(0, "x")0
IsEmpty(0)False

Math

Round(number, decimals?)
Rounds half away from zero (2.5 → 3, −2.5 → −3) to 0…15 decimals (default 0; values outside are clamped, so negative decimals round to whole numbers). Binary floating point applies: Round(1.005, 2) is 1.
Floor(number) · Ceiling(number) · Abs(number)
Round down / up, absolute value.
Min(number, …) · Max(number, …)
Smallest / largest of the arguments as numbers; without arguments 0.
ExpressionResult
Round(2.5)3
Round(-2.5)-3
Round(2.345, 2)2.35
Round(1.005, 2)1
Round([price], -1)1235
Round([price] * 1.2, 2)1481.4
Floor(-2.7)-3
Ceiling([qty] / 2)2
Abs(-3)3
Max(3, "10", 2)10
Min([qty], 2)2
Min()0

Date

Now() · Today()
Current date and time / today at 00:00 from Expressions.Clock (default: the local clock). Set the clock for tests or reprints with the original date.
AddDays(date, days) · AddMonths(date, months) · AddYears(date, years)
Adds (or subtracts, negative) days – fractions allowed – or whole months / years. A text date is converted like ToDate(text). The end of the month is kept valid (31 Jan + 1 month = 28/29 Feb).
ToDate(text, format?, culture?)
Reads a date. With format the text must match exactly (DateTime.TryParseExact, names in culture); otherwise – or when it does not match – the text is read with the engine culture, then invariant. Text that is no date gives 0001-01-01 (no error): check with Format(…, "yyyy") = "0001" if needed.
ExpressionResult
Format(AddDays(Today(), 30), "dd.MM.yyyy")21.11.2026
Format(AddDays(Now(), -0.5), "dd.MM. HH:mm")22.10. 02:30
Format(AddMonths(ToDate([date]), 18), "MM/yyyy")04/2028
Format(AddMonths(ToDate("2027-01-31"), 1), "yyyy-MM-dd")2027-02-28
Format(AddYears([date], 2), "yyyy-MM-dd")2028-10-22
Format(ToDate([deDate], "dd.MM.yyyy"), "yyyy-MM-dd")2026-10-22
Format(ToDate("22. Oktober 2026", "d. MMMM yyyy", "de-DE"), "d")10/22/2026
Format(ToDate([deDate]), "yyyy-MM-dd")0001-01-01
Format(ToDate("10/22/2026"), "yyyy-MM-dd")2026-10-22
Format(ToDate("garbage"), "yyyy")0001

Conversion & formatting

Format(value, format?, culture?) · ToString(value, format?, culture?)
ToString is the same function under another name. Numbers, dates and other formattable values: value.ToString(format, culture) (see number and date formats). Text with a format is read as a number with the engine culture and formatted; other text, booleans and empty values are converted to text unchanged. Without format: the value as text in culture. Errors: unknown culture name, a format the value rejects (D, X on a number with decimals).
Str(value)
Value as text with the engine culture.
Num(value)
Value as number with the engine culture (then invariant); not a number → 0.
ToNumber(text, culture?)
Like Num, the text read in culture first. Accepts sign, one decimal separator and exponent; with a culture argument also that culture's thousands separators in their places ("1.234,5" in de-DE → 1234.5, but "12,5" in en-US is no number). Without culture no thousands separators. Not a number → 0.
IsNumeric(value, culture?)
True for numbers and for text that reads as a number in culture (with a culture argument like ToNumber, thousands separators included; default: engine culture); false for booleans, empty values and other text.
ExpressionResult
Format([price])1234.5
Format([price], "", "de-DE")1234,5
ToString([price], "0.0")1234.5
Format([inStock], "C")True
Str([qty]) + Str(1)31
Num("12.5") * 225
Num("12,5")0
ToNumber("12,5", "de-DE") * 225
ToNumber("1.234,5", "de-DE")1234.5
ToNumber("1,234.5", "en-US")1234.5
ToNumber("12,5", "en-US")0
ToNumber("1.234,5")0
ToNumber(Replace("1.234,5", ".", ""), "de-DE")1234.5
ToNumber("1e3")1000
IsNumeric([textPrice])True
IsNumeric("12,5")False
IsNumeric("12,5", "de-DE")True
IsNumeric("1.234,5", "de-DE")True
IsNumeric([sku])False
IsNumeric(true)False

Check digits

Gs1CheckDigit(digits)
Returns the digits with the GS1 mod-10 check digit appended (GTIN, SSCC, GLN …). The input is not validated – pass digits only.
Mod10(digits) · Luhn(digits) · Mod11(digits) · Mod43(text)
Return only the check character: GS1/UPC weighting 3-1, Luhn (cards), mod 11 weights 2…7 (X for 10), Code 39 mod 43. Errors: invalid characters for the method.
VoicePickCode(gtin, lot, date?)
The 4-digit PTI voice pick code (CRC-16 of GTIN + lot [+ date as YYMMDD]); a date value is written as yyMMdd.
ExpressionResult
Gs1CheckDigit("400638133393")4006381333931
Gs1CheckDigit("0952123454321")09521234543213
Mod10("400638133393")1
Luhn("7992739871")3
Mod11("12345")5
Mod43("CODE39")W
VoicePickCode("10850510002011", "46587443HG234", "101026")1997
VoicePickCode("10850510002011", "46587443HG234")0862

GS1

GS1(ai1, value1, ai2, value2, …)
Builds the GS1 element string in bracketed form (01)…(10)… from pairs. AIs are written as text ("01", 2–4 digits; brackets are removed). Every pair is checked against the GS1 table – known AI, length, character set, check digit, date, mandatory and exclusive combinations – and an error throws FormatException naming the AI, so a wrong code is never printed. Use it as Expression of a GS1-128 barcode (Symbology.Gs1_128): the FNC1 separators after variable-length values are added when the barcode is written. Values must not contain brackets.
ExpressionResult
GS1("01", [gtin], "10", [lot])(01)09521234543213(10)LOT42
GS1("01", [gtin], "17", Format(AddMonths(Today(), 14), "yyMMdd"), "10", [lot])(01)09521234543213(17)271222(10)LOT42
GS1("(01)", [gtin])(01)09521234543213
GS1("01", "09521234543214")GS1(): AI 01: Bad checksum
GS1("01", [gtin], "10", [empty])GS1(): AI 10 has no value
GS1(1, [gtin])GS1(): '1' is not an application identifier (2 to 4 digits, written as text, e.g. "01")
GS1("01")GS1() takes pairs of application identifier and value, e.g. GS1("01", [Gtin], "10", [Lot])
GS1("10", "A(B)")GS1(): the value of AI 10 contains a parenthesis, which GS1-128 printed with ^BC mode D removes from the data
"GTIN " + Mid(GS1("01", [gtin]), 5)GTIN 09521234543213
label.Add(new BarcodeItem
{
    X = 5, Y = 20, Symbology = Symbology.Gs1_128,
    Expression = "GS1(\"01\", [gtin], \"17\", Format(ToDate([expiry]), \"yyMMdd\"), \"10\", [lot])",
});

More on GS1 barcodes: GS1 in the documentation.

13C# API

ExpressionEngine can be used on its own or through label.Expressions:

using System.Globalization;
using ZplCore.Labels;

var engine = new ExpressionEngine
{
    Culture = CultureInfo.GetCultureInfo("de-DE"),          // default: InvariantCulture
    Clock = () => new DateTime(2026, 10, 22, 14, 30, 5),     // Now()/Today()
};
engine.Variables["VAT"] = 0.19;                                // own variables
engine.SetCustomFunction("Euro", a => ExpressionEngine.Num(a[0]).ToString("0.00", CultureInfo.InvariantCulture) + " EUR");

var record = new Dictionary<string, object?> { ["price"] = 12.5m };
object? value = engine.Evaluate("[price] * (1 + VAT)", f => record.GetValueOrDefault(f));   // 14.875 (double)
string text = engine.EvaluateToString("Euro([price])", f => record.GetValueOrDefault(f));   // "12.50 EUR"
string date = engine.EvaluateToString("Format(Today(), \"D\")");                             // "Donnerstag, 22. Oktober 2026"
MemberPurpose
Evaluate(expression, fieldResolver?)Result as value (number = double, text, bool, DateTime, null). The resolver gets every [name] (and unknown bare names); without it fields are empty.
EvaluateToString(expression, fieldResolver?)Result as text with Culture – what a label prints.
CultureEngine culture (see Cultures).
ClockClock of Now()/Today().
Variables, SetCustomVariableOwn variables, names ignore case.
Functions, SetCustomFunctionAll functions (built-in ones can be replaced): Func<object?[], object?> gets the evaluated arguments.
AddTypeReference(type, alias?)Allows Type.Member and members on values of that type, e.g. Math.Max(1, [qty]), DateTime.Now.AddDays(3). Nothing else is reachable.
Str, Num, Bool (static), ToText, ToNumThe conversions of the engine, for own functions (static = invariant).

Thread safety: once configured, Evaluate may run on many threads (label jobs do this). Configuring (variables, functions, type references) is not thread-safe. Custom functions, type references and a custom clock make jobs run sequentially by default, see parallel generation.

14Errors

An invalid expression throws FormatException when the element is generated (ToZpl, Render, binding jobs) – the label is not printed with a wrong text. Messages are English:

ExpressionMessage
Foo(1)Unknown function Foo
Upper([name]Missing )
1 +Unexpected ''
1 2Unexpected '2'
"a" $ "b"Unexpected '$' at 4
[nameMissing ]
1.2.3The input string '1.2.3' was not in a correct format.
Format([price], "C", "!!")Unknown culture '!!'
Format(2.5, "X")Format specifier was invalid.
Upper("abc)Missing closing quote of the text at 6
[customer].nameType JsonElement is not registered (AddTypeReference)
Math.Max(1, 2)Member Max on null

Math.Max(…) without AddTypeReference(typeof(Math)) reads Math as a (missing) field – hence “Member Max on null”.

Wrong data does not throw (except in GS1 and invalid culture names): unknown fields are empty, text that is no number counts as 0, text that is no date is 0001-01-01. Check labels before printing with Label.Validate(record) and the label tests.

15Recipes

TaskExpressionResult
Price with currency of the recordFormat([price], "C", [culture])1.234,50 €
Struck-out old price only when higherIf([oldPrice] > [price], Format([oldPrice], "C", [culture]), "")1.499,00 €
Saving in percent"-" + Format(1 - [price] / [oldPrice], "P0", "de-DE")-18 %
Price with tax, 2 decimalsFormat([price] * 1.2, "N2", "de-DE") + " € incl. 20 % VAT"1.481,40 € incl. 20 % VAT
Unit price (per kg)Format([price] / [weight], "#,##0.00", "de-DE") + " €/kg"2.703,09 €/kg
Best before: today + 30 days"Best before " + Format(AddDays(Today(), 30), "dd.MM.yyyy")Best before 21.11.2026
Best before from a date textFormat(AddMonths(ToDate([deDate], "dd.MM.yyyy"), 6), "MM/yyyy")04/2027
Production date, ISOFormat(Now(), "yyyy-MM-dd HH:mm")2026-10-22 14:30
Weight in g or kgIf([weight] < 1, Format([weight] * 1000, "0 'g'"), Format([weight], "0.00 'kg'", "de-DE"))457 g
Weight with 3 decimalsFormat([weight], "0.000 'kg'", "de-DE")0,457 kg
Barcode data with check digitGs1CheckDigit("400638" + PadLeft([serial], 6, "0"))4006380000421
Serial number with prefix"SN-" + PadLeft(LABEL_NUMBER, 5, "0")SN-00001
Page x of yPAGE_NUMBER + "/" + TOTAL_PAGES1/3
Name: initial + last nameUpper(Left([first], 1)) + ". " + [last]A. Schmidt
Address line, empty parts skippedTrim(Concat([customer.zip], " ", [customer.city], If(IsEmpty([nothing]), "", ", " + [nothing])))8010 Graz
Conditional textIf([qty] >= 10, "BULK", If([qty] > 1, "MULTI", "SINGLE"))MULTI
Fallback textCoalesce([nothing], [customer.name], "unknown")ACME GmbH
Upper-case SKU without dashUpper(Replace([sku], "-", ""))AB123
Prices from text fields comparedIf(Num([textOld]) > Num([textPrice]), "SALE", "")(empty)
GS1-128 data with expiryGS1("01", [gtin], "15", Format(AddDays(Today(), 30), "yyMMdd"), "10", [lot])(01)09521234543213(15)261121(10)LOT42

16Pitfalls

  • Text that is no number is compared as text. "a10" < "a9" is true; two number texts ("9.99", "19.99") are compared as numbers.
  • + with an empty field joins. [missing] + 1 + 1 is "11"; write Num([missing]) + 1 + 1.
  • Dates from JSON/CSV are text. Format([date], "dd.MM.yyyy") prints the text unchanged; use Format(ToDate([date]), …), with a pattern for non-ISO texts: ToDate([deDate], "dd.MM.yyyy").
  • No culture → invariant. Format([price], "C") prints ¤1,234.50; pass a culture or set label.Expressions.Culture in C#.
  • ar-SA and th-TH use other calendars for dates.
  • D/X formats need whole numbers: Format(2.5, "D3") throws; round first or use "000".
  • Thousands separators are read only by ToNumber/IsNumeric with a culture argument; Num and conversions without culture need plain numbers.
  • Text that is no date or number does not throw: it becomes 0001-01-01 or 0.
  • not binds tighter than =: write not ([a] = 1).
  • Both branches of If are evaluated. A GS1() call with an empty value throws even in the branch that is not chosen.
  • Unit texts in formats: quote them ("0.0 'kg'").
  • Quotes in label JSON: "expression": "Format([price], \"C\", \"de-DE\")" – the JSON escapes are not part of the expression.
  • Engine limits: at most 256 levels of nested parentheses, arguments and unary operators (ExpressionEngine.MaxNesting); deeper expressions throw FormatException instead of exhausting the stack. The online builder limits length and nesting further.