703 lines
18 KiB
Rust
703 lines
18 KiB
Rust
//! 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");
|
||
}
|
||
}
|