0.0.1
This commit is contained in:
+892
@@ -0,0 +1,892 @@
|
||||
//! Float32 ↔ two 16-bit words with Modbus-style endian (AB CD).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Endian {
|
||||
/// Big-endian: bytes A B C D as-is (Motorola / network order).
|
||||
AbCd,
|
||||
/// Word-swapped: C D A B.
|
||||
CdAb,
|
||||
/// Bytes swapped inside each word: B A D C.
|
||||
BaDc,
|
||||
/// Little-endian: D C B A (Intel).
|
||||
DcBa,
|
||||
}
|
||||
|
||||
impl Endian {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Endian::AbCd => "AB CD",
|
||||
Endian::CdAb => "CD AB",
|
||||
Endian::BaDc => "BA DC",
|
||||
Endian::DcBa => "DC BA",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all() -> [Endian; 4] {
|
||||
[Endian::AbCd, Endian::CdAb, Endian::BaDc, Endian::DcBa]
|
||||
}
|
||||
|
||||
fn index(self) -> i32 {
|
||||
match self {
|
||||
Endian::AbCd => 0,
|
||||
Endian::CdAb => 1,
|
||||
Endian::BaDc => 2,
|
||||
Endian::DcBa => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(i: i32) -> Self {
|
||||
match i {
|
||||
1 => Endian::CdAb,
|
||||
2 => Endian::BaDc,
|
||||
3 => Endian::DcBa,
|
||||
_ => Endian::AbCd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConvField {
|
||||
Float,
|
||||
Word0,
|
||||
Word1,
|
||||
AsciiChar,
|
||||
AsciiHex,
|
||||
AsciiDec,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConvPanel {
|
||||
Words,
|
||||
Ratio,
|
||||
Ascii,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RatioWidth {
|
||||
Byte,
|
||||
Word,
|
||||
}
|
||||
|
||||
impl RatioWidth {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
RatioWidth::Byte => "Byte",
|
||||
RatioWidth::Word => "Word",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(self) -> u32 {
|
||||
match self {
|
||||
RatioWidth::Byte => 255,
|
||||
RatioWidth::Word => 65535,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative-error precision presets for gear-ratio approximation.
|
||||
pub const PRECISION_PRESETS: &[f64] = &[
|
||||
1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9,
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RatioResult {
|
||||
pub num: u32,
|
||||
pub den: u32,
|
||||
pub approx: f64,
|
||||
pub rel_error: f64,
|
||||
pub exact: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Converter {
|
||||
endian: Endian,
|
||||
float_bits: u32,
|
||||
/// Text currently being edited in the active field.
|
||||
draft: String,
|
||||
active: ConvField,
|
||||
error: Option<&'static str>,
|
||||
panel: ConvPanel,
|
||||
ratio_width: RatioWidth,
|
||||
precision_idx: usize,
|
||||
/// Current ASCII / Latin-1 code (0..=255).
|
||||
ascii_code: u8,
|
||||
}
|
||||
|
||||
impl Default for Converter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Converter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
endian: Endian::AbCd,
|
||||
float_bits: 1.0f32.to_bits(),
|
||||
draft: "1".into(),
|
||||
active: ConvField::Float,
|
||||
error: None,
|
||||
panel: ConvPanel::Words,
|
||||
ratio_width: RatioWidth::Word,
|
||||
precision_idx: 3, // 1e-6
|
||||
ascii_code: b'A',
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panel(&self) -> ConvPanel {
|
||||
self.panel
|
||||
}
|
||||
|
||||
pub fn set_panel(&mut self, panel: ConvPanel) {
|
||||
self.commit_draft();
|
||||
self.panel = panel;
|
||||
self.active = match panel {
|
||||
ConvPanel::Words => ConvField::Float,
|
||||
ConvPanel::Ratio => ConvField::Float,
|
||||
ConvPanel::Ascii => ConvField::AsciiChar,
|
||||
};
|
||||
self.sync_draft();
|
||||
}
|
||||
|
||||
pub fn ratio_width(&self) -> RatioWidth {
|
||||
self.ratio_width
|
||||
}
|
||||
|
||||
pub fn set_ratio_width(&mut self, width: RatioWidth) {
|
||||
self.ratio_width = width;
|
||||
}
|
||||
|
||||
pub fn cycle_ratio_width(&mut self) {
|
||||
self.ratio_width = match self.ratio_width {
|
||||
RatioWidth::Byte => RatioWidth::Word,
|
||||
RatioWidth::Word => RatioWidth::Byte,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn precision_idx(&self) -> usize {
|
||||
self.precision_idx
|
||||
}
|
||||
|
||||
pub fn precision(&self) -> f64 {
|
||||
PRECISION_PRESETS[self.precision_idx.min(PRECISION_PRESETS.len() - 1)]
|
||||
}
|
||||
|
||||
pub fn precision_text(&self) -> String {
|
||||
let p = self.precision();
|
||||
format!("{p:.0e}")
|
||||
}
|
||||
|
||||
pub fn cycle_precision(&mut self) {
|
||||
self.precision_idx = (self.precision_idx + 1) % PRECISION_PRESETS.len();
|
||||
}
|
||||
|
||||
pub fn ratio(&self) -> Option<RatioResult> {
|
||||
let x = self.float_value() as f64;
|
||||
approximate_ratio(x, self.ratio_width.max(), self.precision())
|
||||
}
|
||||
|
||||
pub fn ratio_num_text(&self) -> String {
|
||||
match self.ratio() {
|
||||
Some(r) => format!("{}", r.num),
|
||||
None => "—".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratio_den_text(&self) -> String {
|
||||
match self.ratio() {
|
||||
Some(r) => format!("{}", r.den),
|
||||
None => "—".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratio_frac_text(&self) -> String {
|
||||
match self.ratio() {
|
||||
Some(r) => format!("{} / {}", r.num, r.den),
|
||||
None => "—".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratio_approx_text(&self) -> String {
|
||||
match self.ratio() {
|
||||
Some(r) => format!("{:.10}", r.approx).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
None => "—".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratio_error_text(&self) -> String {
|
||||
match self.ratio() {
|
||||
Some(r) if r.exact => "exact".into(),
|
||||
Some(r) => format!("{:.3e}", r.rel_error),
|
||||
None => "—".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endian(&self) -> Endian {
|
||||
self.endian
|
||||
}
|
||||
|
||||
pub fn endian_index(&self) -> i32 {
|
||||
self.endian.index()
|
||||
}
|
||||
|
||||
pub fn active(&self) -> ConvField {
|
||||
self.active
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&'static str> {
|
||||
self.error
|
||||
}
|
||||
|
||||
pub fn set_endian(&mut self, endian: Endian) {
|
||||
self.commit_draft();
|
||||
self.endian = endian;
|
||||
self.sync_draft();
|
||||
}
|
||||
|
||||
pub fn set_active(&mut self, field: ConvField) {
|
||||
let ok = match self.panel {
|
||||
ConvPanel::Words => matches!(
|
||||
field,
|
||||
ConvField::Float | ConvField::Word0 | ConvField::Word1
|
||||
),
|
||||
ConvPanel::Ratio => field == ConvField::Float,
|
||||
ConvPanel::Ascii => matches!(
|
||||
field,
|
||||
ConvField::AsciiChar | ConvField::AsciiHex | ConvField::AsciiDec
|
||||
),
|
||||
};
|
||||
if !ok || field == self.active {
|
||||
return;
|
||||
}
|
||||
self.commit_draft();
|
||||
self.active = field;
|
||||
self.sync_draft();
|
||||
}
|
||||
|
||||
pub fn ascii_code(&self) -> u8 {
|
||||
self.ascii_code
|
||||
}
|
||||
|
||||
pub fn ascii_char_text(&self) -> String {
|
||||
if self.active == ConvField::AsciiChar {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format_ascii_glyph(self.ascii_code)
|
||||
}
|
||||
|
||||
pub fn ascii_hex_text(&self) -> String {
|
||||
if self.active == ConvField::AsciiHex {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format!("{:02X}", self.ascii_code)
|
||||
}
|
||||
|
||||
pub fn ascii_dec_text(&self) -> String {
|
||||
if self.active == ConvField::AsciiDec {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format!("{}", self.ascii_code)
|
||||
}
|
||||
|
||||
pub fn ascii_name_text(&self) -> String {
|
||||
ascii_control_name(self.ascii_code)
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
if self.ascii_code.is_ascii_graphic() || self.ascii_code == b' ' {
|
||||
format!("'{}'", self.ascii_code as char)
|
||||
} else {
|
||||
format!("0x{:02X}", self.ascii_code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn float_value(&self) -> f32 {
|
||||
f32::from_bits(self.float_bits)
|
||||
}
|
||||
|
||||
pub fn words(&self) -> (u16, u16) {
|
||||
float_to_words(self.float_value(), self.endian)
|
||||
}
|
||||
|
||||
pub fn float_text(&self) -> String {
|
||||
if self.active == ConvField::Float {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format_float(self.float_value())
|
||||
}
|
||||
|
||||
pub fn word0_text(&self) -> String {
|
||||
if self.active == ConvField::Word0 {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format!("{:04X}", self.words().0)
|
||||
}
|
||||
|
||||
pub fn word1_text(&self) -> String {
|
||||
if self.active == ConvField::Word1 {
|
||||
return self.draft.clone();
|
||||
}
|
||||
format!("{:04X}", self.words().1)
|
||||
}
|
||||
|
||||
pub fn wire_bytes_text(&self) -> String {
|
||||
let (w0, w1) = self.words();
|
||||
let a = w0.to_be_bytes();
|
||||
let b = w1.to_be_bytes();
|
||||
format!("{:02X} {:02X} {:02X} {:02X}", a[0], a[1], b[0], b[1])
|
||||
}
|
||||
|
||||
pub fn ieee_hex_text(&self) -> String {
|
||||
format!("0x{:08X}", self.float_bits)
|
||||
}
|
||||
|
||||
pub fn clear_active(&mut self) {
|
||||
self.error = None;
|
||||
self.draft = match self.active {
|
||||
ConvField::Float => "0".into(),
|
||||
ConvField::Word0 | ConvField::Word1 => "0".into(),
|
||||
ConvField::AsciiChar => " ".into(),
|
||||
ConvField::AsciiHex => "00".into(),
|
||||
ConvField::AsciiDec => "0".into(),
|
||||
};
|
||||
let _ = self.apply_draft();
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
self.error = None;
|
||||
if self.active == ConvField::AsciiChar {
|
||||
self.draft = " ".into();
|
||||
self.ascii_code = b' ';
|
||||
return;
|
||||
}
|
||||
self.draft.pop();
|
||||
if self.draft.is_empty() {
|
||||
self.draft = match self.active {
|
||||
ConvField::AsciiHex => "0".into(),
|
||||
_ => "0".into(),
|
||||
};
|
||||
}
|
||||
let _ = self.apply_draft();
|
||||
}
|
||||
|
||||
pub fn input_char(&mut self, ch: char) {
|
||||
self.error = None;
|
||||
match self.active {
|
||||
ConvField::Float => {
|
||||
let ch = ch.to_ascii_uppercase();
|
||||
if !matches!(ch, '0'..='9' | '.' | '-' | '+' | 'E') {
|
||||
return;
|
||||
}
|
||||
if self.draft == "0" && ch != '.' && ch != 'E' {
|
||||
self.draft = ch.to_string();
|
||||
} else if self.draft == "-0" && ch != '.' && ch != 'E' {
|
||||
self.draft = format!("-{ch}");
|
||||
} else {
|
||||
self.draft.push(ch);
|
||||
}
|
||||
}
|
||||
ConvField::Word0 | ConvField::Word1 => {
|
||||
let ch = ch.to_ascii_uppercase();
|
||||
if !ch.is_ascii_hexdigit() {
|
||||
return;
|
||||
}
|
||||
if self.draft == "0" {
|
||||
self.draft = ch.to_string();
|
||||
} else if self.draft.len() < 4 {
|
||||
self.draft.push(ch);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ConvField::AsciiChar => {
|
||||
if (ch as u32) > 255 {
|
||||
return;
|
||||
}
|
||||
self.ascii_code = ch as u8;
|
||||
self.draft = format_ascii_glyph(self.ascii_code);
|
||||
return;
|
||||
}
|
||||
ConvField::AsciiHex => {
|
||||
let ch = ch.to_ascii_uppercase();
|
||||
if !ch.is_ascii_hexdigit() {
|
||||
return;
|
||||
}
|
||||
if self.draft == "0" {
|
||||
self.draft = ch.to_string();
|
||||
} else if self.draft.len() < 2 {
|
||||
self.draft.push(ch);
|
||||
} else {
|
||||
self.draft = ch.to_string();
|
||||
}
|
||||
}
|
||||
ConvField::AsciiDec => {
|
||||
if !ch.is_ascii_digit() {
|
||||
return;
|
||||
}
|
||||
if self.draft == "0" {
|
||||
self.draft = ch.to_string();
|
||||
} else if self.draft.len() < 3 {
|
||||
self.draft.push(ch);
|
||||
} else {
|
||||
self.draft = ch.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = self.apply_draft();
|
||||
}
|
||||
|
||||
pub fn negate_float(&mut self) {
|
||||
if self.panel != ConvPanel::Words && self.panel != ConvPanel::Ratio {
|
||||
return;
|
||||
}
|
||||
if self.active != ConvField::Float {
|
||||
self.set_active(ConvField::Float);
|
||||
}
|
||||
if let Some(rest) = self.draft.strip_prefix('-') {
|
||||
self.draft = rest.to_string();
|
||||
if self.draft.is_empty() {
|
||||
self.draft = "0".into();
|
||||
}
|
||||
} else if self.draft != "0" {
|
||||
self.draft = format!("-{}", self.draft);
|
||||
}
|
||||
let _ = self.apply_draft();
|
||||
}
|
||||
|
||||
fn commit_draft(&mut self) {
|
||||
let _ = self.apply_draft();
|
||||
self.sync_draft();
|
||||
}
|
||||
|
||||
fn sync_draft(&mut self) {
|
||||
self.draft = match self.active {
|
||||
ConvField::Float => format_float(self.float_value()),
|
||||
ConvField::Word0 => format!("{:04X}", self.words().0),
|
||||
ConvField::Word1 => format!("{:04X}", self.words().1),
|
||||
ConvField::AsciiChar => format_ascii_glyph(self.ascii_code),
|
||||
ConvField::AsciiHex => format!("{:02X}", self.ascii_code),
|
||||
ConvField::AsciiDec => format!("{}", self.ascii_code),
|
||||
};
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
fn apply_draft(&mut self) -> Result<(), ()> {
|
||||
match self.active {
|
||||
ConvField::Float => {
|
||||
let t = self.draft.trim();
|
||||
if t.is_empty() || t == "-" || t == "." || t == "-." {
|
||||
return Ok(());
|
||||
}
|
||||
match t.parse::<f32>() {
|
||||
Ok(v) => {
|
||||
self.float_bits = v.to_bits();
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.error = Some("Invalid float");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
ConvField::Word0 => {
|
||||
let w0 = parse_word(&self.draft)?;
|
||||
let w1 = self.words().1;
|
||||
self.float_bits = words_to_float(w0, w1, self.endian).to_bits();
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
ConvField::Word1 => {
|
||||
let w0 = self.words().0;
|
||||
let w1 = parse_word(&self.draft)?;
|
||||
self.float_bits = words_to_float(w0, w1, self.endian).to_bits();
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
ConvField::AsciiChar => {
|
||||
let t = self.draft.trim();
|
||||
if t.is_empty() {
|
||||
self.ascii_code = b' ';
|
||||
} else if let Some(c) = t.chars().next() {
|
||||
if (c as u32) <= 255 {
|
||||
self.ascii_code = c as u8;
|
||||
} else {
|
||||
self.error = Some("Not Latin-1");
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
ConvField::AsciiHex => {
|
||||
let cleaned: String = self
|
||||
.draft
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
if cleaned.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
match u8::from_str_radix(&cleaned, 16) {
|
||||
Ok(v) => {
|
||||
self.ascii_code = v;
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.error = Some("Invalid hex");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
ConvField::AsciiDec => {
|
||||
let t = self.draft.trim();
|
||||
if t.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
match t.parse::<u16>() {
|
||||
Ok(v) if v <= 255 => {
|
||||
self.ascii_code = v as u8;
|
||||
self.error = None;
|
||||
Ok(())
|
||||
}
|
||||
Ok(_) => {
|
||||
self.error = Some("Max 255");
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.error = Some("Invalid dec");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_ascii_glyph(code: u8) -> String {
|
||||
if let Some(name) = ascii_control_name(code) {
|
||||
format!("<{name}>")
|
||||
} else if code == b' ' {
|
||||
" ".into()
|
||||
} else if code.is_ascii_graphic() {
|
||||
(code as char).to_string()
|
||||
} else {
|
||||
// Latin-1 printable or other
|
||||
match char::from_u32(code as u32) {
|
||||
Some(c) if !c.is_control() => c.to_string(),
|
||||
_ => format!("<{code:02X}>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ascii_control_name(code: u8) -> Option<&'static str> {
|
||||
Some(match code {
|
||||
0x00 => "NUL",
|
||||
0x01 => "SOH",
|
||||
0x02 => "STX",
|
||||
0x03 => "ETX",
|
||||
0x04 => "EOT",
|
||||
0x05 => "ENQ",
|
||||
0x06 => "ACK",
|
||||
0x07 => "BEL",
|
||||
0x08 => "BS",
|
||||
0x09 => "TAB",
|
||||
0x0A => "LF",
|
||||
0x0B => "VT",
|
||||
0x0C => "FF",
|
||||
0x0D => "CR",
|
||||
0x0E => "SO",
|
||||
0x0F => "SI",
|
||||
0x10 => "DLE",
|
||||
0x11 => "DC1",
|
||||
0x12 => "DC2",
|
||||
0x13 => "DC3",
|
||||
0x14 => "DC4",
|
||||
0x15 => "NAK",
|
||||
0x16 => "SYN",
|
||||
0x17 => "ETB",
|
||||
0x18 => "CAN",
|
||||
0x19 => "EM",
|
||||
0x1A => "SUB",
|
||||
0x1B => "ESC",
|
||||
0x1C => "FS",
|
||||
0x1D => "GS",
|
||||
0x1E => "RS",
|
||||
0x1F => "US",
|
||||
0x7F => "DEL",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Best rational approximation of `x` with num, den ∈ [0..=max_val], den ≥ 1.
|
||||
/// Stops early when relative error ≤ `max_rel_err` (smallest such convergent preferred).
|
||||
pub fn approximate_ratio(x: f64, max_val: u32, max_rel_err: f64) -> Option<RatioResult> {
|
||||
if !x.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if x == 0.0 {
|
||||
return Some(RatioResult {
|
||||
num: 0,
|
||||
den: 1,
|
||||
approx: 0.0,
|
||||
rel_error: 0.0,
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
|
||||
let x = x.abs();
|
||||
let max_val = max_val as u64;
|
||||
|
||||
let mut best = RatioResult {
|
||||
num: 0,
|
||||
den: 1,
|
||||
approx: 0.0,
|
||||
rel_error: 1.0,
|
||||
exact: false,
|
||||
};
|
||||
|
||||
let consider = |n: u64, d: u64, best: &mut RatioResult| {
|
||||
if d == 0 || d > max_val || n > max_val {
|
||||
return;
|
||||
}
|
||||
let approx = n as f64 / d as f64;
|
||||
let rel = if x == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(approx - x).abs() / x
|
||||
};
|
||||
let exact = (approx - x).abs() <= f64::EPSILON * x.max(1.0) * 4.0;
|
||||
if rel < best.rel_error
|
||||
|| ((rel - best.rel_error).abs() < 1e-18 && d < best.den as u64)
|
||||
|| ((rel - best.rel_error).abs() < 1e-18 && d == best.den as u64 && n < best.num as u64)
|
||||
{
|
||||
*best = RatioResult {
|
||||
num: n as u32,
|
||||
den: d as u32,
|
||||
approx,
|
||||
rel_error: rel,
|
||||
exact,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Continued fraction: h₋₂=0,k₋₂=1 ; h₋₁=1,k₋₁=0
|
||||
let mut h2 = 0u64;
|
||||
let mut k2 = 1u64;
|
||||
let mut h1 = 1u64;
|
||||
let mut k1 = 0u64;
|
||||
|
||||
let mut remaining = x;
|
||||
for _ in 0..64 {
|
||||
if best.rel_error <= max_rel_err {
|
||||
break;
|
||||
}
|
||||
let a = remaining.floor() as u64;
|
||||
let t_start = if a == 0 { 0 } else { 1 };
|
||||
let mut last_h = h1;
|
||||
let mut last_k = k1;
|
||||
let mut fitted = false;
|
||||
|
||||
for t in t_start..=a {
|
||||
let Some(hn) = h1.checked_mul(t).and_then(|v| v.checked_add(h2)) else {
|
||||
fitted = false;
|
||||
break;
|
||||
};
|
||||
let Some(kn) = k1.checked_mul(t).and_then(|v| v.checked_add(k2)) else {
|
||||
fitted = false;
|
||||
break;
|
||||
};
|
||||
if hn > max_val || kn > max_val {
|
||||
let mut lo = t_start;
|
||||
let mut hi = t.saturating_sub(1);
|
||||
while lo < hi {
|
||||
let mid = (lo + hi + 1) / 2;
|
||||
let ok = h1
|
||||
.checked_mul(mid)
|
||||
.and_then(|v| v.checked_add(h2))
|
||||
.zip(k1.checked_mul(mid).and_then(|v| v.checked_add(k2)))
|
||||
.is_some_and(|(h, k)| h <= max_val && k <= max_val);
|
||||
if ok {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
if a == 0 || lo >= t_start {
|
||||
if let (Some(hn), Some(kn)) = (
|
||||
h1.checked_mul(lo).and_then(|v| v.checked_add(h2)),
|
||||
k1.checked_mul(lo).and_then(|v| v.checked_add(k2)),
|
||||
) {
|
||||
if kn > 0 && hn <= max_val && kn <= max_val {
|
||||
consider(hn, kn, &mut best);
|
||||
}
|
||||
}
|
||||
}
|
||||
fitted = false;
|
||||
break;
|
||||
}
|
||||
consider(hn, kn, &mut best);
|
||||
last_h = hn;
|
||||
last_k = kn;
|
||||
fitted = true;
|
||||
if best.rel_error <= max_rel_err {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if best.rel_error <= max_rel_err {
|
||||
break;
|
||||
}
|
||||
|
||||
if fitted {
|
||||
h2 = h1;
|
||||
k2 = k1;
|
||||
h1 = last_h;
|
||||
k1 = last_k;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
let frac = remaining - a as f64;
|
||||
if frac <= 1e-18 {
|
||||
break;
|
||||
}
|
||||
remaining = 1.0 / frac;
|
||||
}
|
||||
|
||||
Some(best)
|
||||
}
|
||||
|
||||
fn parse_word(s: &str) -> Result<u16, ()> {
|
||||
let cleaned: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||
if cleaned.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
u16::from_str_radix(&cleaned, 16).map_err(|_| ())
|
||||
}
|
||||
|
||||
fn format_float(v: f32) -> String {
|
||||
if !v.is_finite() {
|
||||
return if v.is_nan() {
|
||||
"NaN".into()
|
||||
} else if v.is_sign_negative() {
|
||||
"-Inf".into()
|
||||
} else {
|
||||
"Inf".into()
|
||||
};
|
||||
}
|
||||
let s = format!("{v}");
|
||||
s
|
||||
}
|
||||
|
||||
/// IEEE float bytes in natural order A B C D (big-endian bit pattern),
|
||||
/// then rearrange into two register words for the selected endian.
|
||||
pub fn float_to_words(f: f32, endian: Endian) -> (u16, u16) {
|
||||
let abcd = f.to_be_bytes(); // [A, B, C, D]
|
||||
let wire = match endian {
|
||||
Endian::AbCd => abcd,
|
||||
Endian::CdAb => [abcd[2], abcd[3], abcd[0], abcd[1]],
|
||||
Endian::BaDc => [abcd[1], abcd[0], abcd[3], abcd[2]],
|
||||
Endian::DcBa => [abcd[3], abcd[2], abcd[1], abcd[0]],
|
||||
};
|
||||
let w0 = u16::from_be_bytes([wire[0], wire[1]]);
|
||||
let w1 = u16::from_be_bytes([wire[2], wire[3]]);
|
||||
(w0, w1)
|
||||
}
|
||||
|
||||
pub fn words_to_float(w0: u16, w1: u16, endian: Endian) -> f32 {
|
||||
let a = w0.to_be_bytes();
|
||||
let b = w1.to_be_bytes();
|
||||
let wire = [a[0], a[1], b[0], b[1]];
|
||||
let abcd = match endian {
|
||||
Endian::AbCd => wire,
|
||||
Endian::CdAb => [wire[2], wire[3], wire[0], wire[1]],
|
||||
Endian::BaDc => [wire[1], wire[0], wire[3], wire[2]],
|
||||
Endian::DcBa => [wire[3], wire[2], wire[1], wire[0]],
|
||||
};
|
||||
f32::from_be_bytes(abcd)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_point_zero_abcd() {
|
||||
let (w0, w1) = float_to_words(1.0, Endian::AbCd);
|
||||
assert_eq!(w0, 0x3F80);
|
||||
assert_eq!(w1, 0x0000);
|
||||
assert_eq!(words_to_float(w0, w1, Endian::AbCd), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_point_zero_dcba() {
|
||||
let (w0, w1) = float_to_words(1.0, Endian::DcBa);
|
||||
assert_eq!(w0, 0x0000);
|
||||
assert_eq!(w1, 0x803F);
|
||||
assert_eq!(words_to_float(w0, w1, Endian::DcBa), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_point_zero_cdab() {
|
||||
let (w0, w1) = float_to_words(1.0, Endian::CdAb);
|
||||
assert_eq!(w0, 0x0000);
|
||||
assert_eq!(w1, 0x3F80);
|
||||
assert_eq!(words_to_float(w0, w1, Endian::CdAb), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_all_endians() {
|
||||
let samples = [0.0f32, -1.0, 3.1415927, 12345.678, f32::MIN_POSITIVE];
|
||||
for e in Endian::all() {
|
||||
for &f in &samples {
|
||||
let (w0, w1) = float_to_words(f, e);
|
||||
let back = words_to_float(w0, w1, e);
|
||||
assert_eq!(back.to_bits(), f.to_bits(), "endian {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_word_ratio() {
|
||||
let r = approximate_ratio(std::f64::consts::PI, 65535, 1e-6).unwrap();
|
||||
assert_eq!((r.num, r.den), (355, 113));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_byte_ratio() {
|
||||
let r = approximate_ratio(0.5, 255, 1e-9).unwrap();
|
||||
assert_eq!((r.num, r.den), (1, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gear_ratio_example() {
|
||||
// 17/23 electronic gear style
|
||||
let r = approximate_ratio(17.0 / 23.0, 255, 1e-9).unwrap();
|
||||
assert_eq!((r.num, r.den), (17, 23));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_char_to_codes() {
|
||||
let mut c = Converter::new();
|
||||
c.set_panel(ConvPanel::Ascii);
|
||||
c.set_active(ConvField::AsciiChar);
|
||||
c.input_char('A');
|
||||
assert_eq!(c.ascii_code(), 0x41);
|
||||
assert_eq!(c.ascii_hex_text(), "41");
|
||||
assert_eq!(c.ascii_dec_text(), "65");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_hex_to_char() {
|
||||
let mut c = Converter::new();
|
||||
c.set_panel(ConvPanel::Ascii);
|
||||
c.set_active(ConvField::AsciiHex);
|
||||
c.input_char('4');
|
||||
c.input_char('1');
|
||||
assert_eq!(c.ascii_code(), b'A');
|
||||
assert_eq!(c.ascii_char_text(), "A");
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
//! Modbus RTU CRC-16 and Modbus ASCII LRC.
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CrcTool {
|
||||
/// Raw text from the input field (hex bytes, spaces optional).
|
||||
input: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrcView {
|
||||
pub input: String,
|
||||
pub parsed_hex: String,
|
||||
pub byte_count: usize,
|
||||
pub rtu_crc_lo_hi: String,
|
||||
pub rtu_crc_word: String,
|
||||
pub rtu_frame: String,
|
||||
pub ascii_lrc: String,
|
||||
pub ascii_frame: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
impl CrcTool {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
input: "01 03 00 00 00 0A".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input(&self) -> &str {
|
||||
&self.input
|
||||
}
|
||||
|
||||
pub fn set_input(&mut self, text: impl Into<String>) {
|
||||
self.input = text.into();
|
||||
}
|
||||
|
||||
pub fn view(&self) -> CrcView {
|
||||
match parse_hex_bytes(&self.input) {
|
||||
Ok(bytes) => {
|
||||
let crc = modbus_crc16(&bytes);
|
||||
let lo = (crc & 0xFF) as u8;
|
||||
let hi = (crc >> 8) as u8;
|
||||
let lrc = modbus_lrc(&bytes);
|
||||
|
||||
let parsed_hex = format_bytes(&bytes);
|
||||
let mut frame = bytes.clone();
|
||||
frame.push(lo);
|
||||
frame.push(hi);
|
||||
|
||||
let ascii_payload: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
|
||||
let ascii_frame = format!(":{ascii_payload}{lrc:02X}\\r\\n");
|
||||
|
||||
CrcView {
|
||||
input: self.input.clone(),
|
||||
parsed_hex,
|
||||
byte_count: bytes.len(),
|
||||
rtu_crc_lo_hi: format!("{lo:02X} {hi:02X}"),
|
||||
rtu_crc_word: format!("0x{crc:04X}"),
|
||||
rtu_frame: format_bytes(&frame),
|
||||
ascii_lrc: format!("{lrc:02X}"),
|
||||
ascii_frame,
|
||||
error: String::new(),
|
||||
}
|
||||
}
|
||||
Err(e) => CrcView {
|
||||
input: self.input.clone(),
|
||||
parsed_hex: String::new(),
|
||||
byte_count: 0,
|
||||
rtu_crc_lo_hi: "—".into(),
|
||||
rtu_crc_word: "—".into(),
|
||||
rtu_frame: "—".into(),
|
||||
ascii_lrc: "—".into(),
|
||||
ascii_frame: "—".into(),
|
||||
error: e.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modbus RTU CRC-16 (poly 0xA001, init 0xFFFF). Returned value is host u16;
|
||||
/// on the wire it is sent low-byte first.
|
||||
pub fn modbus_crc16(data: &[u8]) -> u16 {
|
||||
let mut crc = 0xFFFFu16;
|
||||
for &b in data {
|
||||
crc ^= u16::from(b);
|
||||
for _ in 0..8 {
|
||||
if crc & 1 != 0 {
|
||||
crc = (crc >> 1) ^ 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// Modbus ASCII LRC: two's complement of the sum of bytes.
|
||||
pub fn modbus_lrc(data: &[u8]) -> u8 {
|
||||
let sum = data.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
|
||||
(!sum).wrapping_add(1)
|
||||
}
|
||||
|
||||
fn format_bytes(data: &[u8]) -> String {
|
||||
data.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Accepts `01 03 00 00`, `01-03-00`, `0x01,0x03`, or continuous `01030000`.
|
||||
fn parse_hex_bytes(text: &str) -> Result<Vec<u8>, &'static str> {
|
||||
let cleaned: String = text
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.collect::<String>()
|
||||
.to_ascii_uppercase();
|
||||
|
||||
if cleaned.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if cleaned.len() % 2 != 0 {
|
||||
return Err("Odd number of hex digits");
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(cleaned.len() / 2);
|
||||
let bytes = cleaned.as_bytes();
|
||||
for i in (0..bytes.len()).step_by(2) {
|
||||
let hi = from_hex(bytes[i])?;
|
||||
let lo = from_hex(bytes[i + 1])?;
|
||||
out.push((hi << 4) | lo);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn from_hex(b: u8) -> Result<u8, &'static str> {
|
||||
match b {
|
||||
b'0'..=b'9' => Ok(b - b'0'),
|
||||
b'A'..=b'F' => Ok(b - b'A' + 10),
|
||||
b'a'..=b'f' => Ok(b - b'a' + 10),
|
||||
_ => Err("Invalid hex"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_modbus_crc() {
|
||||
// 01 03 00 00 00 0A → CRC lo hi = C5 CD
|
||||
let data = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0A];
|
||||
let crc = modbus_crc16(&data);
|
||||
assert_eq!(crc & 0xFF, 0xC5);
|
||||
assert_eq!(crc >> 8, 0xCD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_modbus_lrc() {
|
||||
let data = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0A];
|
||||
assert_eq!(modbus_lrc(&data), 0xF2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_spaced() {
|
||||
let b = parse_hex_bytes("01 03 00 00 00 0A").unwrap();
|
||||
assert_eq!(b, vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]);
|
||||
}
|
||||
}
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
//! Calculator logic (standard / Windows-like).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Op {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
pub fn symbol(self) -> &'static str {
|
||||
match self {
|
||||
Op::Add => "+",
|
||||
Op::Sub => "−",
|
||||
Op::Mul => "×",
|
||||
Op::Div => "÷",
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(self, a: f64, b: f64) -> Result<f64, CalcError> {
|
||||
let r = match self {
|
||||
Op::Add => a + b,
|
||||
Op::Sub => a - b,
|
||||
Op::Mul => a * b,
|
||||
Op::Div => {
|
||||
if b == 0.0 {
|
||||
return Err(CalcError::DivByZero);
|
||||
}
|
||||
a / b
|
||||
}
|
||||
};
|
||||
if !r.is_finite() {
|
||||
Err(CalcError::Overflow)
|
||||
} else {
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CalcError {
|
||||
DivByZero,
|
||||
Overflow,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl CalcError {
|
||||
pub fn message(self) -> &'static str {
|
||||
match self {
|
||||
CalcError::DivByZero => "Cannot divide by zero",
|
||||
CalcError::Overflow => "Overflow",
|
||||
CalcError::Invalid => "Invalid input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator {
|
||||
/// Digits currently being entered (or last result as string).
|
||||
entry: String,
|
||||
/// True while the user is typing a new number.
|
||||
typing: bool,
|
||||
/// Left operand waiting for the next number / equals.
|
||||
pending: Option<f64>,
|
||||
/// Pending binary operator.
|
||||
op: Option<Op>,
|
||||
/// Expression line above the main display (e.g. "12 +").
|
||||
expression: String,
|
||||
/// Last error, if any.
|
||||
error: Option<CalcError>,
|
||||
/// Memory register.
|
||||
memory: f64,
|
||||
/// Whether memory has been set (for UI hint).
|
||||
memory_set: bool,
|
||||
}
|
||||
|
||||
impl Default for Calculator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Calculator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entry: "0".into(),
|
||||
typing: false,
|
||||
pending: None,
|
||||
op: None,
|
||||
expression: String::new(),
|
||||
error: None,
|
||||
memory: 0.0,
|
||||
memory_set: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(&self) -> &str {
|
||||
if let Some(err) = self.error {
|
||||
return err.message();
|
||||
}
|
||||
&self.entry
|
||||
}
|
||||
|
||||
pub fn expression(&self) -> &str {
|
||||
&self.expression
|
||||
}
|
||||
|
||||
pub fn has_memory(&self) -> bool {
|
||||
self.memory_set
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
*self = Self {
|
||||
memory: self.memory,
|
||||
memory_set: self.memory_set,
|
||||
..Self::new()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear_entry(&mut self) {
|
||||
self.error = None;
|
||||
self.entry = "0".into();
|
||||
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 == "-" {
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_digit(&mut self, d: char) {
|
||||
debug_assert!(d.is_ascii_digit());
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
if !self.typing {
|
||||
self.entry = d.to_string();
|
||||
self.typing = true;
|
||||
return;
|
||||
}
|
||||
if self.entry == "0" {
|
||||
self.entry = d.to_string();
|
||||
} else if self.entry == "-0" {
|
||||
self.entry = format!("-{d}");
|
||||
} else if digit_count(&self.entry) < 16 {
|
||||
self.entry.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_dot(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
if !self.typing {
|
||||
self.entry = "0.".into();
|
||||
self.typing = true;
|
||||
return;
|
||||
}
|
||||
if !self.entry.contains('.') {
|
||||
self.entry.push('.');
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negate(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(v) = self.current_value() {
|
||||
self.set_entry(-v);
|
||||
self.typing = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn percent(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let Ok(cur) = self.current_value() else {
|
||||
return;
|
||||
};
|
||||
let result = match (self.pending, self.op) {
|
||||
(Some(a), Some(Op::Add | Op::Sub)) => a * cur / 100.0,
|
||||
_ => cur / 100.0,
|
||||
};
|
||||
self.set_entry(result);
|
||||
self.typing = false;
|
||||
}
|
||||
|
||||
pub fn reciprocal(&mut self) {
|
||||
self.unary(|x| {
|
||||
if x == 0.0 {
|
||||
Err(CalcError::DivByZero)
|
||||
} else {
|
||||
Ok(1.0 / x)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn square(&mut self) {
|
||||
self.unary(|x| {
|
||||
let r = x * x;
|
||||
if r.is_finite() {
|
||||
Ok(r)
|
||||
} else {
|
||||
Err(CalcError::Overflow)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sqrt(&mut self) {
|
||||
self.unary(|x| {
|
||||
if x < 0.0 {
|
||||
Err(CalcError::Invalid)
|
||||
} else {
|
||||
Ok(x.sqrt())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_op(&mut self, op: Op) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = self.commit_pending() {
|
||||
self.fail(e);
|
||||
return;
|
||||
}
|
||||
let Ok(v) = self.current_value() else {
|
||||
return;
|
||||
};
|
||||
self.pending = Some(v);
|
||||
self.op = Some(op);
|
||||
self.expression = format!("{} {}", format_number(v), op.symbol());
|
||||
self.typing = false;
|
||||
}
|
||||
|
||||
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 Ok(b) = self.current_value() else {
|
||||
return;
|
||||
};
|
||||
let a = self.pending.unwrap_or(b);
|
||||
match op.apply(a, b) {
|
||||
Ok(r) => {
|
||||
self.expression = format!(
|
||||
"{} {} {} =",
|
||||
format_number(a),
|
||||
op.symbol(),
|
||||
format_number(b)
|
||||
);
|
||||
self.set_entry(r);
|
||||
self.pending = None;
|
||||
self.op = None;
|
||||
self.typing = false;
|
||||
}
|
||||
Err(e) => self.fail(e),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Memory ---
|
||||
|
||||
pub fn memory_clear(&mut self) {
|
||||
self.memory = 0.0;
|
||||
self.memory_set = false;
|
||||
}
|
||||
|
||||
pub fn memory_recall(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
self.set_entry(self.memory);
|
||||
self.typing = false;
|
||||
}
|
||||
|
||||
pub fn memory_add(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(v) = self.current_value() {
|
||||
self.memory += v;
|
||||
self.memory_set = true;
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_sub(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(v) = self.current_value() {
|
||||
self.memory -= v;
|
||||
self.memory_set = true;
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_store(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Ok(v) = self.current_value() {
|
||||
self.memory = v;
|
||||
self.memory_set = true;
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
|
||||
fn unary(&mut self, f: impl FnOnce(f64) -> Result<f64, CalcError>) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let Ok(v) = self.current_value() else {
|
||||
return;
|
||||
};
|
||||
match f(v) {
|
||||
Ok(r) => {
|
||||
self.expression.clear();
|
||||
self.set_entry(r);
|
||||
self.typing = false;
|
||||
}
|
||||
Err(e) => self.fail(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_pending(&mut self) -> Result<(), CalcError> {
|
||||
let (Some(a), Some(op)) = (self.pending, self.op) else {
|
||||
return Ok(());
|
||||
};
|
||||
if !self.typing {
|
||||
return Ok(());
|
||||
}
|
||||
let b = self.current_value()?;
|
||||
let r = op.apply(a, b)?;
|
||||
self.set_entry(r);
|
||||
self.pending = Some(r);
|
||||
self.op = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_value(&self) -> Result<f64, CalcError> {
|
||||
self.entry
|
||||
.parse::<f64>()
|
||||
.map_err(|_| CalcError::Invalid)
|
||||
}
|
||||
|
||||
fn set_entry(&mut self, v: f64) {
|
||||
self.entry = format_number(v);
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
fn fail(&mut self, e: CalcError) {
|
||||
self.error = Some(e);
|
||||
self.pending = None;
|
||||
self.op = None;
|
||||
self.expression.clear();
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn digit_count(s: &str) -> usize {
|
||||
s.chars().filter(|c| c.is_ascii_digit()).count()
|
||||
}
|
||||
|
||||
fn format_number(v: f64) -> String {
|
||||
if !v.is_finite() {
|
||||
return "0".into();
|
||||
}
|
||||
// Prefer integer display when close enough.
|
||||
if v.fract().abs() < 1e-12 && v.abs() < 1e15 {
|
||||
return format!("{}", v as i64);
|
||||
}
|
||||
let s = format!("{:.12}", v);
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
if s.is_empty() || s == "-" {
|
||||
"0".into()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_add() {
|
||||
let mut c = Calculator::new();
|
||||
c.input_digit('1');
|
||||
c.input_digit('2');
|
||||
c.set_op(Op::Add);
|
||||
c.input_digit('3');
|
||||
c.equals();
|
||||
assert_eq!(c.display(), "15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_by_zero() {
|
||||
let mut c = Calculator::new();
|
||||
c.input_digit('1');
|
||||
c.set_op(Op::Div);
|
||||
c.input_digit('0');
|
||||
c.equals();
|
||||
assert!(c.display().contains("zero"));
|
||||
}
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// Hide the extra console window on Windows GUI builds.
|
||||
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
||||
mod convert;
|
||||
mod crc;
|
||||
mod engine;
|
||||
mod programmer;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
|
||||
use crc::CrcTool;
|
||||
use engine::{Calculator, Op};
|
||||
use programmer::{Base, ProgOp, Programmer};
|
||||
use slint::{ComponentHandle, ModelRc, VecModel};
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Standard,
|
||||
Programmer,
|
||||
Convert,
|
||||
Crc,
|
||||
}
|
||||
|
||||
struct State {
|
||||
mode: Mode,
|
||||
std: Calculator,
|
||||
prog: Programmer,
|
||||
conv: Converter,
|
||||
crc: CrcTool,
|
||||
show_bits: bool,
|
||||
win_profile: u32,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: Mode::Standard,
|
||||
std: Calculator::new(),
|
||||
prog: Programmer::new(),
|
||||
conv: Converter::new(),
|
||||
crc: CrcTool::new(),
|
||||
show_bits: true,
|
||||
win_profile: u32::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), slint::PlatformError> {
|
||||
// Platform must exist first — calling set_xdg_app_id before AppWindow::new
|
||||
// returns NoPlatform and the Wayland app_id stays unset (KDE shows the "W" icon).
|
||||
let ui = AppWindow::new()?;
|
||||
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
|
||||
slint::set_xdg_app_id("rcalc")?;
|
||||
|
||||
let state = Rc::new(RefCell::new(State::default()));
|
||||
|
||||
refresh(&ui, &mut state.borrow_mut());
|
||||
|
||||
let ui_weak = ui.as_weak();
|
||||
let state_keys = state.clone();
|
||||
ui.on_key_pressed(move |id| {
|
||||
let mut s = state_keys.borrow_mut();
|
||||
handle_key(&mut s, id.as_str());
|
||||
if let Some(ui) = ui_weak.upgrade() {
|
||||
refresh(&ui, &mut s);
|
||||
}
|
||||
});
|
||||
|
||||
let ui_weak = ui.as_weak();
|
||||
let state_crc = state.clone();
|
||||
ui.on_crc_edited(move |text| {
|
||||
let mut s = state_crc.borrow_mut();
|
||||
s.crc.set_input(text.as_str());
|
||||
if let Some(ui) = ui_weak.upgrade() {
|
||||
apply_crc_results(&ui, &s);
|
||||
}
|
||||
});
|
||||
|
||||
ui.run()
|
||||
}
|
||||
|
||||
fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
|
||||
let items: Vec<BitInfo> = row
|
||||
.iter()
|
||||
.map(|(on, idx)| BitInfo {
|
||||
value: *on,
|
||||
index: *idx as i32,
|
||||
})
|
||||
.collect();
|
||||
ModelRc::new(VecModel::from(items))
|
||||
}
|
||||
|
||||
fn window_profile(state: &State) -> u32 {
|
||||
let mode = match state.mode {
|
||||
Mode::Standard => 0,
|
||||
Mode::Programmer => 1,
|
||||
Mode::Convert => 2,
|
||||
Mode::Crc => 3,
|
||||
};
|
||||
let bits = if state.show_bits {
|
||||
state.prog.word_size().bits()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let panel = match state.conv.panel() {
|
||||
ConvPanel::Words => 0,
|
||||
ConvPanel::Ratio => 1,
|
||||
ConvPanel::Ascii => 2,
|
||||
};
|
||||
mode * 1000 + bits * 10 + panel
|
||||
}
|
||||
|
||||
fn fit_window(ui: &AppWindow, state: &mut State) {
|
||||
let profile = window_profile(state);
|
||||
if state.win_profile == profile {
|
||||
return;
|
||||
}
|
||||
state.win_profile = profile;
|
||||
|
||||
let (w, h) = match state.mode {
|
||||
Mode::Standard => (340.0, 560.0),
|
||||
Mode::Programmer => {
|
||||
let bit_h = if state.show_bits {
|
||||
match state.prog.word_size().bits() {
|
||||
64 => 120.0,
|
||||
32 => 64.0,
|
||||
16 => 36.0,
|
||||
_ => 36.0,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(420.0, 720.0 + bit_h)
|
||||
}
|
||||
Mode::Convert => (420.0, 820.0),
|
||||
Mode::Crc => (460.0, 560.0),
|
||||
};
|
||||
ui.window()
|
||||
.set_size(slint::LogicalSize::new(w as f32, h as f32));
|
||||
}
|
||||
|
||||
fn apply_crc_results(ui: &AppWindow, state: &State) {
|
||||
let v = state.crc.view();
|
||||
ui.set_crc_parsed(v.parsed_hex.into());
|
||||
ui.set_crc_count(v.byte_count.to_string().into());
|
||||
ui.set_crc_rtu_bytes(v.rtu_crc_lo_hi.into());
|
||||
ui.set_crc_rtu_word(v.rtu_crc_word.into());
|
||||
ui.set_crc_rtu_frame(v.rtu_frame.into());
|
||||
ui.set_crc_ascii_lrc(v.ascii_lrc.into());
|
||||
ui.set_crc_ascii_frame(v.ascii_frame.into());
|
||||
ui.set_crc_error(v.error.into());
|
||||
}
|
||||
|
||||
fn refresh(ui: &AppWindow, state: &mut State) {
|
||||
ui.set_mode(match state.mode {
|
||||
Mode::Standard => 0,
|
||||
Mode::Programmer => 1,
|
||||
Mode::Convert => 2,
|
||||
Mode::Crc => 3,
|
||||
});
|
||||
|
||||
fit_window(ui, state);
|
||||
|
||||
match state.mode {
|
||||
Mode::Standard => {
|
||||
ui.set_display_text(state.std.display().into());
|
||||
ui.set_expression_text(state.std.expression().into());
|
||||
ui.set_has_memory(state.std.has_memory());
|
||||
}
|
||||
Mode::Programmer => {
|
||||
ui.set_display_text(state.prog.display().into());
|
||||
ui.set_expression_text(state.prog.expression().into());
|
||||
ui.set_has_memory(false);
|
||||
ui.set_hex_text(state.prog.hex_text().into());
|
||||
ui.set_dec_text(state.prog.dec_text().into());
|
||||
ui.set_oct_text(state.prog.oct_text().into());
|
||||
ui.set_bin_text(state.prog.bin_text().into());
|
||||
ui.set_active_base(match state.prog.base() {
|
||||
Base::Hex => 0,
|
||||
Base::Dec => 1,
|
||||
Base::Oct => 2,
|
||||
Base::Bin => 3,
|
||||
});
|
||||
ui.set_word_size_label(state.prog.word_size().label().into());
|
||||
ui.set_show_bits(state.show_bits);
|
||||
|
||||
let rows = state.prog.bit_rows();
|
||||
ui.set_bit_row_count(rows.len() as i32);
|
||||
ui.set_bit_row0(bit_row_model(rows.first().map(|r| r.as_slice()).unwrap_or(&[])));
|
||||
ui.set_bit_row1(bit_row_model(rows.get(1).map(|r| r.as_slice()).unwrap_or(&[])));
|
||||
ui.set_bit_row2(bit_row_model(rows.get(2).map(|r| r.as_slice()).unwrap_or(&[])));
|
||||
ui.set_bit_row3(bit_row_model(rows.get(3).map(|r| r.as_slice()).unwrap_or(&[])));
|
||||
}
|
||||
Mode::Convert => {
|
||||
ui.set_conv_panel(match state.conv.panel() {
|
||||
ConvPanel::Words => 0,
|
||||
ConvPanel::Ratio => 1,
|
||||
ConvPanel::Ascii => 2,
|
||||
});
|
||||
ui.set_conv_endian(state.conv.endian_index());
|
||||
ui.set_conv_active(match state.conv.active() {
|
||||
ConvField::Float | ConvField::AsciiChar => 0,
|
||||
ConvField::Word0 | ConvField::AsciiHex => 1,
|
||||
ConvField::Word1 | ConvField::AsciiDec => 2,
|
||||
});
|
||||
ui.set_conv_float(state.conv.float_text().into());
|
||||
ui.set_conv_word0(state.conv.word0_text().into());
|
||||
ui.set_conv_word1(state.conv.word1_text().into());
|
||||
ui.set_conv_bytes(state.conv.wire_bytes_text().into());
|
||||
ui.set_conv_ieee(state.conv.ieee_hex_text().into());
|
||||
ui.set_conv_error(state.conv.error().unwrap_or("").into());
|
||||
ui.set_ratio_width(match state.conv.ratio_width() {
|
||||
RatioWidth::Byte => 0,
|
||||
RatioWidth::Word => 1,
|
||||
});
|
||||
ui.set_ratio_precision(state.conv.precision_text().into());
|
||||
ui.set_ratio_num(state.conv.ratio_num_text().into());
|
||||
ui.set_ratio_den(state.conv.ratio_den_text().into());
|
||||
ui.set_ratio_frac(state.conv.ratio_frac_text().into());
|
||||
ui.set_ratio_approx(state.conv.ratio_approx_text().into());
|
||||
ui.set_ratio_error(state.conv.ratio_error_text().into());
|
||||
ui.set_ascii_char(state.conv.ascii_char_text().into());
|
||||
ui.set_ascii_hex(state.conv.ascii_hex_text().into());
|
||||
ui.set_ascii_dec(state.conv.ascii_dec_text().into());
|
||||
ui.set_ascii_name(state.conv.ascii_name_text().into());
|
||||
}
|
||||
Mode::Crc => {
|
||||
ui.set_crc_input(state.crc.input().into());
|
||||
apply_crc_results(ui, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(state: &mut State, id: &str) {
|
||||
match id {
|
||||
"mode:std" => {
|
||||
state.mode = Mode::Standard;
|
||||
return;
|
||||
}
|
||||
"mode:prog" => {
|
||||
state.mode = Mode::Programmer;
|
||||
return;
|
||||
}
|
||||
"mode:conv" => {
|
||||
state.mode = Mode::Convert;
|
||||
return;
|
||||
}
|
||||
"mode:crc" => {
|
||||
state.mode = Mode::Crc;
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match state.mode {
|
||||
Mode::Standard => handle_standard(&mut state.std, id),
|
||||
Mode::Programmer => handle_programmer(state, id),
|
||||
Mode::Convert => handle_convert(&mut state.conv, id),
|
||||
Mode::Crc => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_standard(calc: &mut Calculator, id: &str) {
|
||||
match id {
|
||||
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
|
||||
calc.input_digit(id.chars().next().unwrap());
|
||||
}
|
||||
"." | "," => calc.input_dot(),
|
||||
"+" => calc.set_op(Op::Add),
|
||||
"-" | "−" => calc.set_op(Op::Sub),
|
||||
"*" | "×" => calc.set_op(Op::Mul),
|
||||
"/" | "÷" => calc.set_op(Op::Div),
|
||||
"=" => calc.equals(),
|
||||
"%" => calc.percent(),
|
||||
"CE" => calc.clear_entry(),
|
||||
"C" => calc.clear_all(),
|
||||
"BS" => calc.backspace(),
|
||||
"1/x" => calc.reciprocal(),
|
||||
"x2" => calc.square(),
|
||||
"sqrt" => calc.sqrt(),
|
||||
"neg" => calc.negate(),
|
||||
"MC" => calc.memory_clear(),
|
||||
"MR" => calc.memory_recall(),
|
||||
"M+" => calc.memory_add(),
|
||||
"M-" => calc.memory_sub(),
|
||||
"MS" => calc.memory_store(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_programmer(state: &mut State, id: &str) {
|
||||
if id == "bits" {
|
||||
state.show_bits = !state.show_bits;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(rest) = id.strip_prefix("bit:") {
|
||||
if let Ok(bit) = rest.parse::<u32>() {
|
||||
state.prog.toggle_bit(bit);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let calc = &mut state.prog;
|
||||
match id {
|
||||
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D"
|
||||
| "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f" => {
|
||||
calc.input_digit(id.chars().next().unwrap());
|
||||
}
|
||||
"base:hex" => calc.set_base(Base::Hex),
|
||||
"base:dec" => calc.set_base(Base::Dec),
|
||||
"base:oct" => calc.set_base(Base::Oct),
|
||||
"base:bin" => calc.set_base(Base::Bin),
|
||||
"word" => calc.cycle_word_size(),
|
||||
"+" => calc.set_op(ProgOp::Add),
|
||||
"-" | "−" => calc.set_op(ProgOp::Sub),
|
||||
"*" | "×" => calc.set_op(ProgOp::Mul),
|
||||
"/" | "÷" => calc.set_op(ProgOp::Div),
|
||||
"%" => calc.set_op(ProgOp::Mod),
|
||||
"AND" => calc.set_op(ProgOp::And),
|
||||
"OR" => calc.set_op(ProgOp::Or),
|
||||
"XOR" => calc.set_op(ProgOp::Xor),
|
||||
"NAND" => calc.set_op(ProgOp::Nand),
|
||||
"NOR" => calc.set_op(ProgOp::Nor),
|
||||
"Lsh" => calc.set_op(ProgOp::Lsh),
|
||||
"Rsh" => calc.set_op(ProgOp::Rsh),
|
||||
"NOT" => calc.not(),
|
||||
"=" => calc.equals(),
|
||||
"CE" => calc.clear_entry(),
|
||||
"clear" => calc.clear_all(),
|
||||
"BS" => calc.backspace(),
|
||||
"neg" => calc.negate(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_convert(conv: &mut Converter, id: &str) {
|
||||
match id {
|
||||
"panel:words" => conv.set_panel(ConvPanel::Words),
|
||||
"panel:ratio" => conv.set_panel(ConvPanel::Ratio),
|
||||
"panel:ascii" => conv.set_panel(ConvPanel::Ascii),
|
||||
"endian:0" => conv.set_endian(Endian::AbCd),
|
||||
"endian:1" => conv.set_endian(Endian::CdAb),
|
||||
"endian:2" => conv.set_endian(Endian::BaDc),
|
||||
"endian:3" => conv.set_endian(Endian::DcBa),
|
||||
"rwidth:byte" => conv.set_ratio_width(RatioWidth::Byte),
|
||||
"rwidth:word" => conv.set_ratio_width(RatioWidth::Word),
|
||||
"rprec" => conv.cycle_precision(),
|
||||
"conv:float" => conv.set_active(ConvField::Float),
|
||||
"conv:w0" => conv.set_active(ConvField::Word0),
|
||||
"conv:w1" => conv.set_active(ConvField::Word1),
|
||||
"ascii:char" => conv.set_active(ConvField::AsciiChar),
|
||||
"ascii:hex" => conv.set_active(ConvField::AsciiHex),
|
||||
"ascii:dec" => conv.set_active(ConvField::AsciiDec),
|
||||
"CE" | "clear" => conv.clear_active(),
|
||||
"BS" => conv.backspace(),
|
||||
"neg" => conv.negate_float(),
|
||||
other => {
|
||||
// Single-character input (digits, letters, space, punctuation).
|
||||
let mut chars = other.chars();
|
||||
if let (Some(ch), None) = (chars.next(), chars.next()) {
|
||||
conv.input_char(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user