====== Zeit & Datum ====== Zeitpunkte bilden, formatieren, einlesen, Differenzen rechnen und prüfen, ob eine Frist abgelaufen ist. Alle Beispiele sind mit ''lyxc 1.1.0A'' übersetzt und ausgeführt; die gezeigten Ausgaben sind die echten. → [[lyx_-_programmiersprache:units:time|std.time]] · [[lyx_-_programmiersprache:units:datetime|std.datetime]] · [[lyx_-_programmiersprache:guides:welche-unit|Welche Unit?]] ---- ===== Zwei Units, klare Arbeitsteilung ===== ^ ^ ''std.time'' ^ ''std.datetime'' ^ | Ebene | untere | obere | | Typen | ''date'', ''time'', ''datetime'', ''timestamp'' | rechnet auf ''int64'' | | Kann | Zeitpunkt holen, Kalenderrechnung, Zeitzonen, ''Sleep'', Timer | Formatieren, Einlesen, Datumsarithmetik, Prüfen | | Wofür | „wie spät ist es", „wie viele Tage bis…", „warte 100 ms" | „als Text ausgeben", „Text einlesen", „plus drei Monate" | **Die beiden lassen sich nicht gemeinsam importieren.** Beide exportieren ''DaysInMonth'': sema error: mehrdeutiges Symbol 'DaysInMonth' — exportiert von 'std.time' und 'std.datetime' Das trifft auch **transitiv**: ''std.cloud.s3'' zieht über ''std.cloud.aws.transport'' das ''std.time'' mit — ein S3-Programm kann ''std.datetime'' deshalb nicht importieren, obwohl im Quelltext nichts darauf hindeutet. Einen Ausweg über Alias oder qualifizierten Zugriff gibt es nicht (→ [[lyx_-_programmiersprache:sprache:module-und-importe|Module & Importe]]). **Also: eine der beiden wählen.** Wer beides braucht, trennt den Code in zwei Units. ---- ===== Die Typen von ''std.time'' ===== Alle vier sind ''int64''-Aliase — die Namen sagen, was der Wert **bedeutet**: ^ Typ ^ Einheit ^ Nullpunkt ^ | ''date'' | Tage | 1970-01-01 | | ''time'' | Sekunden | Mitternacht | | ''datetime'' | Sekunden | Unix-Epoche | | ''timestamp'' | **Mikrosekunden** | Unix-Epoche | Weil es Aliase sind, warnt der Compiler nicht, wenn man sie verwechselt. Ein ''timestamp'', der als ''datetime'' gelesen wird, liegt um den Faktor eine Million daneben. ---- ===== Einen Zeitpunkt holen ===== unit main; import std.io; import std.string; import std.time; fn main(): int64 { Print("Now() Unix-Sekunden : "); PrintLn(IntToStr(Now() as int64)); Print("NowMs() : "); PrintLn(IntToStr(NowMs())); Print("GetTimeMs() : "); PrintLn(IntToStr(GetTimeMs())); return 0; } Now() Unix-Sekunden : 1786940893 NowMs() : 1786940893001 GetTimeMs() : 281969849 > **Die dritte Zahl ist kleiner — und das ist der Punkt.** ''Now()'' und ''NowMs()'' lesen die **Wanduhr** (Sekunden seit 1970), ''GetTimeMs()'' die **monotone Uhr** (seit Systemstart). > > Die Wanduhr kann springen: NTP korrigiert sie, Sommerzeit verschiebt sie, der Benutzer stellt sie. Für „wie lange hat das gedauert" und „ist die Frist abgelaufen" ist sie deshalb **falsch** — dort gehört die monotone Uhr hin. Umgekehrt taugt die monotone Uhr nicht zum Speichern: ihr Nullpunkt ändert sich beim nächsten Neustart. ---- ===== Ist eine Zeit vergangen? ===== unit main; import std.io; import std.string; import std.time; fn main(): int64 { var t0: int64 := GetTimeMs(); // monotone Uhr Sleep(120); var t1: int64 := GetTimeMs(); Print("nach Sleep(120): "); Print(IntToStr(t1 - t0)); PrintLn(" ms vergangen"); var frist: int64 := t0 + 100; if (t1 >= frist) { PrintLn("Frist von 100 ms ist abgelaufen"); } else { PrintLn("Frist laeuft noch"); } return 0; } nach Sleep(120): 120 ms vergangen Frist von 100 ms ist abgelaufen **Das Muster ist immer dasselbe:** Startzeit merken, Frist als ''start + dauer'' ausrechnen, mit ''>='' vergleichen. Niemals die Differenz gegen die Wanduhr bilden. ^ Genauigkeit ^ Funktion ^ | Millisekunden | ''GetTimeMs()'' | | Nanosekunden | ''GetTimeNs(CLOCK_MONOTONIC)'' | | Mikrosekunden, Wanduhr | ''NowUs()'' | Für periodische Aufgaben gibt es ''SleepUntil(clockId, absNs)'' — es schläft bis zu einem **absoluten** Zeitpunkt und vermeidet damit das Driften, das bei wiederholtem ''Sleep(interval)'' entsteht. Wer einen Dateideskriptor braucht, den ''Poll'' überwachen kann, nimmt ''TimerFdCreate''. ---- ===== Kalenderrechnung mit ''std.time'' ===== Für „welcher Wochentag", „wie viele Tage bis" braucht es keine Uhrzeit — dafür ist ''date'' da: unit main; import std.io; import std.string; import std.time; fn Z(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); } fn main(): int64 { var d: date := DateFromYmd(2026, 8, 17); Z("date als Tageszahl : ", d as int64); Z(" Jahr : ", YearFromDate(d)); Z(" Monat : ", MonthFromDate(d)); Z(" Tag : ", DayFromDate(d)); Z(" Wochentag (0=So) : ", DayOfWeekFromDate(d)); var d2: date := DateFromYmd(2026, 12, 24); Z("Tage bis Heiligabend: ", (d2 as int64) - (d as int64)); Z("IsLeapYear(2024) : ", IsLeapYear(2024) as int64); Z("IsLeapYear(2100) : ", IsLeapYear(2100) as int64); return 0; } date als Tageszahl : 20682 Jahr : 2026 Monat : 8 Tag : 17 Wochentag (0=So) : 1 Tage bis Heiligabend: 129 IsLeapYear(2024) : 1 IsLeapYear(2100) : 0 **Ein ''date'' ist eine Tageszahl** — die Differenz zweier Daten ist deshalb schlicht eine Subtraktion. Das ist der bequemste Weg für Fristen in Tagen, und er kennt keine Sommerzeitprobleme. ''IsLeapYear(2100)'' ist ''0'': durch 100 teilbar, aber nicht durch 400. ---- ===== Zeitzonen ===== var utc: datetime := DatetimeFromUnixSeconds(1786977000); var cet: datetime := ApplyTimeZone(utc, ZoneCEST()); UTC : 1786977000 CEST (UTC+2) : 1786984200 Unterschied in s : 7200 ''ApplyTimeZone'' **verschiebt den Wert** um den Zonenversatz. Fertig mitgeliefert sind seit [[https://github.com/SEOLizer/LyX-Compiler/issues/1600|#1600]] alle 117 gebräuchlichen Zonen — ''ZoneByCode("CEST"c)'' statt Selbstbau; ''ZoneUTC()'', ''ZoneCET()'' und ''ZoneCEST()'' gibt es weiterhin. ==== Andere Zonen: selbst bauen ==== ''TimeZone'' ist ein gewöhnliches Struct aus ''name: pchar'' und ''offset_seconds: int64'' — die drei Funktionen oben füllen es nur aus. Jede andere Zone ist deshalb vier Zeilen Arbeit: unit main; import std.io; import std.string; import std.time; fn main(): int64 { var utc: datetime := DatetimeFromUnixSeconds(1786977000); var ist: datetime := ApplyTimeZone(utc, ZoneByCode("IST"c)); // Indien, +05:30 Print("UTC : "); PrintLn(IntToStr(utc as int64)); Print("IST +05:30 : "); PrintLn(IntToStr(ist as int64)); Print("Differenz : "); PrintLn(IntToStr((ist as int64) - (utc as int64))); return 0; } UTC : 1786977000 IST +05:30 : 1786996800 Differenz : 19800 Halbe und dreiviertel Stunden sind dabei kein Sonderfall — der Versatz sind schlicht Sekunden. Indien +05:30 = 19800, Nepal +05:45 = 20700, Neufundland −03:30 = −12600. Wer sie doch selbst hinschreibt, nimmt die Konstanten: ''TZ_OFF_PLUS_0530'', ''TZ_OFF_PLUS_0545''. **Die Versätze aller 117 Zonen stehen fertig in der [[lyx_-_programmiersprache:units:time:zeitzonen|Zeitzonen-Tabelle]]** — von ''SST'' bei −11:00 bis ''LINT'' bei +14:00, jeweils schon in Sekunden. Dort auch die drei Kürzel, die doppelt belegt sind (''IST'', ''GST'', ''ICT''): **den Versatz speichern, nicht das Kürzel.** ==== Zone aus der Umgebung: ''GetTimezoneOffset'' ==== ''std.datetime'' geht einen Schritt weiter und liest die Umgebungsvariable ''TZ'' im POSIX-Format: ^ ''TZ'' ^ ''GetTimezoneOffset()'' ^ ''FormatTimezoneOffset'' ^ | //nicht gesetzt// | 0 | ''+00:00'' | | ''CET-1'' | 3600 | ''+01:00'' | | ''EST5'' | −18000 | ''-05:00'' | | ''IST-5:30'' | 19800 | ''+05:30'' | | ''NPT-5:45'' | 20700 | ''+05:45'' | | ''CET-1CEST,M3.5.0,M10.5.0/3'' | 3600 | ''+01:00'' | **Zwei Dinge sind an dieser Tabelle wichtig.** Erstens das **POSIX-Vorzeichen**: es zählt //westlich// von Greenwich positiv, also genau andersherum, als man es schreibt. ''CET-1'' — mit Minus — ergibt **+1 Stunde**. ''GetTimezoneOffset'' dreht das um und liefert bereits die übliche Zählung („östlich von UTC positiv"), sodass der Rückgabewert direkt addiert werden kann. Zweitens die **letzte Zeile**: die vollständige Angabe mit Sommerzeitregel liefert ''3600'', nicht ''7200'' — die Regel hinter dem Zonennamen (''M3.5.0,M10.5.0/3'') wird **nicht ausgewertet**. Zurück kommt der Standardversatz der Zone, nie die gerade geltende Ortszeit. > **Es gibt keine Zeitzonendatenbank und keine automatische Sommerzeit-Umschaltung.** Weder ''ZoneByCode'' noch ''GetTimezoneOffset'' wissen, welche der beiden gerade gilt; ''/etc/localtime'' wird nirgends gelesen. Wer echte Ortszeit braucht, muss die Umschaltregel selbst anwenden. > > **Intern immer in UTC rechnen** und erst bei der Ausgabe umrechnen — das erspart die ganze Klasse von Fehlern, bei denen eine Stunde doppelt oder gar nicht vorkommt. > > Und: ''ApplyTimeZone'' liegt in ''std.time'', ''GetTimezoneOffset'' in ''std.datetime''. Wegen der Kollision oben sind beide nicht gemeinsam nutzbar — auf der ''std.datetime''-Seite ist die Verschiebung aber ohnehin eine Addition: ''dt + GetTimezoneOffset()''. ---- ===== Formatieren mit ''std.datetime'' ===== unit main; import std.io; import std.string; import std.alloc; import std.datetime; fn S(t: pchar, b: int64): void { Print(t); PrintLn(b as pchar); } fn main(): int64 { var buf: int64 := alloc(64); var t: int64 := FromUnixTimestamp(1786977000); // 2026-08-17 14:30 UTC FormatIso(t, buf as pchar); S("FormatIso : ", buf); FormatRfc2822(t, buf as pchar); S("FormatRfc2822 : ", buf); Format(t, "%Y-%m-%d %H:%M"c, buf as pchar); S("Format Y-m-d H:M : ", buf); FormatDuration(93784, buf as pchar); S("Dauer 93784 s : ", buf); FormatDurationShort(93784, buf as pchar); S(" kurz : ", buf); FormatDuration(45, buf as pchar); S("Dauer 45 s : ", buf); return 0; } FormatIso : 2026-08-17T14:30:00Z FormatRfc2822 : Mon, 17 Aug 2026 14:30:00 +0000 Format Y-m-d H:M : 2026-08-17 14:30 Dauer 93784 s : 26h 03m 04s kurz : 26:03:04 Dauer 45 s : 45s **Der Zielpuffer gehört dem Aufrufer.** Jede ''Format*''-Funktion schreibt hinein und gibt einen Status zurück, keinen Text — dieselbe Konvention wie in ''std.string''. 64 Byte reichen für alle gezeigten Formate. **Negative Dauern und Dauern ab 1000 Stunden** ergaben bis [[https://github.com/SEOLizer/LyX-Compiler/issues/1605|#1605]] stumm Müllzeichen. Jetzt: ''FormatDuration(-93784)'' → ''-26h 03m 04s'', ''FormatDurationShort(-45)'' → ''-00:45'', ''FormatDuration(12345*3600)'' → ''12345h''. Dazu ''FormatRelative'' („vor 3 Stunden"), ''FormatLocale'' — das seit [[https://github.com/SEOLizer/LyX-Compiler/issues/1415|#1415]] wirklich lokalisiert (''de'' → ''17.08.2026 14:30:00'', ''en_US'' → ''8/17/2026 2:30:00 PM'') — sowie ''WeekdayLong''/''WeekdayShort'' und ''MonthLong''/''MonthShort'' für einzelne Namen. ---- ===== Einlesen ===== unit main; import std.io; import std.string; import std.alloc; import std.datetime; fn T(t: pchar, s: pchar): void { var e: int64 := alloc(8); poke64(e, -1); // Marker: unberuehrt var rc: int64 := ParseFlexible(s, e); Print(t); Print(" rc="); Print(IntToStr(rc)); Print(" Wert="); PrintLn(IntToStr(peek64(e))); } fn main(): int64 { T("ISO mit Zeit 2026-08-17T14:30:00Z", "2026-08-17T14:30:00Z"c); T("nur Datum 2026-08-17 ", "2026-08-17"c); T("deutsch 17.08.2026 ", "17.08.2026"c); T("deutsch+Zeit 17.08.2026 14:30 ", "17.08.2026 14:30"c); T("RFC 2822 Mon, 17 Aug 2026 … ", "Mon, 17 Aug 2026 14:30:00 +0000"c); T("US 8/17/2026 ", "8/17/2026"c); T("Unsinn kein Datum ", "kein Datum"c); return 0; } ISO mit Zeit 2026-08-17T14:30:00Z rc=1 Wert=1786977000 nur Datum 2026-08-17 rc=1 Wert=1786924800 deutsch 17.08.2026 rc=1 Wert=1786924800 deutsch+Zeit 17.08.2026 14:30 rc=1 Wert=1786977000 RFC 2822 Mon, 17 Aug 2026 … rc=1 Wert=1786977000 US 8/17/2026 rc=0 Wert=0 Unsinn kein Datum rc=0 Wert=0 **''ParseFlexible'' probiert vier Formate** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1603|#1603]]), in dieser Reihenfolge: ISO 8601 mit Zeitanteil, RFC 2822, ISO-Datum ohne Zeit (Zeitanteil ''00:00:00'') und die deutsche Schreibweise ''17.08.2026'' mit optionalem '' 14:30'' oder '' 14:30:00''. Die amerikanische Schreibweise ''8/17/2026'' scheitert weiterhin. **Bei Misserfolg steht jetzt 0 im Zielpuffer**, nicht mehr der vorherige Inhalt: der Marker ''−1'' von oben ist weg. Ein übersehener ''rc''-Test endet damit bei ''1970-01-01'' — falsch, aber offensichtlich falsch. Dasselbe gilt für ''ParseIso'', ''ParseRfc2822'', ''ParseDate'' und ''ParseTime''. ''rc == 1'' heißt Erfolg, ''rc == 0'' Misserfolg. Trotzdem immer prüfen. Für Datum und Uhrzeit getrennt gibt es ''ParseDate'' und ''ParseTime'', die die Einzelfelder in drei Puffer schreiben: var y: int64 := alloc(8); var mo: int64 := alloc(8); var d: int64 := alloc(8); ParseDate("2026-08-17"c, y, mo, d); // peek64(y) = 2026, peek64(mo) = 8, peek64(d) = 17 ---- ===== Rechnen mit Datumswerten ===== unit main; import std.io; import std.string; import std.alloc; import std.datetime; fn Z(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); } fn S(t: pchar, b: int64): void { Print(t); PrintLn(b as pchar); } fn main(): int64 { var buf: int64 := alloc(64); var start: int64 := 1786977000; // 2026-08-17 14:30:00 UTC var ende: int64 := 1787216400; // 2026-08-20 09:00:00 UTC Z("DiffSeconds(start, ende) : ", DiffSeconds(start, ende)); Z(" in Stunden : ", DiffSeconds(start, ende) / 3600); Z("DiffDays kalendarisch : ", DiffDays(2026, 8, 17, 2026, 8, 20)); FormatIso(AddDays(start, 30), buf as pchar); S("start + 30 Tage : ", buf); FormatIso(AddMonths(start, 6), buf as pchar); S("start + 6 Monate : ", buf); FormatIso(AddYears(start, 1), buf as pchar); S("start + 1 Jahr : ", buf); var jan31: int64 := 1769817600; // 2026-01-31 FormatIso(AddMonths(jan31, 1), buf as pchar); S("31.01. + 1 Monat : ", buf); return 0; } DiffSeconds(start, ende) : 239400 in Stunden : 66 DiffDays kalendarisch : 3 start + 30 Tage : 2026-09-16T14:30:00Z start + 6 Monate : 2027-02-17T14:30:00Z start + 1 Jahr : 2027-08-17T14:30:00Z 31.01. + 1 Monat : 2026-02-28T00:00:00Z **''DiffSeconds(a, b)'' rechnet ''b − a''**, nicht ''a − b''. Es ist als „von ''a'' bis ''b''" zu lesen. Wer die Argumente in der intuitiven Reihenfolge „später, früher" übergibt, bekommt das **negative** Ergebnis: DiffSeconds(start, ende) = 239400 DiffSeconds(ende, start) = -239400 Bei einer Abfrage wie ''%%if (DiffSeconds(x, y) > frist)%%'' entscheidet das über wahr und falsch. > **''AddMonths'' klemmt auf das Monatsende.** Der 31. Januar plus ein Monat ergibt den **28. Februar**, nicht den 3. März. Das ist die übliche und meist gewünschte Festlegung — sie ist aber **nicht umkehrbar**: ''AddMonths(AddMonths(jan31, 1), -1)'' führt nicht zum 31. Januar zurück. Wer Monatsfristen über mehrere Schritte fortschreibt, rechnet vom **ursprünglichen** Datum aus, nicht vom jeweils letzten Ergebnis. ''DiffDays'' nimmt **sechs Einzelwerte** (Jahr, Monat, Tag zweimal), keine Zeitstempel — und zählt Kalendertage, nicht Sekunden geteilt durch 86400. Für Fristen in Tagen ist das die richtige Wahl, weil Sommerzeitsprünge keine Rolle spielen. ---- ===== Prüfen vor dem Rechnen ===== IsValidDate(2026, 2, 30) // 0 — den gibt es nicht IsValidDate(2024, 2, 29) // 1 — Schaltjahr IsValidTime(25, 0, 0) // 0 IsValidDate 2026-02-30: 0 IsValidDate 2024-02-29: 1 IsValidTime 25:00:00 : 0 Dazu ''IsValidDateTime'' für beides zusammen. **Eingaben aus Formularen oder Dateien gehören durch diese Prüfung**, bevor sie in ''FromUnixTimestamp'' oder eine Rechnung wandern — die Arithmetikfunktionen prüfen ihre Argumente nicht. ---- ===== Wiederkehrende Aufgaben auf einen Blick ===== ^ Ich will … ^ So geht es ^ | die aktuelle Zeit speichern | ''Now()'' aus ''std.time'' — Unix-Sekunden | | eine Dauer messen | ''GetTimeMs()'' vorher und nachher, **nie** ''Now()'' | | prüfen, ob eine Frist abgelaufen ist | ''%%GetTimeMs() >= start + dauer%%'' | | Tage zwischen zwei Daten | ''date''-Differenz (''std.time'') oder ''DiffDays'' (''std.datetime'') | | Sekunden zwischen zwei Zeitpunkten | ''DiffSeconds(frueher, spaeter)'' — Reihenfolge beachten | | einen Monat weiterrechnen | ''AddMonths'' — klemmt auf das Monatsende | | ein Datum ausgeben | ''FormatIso'' oder ''Format'' mit Muster | | eine Dauer lesbar ausgeben | ''FormatDuration'' (''26h 03m 04s'') | | einen ISO-Zeitstempel einlesen | ''ParseIso'', Rückgabewert prüfen | | eine Eingabe validieren | ''IsValidDate'' / ''IsValidTime'' | | periodisch etwas tun | ''SleepUntil'' mit absolutem Zeitpunkt, nicht ''Sleep'' in der Schleife | | einen Timer als Dateideskriptor | ''TimerFdCreate'' + ''Poll'' | ---- ===== Die fünf Stolpersteine ===== - **''std.time'' und ''std.datetime'' vertragen sich nicht** — auch transitiv, etwa über ''std.cloud.s3''. - **''DiffSeconds(a, b)'' ist ''b − a''.** - **''ParseFlexible'' kann ISO-mit-Zeit, RFC 2822, ISO-Datum und die deutsche Schreibweise** — nicht aber ''8/17/2026''. Bei Misserfolg steht 0 im Puffer. - **''Now()'' ist die Wanduhr** und kann springen; Dauern gehören auf ''GetTimeMs()''. - **''timestamp'' sind Mikrosekunden**, ''datetime'' Sekunden — beide sind ''int64'', der Compiler warnt nicht. ---- Letzte Aktualisierung: 2026-08-18 — ''ParseFlexible'' auf vier Formate und genulltes Ziel nachgezogen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1603|#1603]]), Zeitzonen auf ''ZoneByCode'' umgestellt ([[https://github.com/SEOLizer/LyX-Compiler/issues/1600|#1600]]), Dauerausgabe mit Vorzeichen ergänzt ([[https://github.com/SEOLizer/LyX-Compiler/issues/1605|#1605]]); Beispiele gegen die Repo-Fassung der Standardbibliothek mit ''lyxc 1.1.2E'' übersetzt. Vorherige Aktualisierung: 2026-08-17 · alle Beispiele mit ''lyxc 1.1.0A'' übersetzt und ausgeführt; Argumentreihenfolge von ''DiffSeconds'', Verhalten von ''ParseFlexible'' und ''AddMonths'' sowie der Unterschied zwischen Wanduhr und monotoner Uhr einzeln nachgemessen.