//! `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 { let z: Vec = 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| { 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 = s.chars().collect(); let mut out = String::new(); for (i, c) in z.iter().enumerate() { if i > 0 && (z.len() - i).is_multiple_of(3) { 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 { 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.126)]), " 3.13"); 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"); } }