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

134
Cargo.lock generated
View File

@@ -14,6 +14,12 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.13.1" version = "2.13.1"
@@ -61,7 +67,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.1",
"crossterm_winapi", "crossterm_winapi",
"mio", "mio",
"parking_lot", "parking_lot",
@@ -114,6 +120,37 @@ dependencies = [
"syn 3.0.4", "syn 3.0.4",
] ]
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.18.0" version = "1.18.0"
@@ -202,6 +239,59 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-link",
]
[[package]]
name = "jiff-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
dependencies = [
"defmt",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.189" version = "0.2.189"
@@ -279,6 +369,21 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -303,7 +408,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.1",
"cassowary", "cassowary",
"compact_str", "compact_str",
"crossterm", "crossterm",
@@ -324,7 +429,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.1",
] ]
[[package]] [[package]]
@@ -333,7 +438,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -358,6 +463,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@@ -487,6 +612,7 @@ dependencies = [
name = "tb-runtime" name = "tb-runtime"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"jiff",
"log", "log",
"thiserror", "thiserror",
"unicode-width 0.2.0", "unicode-width 0.2.0",

View File

@@ -21,8 +21,10 @@ authors = ["Georg Bauer"]
ratatui = "0.29" ratatui = "0.29"
crossterm = "0.28" crossterm = "0.28"
# Text # Text und Zeit
unicode-width = "0.2" unicode-width = "0.2"
# Nur fuer den UTC-Versatz zu einem Zeitpunkt (siehe design.md, D1/D2).
jiff = "0.2"
# Allgemein # Allgemein
anyhow = "1" anyhow = "1"

16
PLAN.md
View File

@@ -347,13 +347,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
**Offene Entscheidungen, die in spätere Phasen zeigen** **Offene Entscheidungen, die in spätere Phasen zeigen**
- Die Uhr lief zunächst in **UTC**, weil Ortszeit eine - Die Uhr lief zunächst in **UTC**, weil Ortszeit eine
Zeitzonenbibliothek gebraucht hätte. Entscheidung 2026-09-04: die Zeitzonenbibliothek gebraucht hätte. Mit dem Change
Bibliothek kommt, die Abweichung wird zurückgezogen → Change `phase-3-ortszeit` (2026-09-04) ist die Abweichung zurückgezogen:
`phase-3-ortszeit`. `jiff` liefert den UTC-Versatz zu einem Zeitpunkt, die
Kalenderrechnung bleibt Eigenimplementierung.
- `LPRINT` schreibt am Programmende nach `LPT1.TXT` im Arbeitsverzeichnis; - `LPRINT` schreibt am Programmende nach `LPT1.TXT` im Arbeitsverzeichnis;
einen plattformübergreifenden Druckerkanal gibt es nicht. einen plattformübergreifenden Druckerkanal gibt es nicht.
- Neue Abhängigkeit `unicode-width` (Doppelzellen). Für ISAM kommt `redb` - Neue Abhängigkeiten: `unicode-width` (Doppelzellen) und `jiff`
hinzu. (Zeitzonenversatz). Für ISAM kommt `redb` hinzu.
## Phase 4 — Forms-Engine (`tb-ui::forms`) ## Phase 4 — Forms-Engine (`tb-ui::forms`)
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen- - [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-
@@ -435,9 +436,8 @@ bleibt gültig. Noch nichts davon ist beschlossen; Sammlung wächst:
2026-09-04): in der Runtime integrierte Rust-Bibliotheken als eigene, 2026-09-04): in der Runtime integrierte Rust-Bibliotheken als eigene,
importierbare Bibliotheken für BASIC-Programme bereitstellen — importierbare Bibliotheken für BASIC-Programme bereitstellen —
- **crossterm** für direkte Terminalsteuerung - **crossterm** für direkte Terminalsteuerung
- **die Zeitzonenbibliothek** (ab dem Change `phase-3-ortszeit` in der - **jiff** (seit dem Change `phase-3-ortszeit` in der Laufzeit):
Laufzeit): Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung zwischen Zonen. Der Kernstandard braucht davon nur den Versatz für
zwischen Zonen. Der Kernstandard braucht davon nur den Versatz für
die Ortszeit; alles Weitere wäre eine Erweiterung. die Ortszeit; alles Weitere wäre eine Erweiterung.
- Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten - Verteilung: `tbc build --exe` als Single-File-Tool-Baukasten

