Everybody loves the power of CalcDate, but what about calculating time? In this video, I set out to create a CalcTime function that gives the same power as CalcDate. Check out the video:

In AL (Business Central’s development language), we have the incredibly useful CalcDate function for date arithmetic — but there’s no equivalent CalcTime for time calculations. In this video, Erik builds a CalcTime function from scratch, exploring how Time variables work under the hood and creating two overloads: one for Time and one for DateTime.
The Problem: No CalcTime in AL
Every AL developer is familiar with CalcDate, which lets you write expressive date formulas like '2W' (two weeks) or '-4D' (minus four days). But when it comes to time calculations — say you need to add 56 hours or 1 hour and 30 minutes — there’s nothing built in. Erik sets out to fix that.
Understanding Time Under the Hood
The key insight is that behind the scenes, a Time variable in AL is simply the number of milliseconds since midnight. With that knowledge, time arithmetic becomes straightforward — once you get past a few quirks of the AL type system.
The Conversion Trick
AL won’t let you directly assign a Time to an Integer. However, it will let you subtract two Time values, which returns an integer (the difference in milliseconds). So to convert a time to an integer, you subtract the midnight constant 000000T:
intTime := Time() - 000000T;
Interestingly, AL allows subtracting times but not adding them — because adding two times could overflow past midnight. However, you can add an integer to the midnight constant to get back to a Time:
t := 000000T + intTime;
Defining Time Units
With milliseconds as the base unit, the time constants are simple multiplication:
Minute := 1000 * 60;
Hour := Minute * 60;
Day := Hour * 24;
This lets you do things like intTime += 5 * Hour to add five hours to a time value.
Building the CalcTime Function
The design follows the same pattern as CalcDate: a string expression where integers are followed by unit postfixes. For example:
1H— one hour-1H— minus one hour56H— fifty-six hours1H30M— one hour and thirty minutes1H2M13S— one hour, two minutes, thirteen seconds
The Parsing Algorithm
The parser walks through each character in the time expression. Digits are accumulated into a string (Op). When a letter is encountered, the accumulated digits are evaluated as an integer, and the appropriate millisecond calculation is applied based on the unit letter:
procedure CalcTime(TimeExpression: Text; StartTime: Time): Time
var
i: Integer;
Op: Text;
OpInt: Integer;
StartTimeInteger: Integer;
begin
StartTimeInteger := StartTime - 000000T;
for i := 1 to strlen(TimeExpression) do begin
if IsChar(TimeExpression[i]) then begin
Evaluate(OpInt, Op);
case TimeExpression[i] of
'H':
StartTimeInteger += OpInt * 60 * 60 * 1000;
'M':
StartTimeInteger += OpInt * 60 * 1000;
'S':
StartTimeInteger += OpInt * 1000;
end;
Op := '';
end else
Op += TimeExpression[i];
end;
StartTimeInteger := StartTimeInteger mod (24 * 60 * 60 * 1000);
exit(000000T + StartTimeInteger);
end;
The mod operation at the end ensures the result wraps around if it exceeds 24 hours, keeping it within valid Time bounds. A small helper function determines whether a character is a letter:
procedure IsChar(c: Char): Boolean
begin
exit(c in ['A' .. 'Z', 'a' .. 'z']);
end;
Handling Negative Values
Erik tests negative values like -21H and discovers that AL handles the arithmetic gracefully — subtracting 21 hours from 20:00 correctly wraps around to just before midnight. The modular arithmetic and AL’s built-in handling of 000000T + intTime take care of this automatically.
The DateTime Overload
The Time overload wraps at midnight, which means adding 56 hours just gives you a time-of-day. To properly handle multi-day offsets, Erik creates a second overload that works with DateTime instead. The key change: instead of using an integer for milliseconds, use AL’s Duration type, which can be added directly to a DateTime:
procedure CalcTime(TimeExpression: Text; StartDateTime: DateTime): DateTime
var
i: Integer;
Op: Text;
OpInt: Integer;
ExpressionDuration: Duration;
begin
for i := 1 to strlen(TimeExpression) do begin
if IsChar(TimeExpression[i]) then begin
Evaluate(OpInt, Op);
case TimeExpression[i] of
'W':
ExpressionDuration += OpInt * 7 * 24 * 60 * 60 * 1000;
'D':
ExpressionDuration += OpInt * 24 * 60 * 60 * 1000;
'H':
ExpressionDuration += OpInt * 60 * 60 * 1000;
'M':
ExpressionDuration += OpInt * 60 * 1000;
'S':
ExpressionDuration += OpInt * 1000;
end;
Op := '';
end else
Op += TimeExpression[i];
end;
exit(StartDateTime + ExpressionDuration);
end;
This version also adds support for days (D) and weeks (W), since crossing day boundaries now makes perfect sense with DateTime. Months are intentionally excluded — they have variable lengths, which would complicate things significantly (and that’s what CalcDate is for).
Testing the DateTime Overload
Erik tests with expressions like 56H, -56H, and 1W4H (one week and four hours). The results correctly span across days and even months — 1W4H from late May correctly lands on June 5th in the early morning hours.
The Complete Extension
Here’s the full source code for the extension, including both overloads and the test trigger:
pageextension 50100 CustomerListExt extends "Customer List"
{
trigger OnOpenPage()
var
t: Time;
intTime: Integer;
Hour: Integer;
Minute: Integer;
Day: Integer;
begin
intTime := Time() - 000000T;
Minute := 1000 * 60;
Hour := Minute * 60;
Day := Hour * 24;
intTime += 5 * Hour;
t := 000000T + intTime;
message('%1 = %2', 'CalcTime', CalcTime('1W4H', CurrentDateTime()));
end;
procedure CalcTime(TimeExpression: Text; StartDateTime: DateTime): DateTime
var
i: Integer;
Op: Text;
OpInt: Integer;
ExpressionDuration: Duration;
begin
for i := 1 to strlen(TimeExpression) do begin
if IsChar(TimeExpression[i]) then begin
Evaluate(OpInt, Op);
case TimeExpression[i] of
'W':
ExpressionDuration += OpInt * 7 * 24 * 60 * 60 * 1000;
'D':
ExpressionDuration += OpInt * 24 * 60 * 60 * 1000;
'H':
ExpressionDuration += OpInt * 60 * 60 * 1000;
'M':
ExpressionDuration += OpInt * 60 * 1000;
'S':
ExpressionDuration += OpInt * 1000;
end;
Op := '';
end else
Op += TimeExpression[i];
end;
exit(StartDateTime + ExpressionDuration);
end;
procedure CalcTime(TimeExpression: Text; StartTime: Time): Time
var
i: Integer;
Op: Text;
OpInt: Integer;
StartTimeInteger: Integer;
begin
StartTimeInteger := StartTime - 000000T;
for i := 1 to strlen(TimeExpression) do begin
if IsChar(TimeExpression[i]) then begin
Evaluate(OpInt, Op);
case TimeExpression[i] of
'H':
StartTimeInteger += OpInt * 60 * 60 * 1000;
'M':
StartTimeInteger += OpInt * 60 * 1000;
'S':
StartTimeInteger += OpInt * 1000;
end;
Op := '';
end else
Op += TimeExpression[i];
end;
StartTimeInteger := StartTimeInteger mod (24 * 60 * 60 * 1000);
exit(000000T + StartTimeInteger);
end;
procedure IsChar(c: Char): Boolean
begin
exit(c in ['A' .. 'Z', 'a' .. 'z']);
end;
}
Key Takeaways
- Time is milliseconds since midnight — this is the fundamental insight that makes time arithmetic possible in AL.
- Subtraction trick — you can’t cast
TimetoIntegerdirectly, butTime - 000000Tgives you the integer value. - Addition trick —
000000T + Integergets you back to aTimevalue. - Two overloads serve different needs — the
Timeoverload wraps at midnight (useful for time-of-day calculations), while theDateTimeoverload usingDurationhandles multi-day spans correctly. - Negative values work — AL handles the modular arithmetic for negative time offsets gracefully.
- Months are excluded by design — variable-length months don’t belong in a time calculation function; that’s what
CalcDatehandles.
This is roughly 100 lines of code that fills a genuine gap in the AL language. Erik mentions considering a pull request to Microsoft’s System App or Base App — if you’d like to see this become a first-class citizen in Business Central, let the community know!