Skip to main content

Time

local Time = Quark.Time

local now = Time.Now("Europe/Berlin")
Console.Log(now:Format("%d.%m.%Y %H:%M"))

local expires = now:Add("7d")
local ban = Time.ParseDuration("2h 30m")

Time.Schedule("0 4 * * *", function()
Chat.BroadcastMessage("Daily restart in 5 minutes")
end, { Zone = "Europe/Berlin" })

Dates with time zones, durations and cron schedules. Time zones come from the system database, with a built-in copy of the IANA database as fallback, so they work the same on Linux and Windows servers.

Dates​

Dates are immutable objects with a time zone. Functions that create dates take an optional zone name like "Europe/Berlin", "UTC" or "Local". Without one the system time zone of the server is used.

Function
Time.Now(zone?)the current date
Time.Date(fields, zone?)date from { Year, Month, Day, Hour, Minute, Second, Millisecond }, only Year is required
Time.FromUnix(seconds, zone?), Time.FromUnixMs(milliseconds, zone?)date from a Unix timestamp
Time.Parse(text, options?)date from text, or nil, message
Time.Unix(), Time.UnixMs()current Unix time in seconds (with fractions) and milliseconds
Time.IsDateTime(value)whether a value is a date

Time.Parse understands ISO 8601 and RFC 3339 ("2026-06-01T10:00:00+02:00", "2026-06-01T08:00:00Z", "2026-06-01 10:00", "2026-06-01"), dates with a zone annotation ("2026-06-01T10:00:00+02:00[Europe/Berlin]") and RFC 2822 ("Mon, 1 Jun 2026 10:00:00 +0000"). Dates with an offset keep it.

Option
Zonezone for texts without offset, and zone the result is converted to
Formatparse with a strftime format like "%d.%m.%Y %H:%M"

Fields and formatting​

Dates have the fields Year, Month, Day, Hour, Minute, Second, Millisecond, Weekday (1 is Monday, 7 is Sunday), YearDay, Zone (the zone name) and Offset (seconds from UTC). :ToTable() returns all of them as a table.

Method
:Format(format, options?)text with strftime directives: %Y-%m-%d %H:%M:%S, %A, %B %-d. Option Locale for month and weekday names in other languages
:ToISO()"2026-06-01T10:00:00+02:00"
:ToRFC2822()"Mon, 1 Jun 2026 10:00:00 +0200", for HTTP and e-mails
tostring(date)ISO with the zone: "2026-06-01T10:00:00+02:00[Europe/Berlin]"
:GetUnix(), :GetUnixMs()Unix timestamp in seconds and milliseconds
:Relative(now?)"in 5 minutes", "3 hours ago", "just now"

With Locale the names of %A, %a, %B and %b are written in that language: de, en, es, fr, it, nl, pl, pt, ru, sv, tr or cs, regional tags like de-AT use their language. Only the names change, the order of the parts is up to the format. Month names are in their standalone form, which reads naturally in headings and lists but not always after a day in languages with grammatical cases like Russian or Polish.

Time.Now("Europe/Berlin"):Format("%A, %-d. %B %Y", { Locale = "de" }) -- "Montag, 1. Juni 2026"

Calculating​

Method
:Add(duration), :Subtract(duration)new date. The duration is milliseconds, a text like "1h 30m" or a table { Years, Months, Weeks, Days, Hours, Minutes, Seconds, Milliseconds }
:Since(other, unit?)time since another date, in milliseconds or as number in Years, Months, Weeks, Days, Hours, Minutes or Seconds
:With(fields)new date with some fields changed
:StartOf(unit), :EndOf(unit)first and last millisecond of the Year, Month, Week (starting Monday), Day, Hour, Minute or Second
:InZone(zone), :ToUTC()the same moment in another time zone
:IsBefore(other), :IsAfter(other), ==, <, <=compare moments, independent of the zone

Days, weeks, months and years follow the calendar of the date's zone: adding one day keeps the time of day across daylight saving changes, while adding 24 hours does not, and adding one month to January 31 gives the last day of February.

local season_end = Time.Now("UTC"):EndOf("Month")
local days_left = math.floor(season_end:Since(Time.Now("UTC"), "Days"))

Time zones​

Time.IsValidZone(name) checks a zone name, Time.GetZones() lists all known zones and Time.GetSystemZone() returns the zone of the server.

Durations​

Time.ParseDuration(text) converts a duration to milliseconds, or returns nil, message. It accepts "90s", "1h 30m", "2 days 3 hours", "1.5h" and ISO 8601 durations like "PT1H30M". Days are 24 hours and weeks 7 days, months and years are rejected because their length varies.

Time.FormatDuration(duration, options?) turns milliseconds (or a duration text) into readable text:

Option
StyleShort gives "1h 30m" (default), Long gives "1 hour, 30 minutes", Clock gives "1:30:00"
Largestmaximum number of units, { Largest = 2 } turns 1d 2h 3m into 1d 2h
local duration = Time.ParseDuration(arguments[2])
if not duration then
return Chat.SendMessage(player, "Usage: /ban <player> <duration>, like 2h or 7d")
end
Chat.BroadcastMessage(target:GetName() .. " was banned for " .. Time.FormatDuration(duration, { Style = "Long" }))

Cron​

Cron expressions describe repeating times. They have five fields, or six with seconds first:

┌───────────── second (0-59, optional)
│ ┌─────────── minute (0-59)
│ │ ┌───────── hour (0-23)
│ │ │ ┌─────── day of the month (1-31, or L for the last day)
│ │ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌─── day of the week (0-7 or SUN-SAT, 0 and 7 are Sunday)
* * * * * *

Fields accept *, lists (1,15), ranges (MON-FRI) and steps (*/15, 9-17/2). When both day fields are set, a day matches if either does, like in classic cron. The macros @yearly, @monthly, @weekly, @daily and @hourly are shortcuts.

Expression
0 4 * * *every day at 04:00
*/15 * * * *every 15 minutes
0 18 * * FRIFridays at 18:00
0 0 L * *midnight on the last day of every month
*/10 * * * * *every 10 seconds

Time.Cron(expression, options?) parses an expression, or returns nil, message. The option Zone sets the time zone the expression is evaluated in (system zone).

Method
:Next(after?)the next matching date after a date (now), nil if there is none in the next 8 years
:NextMany(count, after?)list of the next dates
:Matches(date)whether a date matches
:GetExpression(), :GetZone()

Times that do not exist because the clock jumps forward for daylight saving run once right after the jump, times that exist twice run once.

Schedules​

Time.Schedule(expression, callback, options?) calls the callback with the scheduled date at every matching time, or returns nil, message for invalid expressions. It takes the same Zone option.

Method
:Stop()stops the schedule
:IsActive()whether it still runs
:GetNext()the next time it runs, nil when stopped
:GetExpression()

Schedules keep running when the returned object is not kept, and stop when the package unloads. They follow the wall clock, so changing the system time or a daylight saving change does not shift them.

local restart = Time.Schedule("55 3 * * *", function(time)
Chat.BroadcastMessage("The server restarts in 5 minutes")
end, { Zone = "Europe/Berlin" })

Console.Log("Next restart warning: " .. restart:GetNext():Format("%H:%M"))