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

@@ -0,0 +1,377 @@
//! Finanzmathematische Funktionen des Vorbilds (`FV#`, `PV#`, `Pmt#`,
//! `IPmt#`, `PPmt#`, `NPer#`, `Rate#`, `NPV#`, `IRR#`, `MIRR#`, `SLN#`,
//! `SYD#`, `DDB#`).
//!
//! Vorzeichenkonvention wie beim Vorbild: ausgehende Zahlungen sind negativ,
//! eingehende positiv. Das Fälligkeitsargument (`faellig`) ist 0 für
//! nachschüssige und 1 für vorschüssige Zahlung. Gerechnet wird in DOUBLE.
//!
//! `Rate#` und `IRR#` nähern sich iterativ; die Abbruchbedingung steht in
//! [`ITERATIONEN`] und [`TOLERANZ`] und ist in docs/bibliothek.md
//! dokumentiert.
use crate::errors::RuntimeError;
/// Höchstzahl der Näherungsschritte von `Rate#` und `IRR#`.
pub const ITERATIONEN: usize = 100;
/// Abbruchschranke der Näherung (Betrag der Restabweichung).
pub const TOLERANZ: f64 = 1e-9;
type R = Result<f64, RuntimeError>;
fn ungueltig() -> RuntimeError {
RuntimeError::ILLEGAL_FUNCTION_CALL
}
/// `(1 + zins)^perioden`.
fn faktor(zins: f64, perioden: f64) -> f64 {
(1.0 + zins).powf(perioden)
}
/// Barwertfaktor der Rentenzahlungen.
fn rentenfaktor(zins: f64, perioden: f64, faellig: f64) -> f64 {
if zins == 0.0 {
perioden
} else {
(1.0 + zins * faellig) * (faktor(zins, perioden) - 1.0) / zins
}
}
/// `FV#` — Endwert.
pub fn fv(zins: f64, perioden: f64, zahlung: f64, barwert: f64, faellig: f64) -> R {
if zins <= -1.0 {
return Err(ungueltig());
}
Ok(-(barwert * faktor(zins, perioden) + zahlung * rentenfaktor(zins, perioden, faellig)))
}
/// `PV#` — Barwert.
pub fn pv(zins: f64, perioden: f64, zahlung: f64, endwert: f64, faellig: f64) -> R {
if zins <= -1.0 {
return Err(ungueltig());
}
Ok(-(endwert + zahlung * rentenfaktor(zins, perioden, faellig)) / faktor(zins, perioden))
}
/// `Pmt#` — Zahlung je Periode.
pub fn pmt(zins: f64, perioden: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
if perioden == 0.0 || zins <= -1.0 {
return Err(ungueltig());
}
let rf = rentenfaktor(zins, perioden, faellig);
if rf == 0.0 {
return Err(ungueltig());
}
Ok(-(endwert + barwert * faktor(zins, perioden)) / rf)
}
/// `NPer#` — Anzahl Perioden.
pub fn nper(zins: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64) -> R {
if zins <= -1.0 {
return Err(ungueltig());
}
if zins == 0.0 {
if zahlung == 0.0 {
return Err(ungueltig());
}
return Ok(-(barwert + endwert) / zahlung);
}
let z = zahlung * (1.0 + zins * faellig);
let zaehler = z - endwert * zins;
let nenner = barwert * zins + z;
if nenner == 0.0 || zaehler / nenner <= 0.0 {
return Err(ungueltig());
}
Ok((zaehler / nenner).ln() / (1.0 + zins).ln())
}
/// `IPmt#` — Zinsanteil der Periode `periode` (1-basiert).
pub fn ipmt(
zins: f64,
periode: f64,
perioden: f64,
barwert: f64,
endwert: f64,
faellig: f64,
) -> R {
if periode < 1.0 || periode > perioden {
return Err(ungueltig());
}
let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
if faellig != 0.0 && periode == 1.0 {
// Vorschüssig fällt in der ersten Periode kein Zins an.
return Ok(0.0);
}
let vorher = if faellig != 0.0 {
fv(zins, periode - 2.0, zahlung, barwert, faellig)?
} else {
fv(zins, periode - 1.0, zahlung, barwert, faellig)?
};
Ok(vorher * zins)
}
/// `PPmt#` — Tilgungsanteil der Periode.
pub fn ppmt(
zins: f64,
periode: f64,
perioden: f64,
barwert: f64,
endwert: f64,
faellig: f64,
) -> R {
let zahlung = pmt(zins, perioden, barwert, endwert, faellig)?;
let zinsanteil = ipmt(zins, periode, perioden, barwert, endwert, faellig)?;
Ok(zahlung - zinsanteil)
}
/// `Rate#` — Zinssatz je Periode, iterativ genähert.
pub fn rate(
perioden: f64,
zahlung: f64,
barwert: f64,
endwert: f64,
faellig: f64,
schaetzung: f64,
) -> R {
if perioden <= 0.0 {
return Err(ungueltig());
}
// Sekantenverfahren auf f(r) = FV(r) Sollendwert.
let f = |r: f64| -> Option<f64> {
if r <= -1.0 {
return None;
}
Some(barwert * faktor(r, perioden) + zahlung * rentenfaktor(r, perioden, faellig) + endwert)
};
let mut r0 = schaetzung;
let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 };
let (mut f0, mut f1) = match (f(r0), f(r1)) {
(Some(a), Some(b)) => (a, b),
_ => return Err(ungueltig()),
};
for _ in 0..ITERATIONEN {
if f1.abs() < TOLERANZ {
return Ok(r1);
}
if (f1 - f0).abs() < f64::MIN_POSITIVE {
break;
}
let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
let Some(f2) = f(r2) else { break };
r0 = r1;
f0 = f1;
r1 = r2;
f1 = f2;
}
if f1.abs() < TOLERANZ * 1e3 {
Ok(r1)
} else {
Err(ungueltig())
}
}
/// `NPV#` — Kapitalwert einer Zahlungsreihe (erste Zahlung am Ende der
/// ersten Periode).
pub fn npv(zins: f64, werte: &[f64]) -> R {
if zins <= -1.0 || werte.is_empty() {
return Err(ungueltig());
}
let mut summe = 0.0;
let mut nenner = 1.0 + zins;
for w in werte {
summe += w / nenner;
nenner *= 1.0 + zins;
}
Ok(summe)
}
/// `IRR#` — interner Zinsfuß, iterativ genähert.
pub fn irr(werte: &[f64], schaetzung: f64) -> R {
// Ohne Vorzeichenwechsel gibt es keine Lösung.
let hat_plus = werte.iter().any(|w| *w > 0.0);
let hat_minus = werte.iter().any(|w| *w < 0.0);
if !hat_plus || !hat_minus {
return Err(ungueltig());
}
let f = |r: f64| -> Option<f64> {
if r <= -1.0 {
return None;
}
let mut summe = 0.0;
let mut nenner = 1.0;
for w in werte {
summe += w / nenner;
nenner *= 1.0 + r;
}
Some(summe)
};
let mut r0 = schaetzung;
let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 };
let (mut f0, mut f1) = match (f(r0), f(r1)) {
(Some(a), Some(b)) => (a, b),
_ => return Err(ungueltig()),
};
for _ in 0..ITERATIONEN {
if f1.abs() < TOLERANZ {
return Ok(r1);
}
if (f1 - f0).abs() < f64::MIN_POSITIVE {
break;
}
let r2 = r1 - f1 * (r1 - r0) / (f1 - f0);
let Some(f2) = f(r2) else { break };
r0 = r1;
f0 = f1;
r1 = r2;
f1 = f2;
}
if f1.abs() < TOLERANZ * 1e3 {
Ok(r1)
} else {
Err(ungueltig())
}
}
/// `MIRR#` — modifizierter interner Zinsfuß.
pub fn mirr(werte: &[f64], finanzierungszins: f64, wiederanlagezins: f64) -> R {
let n = werte.len();
if n < 2 || finanzierungszins <= -1.0 || wiederanlagezins <= -1.0 {
return Err(ungueltig());
}
let negative: Vec<f64> = werte.iter().map(|w| w.min(0.0)).collect();
let positive: Vec<f64> = werte.iter().map(|w| w.max(0.0)).collect();
let bw_neg = npv(finanzierungszins, &negative)?;
let bw_pos = npv(wiederanlagezins, &positive)?;
if bw_neg == 0.0 {
return Err(ungueltig());
}
let n = n as f64;
let verhaeltnis = -bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins));
if verhaeltnis <= 0.0 {
return Err(ungueltig());
}
Ok(verhaeltnis.powf(1.0 / (n - 1.0)) - 1.0)
}
/// `SLN#` — lineare Abschreibung.
pub fn sln(anschaffung: f64, restwert: f64, nutzungsdauer: f64) -> R {
if nutzungsdauer == 0.0 {
return Err(ungueltig());
}
Ok((anschaffung - restwert) / nutzungsdauer)
}
/// `SYD#` — arithmetisch-degressive Abschreibung.
pub fn syd(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
return Err(ungueltig());
}
Ok((anschaffung - restwert) * (nutzungsdauer - periode + 1.0) * 2.0
/ (nutzungsdauer * (nutzungsdauer + 1.0)))
}
/// `DDB#` — geometrisch-degressive Abschreibung (doppelter linearer Satz).
pub fn ddb(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) -> R {
if nutzungsdauer <= 0.0 || periode < 1.0 || periode > nutzungsdauer {
return Err(ungueltig());
}
let mut buchwert = anschaffung;
let mut betrag = 0.0;
let satz = 2.0 / nutzungsdauer;
for _ in 0..(periode as u64) {
betrag = (buchwert * satz).min(buchwert - restwert).max(0.0);
buchwert -= betrag;
}
Ok(betrag)
}
#[cfg(test)]
mod tests {
use super::*;
fn nah(a: f64, b: f64) {
assert!((a - b).abs() < 1e-6, "{a} ≠ {b}");
}
#[test]
fn endwert_einer_rentenzahlung() {
// 10 Perioden, 5 % Zins, Zahlung 100 nachschüssig, Barwert 0.
nah(fv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap(), 1257.789253);
}
#[test]
fn barwert_und_endwert_sind_gegenlaeufig() {
let b = pv(0.05, 10.0, -100.0, 0.0, 0.0).unwrap();
nah(fv(0.05, 10.0, -100.0, b, 0.0).unwrap(), 0.0);
}
#[test]
fn zins_und_tilgung_ergeben_die_zahlung() {
let (r, n, bw) = (0.05, 10.0, 1000.0);
let z = pmt(r, n, bw, 0.0, 0.0).unwrap();
for periode in 1..=10 {
let i = ipmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
let t = ppmt(r, periode as f64, n, bw, 0.0, 0.0).unwrap();
nah(i + t, z);
}
}
#[test]
fn zinssatz_wird_zurueckgerechnet() {
let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
nah(rate(10.0, z, 1000.0, 0.0, 0.0, 0.1).unwrap(), 0.05);
}
#[test]
fn nper_zaehlt_die_perioden() {
let z = pmt(0.05, 10.0, 1000.0, 0.0, 0.0).unwrap();
nah(nper(0.05, z, 1000.0, 0.0, 0.0).unwrap(), 10.0);
}
#[test]
fn kapitalwert_und_interner_zinsfuss() {
let reihe = [-1000.0, 400.0, 400.0, 400.0];
let r = irr(&reihe, 0.1).unwrap();
// Bei r ist der Kapitalwert (inkl. Anfangsauszahlung) null.
let mut summe = 0.0;
let mut nenner = 1.0;
for w in reihe {
summe += w / nenner;
nenner *= 1.0 + r;
}
nah(summe, 0.0);
nah(npv(0.05, &[100.0, 100.0]).unwrap(), 185.941043);
}
#[test]
fn irr_ohne_vorzeichenwechsel_ist_fehler() {
assert!(irr(&[100.0, 200.0], 0.1).is_err());
assert!(irr(&[-100.0, -200.0], 0.1).is_err());
}
#[test]
fn abschreibungen() {
nah(sln(1000.0, 100.0, 10.0).unwrap(), 90.0);
// SYD: Summe über alle Perioden = Abschreibungsvolumen.
let summe: f64 = (1..=10).map(|p| syd(1000.0, 100.0, 10.0, p as f64).unwrap()).sum();
nah(summe, 900.0);
nah(syd(1000.0, 100.0, 10.0, 1.0).unwrap(), 163.636363);
nah(ddb(1000.0, 100.0, 10.0, 1.0).unwrap(), 200.0);
nah(ddb(1000.0, 100.0, 10.0, 2.0).unwrap(), 160.0);
}
#[test]
fn ungueltige_argumente() {
assert!(sln(1.0, 0.0, 0.0).is_err());
assert!(syd(1.0, 0.0, 10.0, 11.0).is_err());
assert!(ipmt(0.05, 11.0, 10.0, 1000.0, 0.0, 0.0).is_err());
assert!(pmt(0.05, 0.0, 1000.0, 0.0, 0.0).is_err());
assert!(nper(0.0, 0.0, 1000.0, 0.0, 0.0).is_err());
}
#[test]
fn mirr_liegt_zwischen_den_zinsen() {
let r = mirr(&[-1000.0, 400.0, 400.0, 400.0], 0.05, 0.06).unwrap();
assert!(r > 0.0 && r < 0.3, "{r}");
}
}