Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A

Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben).
Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature.

Vollstaendigkeits-Inventar
- docs/inventar.md mit 285 Eintraegen aus den Themenlisten von
  bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle
- crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide
  Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten

Bruchschritt (Puffer, Host, Korpus)
- Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui
  behaelt Farbabbildung, Widget und den neuen Terminal-Host
- Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus
  Ereignisse (Taste, Groessenaenderung, Abbruch)
- Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht
  bestehenden Sollausgaben blieben dabei unveraendert

Groessenunabhaengigkeit
- 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend
  der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor
  und VIEW PRINT geklemmt)
- Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener
  Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf

Bibliothek
- Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch)
- Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS
- Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON)
- PRINT USING, LPRINT USING, FORMAT$, SetFormatCC
- Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge)
- Datum und Zeit mit Serienwerten ab 1899
- Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#,
  IRR#, MIRR#, SLN#, SYD#, DDB#
- Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen),
  BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV
- System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung

Altlasten aus Phase 2
- ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar
  (prozeduruebergreifender Fixup im Codegenerator)
- DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum)
- Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest

Vom Inventar aufgedeckte Fehler
- Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht
  ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die
  String*-Routinen, LINE und OPEN "COMn:") -- geschlossen
- Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik,
  Record-Konvertierung, Rest der Dateisystemfunktionen
- bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar
  bildet die Vereinigung mit qb45advr.hlp

Neue Changes
- phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb)
- phase-3-ortszeit: zieht die UTC-Abweichung zurueck

Neue Abhaengigkeit: unicode-width.
Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei,
docs/bibliothek.md neu, PLAN.md fortgeschrieben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 06:54:19 +02:00
parent a6f005d023
commit 52ccbb5848
94 changed files with 10421 additions and 792 deletions

View File