View File

@@ -94,6 +94,12 @@ fn cmd_run(args: &[String]) -> ExitCode {
// Im Rohmodus ist Strg+C kein Signal mehr — der Abbruch kommt als // Im Rohmodus ist Strg+C kein Signal mehr — der Abbruch kommt als
// Ereignis und muss den Lauf beenden können. // Ereignis und muss den Lauf beenden können.
vm.set_poll_interrupt(true); vm.set_poll_interrupt(true);
// Der Rückfall auf UTC wird einmal beim Start gemeldet, nicht je
// Zeitabfrage — ein stiller Wechsel wäre ein Verstoß gegen den
// Guiding Principle.
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
}
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost: // Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot. // Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.

View File

@@ -57,6 +57,28 @@ fn screen_groessen(name: &str, src: &str) -> Vec<(usize, usize)> {
wert.split(',').map(|w| groesse_lesen(name, w)).collect() wert.split(',').map(|w| groesse_lesen(name, w)).collect()
} }
/// Optionale Zeitzone `' tb-tz: ±HH:MM`. Ohne Angabe gilt die Zone des
/// Rechners — Zeitprogramme im Korpus setzen sie, damit ihre Sollausgabe
/// unabhängig vom Standort des Testrechners bleibt.
fn tz_direktive(name: &str, src: &str) -> Option<tb_runtime::datetime::Zeitzone> {
let wert = direktive(src, "tb-tz:")?;
let (vorzeichen, rest) = match wert.strip_prefix('-') {
Some(r) => (-1i32, r),
None => (1i32, wert.trim_start_matches('+')),
};
let (h, m) = rest
.split_once(':')
.unwrap_or_else(|| panic!("{name}: tb-tz erwartet ±HH:MM, fand {wert:?}"));
let parse = |s: &str| {
s.trim()
.parse::<i32>()
.unwrap_or_else(|_| panic!("{name}: {s:?} ist keine Zahl"))
};
Some(tb_runtime::datetime::Zeitzone::Fest(
vorzeichen * (parse(h) * 3600 + parse(m) * 60),
))
}
/// Optionale Größenänderung mitten im Lauf: /// Optionale Größenänderung mitten im Lauf:
/// `' tb-resize: nach <n> auf <cols>x<rows>`. /// `' tb-resize: nach <n> auf <cols>x<rows>`.
fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> { fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
@@ -112,6 +134,9 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display())); .unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let mut vm = Vm::new(module); let mut vm = Vm::new(module);
vm.rt.screen.resize(cols, rows); vm.rt.screen.resize(cols, rows);
if let Some(z) = tz_direktive(&name, &src) {
vm.rt.zeitzone = z;
}
// `' tb-tempdir` — das Programm arbeitet mit Dateien. // `' tb-tempdir` — das Programm arbeitet mit Dateien.
let _temp = direktive(&src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name)); let _temp = direktive(&src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
let mut host = CaptureHost::default(); let mut host = CaptureHost::default();

View File

@@ -8,5 +8,6 @@ authors.workspace = true
[dependencies] [dependencies]
unicode-width.workspace = true unicode-width.workspace = true
jiff.workspace = true
thiserror.workspace = true thiserror.workspace = true
log.workspace = true log.workspace = true

View File

@@ -45,8 +45,11 @@ pub struct RtState {
pub dir_index: usize, pub dir_index: usize,
/// Versatz der Programmuhr in Sekunden. `DATE$ =` und `TIME$ =` können /// Versatz der Programmuhr in Sekunden. `DATE$ =` und `TIME$ =` können
/// die Systemuhr nicht stellen; sie verschieben stattdessen diesen /// die Systemuhr nicht stellen; sie verschieben stattdessen diesen
/// Versatz (dokumentierte Abweichung). /// Versatz (dokumentierte Abweichung). Er wirkt auf die Ortszeit.
pub uhr_offset: i64, pub uhr_offset: i64,
/// Woher der UTC-Versatz kommt. Vorgabe ist die Zone des Rechners;
/// ist sie nicht ermittelbar, steht hier `Unbekannt` und es gilt UTC.
pub zeitzone: dt::Zeitzone,
rng: u32, rng: u32,
rnd_last: f32, rnd_last: f32,
pub command: String, pub command: String,
@@ -70,6 +73,7 @@ impl Default for RtState {
dir_treffer: Vec::new(), dir_treffer: Vec::new(),
dir_index: 0, dir_index: 0,
uhr_offset: 0, uhr_offset: 0,
zeitzone: dt::Zeitzone::ermitteln(),
// Startzustand des Vorbild-PRNG; die exakte // Startzustand des Vorbild-PRNG; die exakte
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3 // PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
// („RND/RANDOMIZE — kompatibler PRNG"). // („RND/RANDOMIZE — kompatibler PRNG").
@@ -1444,23 +1448,24 @@ fn bi_input_s(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<
// ---- Sonstiges ------------------------------------------------------------------ // ---- Sonstiges ------------------------------------------------------------------
fn bi_timer(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_timer(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Sekunden seit Mitternacht (UTC-basiert; lokale Zeitzone: Phase 3). // Sekunden seit der **lokalen** Mitternacht, mit Bruchteil.
let secs = std::time::SystemTime::now() let bruchteil = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64() % 86_400.0) .map(|d| d.as_secs_f64().fract())
.unwrap_or(0.0); .unwrap_or(0.0);
Ok(Some(Value::Sng(secs as f32))) let ganze = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
} }
fn bi_date(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_date(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = dt::jetzt_sekunden() + st.uhr_offset; let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400)); let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400));
s_ok(format!("{m:02}-{d:02}-{y:04}")) s_ok(format!("{m:02}-{d:02}-{y:04}"))
} }
fn bi_time(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_time(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = (dt::jetzt_sekunden() + st.uhr_offset).rem_euclid(86_400); let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
s_ok(format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60)) s_ok(format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60))
} }
@@ -1468,7 +1473,7 @@ fn bi_date_set(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Op
let t = arg_str(a, 0)?; let t = arg_str(a, 0)?;
let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?; let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Nur den Datumsanteil verschieben, die Tageszeit bleibt. // Nur den Datumsanteil verschieben, die Tageszeit bleibt.
let jetzt = dt::jetzt_serial(st.uhr_offset); let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let diff_tage = ziel.floor() - jetzt.floor(); let diff_tage = ziel.floor() - jetzt.floor();
st.uhr_offset += (diff_tage * 86_400.0) as i64; st.uhr_offset += (diff_tage * 86_400.0) as i64;
Ok(None) Ok(None)
@@ -1477,14 +1482,14 @@ fn bi_date_set(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Op
fn bi_time_set(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_time_set(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?; let t = arg_str(a, 0)?;
let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?; let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let jetzt = dt::jetzt_serial(st.uhr_offset); let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let diff = ziel - (jetzt - jetzt.floor()); let diff = ziel - (jetzt - jetzt.floor());
st.uhr_offset += (diff * 86_400.0).round() as i64; st.uhr_offset += (diff * 86_400.0).round() as i64;
Ok(None) Ok(None)
} }
fn bi_now(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_now(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(dt::jetzt_serial(st.uhr_offset)))) Ok(Some(Value::Dbl(dt::jetzt_serial(st.zeitzone, st.uhr_offset))))
} }
fn bi_date_serial(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_date_serial(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {

View File

@@ -4,11 +4,11 @@
//! Anteil zählt Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900), der //! 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. //! Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
//! //!
//! ponytail: Die Uhr läuft in UTC — die Standardbibliothek kennt keine //! Die Zeitzone kommt von außen: [`utc_versatz`] ist der **einzige** Punkt,
//! Zeitzonen, und eine Zeitzonenbibliothek wäre die erste schwere //! an dem dieses Modul eine Bibliothek befragt (Entscheidung 2026-09-04,
//! Abhängigkeit der Laufzeit. Die Abweichung steht in der Sprachreferenz. //! design.md D1/D2). Die Kalenderrechnung darunter ist Eigenimplementierung
//! Ceiling: sobald lokale Zeit gefordert ist, hier eine Zeitzonenquelle //! und sieht die Bibliothek nie — zwei Kalendermodelle nebeneinander wären
//! einhängen — der Rest des Moduls rechnet bereits auf Sekunden seit Epoche. //! eine dauerhafte Fehlerquelle.
/// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0). /// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0).
const EPOCH_OFFSET_TAGE: i64 = 25_569; const EPOCH_OFFSET_TAGE: i64 = 25_569;
@@ -95,18 +95,74 @@ pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 {
(h * 3600 + m * 60 + s) as f64 / 86_400.0 (h * 3600 + m * 60 + s) as f64 / 86_400.0
} }
/// Sekunden seit der Unix-Epoche (UTC). /// Sekunden seit der Unix-Epoche (UTC) — der rohe Zeitpunkt.
pub fn jetzt_sekunden() -> i64 { pub fn jetzt_utc() -> i64 {
std::time::SystemTime::now() std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64) .map(|d| d.as_secs() as i64)
.unwrap_or(0) .unwrap_or(0)
} }
/// `NOW` — aktueller Zeitpunkt als serieller Wert. /// UTC-Versatz der Systemzone zum Zeitpunkt `zeitpunkt` (Sekunden seit der
pub fn jetzt_serial(offset_sekunden: i64) -> f64 { /// Unix-Epoche), in Sekunden. `None`, wenn sich die Zone nicht ermitteln
let s = jetzt_sekunden() + offset_sekunden; /// lässt — dann gilt UTC.
s as f64 / 86_400.0 + EPOCH_OFFSET_TAGE as f64 ///
/// 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 — die Rückfallebene ist über
/// [`Zeitzone::ermitteln`] erkennbar.
pub fn versatz(&self, zeitpunkt: i64) -> i32 {
match self {
Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0),
Zeitzone::Fest(n) => *n,
Zeitzone::Unbekannt => 0,
}
}
/// Einmal beim Start: `System`, wenn die Zone lesbar ist, sonst
/// `Unbekannt`. Damit ist der Rückfall auf UTC sichtbar statt still.
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`, /// `DATEVALUE` — Datum aus einem Text lesen (`MM-TT-JJJJ`, `MM/TT/JJJJ`,
@@ -187,6 +243,47 @@ mod tests {
assert!(!gueltig(1992, 4, 31)); 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] #[test]
fn text_in_serienwert() { fn text_in_serienwert() {
assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15))); assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15)));

View File

@@ -660,3 +660,72 @@ fn lokaler_handler_hat_vorrang() {
END SUB\n"; END SUB\n";
assert_eq!(out(src), "Lokal\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:?}");
}

View File

@@ -65,9 +65,18 @@ Serielle Werte zählen Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900);
der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag. der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
Ungültige Datums- oder Zeitangaben lösen Fehler 5 aus. Ungültige Datums- oder Zeitangaben lösen Fehler 5 aus.
Die Uhr läuft in **UTC** (dokumentierte Abweichung). `DATE$ = …` und Die Uhr rechnet in der **Ortszeit** des Rechners; `TIMER` zählt ab der
`TIME$ = …` stellen nicht die Systemuhr, sondern einen lokalen Mitternacht. Der Versatz wird für den jeweils abgefragten
programmeigenen Versatz, den alle Zeitfunktionen mitrechnen. Zeitpunkt bestimmt, nicht einmal beim Programmstart — ein Programm, das
über eine Zeitumstellung hinweg läuft, folgt ihr.
Lässt sich die Zone nicht ermitteln, gilt UTC; das Programm läuft weiter
und der Runner meldet es einmal beim Start auf der Fehlerausgabe.
`DATE$ = …` und `TIME$ = …` stellen nicht die Systemuhr, sondern einen
programmeigenen Versatz auf die Ortszeit, den alle Zeitfunktionen
mitrechnen. `DATEVALUE` und `TIMEVALUE` lesen Wanduhrzeit und rechnen
nicht um.
## Deklaration ## Deklaration

View File

@@ -381,12 +381,16 @@ Dialoge:
Betriebssystem-Handle bildet der Dialekt nicht ab und liefert 0. Betriebssystem-Handle bildet der Dialekt nicht ab und liefert 0.
5c. **`CHDRIVE`** ist außerhalb von Windows folgenlos — Laufwerksbuchstaben 5c. **`CHDRIVE`** ist außerhalb von Windows folgenlos — Laufwerksbuchstaben
gibt es dort nicht. gibt es dort nicht.
6a. **Uhr läuft in UTC** (2026-09-03): `DATE$`, `TIME$`, `TIMER` und `NOW` 6a. **Uhr in Ortszeit, mit Rückfall auf UTC** (2026-09-04): `DATE$`,
liefern koordinierte Weltzeit, nicht Ortszeit — die Standardbibliothek `TIME$`, `TIMER` und `NOW` rechnen in der Zeitzone des Rechners;
von Rust kennt keine Zeitzonen, und eine Zeitzonenbibliothek wäre die `TIMER` zählt ab der **lokalen** Mitternacht. Der Versatz wird für den
erste schwere Abhängigkeit der Laufzeit. `DATE$ = …` und `TIME$ = …` jeweils abgefragten Zeitpunkt bestimmt, nicht einmal beim Start — ein
stellen nicht die Systemuhr, sondern einen programmeigenen Versatz; Programm, das über eine Zeitumstellung hinweg läuft, folgt ihr.
alle Zeitfunktionen rechnen ihn mit ein. Lässt sich die Zone des Rechners nicht ermitteln, gilt UTC; das
Programm läuft weiter, und der Runner meldet es einmal beim Start auf
der Fehlerausgabe („Zeitzone nicht ermittelbar …"). `DATE$ = …` und
`TIME$ = …` stellen nicht die Systemuhr, sondern einen programmeigenen
Versatz auf die Ortszeit; alle Zeitfunktionen rechnen ihn mit ein.
6b. **Serielle Datumswerte** zählen Tage ab dem 30.12.1899 (Serie 2 = 6b. **Serielle Datumswerte** zählen Tage ab dem 30.12.1899 (Serie 2 =
1.1.1900), der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 1.1.1900), der Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1
für Sonntag. Zweistellige Jahre gelten in `DATESERIAL` als 19xx, in für Sonntag. Zweistellige Jahre gelten in `DATESERIAL` als 19xx, in

View File

@@ -1,35 +1,35 @@
## 1. Versatzermittlung ## 1. Versatzermittlung
- [ ] 1.1 `jiff` als Workspace-Abhängigkeit aufnehmen und in `tb-runtime` einbinden; verifiziert dadurch, dass `cargo build -p tb-runtime` durchläuft und `cargo tree -p tb-runtime` die Bibliothek genau einmal zeigt - [x] 1.1 `jiff` als Workspace-Abhängigkeit aufnehmen und in `tb-runtime` einbinden; verifiziert dadurch, dass `cargo build -p tb-runtime` durchläuft und `cargo tree -p tb-runtime` die Bibliothek genau einmal zeigt
- [ ] 1.2 `datetime::utc_versatz(zeitpunkt: i64) -> Option<i32>` als einzigen Aufrufpunkt der Bibliothek schreiben (Sekunden Versatz, `None` = Zone nicht ermittelbar); verifiziert durch Unit-Test, dass der Versatz für einen festen Zeitpunkt im gültigen Bereich 50400…50400 liegt - [x] 1.2 `datetime::utc_versatz(zeitpunkt: i64) -> Option<i32>` als einzigen Aufrufpunkt der Bibliothek schreiben (Sekunden Versatz, `None` = Zone nicht ermittelbar); verifiziert durch Unit-Test, dass der Versatz für einen festen Zeitpunkt im gültigen Bereich 50400…50400 liegt
- [ ] 1.3 Sommerzeit prüfen: für dieselbe Zone einen Zeitpunkt im Januar und einen im Juli abfragen; verifiziert durch Unit-Test, dass sich die Versätze in einer Zone mit Sommerzeit unterscheiden und in einer ohne gleich bleiben - [x] 1.3 Sommerzeit prüfen: für dieselbe Zone einen Zeitpunkt im Januar und einen im Juli abfragen; verifiziert durch Unit-Test, dass sich die Versätze in einer Zone mit Sommerzeit unterscheiden und in einer ohne gleich bleiben
- [ ] 1.4 Sicherstellen, dass die Kalenderrechnung unberührt bleibt; verifiziert dadurch, dass `datetime.rs` außerhalb von `utc_versatz` keinen Bibliotheksaufruf enthält und die bestehenden `datetime`-Tests unverändert grün sind - [x] 1.4 Sicherstellen, dass die Kalenderrechnung unberührt bleibt; verifiziert dadurch, dass `datetime.rs` außerhalb von `utc_versatz` keinen Bibliotheksaufruf enthält und die bestehenden `datetime`-Tests unverändert grün sind
## 2. Zonenzustand in der Laufzeit ## 2. Zonenzustand in der Laufzeit
- [ ] 2.1 `Zeitzone { System, Fest(i32), Unbekannt }` einführen und in `RtState` halten; Vorgabe `System`, bei fehlgeschlagener Ermittlung `Unbekannt`; verifiziert durch Unit-Test je Variante - [x] 2.1 `Zeitzone { System, Fest(i32), Unbekannt }` einführen und in `RtState` halten; Vorgabe `System`, bei fehlgeschlagener Ermittlung `Unbekannt`; verifiziert durch Unit-Test je Variante
- [ ] 2.2 `jetzt_sekunden`/`jetzt_serial` auf den Zonenzustand umstellen; verifiziert durch Unit-Test, dass `Fest(3600)` genau eine Stunde gegenüber `Unbekannt` verschiebt - [x] 2.2 `jetzt_sekunden`/`jetzt_serial` auf den Zonenzustand umstellen; verifiziert durch Unit-Test, dass `Fest(3600)` genau eine Stunde gegenüber `Unbekannt` verschiebt
- [ ] 2.3 `DATE$`, `TIME$`, `TIMER` und `NOW` über den Zonenzustand rechnen; verifiziert durch Test, dass `HOUR(NOW)` und die Stunde aus `TIME$` bei `Fest(n)` übereinstimmen - [x] 2.3 `DATE$`, `TIME$`, `TIMER` und `NOW` über den Zonenzustand rechnen; verifiziert durch Test, dass `HOUR(NOW)` und die Stunde aus `TIME$` bei `Fest(n)` übereinstimmen
- [ ] 2.4 `TIMER` zählt ab lokaler Mitternacht; verifiziert durch Test, dass sich `TIMER` bei `Fest(3600)` gegenüber `Unbekannt` um 3600 verschiebt (modulo Tageslänge) - [x] 2.4 `TIMER` zählt ab lokaler Mitternacht; verifiziert durch Test, dass sich `TIMER` bei `Fest(3600)` gegenüber `Unbekannt` um 3600 verschiebt (modulo Tageslänge)
- [ ] 2.5 `DATE$ =` / `TIME$ =` wirken auf die Ortszeit; verifiziert durch Test, dass `TIME$` nach `TIME$ = "10:20:30"` genau diesen Wert nennt und `HOUR(NOW)` 10 liefert - [x] 2.5 `DATE$ =` / `TIME$ =` wirken auf die Ortszeit; verifiziert durch Test, dass `TIME$` nach `TIME$ = "10:20:30"` genau diesen Wert nennt und `HOUR(NOW)` 10 liefert
## 3. Rückfallebene ## 3. Rückfallebene
- [ ] 3.1 Bei nicht ermittelbarer Zone auf UTC zurückfallen, ohne Fehler; verifiziert durch Test mit `Zeitzone::Unbekannt`, dass alle Zeitfunktionen Werte liefern - [x] 3.1 Bei nicht ermittelbarer Zone auf UTC zurückfallen, ohne Fehler; verifiziert durch Test mit `Zeitzone::Unbekannt`, dass alle Zeitfunktionen Werte liefern
- [ ] 3.2 Den Rückfall beim Programmstart einmalig auf die Fehlerausgabe melden (nicht je Abfrage); verifiziert dadurch, dass `tbc run` bei `Unbekannt` genau eine Meldung schreibt und die Standardausgabe unberührt bleibt - [x] 3.2 Den Rückfall beim Programmstart einmalig auf die Fehlerausgabe melden (nicht je Abfrage); verifiziert dadurch, dass `tbc run` bei `Unbekannt` genau eine Meldung schreibt und die Standardausgabe unberührt bleibt
- [ ] 3.3 Rückfallebene in docs/sprachreferenz.md beschreiben; verifiziert durch den Abschnittseintrag - [x] 3.3 Rückfallebene in docs/sprachreferenz.md beschreiben; verifiziert durch den Abschnittseintrag
## 4. Prüfbarkeit ohne Abhängigkeit von der Testmaschine ## 4. Prüfbarkeit ohne Abhängigkeit von der Testmaschine
- [ ] 4.1 Kopfzeilen-Direktive `' tb-tz: ±HH:MM` im Korpus-Harness auswerten und als `Zeitzone::Fest` setzen; verifiziert dadurch, dass ein Korpusprogramm ohne die Direktive weiterhin läuft und eines mit ihr den gesetzten Versatz verwendet - [x] 4.1 Kopfzeilen-Direktive `' tb-tz: ±HH:MM` im Korpus-Harness auswerten und als `Zeitzone::Fest` setzen; verifiziert dadurch, dass ein Korpusprogramm ohne die Direktive weiterhin läuft und eines mit ihr den gesetzten Versatz verwendet
- [ ] 4.2 Korpusprogramm `zeitzone.bas` mit festem Versatz: `DATE$`/`TIME$`/`TIMER`/`NOW` gegen eine per `DATE$ =`/`TIME$ =` gesetzte Uhrzeit prüfen; verifiziert dadurch, dass die Sollausgabe unabhängig von der Zone des Testrechners stimmt - [x] 4.2 Korpusprogramm `zeitzone.bas` mit festem Versatz: `DATE$`/`TIME$`/`TIMER`/`NOW` gegen eine per `DATE$ =`/`TIME$ =` gesetzte Uhrzeit prüfen; verifiziert dadurch, dass die Sollausgabe unabhängig von der Zone des Testrechners stimmt
- [ ] 4.3 Bestehendes `datumzeit.bas` auf die Direktive umstellen, damit es auch nach der Umstellung deterministisch bleibt; verifiziert dadurch, dass seine Sollausgabe unverändert bleibt - [x] 4.3 Bestehendes `datumzeit.bas` auf die Direktive umstellen, damit es auch nach der Umstellung deterministisch bleibt; verifiziert dadurch, dass seine Sollausgabe unverändert bleibt
## 5. Abschluss ## 5. Abschluss
- [ ] 5.1 Abweichung „Uhr läuft in UTC" aus docs/sprachreferenz.md entfernen und durch die Beschreibung der Zonenermittlung ersetzen; verifiziert dadurch, dass der Abweichungspunkt nicht mehr auftaucht - [x] 5.1 Abweichung „Uhr läuft in UTC" aus docs/sprachreferenz.md entfernen und durch die Beschreibung der Zonenermittlung ersetzen; verifiziert dadurch, dass der Abweichungspunkt nicht mehr auftaucht
- [ ] 5.2 `docs/bibliothek.md`, Abschnitt Datum/Zeit, fortschreiben; verifiziert durch den geänderten Abschnitt - [x] 5.2 `docs/bibliothek.md`, Abschnitt Datum/Zeit, fortschreiben; verifiziert durch den geänderten Abschnitt
- [ ] 5.3 PLAN.md Stufe 2, Eintrag „Runtime-Bibliotheken als BASIC-Bibliotheken", um die Zeitzonenbibliothek erweitern (Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung zwischen Zonen) — neben crossterm; verifiziert durch den ergänzten Eintrag - [x] 5.3 PLAN.md Stufe 2, Eintrag „Runtime-Bibliotheken als BASIC-Bibliotheken", um die Zeitzonenbibliothek erweitern (Zonennamen, Versatz zu einem Zeitpunkt, Umrechnung zwischen Zonen) — neben crossterm; verifiziert durch den ergänzten Eintrag
- [ ] 5.4 PLAN.md Phase 3: den Hinweis auf die UTC-Abweichung durch den Verweis auf diesen Change ersetzen; verifiziert durch den aktualisierten Abschnitt - [x] 5.4 PLAN.md Phase 3: den Hinweis auf die UTC-Abweichung durch den Verweis auf diesen Change ersetzen; verifiziert durch den aktualisierten Abschnitt
- [ ] 5.5 Inventar unverändert lassen und den Abgleichstest prüfen; verifiziert dadurch, dass `cargo test -p tb-frontend --test inventar` grün bleibt (die Elemente waren bereits implementiert, nur ihr Zeitbezug ändert sich) - [x] 5.5 Inventar unverändert lassen und den Abgleichstest prüfen; verifiziert dadurch, dass `cargo test -p tb-frontend --test inventar` grün bleibt (die Elemente waren bereits implementiert, nur ihr Zeitbezug ändert sich)
- [ ] 5.6 Vollständige Suite ausführen; verifiziert dadurch, dass `cargo test --workspace` grün ist - [x] 5.6 Vollständige Suite ausführen; verifiziert dadurch, dass `cargo test --workspace` grün ist

View File

@@ -1,4 +1,5 @@
' tb-screen: 80x25 ' tb-screen: 80x25
' tb-tz: +02:00
' Datum und Zeit (Spec konsolen-basisbibliothek): Serienwert-Zaehlweise, ' Datum und Zeit (Spec konsolen-basisbibliothek): Serienwert-Zaehlweise,
' Rueckumwandlung, WEEKDAY = 1 fuer Sonntag, Textformen und die ' Rueckumwandlung, WEEKDAY = 1 fuer Sonntag, Textformen und die
' programmeigene Uhr ueber DATE$ =/TIME$ =. ' programmeigene Uhr ueber DATE$ =/TIME$ =.

18
tests/compat/zeitzone.bas Normal file
View File

@@ -0,0 +1,18 @@
' tb-screen: 80x25
' tb-tz: -05:00
' Ortszeit (Spec konsolen-basisbibliothek): die Zeitfunktionen rechnen in
' der Zone des Rechners. Der Korpus legt sie per tb-tz fest, damit die
' Sollausgabe unabhaengig vom Standort des Testrechners bleibt.
' Alle Werte werden gegen eine gesetzte Uhrzeit geprueft, nicht gegen die
' echte -- sonst waere die Ausgabe nicht reproduzierbar.
DATE$ = "03-15-1992"
TIME$ = "13:45:30"
PRINT DATE$; " "; TIME$
n# = NOW
PRINT YEAR(n#); MONTH(n#); DAY(n#); WEEKDAY(n#)
PRINT HOUR(n#); MINUTE(n#); SECOND(n#)
' TIMER zaehlt ab lokaler Mitternacht: 13*3600 + 45*60 + 30 = 49530.
PRINT INT(TIMER)
' Stunde aus TIME$ und aus NOW muessen uebereinstimmen.
PRINT VAL(LEFT$(TIME$, 2)) = HOUR(n#)
END

View File

@@ -0,0 +1,5 @@
03-15-1992 13:45:30
1992 3 15 1
13 45 30
49530
-1