formular lesen (binär) und openspec für codex
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 0–31, hg 0–7). 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user