12.2 Expressions, Control Flow Statements & Built-in Functions
Key Takeaways
- AL supports full arithmetic, relational, and boolean logical operators, including DIV for integer division truncation, MOD for remainder calculation, and short-circuit boolean evaluation.
- In if...then...else statements, placing a semicolon after then or before else terminates the conditional statement prematurely and triggers compiler syntax errors.
- repeat...until loops evaluate conditions at the end and are guaranteed to execute at least once, making them the standard pattern for database recordset traversal when guarded by if FindSet().
- String manipulation functions (StrSubstNo, CopyStr with MaxStrLen, DelChr, PadStr, IncStr) provide robust data sanitization and prevent string length overflow runtime crashes.
- CalcDate uses language-invariant date formulas enclosed in angle brackets (<...>) to calculate dynamic fiscal and calendar intervals relative to WorkDate() or Today().
12.2 Expressions, Control Flow Statements & Built-in Functions
Control flow statements and built-in system functions form the procedural backbone of business logic in Business Central. AL provides structured programming constructs for conditional branching, deterministic and non-deterministic iteration, string parsing, mathematical calculation, and temporal date calculations. For the MB-820 exam, developers must thoroughly understand statement execution rules, syntax pitfalls (such as semicolon placement in if-then-else blocks), loop execution guarantees, and standard built-in functions.
1. AL Operators & Expression Evaluation
AL expressions combine operands (variables, constants, literals, and function returns) with operators to yield a computed value.
+---------------------------------------------------------------------------------------------------+
| AL OPERATORS TAXONOMY |
+-------------------+--------------------+----------------------------------------------------------+
| Category | Operators | Operational Behavior & Notes |
+-------------------+--------------------+----------------------------------------------------------+
| Arithmetic | + , - , * | Standard addition, subtraction, multiplication |
| Division | / | Floating-point / Decimal division (e.g., 7 / 2 = 3.5) |
| | DIV | Integer division truncation (e.g., 7 DIV 2 = 3) |
| | MOD | Integer modulus remainder (e.g., 7 MOD 2 = 1) |
| Relational | = , <> | Equal to, Not equal to |
| | < , <= , > , >= | Less than, Less than or equal, Greater than, Greater eq. |
| Set Membership | in | Evaluates if value exists in a set [1..5, 10] |
| Logical (Boolean) | and , or , not | Short-circuit logical conjunction, disjunction, negation |
| | xor | Exclusive OR logical evaluation |
| Assignment | := | Standard assignment |
| Compound Assign | += , -= , *= , /= | In-place arithmetic assignment (e.g., Total += LineTotal)|
| String Concatenat.| + | Concatenates text and code strings |
+-------------------+--------------------+----------------------------------------------------------+
Division Operators: / vs. DIV vs. MOD
- Decimal Division (
/): Returns a high-precisionDecimalresult. When dividing integers (17 / 5), the result is implicitly promoted to decimal3.4. - Integer Division (
DIV): Divides two numeric values and returns only the integer portion of the quotient, truncating any fractional remainder (17 DIV 5 = 3). - Modulus (
MOD): Returns the integer remainder resulting from division (17 MOD 5 = 2).
2. Conditional Branching: if...then...else & case...of
Conditional logic routes execution paths based on boolean evaluations.
if...then...else Syntax & The Semicolon Rule
// Single statement branch (No begin...end required)
if Customer."Balance (LCY)" > Customer."Credit Limit (LCY)" then
Customer.Blocked := Customer.Blocked::All
else
Customer.Blocked := Customer.Blocked::" ";
// Compound block branch (begin...end)
if SalesHeader."Document Type" = SalesHeader."Document Type"::Order then begin
CheckInventoryAvailability(SalesHeader);
ReleaseSalesDocument(SalesHeader);
end else begin
ValidateQuoteTerms(SalesHeader);
end;
Exam Trap — Semicolon Placement in
if-then-else: In AL, placing a semicolon immediately after thethenkeyword or on the statement/endimmediately preceding theelsekeyword terminates theifstatement. The compiler will then encounter theelsekeyword in isolation and throw a syntax error reporting that theelsekeyword is not expected in that position.Incorrect:
if A > B then; DoWork();(Semicolon makesDoWork()execute unconditionally!) Incorrect:if A > B then DoWork(); else DoOther();(Semicolon beforeelsecauses compile error!) Correct:if A > B then DoWork() else DoOther();
case...of Statements
The case statement evaluates an expression against a list of matching values or value ranges, providing a clean alternative to deeply nested if-then-else blocks:
local procedure DetermineShippingPriority(DeliveryDays: Integer): Text[20]
begin
case DeliveryDays of
0: // Exact single value match
exit('Same-Day Express');
1, 2: // Multiple comma-separated values
exit('Priority Overnight');
3 .. 5: // Range match (inclusive)
exit('Standard Ground');
else // Fallback catch-all
exit('Economy Freight');
end;
end;
3. Iteration & Looping Constructs
AL supports four looping constructs designed for specific traversal and iteration scenarios:
// 1. repeat...until (Standard Record Traversal Pattern)
if Customer.FindSet() then
repeat
ProcessCustomerAccount(Customer);
until Customer.Next() = 0;
// 2. while...do (Pre-Condition Check Loop)
while (BufferRemaining > 0) and (not EndOfStream) do begin
ReadNextChunk();
BufferRemaining -= 1;
end;
// 3. for...to / for...downto (Counter-Controlled Loops)
for Index := 1 to CustomerList.Count() do begin
ProcessItemByIndex(Index);
end;
for Counter := 10 downto 1 do begin
Countdown(Counter);
end;
// 4. foreach...in (Collection & Array Enumeration)
foreach CurrentEmail in EmailRecipientList do begin
SendNotification(CurrentEmail);
end;
Looping Statement Comparison & Best Practices
| Looping Construct | Condition Evaluation | Execution Guarantee | Primary Use Case in Business Central |
|---|---|---|---|
repeat...until | Post-test (at bottom) | Guaranteed at least once | Navigating database recordsets (Customer.FindSet() ... until Customer.Next() = 0). Must be guarded by if FindSet() to prevent running on empty sets. |
while...do | Pre-test (at top) | Zero or more times | Stream reading and processing where loop condition might be false immediately. |
for...to / downto | Counter bound | Fixed number of iterations | Traversing fixed arrays or numeric ranges with deterministic boundaries. |
foreach...in | Collection enumerator | Zero or more times | Iterating over in-memory List of [T] collections without managing index pointers. |
Loop Control Statements: break and exit
break: Immediately terminates the innermost loop (repeat,while,for,foreach) and resumes execution at the statement following the loop.exit: Immediately exits the active procedure or trigger. If the procedure declares a return value,exit(ReturnValue)returns the specified result to the calling procedure.
4. Built-in String Manipulation Functions
String manipulation functions in AL are essential for preparing user-facing messages, sanitizing input data, and constructing structured payloads.
| Function | Syntax Signature | Description & Practical Example |
|---|---|---|
StrSubstNo | StrSubstNo(FormatString, [Val1], ...) | Replaces placeholders (%1, %2, etc.) with string representations of values.<br/>Msg := StrSubstNo('Invoice %1 total is %2', InvNo, Amount); |
Format | Format(Value, [Length], [FormatNumber]) | Converts any data type (Date, Decimal, Enum) to formatted Text.<br/>DateText := Format(Today(), 0, '<Day,2>/<Month,2>/<Year4>'); |
CopyStr | CopyStr(String, Position, [Length]) | Extracts a substring starting at 1-based Position. Use MaxStrLen to prevent overflow:<br/>CustName20 := CopyStr(LongName, 1, MaxStrLen(CustName20)); |
StrLen | StrLen(String) | Returns the integer character count of the specified string.<br/>Len := StrLen(Customer.Name); |
LowerCase | LowerCase(String) | Converts all characters in the string to lowercase.<br/>Email := LowerCase(RawEmail); |
UpperCase | UpperCase(String) | Converts all characters in the string to uppercase.<br/>CleanCode := UpperCase(InputText); |
DelChr | DelChr(String, [Where], [Which]) | Deletes characters specified in Which. Where flags: '=' (all occurrences), '<' (leading only), '>' (trailing only), '<>' (leading and trailing).<br/>CleanPhone := DelChr(Phone, '=', ' -()'); |
PadStr | PadStr(String, Length, [FillChar]) | Appends or truncates the string with FillChar to reach exact Length.<br/>Padded := PadStr('123', 6, '0'); // '123000' |
StrPos | StrPos(String, SubString) | Returns the 1-based integer index of the first occurrence of SubString, or 0 if not found.<br/>AtPos := StrPos(Email, '@'); |
IncStr | IncStr(String) | Increments the rightmost positive number inside a string identifier.<br/>NextNo := IncStr('SO-0099'); // Returns 'SO-0100' |
SelectStr | SelectStr(Index, CommaSeparatedString) | Retrieves the 1-based nth comma-separated token from a delimited string.<br/>Token := SelectStr(2, 'Red,Green,Blue'); // 'Green' |
Exam Watchout — Safe String Assignment with
CopyStr: Assigning a longerTextvariable to a shorter table field (e.g., assigning a 100-character description to aText[50]field) causes a fatal runtime error: "The length of the string is X, but it must be less than or equal to 50 characters." To avoid runtime crashes, always guard truncation usingCopyStr(SourceText, 1, MaxStrLen(TargetField)).
5. Built-in Date and Time Functions
Business Central applications rely heavily on accurate accounting dates, fiscal periods, and posting validations.
Core System Date Functions
Today(): Returns the current system date from the host server/client operating system.Time(): Returns the current system time.CurrentDateTime(): Returns the current UTCDateTimetimestamp.WorkDate(): Returns or sets the user's active accounting work date configured in My Settings. In Business Central business logic, default transaction and posting dates must always useWorkDate()rather thanToday()so accounting users can post into historical or future open fiscal periods.
Dynamic Date Calculation: CalcDate
The CalcDate function computes a target date by applying a DateFormula expression to a reference date. If no reference date is specified, it calculates against Today().
local procedure CalculatePaymentDueDates(BaseDate: Date)
var
DueDate: Date;
EndOfCurrentMonth: Date;
NextQuarterStart: Date;
begin
// Add 30 calendar days
DueDate := CalcDate('<+30D>', BaseDate);
// Current Month end (CM) plus 10 days
DueDate := CalcDate('<CM+10D>', BaseDate);
// Current Quarter end (CQ) plus 1 day to find start of next quarter
NextQuarterStart := CalcDate('<CQ+1D>', BaseDate);
// Current Year start (-CY)
DueDate := CalcDate('<-CY>', BaseDate);
end;
DateFormula Syntax Codes
D(Day):+10D(add 10 days),-5D(subtract 5 days).W(Week):+2W(add 2 weeks).M(Month):+1M(add 1 calendar month),-3M(subtract 3 months).Q(Quarter):+1Q(add 1 financial quarter).Y(Year):+1Y(add 1 calendar year).C(Current): Prefixed before a time unit to designate the end of the current period (CM= end of current month,CQ= end of current quarter,CY= end of current year). Prefixed with a minus sign (-CM,-CY) to designate the beginning of the period.- Enclosing in Angle Brackets (
<...>): Ensures the date formula is language-invariant and compiles regardless of the tenant's localized language (e.g., using<+1M>instead of localized+1M).
Date Deconstruction & Reconstruction
Date2DMY(Date, What): Extracts the Day (1), Month (2), or Year (3) as an integer (e.g.,MonthInt := Date2DMY(WorkDate(), 2);).DMY2Date(Day, [Month], [Year]): Constructs a validDatevalue from day, month, and year integers (e.g.,FirstDay := DMY2Date(1, 1, 2026);).Date2DWY(Date, What): Extracts Day of the week (1= Monday to7= Sunday), Week number (2), or Year (3).ClosingDate(Date)vs.NormalDate(Date): Converts a normal date to a special closing date (C12/31/2026) used exclusively in financial close entries, and vice versa.
A developer writes the following AL code to evaluate a customer's credit status: if Customer."Credit Limit (LCY)" > 100000 then; Message('VIP Customer') else Message('Standard Customer'); What occurs when the developer attempts to compile this AL code?
An accounting transaction is posted on March 15, 2026. The payment terms specify a calculation formula of '<CM+10D>'. What is the calculated due date returned by CalcDate('<CM+10D>', 2026-03-15D)?
A developer executes the following AL expressions with integer variables: A := 17; B := 5; Q := A DIV B; R := A MOD B; D := A / B;. What are the resulting values stored in Q, R, and D?
An AL developer processes telephone strings using the built-in function call: CleanPhone := DelChr(RawPhone, '=', ' -()');. Given the raw input string ' (555) 019-2834 ', what is the exact string returned by this function?