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:
134
Cargo.lock
generated
134
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
16
PLAN.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
|||||||
@@ -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:?}");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
18
tests/compat/zeitzone.bas
Normal 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
|
||||||
5
tests/compat/zeitzone.out
Normal file
5
tests/compat/zeitzone.out
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
03-15-1992 13:45:30
|
||||||
|
1992 3 15 1
|
||||||
|
13 45 30
|
||||||
|
49530
|
||||||
|
-1
|
||||||
Reference in New Issue
Block a user