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:
377
crates/tb-runtime/src/finanz.rs
Normal file
377
crates/tb-runtime/src/finanz.rs
Normal 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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user