Files
RCalc/src/programmer.rs
T
2026-08-30 12:22:00 +03:00

734 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Programmer calculator (Windows-like): integer bases + bitwise ops.
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Base {
Hex,
Dec,
Oct,
Bin,
}
impl Base {
pub fn radix(self) -> u32 {
match self {
Base::Hex => 16,
Base::Dec => 10,
Base::Oct => 8,
Base::Bin => 2,
}
}
pub fn label(self) -> &'static str {
match self {
Base::Hex => "HEX",
Base::Dec => "DEC",
Base::Oct => "OCT",
Base::Bin => "BIN",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordSize {
/// 64-bit (signed DEC).
Qword,
/// 32-bit unsigned (PLC DWORD).
Dword,
/// 32-bit signed (PLC DINT).
Dint,
/// 16-bit unsigned (PLC WORD).
Word,
/// 16-bit signed (PLC INT).
Int,
/// 8-bit (signed DEC).
Byte,
}
impl WordSize {
pub fn bits(self) -> u32 {
match self {
WordSize::Qword => 64,
WordSize::Dword | WordSize::Dint => 32,
WordSize::Word | WordSize::Int => 16,
WordSize::Byte => 8,
}
}
/// Signed two's-complement DEC display (INT / DINT / QWORD / BYTE).
pub fn is_signed(self) -> bool {
matches!(
self,
WordSize::Qword | WordSize::Dint | WordSize::Int | WordSize::Byte
)
}
pub fn label(self) -> &'static str {
match self {
WordSize::Qword => "QWORD",
WordSize::Dword => "DWORD",
WordSize::Dint => "DINT",
WordSize::Word => "WORD",
WordSize::Int => "INT",
WordSize::Byte => "BYTE",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s.trim().to_ascii_uppercase().as_str() {
"QWORD" => Some(WordSize::Qword),
"DWORD" => Some(WordSize::Dword),
"DINT" => Some(WordSize::Dint),
"WORD" => Some(WordSize::Word),
"INT" => Some(WordSize::Int),
"BYTE" => Some(WordSize::Byte),
_ => None,
}
}
pub fn cycle(self) -> Self {
match self {
WordSize::Qword => WordSize::Dword,
WordSize::Dword => WordSize::Dint,
WordSize::Dint => WordSize::Word,
WordSize::Word => WordSize::Int,
WordSize::Int => WordSize::Byte,
WordSize::Byte => WordSize::Qword,
}
}
fn mask(self) -> u64 {
match self.bits() {
64 => u64::MAX,
bits => (1u64 << bits) - 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgOp {
Add,
Sub,
Mul,
Div,
Mod,
And,
Or,
Xor,
Nand,
Nor,
Lsh,
Rsh,
}
impl ProgOp {
pub fn symbol(self) -> &'static str {
match self {
ProgOp::Add => "+",
ProgOp::Sub => "−",
ProgOp::Mul => "×",
ProgOp::Div => "÷",
ProgOp::Mod => "%",
ProgOp::And => "AND",
ProgOp::Or => "OR",
ProgOp::Xor => "XOR",
ProgOp::Nand => "NAND",
ProgOp::Nor => "NOR",
ProgOp::Lsh => "Lsh",
ProgOp::Rsh => "Rsh",
}
}
fn apply(self, a: u64, b: u64, size: WordSize) -> Result<u64, ProgError> {
let mask = size.mask();
let a = a & mask;
let b = b & mask;
let r = match self {
ProgOp::Add => a.wrapping_add(b),
ProgOp::Sub => a.wrapping_sub(b),
ProgOp::Mul => a.wrapping_mul(b),
ProgOp::Div => {
if b == 0 {
return Err(ProgError::DivByZero);
}
a / b
}
ProgOp::Mod => {
if b == 0 {
return Err(ProgError::DivByZero);
}
a % b
}
ProgOp::And => a & b,
ProgOp::Or => a | b,
ProgOp::Xor => a ^ b,
ProgOp::Nand => !(a & b),
ProgOp::Nor => !(a | b),
ProgOp::Lsh => {
let sh = (b as u32) % size.bits().max(1);
a << sh
}
ProgOp::Rsh => {
let sh = (b as u32) % size.bits().max(1);
a >> sh
}
};
Ok(r & mask)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgError {
DivByZero,
Invalid,
}
impl ProgError {
pub fn message(self) -> &'static str {
match self {
ProgError::DivByZero => "Cannot divide by zero",
ProgError::Invalid => "Invalid input",
}
}
}
#[derive(Debug, Clone)]
pub struct Programmer {
entry: String,
typing: bool,
value: u64,
pending: Option<u64>,
op: Option<ProgOp>,
expression: String,
error: Option<ProgError>,
base: Base,
word_size: WordSize,
}
impl Default for Programmer {
fn default() -> Self {
Self::new()
}
}
impl Programmer {
pub fn new() -> Self {
Self {
entry: "0".into(),
typing: false,
value: 0,
pending: None,
op: None,
expression: String::new(),
error: None,
base: Base::Dec,
word_size: WordSize::Qword,
}
}
pub fn base(&self) -> Base {
self.base
}
pub fn word_size(&self) -> WordSize {
self.word_size
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn display(&self) -> String {
if let Some(err) = self.error {
return err.message().into();
}
if self.typing {
format_grouped_entry(&self.entry, self.base)
} else {
format_value(self.value, self.base, self.word_size)
}
}
pub fn hex_text(&self) -> String {
format_value(self.current_bits(), Base::Hex, self.word_size)
}
pub fn dec_text(&self) -> String {
format_value(self.current_bits(), Base::Dec, self.word_size)
}
pub fn oct_text(&self) -> String {
format_value(self.current_bits(), Base::Oct, self.word_size)
}
pub fn bin_text(&self) -> String {
format_value(self.current_bits(), Base::Bin, self.word_size)
}
/// Bit rows for the clickable bit board (MSB-first within each row).
/// Each entry is `(is_set, bit_index)` with `bit_index` counted from LSB = 0.
pub fn bit_rows(&self) -> Vec<Vec<(bool, u32)>> {
let n = self.word_size.bits();
let v = self.current_bits();
let cols: u32 = if n <= 8 { 8 } else { 16 };
let mut rows = Vec::new();
let mut row = Vec::new();
for i in (0..n).rev() {
row.push(((v >> i) & 1 == 1, i));
if row.len() == cols as usize {
rows.push(std::mem::take(&mut row));
}
}
if !row.is_empty() {
rows.push(row);
}
rows
}
pub fn toggle_bit(&mut self, bit: u32) {
if bit >= self.word_size.bits() {
return;
}
if self.error.is_some() {
self.clear_entry();
}
let v = self.current_bits();
self.value = (v ^ (1u64 << bit)) & self.word_size.mask();
self.typing = false;
self.error = None;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn set_base(&mut self, base: Base) {
if self.error.is_some() {
self.clear_entry();
}
// Commit typed digits into value when switching bases.
if self.typing {
if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v & self.word_size.mask();
}
self.typing = false;
}
self.base = base;
self.entry = format_raw(self.value, base, self.word_size);
}
pub fn cycle_word_size(&mut self) {
if self.typing {
if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v;
}
self.typing = false;
}
self.set_word_size(self.word_size.cycle());
}
pub fn set_word_size(&mut self, size: WordSize) {
if self.typing {
if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v;
}
self.typing = false;
}
self.word_size = size;
self.value &= self.word_size.mask();
if let Some(p) = self.pending.as_mut() {
*p &= self.word_size.mask();
}
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn clear_all(&mut self) {
let base = self.base;
let word_size = self.word_size;
*self = Self::new();
self.base = base;
self.word_size = word_size;
}
pub fn clear_entry(&mut self) {
self.error = None;
self.entry = "0".into();
self.value = 0;
self.typing = false;
}
pub fn backspace(&mut self) {
if self.error.is_some() {
self.clear_entry();
return;
}
if !self.typing {
return;
}
self.entry.pop();
if self.entry.is_empty() {
self.entry = "0".into();
self.typing = false;
self.value = 0;
} else if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v & self.word_size.mask();
}
}
pub fn input_digit(&mut self, ch: char) {
let ch = ch.to_ascii_uppercase();
if !is_valid_digit(ch, self.base) {
return;
}
if self.error.is_some() {
self.clear_all();
}
if !self.typing {
self.entry = ch.to_string();
self.typing = true;
} else if self.entry == "0" {
self.entry = ch.to_string();
} else {
let max_digits = max_digits(self.base, self.word_size);
if digit_len(&self.entry) >= max_digits {
return;
}
self.entry.push(ch);
}
match parse_entry(&self.entry, self.base) {
Ok(v) => {
let masked = v & self.word_size.mask();
// Reject overflow beyond word size (except wrapping interpretation of typed hex).
if v != masked && self.base != Base::Bin {
self.entry.pop();
if self.entry.is_empty() {
self.entry = "0".into();
self.typing = false;
}
return;
}
self.value = masked;
}
Err(_) => {
self.entry.pop();
}
}
}
pub fn negate(&mut self) {
if self.error.is_some() {
return;
}
let v = self.current_bits();
self.value = (!v).wrapping_add(1) & self.word_size.mask();
self.typing = false;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn not(&mut self) {
if self.error.is_some() {
return;
}
let v = self.current_bits();
self.value = (!v) & self.word_size.mask();
self.expression.clear();
self.typing = false;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn set_op(&mut self, op: ProgOp) {
if self.error.is_some() {
return;
}
if let Err(e) = self.commit_pending() {
self.fail(e);
return;
}
let v = self.current_bits();
self.pending = Some(v);
self.op = Some(op);
self.expression = format!(
"{} {}",
format_value(v, self.base, self.word_size),
op.symbol()
);
self.typing = false;
self.entry = format_raw(v, self.base, self.word_size);
}
pub fn equals(&mut self) {
if self.error.is_some() {
return;
}
let Some(op) = self.op else {
self.expression.clear();
self.typing = false;
return;
};
let b = self.current_bits();
let a = self.pending.unwrap_or(b);
match op.apply(a, b, self.word_size) {
Ok(r) => {
self.expression = format!(
"{} {} {} =",
format_value(a, self.base, self.word_size),
op.symbol(),
format_value(b, self.base, self.word_size)
);
self.value = r;
self.pending = None;
self.op = None;
self.typing = false;
self.entry = format_raw(r, self.base, self.word_size);
}
Err(e) => self.fail(e),
}
}
fn current_bits(&self) -> u64 {
if self.typing {
parse_entry(&self.entry, self.base)
.unwrap_or(self.value)
& self.word_size.mask()
} else {
self.value & self.word_size.mask()
}
}
fn commit_pending(&mut self) -> Result<(), ProgError> {
let (Some(a), Some(op)) = (self.pending, self.op) else {
return Ok(());
};
if !self.typing {
return Ok(());
}
let b = self.current_bits();
let r = op.apply(a, b, self.word_size)?;
self.value = r;
self.pending = Some(r);
self.op = None;
self.entry = format_raw(r, self.base, self.word_size);
Ok(())
}
fn fail(&mut self, e: ProgError) {
self.error = Some(e);
self.pending = None;
self.op = None;
self.expression.clear();
self.typing = false;
}
}
fn is_valid_digit(ch: char, base: Base) -> bool {
match base {
Base::Bin => matches!(ch, '0' | '1'),
Base::Oct => matches!(ch, '0'..='7'),
Base::Dec => ch.is_ascii_digit(),
Base::Hex => ch.is_ascii_hexdigit(),
}
}
fn digit_len(s: &str) -> usize {
s.chars().filter(|c| c.is_ascii_hexdigit()).count()
}
fn max_digits(base: Base, size: WordSize) -> usize {
let bits = size.bits() as f64;
match base {
Base::Bin => size.bits() as usize,
Base::Oct => (bits / 3.0).ceil() as usize,
Base::Dec => (bits * std::f64::consts::LN_2 / std::f64::consts::LN_10).ceil() as usize,
Base::Hex => (bits / 4.0).ceil() as usize,
}
}
fn parse_entry(entry: &str, base: Base) -> Result<u64, ProgError> {
let cleaned: String = entry
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect();
if cleaned.is_empty() {
return Ok(0);
}
u64::from_str_radix(&cleaned, base.radix()).map_err(|_| ProgError::Invalid)
}
fn format_raw(v: u64, base: Base, size: WordSize) -> String {
let v = v & size.mask();
match base {
Base::Dec => {
if size.is_signed() {
format!("{}", to_signed(v, size))
} else {
format!("{v}")
}
}
Base::Hex => format!("{:X}", v),
Base::Oct => format!("{:o}", v),
Base::Bin => format!("{:b}", v),
}
}
fn format_value(v: u64, base: Base, size: WordSize) -> String {
format_grouped_entry(&format_raw(v, base, size), base)
}
fn format_grouped_entry(raw: &str, base: Base) -> String {
// Keep leading minus for signed decimal.
let (sign, digits) = if let Some(rest) = raw.strip_prefix('-') {
("-", rest)
} else {
("", raw)
};
let digits: String = digits
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<String>()
.to_ascii_uppercase();
if digits.is_empty() {
return format!("{sign}0");
}
let group = match base {
Base::Bin => 4,
Base::Hex | Base::Oct => 4,
Base::Dec => 3,
};
let mut out = String::new();
for (i, ch) in digits.chars().rev().enumerate() {
if i > 0 && i % group == 0 {
out.push(' ');
}
out.push(ch);
}
let grouped: String = out.chars().rev().collect();
let mut result = String::new();
let _ = write!(result, "{sign}{grouped}");
result
}
fn to_signed(v: u64, size: WordSize) -> i64 {
let bits = size.bits();
if bits == 64 {
return v as i64;
}
let mask = size.mask();
let v = v & mask;
let sign_bit = 1u64 << (bits - 1);
if v & sign_bit != 0 {
(v | !mask) as i64
} else {
v as i64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_and() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
p.input_digit('F');
p.input_digit('F');
p.set_op(ProgOp::And);
p.input_digit('0');
p.input_digit('F');
p.equals();
assert_eq!(p.current_bits(), 0x0F);
}
#[test]
fn simultaneous_bases() {
let mut p = Programmer::new();
p.input_digit('2');
p.input_digit('5');
p.input_digit('5');
assert_eq!(p.hex_text().replace(' ', ""), "FF");
assert_eq!(p.bin_text().replace(' ', ""), "11111111");
}
#[test]
fn toggle_bit_flips() {
let mut p = Programmer::new();
// QWORD → DWORD → DINT → WORD → INT → BYTE
for _ in 0..5 {
p.cycle_word_size();
}
assert_eq!(p.word_size(), WordSize::Byte);
p.toggle_bit(0);
p.toggle_bit(7);
assert_eq!(p.current_bits(), 0b1000_0001);
p.toggle_bit(0);
assert_eq!(p.current_bits(), 0b1000_0000);
}
#[test]
fn int_vs_word_dec() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
// → DWORD → DINT → WORD
p.cycle_word_size();
p.cycle_word_size();
p.cycle_word_size();
assert_eq!(p.word_size(), WordSize::Word);
p.input_digit('F');
p.input_digit('F');
p.input_digit('F');
p.input_digit('F');
assert_eq!(p.dec_text().replace(' ', ""), "65535");
p.cycle_word_size(); // INT
assert_eq!(p.word_size(), WordSize::Int);
assert_eq!(p.dec_text().replace(' ', ""), "-1");
}
#[test]
fn dint_vs_dword_dec() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
p.cycle_word_size(); // DWORD
assert_eq!(p.word_size(), WordSize::Dword);
for _ in 0..8 {
p.input_digit('F');
}
assert_eq!(p.dec_text().replace(' ', ""), "4294967295");
p.cycle_word_size(); // DINT
assert_eq!(p.word_size(), WordSize::Dint);
assert_eq!(p.dec_text().replace(' ', ""), "-1");
}
#[test]
fn shift_count_mods_word_size() {
// Locks hardware-style semantics: shift count is taken modulo the
// word size (1 << 100000000 → 1e8 % 64 == 0 → no shift).
let mut p = Programmer::new();
p.input_digit('1');
p.set_op(ProgOp::Lsh);
for ch in "100000000".chars() {
p.input_digit(ch);
}
p.equals();
assert_eq!(p.current_bits(), 1);
}
#[test]
fn shift_out_of_word_gives_zero() {
// 0xFFFF_FFFF_FFFF_FFFF Lsh 63 → only bit 0 survives at bit 63.
let mut p = Programmer::new();
p.set_base(Base::Hex);
for ch in "FFFFFFFFFFFFFFFF".chars() {
p.input_digit(ch);
}
p.set_op(ProgOp::Lsh);
p.set_base(Base::Dec); // type the shift count in decimal
p.input_digit('6');
p.input_digit('3');
p.equals();
// (u64::MAX << 63) & u64::MAX = 0x8000_0000_0000_0000
assert_eq!(p.current_bits(), 0x8000_0000_0000_0000);
}
}