formular lesen (binär) und openspec für codex

This commit is contained in:
2026-09-05 08:36:40 +02:00
parent 05837cd846
commit 1cb6ae8fd9
34 changed files with 2211 additions and 149 deletions

View File

@@ -520,10 +520,7 @@ fn fehlende_groessenangabe_wird_abgewiesen() {
std::fs::write(&f, "PRINT 1\n").unwrap();
let r = std::panic::catch_unwind(|| run_corpus_file(&f, None));
let e = r.expect_err("Datei ohne Größenangabe muss abgewiesen werden");
let m = e
.downcast_ref::<String>()
.cloned()
.unwrap_or_default();
let m = e.downcast_ref::<String>().cloned().unwrap_or_default();
assert!(m.contains("keine Bildschirmgröße deklariert"), "{m}");
let _ = std::fs::remove_dir_all(&dir);
}

View File

@@ -6,8 +6,8 @@
//! nach `LOCATE`, `CLS` oder einem Umbruch am rechten Rand überein.
use crate::errors::RuntimeError;
use crate::format::format_print;
use crate::fileio::Dateien;
use crate::format::format_print;
use crate::screen::TextScreen;
use crate::value::Value;

View File

@@ -264,7 +264,10 @@ mod tests {
// In einer Zone mit Sommerzeit unterscheiden sie sich um genau
// die Umstellungsspanne, sonst gar nicht.
let d = (a - b).abs();
assert!(d == 0 || (1800..=7200).contains(&d), "Spanne {d} unplausibel");
assert!(
d == 0 || (1800..=7200).contains(&d),
"Spanne {d} unplausibel"
);
}
}

View File

