std.time — Zeit, Datum und Timer
Drei Aufgabenbereiche in einer Unit: Kalenderrechnung (Datum ↔ Tageszahl, Schaltjahre, Wochentag), Uhrzeit lesen aus verschiedenen Systemuhren und Warten — von einfachem Sleep bis zu Kernel-Timern über Dateideskriptoren, die sich in eine epoll-Schleife einhängen lassen.
Alle Zeitquellen sind Syscalls (clock_gettime, nanosleep, timerfd_*); es wird keine C-Bibliothek gebraucht, das Programm bleibt statisch linkbar.
Einsatzbereiche: Zeitstempel für Protokolle und Dateiformate, Laufzeitmessung, Scheduling, Rate-Limiting, Timeouts.
Autor: Andreas Röne
Copyright: 2024–2025 Andreas Röne
Quelle: std/time.lyx
<WRAP info>
Die frühere Einheitenfalle (#1465) ist behoben: Die Zeitkonstanten tragen ihre Einheit jetzt im Namen — US_SECOND … US_DAY für Mikrosekunden, MS_SECOND … MS_DAY für Millisekunden. Sleep(MS_SECOND) wartet damit tatsächlich eine Sekunde.
</WRAP>
Import
import std.time;
Typen
Alle vier Typen sind int64-Aliase — sie dokumentieren die Bedeutung, der Compiler unterscheidet sie nicht. Ein date lässt sich also versehentlich dort einsetzen, wo ein datetime erwartet wird.
| Typ | Bedeutung | Nullpunkt |
|---|---|---|
date | Tage | 1970-01-01 = 0 |
time | Sekunden seit Mitternacht | 00:00:00 = 0 |
datetime | Unix-Sekunden | 1970-01-01 00:00:00 UTC |
timestamp | Unix-Mikrosekunden | dito |
TimeZone ist ein Struct aus name: pchar und offset_seconds: int64.
Weil date eine schlichte Tageszahl ist, rechnet man mit Datumsangaben ganz normal: tag + 7 ist eine Woche später, endeJahr - tag die Zahl der verbleibenden Tage.
Kalenderrechnung
| Signatur | Beschreibung |
|---|---|
DateFromYmd(year, month, day: int64): date | Datum in Tageszahl |
YearFromDate(d: date): int64 | Jahr aus Tageszahl |
MonthFromDate(d: date): int64 | Monat (1–12) |
DayFromDate(d: date): int64 | Tag im Monat |
DaysFromCivil(year, month, day: int64): int64 | Rechenkern hinter DateFromYmd |
CivilYearFromDays(days: int64): int64 | Rechenkern hinter YearFromDate |
CivilMonthFromDays(days: int64): int64 | Rechenkern hinter MonthFromDate |
CivilDayFromDays(days: int64): int64 | Rechenkern hinter DayFromDate |
DayOfWeekFromDate(d: date): int64 | Wochentag, 0 = Sonntag bis 6 = Samstag |
IsLeapYear(y: int64): bool | Schaltjahr nach gregorianischer Regel |
DaysInMonth(year, month: int64): int64 | Tage im Monat, Schaltjahr berücksichtigt |
Die Civil*-Funktionen sind das gleiche wie die *FromDate-Varianten — letztere sind nur dünne Hüllen. Negative Tageszahlen (vor 1970) und weit zurückliegende Jahre funktionieren; geprüft bis Jahr 1.
Uhrzeit innerhalb eines Tages
| Signatur | Beschreibung |
|---|---|
TimeFromHms(hour, minute, second: int64): time | Sekunden seit Mitternacht |
HourFromTime(t: time): int64 | Stunde |
MinuteFromTime(t: time): int64 | Minute |
SecondFromTime(t: time): int64 | Sekunde |
Umrechnung zwischen den Typen
| Signatur | Beschreibung |
|---|---|
DatetimeFromUnixSeconds(sec: int64): datetime | Identität — dient der Lesbarkeit |
DatetimeToUnixSeconds(dt: datetime): int64 | Identität |
TimestampFromUnixSeconds(sec: int64): timestamp | Sekunden → Mikrosekunden |
UnixSecondsFromTimestamp(ts: timestamp): int64 | Mikrosekunden → Sekunden |
Uhrzeit lesen
| Signatur | Uhr | Einheit |
|---|---|---|
Now(): datetime | CLOCK_REALTIME | Sekunden |
NowMs(): int64 | CLOCK_REALTIME | Millisekunden |
NowUs(): timestamp | CLOCK_REALTIME | Mikrosekunden |
GetUnixMs(): int64 | gettimeofday | Millisekunden |
GetTimeNs(clockId: int64): int64 | frei wählbar | Nanosekunden |
GetTimeMs(): int64 | CLOCK_MONOTONIC | Millisekunden |
GetTimeOfDay(tvSecOut, tvUsecOut: int64): int64 | gettimeofday | schreibt Sekunden und Mikrosekunden in zwei Puffer |
| Uhr | Wert | Eigenschaft |
|---|---|---|
CLOCK_REALTIME | 0 | Wanduhrzeit; springt bei Zeitumstellung und NTP-Korrektur |
CLOCK_MONOTONIC | 1 | läuft seit Systemstart gleichmäßig vorwärts, springt nie; die richtige Wahl für Zeitmessungen |
CLOCK_PROCESS_CPUTIME_ID | 2 | verbrauchte CPU-Zeit des Prozesses |
CLOCK_THREAD_CPUTIME_ID | 3 | verbrauchte CPU-Zeit des Threads |
CLOCK_MONOTONIC_RAW | 4 | wie MONOTONIC, ohne NTP-Feinregelung |
CLOCK_BOOTTIME | 7 | wie MONOTONIC, zählt Suspend-Zeiten mit |
Der Unterschied ist messbar: auf einem System, das zwischendurch im Ruhezustand war, meldete CLOCK_MONOTONIC 81561 Sekunden, CLOCK_BOOTTIME dagegen 93227 — passend zu /proc/uptime.
Warten
| Signatur | Einheit | Beschreibung |
|---|---|---|
Sleep(ms: int64): int64 | Millisekunden | Rückgabe 0 bei Erfolg, negativer Fehlercode sonst |
SleepNs(ns: int64): int64 | Nanosekunden | dito |
SleepUntil(clockId, absNs: int64): int64 | absolute Nanosekunden | schläft bis zu einem Zeitpunkt statt einer Dauer |
Sleep(0) kehrt sofort zurück, Sleep(-5) liefert -22 (EINVAL) ohne zu warten. Ein Signal kann den Schlaf vorzeitig beenden — dann kommt ein negativer Wert zurück, und die restliche Zeit ist nicht abgewartet.
SleepUntil ist die richtige Wahl für gleichmäßige Takte: bei wiederholtem Sleep(100) summiert sich die Bearbeitungszeit zwischen den Aufrufen auf, bei absoluten Zielzeitpunkten nicht.
Hilfskonstanten
| Konstante | Wert | Bedeutung |
|---|---|---|
MS_SECOND | 1000 | eine Sekunde in Millisekunden — passt zu Sleep |
MS_MINUTE | 60000 | eine Minute in Millisekunden |
MS_HOUR | 3600000 | eine Stunde in Millisekunden |
MS_DAY | 86400000 | ein Tag in Millisekunden |
US_SECOND | 1000000 | eine Sekunde in Mikrosekunden — passt zu timestamp |
US_MINUTE | 60000000 | eine Minute in Mikrosekunden |
US_HOUR | 3600000000 | eine Stunde in Mikrosekunden |
US_DAY | 86400000000 | ein Tag in Mikrosekunden |
NS_PER_MS | 1000000 | Nanosekunden je Millisekunde — für SleepNs |
NS_PER_SEC | 1000000000 | Nanosekunden je Sekunde |
US_PER_SEC | 1000000 | Mikrosekunden je Sekunde |
Das Präfix nennt die Einheit: MS_* für Sleep, NS_* für SleepNs und SleepUntil, US_* für Werte vom Typ timestamp.
Kernel-Timer (timerfd)
Ein timerfd ist ein Timer, der sich wie eine Datei verhält: Er wird lesbar, wenn er abläuft. Damit lässt er sich zusammen mit Sockets in einer epoll-Schleife überwachen — der übliche Weg, Timeouts und periodische Aufgaben in einem Server unterzubringen, ohne einen eigenen Thread dafür zu opfern.
| Signatur | Beschreibung |
|---|---|
TimerFdCreate(clockId, flags: int64): int64 | Erzeugt den Timer, liefert einen fd |
TimerFdSetTime(fd, intervalNs, valueNs: int64): int64 | Stellt ihn: valueNs bis zum ersten Ablauf, intervalNs als Wiederholung (0 = einmalig) |
TimerFdGetTime(fd: int64): int64 | Verbleibende Nanosekunden bis zum nächsten Ablauf |
TimerFdRead(fd: int64): int64 | Blockiert bis zum Ablauf, liefert die Zahl der seither vergangenen Abläufe |
| Flag | Wert | Bedeutung |
|---|---|---|
TFD_CLOEXEC | 524288 | fd bei execve schließen |
TFD_NONBLOCK | 2048 | TimerFdRead blockiert nicht |
TFD_TIMER_ABSTIME | 1 | Zeitangabe absolut statt relativ |
TFD_TIMER_CANCEL_ON_SET | 2 | Timer abbrechen, wenn die Systemzeit gestellt wird |
Der Rückgabewert von TimerFdRead ist wichtig: Wenn das Programm mit der Bearbeitung nicht hinterherkommt, liefert er die Zahl der verpassten Abläufe. Eine Uhr, die 3 zurückbekommt, hat zwei Takte übersprungen.
Beispiele
Mit Datumsangaben rechnen
import std.time;
fn main(): int64 {
// Datum zerlegen und wieder zusammensetzen
var tag: date := DateFromYmd(2026, 8, 13);
PrintLn("2026-08-13 = Tag ", IntToStr(tag), " seit 1970-01-01");
PrintLn("zurueck: ", IntToStr(YearFromDate(tag)), "-",
IntToStr(MonthFromDate(tag)), "-", IntToStr(DayFromDate(tag)));
// Wochentag (0 = Sonntag)
PrintLn("Wochentag: ", IntToStr(DayOfWeekFromDate(tag)), " (4 = Donnerstag)");
// Kalenderrechnen ist Ganzzahlrechnen
var inEinerWoche: date := tag + 7;
PrintLn("in einer Woche: ", IntToStr(YearFromDate(inEinerWoche)), "-",
IntToStr(MonthFromDate(inEinerWoche)), "-", IntToStr(DayFromDate(inEinerWoche)));
PrintLn("Tage bis Jahresende: ", IntToStr(DateFromYmd(2026, 12, 31) - tag));
// Schaltjahre und Monatslaengen
PrintLn("2024 Schaltjahr? ", IntToStr(IsLeapYear(2024) as int64),
" 1900? ", IntToStr(IsLeapYear(1900) as int64),
" 2000? ", IntToStr(IsLeapYear(2000) as int64));
PrintLn("Februar 2024 hat ", IntToStr(DaysInMonth(2024, 2)), " Tage");
return 0;
}
Ausgabe:
2026-08-13 = Tag 20678 seit 1970-01-01
zurueck: 2026-8-13
Wochentag: 4 (4 = Donnerstag)
in einer Woche: 2026-8-20
Tage bis Jahresende: 140
2024 Schaltjahr? 1 1900? 0 2000? 1
Februar 2024 hat 29 Tage
Der Rundlauf Datum → Tageszahl → Datum wurde über den ganzen Bereich geprüft, einschließlich 1969-12-31 (Tag −1), Schalttagen und Jahr 1.
Aktuelle Zeit in Datum und Uhrzeit zerlegen
import std.time;
fn main(): int64 {
var jetzt: datetime := Now();
PrintLn("Unix-Sekunden: ", IntToStr(jetzt));
// Unix-Sekunden in Datum und Uhrzeit zerlegen (UTC)
var tage: int64 := jetzt / 86400;
var restSek: int64 := jetzt % 86400;
PrintLn("Datum (UTC): ", IntToStr(CivilYearFromDays(tage)), "-",
IntToStr(CivilMonthFromDays(tage)), "-", IntToStr(CivilDayFromDays(tage)));
PrintLn("Uhrzeit (UTC): ", IntToStr(HourFromTime(restSek)), ":",
IntToStr(MinuteFromTime(restSek)), ":", IntToStr(SecondFromTime(restSek)));
// Ortszeit: Offset selbst waehlen — Sommerzeit wird nicht erkannt
var lokal: datetime := ApplyTimeZone(jetzt, ZoneCEST());
var lr: int64 := lokal % 86400;
PrintLn("Uhrzeit (CEST): ", IntToStr(HourFromTime(lr)), ":", IntToStr(MinuteFromTime(lr)));
return 0;
}
Ausgabe (Gegenprobe mit date -u in derselben Sekunde):
Unix-Sekunden: 1786647280
Datum (UTC): 2026-8-13
Uhrzeit (UTC): 18:54:40
Uhrzeit (CEST): 20:54
Referenz UTC: 2026-08-13 18:54
Das Zerlegen läuft über / 86400 und % 86400 — es gibt keine fertige Funktion dafür. Für negative Unix-Zeiten (vor 1970) müsste man den Rest zusätzlich korrigieren, weil % in Lyx dem Vorzeichen des Dividenden folgt.
Laufzeit messen und warten
import std.time;
fn main(): int64 {
// Laufzeit messen: immer die MONOTONE Uhr, nie CLOCK_REALTIME
var start: int64 := GetTimeNs(CLOCK_MONOTONIC);
var summe: int64 := 0;
var i: int64 := 0;
while (i < 3000000) { summe := summe + i; i := i + 1; }
var dauer: int64 := GetTimeNs(CLOCK_MONOTONIC) - start;
PrintLn("Schleife: ", IntToStr(dauer / NS_PER_MS), " ms (", IntToStr(dauer), " ns)");
PrintLn("Summe = ", IntToStr(summe));
// Schlafen
var a: int64 := GetTimeMs();
Sleep(150);
PrintLn("Sleep(150) dauerte ", IntToStr(GetTimeMs() - a), " ms");
var b: int64 := GetTimeMs();
SleepNs(50 * NS_PER_MS);
PrintLn("SleepNs(50 ms) dauerte ", IntToStr(GetTimeMs() - b), " ms");
return 0;
}
Ausgabe:
Schleife: 36 ms (36095621 ns)
Summe = 4499998500000
Sleep(150) dauerte 150 ms
SleepNs(50 ms) dauerte 50 ms
Für Messungen ist CLOCK_MONOTONIC zwingend: CLOCK_REALTIME kann durch NTP oder eine Zeitumstellung springen und dann negative Differenzen liefern.
Periodischer Kernel-Timer
import std.time;
fn main(): int64 {
// Wiederholender Kernel-Timer: alle 100 ms
var fd: int64 := TimerFdCreate(CLOCK_MONOTONIC, 0);
if (fd < 0) { PrintLn("timerfd nicht verfuegbar"); return 1; }
TimerFdSetTime(fd, 100 * NS_PER_MS, 100 * NS_PER_MS);
PrintLn("Timer laeuft, erster Ablauf in ", IntToStr(TimerFdGetTime(fd) / NS_PER_MS), " ms");
var start: int64 := GetTimeNs(CLOCK_MONOTONIC);
var i: int64 := 0;
while (i < 3) {
var n: int64 := TimerFdRead(fd); // blockiert bis zum Ablauf
var t: int64 := (GetTimeNs(CLOCK_MONOTONIC) - start) / NS_PER_MS;
PrintLn(" Tick ", IntToStr(i + 1), ": ", IntToStr(n), " Ablauf nach ", IntToStr(t), " ms");
i := i + 1;
}
return 0;
}
Ausgabe:
Timer laeuft, erster Ablauf in 99 ms
Tick 1: 1 Ablauf nach 99 ms
Tick 2: 1 Ablauf nach 199 ms
Tick 3: 1 Ablauf nach 300 ms
Die Takte bleiben auf dem 100-ms-Raster — der Kernel rechnet vom ursprünglichen Startzeitpunkt aus, nicht vom letzten Lesen. Mit TFD_NONBLOCK und std.net.epoll lässt sich derselbe fd in eine Ereignisschleife neben Netzwerkverbindungen hängen.
Fallstricke
Geprüft mit lyxc 1.0.21A.
Die Einheit steht im Namen
Sleep(MS_SECOND); // eine Sekunde
Sleep(MS_SECOND / 10); // 100 ms — gemessen: 100 ms
SleepNs(1 * NS_PER_SEC); // dieselbe Sekunde in Nanosekunden
SleepNs(500 * NS_PER_MS); // eine halbe Sekunde
Vor dem Fix von #1465 hießen die Konstanten SECOND, MINUTE, HOUR und DAY und standen für Mikrosekunden — Sleep(SECOND) wartete deshalb gut 16 Minuten statt einer Sekunde. Die alten Namen gibt es nicht mehr; Code, der sie verwendet, meldet jetzt undefined symbol statt still zu hängen.
ApplyTimeZone kennt keine Sommerzeit
ApplyTimeZone addiert schlicht den Offset. Welche Zone gerade gilt, muss der Aufrufer selbst entscheiden — auch ZoneByCode liefert nur feste Versätze, keine Umschaltautomatik: Standard- und Sommerzeit haben eigene Kürzel (CET/CEST). Es gibt keine Zeitzonendatenbank und keine Auswertung von TZ oder /etc/localtime.
Für Protokolle und Dateiformate ist das kein Nachteil: dort schreibt man ohnehin UTC. Erst bei der Anzeige für Menschen braucht es die richtige Zone, und die muss von außen kommen.
Mitgeliefert sind seit #1600 alle 117 gebräuchlichen Zonen: ZoneByCode(„NPT“c) liefert die Zone zum Kürzel, ZoneCodeKnown sagt, ob es das Kürzel gibt, ZoneMake(name, offset) baut eine eigene. Dazu 33 Versatz-Konstanten (TZ_OFF_PLUS_0545 = 20700 usw.), die elf fehlenden Sommerzeiten (ZoneEest, ZoneNzdt, …) und eindeutig benannte Funktionen für die drei doppelt belegten Kürzel (ZoneIndia/ZoneIsrael, ZoneGulf/ZoneSouthGeorgia, ZoneIndochina/ZoneChagos). Die vollständige Liste steht in der Zeitzonen-Tabelle.
Zweiter Punkt: Das Ergebnis von ApplyTimeZone ist wieder ein datetime — also formal Unix-Sekunden, tatsächlich aber verschobene Ortszeit. Wer diesen Wert weiterreicht, transportiert eine Zeit ohne Zonenangabe. Deshalb erst unmittelbar vor der Ausgabe umrechnen.
Die Typen schützen nicht
date, time, datetime und timestamp sind allesamt int64. Ein datetime (Unix-Sekunden, derzeit ~1,79 Milliarden) an YearFromDate übergeben — das eine Tageszahl erwartet — liefert kein Fehlersignal, sondern ein Jahr in ferner Zukunft. Die Bedeutung steckt allein in der Benennung.
Sleep kann vorzeitig enden
Ein Signal unterbricht nanosleep. Sleep gibt dann einen negativen Wert zurück und die restliche Zeit ist nicht abgewartet. Wer eine Mindestwartezeit braucht, prüft den Rückgabewert und wiederholt — oder nimmt SleepUntil mit einem absoluten Zielzeitpunkt, das sich gefahrlos wiederholen lässt.
Kein Zeitformat, kein Parser
Die Unit erzeugt keine Zeichenketten und liest keine. Für ISO-8601, RFC 2822 oder eigene Formate ist std.datetime zuständig.
Verwandte Units
- std.datetime — Zeitangaben formatieren und einlesen
- std.net.epoll —
timerfdin einer Ereignisschleife überwachen - std.os — Prozess- und Systeminformationen
