Major refactor, code cleanup, and documentation

This commit is contained in:
Austen Adler 2021-05-25 21:42:04 -04:00
parent 43c3dfd0f9
commit 4b0e6e7e10
6 changed files with 540 additions and 395 deletions

View File

@ -15,40 +15,58 @@ use types::{
RegisterState, RegisterState,
}; };
/// The maximum precision allowed for the calculator
const MAX_PRECISION: usize = 20; const MAX_PRECISION: usize = 20;
/// The name of the app, used for configuration file generation
const APP_NAME: &str = "rpn_rs"; const APP_NAME: &str = "rpn_rs";
/// The default precision to sue
const DEFAULT_PRECISION: usize = 3; const DEFAULT_PRECISION: usize = 3;
/// The history mode of the entry - either a single change or a macro bound
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
enum HistoryMode { enum HistoryMode {
One, One,
Macro, Macro,
} }
/// The main calculator struct that contains all fields internally
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Calculator { pub struct Calculator {
/// The entry buffer
#[serde(skip)] #[serde(skip)]
l: String, l: String,
/// The stack
pub stack: VecDeque<f64>, pub stack: VecDeque<f64>,
/// True if the user would like to save on quit
save_on_close: bool, save_on_close: bool,
/// Left or right aligned display
pub calculator_alignment: CalculatorAlignment, pub calculator_alignment: CalculatorAlignment,
/// The angle mode, such as DEG or RAD
#[serde(flatten)] #[serde(flatten)]
pub angle_mode: CalculatorAngleMode, pub angle_mode: CalculatorAngleMode,
/// The display format such as separated or scientific
#[serde(flatten)] #[serde(flatten)]
pub display_mode: CalculatorDisplayMode, pub display_mode: CalculatorDisplayMode,
#[serde(serialize_with = "ordered_char_map")] /// A set of the currently running macros, used for ensuring there are no recursive macro calls
pub macros: CalculatorMacros,
#[serde(skip)] #[serde(skip)]
active_macros: HashSet<char>, active_macros: HashSet<char>,
/// 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")] #[serde(serialize_with = "ordered_char_map")]
pub constants: CalculatorConstants, pub constants: CalculatorConstants,
/// Map of chars to registers
#[serde(skip)] #[serde(skip)]
pub registers: CalculatorRegisters, pub registers: CalculatorRegisters,
/// Vec of state changes that can be undone
#[serde(skip)] #[serde(skip)]
undo_buf: Vec<CalculatorStateChange>, undo_buf: Vec<CalculatorStateChange>,
/// Vec of state changes that can be redone
#[serde(skip)] #[serde(skip)]
redo_buf: Vec<CalculatorStateChange>, redo_buf: Vec<CalculatorStateChange>,
/// The current state of the calculator, such as normal, or waiting for macro char
#[serde(skip)] #[serde(skip)]
pub state: CalculatorState, pub state: CalculatorState,
} }
@ -145,12 +163,46 @@ impl Calculator {
store(APP_NAME, self).map_err(|e| CalculatorError::SaveError(Some(e))) 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<()> { pub fn take_input(&mut self, c: char) -> CalculatorResult<()> {
match &self.state { match &self.state {
CalculatorState::Normal => match c { CalculatorState::Normal => self.normal_input(c),
c @ '0'..='9' | c @ '.' | c @ 'e' => self.entry(c), CalculatorState::WaitingForConstant => self.constant_input(c),
CalculatorState::WaitingForMacro => self.macro_input(c),
CalculatorState::WaitingForRegister(register_state) => {
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::Add),
'-' => self.op(CalculatorOperation::Subtract), '-' => self.op(CalculatorOperation::Subtract),
'*' => self.op(CalculatorOperation::Multiply), '*' => self.op(CalculatorOperation::Multiply),
@ -198,8 +250,9 @@ impl Calculator {
Ok(()) Ok(())
} }
_ => Err(CalculatorError::NoSuchOperator(c)), _ => Err(CalculatorError::NoSuchOperator(c)),
}, }
CalculatorState::WaitingForConstant => { }
fn constant_input(&mut self, c: char) -> CalculatorResult<()> {
let f = self let f = self
.constants .constants
.get(&c) .get(&c)
@ -210,7 +263,7 @@ impl Calculator {
self.state = CalculatorState::Normal; self.state = CalculatorState::Normal;
Ok(()) Ok(())
} }
CalculatorState::WaitingForMacro => { fn macro_input(&mut self, c: char) -> CalculatorResult<()> {
let mac = self.macros.get(&c).ok_or(CalculatorError::NoSuchMacro(c))?; 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 // self.take_input below takes a mutable reference to self, so must clone the value here
let value = mac.value.clone(); let value = mac.value.clone();
@ -243,7 +296,7 @@ impl Calculator {
Ok(()) Ok(())
} }
CalculatorState::WaitingForRegister(register_state) => { fn register_input(&mut self, register_state: RegisterState, c: char) -> CalculatorResult<()> {
match register_state { match register_state {
RegisterState::Save => { RegisterState::Save => {
let f = self.pop()?; let f = self.pop()?;
@ -262,7 +315,7 @@ impl Calculator {
self.state = CalculatorState::Normal; self.state = CalculatorState::Normal;
Ok(()) Ok(())
} }
CalculatorState::WaitingForSetting => { fn setting_input(&mut self, c: char) -> CalculatorResult<()> {
self.flush_l()?; self.flush_l()?;
match c { match c {
'q' => self.state = CalculatorState::Normal, 'q' => self.state = CalculatorState::Normal,
@ -311,9 +364,8 @@ impl Calculator {
self.state = CalculatorState::Normal; self.state = CalculatorState::Normal;
Ok(()) Ok(())
} }
}
}
/// Resets the calculator state to normal, and exits out of a macro if one is running
pub fn cancel(&mut self) -> CalculatorResult<()> { pub fn cancel(&mut self) -> CalculatorResult<()> {
self.state = CalculatorState::Normal; self.state = CalculatorState::Normal;
// We died in a macro. Quit and push an end macro state // We died in a macro. Quit and push an end macro state
@ -324,10 +376,12 @@ impl Calculator {
} }
Ok(()) Ok(())
} }
/// Handles the backspace key which only deletes a char a char from l
pub fn backspace(&mut self) -> CalculatorResult<()> { pub fn backspace(&mut self) -> CalculatorResult<()> {
self.l.pop(); self.l.pop();
Ok(()) Ok(())
} }
/// Places the bottom of the stack into l for editing, only if the value is empty
pub fn edit(&mut self) -> CalculatorResult<()> { pub fn edit(&mut self) -> CalculatorResult<()> {
if !self.l.is_empty() { if !self.l.is_empty() {
return Ok(()); return Ok(());
@ -339,38 +393,12 @@ impl Calculator {
.to_string(); .to_string();
Ok(()) Ok(())
} }
/// Get the value of l
pub fn get_l(&mut self) -> &str { pub fn get_l(&mut self) -> &str {
self.l.as_ref() 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 { pub fn get_status_line(&self) -> String {
format!( 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<bool> { pub fn flush_l(&mut self) -> CalculatorResult<bool> {
if self.l.is_empty() { if self.l.is_empty() {
Ok(false) return Ok(false);
} else { }
let f = self.l.parse::<f64>().or(Err(CalculatorError::ParseError))?; let f = self.l.parse::<f64>().or(Err(CalculatorError::ParseError))?;
self.push(f)?; self.push(f)?;
self.l.clear(); self.l.clear();
Ok(true) Ok(true)
} }
} /// Checks if the calculator is currently running a macro
fn within_macro(&self) -> bool { fn within_macro(&self) -> bool {
!self.active_macros.is_empty() !self.active_macros.is_empty()
} }
/// Pushes a value onto the stack and makes a state change
fn push(&mut self, f: f64) -> CalculatorResult<()> { fn push(&mut self, f: f64) -> CalculatorResult<()> {
self.direct_state_change(CalculatorStateChange { self.direct_state_change(CalculatorStateChange {
pop: OpArgs::None, pop: OpArgs::None,
push: OpArgs::Unary(f), push: OpArgs::Unary(f),
}) })
} }
pub fn peek(&mut self) -> CalculatorResult<f64> { /// Returns the value of the bottom of the stack by popping it using a state change
self.flush_l()?;
self.checked_get(0)
}
pub fn pop(&mut self) -> CalculatorResult<f64> { pub fn pop(&mut self) -> CalculatorResult<f64> {
let f = self.peek()?; let f = self.peek(0)?;
self.direct_state_change(CalculatorStateChange { self.direct_state_change(CalculatorStateChange {
pop: OpArgs::Unary(f), pop: OpArgs::Unary(f),
push: OpArgs::None, push: OpArgs::None,
})?; })?;
Ok(f) Ok(f)
} }
/// Returns a calculator value
fn peek(&mut self, idx: usize) -> CalculatorResult<f64> {
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<usize> { pub fn pop_precision(&mut self) -> CalculatorResult<usize> {
let f = self.peek()?; let f = self.peek(0)?;
// Ensure this can be cast to a usize // Ensure this can be cast to a usize
if !f.is_finite() || f.is_sign_negative() { if !f.is_finite() || f.is_sign_negative() {
return Err(CalculatorError::ArithmeticError); return Err(CalculatorError::ArithmeticError);
@ -432,6 +469,7 @@ impl Calculator {
})?; })?;
Ok(u) Ok(u)
} }
/// Performs a calculator operation such as undo, redo, operator, or dup
pub fn op(&mut self, op: CalculatorOperation) -> CalculatorResult<()> { pub fn op(&mut self, op: CalculatorOperation) -> CalculatorResult<()> {
// Dup is special -- don't actually run it if l needs to be flushed // Dup is special -- don't actually run it if l needs to be flushed
if self.flush_l()? { if self.flush_l()? {
@ -521,6 +559,9 @@ impl Calculator {
self.direct_state_change(state_change?) 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<()> { fn history_op(&mut self, forward: bool) -> CalculatorResult<()> {
let s = if forward { let s = if forward {
&self.redo_buf &self.redo_buf
@ -573,6 +614,7 @@ impl Calculator {
} }
} }
} }
/// Performs a state change on a unary operation
fn unary_op( fn unary_op(
&mut self, &mut self,
op: impl FnOnce(f64) -> OpArgs, op: impl FnOnce(f64) -> OpArgs,
@ -586,6 +628,7 @@ impl Calculator {
push: op(*arg), push: op(*arg),
}) })
} }
/// Performs a state change on a binary operation
fn binary_op( fn binary_op(
&mut self, &mut self,
op: impl FnOnce([f64; 2]) -> OpArgs, 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<()> { fn direct_state_change(&mut self, c: CalculatorStateChange) -> CalculatorResult<()> {
let result = self.apply_state_change(c, true); let result = self.apply_state_change(c, true);
if result.is_ok() { if result.is_ok() {
@ -615,6 +659,7 @@ impl Calculator {
result 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( fn apply_state_change(
&mut self, &mut self,
c: CalculatorStateChange, c: CalculatorStateChange,
@ -674,23 +719,69 @@ impl Calculator {
Ok(()) Ok(())
} }
fn stack_eq(&self, idx: usize, value: f64) -> CalculatorResult<()> { /// Checks if a value on the stack is equal to a given value
if (self.checked_get(idx)? - value).abs() > f64::EPSILON { fn stack_eq(&mut self, idx: usize, value: f64) -> CalculatorResult<()> {
if (self.peek(idx)? - value).abs() > f64::EPSILON {
Err(CalculatorError::CorruptStateChange(format!( Err(CalculatorError::CorruptStateChange(format!(
"Stack index {} should be {}, but is {}", "Stack index {} should be {}, but is {}",
idx, idx,
value, value,
self.checked_get(idx)?, self.peek(idx)?,
))) )))
} else { } else {
Ok(()) Ok(())
} }
} }
}
fn checked_get(&self, idx: usize) -> CalculatorResult<f64> { #[cfg(test)]
match self.stack.get(idx) { mod tests {
None => Err(CalculatorError::NotEnoughStackEntries), use super::*;
Some(r) => Ok(*r),
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);
}
} }

View File

@ -4,21 +4,36 @@ use std::fmt;
pub type CalculatorResult<T> = Result<T, CalculatorError>; pub type CalculatorResult<T> = Result<T, CalculatorError>;
/// All possible errors the calculator can throw
#[derive(Debug)] #[derive(Debug)]
pub enum CalculatorError { pub enum CalculatorError {
/// Divide by zero, log(-1), etc
ArithmeticError, ArithmeticError,
/// Not enough stck entries for operation
NotEnoughStackEntries, NotEnoughStackEntries,
/// Thrown when an undo or redo cannot be performed
CorruptStateChange(String), CorruptStateChange(String),
/// Cannot undo or redo
EmptyHistory(String), EmptyHistory(String),
/// Constant undefined
NoSuchConstant(char), NoSuchConstant(char),
/// Register undefined
NoSuchRegister(char), NoSuchRegister(char),
/// Macro undefined
NoSuchMacro(char), NoSuchMacro(char),
/// Operator undefined
NoSuchOperator(char), NoSuchOperator(char),
/// Setting undefined
NoSuchSetting(char), NoSuchSetting(char),
/// Macro calls itself
RecursiveMacro(char), RecursiveMacro(char),
/// Could not convert l to number
ParseError, ParseError,
/// Requested precision is too high
PrecisionTooHigh, PrecisionTooHigh,
/// Config serialization error
SaveError(Option<ConfyError>), SaveError(Option<ConfyError>),
/// Config deserialization error
LoadError(Option<ConfyError>), LoadError(Option<ConfyError>),
} }

View File

@ -1,10 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(PartialEq, Debug, Serialize, Deserialize)] /// Operations that can be sent to the calculator such as +, -, or undo
pub enum MacroState {
Start,
End,
}
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
pub enum CalculatorOperation { pub enum CalculatorOperation {
Add, Add,
@ -37,14 +32,27 @@ pub enum CalculatorOperation {
Macro(MacroState), 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)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
pub enum OpArgs { pub enum OpArgs {
/// This is a macro start and end noop
Macro(MacroState), Macro(MacroState),
/// Operation takes 1 argument, ex: sqrt or negate
Unary(f64), Unary(f64),
/// Operation takes 2 arguments, ex: + or -
Binary([f64; 2]), Binary([f64; 2]),
/// Operation takes no arguments, ex: push
None, None,
} }
/// Record of what to pop and push. Used for undo and redo buffers
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
pub struct CalculatorStateChange { pub struct CalculatorStateChange {
pub pop: OpArgs, pub pop: OpArgs,

View File

@ -2,12 +2,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
#[derive(Debug, Serialize, Deserialize)] /// The calculator state
pub enum RegisterState {
Save,
Load,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum CalculatorState { pub enum CalculatorState {
Normal, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalculatorConstant { pub struct CalculatorConstant {
pub help: String, pub help: String,
pub value: f64, pub value: f64,
} }
/// One calculator macro containing a messsage and value
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalculatorMacro { pub struct CalculatorMacro {
pub help: String, pub help: String,
pub value: String, pub value: String,
} }
/// Map of chars to constants
pub type CalculatorConstants = HashMap<char, CalculatorConstant>; pub type CalculatorConstants = HashMap<char, CalculatorConstant>;
/// Map of chars to macros
pub type CalculatorMacros = HashMap<char, CalculatorMacro>; pub type CalculatorMacros = HashMap<char, CalculatorMacro>;
/// Map of chars to registers
pub type CalculatorRegisters = HashMap<char, f64>; pub type CalculatorRegisters = HashMap<char, f64>;
/// Possible calculator angle modes
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "angle_mode")] #[serde(tag = "angle_mode")]
pub enum CalculatorAngleMode { pub enum CalculatorAngleMode {
@ -65,14 +73,21 @@ impl fmt::Display for CalculatorAngleMode {
} }
} }
/// The calculator digit display mode
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "display_mode")] #[serde(tag = "display_mode")]
// Could also have added content="precision"
pub enum CalculatorDisplayMode { pub enum CalculatorDisplayMode {
/// Rust's default f64 format
Default, Default,
/// Thousands separator
Separated { separator: char }, Separated { separator: char },
/// Aligned scientific format
Scientific { precision: usize }, Scientific { precision: usize },
/// Scientific format, chunked by groups of 3
///
/// Example: 1 E+5 or 100E+5
Engineering { precision: usize }, Engineering { precision: usize },
/// Fixed precision
Fixed { precision: usize }, 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::<isize>().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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CalculatorAlignment { pub enum CalculatorAlignment {
Right, 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
);
}
}
}

View File

@ -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::<isize>().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);
}
}
}

View File

@ -7,13 +7,12 @@
mod calc; mod calc;
mod event; mod event;
mod format;
const BORDER_SIZE: u16 = 2; const BORDER_SIZE: u16 = 2;
use calc::{ use calc::{
errors::CalculatorResult, errors::CalculatorResult,
types::{CalculatorAlignment, CalculatorDisplayMode, CalculatorState, RegisterState}, types::{CalculatorAlignment, CalculatorState, RegisterState},
Calculator, Calculator,
}; };
use crossterm::{ use crossterm::{
@ -110,7 +109,12 @@ impl App {
.constants .constants
.iter() .iter()
.map(|(key, constant)| { .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") .fold(String::new(), |acc, s| acc + &s + "\n")
.trim_end(), .trim_end(),
@ -159,6 +163,7 @@ impl App {
msg: "\ msg: "\
d => Degrees\n\ d => Degrees\n\
r => Radians\n\ r => Radians\n\
g => Grads\n\
_ => Default\n\ _ => Default\n\
, => Comma separated\n\ , => Comma separated\n\
<space> => Space separated\n\ <space> => Space separated\n\
@ -228,21 +233,7 @@ impl App {
.enumerate() .enumerate()
.rev() .rev()
.map(|(i, m)| { .map(|(i, m)| {
let number = match self.calculator.display_mode { let number = self.calculator.display_mode.format_number(*m);
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 content = match self.calculator.calculator_alignment { let content = match self.calculator.calculator_alignment {
CalculatorAlignment::Left => format!("{:>2}: {}", i, number), CalculatorAlignment::Left => format!("{:>2}: {}", i, number),
CalculatorAlignment::Right => { CalculatorAlignment::Right => {