This commit is contained in:
workD12
2026-07-31 20:16:28 +03:00
commit 8e24f19a07
20 changed files with 10037 additions and 0 deletions
+680
View File
@@ -0,0 +1,680 @@
//! 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 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.word_size = self.word_size.cycle();
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");
}
}