//! Datums- und Zeitfunktionen der Laufzeitbibliothek. //! //! Serielle Datumswerte folgen der Zählweise des Vorbilds: der ganzzahlige //! Anteil zählt Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900), der //! Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag. //! //! Die Zeitzone kommt von außen: [`utc_versatz`] ist der **einzige** Punkt, //! an dem dieses Modul eine Bibliothek befragt (Entscheidung 2026-09-04, //! design.md D1/D2). Die Kalenderrechnung darunter ist Eigenimplementierung //! und sieht die Bibliothek nie — zwei Kalendermodelle nebeneinander wären //! eine dauerhafte Fehlerquelle. /// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0). const EPOCH_OFFSET_TAGE: i64 = 25_569; /// Tage → (Jahr, Monat, Tag) ab Unix-Epoche (Howard Hinnant). pub fn civil_from_days(z: i64) -> (i64, u32, u32) { let z = z + 719_468; let era = if z >= 0 { z } else { z - 146_096 } / 146_097; let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; (y + i64::from(m <= 2), m, d) } /// (Jahr, Monat, Tag) → Tage ab Unix-Epoche. pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 { let y = y - i64::from(m <= 2); let era = if y >= 0 { y } else { y - 399 } / 400; let yoe = (y - era * 400) as u64; let mp = if m > 2 { m - 3 } else { m + 9 } as u64; let doy = (153 * mp + 2) / 5 + u64::from(d) - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe as i64 - 719_468 } /// Gültige Kalenderangabe? (Monat 1–12, Tag im Monat.) pub fn gueltig(y: i64, m: i64, d: i64) -> bool { if !(1..=12).contains(&m) || d < 1 { return false; } let tage = tage_im_monat(y, m as u32); d <= tage as i64 } fn tage_im_monat(y: i64, m: u32) -> u32 { match m { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if schaltjahr(y) => 29, 2 => 28, _ => 0, } } fn schaltjahr(y: i64) -> bool { (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 } /// Serieller Wert aus Jahr/Monat/Tag. pub fn serial_aus_ymd(y: i64, m: u32, d: u32) -> f64 { (days_from_civil(y, m, d) + EPOCH_OFFSET_TAGE) as f64 } /// (Jahr, Monat, Tag) aus einem seriellen Wert. pub fn ymd_aus_serial(s: f64) -> (i64, u32, u32) { civil_from_days(s.floor() as i64 - EPOCH_OFFSET_TAGE) } /// Tageszeit eines seriellen Werts als (Stunde, Minute, Sekunde). pub fn hms_aus_serial(s: f64) -> (u32, u32, u32) { // Auf die Sekunde runden, damit 0.5 exakt 12:00:00 ergibt. let sekunden = ((s - s.floor()) * 86_400.0).round() as i64; let sekunden = sekunden.rem_euclid(86_400); ( (sekunden / 3600) as u32, ((sekunden / 60) % 60) as u32, (sekunden % 60) as u32, ) } /// Wochentag: 1 = Sonntag … 7 = Samstag. pub fn weekday(s: f64) -> u32 { // Der 1.1.1970 war ein Donnerstag (Index 4 in dieser Zählung). let tage = s.floor() as i64 - EPOCH_OFFSET_TAGE; (tage.rem_euclid(7) as u32 + 4) % 7 + 1 } /// Tageszeit als serieller Bruchteil. pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 { (h * 3600 + m * 60 + s) as f64 / 86_400.0 } /// Sekunden seit der Unix-Epoche (UTC) — der rohe Zeitpunkt. pub fn jetzt_utc() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0) } /// UTC-Versatz der Systemzone zum Zeitpunkt `zeitpunkt` (Sekunden seit der /// Unix-Epoche), in Sekunden. `None`, wenn sich die Zone nicht ermitteln /// lässt — dann gilt UTC. /// /// Der Versatz gilt **je Zeitpunkt**, nicht je Programmlauf: ein Programm, /// das über eine Zeitumstellung hinweg läuft, folgt ihr, und ein Sprung in /// einen anderen Teil des Jahres bekommt den dort gültigen Versatz. /// /// Einziger Aufrufpunkt der Zeitzonenbibliothek (design.md D1/D2). pub fn utc_versatz(zeitpunkt: i64) -> Option { let ts = jiff::Timestamp::from_second(zeitpunkt).ok()?; let zone = jiff::tz::TimeZone::try_system().ok()?; Some(zone.to_offset(ts).seconds()) } /// Wohin die Zeitfunktionen rechnen. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Zeitzone { /// Zone des Rechners (Vorgabe). #[default] System, /// Fester Versatz in Sekunden — für Tests und den Korpus. Fest(i32), /// Zone nicht ermittelbar; es gilt UTC. Unbekannt, } impl Zeitzone { /// Versatz zu einem Zeitpunkt. Bei `System` ohne ermittelbare Zone /// fällt der Wert auf 0 (UTC) zurück. BASIC und Einbetter verwenden /// [`crate::builtins::RtState::zeitpunkt`] für Zeit und aktuellen Status. pub fn versatz(&self, zeitpunkt: i64) -> i32 { match self { Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0), Zeitzone::Fest(n) => *n, Zeitzone::Unbekannt => 0, } } /// Momentaufnahme: `System`, wenn die Zone lesbar ist, sonst `Unbekannt`. /// Die Laufzeit behält `System` als Quelle, damit sie nach Ausfällen /// erneut auflösen kann; `RtState::zeitpunkt` liefert den aktuellen Status. pub fn ermitteln() -> Zeitzone { match utc_versatz(jetzt_utc()) { Some(_) => Zeitzone::System, None => Zeitzone::Unbekannt, } } } /// Sekunden seit der Unix-Epoche in **Ortszeit** gelesen: der rohe /// Zeitpunkt plus Zonenversatz plus programmeigener Versatz. Alles /// Weitere (`DATE$`, `TIME$`, `TIMER`, `NOW`) rechnet darauf. pub fn jetzt_sekunden(zone: Zeitzone, offset_sekunden: i64) -> i64 { let utc = jetzt_utc(); utc + zone.versatz(utc) as i64 + offset_sekunden } /// `NOW` — aktueller Zeitpunkt als serieller Wert (Ortszeit). pub fn jetzt_serial(zone: Zeitzone, offset_sekunden: i64) -> f64 { jetzt_sekunden(zone, offset_sekunden) as f64 / 86_400.0 + EPOCH_OFFSET_TAGE as f64 } /// `DATEVALUE` — Datum aus einem Text lesen (`MM-TT-JJJJ`, `MM/TT/JJJJ`, /// zweistelliges Jahr wird als 19xx/20xx gedeutet wie beim Vorbild). pub fn datevalue(t: &str) -> Option { let teile: Vec<&str> = t .trim() .split(['-', '/', '.']) .filter(|p| !p.is_empty()) .collect(); if teile.len() != 3 { return None; } let m: i64 = teile[0].trim().parse().ok()?; let d: i64 = teile[1].trim().parse().ok()?; let mut y: i64 = teile[2].trim().parse().ok()?; if y < 100 { y += if y < 80 { 2000 } else { 1900 }; } if !gueltig(y, m, d) { return None; } Some(serial_aus_ymd(y, m as u32, d as u32)) } /// `TIMEVALUE` — Tageszeit aus einem Text lesen (`HH:MM[:SS]`). pub fn timevalue(t: &str) -> Option { let teile: Vec<&str> = t.trim().split(':').collect(); if !(2..=3).contains(&teile.len()) { return None; } let h: i64 = teile[0].trim().parse().ok()?; let m: i64 = teile[1].trim().parse().ok()?; let s: i64 = teile.get(2).map_or(Ok(0), |x| x.trim().parse()).ok()?; if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&s) { return None; } Some(serial_aus_hms(h, m, s)) } #[cfg(test)] mod tests { use super::*; #[test] fn serienwert_und_rueckumwandlung() { let s = serial_aus_ymd(1992, 3, 15); assert_eq!(ymd_aus_serial(s), (1992, 3, 15)); } #[test] fn zaehlweise_des_vorbilds() { // Serie 2 = 1. Januar 1900. assert_eq!(serial_aus_ymd(1900, 1, 1), 2.0); assert_eq!(ymd_aus_serial(2.0), (1900, 1, 1)); } #[test] fn wochentag_sonntag_ist_eins() { // 15. März 1992 war ein Sonntag. assert_eq!(weekday(serial_aus_ymd(1992, 3, 15)), 1); // 16. März 1992 war ein Montag. assert_eq!(weekday(serial_aus_ymd(1992, 3, 16)), 2); } #[test] fn tageszeit() { let s = serial_aus_hms(13, 45, 30); assert_eq!(hms_aus_serial(s), (13, 45, 30)); assert_eq!(hms_aus_serial(0.5), (12, 0, 0)); } #[test] fn gueltigkeitspruefung() { assert!(gueltig(1992, 2, 29)); // Schaltjahr assert!(!gueltig(1993, 2, 29)); assert!(!gueltig(1992, 13, 1)); assert!(!gueltig(1992, 4, 31)); } #[test] fn versatz_liegt_im_gueltigen_bereich() { // Zonenversätze liegen zwischen −14 h und +14 h. if let Some(v) = utc_versatz(1_000_000_000) { assert!((-50_400..=50_400).contains(&v), "Versatz {v} unplausibel"); } } #[test] fn versatz_gilt_je_zeitpunkt() { // 15.01.2021 und 15.07.2021 als Unix-Zeitpunkte. let januar = 1_610_668_800; let juli = 1_626_307_200; let (a, b) = (utc_versatz(januar), utc_versatz(juli)); // Beide Abfragen liefern dasselbe Ergebnis-Muster: entweder kennt // die Maschine ihre Zone oder nicht. assert_eq!(a.is_some(), b.is_some()); if let (Some(a), Some(b)) = (a, b) { // In einer Zone mit Sommerzeit unterscheiden sie sich um genau // die Umstellungsspanne, sonst gar nicht. let d = (a - b).abs(); assert!( d == 0 || (1800..=7200).contains(&d), "Spanne {d} unplausibel" ); } } #[test] fn fester_versatz_verschiebt_genau() { let ohne = jetzt_sekunden(Zeitzone::Unbekannt, 0); let mit = jetzt_sekunden(Zeitzone::Fest(3600), 0); // Die Systemuhr kann zwischen beiden Aufrufen eine Sekunde weiter // sein; die Verschiebung bleibt eine Stunde. assert!((mit - ohne - 3600).abs() <= 1); } #[test] fn programmversatz_kommt_hinzu() { let a = jetzt_sekunden(Zeitzone::Fest(0), 0); let b = jetzt_sekunden(Zeitzone::Fest(0), 120); assert!((b - a - 120).abs() <= 1); } #[test] fn text_in_serienwert() { assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15))); assert_eq!(datevalue("3/15/92"), Some(serial_aus_ymd(1992, 3, 15))); assert_eq!(datevalue("13-01-1992"), None); assert_eq!(timevalue("13:45:30"), Some(serial_aus_hms(13, 45, 30))); assert_eq!(timevalue("13:45"), Some(serial_aus_hms(13, 45, 0))); assert_eq!(timevalue("25:00"), None); } }