@@ -84,12 +84,14 @@ impl Datei {
}
let datei = opt.open(pfad).map_err(fehler_aus_io)?;
let leser = match modus {
Modus::Input => Some(BufReader::new(
File::open(pfad).map_err(fehler_aus_io)?,
)),
Modus::Input => Some(BufReader::new(File::open(pfad).map_err(fehler_aus_io)?)),
_ => None,
};
let reclen = if modus == Modus::Random { reclen.max(1) } else { 1 };
let reclen = if modus == Modus::Random {
reclen.max(1)
} else {
1
};
Ok(Datei {
modus,
pfad: pfad.to_path_buf(),
@@ -104,7 +106,10 @@ impl Datei {
/// Dateigröße in Bytes.
pub fn laenge(&self) -> Result<u64, RuntimeError> {
self.datei.metadata().map(|m| m.len()).map_err(fehler_aus_io)
self.datei
.metadata()
.map(|m| m.len())
.map_err(fehler_aus_io)
}
/// `EOF` — bei sequenziellem Lesen: nichts mehr im Puffer; sonst:
@@ -127,9 +132,7 @@ impl Datei {
if !matches!(self.modus, Modus::Output | Modus::Append) {
return Err(RuntimeError(54)); // Bad file mode
}
self.datei
.write_all(text.as_bytes())
.map_err(fehler_aus_io)
self.datei.write_all(text.as_bytes()).map_err(fehler_aus_io)
}
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
@@ -243,7 +246,8 @@ impl Dateien {
if modus == Modus::Input && !p.exists() {
return Err(RuntimeError(53)); // File not found
}
self.offen.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
self.offen
.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
Ok(())
}
@@ -403,7 +407,10 @@ pub fn wert_schreiben(
feld_setzen(puffer, offset, *len as usize, s, false)
}
(TypeInit::Udt(i), Value::Rec(r)) => {
let felder = &layouts.get(*i as usize).ok_or(RuntimeError::TYPE_MISMATCH)?.fields;
let felder = &layouts
.get(*i as usize)
.ok_or(RuntimeError::TYPE_MISMATCH)?
.fields;
let rec = r.borrow();
let mut off = offset;
for (ft, fv) in felder.iter().zip(rec.fields.iter()) {
@@ -433,11 +440,14 @@ pub fn wert_lesen(
TypeInit::Sng => Value::Sng(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
TypeInit::Dbl => Value::Dbl(f64::from_le_bytes(b[..8].try_into().unwrap())),
TypeInit::Cur => Value::Cur(i64::from_le_bytes(b[..8].try_into().unwrap())),
TypeInit::FixedStr(len) => {
Value::Str(std::rc::Rc::from(feld_lesen(puffer, offset, *len as usize).as_str()))
}
TypeInit::FixedStr(len) => Value::Str(std::rc::Rc::from(
feld_lesen(puffer, offset, *len as usize).as_str(),
)),
TypeInit::Udt(i) => {
let felder = &layouts.get(*i as usize).ok_or(RuntimeError::TYPE_MISMATCH)?.fields;
let felder = &layouts
.get(*i as usize)
.ok_or(RuntimeError::TYPE_MISMATCH)?
.fields;
let mut off = offset;
let mut werte = Vec::with_capacity(felder.len());
for ft in felder {
@@ -468,10 +478,10 @@ pub fn pfad_normieren(p: &str) -> PathBuf {
fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
use std::io::ErrorKind::*;
match e.kind() {
NotFound => RuntimeError(53), // File not found
NotFound => RuntimeError(53), // File not found
PermissionDenied => RuntimeError(70), // Permission denied
AlreadyExists => RuntimeError(58), // File already exists
_ => RuntimeError(57), // Device I/O error
AlreadyExists => RuntimeError(58), // File already exists
_ => RuntimeError(57), // Device I/O error
}
}
@@ -495,7 +505,9 @@ pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
/// `MKDIR` / `RMDIR` / `CHDIR`.
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
let p = pfad_normieren(pfad);
if p.parent().is_some_and(|e| !e.as_os_str().is_empty() && !e.exists()) {
if p.parent()
.is_some_and(|e| !e.as_os_str().is_empty() && !e.exists())
{
return Err(RuntimeError(76)); // Path not found
}
std::fs::create_dir(p).map_err(fehler_aus_io)
@@ -558,11 +570,7 @@ pub fn passt(name: &str, muster: &str) -> bool {
None => n.is_empty(),
Some('*') => rek(n, &m[1..]) || (!n.is_empty() && rek(&n[1..], m)),
Some('?') => !n.is_empty() && rek(&n[1..], &m[1..]),
Some(c) => {
!n.is_empty()
&& n[0].eq_ignore_ascii_case(c)
&& rek(&n[1..], &m[1..])
}
Some(c) => !n.is_empty() && n[0].eq_ignore_ascii_case(c) && rek(&n[1..], &m[1..]),
}
}
rek(&n, &m)

View File

@@ -86,14 +86,7 @@ pub fn nper(zins: f64, zahlung: f64, barwert: f64, endwert: f64, faellig: f64) -
}
/// `IPmt#` — Zinsanteil der Periode `periode` (1-basiert).
pub fn ipmt(
zins: f64,
periode: f64,
perioden: f64,
barwert: f64,
endwert: f64,
faellig: f64,
) -> R {
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());
}
@@ -111,14 +104,7 @@ pub fn ipmt(
}
/// `PPmt#` — Tilgungsanteil der Periode.
pub fn ppmt(
zins: f64,
periode: f64,
perioden: f64,
barwert: f64,
endwert: f64,
faellig: f64,
) -> R {
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)
@@ -144,7 +130,11 @@ pub fn rate(
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 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()),
@@ -206,7 +196,11 @@ pub fn irr(werte: &[f64], schaetzung: f64) -> R {
Some(summe)
};
let mut r0 = schaetzung;
let mut r1 = if schaetzung == 0.0 { 0.1 } else { schaetzung * 1.1 };
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()),
@@ -246,7 +240,8 @@ pub fn mirr(werte: &[f64], finanzierungszins: f64, wiederanlagezins: f64) -> R {
return Err(ungueltig());
}
let n = n as f64;
let verhaeltnis = -bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins));
let verhaeltnis =
-bw_pos * (1.0 + wiederanlagezins).powf(n) / (bw_neg * (1.0 + finanzierungszins));
if verhaeltnis <= 0.0 {
return Err(ungueltig());
}
@@ -266,8 +261,10 @@ pub fn syd(anschaffung: f64, restwert: f64, nutzungsdauer: f64, periode: f64) ->
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)))
Ok(
(anschaffung - restwert) * (nutzungsdauer - periode + 1.0) * 2.0
/ (nutzungsdauer * (nutzungsdauer + 1.0)),
)
}
/// `DDB#` — geometrisch-degressive Abschreibung (doppelter linearer Satz).
@@ -353,7 +350,9 @@ mod tests {
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();
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);

View File

