From 2f2976bfd7a12fbb6a4ba00d51ea7b25ae5702a3 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Sat, 2 Oct 2021 08:20:23 -0400 Subject: [PATCH] Split all datatypes --- Cargo.lock | 2 +- Cargo.toml | 2 +- pipeline.yml | 22 + src/calc.rs | 4 +- src/calc/entries.rs | 1211 ++++++++---------------------------- src/calc/entries/matrix.rs | 353 +++++++++++ src/calc/entries/number.rs | 339 ++++++++++ src/calc/entries/vector.rs | 291 +++++++++ src/main.rs | 2 +- 9 files changed, 1255 insertions(+), 971 deletions(-) create mode 100644 pipeline.yml create mode 100644 src/calc/entries/matrix.rs create mode 100644 src/calc/entries/number.rs create mode 100644 src/calc/entries/vector.rs diff --git a/Cargo.lock b/Cargo.lock index 3603ef7..e2a47cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,7 +231,7 @@ dependencies = [ [[package]] name = "rpn_rs" -version = "0.5.0" +version = "0.6.0" dependencies = [ "confy", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index a696739..33f3453 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rpn_rs" -version = "0.5.0" +version = "0.6.0" description = "A TUI RPN calculator, similar to Orpie" authors = ["Austen Adler "] edition = "2018" diff --git a/pipeline.yml b/pipeline.yml new file mode 100644 index 0000000..690173e --- /dev/null +++ b/pipeline.yml @@ -0,0 +1,22 @@ +--- +resources: + - name: source + type: git + source: + uri: https://gitea.austen-wares.com/stonewareslord/rpn_rs + branch: develop +jobs: + - name: build + serial: true + plan: + - get: source + - task: build + config: + platform: linux + image_resource: + type: docker-image + source: {repository: } + inputs: + - name: source + outputs: + - name: out diff --git a/src/calc.rs b/src/calc.rs index 444de3a..1812acf 100644 --- a/src/calc.rs +++ b/src/calc.rs @@ -850,14 +850,14 @@ impl Calculator { /// Checks if a value on the stack is equal to a given value fn stack_eq(&mut self, idx: usize, value: &Entry) -> CalculatorResult<()> { if self.peek(idx)? == *value { + Ok(()) + } else { Err(CalculatorError::CorruptStateChange(format!( "Stack index {} should be {}, but is {}", idx, value, self.peek(idx)?, ))) - } else { - Ok(()) } } diff --git a/src/calc/entries.rs b/src/calc/entries.rs index 3bf97d2..12f24f2 100644 --- a/src/calc/entries.rs +++ b/src/calc/entries.rs @@ -1,11 +1,17 @@ // TODO: Clippy is recommending pass by value instead of by ref, but I plan to add imaginary numbers, which will change this #![allow(clippy::trivially_copy_pass_by_ref)] +mod matrix; +mod number; +mod vector; use super::errors::{CalculatorError, CalculatorResult}; -use super::types::CalculatorAngleMode; +use crate::calc::types::CalculatorAngleMode; use crate::calc::CalculatorDisplayMode; +pub use matrix::{Matrix, MatrixDimensions}; +pub use number::Number; use serde::{Deserialize, Serialize}; use std::cmp::PartialEq; use std::fmt; +pub use vector::{Vector, VectorDirection}; pub trait CalculatorEntry where @@ -43,85 +49,6 @@ where fn pow(&self, arg: &Entry) -> CalculatorResult; } -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub struct Number { - pub value: f64, -} - -impl PartialEq for Number { - fn eq(&self, other: &Self) -> bool { - if self.value.is_nan() && other.value.is_nan() - || self.value.is_infinite() && other.value.is_infinite() - { - true - } else if self.value.is_nan() - || self.value.is_infinite() - || other.value.is_infinite() - || other.value.is_nan() - { - false - } else { - (self.value - other.value).abs() >= f64::EPSILON - } - } -} - -#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] -pub enum VectorDirection { - Row, - Column, -} - -impl VectorDirection { - pub const fn swap(&self) -> Self { - match self { - Self::Row => Self::Column, - Self::Column => Self::Row, - } - } - - pub const fn get_separator(&self) -> &str { - match self { - Self::Row => " ", - Self::Column => "; ", - } - } -} - -impl Default for VectorDirection { - fn default() -> Self { - // Column vectors are the default - Self::Column - } -} - -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -pub struct Vector { - pub values: Vec, - pub direction: VectorDirection, -} - -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -pub struct MatrixDimensions { - rows: usize, - columns: usize, -} - -impl MatrixDimensions { - pub const fn transpose(&self) -> Self { - Self { - rows: self.columns, - columns: self.rows, - } - } -} - -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -pub struct Matrix { - pub vectors: Vec, - pub dimensions: MatrixDimensions, -} - #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Entry { @@ -317,788 +244,6 @@ impl CalculatorEntry for Entry { } } -impl CalculatorEntry for Matrix { - fn to_editable_string(&self) -> CalculatorResult { - // TODO: Eventualy we can parse and edit a matrix as a string - Err(CalculatorError::TypeMismatch) - } - fn is_valid(&self) -> bool { - // The the number of vectors is equal to the 0th dimension - self.vectors.len() == self.dimensions.columns - // The number of elements in all vectors are equal to the 1st dimension, and each is valid - && self - .vectors - .iter() - .all(|v| v.values.len() == self.dimensions.rows && v.is_valid()) - // The dimensions are not zero - && self.dimensions.rows > 0 && self.dimensions.columns > 0 - } - fn validate(self) -> CalculatorResult { - if self.is_valid() { - Ok(Entry::Matrix(self)) - } else { - Err(CalculatorError::ArithmeticError) - } - } - - fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { - format!( - "[ {} ]", - self.vectors - .iter() - .map(|vector| vector.format_entry(display_mode)) - .collect::>() - .join(" ") - ) - } - - // Mathematical operations - fn negate(&self) -> CalculatorResult { - self.iterated_unary(Vector::negate) - } - fn abs(&self) -> CalculatorResult { - // TODO: Compute determinant - Err(CalculatorError::NotYetImplemented) - } - fn inverse(&self) -> CalculatorResult { - // TODO: Inverse - Err(CalculatorError::NotYetImplemented) - } - fn transpose(&self) -> CalculatorResult { - // Iterate over all rows - let mut vectors: Vec = vec![]; - for r in 0..self.dimensions.rows { - vectors.push(Vector { - values: self - .vectors - .iter() - .map(|v| { - // For each row, get the r'th element to build a new vector - v.values - .get(r) - .map_or_else(|| Err(CalculatorError::DimensionMismatch), |n| Ok(*n)) - }) - .collect::>>()?, - direction: VectorDirection::Column, - }); - } - - Self { - vectors, - dimensions: self.dimensions.transpose(), - } - .validate() - } - fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.sin(angle_mode)) - } - fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.cos(angle_mode)) - } - fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.tan(angle_mode)) - } - fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.asin(angle_mode)) - } - fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.acos(angle_mode)) - } - fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|v| v.atan(angle_mode)) - } - fn sqrt(&self) -> CalculatorResult { - self.iterated_unary(Vector::sqrt) - } - fn log(&self) -> CalculatorResult { - self.iterated_unary(Vector::log) - } - fn ln(&self) -> CalculatorResult { - self.iterated_unary(Vector::ln) - } - - // Binary - fn add(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::add), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::add), - } - } - fn sub(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::sub), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::sub), - } - } - fn mul(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => { - if self.dimensions.columns != m2.dimensions.rows { - return Err(CalculatorError::DimensionMismatch); - } - let dimensions = MatrixDimensions { - rows: self.dimensions.rows, - columns: m2.dimensions.columns, - }; - - // A matrix is a list of column vectors, so transpose self and zip the columns - let transposed_self: Self = match self.transpose()? { - Entry::Matrix(t) => t, - _ => { - return Err(CalculatorError::InternalError(String::from( - "Matrix transpose produced wrong type", - ))) - } - }; - - let mut vectors: Vec = vec![]; - - for c in &m2.vectors { - let mut vector: Vector = Vector { - values: vec![], - direction: VectorDirection::Column, - }; - for r in &transposed_self.vectors { - if let Entry::Number(number) = - c.transpose()?.mul(&Entry::Vector(r.clone()))? - { - vector.values.push(number); - } else { - return Err(CalculatorError::InternalError(String::from( - "Vector multiplication did not produce a number", - ))); - } - } - vectors.push(vector); - } - - Self { - vectors, - dimensions, - } - .validate() - } - Entry::Vector(vector) => self.mul(&Self::from( - &[Entry::Vector(vector.clone())], // Treat a vector as a 1D matrix - )?), - Entry::Number(number) => self.iterated_binary_num(number, Vector::mul), - } - } - fn div(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::div), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::div), - } - } - fn int_divide(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::int_divide), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::int_divide), - } - } - fn modulo(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::modulo), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::modulo), - } - } - fn pow(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_m2) => Err(CalculatorError::TypeMismatch), - Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => self.iterated_binary_num(number, Vector::pow), - } - } -} - -impl Matrix { - pub fn from(entries: &[Entry]) -> CalculatorResult { - if entries.is_empty() { - return Err(CalculatorError::NotEnoughStackEntries); - } - - let vectors = entries - .iter() - .map(|e| match e { - Entry::Matrix(_) | Entry::Number(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(vector) => Ok(vector.clone()), - }) - .collect::>>()?; - - // Get the num_rows and dimension of the matrix - let first_vector = vectors - .get(0) - .ok_or(CalculatorError::NotEnoughStackEntries)?; - - // The number of rows in this column-based matrix - let num_rows = first_vector.values.len(); - // The direction all vectors must face - let vector_direction = first_vector.direction; - - // Either the dimension lengths mismatch, or the vectors are facing different directions (and are longer than 1, since a 1-length vector orientation does not matter - if vectors.iter().any(|v| v.values.len() != num_rows) - || (num_rows > 1 && vectors.iter().any(|v| v.direction != vector_direction)) - { - return Err(CalculatorError::DimensionMismatch); - } - - let dimensions = MatrixDimensions { - rows: num_rows, - columns: vectors.len(), - }; - - let ret = Self { - vectors, - dimensions, - }; - - // If the user tried making a matrix out of row vectors, we need to transpose it, which forces column vectors - if vector_direction == VectorDirection::Row && num_rows > 1 { - ret.transpose() - } else { - ret.validate() - } - } - - fn iterated_unary( - &self, - op: impl Fn(&Vector) -> CalculatorResult, - ) -> CalculatorResult { - Self { - vectors: self - .vectors - .iter() - .map(|v| op(v)) - .map(|e| match e { - Ok(Entry::Vector(vector)) => Ok(vector), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - dimensions: self.dimensions.clone(), - } - .validate() - } - - fn iterated_binary_num( - &self, - number: &Number, - op: impl Fn(&Vector, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - Self { - vectors: self - .vectors - .iter() - .map(|v| op(v, &Entry::Number(*number))) - .map(|e| match e { - Ok(Entry::Vector(vector)) => Ok(vector), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - dimensions: self.dimensions.clone(), - } - .validate() - } - - fn iterated_binary_mat( - &self, - m2: &Self, - op: impl Fn(&Vector, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - if self.dimensions != m2.dimensions { - return Err(CalculatorError::DimensionMismatch); - } - Self { - vectors: self - .vectors - .iter() - .zip(m2.vectors.iter()) - .map(|(v1, v2)| op(v1, &Entry::Vector(v2.clone()))) - .map(|e| match e { - Ok(Entry::Vector(vector)) => Ok(vector), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - dimensions: self.dimensions.clone(), - } - .validate() - } -} - -impl CalculatorEntry for Vector { - // Misc - fn to_editable_string(&self) -> CalculatorResult { - // TODO: Eventualy we can parse and edit a vector as a string - Err(CalculatorError::TypeMismatch) - } - fn is_valid(&self) -> bool { - self.values.iter().all(|number| number.is_valid()) - } - fn validate(self) -> CalculatorResult { - if self.is_valid() { - Ok(Entry::Vector(self)) - } else { - Err(CalculatorError::ArithmeticError) - } - } - - fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { - format!( - "[{}]", - self.values - .iter() - .map(|number| number.format_entry(display_mode)) - .collect::>() - .join(self.direction.get_separator()) - ) - } - // Mathematical operations - fn negate(&self) -> CalculatorResult { - self.iterated_unary(Number::negate) - } - fn abs(&self) -> CalculatorResult { - let value: Entry = self - .values - .iter() - .try_fold(Entry::Number(Number::ZERO), |acc, n2| { - acc.add(&n2.pow(&Entry::Number(Number { value: 2.0_f64 }))?) - })?; - value.sqrt() - } - fn inverse(&self) -> CalculatorResult { - // TODO: Correct implementation of this. This is the transpose - Ok(Entry::Vector(Self { - values: self.values.clone(), - direction: self.direction.swap(), - })) - } - fn transpose(&self) -> CalculatorResult { - Ok(Entry::Vector(Self { - values: self.values.clone(), - direction: self.direction.swap(), - })) - } - fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.sin(angle_mode)) - } - fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.cos(angle_mode)) - } - fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.tan(angle_mode)) - } - fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.asin(angle_mode)) - } - fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.acos(angle_mode)) - } - fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - self.iterated_unary(|n| n.atan(angle_mode)) - } - fn sqrt(&self) -> CalculatorResult { - self.iterated_unary(Number::sqrt) - } - fn log(&self) -> CalculatorResult { - self.iterated_unary(Number::log) - } - fn ln(&self) -> CalculatorResult { - self.iterated_unary(Number::ln) - } - - fn add(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::add), - Entry::Number(number) => self.iterated_binary_num(number, Number::add), - } - } - fn sub(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::sub), - Entry::Number(number) => self.iterated_binary_num(number, Number::sub), - } - } - fn mul(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_matrix) => Matrix::from(&[Entry::Vector(self.clone())])?.mul(arg), - Entry::Vector(v2) => { - if self.values.len() != v2.values.len() { - return Err(CalculatorError::DimensionMismatch); - } - match (self.direction, v2.direction) { - (VectorDirection::Row, VectorDirection::Column) => { - // Row by column -- will produce a scalar - self.values - .iter() - .zip(v2.values.iter()) - .try_fold(Entry::Number(Number::ZERO), |acc, (n1, n2)| { - acc.add(&n1.mul(&Entry::Number(*n2))?) - }) - } - (VectorDirection::Column, VectorDirection::Row) => { - // TODO: Do we need to clone? - Matrix::from(&[Entry::Vector(self.clone())])? - .mul(&Matrix::from(&[arg.clone()])?) - } - (VectorDirection::Row, VectorDirection::Row) - | (VectorDirection::Column, VectorDirection::Column) => { - Err(CalculatorError::DimensionMismatch) - } - } - } - Entry::Number(number) => self.iterated_binary_num(number, Number::mul), - } - } - fn div(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::div), - Entry::Number(number) => self.iterated_binary_num(number, Number::div), - } - } - fn int_divide(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::int_divide), - Entry::Number(number) => self.iterated_binary_num(number, Number::int_divide), - } - } - fn modulo(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::modulo), - Entry::Number(number) => self.iterated_binary_num(number, Number::modulo), - } - } - fn pow(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), - Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::pow), - Entry::Number(number) => self.iterated_binary_num(number, Number::pow), - } - } -} - -impl Vector { - pub fn from(entries: &[Entry]) -> CalculatorResult { - if entries.is_empty() { - return Err(CalculatorError::NotEnoughStackEntries); - } - Self { - values: entries - .iter() - .map(|e| match e { - Entry::Matrix(_) | Entry::Vector(_) => Err(CalculatorError::TypeMismatch), - Entry::Number(number) => Ok(*number), - }) - .collect::>>()?, - direction: VectorDirection::default(), - } - .validate() - } - - fn iterated_unary( - &self, - op: impl Fn(&Number) -> CalculatorResult, - ) -> CalculatorResult { - Ok(Entry::Vector(Self { - values: self - .values - .iter() - .map(|n| op(n)) - .map(|e| match e { - // Only numbers are valid in a vector - Ok(Entry::Number(number)) => Ok(number), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - direction: self.direction, - })) - } - - fn iterated_binary_vec( - &self, - v2: &Self, - op: impl Fn(&Number, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - if self.values.len() != v2.values.len() { - return Err(CalculatorError::DimensionMismatch); - } - if self.direction != v2.direction { - return Err(CalculatorError::DimensionMismatch); - } - Ok(Entry::Vector(Self { - values: self - .values - .iter() - .zip(v2.values.iter()) - .map(|(n1, n2)| op(n1, &Entry::Number(*n2))) - .map(|e| match e { - // Only numbers are valid in a vector - Ok(Entry::Number(number)) => Ok(number), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - direction: self.direction, - })) - } - fn iterated_binary_num( - &self, - n2: &Number, - op: impl Fn(&Number, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - Ok(Entry::Vector(Self { - values: self - .values - .iter() - .map(|n| op(n, &Entry::Number(*n2))) - .map(|e| match e { - // Only numbers are valid in a vector - Ok(Entry::Number(number)) => Ok(number), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - direction: self.direction, - })) - } -} - -impl CalculatorEntry for Number { - fn to_editable_string(&self) -> CalculatorResult { - Ok(format!("{}", self.value)) - } - fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { - match display_mode { - CalculatorDisplayMode::Default => format!("{}", self.value), - CalculatorDisplayMode::Separated { separator } => separated(self.value, *separator), - CalculatorDisplayMode::Scientific { precision } => scientific(self.value, *precision), - CalculatorDisplayMode::Engineering { precision } => engineering(self.value, *precision), - CalculatorDisplayMode::Fixed { precision } => { - format!("{:0>.precision$}", self.value, precision = precision) - } - } - } - fn is_valid(&self) -> bool { - !self.value.is_nan() && !self.value.is_infinite() - } - fn validate(self) -> CalculatorResult { - if self.is_valid() { - Ok(Entry::Number(self)) - } else { - Err(CalculatorError::ArithmeticError) - } - } - - fn negate(&self) -> CalculatorResult { - Ok(Entry::Number(Self { value: -self.value })) - } - fn abs(&self) -> CalculatorResult { - Ok(Entry::Number(Self { - value: self.value.abs(), - })) - } - fn inverse(&self) -> CalculatorResult { - Self { - value: self.value.recip(), - } - .validate() - } - fn transpose(&self) -> CalculatorResult { - Err(CalculatorError::TypeMismatch) - } - fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.to_radians().sin(), - CalculatorAngleMode::Radians => self.value.sin(), - CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).sin(), - }, - })) - } - fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.to_radians().cos(), - CalculatorAngleMode::Radians => self.value.cos(), - CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).cos(), - }, - })) - } - fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.to_radians().tan(), - CalculatorAngleMode::Radians => self.value.tan(), - CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).tan(), - }, - })) - } - fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.asin().to_degrees(), - CalculatorAngleMode::Radians => self.value.asin(), - CalculatorAngleMode::Grads => self.value.asin() * 200.0 / std::f64::consts::PI, - }, - })) - } - fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.acos().to_degrees(), - CalculatorAngleMode::Radians => self.value.acos(), - CalculatorAngleMode::Grads => self.value.acos() * 200.0 / std::f64::consts::PI, - }, - })) - } - fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { - Ok(Entry::Number(Self { - value: match angle_mode { - CalculatorAngleMode::Degrees => self.value.atan().to_degrees(), - CalculatorAngleMode::Radians => self.value.atan(), - CalculatorAngleMode::Grads => self.value.atan() * 200.0 / std::f64::consts::PI, - }, - })) - } - fn sqrt(&self) -> CalculatorResult { - Ok(Entry::Number(Self { - value: self.value.sqrt(), - })) - } - fn log(&self) -> CalculatorResult { - Ok(Entry::Number(Self { - value: self.value.log10(), - })) - } - fn ln(&self) -> CalculatorResult { - Ok(Entry::Number(Self { - value: self.value.ln(), - })) - } - - fn add(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::add), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::add), - Entry::Number(number) => Self { - value: self.value + number.value, - } - .validate(), - } - } - fn sub(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::sub), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::sub), - Entry::Number(number) => Self { - value: self.value - number.value, - } - .validate(), - } - } - fn mul(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::mul), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::mul), - Entry::Number(number) => Self { - value: self.value * number.value, - } - .validate(), - } - } - fn div(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::div), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::div), - Entry::Number(number) => Self { - value: self.value / number.value, - } - .validate(), - } - } - fn int_divide(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide), - Entry::Number(number) => Self { - value: self.value.div_euclid(number.value), - } - .validate(), - } - } - fn modulo(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::modulo), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::modulo), - Entry::Number(number) => Self { - value: self.value % number.value, - } - .validate(), - } - } - fn pow(&self, arg: &Entry) -> CalculatorResult { - match arg { - Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow), - Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow), - Entry::Number(number) => Self { - value: self.value.powf(number.value), - } - .validate(), - } - } -} - -impl Number { - pub const ZERO: Self = Self { value: 0.0_f64 }; - - fn iterated_binary_vec( - self, - vector: &Vector, - op: impl Fn(&Self, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - Ok(Entry::Vector(Vector { - values: vector - .values - .iter() - .map(|n| op(&self, &Entry::Number(*n))) - .map(|e| match e { - // Only numbers are valid in a vector - Ok(Entry::Number(number)) => Ok(number), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - direction: vector.direction, - })) - } - - fn iterated_binary_mat( - self, - matrix: &Matrix, - op: impl Fn(&Self, &Entry) -> CalculatorResult, - ) -> CalculatorResult { - Matrix { - vectors: matrix - .vectors - .iter() - .map(|v| op(&self, &Entry::Vector(v.clone()))) - .map(|e| match e { - // Only numbers are valid in a vector - Ok(Entry::Vector(vector)) => Ok(vector), - _ => Err(CalculatorError::ArithmeticError), - }) - .collect::>>()?, - dimensions: matrix.dimensions.clone(), - } - .validate() - } -} - impl fmt::Display for Entry { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -1109,109 +254,243 @@ impl fmt::Display for Entry { } } -impl fmt::Display for Matrix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "[ {} ]", - self.vectors - .iter() - .map(|vector| format!("{}", vector)) - .collect::>() - .join("; ") - ) +#[cfg(test)] +mod tests { + use super::*; + + fn valid_square_matrix() -> Entry { + Entry::Matrix(Matrix { + dimensions: MatrixDimensions { + columns: 3, + rows: 3, + }, + vectors: vec![ + Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { value: 2.0_f64 }, + Number { value: -3.0_f64 }, + ], + }, + Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 4.0_f64 }, + Number { value: -5.0_f64 }, + Number { value: 0.0_f64 }, + ], + }, + Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: -7.0_f64 }, + Number { value: 8.0_f64 }, + Number { value: 9.0_f64 }, + ], + }, + ], + }) + } + + #[test] + fn valid_cascading_operations() { + for (op, entry, goal) in &[ + ( + "negate", + valid_square_matrix().negate(), + Matrix::from(&[ + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: -1.0_f64 }, + Number { value: -2.0_f64 }, + Number { value: 3.0_f64 }, + ], + }), + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: -4.0_f64 }, + Number { value: 5.0_f64 }, + Number { value: -0.0_f64 }, + ], + }), + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 7.0_f64 }, + Number { value: -8.0_f64 }, + Number { value: -9.0_f64 }, + ], + }), + ]), + ), + // ( + // "abs", + // valid_square_matrix().abs(), + // Matrix::from(&[ + // Entry::Vector(Vector { + // direction: VectorDirection::Column, + // values: vec![ + // Number { value: 1.0_f64 }, + // Number { value: 2.0_f64 }, + // Number { value: 3.0_f64 }, + // ], + // }), + // Entry::Vector(Vector { + // direction: VectorDirection::Column, + // values: vec![ + // Number { value: 4.0_f64 }, + // Number { value: 5.0_f64 }, + // Number { value: 0.0_f64 }, + // ], + // }), + // Entry::Vector(Vector { + // direction: VectorDirection::Column, + // values: vec![ + // Number { value: 7.0_f64 }, + // Number { value: 8.0_f64 }, + // Number { value: 9.0_f64 }, + // ], + // }), + // ]), + // ), + // (valid_square_matrix().inverse(), Matrix::from( + // Entry::Vector(Vector { + // direction: VectorDirection::Column, + // values: vec![ + // Number{value: 1.0_f64}, + // Number{value: 2.0_f64}, + // Number{value: -3.0_f64}, + // ]}), + // Entry::Vector (Vector{ + // direction: VectorDirection::Column, + // values: vec![ + // Number{value: 4.0_f64}, + // Number{value: -5.0_f64}, + // Number{value: 0.0_f64}, + // ]}), + // Entry::Vector (Vector{ + // direction: VectorDirection::Column, + // values: vec![ + // Number{value: -7.0_f64}, + // Number{value: 8.0_f64}, + // Number{value: 9.0_f64}, + // ])), + ( + "transpose", + valid_square_matrix().transpose(), + Matrix::from(&[ + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { value: 4.0_f64 }, + Number { value: -7.0_f64 }, + ], + }), + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 2.0_f64 }, + Number { value: -5.0_f64 }, + Number { value: 8.0_f64 }, + ], + }), + Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: -3.0_f64 }, + Number { value: 0.0_f64 }, + Number { value: 9.0_f64 }, + ], + }), + ]), + ), + ( + "sqrt", + (Matrix { + dimensions: MatrixDimensions { + columns: 1, + rows: 3, + }, + vectors: vec![Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { value: 100.0_f64 }, + Number { value: 64.0_f64 }, + ], + }], + }) + .sqrt(), + Matrix::from(&[Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { value: 10.0_f64 }, + Number { value: 8.0_f64 }, + ], + })]), + ), + // NEWS + ( + "log", + (Matrix { + dimensions: MatrixDimensions { + columns: 1, + rows: 3, + }, + vectors: vec![Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { value: 100.0_f64 }, + Number { + value: 100_000.0_f64, + }, + ], + }], + }) + .log(), + Matrix::from(&[Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 0.0_f64 }, + Number { value: 2.0_f64 }, + Number { value: 5.0_f64 }, + ], + })]), + ), + ( + "ln", + (Matrix { + dimensions: MatrixDimensions { + columns: 1, + rows: 3, + }, + vectors: vec![Vector { + direction: VectorDirection::Column, + values: vec![ + Number { value: 1.0_f64 }, + Number { + value: std::f64::consts::E, + }, + ], + }], + }) + .ln(), + Matrix::from(&[Entry::Vector(Vector { + direction: VectorDirection::Column, + values: vec![Number { value: 0.0_f64 }, Number { value: 1.0_f64 }], + })]), + ), + ] { + println!("Testing: {}", op); + let left = entry.as_ref().expect("Operation failed"); + let right = goal.as_ref().expect("Invalid test goal construction"); + assert_eq!(left, right); + } } } - -impl fmt::Display for Vector { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "[{}]", - self.values - .iter() - .map(|number| format!("{}", number)) - .collect::>() - .join("; ") - ) - } -} - -impl fmt::Display for Number { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.value) - } -} - -// 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 Entry 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 -} diff --git a/src/calc/entries/matrix.rs b/src/calc/entries/matrix.rs new file mode 100644 index 0000000..c001e25 --- /dev/null +++ b/src/calc/entries/matrix.rs @@ -0,0 +1,353 @@ +use super::VectorDirection; +use super::{Entry, Number, Vector}; +use crate::calc::errors::{CalculatorError, CalculatorResult}; +use crate::calc::types::CalculatorAngleMode; +use crate::calc::CalculatorDisplayMode; +use crate::CalculatorEntry; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct MatrixDimensions { + pub rows: usize, + pub columns: usize, +} + +impl MatrixDimensions { + pub const fn transpose(&self) -> Self { + Self { + rows: self.columns, + columns: self.rows, + } + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Matrix { + pub vectors: Vec, + pub dimensions: MatrixDimensions, +} +impl Matrix { + pub fn from(entries: &[Entry]) -> CalculatorResult { + if entries.is_empty() { + return Err(CalculatorError::NotEnoughStackEntries); + } + + let vectors = entries + .iter() + .map(|e| match e { + Entry::Matrix(_) | Entry::Number(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(vector) => Ok(vector.clone()), + }) + .collect::>>()?; + + // Get the num_rows and dimension of the matrix + let first_vector = vectors + .get(0) + .ok_or(CalculatorError::NotEnoughStackEntries)?; + + // The number of rows in this column-based matrix + let num_rows = first_vector.values.len(); + // The direction all vectors must face + let vector_direction = first_vector.direction; + + // Either the dimension lengths mismatch, or the vectors are facing different directions (and are longer than 1, since a 1-length vector orientation does not matter + if vectors.iter().any(|v| v.values.len() != num_rows) + || (num_rows > 1 && vectors.iter().any(|v| v.direction != vector_direction)) + { + return Err(CalculatorError::DimensionMismatch); + } + + let dimensions = MatrixDimensions { + rows: num_rows, + columns: vectors.len(), + }; + + let ret = Self { + vectors, + dimensions, + }; + + // If the user tried making a matrix out of row vectors, we need to transpose it, which forces column vectors + if vector_direction == VectorDirection::Row && num_rows > 1 { + ret.transpose() + } else { + ret.validate() + } + } + + fn iterated_unary( + &self, + op: impl Fn(&Vector) -> CalculatorResult, + ) -> CalculatorResult { + Self { + vectors: self + .vectors + .iter() + .map(|v| op(v)) + .map(|e| match e { + Ok(Entry::Vector(vector)) => Ok(vector), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + dimensions: self.dimensions.clone(), + } + .validate() + } + + fn iterated_binary_num( + &self, + number: &Number, + op: impl Fn(&Vector, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + Self { + vectors: self + .vectors + .iter() + .map(|v| op(v, &Entry::Number(*number))) + .map(|e| match e { + Ok(Entry::Vector(vector)) => Ok(vector), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + dimensions: self.dimensions.clone(), + } + .validate() + } + + fn iterated_binary_mat( + &self, + m2: &Self, + op: impl Fn(&Vector, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + if self.dimensions != m2.dimensions { + return Err(CalculatorError::DimensionMismatch); + } + Self { + vectors: self + .vectors + .iter() + .zip(m2.vectors.iter()) + .map(|(v1, v2)| op(v1, &Entry::Vector(v2.clone()))) + .map(|e| match e { + Ok(Entry::Vector(vector)) => Ok(vector), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + dimensions: self.dimensions.clone(), + } + .validate() + } +} + +impl CalculatorEntry for Matrix { + fn to_editable_string(&self) -> CalculatorResult { + // TODO: Eventualy we can parse and edit a matrix as a string + Err(CalculatorError::TypeMismatch) + } + fn is_valid(&self) -> bool { + // The the number of vectors is equal to the 0th dimension + self.vectors.len() == self.dimensions.columns + // The number of elements in all vectors are equal to the 1st dimension, and each is valid + && self + .vectors + .iter() + .all(|v| v.values.len() == self.dimensions.rows && v.is_valid()) + // The dimensions are not zero + && self.dimensions.rows > 0 && self.dimensions.columns > 0 + } + fn validate(self) -> CalculatorResult { + if self.is_valid() { + Ok(Entry::Matrix(self)) + } else { + Err(CalculatorError::ArithmeticError) + } + } + + fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { + format!( + "[ {} ]", + self.vectors + .iter() + .map(|vector| vector.format_entry(display_mode)) + .collect::>() + .join(" ") + ) + } + + // Mathematical operations + fn negate(&self) -> CalculatorResult { + self.iterated_unary(Vector::negate) + } + fn abs(&self) -> CalculatorResult { + // TODO: Compute determinant + Err(CalculatorError::NotYetImplemented) + } + fn inverse(&self) -> CalculatorResult { + // TODO: Inverse + Err(CalculatorError::NotYetImplemented) + } + fn transpose(&self) -> CalculatorResult { + // Iterate over all rows + let mut vectors: Vec = vec![]; + for r in 0..self.dimensions.rows { + vectors.push(Vector { + values: self + .vectors + .iter() + .map(|v| { + // For each row, get the r'th element to build a new vector + v.values + .get(r) + .map_or_else(|| Err(CalculatorError::DimensionMismatch), |n| Ok(*n)) + }) + .collect::>>()?, + direction: VectorDirection::Column, + }); + } + + Self { + vectors, + dimensions: self.dimensions.transpose(), + } + .validate() + } + fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.sin(angle_mode)) + } + fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.cos(angle_mode)) + } + fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.tan(angle_mode)) + } + fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.asin(angle_mode)) + } + fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.acos(angle_mode)) + } + fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|v| v.atan(angle_mode)) + } + fn sqrt(&self) -> CalculatorResult { + self.iterated_unary(Vector::sqrt) + } + fn log(&self) -> CalculatorResult { + self.iterated_unary(Vector::log) + } + fn ln(&self) -> CalculatorResult { + self.iterated_unary(Vector::ln) + } + + // Binary + fn add(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::add), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::add), + } + } + fn sub(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::sub), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::sub), + } + } + fn mul(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => { + if self.dimensions.columns != m2.dimensions.rows { + return Err(CalculatorError::DimensionMismatch); + } + let dimensions = MatrixDimensions { + rows: self.dimensions.rows, + columns: m2.dimensions.columns, + }; + + // A matrix is a list of column vectors, so transpose self and zip the columns + let transposed_self: Self = match self.transpose()? { + Entry::Matrix(t) => t, + _ => { + return Err(CalculatorError::InternalError(String::from( + "Matrix transpose produced wrong type", + ))) + } + }; + + let mut vectors: Vec = vec![]; + + for c in &m2.vectors { + let mut vector: Vector = Vector { + values: vec![], + direction: VectorDirection::Column, + }; + for r in &transposed_self.vectors { + if let Entry::Number(number) = + c.transpose()?.mul(&Entry::Vector(r.clone()))? + { + vector.values.push(number); + } else { + return Err(CalculatorError::InternalError(String::from( + "Vector multiplication did not produce a number", + ))); + } + } + vectors.push(vector); + } + + Self { + vectors, + dimensions, + } + .validate() + } + Entry::Vector(vector) => self.mul(&Self::from( + &[Entry::Vector(vector.clone())], // Treat a vector as a 1D matrix + )?), + Entry::Number(number) => self.iterated_binary_num(number, Vector::mul), + } + } + fn div(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::div), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::div), + } + } + fn int_divide(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::int_divide), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::int_divide), + } + } + fn modulo(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::modulo), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::modulo), + } + } + fn pow(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_m2) => Err(CalculatorError::TypeMismatch), + Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => self.iterated_binary_num(number, Vector::pow), + } + } +} +impl fmt::Display for Matrix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "[ {} ]", + self.vectors + .iter() + .map(|vector| format!("{}", vector)) + .collect::>() + .join("; ") + ) + } +} diff --git a/src/calc/entries/number.rs b/src/calc/entries/number.rs new file mode 100644 index 0000000..d396866 --- /dev/null +++ b/src/calc/entries/number.rs @@ -0,0 +1,339 @@ +use super::VectorDirection; +use super::{Entry, Matrix, Vector}; +use crate::calc::errors::{CalculatorError, CalculatorResult}; +use crate::calc::types::CalculatorAngleMode; +use crate::calc::CalculatorDisplayMode; +use crate::CalculatorEntry; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct Number { + pub value: f64, +} + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + if self.value.is_nan() && other.value.is_nan() + || self.value.is_infinite() && other.value.is_infinite() + { + true + } else if self.value.is_nan() + || self.value.is_infinite() + || other.value.is_infinite() + || other.value.is_nan() + { + false + } else { + (self.value - other.value).abs() < f64::EPSILON + } + } +} + +impl CalculatorEntry for Number { + fn to_editable_string(&self) -> CalculatorResult { + Ok(format!("{}", self.value)) + } + fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { + match display_mode { + CalculatorDisplayMode::Default => format!("{}", self.value), + CalculatorDisplayMode::Separated { separator } => separated(self.value, *separator), + CalculatorDisplayMode::Scientific { precision } => scientific(self.value, *precision), + CalculatorDisplayMode::Engineering { precision } => engineering(self.value, *precision), + CalculatorDisplayMode::Fixed { precision } => { + format!("{:0>.precision$}", self.value, precision = precision) + } + } + } + fn is_valid(&self) -> bool { + !self.value.is_nan() && !self.value.is_infinite() + } + fn validate(self) -> CalculatorResult { + if self.is_valid() { + Ok(Entry::Number(self)) + } else { + Err(CalculatorError::ArithmeticError) + } + } + + fn negate(&self) -> CalculatorResult { + Ok(Entry::Number(Self { value: -self.value })) + } + fn abs(&self) -> CalculatorResult { + Ok(Entry::Number(Self { + value: self.value.abs(), + })) + } + fn inverse(&self) -> CalculatorResult { + Self { + value: self.value.recip(), + } + .validate() + } + fn transpose(&self) -> CalculatorResult { + Err(CalculatorError::TypeMismatch) + } + fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.to_radians().sin(), + CalculatorAngleMode::Radians => self.value.sin(), + CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).sin(), + }, + })) + } + fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.to_radians().cos(), + CalculatorAngleMode::Radians => self.value.cos(), + CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).cos(), + }, + })) + } + fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.to_radians().tan(), + CalculatorAngleMode::Radians => self.value.tan(), + CalculatorAngleMode::Grads => (self.value * std::f64::consts::PI / 200.0).tan(), + }, + })) + } + fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.asin().to_degrees(), + CalculatorAngleMode::Radians => self.value.asin(), + CalculatorAngleMode::Grads => self.value.asin() * 200.0 / std::f64::consts::PI, + }, + })) + } + fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.acos().to_degrees(), + CalculatorAngleMode::Radians => self.value.acos(), + CalculatorAngleMode::Grads => self.value.acos() * 200.0 / std::f64::consts::PI, + }, + })) + } + fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + Ok(Entry::Number(Self { + value: match angle_mode { + CalculatorAngleMode::Degrees => self.value.atan().to_degrees(), + CalculatorAngleMode::Radians => self.value.atan(), + CalculatorAngleMode::Grads => self.value.atan() * 200.0 / std::f64::consts::PI, + }, + })) + } + fn sqrt(&self) -> CalculatorResult { + Ok(Entry::Number(Self { + value: self.value.sqrt(), + })) + } + fn log(&self) -> CalculatorResult { + Ok(Entry::Number(Self { + value: self.value.log10(), + })) + } + fn ln(&self) -> CalculatorResult { + Ok(Entry::Number(Self { + value: self.value.ln(), + })) + } + + fn add(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::add), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::add), + Entry::Number(number) => Self { + value: self.value + number.value, + } + .validate(), + } + } + fn sub(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::sub), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::sub), + Entry::Number(number) => Self { + value: self.value - number.value, + } + .validate(), + } + } + fn mul(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::mul), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::mul), + Entry::Number(number) => Self { + value: self.value * number.value, + } + .validate(), + } + } + fn div(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::div), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::div), + Entry::Number(number) => Self { + value: self.value / number.value, + } + .validate(), + } + } + fn int_divide(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide), + Entry::Number(number) => Self { + value: self.value.div_euclid(number.value), + } + .validate(), + } + } + fn modulo(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::modulo), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::modulo), + Entry::Number(number) => Self { + value: self.value % number.value, + } + .validate(), + } + } + fn pow(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow), + Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow), + Entry::Number(number) => Self { + value: self.value.powf(number.value), + } + .validate(), + } + } +} + +impl Number { + pub const ZERO: Self = Self { value: 0.0_f64 }; + + fn iterated_binary_vec( + self, + vector: &Vector, + op: impl Fn(&Self, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + Ok(Entry::Vector(Vector { + values: vector + .values + .iter() + .map(|n| op(&self, &Entry::Number(*n))) + .map(|e| match e { + // Only numbers are valid in a vector + Ok(Entry::Number(number)) => Ok(number), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + direction: vector.direction, + })) + } + + fn iterated_binary_mat( + self, + matrix: &Matrix, + op: impl Fn(&Self, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + Matrix { + vectors: matrix + .vectors + .iter() + .map(|v| op(&self, &Entry::Vector(v.clone()))) + .map(|e| match e { + // Only numbers are valid in a vector + Ok(Entry::Vector(vector)) => Ok(vector), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + dimensions: matrix.dimensions.clone(), + } + .validate() + } +} +impl fmt::Display for Number { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +// 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 Entry 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 +} diff --git a/src/calc/entries/vector.rs b/src/calc/entries/vector.rs new file mode 100644 index 0000000..1118a8f --- /dev/null +++ b/src/calc/entries/vector.rs @@ -0,0 +1,291 @@ +use super::{Entry, Matrix, Number}; +use crate::calc::errors::{CalculatorError, CalculatorResult}; +use crate::calc::types::CalculatorAngleMode; +use crate::calc::CalculatorDisplayMode; +use crate::CalculatorEntry; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +pub enum VectorDirection { + Row, + Column, +} + +impl VectorDirection { + pub const fn swap(&self) -> Self { + match self { + Self::Row => Self::Column, + Self::Column => Self::Row, + } + } + + pub const fn get_separator(&self) -> &str { + match self { + Self::Row => " ", + Self::Column => "; ", + } + } +} + +impl Default for VectorDirection { + fn default() -> Self { + // Column vectors are the default + Self::Column + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Vector { + pub values: Vec, + pub direction: VectorDirection, +} + +impl CalculatorEntry for Vector { + // Misc + fn to_editable_string(&self) -> CalculatorResult { + // TODO: Eventualy we can parse and edit a vector as a string + Err(CalculatorError::TypeMismatch) + } + fn is_valid(&self) -> bool { + self.values.iter().all(|number| number.is_valid()) + } + fn validate(self) -> CalculatorResult { + if self.is_valid() { + Ok(Entry::Vector(self)) + } else { + Err(CalculatorError::ArithmeticError) + } + } + + fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String { + format!( + "[{}]", + self.values + .iter() + .map(|number| number.format_entry(display_mode)) + .collect::>() + .join(self.direction.get_separator()) + ) + } + // Mathematical operations + fn negate(&self) -> CalculatorResult { + self.iterated_unary(Number::negate) + } + fn abs(&self) -> CalculatorResult { + let value: Entry = self + .values + .iter() + .try_fold(Entry::Number(Number::ZERO), |acc, n2| { + acc.add(&n2.pow(&Entry::Number(Number { value: 2.0_f64 }))?) + })?; + value.sqrt() + } + fn inverse(&self) -> CalculatorResult { + Err(CalculatorError::TypeMismatch) + } + fn transpose(&self) -> CalculatorResult { + Ok(Entry::Vector(Self { + values: self.values.clone(), + direction: self.direction.swap(), + })) + } + fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.sin(angle_mode)) + } + fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.cos(angle_mode)) + } + fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.tan(angle_mode)) + } + fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.asin(angle_mode)) + } + fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.acos(angle_mode)) + } + fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult { + self.iterated_unary(|n| n.atan(angle_mode)) + } + fn sqrt(&self) -> CalculatorResult { + self.iterated_unary(Number::sqrt) + } + fn log(&self) -> CalculatorResult { + self.iterated_unary(Number::log) + } + fn ln(&self) -> CalculatorResult { + self.iterated_unary(Number::ln) + } + + fn add(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::add), + Entry::Number(number) => self.iterated_binary_num(number, Number::add), + } + } + fn sub(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::sub), + Entry::Number(number) => self.iterated_binary_num(number, Number::sub), + } + } + fn mul(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_matrix) => Matrix::from(&[Entry::Vector(self.clone())])?.mul(arg), + Entry::Vector(v2) => { + if self.values.len() != v2.values.len() { + return Err(CalculatorError::DimensionMismatch); + } + match (self.direction, v2.direction) { + (VectorDirection::Row, VectorDirection::Column) => { + // Row by column -- will produce a scalar + self.values + .iter() + .zip(v2.values.iter()) + .try_fold(Entry::Number(Number::ZERO), |acc, (n1, n2)| { + acc.add(&n1.mul(&Entry::Number(*n2))?) + }) + } + (VectorDirection::Column, VectorDirection::Row) => { + // TODO: Do we need to clone? + Matrix::from(&[Entry::Vector(self.clone())])? + .mul(&Matrix::from(&[arg.clone()])?) + } + (VectorDirection::Row, VectorDirection::Row) + | (VectorDirection::Column, VectorDirection::Column) => { + Err(CalculatorError::DimensionMismatch) + } + } + } + Entry::Number(number) => self.iterated_binary_num(number, Number::mul), + } + } + fn div(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::div), + Entry::Number(number) => self.iterated_binary_num(number, Number::div), + } + } + fn int_divide(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::int_divide), + Entry::Number(number) => self.iterated_binary_num(number, Number::int_divide), + } + } + fn modulo(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::modulo), + Entry::Number(number) => self.iterated_binary_num(number, Number::modulo), + } + } + fn pow(&self, arg: &Entry) -> CalculatorResult { + match arg { + Entry::Matrix(_) => Err(CalculatorError::TypeMismatch), + Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::pow), + Entry::Number(number) => self.iterated_binary_num(number, Number::pow), + } + } +} + +impl Vector { + pub fn from(entries: &[Entry]) -> CalculatorResult { + if entries.is_empty() { + return Err(CalculatorError::NotEnoughStackEntries); + } + Self { + values: entries + .iter() + .map(|e| match e { + Entry::Matrix(_) | Entry::Vector(_) => Err(CalculatorError::TypeMismatch), + Entry::Number(number) => Ok(*number), + }) + .collect::>>()?, + direction: VectorDirection::default(), + } + .validate() + } + + fn iterated_unary( + &self, + op: impl Fn(&Number) -> CalculatorResult, + ) -> CalculatorResult { + Ok(Entry::Vector(Self { + values: self + .values + .iter() + .map(|n| op(n)) + .map(|e| match e { + // Only numbers are valid in a vector + Ok(Entry::Number(number)) => Ok(number), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + direction: self.direction, + })) + } + + fn iterated_binary_vec( + &self, + v2: &Self, + op: impl Fn(&Number, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + if self.values.len() != v2.values.len() { + return Err(CalculatorError::DimensionMismatch); + } + if self.direction != v2.direction { + return Err(CalculatorError::DimensionMismatch); + } + Ok(Entry::Vector(Self { + values: self + .values + .iter() + .zip(v2.values.iter()) + .map(|(n1, n2)| op(n1, &Entry::Number(*n2))) + .map(|e| match e { + // Only numbers are valid in a vector + Ok(Entry::Number(number)) => Ok(number), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + direction: self.direction, + })) + } + fn iterated_binary_num( + &self, + n2: &Number, + op: impl Fn(&Number, &Entry) -> CalculatorResult, + ) -> CalculatorResult { + Ok(Entry::Vector(Self { + values: self + .values + .iter() + .map(|n| op(n, &Entry::Number(*n2))) + .map(|e| match e { + // Only numbers are valid in a vector + Ok(Entry::Number(number)) => Ok(number), + _ => Err(CalculatorError::ArithmeticError), + }) + .collect::>>()?, + direction: self.direction, + })) + } +} +impl fmt::Display for Vector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "[{}]", + self.values + .iter() + .map(|number| format!("{}", number)) + .collect::>() + .join("; ") + ) + } +} diff --git a/src/main.rs b/src/main.rs index 3085d25..59e8949 100644 --- a/src/main.rs +++ b/src/main.rs @@ -470,7 +470,7 @@ struct ClippyRectangle<'a> { } impl ClippyRectangle<'_> { - // TODO: Make this static somehow + // Cannot be static since the clippy rectangle's text can change fn size(&self) -> Dimensions { let (width, height) = self.msg.lines().fold((0, 0), |(width, height), l| { (cmp::max(width, l.len()), height + 1)