@@ -1,3 +1,199 @@
//! Datums- und Zeitfunktionen der Laufzeitbibliothek.
//!
//! Serielle Datumswerte folgen der Zählweise des Vorbilds: der ganzzahlige
//! Anteil zählt Tage ab dem 30.12.1899 (Serie 2 = 1.1.1900), der
//! Nachkommaanteil ist die Tageszeit. `WEEKDAY` liefert 1 für Sonntag.
//!
//! ponytail: Die Uhr läuft in UTC — die Standardbibliothek kennt keine
//! Zeitzonen, und eine Zeitzonenbibliothek wäre die erste schwere
//! Abhängigkeit der Laufzeit. Die Abweichung steht in der Sprachreferenz.
//! Ceiling: sobald lokale Zeit gefordert ist, hier eine Zeitzonenquelle
//! einhängen — der Rest des Moduls rechnet bereits auf Sekunden seit Epoche.
// Platzhalter — wird in Phase 3 ausgearbeitet (siehe PLAN.md)
/// Tage zwischen dem 1.1.1970 (Unix) und dem 30.12.1899 (Serie 0).
const EPOCH_OFFSET_TAGE: i64 = 25_569;
/// Tage → (Jahr, Monat, Tag) ab Unix-Epoche (Howard Hinnant).
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
(y + i64::from(m <= 2), m, d)
}
/// (Jahr, Monat, Tag) → Tage ab Unix-Epoche.
pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = y - i64::from(m <= 2);
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let mp = if m > 2 { m - 3 } else { m + 9 } as u64;
let doy = (153 * mp + 2) / 5 + u64::from(d) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe as i64 - 719_468
}
/// Gültige Kalenderangabe? (Monat 112, Tag im Monat.)
pub fn gueltig(y: i64, m: i64, d: i64) -> bool {
if !(1..=12).contains(&m) || d < 1 {
return false;
}
let tage = tage_im_monat(y, m as u32);
d <= tage as i64
}
fn tage_im_monat(y: i64, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if schaltjahr(y) => 29,
2 => 28,
_ => 0,
}
}
fn schaltjahr(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
/// Serieller Wert aus Jahr/Monat/Tag.
pub fn serial_aus_ymd(y: i64, m: u32, d: u32) -> f64 {
(days_from_civil(y, m, d) + EPOCH_OFFSET_TAGE) as f64
}
/// (Jahr, Monat, Tag) aus einem seriellen Wert.
pub fn ymd_aus_serial(s: f64) -> (i64, u32, u32) {
civil_from_days(s.floor() as i64 - EPOCH_OFFSET_TAGE)
}
/// Tageszeit eines seriellen Werts als (Stunde, Minute, Sekunde).
pub fn hms_aus_serial(s: f64) -> (u32, u32, u32) {
// Auf die Sekunde runden, damit 0.5 exakt 12:00:00 ergibt.
let sekunden = ((s - s.floor()) * 86_400.0).round() as i64;
let sekunden = sekunden.rem_euclid(86_400);
(
(sekunden / 3600) as u32,
((sekunden / 60) % 60) as u32,
(sekunden % 60) as u32,
)
}
/// Wochentag: 1 = Sonntag … 7 = Samstag.
pub fn weekday(s: f64) -> u32 {
// Der 1.1.1970 war ein Donnerstag (Index 4 in dieser Zählung).
let tage = s.floor() as i64 - EPOCH_OFFSET_TAGE;
(tage.rem_euclid(7) as u32 + 4) % 7 + 1
}
/// Tageszeit als serieller Bruchteil.
pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 {
(h * 3600 + m * 60 + s) as f64 / 86_400.0
}
/// Sekunden seit der Unix-Epoche (UTC).
pub fn jetzt_sekunden() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
/// `NOW` — aktueller Zeitpunkt als serieller Wert.
pub fn jetzt_serial(offset_sekunden: i64) -> f64 {
let s = jetzt_sekunden() + offset_sekunden;
s as f64 / 86_400.0 + EPOCH_OFFSET_TAGE as f64
}
/// `DATEVALUE` — Datum aus einem Text lesen (`MM-TT-JJJJ`, `MM/TT/JJJJ`,
/// zweistelliges Jahr wird als 19xx/20xx gedeutet wie beim Vorbild).
pub fn datevalue(t: &str) -> Option<f64> {
let teile: Vec<&str> = t
.trim()
.split(['-', '/', '.'])
.filter(|p| !p.is_empty())
.collect();
if teile.len() != 3 {
return None;
}
let m: i64 = teile[0].trim().parse().ok()?;
let d: i64 = teile[1].trim().parse().ok()?;
let mut y: i64 = teile[2].trim().parse().ok()?;
if y < 100 {
y += if y < 80 { 2000 } else { 1900 };
}
if !gueltig(y, m, d) {
return None;
}
Some(serial_aus_ymd(y, m as u32, d as u32))
}
/// `TIMEVALUE` — Tageszeit aus einem Text lesen (`HH:MM[:SS]`).
pub fn timevalue(t: &str) -> Option<f64> {
let teile: Vec<&str> = t.trim().split(':').collect();
if !(2..=3).contains(&teile.len()) {
return None;
}
let h: i64 = teile[0].trim().parse().ok()?;
let m: i64 = teile[1].trim().parse().ok()?;
let s: i64 = teile.get(2).map_or(Ok(0), |x| x.trim().parse()).ok()?;
if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&s) {
return None;
}
Some(serial_aus_hms(h, m, s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serienwert_und_rueckumwandlung() {
let s = serial_aus_ymd(1992, 3, 15);
assert_eq!(ymd_aus_serial(s), (1992, 3, 15));
}
#[test]
fn zaehlweise_des_vorbilds() {
// Serie 2 = 1. Januar 1900.
assert_eq!(serial_aus_ymd(1900, 1, 1), 2.0);
assert_eq!(ymd_aus_serial(2.0), (1900, 1, 1));
}
#[test]
fn wochentag_sonntag_ist_eins() {
// 15. März 1992 war ein Sonntag.
assert_eq!(weekday(serial_aus_ymd(1992, 3, 15)), 1);
// 16. März 1992 war ein Montag.
assert_eq!(weekday(serial_aus_ymd(1992, 3, 16)), 2);
}
#[test]
fn tageszeit() {
let s = serial_aus_hms(13, 45, 30);
assert_eq!(hms_aus_serial(s), (13, 45, 30));
assert_eq!(hms_aus_serial(0.5), (12, 0, 0));
}
#[test]
fn gueltigkeitspruefung() {
assert!(gueltig(1992, 2, 29)); // Schaltjahr
assert!(!gueltig(1993, 2, 29));
assert!(!gueltig(1992, 13, 1));
assert!(!gueltig(1992, 4, 31));
}
#[test]
fn text_in_serienwert() {
assert_eq!(datevalue("03-15-1992"), Some(serial_aus_ymd(1992, 3, 15)));
assert_eq!(datevalue("3/15/92"), Some(serial_aus_ymd(1992, 3, 15)));
assert_eq!(datevalue("13-01-1992"), None);
assert_eq!(timevalue("13:45:30"), Some(serial_aus_hms(13, 45, 30)));
assert_eq!(timevalue("13:45"), Some(serial_aus_hms(13, 45, 0)));
assert_eq!(timevalue("25:00"), None);
}
}