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:
487
crates/tb-runtime/src/using.rs
Normal file
487
crates/tb-runtime/src/using.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! `PRINT USING`, `LPRINT USING` und `FORMAT$`: Formatzeichenketten des
|
||||
//! Vorbilds.
|
||||
//!
|
||||
//! Die Formatzeichenkette beschreibt Felder; jeder Wert füllt das nächste
|
||||
//! Feld. Sind noch Werte übrig, wenn die Zeichenkette zu Ende ist, beginnt
|
||||
//! sie von vorn. Passt ein Wert nicht in sein Feld, wird er vollständig
|
||||
//! ausgegeben und mit `%` gekennzeichnet.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::value::{as_f64, Value};
|
||||
|
||||
/// Numerisches Feld.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
struct NumFeld {
|
||||
/// Ziffernstellen vor dem Dezimalpunkt (ohne die Stellen aus `$$`/`**`).
|
||||
vor: usize,
|
||||
/// Ziffernstellen nach dem Dezimalpunkt.
|
||||
nach: usize,
|
||||
/// Dezimalpunkt vorhanden?
|
||||
punkt: bool,
|
||||
/// Tausendertrennung (`,` vor dem Punkt).
|
||||
tausender: bool,
|
||||
/// `+` vorangestellt bzw. nachgestellt.
|
||||
plus_vorn: bool,
|
||||
plus_hinten: bool,
|
||||
/// `-` nachgestellt (nur für negative Werte).
|
||||
minus_hinten: bool,
|
||||
/// `$$` — mitlaufendes Währungszeichen.
|
||||
waehrung: bool,
|
||||
/// `**` — führende Stellen mit `*` füllen.
|
||||
sterne: bool,
|
||||
/// `^^^^` — Exponentialform.
|
||||
exponent: bool,
|
||||
}
|
||||
|
||||
/// Stringfeld.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum StrFeld {
|
||||
/// `!` — nur das erste Zeichen.
|
||||
Erstes,
|
||||
/// `\ \` — feste Länge (Anzahl Zeichen inklusive beider Schrägstriche).
|
||||
Fest(usize),
|
||||
/// `&` — der ganze String.
|
||||
Ganz,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Teil {
|
||||
Literal(String),
|
||||
Zahl(NumFeld),
|
||||
Text(StrFeld),
|
||||
}
|
||||
|
||||
/// Währungszeichen für `$$` (siehe `SetFormatCC`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Waehrung {
|
||||
pub zeichen: String,
|
||||
}
|
||||
|
||||
impl Default for Waehrung {
|
||||
fn default() -> Self {
|
||||
Waehrung { zeichen: "$".into() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Zerlegen der Formatzeichenkette ---------------------------------------
|
||||
|
||||
fn zerlege(fmt: &str) -> Vec<Teil> {
|
||||
let z: Vec<char> = fmt.chars().collect();
|
||||
let mut teile = Vec::new();
|
||||
let mut lit = String::new();
|
||||
let mut i = 0;
|
||||
let schiebe = |lit: &mut String, teile: &mut Vec<Teil>| {
|
||||
if !lit.is_empty() {
|
||||
teile.push(Teil::Literal(std::mem::take(lit)));
|
||||
}
|
||||
};
|
||||
|
||||
while i < z.len() {
|
||||
match z[i] {
|
||||
// `_` macht das Folgezeichen literal.
|
||||
'_' if i + 1 < z.len() => {
|
||||
lit.push(z[i + 1]);
|
||||
i += 2;
|
||||
}
|
||||
'!' => {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Erstes));
|
||||
i += 1;
|
||||
}
|
||||
'&' => {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Ganz));
|
||||
i += 1;
|
||||
}
|
||||
// `\ \` — Leerzeichen zwischen zwei Schrägstrichen.
|
||||
'\\' => {
|
||||
let mut j = i + 1;
|
||||
while j < z.len() && z[j] == ' ' {
|
||||
j += 1;
|
||||
}
|
||||
if j < z.len() && z[j] == '\\' {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Text(StrFeld::Fest(j - i + 1)));
|
||||
i = j + 1;
|
||||
} else {
|
||||
lit.push('\\');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'#' | '+' | '.' | '*' | '$' => {
|
||||
if let Some((feld, weiter)) = zahlfeld(&z, i) {
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile.push(Teil::Zahl(feld));
|
||||
i = weiter;
|
||||
} else {
|
||||
lit.push(z[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
lit.push(c);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
schiebe(&mut lit, &mut teile);
|
||||
teile
|
||||
}
|
||||
|
||||
/// Versucht ab `start` ein numerisches Feld zu lesen.
|
||||
fn zahlfeld(z: &[char], start: usize) -> Option<(NumFeld, usize)> {
|
||||
let mut f = NumFeld::default();
|
||||
let mut i = start;
|
||||
|
||||
if z[i] == '+' {
|
||||
f.plus_vorn = true;
|
||||
i += 1;
|
||||
}
|
||||
// `**$`, `**`, `$$` belegen selbst Ziffernstellen.
|
||||
if i + 1 < z.len() && z[i] == '*' && z[i + 1] == '*' {
|
||||
f.sterne = true;
|
||||
f.vor += 2;
|
||||
i += 2;
|
||||
if i < z.len() && z[i] == '$' {
|
||||
f.waehrung = true;
|
||||
f.vor += 1;
|
||||
i += 1;
|
||||
}
|
||||
} else if i + 1 < z.len() && z[i] == '$' && z[i + 1] == '$' {
|
||||
f.waehrung = true;
|
||||
f.vor += 2;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
let ziffern_start = i;
|
||||
while i < z.len() {
|
||||
match z[i] {
|
||||
'#' => {
|
||||
f.vor += 1;
|
||||
i += 1;
|
||||
}
|
||||
',' if i + 1 < z.len() && (z[i + 1] == '#' || z[i + 1] == '.') => {
|
||||
f.tausender = true;
|
||||
i += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
// Ohne jede Ziffernstelle ist es kein Zahlfeld.
|
||||
if i == ziffern_start && !f.waehrung && !f.sterne {
|
||||
return None;
|
||||
}
|
||||
if i < z.len() && z[i] == '.' {
|
||||
f.punkt = true;
|
||||
i += 1;
|
||||
while i < z.len() && z[i] == '#' {
|
||||
f.nach += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
// Exponentialform: vier oder mehr `^`.
|
||||
let exp_start = i;
|
||||
while i < z.len() && z[i] == '^' {
|
||||
i += 1;
|
||||
}
|
||||
if i - exp_start >= 4 {
|
||||
f.exponent = true;
|
||||
} else {
|
||||
i = exp_start;
|
||||
}
|
||||
if i < z.len() && z[i] == '+' {
|
||||
f.plus_hinten = true;
|
||||
i += 1;
|
||||
} else if i < z.len() && z[i] == '-' {
|
||||
f.minus_hinten = true;
|
||||
i += 1;
|
||||
}
|
||||
Some((f, i))
|
||||
}
|
||||
|
||||
// ---- Werte formatieren ------------------------------------------------------
|
||||
|
||||
fn zahl_formatieren(f: &NumFeld, wert: f64, waehrung: &Waehrung) -> String {
|
||||
let negativ = wert < 0.0 || (wert == 0.0 && wert.is_sign_negative());
|
||||
let betrag = wert.abs();
|
||||
|
||||
if f.exponent {
|
||||
return exponent_formatieren(f, wert);
|
||||
}
|
||||
|
||||
// Ziffern runden (kaufmännisch zur geraden Zahl wie sonst auch).
|
||||
let gerundet = runde(betrag, f.nach);
|
||||
let mut ganz = format!("{:.0}", gerundet.trunc());
|
||||
let bruch = if f.punkt {
|
||||
let b = gerundet - gerundet.trunc();
|
||||
let s = format!("{:.*}", f.nach, b);
|
||||
// "0.25" → "25"
|
||||
s.split_once('.').map(|(_, r)| r.to_string()).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// Die Tausendertrennung fügt Zeichen hinzu, die keine Ziffernstelle
|
||||
// belegen; der Überlauf entscheidet sich an der Ziffernzahl.
|
||||
let ziffern = ganz.chars().count();
|
||||
if f.tausender {
|
||||
ganz = tausender_gruppieren(&ganz);
|
||||
}
|
||||
|
||||
// Vorzeichen mit eigener Position: `+` vorn bzw. `+`/`-` hinten stehen
|
||||
// neben dem Zahlenfeld, nicht darin. Ohne Vorzeichenangabe teilt sich ein
|
||||
// `-` die Ziffernstellen.
|
||||
let (vorz_vorn, vorz_hinten, minus_im_feld) = if f.plus_vorn {
|
||||
(if negativ { "-" } else { "+" }.to_string(), String::new(), false)
|
||||
} else if f.plus_hinten {
|
||||
(String::new(), if negativ { "-" } else { "+" }.to_string(), false)
|
||||
} else if f.minus_hinten {
|
||||
(String::new(), if negativ { "-" } else { " " }.to_string(), false)
|
||||
} else {
|
||||
(String::new(), String::new(), negativ)
|
||||
};
|
||||
|
||||
// Inhalt der Ziffernstellen, rechtsbündig: Vorzeichen (falls im Feld),
|
||||
// Währungszeichen, Ziffern. `$$`/`**` haben ihre Stellen mitgebracht.
|
||||
let mut inhalt = String::new();
|
||||
if minus_im_feld {
|
||||
inhalt.push('-');
|
||||
}
|
||||
if f.waehrung {
|
||||
inhalt.push_str(&waehrung.zeichen);
|
||||
}
|
||||
inhalt.push_str(&ganz);
|
||||
|
||||
let ueberlauf = ziffern + usize::from(minus_im_feld) + usize::from(f.waehrung) > f.vor;
|
||||
let gefuellt = if ueberlauf {
|
||||
inhalt
|
||||
} else {
|
||||
// Trennzeichen belegen keine Ziffernstelle und können die Breite
|
||||
// überschreiten, ohne Überlauf zu sein.
|
||||
let fehlt = f.vor.saturating_sub(inhalt.chars().count());
|
||||
let fueller = if f.sterne { '*' } else { ' ' };
|
||||
let mut t: String = std::iter::repeat_n(fueller, fehlt).collect();
|
||||
t.push_str(&inhalt);
|
||||
t
|
||||
};
|
||||
|
||||
let mut out = String::new();
|
||||
if ueberlauf {
|
||||
out.push('%');
|
||||
}
|
||||
out.push_str(&vorz_vorn);
|
||||
out.push_str(&gefuellt);
|
||||
if f.punkt {
|
||||
out.push('.');
|
||||
out.push_str(&bruch);
|
||||
}
|
||||
out.push_str(&vorz_hinten);
|
||||
out
|
||||
}
|
||||
|
||||
/// Kaufmännische Rundung zur geraden Zahl auf `stellen` Nachkommastellen.
|
||||
fn runde(x: f64, stellen: usize) -> f64 {
|
||||
let faktor = 10f64.powi(stellen as i32);
|
||||
let skaliert = x * faktor;
|
||||
let ab = skaliert.floor();
|
||||
let rest = skaliert - ab;
|
||||
let gerundet = if (rest - 0.5).abs() < f64::EPSILON * skaliert.abs().max(1.0) {
|
||||
if (ab as i64) % 2 == 0 {
|
||||
ab
|
||||
} else {
|
||||
ab + 1.0
|
||||
}
|
||||
} else {
|
||||
skaliert.round()
|
||||
};
|
||||
gerundet / faktor
|
||||
}
|
||||
|
||||
fn tausender_gruppieren(s: &str) -> String {
|
||||
let z: Vec<char> = s.chars().collect();
|
||||
let mut out = String::new();
|
||||
for (i, c) in z.iter().enumerate() {
|
||||
if i > 0 && (z.len() - i) % 3 == 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(*c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn exponent_formatieren(f: &NumFeld, wert: f64) -> String {
|
||||
let stellen = f.nach;
|
||||
let s = format!("{:.*E}", stellen, wert);
|
||||
// Rust liefert `1.5E3`; das Vorbild schreibt `1.5E+03`.
|
||||
match s.split_once('E') {
|
||||
Some((m, e)) => {
|
||||
let (vz, betrag) = match e.strip_prefix('-') {
|
||||
Some(r) => ('-', r),
|
||||
None => ('+', e.trim_start_matches('+')),
|
||||
};
|
||||
format!("{m}E{vz}{betrag:0>2}")
|
||||
}
|
||||
None => s,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_formatieren(f: &StrFeld, s: &str) -> String {
|
||||
match f {
|
||||
StrFeld::Erstes => s.chars().take(1).collect(),
|
||||
StrFeld::Ganz => s.to_string(),
|
||||
StrFeld::Fest(n) => {
|
||||
let mut t: String = s.chars().take(*n).collect();
|
||||
let fehlt = n.saturating_sub(t.chars().count());
|
||||
t.extend(std::iter::repeat_n(' ', fehlt));
|
||||
t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Einstiegspunkt ---------------------------------------------------------
|
||||
|
||||
/// Werte nach `fmt` formatieren. Die Zeichenkette wird so oft wiederholt,
|
||||
/// wie Werte übrig sind; nach dem letzten Wert folgen noch die Literale bis
|
||||
/// zum nächsten Feld.
|
||||
pub fn using(fmt: &str, werte: &[Value], waehrung: &Waehrung) -> Result<String, RuntimeError> {
|
||||
let teile = zerlege(fmt);
|
||||
let hat_feld = teile
|
||||
.iter()
|
||||
.any(|t| matches!(t, Teil::Zahl(_) | Teil::Text(_)));
|
||||
if !hat_feld {
|
||||
if werte.is_empty() {
|
||||
return Ok(fmt.to_string());
|
||||
}
|
||||
// Werte ohne Feld sind ein Bedienfehler (Vorbild: Fehler 5).
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
let mut i = 0usize; // Index im Teile-Vektor
|
||||
let mut w = 0usize; // Index im Wertevektor
|
||||
// Schutz gegen Endlosläufe bei Formaten ohne Fortschritt.
|
||||
let mut runden = 0usize;
|
||||
|
||||
while w < werte.len() {
|
||||
if i >= teile.len() {
|
||||
i = 0;
|
||||
runden += 1;
|
||||
if runden > werte.len() + 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match &teile[i] {
|
||||
Teil::Literal(l) => out.push_str(l),
|
||||
Teil::Zahl(f) => {
|
||||
if matches!(werte[w], Value::Str(_)) {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
}
|
||||
let v = as_f64(&werte[w]);
|
||||
out.push_str(&zahl_formatieren(f, v, waehrung));
|
||||
w += 1;
|
||||
}
|
||||
Teil::Text(f) => {
|
||||
let Value::Str(s) = &werte[w] else {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
};
|
||||
out.push_str(&text_formatieren(f, s));
|
||||
w += 1;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
// Nachlaufende Literale bis zum nächsten Feld.
|
||||
while i < teile.len() {
|
||||
match &teile[i] {
|
||||
Teil::Literal(l) => out.push_str(l),
|
||||
_ => break,
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
fn u(fmt: &str, werte: &[Value]) -> String {
|
||||
using(fmt, werte, &Waehrung::default()).unwrap()
|
||||
}
|
||||
fn z(x: f64) -> Value {
|
||||
Value::Dbl(x)
|
||||
}
|
||||
fn s(x: &str) -> Value {
|
||||
Value::Str(Rc::from(x))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numerisches_feld_mit_nachkommastellen() {
|
||||
assert_eq!(u("###.##", &[z(3.14159)]), " 3.14");
|
||||
assert_eq!(u("##.##", &[z(-1.5)]), "-1.50");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feldueberlauf_bekommt_prozentzeichen() {
|
||||
let r = u("##", &[z(12345.0)]);
|
||||
assert!(r.starts_with('%'), "{r}");
|
||||
assert!(r.contains("12345"), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_wird_wiederverwendet() {
|
||||
assert_eq!(u("## ", &[z(1.0), z(2.0), z(3.0)]), " 1 2 3 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tausendertrennung() {
|
||||
assert_eq!(u("#######", &[z(1234567.0)]), "1234567");
|
||||
assert_eq!(u("##,#####", &[z(1234567.0)]), "1,234,567");
|
||||
// Mehr Ziffern als Stellen → Überlauf mit %-Präfix.
|
||||
assert!(u("##,####", &[z(1234567.0)]).starts_with('%'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vorzeichenfuehrung() {
|
||||
assert_eq!(u("+###", &[z(12.0)]), "+ 12");
|
||||
assert_eq!(u("###-", &[z(-12.0)]), " 12-");
|
||||
assert_eq!(u("###-", &[z(12.0)]), " 12 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waehrung_und_sterne() {
|
||||
// `$$` bringt zwei Stellen mit, eine davon ist das Zeichen selbst.
|
||||
assert_eq!(u("$$###", &[z(12.0)]), " $12");
|
||||
assert_eq!(u("**###", &[z(12.0)]), "***12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringfelder() {
|
||||
assert_eq!(u("!", &[s("abc")]), "a");
|
||||
assert_eq!(u("&", &[s("abc")]), "abc");
|
||||
assert_eq!(u("\\ \\", &[s("abcdef")]), "abcd");
|
||||
assert_eq!(u("\\ \\", &[s("ab")]), "ab ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literale_und_unterstrich() {
|
||||
assert_eq!(u("Wert: ##!", &[z(7.0), s("x")]), "Wert: 7x");
|
||||
assert_eq!(u("_###", &[z(7.0)]), "# 7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exponentialform() {
|
||||
assert_eq!(u("#.##^^^^", &[z(1234.0)]), "1.23E+03");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_feld_aber_mit_werten_ist_fehler_5() {
|
||||
let e = using("nur Text", &[z(1.0)], &Waehrung::default()).unwrap_err();
|
||||
assert_eq!(e, RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_feld_und_ohne_werte_bleibt_der_text() {
|
||||
assert_eq!(u("nur Text", &[]), "nur Text");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user