Major refactor, code cleanup, and documentation
This commit is contained in:
parent
43c3dfd0f9
commit
4b0e6e7e10
513
src/calc.rs
513
src/calc.rs
@ -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,175 +163,209 @@ 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),
|
||||||
'+' => self.op(CalculatorOperation::Add),
|
CalculatorState::WaitingForMacro => self.macro_input(c),
|
||||||
'-' => 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::WaitingForRegister(register_state) => {
|
CalculatorState::WaitingForRegister(register_state) => {
|
||||||
match register_state {
|
let register_state = *register_state;
|
||||||
RegisterState::Save => {
|
self.register_input(register_state, c)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
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<()> {
|
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))?;
|
|
||||||
self.push(f)?;
|
|
||||||
self.l.clear();
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let f = self.l.parse::<f64>().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 {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
153
src/format.rs
153
src/format.rs
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
27
src/main.rs
27
src/main.rs
@ -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 => {
|
||||||
|
Loading…
Reference in New Issue
Block a user