Laufzeit-Eingabe und Zonenzustand korrigieren und Change archivieren
This commit is contained in:
@@ -30,6 +30,8 @@ pub struct RtState {
|
||||
pub print: PrintState,
|
||||
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
|
||||
pub eingaben: VecDeque<Eingabe>,
|
||||
/// Bereits angenommener Rest einer von INPUT$ geteilten Taste.
|
||||
tastenrest: String,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -52,8 +54,10 @@ pub struct RtState {
|
||||
/// Versatz (dokumentierte Abweichung). Er wirkt auf die Ortszeit.
|
||||
pub uhr_offset: i64,
|
||||
/// Woher der UTC-Versatz kommt. Vorgabe ist die Zone des Rechners;
|
||||
/// ist sie nicht ermittelbar, steht hier `Unbekannt` und es gilt UTC.
|
||||
/// bei fehlgeschlagener Auflösung gilt für diese Abfrage UTC.
|
||||
pub zeitzone: dt::Zeitzone,
|
||||
/// Auflösung der Systemzone; durch Einbetter auch ohne Systemänderung ersetzbar.
|
||||
pub zonen_ermittler: fn(i64) -> Option<i32>,
|
||||
rng: u32,
|
||||
rnd_last: f32,
|
||||
pub command: String,
|
||||
@@ -85,6 +89,7 @@ impl Default for RtState {
|
||||
isam: Default::default(),
|
||||
print: PrintState::default(),
|
||||
eingaben: VecDeque::new(),
|
||||
tastenrest: String::new(),
|
||||
abbruch: false,
|
||||
ende: false,
|
||||
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
||||
@@ -97,7 +102,8 @@ impl Default for RtState {
|
||||
dir_treffer: Vec::new(),
|
||||
dir_index: 0,
|
||||
uhr_offset: 0,
|
||||
zeitzone: dt::Zeitzone::ermitteln(),
|
||||
zeitzone: dt::Zeitzone::System,
|
||||
zonen_ermittler: dt::utc_versatz,
|
||||
// Startzustand des Vorbild-PRNG; die exakte
|
||||
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
|
||||
// („RND/RANDOMIZE — kompatibler PRNG").
|
||||
@@ -110,6 +116,32 @@ impl Default for RtState {
|
||||
}
|
||||
|
||||
impl RtState {
|
||||
/// Ortszeit und Gültigkeit stammen aus derselben aktuellen Zonenauflösung.
|
||||
pub fn zeitpunkt(&self) -> (i64, bool) {
|
||||
let utc = dt::jetzt_utc();
|
||||
let zone = match self.zeitzone {
|
||||
dt::Zeitzone::System => (self.zonen_ermittler)(utc),
|
||||
dt::Zeitzone::Fest(n) => Some(n),
|
||||
dt::Zeitzone::Unbekannt => None,
|
||||
};
|
||||
(
|
||||
utc + i64::from(zone.unwrap_or(0)) + self.uhr_offset,
|
||||
zone.is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
fn jetzt_serial(&self) -> f64 {
|
||||
self.zeitpunkt().0 as f64 / 86_400.0 + dt::serial_aus_ymd(1970, 1, 1)
|
||||
}
|
||||
|
||||
/// Höchstens n Codepoints annehmen, übrige Zeichen vor der nächsten Taste lesen.
|
||||
pub fn tastenanteil(&mut self, mut taste: String, n: usize) -> String {
|
||||
if let Some((ende, _)) = taste.char_indices().nth(n) {
|
||||
self.tastenrest = taste.split_off(ende);
|
||||
}
|
||||
taste
|
||||
}
|
||||
|
||||
/// Ereignisse vom Host abholen und einsortieren: Größenänderungen wirken
|
||||
/// sofort auf den Bildschirm, Abbruch und Eingabeende werden gemerkt,
|
||||
/// Tastendrücke landen in der Warteschlange für `INKEY$`/`INPUT`.
|
||||
@@ -207,6 +239,9 @@ impl RtState {
|
||||
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
|
||||
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
|
||||
pub fn gepufferte_taste(&mut self) -> Option<String> {
|
||||
if !self.tastenrest.is_empty() {
|
||||
return Some(std::mem::take(&mut self.tastenrest));
|
||||
}
|
||||
while let Some(i) = self
|
||||
.eingaben
|
||||
.iter()
|
||||
@@ -496,7 +531,8 @@ pub mod ids {
|
||||
pub const GRAPHICS_LINE: u16 = 153;
|
||||
pub const GRAPHICS_PAINT: u16 = 154;
|
||||
pub const GRAPHICS_VIEW: u16 = 155;
|
||||
pub const COUNT: u16 = 156;
|
||||
pub const TIMEZONEKNOWN: u16 = 156;
|
||||
pub const COUNT: u16 = 157;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
@@ -659,6 +695,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_graphics_line,
|
||||
bi_graphics_paint,
|
||||
bi_graphics_view,
|
||||
bi_timezoneknown,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
@@ -1470,7 +1507,9 @@ fn bi_seek_stmt(
|
||||
if pos < 1 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
st.dateien.get(arg_i32(a, 0)?)?.position = pos as u64;
|
||||
st.dateien
|
||||
.get(arg_i32(a, 0)?)?
|
||||
.position_setzen(pos as u64)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1706,9 +1745,9 @@ fn bi_cv(
|
||||
};
|
||||
let bytes: Vec<u8> = text
|
||||
.chars()
|
||||
.map(|c| u8::try_from(c as u32).unwrap_or(0))
|
||||
.collect();
|
||||
if bytes.len() < breite {
|
||||
.map(|c| u8::try_from(c as u32).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL))
|
||||
.collect::<Result<_, _>>()?;
|
||||
if bytes.len() != breite {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
Ok(Some(match art {
|
||||
@@ -2180,18 +2219,18 @@ fn bi_input_s(
|
||||
if a.len() > 1 {
|
||||
let nummer = arg_i32(a, 1)?;
|
||||
let datei = st.dateien.get(nummer)?;
|
||||
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
|
||||
return Ok(Some(Value::Str(Rc::from(
|
||||
String::from_utf8_lossy(&bytes).as_ref(),
|
||||
))));
|
||||
return s_ok(datei.zeichen_lesen(n as usize)?);
|
||||
}
|
||||
// Blockierend, ohne Echo auf dem Bildschirm.
|
||||
let mut s = String::new();
|
||||
for _ in 0..n {
|
||||
let mut rest = n as usize;
|
||||
while rest > 0 {
|
||||
host.present(&st.screen);
|
||||
let Some(t) = st.naechste_taste(host, true) else {
|
||||
return Err(RuntimeError(62)); // Input past end of file
|
||||
};
|
||||
let t = st.tastenanteil(t, rest);
|
||||
rest -= t.chars().count();
|
||||
s.push_str(&t);
|
||||
}
|
||||
Ok(Some(Value::Str(Rc::from(s.as_str()))))
|
||||
@@ -2199,6 +2238,14 @@ fn bi_input_s(
|
||||
|
||||
// ---- Sonstiges ------------------------------------------------------------------
|
||||
|
||||
fn bi_timezoneknown(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Int(if st.zeitpunkt().1 { -1 } else { 0 })))
|
||||
}
|
||||
|
||||
fn bi_timer(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
@@ -2209,7 +2256,7 @@ fn bi_timer(
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs_f64().fract())
|
||||
.unwrap_or(0.0);
|
||||
let ganze = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
|
||||
let ganze = st.zeitpunkt().0.rem_euclid(86_400);
|
||||
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
|
||||
}
|
||||
|
||||
@@ -2218,7 +2265,7 @@ fn bi_date(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
|
||||
let secs = st.zeitpunkt().0;
|
||||
let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400));
|
||||
s_ok(format!("{m:02}-{d:02}-{y:04}"))
|
||||
}
|
||||
@@ -2228,7 +2275,7 @@ fn bi_time(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
|
||||
let s = st.zeitpunkt().0.rem_euclid(86_400);
|
||||
s_ok(format!(
|
||||
"{:02}:{:02}:{:02}",
|
||||
s / 3600,
|
||||
@@ -2245,7 +2292,7 @@ fn bi_date_set(
|
||||
let t = arg_str(a, 0)?;
|
||||
let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
// Nur den Datumsanteil verschieben, die Tageszeit bleibt.
|
||||
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
|
||||
let jetzt = st.jetzt_serial();
|
||||
let diff_tage = ziel.floor() - jetzt.floor();
|
||||
st.uhr_offset += (diff_tage * 86_400.0) as i64;
|
||||
Ok(None)
|
||||
@@ -2258,7 +2305,7 @@ fn bi_time_set(
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let t = arg_str(a, 0)?;
|
||||
let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
|
||||
let jetzt = st.jetzt_serial();
|
||||
let diff = ziel - (jetzt - jetzt.floor());
|
||||
st.uhr_offset += (diff * 86_400.0).round() as i64;
|
||||
Ok(None)
|
||||
@@ -2269,10 +2316,7 @@ fn bi_now(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(dt::jetzt_serial(
|
||||
st.zeitzone,
|
||||
st.uhr_offset,
|
||||
))))
|
||||
Ok(Some(Value::Dbl(st.jetzt_serial())))
|
||||
}
|
||||
|
||||
fn bi_date_serial(
|
||||
@@ -2762,6 +2806,72 @@ mod tests {
|
||||
Value::Str(Rc::from(v))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_s_dispatch_bewahrt_codepointreste_auch_ohne_vm() {
|
||||
let mut st = RtState::default();
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("\0;".into(), 0));
|
||||
host.ereignis(Ereignis::Taste("ä😀".into(), 0));
|
||||
for (n, expected) in [(0, ""), (1, "\0"), (2, ";ä"), (1, "😀")] {
|
||||
let got = bi_input_s(&mut st, &mut host, &mut [Value::Int(n)]).unwrap();
|
||||
assert!(matches!(got, Some(Value::Str(s)) if &*s == expected));
|
||||
}
|
||||
host.ereignis(Ereignis::Ende);
|
||||
assert_eq!(
|
||||
bi_input_s(&mut st, &mut host, &mut [Value::Int(1)]).unwrap_err(),
|
||||
RuntimeError(62)
|
||||
);
|
||||
assert_eq!(
|
||||
bi_input_s(&mut st, &mut host, &mut [Value::Int(-1)]).unwrap_err(),
|
||||
RuntimeError(5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen() {
|
||||
thread_local! {
|
||||
static ZONE: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
let mut st = RtState {
|
||||
zonen_ermittler: |_| ZONE.get(),
|
||||
uhr_offset: 4321,
|
||||
..RtState::default()
|
||||
};
|
||||
let mut host = CaptureHost::default();
|
||||
for zone in [None, Some(7200), None, Some(0)] {
|
||||
ZONE.set(zone);
|
||||
let before = dt::jetzt_utc();
|
||||
let (time, known) = st.zeitpunkt();
|
||||
assert_eq!(known, zone.is_some());
|
||||
let shift = i64::from(zone.unwrap_or(0)) + st.uhr_offset;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&time));
|
||||
assert!(
|
||||
matches!(bi_timezoneknown(&mut st, &mut host, &mut []).unwrap(), Some(Value::Int(n)) if n == if known { -1 } else { 0 })
|
||||
);
|
||||
let Some(Value::Dbl(now)) = bi_now(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let seconds = ((now - dt::serial_aus_ymd(1970, 1, 1)) * 86_400.0).round() as i64;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&seconds));
|
||||
let Some(Value::Str(date)) = bi_date(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let Some(Value::Str(clock)) = bi_time(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let date_time = ((dt::datevalue(&date).unwrap() + dt::timevalue(&clock).unwrap()
|
||||
- dt::serial_aus_ymd(1970, 1, 1))
|
||||
* 86_400.0)
|
||||
.round() as i64;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&date_time));
|
||||
let Some(Value::Sng(timer)) = bi_timer(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!((timer as f64 - seconds.rem_euclid(86_400) as f64).abs() < 2.0);
|
||||
assert_eq!(st.uhr_offset, 4321);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringfunktionen_randfaelle() {
|
||||
// VAL liest Präfix
|
||||
|
||||
Reference in New Issue
Block a user