301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
//! 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<i32> {
|
||
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<f64> {
|
||
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<f64> {
|
||
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);
|
||
}
|
||
}
|