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 }
| Expression | Record | Result |
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-US | instead 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
| Element | Written as | Notes |
| 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 brackets | price, LABEL_NUMBER, SHIFT | Letters, digits, _. Looked up as variable first, then as field. |
| Number | 42, 1.5, .5 | Digits 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\")". |
| Boolean | true, false | Any case. Printed as True/False. |
| Function call | Format([price], "C") | Function names ignore case. Arguments are expressions separated by ,. |
| Parentheses | ([a] + [b]) * 2 | Grouping. |
| Member access | Math.Max(1, [qty]) | Only for types registered with AddTypeReference (C#), see C# API. |
| Expression | Result |
"He said ""hi""" | He said "hi" |
.5 + 1 | 1.5 |
007 | 7 |
price * 2 | 2469 |
[PRICE] | 1234.5 |
TRUE | True |
1e3 | Unexpected 'e3' |
04Operators & precedence
From lowest to highest precedence; operators of the same level are evaluated left to right.
| Level | Operators | Meaning |
| 1 | or || | Logical or (both sides are evaluated) |
| 2 | and && | 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 |
| Expression | Result | Why |
1 + 2 * 3 | 7 | * before + |
(1 + 2) * 3 | 9 | grouping |
10 / 4 | 2.5 | division is never integer division |
10 % 3 | 1 | remainder |
-7 % 3 | -1 | sign of the left side |
10 / 0 | Infinity | no error; guard with If |
2 + "3" | 23 | text on one side → joined |
"2" * "3" | 6 | * always converts to numbers |
"2" - "3" | -1 | - always converts to numbers |
1 + 2 & 3 | 33 | same level, left to right: (1 + 2) & 3 |
[qty] & " pcs" | 3 pcs | & joins |
"a" = "A" | True | text comparison ignores case |
"01" = 1 | True | number on one side → numeric |
"01" = "1" | True | both texts are numbers → numeric |
"10" < "9" | False | both texts are numbers → 10 < 9 |
"a10" < "a9" | True | text comparison, character by character |
3 > 2 > 1 | False | (3 > 2) = True, then True > 1 → 1 > 1 |
[qty] > 2 and [inStock] | True | |
not [qty] = 3 | False | not binds first: (not 3) = 3 |
not ([qty] = 3) | False | with parentheses |
[qty] <> 3 || [qty] != 4 | True | |
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:
| Conversion | Rule |
| → text | empty → ""; booleans → True/False; numbers and dates with the engine culture (invariant: 1234.5, 10/22/2026 14:30:05). |
| → number | empty → 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). |
| → boolean | empty → false; text → true unless "", "0" or "false" (any case); numbers → true unless 0. |
| → date | a 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:
| Expression | Result | Why |
[qty] + 1 | 4 | JSON number |
[textPrice] + 1 | 19.991 | text + number → joined |
Num([textPrice]) + 1 | 20.99 | converted first |
[textOld] > [textPrice] | False | both are number texts: 9.99 > 19.99 |
Num([textOld]) > Num([textPrice]) | False | 9.99 > 19.99 as numbers |
[textPrice] * 2 | 39.98 | * converts |
true + 1 | True1 | a boolean is not a number for + |
1 / 3 | 0.3333333333333333 | full precision; round with Round or Format |
0.1 + 0.2 | 0.30000000000000004 | binary floating point |
Format(0.1 + 0.2, "0.00") | 0.30 | format for printing |
Today() | 10/22/2026 00:00:00 | date → 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") * 2 | 39,98 |
Num("19.99") * 2 | 39,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.
| Expression | Result | Why |
[missing] | (empty) | unknown field |
Upper([nothing]) & "|" | | | empty text |
[nothing] * 2 | 0 | empty → 0 |
[missing] + 1 | 1 | empty is not a number → joined: "" + "1" |
[missing] + 1 + 1 | 11 | "1" + 1 → joined again |
Num([missing]) + 1 + 1 | 2 | convert first |
If([nothing], "yes", "no") | no | empty → false |
IsEmpty([empty]) | True | "" |
IsEmpty([nothing]) | True | null |
IsEmpty(" ") | False | a space is text |
Coalesce([empty], [nothing], "n/a") | n/a | first non-empty value |
If(IsEmpty([lot]), "", "LOT " + [lot]) | LOT LOT42 | text 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:
- 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).
- Exact name first, then ignoring case.
- 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.
| Expression | Result |
[customer.city] | Graz |
[items[1].name] | Nut |
[items[5].name] | (empty) |
[Preis [EUR]] * 2 | 9 |
[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).
| Variable | Value | […] |
LABEL_INDEX | Zero-based number of the label (record) in the job | – |
LABEL_NUMBER | One-based: LABEL_INDEX + 1 | – |
REPEAT_INDEX | Zero-based element index inside a RepeaterItem | – |
RECORD_INDEX | Zero-based record index (bindings) | ✓ |
PAGE_NUMBER | One-based record number (bindings) | ✓ |
TOTAL_PAGES | Number of records when known (collections, WithRecordCount), else empty | ✓ |
LABEL_WIDTH, LABEL_HEIGHT | Label size in label units (bindings) | ✓ |
own, e.g. SHIFT | label.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.
| Expression | Result |
"Label " + LABEL_NUMBER | Label 1 |
"Page " + PAGE_NUMBER + " of " + TOTAL_PAGES | Page 1 of 3 |
RECORD_INDEX | 0 |
[LABEL_WIDTH] + " x " + LABEL_HEIGHT + " mm" | 100 x 50 mm |
[LABEL_NUMBER] | (empty) |
"Page " + PAGE_NUMBER | Page 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:
| Where | Applies to | Default |
Culture argument of Format/ToString (3rd), ToNumber (2nd), IsNumeric (2nd), ToDate (3rd) | only this call | the engine culture |
Engine culture label.Expressions.Culture (C#) or engine.Culture | every conversion of the label: numbers and dates as text, reading numbers and dates, Upper/Lower, Format without culture | CultureInfo.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.
| Expression | Result | Culture 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,50 | computed |
Format([price], "C") | ¤1,234.50 | engine (invariant) |
Format([price], "C", "") | ¤1,234.50 | engine |
Format([price], "C", [missing]) | ¤1,234.50 | engine |
Format([price], "N2", "de") | 1.234,50 | neutral culture |
Format([price], "C", "!!") | Unknown culture '!!' | malformed |
Format([price], "C", "xx-YY") | Unknown culture 'xx-YY' | unknown |
Upper("istanbul") | ISTANBUL | engine (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.
| Expression | Result |
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 د.إ. |
| Expression | de-DE | en-US | en-GB | fr-FR | fr-CH | ja-JP | ar-SA |
Format(1234.5, "N2", c) |
Format(1234.5, "N2", "de-DE")1.234,50 |
Format(1234.5, "N2", "en-US")1,234.50 |
Format(1234.5, "N2", "en-GB")1,234.50 |
Format(1234.5, "N2", "fr-FR")1 234,50 |
Format(1234.5, "N2", "fr-CH")1 234,50 |
Format(1234.5, "N2", "ja-JP")1,234.50 |
Format(1234.5, "N2", "ar-SA")1٬234٫50 |
Format(0.125, "P1", c) |
Format(0.125, "P1", "de-DE")12,5 % |
Format(0.125, "P1", "en-US")12.5% |
Format(0.125, "P1", "en-GB")12.5% |
Format(0.125, "P1", "fr-FR")12,5 % |
Format(0.125, "P1", "fr-CH")12,5% |
Format(0.125, "P1", "ja-JP")12.5% |
Format(0.125, "P1", "ar-SA")12٫5٪ |
Format(1234.5, "#,##0.00", c) |
Format(1234.5, "#,##0.00", "de-DE")1.234,50 |
Format(1234.5, "#,##0.00", "en-US")1,234.50 |
Format(1234.5, "#,##0.00", "en-GB")1,234.50 |
Format(1234.5, "#,##0.00", "fr-FR")1 234,50 |
Format(1234.5, "#,##0.00", "fr-CH")1 234,50 |
Format(1234.5, "#,##0.00", "ja-JP")1,234.50 |
Format(1234.5, "#,##0.00", "ar-SA")1٬234٫50 |
Format(ToDate([date]), "d", c) |
Format(ToDate([date]), "d", "de-DE")22.10.2026 |
Format(ToDate([date]), "d", "en-US")10/22/2026 |
Format(ToDate([date]), "d", "en-GB")22/10/2026 |
Format(ToDate([date]), "d", "fr-FR")22/10/2026 |
Format(ToDate([date]), "d", "fr-CH")22.10.2026 |
Format(ToDate([date]), "d", "ja-JP")2026/10/22 |
Format(ToDate([date]), "dd/MM/yyyy", "ar-SA")11/05/1448 |
Format(ToDate([date]), "D", c) |
Format(ToDate([date]), "D", "de-DE")Donnerstag, 22. Oktober 2026 |
Format(ToDate([date]), "D", "en-US")Thursday, October 22, 2026 |
Format(ToDate([date]), "D", "en-GB")Thursday, 22 October 2026 |
Format(ToDate([date]), "D", "fr-FR")jeudi 22 octobre 2026 |
Format(ToDate([date]), "D", "fr-CH")jeudi, 22 octobre 2026 |
Format(ToDate([date]), "D", "ja-JP")2026年10月22日木曜日 |
Format(ToDate([date]), "MMMM", "ar-SA")جمادى الأولى |
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").
| Expression | Result |
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 |
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.
| Expression | Result |
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.
| Expression | Result |
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.
| Expression | Result |
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.
| Expression | Result |
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.
| Expression | Result |
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") * 2 | 25 |
Num("12,5") | 0 |
ToNumber("12,5", "de-DE") * 2 | 25 |
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.
| Expression | Result |
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.
| Expression | Result |
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"
| Member | Purpose |
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. |
Culture | Engine culture (see Cultures). |
Clock | Clock of Now()/Today(). |
Variables, SetCustomVariable | Own variables, names ignore case. |
Functions, SetCustomFunction | All 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, ToNum | The 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:
| Expression | Message |
Foo(1) | Unknown function Foo |
Upper([name] | Missing ) |
1 + | Unexpected '' |
1 2 | Unexpected '2' |
"a" $ "b" | Unexpected '$' at 4 |
[name | Missing ] |
1.2.3 | The 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].name | Type 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
| Task | Expression | Result |
| Price with currency of the record | Format([price], "C", [culture]) | 1.234,50 € |
| Struck-out old price only when higher | If([oldPrice] > [price], Format([oldPrice], "C", [culture]), "") | 1.499,00 € |
| Saving in percent | "-" + Format(1 - [price] / [oldPrice], "P0", "de-DE") | -18 % |
| Price with tax, 2 decimals | Format([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 text | Format(AddMonths(ToDate([deDate], "dd.MM.yyyy"), 6), "MM/yyyy") | 04/2027 |
| Production date, ISO | Format(Now(), "yyyy-MM-dd HH:mm") | 2026-10-22 14:30 |
| Weight in g or kg | If([weight] < 1, Format([weight] * 1000, "0 'g'"), Format([weight], "0.00 'kg'", "de-DE")) | 457 g |
| Weight with 3 decimals | Format([weight], "0.000 'kg'", "de-DE") | 0,457 kg |
| Barcode data with check digit | Gs1CheckDigit("400638" + PadLeft([serial], 6, "0")) | 4006380000421 |
| Serial number with prefix | "SN-" + PadLeft(LABEL_NUMBER, 5, "0") | SN-00001 |
| Page x of y | PAGE_NUMBER + "/" + TOTAL_PAGES | 1/3 |
| Name: initial + last name | Upper(Left([first], 1)) + ". " + [last] | A. Schmidt |
| Address line, empty parts skipped | Trim(Concat([customer.zip], " ", [customer.city], If(IsEmpty([nothing]), "", ", " + [nothing]))) | 8010 Graz |
| Conditional text | If([qty] >= 10, "BULK", If([qty] > 1, "MULTI", "SINGLE")) | MULTI |
| Fallback text | Coalesce([nothing], [customer.name], "unknown") | ACME GmbH |
| Upper-case SKU without dash | Upper(Replace([sku], "-", "")) | AB123 |
| Prices from text fields compared | If(Num([textOld]) > Num([textPrice]), "SALE", "") | (empty) |
| GS1-128 data with expiry | GS1("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.