@@ -69,7 +69,11 @@ fn fmt_float(x: f64, sig: usize, expch: char) -> String {
return "NaN".to_string();
}
if x.is_infinite() {
return if x < 0.0 { "-1E+38".into() } else { "1E+38".into() };
return if x < 0.0 {
"-1E+38".into()
} else {
"1E+38".into()
};
}
let neg = x < 0.0;
let ax = x.abs();
@@ -92,7 +96,11 @@ fn fmt_float(x: f64, sig: usize, expch: char) -> String {
m.push('.');
m.push_str(&digits[1..]);
}
format!("{m}{expch}{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
format!(
"{m}{expch}{}{:02}",
if exp < 0 { '-' } else { '+' },
exp.abs()
)
} else if exp >= 0 {
let e = exp as usize;
if (e + 1) >= digits.len() {
@@ -128,7 +136,10 @@ pub fn val(s: &str) -> f64 {
return i64::from_str_radix(&hex, 16).unwrap_or(0) as f64;
}
if let Some(rest) = t.strip_prefix("&O").or_else(|| t.strip_prefix("&o")) {
let oct: String = rest.chars().take_while(|c| ('0'..='7').contains(c)).collect();
let oct: String = rest
.chars()
.take_while(|c| ('0'..='7').contains(c))
.collect();
return i64::from_str_radix(&oct, 8).unwrap_or(0) as f64;
}
let bytes: Vec<char> = t.chars().collect();

View File

@@ -154,10 +154,19 @@ struct Bindung {
#[derive(Debug, Clone)]
enum Undo {
/// Der Satz wurde eingefügt — Rücknahme entfernt ihn wieder.
Eingefuegt { pfad: PathBuf, tabelle: String, id: u64 },
Eingefuegt {
pfad: PathBuf,
tabelle: String,
id: u64,
},
/// Der Satz wurde geändert oder gelöscht — Rücknahme stellt die
/// alten Bytes wieder her.
Vorher { pfad: PathBuf, tabelle: String, id: u64, bytes: Vec<u8> },
Vorher {
pfad: PathBuf,
tabelle: String,
id: u64,
bytes: Vec<u8>,
},
}
impl Undo {
@@ -294,7 +303,11 @@ fn schluessel_anhaengen(out: &mut Vec<u8>, wert: &Value, typ: &TypeInit, absteig
}
TypeInit::Sng => {
let bits = (crate::value::as_f64(wert) as f32).to_bits();
let k = if bits & 0x8000_0000 != 0 { !bits } else { bits | 0x8000_0000 };
let k = if bits & 0x8000_0000 != 0 {
!bits
} else {
bits | 0x8000_0000
};
out.extend_from_slice(&k.to_be_bytes());
}
TypeInit::Dbl => {
@@ -538,11 +551,7 @@ impl Isam {
return Err(RuntimeError::TYPE_MISMATCH);
}
let layout = Layout {
spalten: namen
.iter()
.map(|n| n.to_string())
.zip(felder)
.collect(),
spalten: namen.iter().map(|n| n.to_string()).zip(felder).collect(),
};
let pfad = fileio::pfad_normieren(datenbank);
@@ -616,9 +625,7 @@ impl Isam {
}
}
Some(_) => return Err(INKONSISTENT),
None if neu => {
meta_schreiben(&txn, "version", &FORMATVERSION.to_le_bytes())?
}
None if neu => meta_schreiben(&txn, "version", &FORMATVERSION.to_le_bytes())?,
// Bestehende Datei ohne Versionsmarke: keine Datenbank
// dieses Formats.
None => return Err(INKONSISTENT),
@@ -760,7 +767,11 @@ impl Isam {
if spalten.is_empty() {
return Err(SPALTE_UNGUELTIG);
}
let mut def = Indexdef { name: name.to_string(), eindeutig, spalten: Vec::new() };
let mut def = Indexdef {
name: name.to_string(),
eindeutig,
spalten: Vec::new(),
};
for s in spalten {
let (absteigend, sname) = match s.strip_prefix('-') {
Some(r) => (true, r),
@@ -930,7 +941,11 @@ impl Isam {
let id = self.schreiben(&pfad, move |txn, log| {
let id = naechste_id(txn, &tabelle)?;
satz_schreiben(txn, &tabelle, &layout, &udts, id, Some(&bytes), None)?;
log.push(Undo::Eingefuegt { pfad: fuer_log, tabelle: tabelle.clone(), id });
log.push(Undo::Eingefuegt {
pfad: fuer_log,
tabelle: tabelle.clone(),
id,
});
Ok(id)
})?;
self.belegt += bytes_von(&self.trans);
@@ -1132,7 +1147,9 @@ impl Isam {
praefix.clone()
};
let mut bereich = t.range(von.as_slice()..).map_err(|_| INKONSISTENT)?;
let Some(e) = bereich.next() else { return Ok(None) };
let Some(e) = bereich.next() else {
return Ok(None);
};
let (k, v) = e.map_err(|_| INKONSISTENT)?;
if art == Suchart::Gleich && !k.value().starts_with(&praefix) {
return Ok(None);
@@ -1215,7 +1232,10 @@ impl Isam {
/// `SAVEPOINT` — liefert die Kennung des gesetzten Sicherungspunkts.
pub fn sicherungspunkt(&mut self) -> Result<i16, RuntimeError> {
let t = self.trans.as_mut().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let t = self
.trans
.as_mut()
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let kennung = t.naechste_kennung;
t.naechste_kennung = t.naechste_kennung.saturating_add(1);
let pos = t.log.len();
@@ -1283,18 +1303,15 @@ impl Isam {
let layout = self.layout_von(&pfad, &tabelle)?;
self.schreiben(&pfad, move |txn, _| {
let alt = satz_bytes_lesen(txn, &tabelle, id)?;
satz_schreiben(
txn,
&tabelle,
&layout,
&udts,
id,
None,
alt.as_deref(),
)
satz_schreiben(txn, &tabelle, &layout, &udts, id, None, alt.as_deref())
})?;
}
Undo::Vorher { pfad, tabelle, id, bytes } => {
Undo::Vorher {
pfad,
tabelle,
id,
bytes,
} => {
let layout = self.layout_von(&pfad, &tabelle)?;
self.schreiben(&pfad, move |txn, _| {
let alt = satz_bytes_lesen(txn, &tabelle, id)?;
@@ -1372,7 +1389,10 @@ fn satz_bytes_lesen(
let name = satz_def(tabelle);
let def: SatzTab = TableDefinition::new(&name);
let t = txn.open_table(def).map_err(|_| INKONSISTENT)?;
let gefunden = t.get(id).map_err(|_| INKONSISTENT)?.map(|v| v.value().to_vec());
let gefunden = t
.get(id)
.map_err(|_| INKONSISTENT)?
.map(|v| v.value().to_vec());
Ok(gefunden)
}

View File

@@ -38,8 +38,11 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let a = r.next() as i16;
let b = r.next() as i16;
assert_eq!(
kodiere(&Value::Int(a), &TypeInit::Int, false)
.cmp(&kodiere(&Value::Int(b), &TypeInit::Int, false)),
kodiere(&Value::Int(a), &TypeInit::Int, false).cmp(&kodiere(
&Value::Int(b),
&TypeInit::Int,
false
)),
a.cmp(&b),
"INTEGER {a} vs {b}"
);
@@ -48,8 +51,11 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let a = r.next() as i32;
let b = r.next() as i32;
assert_eq!(
kodiere(&Value::Lng(a), &TypeInit::Lng, false)
.cmp(&kodiere(&Value::Lng(b), &TypeInit::Lng, false)),
kodiere(&Value::Lng(a), &TypeInit::Lng, false).cmp(&kodiere(
&Value::Lng(b),
&TypeInit::Lng,
false
)),
a.cmp(&b),
"LONG {a} vs {b}"
);
@@ -58,8 +64,11 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let a = r.next() as i32 as i64;
let b = r.next() as i32 as i64;
assert_eq!(
kodiere(&Value::Cur(a), &TypeInit::Cur, false)
.cmp(&kodiere(&Value::Cur(b), &TypeInit::Cur, false)),
kodiere(&Value::Cur(a), &TypeInit::Cur, false).cmp(&kodiere(
&Value::Cur(b),
&TypeInit::Cur,
false
)),
a.cmp(&b),
"CURRENCY {a} vs {b}"
);
@@ -68,8 +77,11 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let a = (r.next() as i32 as f64) / 1024.0;
let b = (r.next() as i32 as f64) / 1024.0;
assert_eq!(
kodiere(&Value::Dbl(a), &TypeInit::Dbl, false)
.cmp(&kodiere(&Value::Dbl(b), &TypeInit::Dbl, false)),
kodiere(&Value::Dbl(a), &TypeInit::Dbl, false).cmp(&kodiere(
&Value::Dbl(b),
&TypeInit::Dbl,
false
)),
a.partial_cmp(&b).unwrap(),
"DOUBLE {a} vs {b}"
);
@@ -78,8 +90,11 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
let a = (r.next() as i32 as f32) / 64.0;
let b = (r.next() as i32 as f32) / 64.0;
assert_eq!(
kodiere(&Value::Sng(a), &TypeInit::Sng, false)
.cmp(&kodiere(&Value::Sng(b), &TypeInit::Sng, false)),
kodiere(&Value::Sng(a), &TypeInit::Sng, false).cmp(&kodiere(
&Value::Sng(b),
&TypeInit::Sng,
false
)),
a.partial_cmp(&b).unwrap(),
"SINGLE {a} vs {b}"
);
@@ -88,14 +103,19 @@ fn kodierung_erhaelt_die_ordnung_bei_zufallspaaren() {
// Dialekts, der auf UTF-8-Bytes vergleicht (interp::CmpStr).
let mach = |x: u32| -> String {
let zeichen = ['a', 'b', 'z', 'A', 'Z', 'ä', 'ß', '€'];
(0..4).map(|i| zeichen[((x >> (i * 3)) & 7) as usize]).collect()
(0..4)
.map(|i| zeichen[((x >> (i * 3)) & 7) as usize])
.collect()
};
let sa = mach(r.next());
let sb = mach(r.next());
let t = TypeInit::FixedStr(4);
assert_eq!(
kodiere(&Value::Str(Rc::from(sa.as_str())), &t, false)
.cmp(&kodiere(&Value::Str(Rc::from(sb.as_str())), &t, false)),
kodiere(&Value::Str(Rc::from(sa.as_str())), &t, false).cmp(&kodiere(
&Value::Str(Rc::from(sb.as_str())),
&t,
false
)),
sa.as_bytes().cmp(sb.as_bytes()),
"TEXT {sa:?} vs {sb:?}"
);
@@ -181,7 +201,10 @@ impl Drop for TempDb {
}
fn isam_mit_tabelle(db: &str) -> Isam {
let mut i = Isam { udts: test_udts(), ..Default::default() };
let mut i = Isam {
udts: test_udts(),
..Default::default()
};
i.oeffnen(1, db, "Kunden", "Nummer,Name", 0).unwrap();
i
}
@@ -203,9 +226,15 @@ fn hoehere_formatversion_wird_abgewiesen() {
meta_schreiben(&txn, "version", &(FORMATVERSION + 1).to_le_bytes()).unwrap();
txn.commit().unwrap();
}
let mut i = Isam { udts: test_udts(), ..Default::default() };
let mut i = Isam {
udts: test_udts(),
..Default::default()
};
let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err();
assert_eq!(e, INKONSISTENT, "höhere Formatversion muss benannt scheitern");
assert_eq!(
e, INKONSISTENT,
"höhere Formatversion muss benannt scheitern"
);
}
#[test]
@@ -213,7 +242,10 @@ fn beschaedigte_datei_meldet_fehler_88() {
let t = TempDb::neu("kaputt");
let pfad = t.pfad("kaputt.isam");
std::fs::write(&pfad, b"das ist keine Datenbankdatei").unwrap();
let mut i = Isam { udts: test_udts(), ..Default::default() };
let mut i = Isam {
udts: test_udts(),
..Default::default()
};
let e = i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap_err();
assert_eq!(e, RuntimeError(88));
}
@@ -229,7 +261,10 @@ fn geloeschte_satz_id_wird_nicht_neu_vergeben() {
i.satz_loeschen(1).unwrap();
i.einfuegen(1, &satz(2, "zwei")).unwrap();
let zweite = i.satznummer(1).unwrap();
assert_ne!(erste, zweite, "eine gelöschte ID darf nicht neu vergeben werden");
assert_ne!(
erste, zweite,
"eine gelöschte ID darf nicht neu vergeben werden"
);
assert!(zweite > erste);
}
@@ -249,8 +284,12 @@ fn satz_roundtrip_ueber_alle_feldtypen() {
TypeInit::FixedStr(5),
],
}];
let mut i = Isam { udts, ..Default::default() };
i.oeffnen(1, &t.pfad("db.isam"), "Alles", "A,B,C,D,E,F", 0).unwrap();
let mut i = Isam {
udts,
..Default::default()
};
i.oeffnen(1, &t.pfad("db.isam"), "Alles", "A,B,C,D,E,F", 0)
.unwrap();
let original = Value::Rec(Rc::new(RefCell::new(RecordObj {
fields: vec![
Value::Int(-7),
@@ -262,7 +301,9 @@ fn satz_roundtrip_ueber_alle_feldtypen() {
],
})));
i.einfuegen(1, &original).unwrap();
let Value::Rec(gelesen) = i.satz_lesen(1).unwrap() else { panic!("kein Record") };
let Value::Rec(gelesen) = i.satz_lesen(1).unwrap() else {
panic!("kein Record")
};
let f = &gelesen.borrow().fields;
assert!(matches!(f[0], Value::Int(-7)));
assert!(matches!(f[1], Value::Lng(123456)));
@@ -279,7 +320,8 @@ fn satz_roundtrip_ueber_alle_feldtypen() {
fn indexeintraege_stehen_in_schluesselreihenfolge() {
let t = TempDb::neu("indexordnung");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.index_anlegen(1, "NachName", false, &["Name".into()]).unwrap();
i.index_anlegen(1, "NachName", false, &["Name".into()])
.unwrap();
for (n, name) in [(3, "Cäsar"), (1, "Anton"), (2, "Berta")] {
i.einfuegen(1, &satz(n, name)).unwrap();
}
@@ -287,8 +329,12 @@ fn indexeintraege_stehen_in_schluesselreihenfolge() {
let mut namen = Vec::new();
i.bewegen(1, Richtung::Erster).unwrap();
while !i.eof(1) {
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
let Value::Rec(r) = i.satz_lesen(1).unwrap() else {
panic!()
};
let Value::Str(s) = r.borrow().fields[1].clone() else {
panic!()
};
namen.push(s.trim_end().to_string());
i.bewegen(1, Richtung::Naechster).unwrap();
}
@@ -303,7 +349,8 @@ fn indexeintraege_stehen_in_schluesselreihenfolge() {
fn cursor_ueberlebt_eine_satzaenderung() {
let t = TempDb::neu("cursor");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.index_anlegen(1, "NachName", false, &["Name".into()]).unwrap();
i.index_anlegen(1, "NachName", false, &["Name".into()])
.unwrap();
for (n, name) in [(1, "Anton"), (2, "Berta"), (3, "Cäsar")] {
i.einfuegen(1, &satz(n, name)).unwrap();
}
@@ -313,18 +360,31 @@ fn cursor_ueberlebt_eine_satzaenderung() {
let vorher = i.satznummer(1).unwrap();
// Über eine zweite Bindung einen anderen Satz einfügen.
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0).unwrap();
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0)
.unwrap();
i.einfuegen(2, &satz(4, "Anna")).unwrap();
assert_eq!(i.satznummer(1).unwrap(), vorher, "Cursor darf nicht wandern");
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
assert_eq!(
i.satznummer(1).unwrap(),
vorher,
"Cursor darf nicht wandern"
);
let Value::Rec(r) = i.satz_lesen(1).unwrap() else {
panic!()
};
let Value::Str(s) = r.borrow().fields[1].clone() else {
panic!()
};
assert_eq!(s.trim_end(), "Berta");
// Und er bewegt sich weiter in der neuen Ordnung.
i.bewegen(1, Richtung::Naechster).unwrap();
let Value::Rec(r) = i.satz_lesen(1).unwrap() else { panic!() };
let Value::Str(s) = r.borrow().fields[1].clone() else { panic!() };
let Value::Rec(r) = i.satz_lesen(1).unwrap() else {
panic!()
};
let Value::Str(s) = r.borrow().fields[1].clone() else {
panic!()
};
assert_eq!(s.trim_end(), "Cäsar");
}
@@ -383,12 +443,16 @@ fn ungueltige_namen_werden_abgewiesen() {
fn close_beendet_keine_transaktion() {
let t = TempDb::neu("close_trans");
let mut i = isam_mit_tabelle(&t.pfad("db.isam"));
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0).unwrap();
i.oeffnen(2, &t.pfad("db.isam"), "Kunden", "Nummer,Name", 0)
.unwrap();
i.trans_beginn().unwrap();
i.einfuegen(1, &satz(1, "eins")).unwrap();
i.schliessen(2).unwrap();
assert!(i.trans.is_some(), "CLOSE darf die Transaktion nicht beenden");
assert!(
i.trans.is_some(),
"CLOSE darf die Transaktion nicht beenden"
);
// Die Rücknahme wirkt danach noch auf die Änderung an #1.
i.ruecknahme(ROLLBACK_ALL).unwrap();
@@ -407,7 +471,10 @@ fn nicht_festgeschriebene_transaktion_verfaellt() {
i.einfuegen(1, &satz(1, "eins")).unwrap();
i.alles_schliessen();
}
let mut i = Isam { udts: test_udts(), ..Default::default() };
let mut i = Isam {
udts: test_udts(),
..Default::default()
};
i.oeffnen(1, &pfad, "Kunden", "Nummer,Name", 0).unwrap();
assert_eq!(
i.satzzahl(1).unwrap(),
@@ -427,7 +494,9 @@ fn gescheitertes_createindex_hinterlaesst_keinen_index() {
i.einfuegen(1, &satz(2, "gleich")).unwrap();
i.trans_beginn().unwrap();
let e = i.index_anlegen(1, "Eindeutig", true, &["Name".into()]).unwrap_err();
let e = i
.index_anlegen(1, "Eindeutig", true, &["Name".into()])
.unwrap_err();
assert_eq!(e, DOPPELTER_SCHLUESSEL);
i.trans_festschreiben().unwrap();

View File

@@ -39,7 +39,12 @@ pub struct Cell {
impl Default for Cell {
fn default() -> Self {
Cell { ch: ' ', fg: 7, bg: 0, fortsetzung: false }
Cell {
ch: ' ',
fg: 7,
bg: 0,
fortsetzung: false,
}
}
}
@@ -125,8 +130,15 @@ impl TextScreen {
if cols == self.cols && rows == self.rows {
return;
}
let mut cells =
vec![Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false }; cols * rows];
let mut cells = vec![
Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false
};
cols * rows
];
for row in 0..self.rows.min(rows) {
for col in 0..self.cols.min(cols) {
cells[row * cols + col] = self.cells[row * self.cols + col];
@@ -158,7 +170,12 @@ impl TextScreen {
/// Cursor an den Anfang des Bereichs.
pub fn cls(&mut self) {
self.veraendert = true;
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
let blank = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
for row in self.view_top..=self.view_bottom {
self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
}
@@ -190,7 +207,11 @@ impl TextScreen {
/// Breite der belegten Zeile `z` (1-basiert); 0 für unberührte Zeilen.
pub fn belegte_breite(&self, z: usize) -> usize {
self.belegt.get(z - 1).copied().flatten().map_or(0, |c| c + 1)
self.belegt
.get(z - 1)
.copied()
.flatten()
.map_or(0, |c| c + 1)
}
/// `COLOR vg, hg` (vg 031, hg 07). Blinkende Vordergrundfarben
@@ -273,10 +294,19 @@ impl TextScreen {
let attr = (self.fg, self.bg);
let start = self.cur_row * self.cols + self.cur_col;
self.haelften_freimachen(self.cur_col, breite);
self.cells[start] = Cell { ch, fg: attr.0, bg: attr.1, fortsetzung: false };
self.cells[start] = Cell {
ch,
fg: attr.0,
bg: attr.1,
fortsetzung: false,
};
for i in 1..breite {
self.cells[start + i] =
Cell { ch: ' ', fg: attr.0, bg: attr.1, fortsetzung: true };
self.cells[start + i] = Cell {
ch: ' ',
fg: attr.0,
bg: attr.1,
fortsetzung: true,
};
}
let letzte = self.cur_col + breite - 1;
let b = &mut self.belegt[self.cur_row];
@@ -293,7 +323,12 @@ impl TextScreen {
/// dessen Fortsetzung geleert. Sonst blieben Geisterzeichen stehen.
fn haelften_freimachen(&mut self, col: usize, breite: usize) {
let zeilenanfang = self.cur_row * self.cols;
let leer = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
let leer = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
if col > 0 && self.cells[zeilenanfang + col].fortsetzung {
self.cells[zeilenanfang + col - 1] = leer;
}
@@ -323,12 +358,20 @@ impl TextScreen {
self.cur_col -= 1;
// War das gelöschte eine Fortsetzung, gehört die Hälfte davor dazu.
if self.cells[self.cur_row * self.cols + self.cur_col].fortsetzung && self.cur_col > 0 {
self.cells[self.cur_row * self.cols + self.cur_col] =
Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
self.cur_col -= 1;
}
self.cells[self.cur_row * self.cols + self.cur_col] =
Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
// Die gelöschte Zelle gilt nicht mehr als belegt.
let b = &mut self.belegt[self.cur_row];
*b = match (self.cur_col, *b) {
@@ -342,13 +385,17 @@ impl TextScreen {
/// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
pub fn scroll_up(&mut self) {
self.veraendert = true;
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg, fortsetzung: false };
let blank = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
for row in self.view_top..self.view_bottom {
let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
a[row * self.cols..].copy_from_slice(&b[..self.cols]);
}
self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols]
.fill(blank);
self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols].fill(blank);
for row in self.view_top..self.view_bottom {
self.belegt[row] = self.belegt[row + 1];
}
@@ -553,5 +600,4 @@ mod tests {
assert_eq!(s.cell(39, 1).ch, 'a');
assert_eq!(s.cell(40, 1).ch, 'b');
}
}

View File

@@ -59,7 +59,9 @@ pub struct Waehrung {
impl Default for Waehrung {
fn default() -> Self {
Waehrung { zeichen: "$".into() }
Waehrung {
zeichen: "$".into(),
}
}
}
@@ -216,7 +218,9 @@ fn zahl_formatieren(f: &NumFeld, wert: f64, waehrung: &Waehrung) -> String {
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()
s.split_once('.')
.map(|(_, r)| r.to_string())
.unwrap_or_default()
} else {
String::new()
};
@@ -231,11 +235,23 @@ fn zahl_formatieren(f: &NumFeld, wert: f64, waehrung: &Waehrung) -> String {
// 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)
(
if negativ { "-" } else { "+" }.to_string(),
String::new(),
false,
)
} else if f.plus_hinten {
(String::new(), if negativ { "-" } else { "+" }.to_string(), false)
(
String::new(),
if negativ { "-" } else { "+" }.to_string(),
false,
)
} else if f.minus_hinten {
(String::new(), if negativ { "-" } else { " " }.to_string(), false)
(
String::new(),
if negativ { "-" } else { " " }.to_string(),
false,
)
} else {
(String::new(), String::new(), negativ)
};
@@ -358,7 +374,7 @@ pub fn using(fmt: &str, werte: &[Value], waehrung: &Waehrung) -> Result<String,
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.
// Schutz gegen Endlosläufe bei Formaten ohne Fortschritt.
let mut runden = 0usize;
while w < werte.len() {

View File

@@ -5,7 +5,9 @@
//! Getippte Zeichen erscheinen im Eingabebereich; Sondertasten und
//! Mausereignisse werden in der Statuszeile angezeigt.
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind};
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
};
use crossterm::execute;
use std::io::stdout;
use std::time::Duration;
@@ -13,7 +15,10 @@ use tb_ui::screen::{ScreenWidget, TextScreen};
fn testbild(s: &mut TextScreen) {
s.set_color(15, 1);
s.print(&format!("{:^80}", "Terminal Basic — Phase-0-Spike (Esc beendet)"));
s.print(&format!(
"{:^80}",
"Terminal Basic — Phase-0-Spike (Esc beendet)"
));
// Farbraster: alle Vordergrundfarben auf allen Hintergründen
for bg in 0u8..8 {
@@ -70,15 +75,26 @@ fn main() -> anyhow::Result<()> {
KeyCode::Esc => break,
KeyCode::Char(c) => {
screen.print(&c.to_string());
status(&mut screen, &format!("Taste: {:?} Modifier: {:?}", k.code, k.modifiers));
status(
&mut screen,
&format!("Taste: {:?} Modifier: {:?}", k.code, k.modifiers),
);
}
KeyCode::Enter => screen.print("\n"),
other => status(&mut screen, &format!("Sondertaste: {other:?} Modifier: {:?}", k.modifiers)),
other => status(
&mut screen,
&format!("Sondertaste: {other:?} Modifier: {:?}", k.modifiers),
),
},
Event::Mouse(m) => {
status(
&mut screen,
&format!("Maus: {:?} bei Spalte {}, Zeile {}", m.kind, m.column + 1, m.row + 1),
&format!(
"Maus: {:?} bei Spalte {}, Zeile {}",
m.kind,
m.column + 1,
m.row + 1
),
);
}
Event::Resize(w, h) => {

View File

@@ -50,19 +50,21 @@ impl Widget for ScreenWidget<'_> {
let mut s = String::new();
s.push(c.ch);
cell.set_symbol(&s);
cell.set_style(
Style::default().fg(basic_color(c.fg)).bg(basic_color(c.bg)),
);
cell.set_style(Style::default().fg(basic_color(c.fg)).bg(basic_color(c.bg)));
}
}
}
// Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in
// der Forms-/Runtime-Schicht später gezielt gesteuert).
if self.0.cursor_visible && self.0.csrlin() <= rows && self.0.pos() <= cols {
let (cx, cy) = (area.x + (self.0.pos() - 1) as u16, area.y + (self.0.csrlin() - 1) as u16);
let (cx, cy) = (
area.x + (self.0.pos() - 1) as u16,
area.y + (self.0.csrlin() - 1) as u16,
);
if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_style(
cell.style().add_modifier(ratatui::style::Modifier::REVERSED),
cell.style()
.add_modifier(ratatui::style::Modifier::REVERSED),
);
}
}

View File

@@ -74,7 +74,11 @@ fn main() {
println!(
" Einzelmodul: {single_lines} Zeilen in {:.2} ms (Budget 50 ms) {}",
best_single * 1000.0,
if best_single < 0.050 { "OK" } else { "VERFEHLT" }
if best_single < 0.050 {
"OK"
} else {
"VERFEHLT"
}
);
println!(
" Projekt: {project_lines} Zeilen in {:.0} ms (Budget 1000 ms) {}{instrs} Instruktionen",