From 4b0e6e7e10b9445248329f1b2a65f231efc03752 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Tue, 25 May 2021 21:42:04 -0400 Subject: [PATCH] Major refactor, code cleanup, and documentation --- src/calc.rs | 513 ++++++++++++++++++++++++----------------- src/calc/errors.rs | 15 ++ src/calc/operations.rs | 20 +- src/calc/types.rs | 207 ++++++++++++++++- src/format.rs | 153 ------------ src/main.rs | 27 +-- 6 files changed, 540 insertions(+), 395 deletions(-) delete mode 100644 src/format.rs diff --git a/src/calc.rs b/src/calc.rs index 1466fd1..a837858 100644 --- a/src/calc.rs +++ b/src/calc.rs @@ -15,40 +15,58 @@ use types::{ RegisterState, }; +/// The maximum precision allowed for the calculator const MAX_PRECISION: usize = 20; +/// The name of the app, used for configuration file generation const APP_NAME: &str = "rpn_rs"; +/// The default precision to sue const DEFAULT_PRECISION: usize = 3; +/// The history mode of the entry - either a single change or a macro bound #[derive(PartialEq, Debug, Serialize, Deserialize)] enum HistoryMode { One, Macro, } +/// The main calculator struct that contains all fields internally #[derive(Serialize, Deserialize)] #[serde(default)] pub struct Calculator { + /// The entry buffer #[serde(skip)] l: String, + /// The stack pub stack: VecDeque, + /// True if the user would like to save on quit save_on_close: bool, + /// Left or right aligned display pub calculator_alignment: CalculatorAlignment, + /// The angle mode, such as DEG or RAD #[serde(flatten)] pub angle_mode: CalculatorAngleMode, + /// The display format such as separated or scientific #[serde(flatten)] pub display_mode: CalculatorDisplayMode, - #[serde(serialize_with = "ordered_char_map")] - pub macros: CalculatorMacros, + /// A set of the currently running macros, used for ensuring there are no recursive macro calls #[serde(skip)] active_macros: HashSet, + /// The map of chars to macros + #[serde(serialize_with = "ordered_char_map")] + pub macros: CalculatorMacros, + /// Map of chars to constants #[serde(serialize_with = "ordered_char_map")] pub constants: CalculatorConstants, + /// Map of chars to registers #[serde(skip)] pub registers: CalculatorRegisters, + /// Vec of state changes that can be undone #[serde(skip)] undo_buf: Vec, + /// Vec of state changes that can be redone #[serde(skip)] redo_buf: Vec, + /// The current state of the calculator, such as normal, or waiting for macro char #[serde(skip)] pub state: CalculatorState, } @@ -145,175 +163,209 @@ impl Calculator { store(APP_NAME, self).map_err(|e| CalculatorError::SaveError(Some(e))) } - // This maps chars to operations. Not sure I can make this shorter - #[allow(clippy::too_many_lines)] pub fn take_input(&mut self, c: char) -> CalculatorResult<()> { match &self.state { - CalculatorState::Normal => match c { - c @ '0'..='9' | c @ '.' | c @ 'e' => self.entry(c), - '+' => self.op(CalculatorOperation::Add), - '-' => self.op(CalculatorOperation::Subtract), - '*' => self.op(CalculatorOperation::Multiply), - '/' => self.op(CalculatorOperation::Divide), - 'n' => self.op(CalculatorOperation::Negate), - '|' => self.op(CalculatorOperation::AbsoluteValue), - 'i' => self.op(CalculatorOperation::Inverse), - '%' => self.op(CalculatorOperation::Modulo), - '\\' => self.op(CalculatorOperation::Drop), - '?' => self.op(CalculatorOperation::IntegerDivide), - ' ' => self.op(CalculatorOperation::Dup), - '>' => self.op(CalculatorOperation::Swap), - 's' => self.op(CalculatorOperation::Sin), - 'c' => self.op(CalculatorOperation::Cos), - 't' => self.op(CalculatorOperation::Tan), - 'S' => self.op(CalculatorOperation::ASin), - 'C' => self.op(CalculatorOperation::ACos), - 'T' => self.op(CalculatorOperation::ATan), - 'v' => self.op(CalculatorOperation::Sqrt), - '^' => self.op(CalculatorOperation::Pow), - 'l' => self.op(CalculatorOperation::Log), - 'L' => self.op(CalculatorOperation::Ln), - // Special - 'u' => self.op(CalculatorOperation::Undo), - 'U' => self.op(CalculatorOperation::Redo), - // State modifiers - 'm' => { - self.state = CalculatorState::WaitingForMacro; - Ok(()) - } - 'r' => { - self.state = CalculatorState::WaitingForRegister(RegisterState::Load); - Ok(()) - } - 'R' => { - self.state = CalculatorState::WaitingForRegister(RegisterState::Save); - Ok(()) - } - '`' => { - self.state = CalculatorState::WaitingForConstant; - Ok(()) - } - '@' => { - self.state = CalculatorState::WaitingForSetting; - Ok(()) - } - _ => Err(CalculatorError::NoSuchOperator(c)), - }, - CalculatorState::WaitingForConstant => { - let f = self - .constants - .get(&c) - .ok_or(CalculatorError::NoSuchConstant(c))? - .value; - - self.push(f)?; - self.state = CalculatorState::Normal; - Ok(()) - } - CalculatorState::WaitingForMacro => { - let mac = self.macros.get(&c).ok_or(CalculatorError::NoSuchMacro(c))?; - // self.take_input below takes a mutable reference to self, so must clone the value here - let value = mac.value.clone(); - - if self.active_macros.contains(&c) { - return Err(CalculatorError::RecursiveMacro(c)); - } - - // Record the macro started, if this is the outer macro - self.op(CalculatorOperation::Macro(MacroState::Start))?; - - // Record that we are running macro c - self.active_macros.insert(c); - - // The macro needs to run in normal mode - self.state = CalculatorState::Normal; - - for c in value.chars() { - self.take_input(c).map_err(|e| { - // Try cancelling, but if you cannot, that is okay - self.cancel().unwrap_or(()); - e - })?; - } - // Macro c should be over now - self.active_macros.remove(&c); - - // Record the macro is over, if this is the outer macro - self.op(CalculatorOperation::Macro(MacroState::End))?; - - Ok(()) - } + CalculatorState::Normal => self.normal_input(c), + CalculatorState::WaitingForConstant => self.constant_input(c), + CalculatorState::WaitingForMacro => self.macro_input(c), CalculatorState::WaitingForRegister(register_state) => { - match register_state { - RegisterState::Save => { - let f = self.pop()?; - self.registers.insert(c, f); - } - RegisterState::Load => { - let f = self - .registers - .get(&c) - .ok_or(CalculatorError::NoSuchRegister(c))?; - let f = *f; - self.push(f)?; - } - } - - self.state = CalculatorState::Normal; - Ok(()) - } - CalculatorState::WaitingForSetting => { - self.flush_l()?; - match c { - 'q' => self.state = CalculatorState::Normal, - 'd' => self.angle_mode = CalculatorAngleMode::Degrees, - 'r' => self.angle_mode = CalculatorAngleMode::Radians, - 'g' => self.angle_mode = CalculatorAngleMode::Grads, - '_' => self.display_mode = CalculatorDisplayMode::Default, - ',' => self.display_mode = CalculatorDisplayMode::Separated { separator: ',' }, - ' ' => self.display_mode = CalculatorDisplayMode::Separated { separator: ' ' }, - 's' => { - self.display_mode = CalculatorDisplayMode::Scientific { - precision: DEFAULT_PRECISION, - } - } - 'S' => { - self.display_mode = CalculatorDisplayMode::Scientific { - precision: self.pop_precision()?, - } - } - 'e' => { - self.display_mode = CalculatorDisplayMode::Engineering { - precision: DEFAULT_PRECISION, - } - } - 'E' => { - self.display_mode = CalculatorDisplayMode::Engineering { - precision: self.pop_precision()?, - } - } - 'f' => { - self.display_mode = CalculatorDisplayMode::Fixed { - precision: DEFAULT_PRECISION, - } - } - 'F' => { - self.display_mode = CalculatorDisplayMode::Fixed { - precision: self.pop_precision()?, - } - } - 'w' => self.save_on_close = false, - 'W' => self.save_on_close = true, - 'L' => self.calculator_alignment = CalculatorAlignment::Left, - 'R' => self.calculator_alignment = CalculatorAlignment::Right, - _ => return Err(CalculatorError::NoSuchSetting(c)), - }; - self.state = CalculatorState::Normal; - Ok(()) + let register_state = *register_state; + self.register_input(register_state, c) } + CalculatorState::WaitingForSetting => self.setting_input(c), } } + fn normal_input(&mut self, c: char) -> CalculatorResult<()> { + match c { + c @ '0'..='9' | c @ '.' | c @ 'e' => match c { + '0'..='9' => { + self.l.push(c); + Ok(()) + } + 'e' => { + if self.l.is_empty() { + let f = self.pop().or(Err(CalculatorError::NotEnoughStackEntries))?; + self.l = f.to_string(); + } + + if !self.l.contains('e') { + self.l.push('e'); + } + Ok(()) + } + + '.' => { + if !self.l.contains('.') && !self.l.contains('e') { + self.l.push('.'); + } + Ok(()) + } + _ => Err(CalculatorError::ParseError), + }, + '+' => self.op(CalculatorOperation::Add), + '-' => self.op(CalculatorOperation::Subtract), + '*' => self.op(CalculatorOperation::Multiply), + '/' => self.op(CalculatorOperation::Divide), + 'n' => self.op(CalculatorOperation::Negate), + '|' => self.op(CalculatorOperation::AbsoluteValue), + 'i' => self.op(CalculatorOperation::Inverse), + '%' => self.op(CalculatorOperation::Modulo), + '\\' => self.op(CalculatorOperation::Drop), + '?' => self.op(CalculatorOperation::IntegerDivide), + ' ' => self.op(CalculatorOperation::Dup), + '>' => self.op(CalculatorOperation::Swap), + 's' => self.op(CalculatorOperation::Sin), + 'c' => self.op(CalculatorOperation::Cos), + 't' => self.op(CalculatorOperation::Tan), + 'S' => self.op(CalculatorOperation::ASin), + 'C' => self.op(CalculatorOperation::ACos), + 'T' => self.op(CalculatorOperation::ATan), + 'v' => self.op(CalculatorOperation::Sqrt), + '^' => self.op(CalculatorOperation::Pow), + 'l' => self.op(CalculatorOperation::Log), + 'L' => self.op(CalculatorOperation::Ln), + // Special + 'u' => self.op(CalculatorOperation::Undo), + 'U' => self.op(CalculatorOperation::Redo), + // State modifiers + 'm' => { + self.state = CalculatorState::WaitingForMacro; + Ok(()) + } + 'r' => { + self.state = CalculatorState::WaitingForRegister(RegisterState::Load); + Ok(()) + } + 'R' => { + self.state = CalculatorState::WaitingForRegister(RegisterState::Save); + Ok(()) + } + '`' => { + self.state = CalculatorState::WaitingForConstant; + Ok(()) + } + '@' => { + self.state = CalculatorState::WaitingForSetting; + Ok(()) + } + _ => Err(CalculatorError::NoSuchOperator(c)), + } + } + fn constant_input(&mut self, c: char) -> CalculatorResult<()> { + let f = self + .constants + .get(&c) + .ok_or(CalculatorError::NoSuchConstant(c))? + .value; + + self.push(f)?; + self.state = CalculatorState::Normal; + Ok(()) + } + fn macro_input(&mut self, c: char) -> CalculatorResult<()> { + let mac = self.macros.get(&c).ok_or(CalculatorError::NoSuchMacro(c))?; + // self.take_input below takes a mutable reference to self, so must clone the value here + let value = mac.value.clone(); + + if self.active_macros.contains(&c) { + return Err(CalculatorError::RecursiveMacro(c)); + } + + // Record the macro started, if this is the outer macro + self.op(CalculatorOperation::Macro(MacroState::Start))?; + + // Record that we are running macro c + self.active_macros.insert(c); + + // The macro needs to run in normal mode + self.state = CalculatorState::Normal; + + for c in value.chars() { + self.take_input(c).map_err(|e| { + // Try cancelling, but if you cannot, that is okay + self.cancel().unwrap_or(()); + e + })?; + } + // Macro c should be over now + self.active_macros.remove(&c); + + // Record the macro is over, if this is the outer macro + self.op(CalculatorOperation::Macro(MacroState::End))?; + + Ok(()) + } + fn register_input(&mut self, register_state: RegisterState, c: char) -> CalculatorResult<()> { + match register_state { + RegisterState::Save => { + let f = self.pop()?; + self.registers.insert(c, f); + } + RegisterState::Load => { + let f = self + .registers + .get(&c) + .ok_or(CalculatorError::NoSuchRegister(c))?; + let f = *f; + self.push(f)?; + } + } + + self.state = CalculatorState::Normal; + Ok(()) + } + fn setting_input(&mut self, c: char) -> CalculatorResult<()> { + self.flush_l()?; + match c { + 'q' => self.state = CalculatorState::Normal, + 'd' => self.angle_mode = CalculatorAngleMode::Degrees, + 'r' => self.angle_mode = CalculatorAngleMode::Radians, + 'g' => self.angle_mode = CalculatorAngleMode::Grads, + '_' => self.display_mode = CalculatorDisplayMode::Default, + ',' => self.display_mode = CalculatorDisplayMode::Separated { separator: ',' }, + ' ' => self.display_mode = CalculatorDisplayMode::Separated { separator: ' ' }, + 's' => { + self.display_mode = CalculatorDisplayMode::Scientific { + precision: DEFAULT_PRECISION, + } + } + 'S' => { + self.display_mode = CalculatorDisplayMode::Scientific { + precision: self.pop_precision()?, + } + } + 'e' => { + self.display_mode = CalculatorDisplayMode::Engineering { + precision: DEFAULT_PRECISION, + } + } + 'E' => { + self.display_mode = CalculatorDisplayMode::Engineering { + precision: self.pop_precision()?, + } + } + 'f' => { + self.display_mode = CalculatorDisplayMode::Fixed { + precision: DEFAULT_PRECISION, + } + } + 'F' => { + self.display_mode = CalculatorDisplayMode::Fixed { + precision: self.pop_precision()?, + } + } + 'w' => self.save_on_close = false, + 'W' => self.save_on_close = true, + 'L' => self.calculator_alignment = CalculatorAlignment::Left, + 'R' => self.calculator_alignment = CalculatorAlignment::Right, + _ => return Err(CalculatorError::NoSuchSetting(c)), + }; + self.state = CalculatorState::Normal; + Ok(()) + } + + /// Resets the calculator state to normal, and exits out of a macro if one is running pub fn cancel(&mut self) -> CalculatorResult<()> { self.state = CalculatorState::Normal; // We died in a macro. Quit and push an end macro state @@ -324,10 +376,12 @@ impl Calculator { } Ok(()) } + /// Handles the backspace key which only deletes a char a char from l pub fn backspace(&mut self) -> CalculatorResult<()> { self.l.pop(); Ok(()) } + /// Places the bottom of the stack into l for editing, only if the value is empty pub fn edit(&mut self) -> CalculatorResult<()> { if !self.l.is_empty() { return Ok(()); @@ -339,38 +393,12 @@ impl Calculator { .to_string(); Ok(()) } + /// Get the value of l pub fn get_l(&mut self) -> &str { self.l.as_ref() } - fn entry(&mut self, c: char) -> CalculatorResult<()> { - match c { - '0'..='9' => { - self.l.push(c); - Ok(()) - } - 'e' => { - if self.l.is_empty() { - let f = self.pop().or(Err(CalculatorError::NotEnoughStackEntries))?; - - self.l = f.to_string(); - } - - if !self.l.contains('e') { - self.l.push('e'); - } - Ok(()) - } - - '.' => { - if !self.l.contains('.') && !self.l.contains('e') { - self.l.push('.'); - } - Ok(()) - } - _ => Err(CalculatorError::ParseError), - } - } + /// Returns a formatted string that shows the status of the calculator pub fn get_status_line(&self) -> String { format!( "[{}] [{}] [{}] [{}]", @@ -381,40 +409,49 @@ impl Calculator { ) } + /// Pushes l onto the stack if parseable and not empty returns result true if the value was changed pub fn flush_l(&mut self) -> CalculatorResult { if self.l.is_empty() { - Ok(false) - } else { - let f = self.l.parse::().or(Err(CalculatorError::ParseError))?; - self.push(f)?; - self.l.clear(); - Ok(true) + return Ok(false); } + + let f = self.l.parse::().or(Err(CalculatorError::ParseError))?; + self.push(f)?; + self.l.clear(); + Ok(true) } + /// Checks if the calculator is currently running a macro fn within_macro(&self) -> bool { !self.active_macros.is_empty() } + /// Pushes a value onto the stack and makes a state change fn push(&mut self, f: f64) -> CalculatorResult<()> { self.direct_state_change(CalculatorStateChange { pop: OpArgs::None, push: OpArgs::Unary(f), }) } - pub fn peek(&mut self) -> CalculatorResult { - self.flush_l()?; - self.checked_get(0) - } + /// Returns the value of the bottom of the stack by popping it using a state change pub fn pop(&mut self) -> CalculatorResult { - let f = self.peek()?; + let f = self.peek(0)?; self.direct_state_change(CalculatorStateChange { pop: OpArgs::Unary(f), push: OpArgs::None, })?; Ok(f) } + /// Returns a calculator value + fn peek(&mut self, idx: usize) -> CalculatorResult { + self.flush_l()?; + match self.stack.get(idx) { + None => Err(CalculatorError::NotEnoughStackEntries), + Some(r) => Ok(*r), + } + } + /// Pops a precision instead of an f64. Precisions are of type usize pub fn pop_precision(&mut self) -> CalculatorResult { - let f = self.peek()?; + let f = self.peek(0)?; // Ensure this can be cast to a usize if !f.is_finite() || f.is_sign_negative() { return Err(CalculatorError::ArithmeticError); @@ -432,6 +469,7 @@ impl Calculator { })?; Ok(u) } + /// Performs a calculator operation such as undo, redo, operator, or dup pub fn op(&mut self, op: CalculatorOperation) -> CalculatorResult<()> { // Dup is special -- don't actually run it if l needs to be flushed if self.flush_l()? { @@ -521,6 +559,9 @@ impl Calculator { self.direct_state_change(state_change?) } + /// Performs a history operation, either an undo or redo + /// + /// This will undo until it reaches a macro boundary, so this effictively undoes or redoes all macro operations in one stroke fn history_op(&mut self, forward: bool) -> CalculatorResult<()> { let s = if forward { &self.redo_buf @@ -573,6 +614,7 @@ impl Calculator { } } } + /// Performs a state change on a unary operation fn unary_op( &mut self, op: impl FnOnce(f64) -> OpArgs, @@ -586,6 +628,7 @@ impl Calculator { push: op(*arg), }) } + /// Performs a state change on a binary operation fn binary_op( &mut self, op: impl FnOnce([f64; 2]) -> OpArgs, @@ -606,6 +649,7 @@ impl Calculator { }) } + /// Performs a state change and clears the redo buf. This is used when *not* undoing/redoing. fn direct_state_change(&mut self, c: CalculatorStateChange) -> CalculatorResult<()> { let result = self.apply_state_change(c, true); if result.is_ok() { @@ -615,6 +659,7 @@ impl Calculator { result } + /// Applies a state change to the stack. Pass in the state change and the state change is applied forward or backwards fn apply_state_change( &mut self, c: CalculatorStateChange, @@ -674,23 +719,69 @@ impl Calculator { Ok(()) } - fn stack_eq(&self, idx: usize, value: f64) -> CalculatorResult<()> { - if (self.checked_get(idx)? - value).abs() > f64::EPSILON { + /// Checks if a value on the stack is equal to a given value + fn stack_eq(&mut self, idx: usize, value: f64) -> CalculatorResult<()> { + if (self.peek(idx)? - value).abs() > f64::EPSILON { Err(CalculatorError::CorruptStateChange(format!( "Stack index {} should be {}, but is {}", idx, value, - self.checked_get(idx)?, + self.peek(idx)?, ))) } else { Ok(()) } } +} - fn checked_get(&self, idx: usize) -> CalculatorResult { - match self.stack.get(idx) { - None => Err(CalculatorError::NotEnoughStackEntries), - Some(r) => Ok(*r), +#[cfg(test)] +mod tests { + use super::*; + + fn gen_sample_calculator() -> Calculator { + let mut calc = Calculator::default(); + // Empty the stack and push a few numbers + input_str(&mut calc, "\\\\123 456 789"); + calc + } + + fn input_str(calc: &mut Calculator, input: &str) { + for c in input.chars() { + assert!(calc.take_input(c).is_ok()); } } + + fn assert_float_eq(a: f64, b: f64) { + assert!(a - b < f64::EPSILON, "Value '{}' did not match '{}'", a, b); + } + + #[test] + fn basic_ops() { + let mut calc = gen_sample_calculator(); + assert_float_eq(calc.peek(0).unwrap(), 789_f64); + input_str(&mut calc, "+"); + assert_float_eq(calc.peek(0).unwrap(), 1_245_f64); + input_str(&mut calc, "+"); + assert_float_eq(calc.peek(0).unwrap(), 1_368_f64); + // The stack now only has one element + assert!(!calc.take_input('+').is_ok()); + + input_str(&mut calc, "n"); + assert_float_eq(calc.pop().unwrap(), -1_368_f64); + + input_str(&mut calc, "64v100v"); + assert_float_eq(calc.pop().unwrap(), 10_f64); + assert_float_eq(calc.pop().unwrap(), 8_f64); + } + + #[test] + fn peek() { + let mut calc = gen_sample_calculator(); + // There should be three digits + assert_float_eq(calc.peek(0).unwrap(), 789_f64); + assert_float_eq(calc.peek(1).unwrap(), 456_f64); + assert_float_eq(calc.peek(2).unwrap(), 123_f64); + assert!(!calc.peek(3).is_ok()); + assert!(true); + } } diff --git a/src/calc/errors.rs b/src/calc/errors.rs index 5bb0a42..9d8a219 100644 --- a/src/calc/errors.rs +++ b/src/calc/errors.rs @@ -4,21 +4,36 @@ use std::fmt; pub type CalculatorResult = Result; +/// All possible errors the calculator can throw #[derive(Debug)] pub enum CalculatorError { + /// Divide by zero, log(-1), etc ArithmeticError, + /// Not enough stck entries for operation NotEnoughStackEntries, + /// Thrown when an undo or redo cannot be performed CorruptStateChange(String), + /// Cannot undo or redo EmptyHistory(String), + /// Constant undefined NoSuchConstant(char), + /// Register undefined NoSuchRegister(char), + /// Macro undefined NoSuchMacro(char), + /// Operator undefined NoSuchOperator(char), + /// Setting undefined NoSuchSetting(char), + /// Macro calls itself RecursiveMacro(char), + /// Could not convert l to number ParseError, + /// Requested precision is too high PrecisionTooHigh, + /// Config serialization error SaveError(Option), + /// Config deserialization error LoadError(Option), } diff --git a/src/calc/operations.rs b/src/calc/operations.rs index d004eb9..fb9f093 100644 --- a/src/calc/operations.rs +++ b/src/calc/operations.rs @@ -1,10 +1,5 @@ use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Debug, Serialize, Deserialize)] -pub enum MacroState { - Start, - End, -} - +/// Operations that can be sent to the calculator such as +, -, or undo #[derive(PartialEq, Debug, Serialize, Deserialize)] pub enum CalculatorOperation { Add, @@ -37,14 +32,27 @@ pub enum CalculatorOperation { Macro(MacroState), } +/// Macro bundary; defined by the start or end of a macro invocation +#[derive(PartialEq, Debug, Serialize, Deserialize)] +pub enum MacroState { + Start, + End, +} + +/// Arguments for a given operation #[derive(PartialEq, Debug, Serialize, Deserialize)] pub enum OpArgs { + /// This is a macro start and end noop Macro(MacroState), + /// Operation takes 1 argument, ex: sqrt or negate Unary(f64), + /// Operation takes 2 arguments, ex: + or - Binary([f64; 2]), + /// Operation takes no arguments, ex: push None, } +/// Record of what to pop and push. Used for undo and redo buffers #[derive(PartialEq, Debug, Serialize, Deserialize)] pub struct CalculatorStateChange { pub pop: OpArgs, diff --git a/src/calc/types.rs b/src/calc/types.rs index 45a6c02..806872f 100644 --- a/src/calc/types.rs +++ b/src/calc/types.rs @@ -2,12 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; -#[derive(Debug, Serialize, Deserialize)] -pub enum RegisterState { - Save, - Load, -} - +/// The calculator state #[derive(Debug, Serialize, Deserialize)] pub enum CalculatorState { Normal, @@ -23,24 +18,37 @@ impl Default for CalculatorState { } } +/// The state of the requested register operation +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum RegisterState { + Save, + Load, +} + +/// One calculator constant containing a message and value #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalculatorConstant { pub help: String, pub value: f64, } +/// One calculator macro containing a messsage and value #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalculatorMacro { pub help: String, pub value: String, } +/// Map of chars to constants pub type CalculatorConstants = HashMap; +/// Map of chars to macros pub type CalculatorMacros = HashMap; +/// Map of chars to registers pub type CalculatorRegisters = HashMap; +/// Possible calculator angle modes #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "angle_mode")] pub enum CalculatorAngleMode { @@ -65,14 +73,21 @@ impl fmt::Display for CalculatorAngleMode { } } +/// The calculator digit display mode #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "display_mode")] -// Could also have added content="precision" pub enum CalculatorDisplayMode { + /// Rust's default f64 format Default, + /// Thousands separator Separated { separator: char }, + /// Aligned scientific format Scientific { precision: usize }, + /// Scientific format, chunked by groups of 3 + /// + /// Example: 1 E+5 or 100E+5 Engineering { precision: usize }, + /// Fixed precision Fixed { precision: usize }, } @@ -94,6 +109,94 @@ impl Default for CalculatorDisplayMode { } } +impl CalculatorDisplayMode { + pub fn format_number(&self, number: f64) -> String { + match self { + Self::Default => format!("{}", number), + Self::Separated { separator } => Self::separated(number, *separator), + Self::Scientific { precision } => Self::scientific(number, *precision), + Self::Engineering { precision } => Self::engineering(number, *precision), + Self::Fixed { precision } => { + format!("{:0>.precision$}", number, precision = precision) + } + } + } + + // Based on https://stackoverflow.com/a/65266882 + fn scientific(f: f64, precision: usize) -> String { + let mut ret = format!("{:.precision$E}", f, precision = precision); + let exp = ret.split_off(ret.find('E').unwrap_or(0)); + let (exp_sign, exp) = exp + .strip_prefix("E-") + .map_or_else(|| ('+', &exp[1..]), |stripped| ('-', stripped)); + + let sign = if ret.starts_with('-') { "" } else { " " }; + format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2) + } + + fn engineering(f: f64, precision: usize) -> String { + // Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string + // 1,000 => 1000E3 + let all = format!(" {:.precision$E}", f, precision = precision) + // Remove . since it can be moved + .replacen(".", "", 1) + // Add 00E before E here so the length is enough for slicing below + .replacen("E", "00E", 1); + // Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E + // 1000E3 => (1000, E3) + let (num_str, exp_str) = all.split_at(all.find('E').unwrap()); + // Extract the exponent as an isize. This should always be true because f64 max will be ~400 + // E3 => 3 as isize + let exp = exp_str[1..].parse::().unwrap(); + // Sign of the exponent. If string representation starts with E-, then negative + let display_exp_sign = if exp_str.strip_prefix("E-").is_some() { + '-' + } else { + '+' + }; + + // The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above + // 100 => 0, 1000 => 3, .1 => 3 (but will show as -3) + let display_exp = (exp.div_euclid(3) * 3).abs(); + // Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility + let num_whole_digits = exp.rem_euclid(3) as usize + 1; + + // If this is a negative number, strip off the added space, otherwise keep the space (and next digit) + let num_str = if num_str.strip_prefix(" -").is_some() { + &num_str[1..] + } else { + num_str + }; + + // Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit) + // Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10 + let whole = &num_str[0..=num_whole_digits]; + // Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2 + let decimal = &num_str[(num_whole_digits + 1)..=(precision + num_whole_digits)]; + // Right align whole portion, always have decimal point + format!( + "{: >4}.{} E{}{:0>pad$}", + // display_sign, + whole, + decimal, + display_exp_sign, + display_exp, + pad = 2 + ) + } + + fn separated(f: f64, sep: char) -> String { + let mut ret = f.to_string(); + let start = if ret.starts_with('-') { 1 } else { 0 }; + let end = ret.find('.').unwrap_or_else(|| ret.len()); + for i in 0..((end - start - 1).div_euclid(3)) { + ret.insert(end - (i + 1) * 3, sep); + } + ret + } +} + +/// Left or right calculator alignment #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CalculatorAlignment { Right, @@ -114,3 +217,93 @@ impl fmt::Display for CalculatorAlignment { } } } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_scientific() { + for (f, precision, s) in vec![ + // Basic + (1.0, 0, " 1 E+00"), + (-1.0, 0, "-1 E+00"), + (100.0, 0, " 1 E+02"), + (0.1, 0, " 1 E-01"), + (0.01, 0, " 1 E-02"), + (-0.1, 0, "-1 E-01"), + // i + (1.0, 0, " 1 E+00"), + // Precision + (-0.123456789, 3, "-1.235 E-01"), + (-0.123456789, 2, "-1.23 E-01"), + (-0.123456789, 2, "-1.23 E-01"), + (-1e99, 2, "-1.00 E+99"), + (-1e100, 2, "-1.00 E+100"), + // Rounding + (0.5, 2, " 5.00 E-01"), + (0.5, 1, " 5.0 E-01"), + (0.5, 0, " 5 E-01"), + (1.5, 2, " 1.50 E+00"), + (1.5, 1, " 1.5 E+00"), + (1.5, 0, " 2 E+00"), + ] { + assert_eq!( + CalculatorDisplayMode::Scientific { precision }.format_number(f), + s + ); + } + } + + #[test] + fn test_separated() { + for (f, separator, s) in vec![ + (100.0, ',', "100"), + (100.0, ',', "100"), + (-100.0, ',', "-100"), + (1_000.0, ',', "1,000"), + (-1_000.0, ',', "-1,000"), + (10_000.0, ',', "10,000"), + (-10_000.0, ',', "-10,000"), + (100_000.0, ',', "100,000"), + (-100_000.0, ',', "-100,000"), + (1_000_000.0, ',', "1,000,000"), + (-1_000_000.0, ',', "-1,000,000"), + (1_000_000.123456789, ',', "1,000,000.123456789"), + (-1_000_000.123456789, ',', "-1,000,000.123456789"), + (1_000_000.123456789, ' ', "1 000 000.123456789"), + (1_000_000.123456789, ' ', "1 000 000.123456789"), + ] { + assert_eq!( + CalculatorDisplayMode::Separated { separator }.format_number(f), + s + ); + } + } + + #[test] + fn test_engineering() { + for (f, precision, s) in vec![ + (100.0, 3, " 100.000 E+00"), + (100.0, 3, " 100.000 E+00"), + (-100.0, 3, "-100.000 E+00"), + (100.0, 0, " 100. E+00"), + (-100.0, 0, "-100. E+00"), + (0.1, 2, " 100.00 E-03"), + (0.01, 2, " 10.00 E-03"), + (0.001, 2, " 1.00 E-03"), + (0.0001, 2, " 100.00 E-06"), + // Rounding + (0.5, 2, " 500.00 E-03"), + (0.5, 1, " 500.0 E-03"), + (0.5, 0, " 500. E-03"), + (1.5, 2, " 1.50 E+00"), + (1.5, 1, " 1.5 E+00"), + (1.5, 0, " 2. E+00"), + ] { + assert_eq!( + CalculatorDisplayMode::Engineering { precision }.format_number(f), + s + ); + } + } +} diff --git a/src/format.rs b/src/format.rs deleted file mode 100644 index 7849118..0000000 --- a/src/format.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Based on https://stackoverflow.com/a/65266882 -pub fn scientific(f: f64, precision: usize) -> String { - let mut ret = format!("{:.precision$E}", f, precision = precision); - let exp = ret.split_off(ret.find('E').unwrap_or(0)); - let (exp_sign, exp) = exp - .strip_prefix("E-") - .map_or_else(|| ('+', &exp[1..]), |stripped| ('-', stripped)); - - let sign = if ret.starts_with('-') { "" } else { " " }; - format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2) -} - -pub fn engineering(f: f64, precision: usize) -> String { - // Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string - // 1,000 => 1000E3 - let all = format!(" {:.precision$E}", f, precision = precision) - // Remove . since it can be moved - .replacen(".", "", 1) - // Add 00E before E here so the length is enough for slicing below - .replacen("E", "00E", 1); - // Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E - // 1000E3 => (1000, E3) - let (num_str, exp_str) = all.split_at(all.find('E').unwrap()); - // Extract the exponent as an isize. This should always be true because f64 max will be ~400 - // E3 => 3 as isize - let exp = exp_str[1..].parse::().unwrap(); - // Sign of the exponent. If string representation starts with E-, then negative - let display_exp_sign = if exp_str.strip_prefix("E-").is_some() { - '-' - } else { - '+' - }; - - // The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above - // 100 => 0, 1000 => 3, .1 => 3 (but will show as -3) - let display_exp = (exp.div_euclid(3) * 3).abs(); - // Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility - let num_whole_digits = exp.rem_euclid(3) as usize + 1; - - // If this is a negative number, strip off the added space, otherwise keep the space (and next digit) - let num_str = if num_str.strip_prefix(" -").is_some() { - &num_str[1..] - } else { - num_str - }; - - // Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit) - // Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10 - let whole = &num_str[0..=num_whole_digits]; - // Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2 - let decimal = &num_str[(num_whole_digits + 1)..=(precision + num_whole_digits)]; - // Right align whole portion, always have decimal point - format!( - "{: >4}.{} E{}{:0>pad$}", - // display_sign, - whole, - decimal, - display_exp_sign, - display_exp, - pad = 2 - ) -} - -pub fn separated(f: f64, sep: char) -> String { - let mut ret = f.to_string(); - let start = if ret.starts_with('-') { 1 } else { 0 }; - let end = ret.find('.').unwrap_or_else(|| ret.len()); - for i in 0..((end - start - 1).div_euclid(3)) { - ret.insert(end - (i + 1) * 3, sep); - } - ret -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_scientific() { - for (f, p, s) in vec![ - // Basic - (1.0, 0, " 1 E+00"), - (-1.0, 0, "-1 E+00"), - (100.0, 0, " 1 E+02"), - (0.1, 0, " 1 E-01"), - (0.01, 0, " 1 E-02"), - (-0.1, 0, "-1 E-01"), - // i - (1.0, 0, " 1 E+00"), - // Precision - (-0.123456789, 3, "-1.235 E-01"), - (-0.123456789, 2, "-1.23 E-01"), - (-0.123456789, 2, "-1.23 E-01"), - (-1e99, 2, "-1.00 E+99"), - (-1e100, 2, "-1.00 E+100"), - // Rounding - (0.5, 2, " 5.00 E-01"), - (0.5, 1, " 5.0 E-01"), - (0.5, 0, " 5 E-01"), - (1.5, 2, " 1.50 E+00"), - (1.5, 1, " 1.5 E+00"), - (1.5, 0, " 2 E+00"), - ] { - assert_eq!(scientific(f, p), s); - } - } - - #[test] - fn test_separated() { - for (f, c, s) in vec![ - (100.0, ',', "100"), - (100.0, ',', "100"), - (-100.0, ',', "-100"), - (1_000.0, ',', "1,000"), - (-1_000.0, ',', "-1,000"), - (10_000.0, ',', "10,000"), - (-10_000.0, ',', "-10,000"), - (100_000.0, ',', "100,000"), - (-100_000.0, ',', "-100,000"), - (1_000_000.0, ',', "1,000,000"), - (-1_000_000.0, ',', "-1,000,000"), - (1_000_000.123456789, ',', "1,000,000.123456789"), - (-1_000_000.123456789, ',', "-1,000,000.123456789"), - (1_000_000.123456789, ' ', "1 000 000.123456789"), - (1_000_000.123456789, ' ', "1 000 000.123456789"), - ] { - assert_eq!(separated(f, c), s); - } - } - - #[test] - fn test_engineering() { - for (f, c, s) in vec![ - (100.0, 3, " 100.000 E+00"), - (100.0, 3, " 100.000 E+00"), - (-100.0, 3, "-100.000 E+00"), - (100.0, 0, " 100. E+00"), - (-100.0, 0, "-100. E+00"), - (0.1, 2, " 100.00 E-03"), - (0.01, 2, " 10.00 E-03"), - (0.001, 2, " 1.00 E-03"), - (0.0001, 2, " 100.00 E-06"), - // Rounding - (0.5, 2, " 500.00 E-03"), - (0.5, 1, " 500.0 E-03"), - (0.5, 0, " 500. E-03"), - (1.5, 2, " 1.50 E+00"), - (1.5, 1, " 1.5 E+00"), - (1.5, 0, " 2. E+00"), - ] { - assert_eq!(engineering(f, c), s); - } - } -} diff --git a/src/main.rs b/src/main.rs index 44ebbf4..969bfee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,13 +7,12 @@ mod calc; mod event; -mod format; const BORDER_SIZE: u16 = 2; use calc::{ errors::CalculatorResult, - types::{CalculatorAlignment, CalculatorDisplayMode, CalculatorState, RegisterState}, + types::{CalculatorAlignment, CalculatorState, RegisterState}, Calculator, }; use crossterm::{ @@ -110,7 +109,12 @@ impl App { .constants .iter() .map(|(key, constant)| { - format!("{}: {} ({})", key, constant.help, constant.value) + format!( + "{}: {} ({})", + key, + constant.help, + self.calculator.display_mode.format_number(constant.value) + ) }) .fold(String::new(), |acc, s| acc + &s + "\n") .trim_end(), @@ -159,6 +163,7 @@ impl App { msg: "\ d => Degrees\n\ r => Radians\n\ + g => Grads\n\ _ => Default\n\ , => Comma separated\n\ => Space separated\n\ @@ -228,21 +233,7 @@ impl App { .enumerate() .rev() .map(|(i, m)| { - let number = match self.calculator.display_mode { - CalculatorDisplayMode::Default => format!("{}", m), - CalculatorDisplayMode::Separated { separator } => { - format::separated(*m, separator) - } - CalculatorDisplayMode::Scientific { precision } => { - format::scientific(*m, precision) - } - CalculatorDisplayMode::Engineering { precision } => { - format::engineering(*m, precision) - } - CalculatorDisplayMode::Fixed { precision } => { - format!("{:0>.precision$}", m, precision = precision) - } - }; + let number = self.calculator.display_mode.format_number(*m); let content = match self.calculator.calculator_alignment { CalculatorAlignment::Left => format!("{:>2}: {}", i, number), CalculatorAlignment::Right => {