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:
@@ -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 1–12, Tag im Monat.)
|
||||
pub fn gueltig(y: i64, m: i64, d: i64) -> bool {
|
||||
if !(1..=12).contains(&m) || d < 1 {
|
||||
return false;
|
||||
}
|
||||
let tage = tage_im_monat(y, m as u32);
|
||||
d <= tage as i64
|
||||
}
|
||||
|
||||
fn tage_im_monat(y: i64, m: u32) -> u32 {
|
||||
match m {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
2 if schaltjahr(y) => 29,
|
||||
2 => 28,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn schaltjahr(y: i64) -> bool {
|
||||
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
|
||||
}
|
||||
|
||||
/// Serieller Wert aus Jahr/Monat/Tag.
|
||||
pub fn serial_aus_ymd(y: i64, m: u32, d: u32) -> f64 {
|
||||
(days_from_civil(y, m, d) + EPOCH_OFFSET_TAGE) as f64
|
||||
}
|
||||
|
||||
/// (Jahr, Monat, Tag) aus einem seriellen Wert.
|
||||
pub fn ymd_aus_serial(s: f64) -> (i64, u32, u32) {
|
||||
civil_from_days(s.floor() as i64 - EPOCH_OFFSET_TAGE)
|
||||
}
|
||||
|
||||
/// Tageszeit eines seriellen Werts als (Stunde, Minute, Sekunde).
|
||||
pub fn hms_aus_serial(s: f64) -> (u32, u32, u32) {
|
||||
// Auf die Sekunde runden, damit 0.5 exakt 12:00:00 ergibt.
|
||||
let sekunden = ((s - s.floor()) * 86_400.0).round() as i64;
|
||||
let sekunden = sekunden.rem_euclid(86_400);
|
||||
(
|
||||
(sekunden / 3600) as u32,
|
||||
((sekunden / 60) % 60) as u32,
|
||||
(sekunden % 60) as u32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Wochentag: 1 = Sonntag … 7 = Samstag.
|
||||
pub fn weekday(s: f64) -> u32 {
|
||||
// Der 1.1.1970 war ein Donnerstag (Index 4 in dieser Zählung).
|
||||
let tage = s.floor() as i64 - EPOCH_OFFSET_TAGE;
|
||||
(tage.rem_euclid(7) as u32 + 4) % 7 + 1
|
||||
}
|
||||
|
||||
/// Tageszeit als serieller Bruchteil.
|
||||
pub fn serial_aus_hms(h: i64, m: i64, s: i64) -> f64 {
|
||||
(h * 3600 + m * 60 + s) as f64 / 86_400.0
|
||||
}
|
||||
|
||||
/// Sekunden seit der Unix-Epoche (UTC).
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user