Ortszeit statt UTC

Setzt den OpenSpec-Change phase-3-ortszeit um (21/21 Aufgaben) und zieht
damit die in Phase 3 eingegangene Abweichung zurueck: DATE$, TIME$, TIMER
und NOW rechnen in der Zeitzone des Rechners, TIMER zaehlt ab der lokalen
Mitternacht.

Versatzermittlung
- jiff 0.2.35 als Abhaengigkeit von tb-runtime
- datetime::utc_versatz(zeitpunkt) ist der einzige Aufrufpunkt der
  Bibliothek; die Kalenderrechnung darunter bleibt Eigenimplementierung
  und sieht sie nie -- zwei Kalendermodelle nebeneinander waeren eine
  dauerhafte Fehlerquelle
- Der Versatz gilt je Zeitpunkt, nicht je Programmlauf: ein Programm, das
  ueber eine Zeitumstellung hinweg laeuft, folgt ihr

Zonenzustand
- Zeitzone { System, Fest(i32), Unbekannt } im RtState
- Fest(n) macht Tests und Korpusprogramme unabhaengig von der Zone des
  Testrechners; TZ waere prozessweit und set_var seit Rust 2024 unsafe

Rueckfallebene
- Ohne ermittelbare Zone gilt UTC, das Programm laeuft weiter
- tbc run meldet den Rueckfall einmal beim Start auf der Fehlerausgabe,
  nicht je Abfrage -- ein stiller Wechsel waere ein Verstoss gegen den
  Guiding Principle

Korpus
- Neue Direktive ' tb-tz: +-HH:MM im Harness
- zeitzone.bas prueft Ortszeit, TIMER ab lokaler Mitternacht und den
  Programmversatz gegen eine gesetzte Uhrzeit
- datumzeit.bas auf die Direktive umgestellt; Sollausgabe unveraendert

Nebenbefund
- TIMER trennt jetzt ganze Sekunden (Ortszeit) vom Bruchteil
  (Systemuhr). Ohne die Trennung waere TIMER in Zonen mit halbstuendigem
  Versatz gesprungen.

Dokumentation: die Abweichung "Uhr laeuft in UTC" entfaellt aus
sprachreferenz.md; bibliothek.md und PLAN.md nachgezogen, jiff steht im
Stufe-2-Ideenspeicher neben crossterm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 07:01:53 +02:00
parent d0461bea34
commit a0fb41420e
15 changed files with 433 additions and 65 deletions

View File

@@ -660,3 +660,72 @@ fn lokaler_handler_hat_vorrang() {
END SUB\n";
assert_eq!(out(src), "Lokal\n");
}
// ---- Ortszeit (Spec konsolen-basisbibliothek) ------------------------------
use tb_runtime::datetime::Zeitzone;
/// Programm mit festgelegter Zeitzone laufen lassen.
fn out_mit_zone(src: &str, zone: Zeitzone) -> String {
use tb_runtime::host::CaptureHost;
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| panic!("{d:?}"));
let mut vm = Vm::new(module);
vm.rt.zeitzone = zone;
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
tb_runtime::snapshot::text(&vm.rt.screen)
}
/// `TIME$` und `HOUR(NOW)` nennen dieselbe Stunde — sie rechnen beide
/// über den Zonenzustand.
#[test]
fn time_und_now_stimmen_ueberein() {
let t = out_mit_zone("PRINT LEFT$(TIME$, 2); HOUR(NOW)", Zeitzone::Fest(7200));
let zeile = t.lines().next().unwrap();
let aus_time: i32 = zeile[..2].parse().unwrap();
let aus_now: i32 = zeile[2..].trim().parse().unwrap();
assert_eq!(aus_time, aus_now, "{zeile:?}");
}
/// Ein fester Versatz verschiebt die Uhr um genau diesen Betrag.
#[test]
fn fester_versatz_verschiebt_die_uhr() {
let src = "PRINT INT(TIMER)";
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
let diff = (mit - ohne).rem_euclid(86_400.0);
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
}
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
#[test]
fn timer_zaehlt_ab_lokaler_mitternacht() {
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
let zahlen: Vec<f64> = t
.split_whitespace()
.filter_map(|w| w.parse().ok())
.collect();
assert_eq!(zahlen.len(), 2, "{t:?}");
assert_eq!(zahlen[0], zahlen[1], "TIMER-Stunde ≠ TIME$-Stunde: {t:?}");
}
/// `TIME$ =` wirkt auf die Ortszeit und alle davon abgeleiteten Werte.
#[test]
fn programmversatz_wirkt_auf_alle_zeitfunktionen() {
let t = out_mit_zone(
"TIME$ = \"10:20:30\"\nDATE$ = \"01-02-1993\"\nPRINT TIME$; \" \"; DATE$; HOUR(NOW); YEAR(NOW)",
Zeitzone::Fest(3600),
);
assert!(t.starts_with("10:20:30 01-02-1993 10 1993"), "{t:?}");
}
/// Ohne ermittelbare Zone gilt UTC — ohne Fehler.
#[test]
fn rueckfall_auf_utc_laeuft_fehlerfrei() {
let t = out_mit_zone(
"PRINT LEN(DATE$); LEN(TIME$); TIMER >= 0; YEAR(NOW) > 2000",
Zeitzone::Unbekannt,
);
assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
}