diff --git a/Cargo.lock b/Cargo.lock index 41ea50d..15f6281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,9 +264,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -278,39 +278,37 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -319,11 +317,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -331,9 +328,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] diff --git a/Cargo.toml b/Cargo.toml index e4a7056..3e61956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ repository = "https://github.com/bytesized/utilities" [dependencies] clap = { version = "4.0.29", features = ["derive"] } crossterm = "0.25.0" -num = "0.4.0" +num = "0.4.2" rusqlite = { version = "0.28.0", features = ["bundled"] } diff --git a/README.md b/README.md index 7a11048..1754ffa 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A command line calculator In addition to allowing numbers to be arbitrarily large, bcalc stores non-integers via ratios rather than as floating point binary numbers. This means that precision isn't lost when binary floating point representations can't accurately represent a value. See [this Wikipedia article](https://en.wikipedia.org/wiki/Binary_number#Fractions) for more information on this problem. -Note that this approach can't really be used for irrational numbers. This isn't a problem for currently implemented features, but planned future features will be able to result in irrational numbers such as the square root of 2. The current plan is to use some sort of approximation in the case of irrational numbers. +Note that this approach can't really be used for irrational numbers. Operations that result in irrational numbers such as `sqrt 2` will use the configurable precision values to determine how many digits of precision to calculate. See `/help precision` for more details. ### Input History @@ -25,7 +25,7 @@ Variables can then be used in the place of numbers in later expressions. ### Multisession support -bcalc can remember the input and variable history from previous sessions. This feature currently won't work properly, however, unless the environment is set up properly. This set up is performed automatically when via the installer for my [utilities](https://github.com/bytesized/utilities). +bcalc can remember the input and variable history from previous sessions. This feature currently won't work properly, however, unless the environment is set up properly. This set up is performed automatically when installed via [my utilities](https://github.com/bytesized/utilities) installer. ### Commands @@ -36,6 +36,10 @@ bcalc has support for several commands which are invoked by beginning the calcul /help help ``` +### Consistent exit key + +Control+D exits on all operating system including when using `-a`. + ### Hotkeys bcalc supports several navigation hotkeys: @@ -47,5 +51,11 @@ bcalc supports several navigation hotkeys: This project is still a work in progress. A number of features are planned or do not yet work properly: - - Fractional exponents - - The square root operator `sqrt` + - Fix navigation hotkeys. They don't seem to be working right, at least on macOS. + - Allow argument configuration values to be saved. + - Enable more detailed errors that point at the location of the error in the input. + - Add a `/quit` command. + - Add logarithm support. + - Add common constants such as pi. + - Add trigonometric functions. + - Support for imaginary numbers. diff --git a/src/error.rs b/src/error.rs index a609f1f..1565d37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -170,7 +170,7 @@ pub enum MathExecutionError { UnknownVariable(String), DivisionByZero, FunctionNeedsArguments(FunctionNameToken), - Unimplemented, + ImaginaryResult, } impl fmt::Display for MathExecutionError { @@ -181,8 +181,8 @@ impl fmt::Display for MathExecutionError { MathExecutionError::FunctionNeedsArguments(function) => { write!(f, "{} has no arguments but requires them", function) } - MathExecutionError::Unimplemented => { - write!(f, "Encountered operation that is not yet supported") + MathExecutionError::ImaginaryResult => { + write!(f, "Unable to take the root of a negative number except unless the degree is an odd integer") } } } diff --git a/src/main.rs b/src/main.rs index 8a7fd1a..547eb95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -564,7 +564,7 @@ fn calculate( } let st = SyntaxTree::new(tokens.into())?; - let result = st.execute(maybe_input_history_id, maybe_vars, maybe_db)?; + let result = st.execute(maybe_input_history_id, maybe_vars, maybe_db, args)?; if args.fractional { Ok(result.to_string()) diff --git a/src/operations.rs b/src/operations.rs index 0a579c3..3686a51 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -1,4 +1,8 @@ -use num::{bigint::BigInt, rational::BigRational, Signed, Zero}; +use crate::error::MathExecutionError::{self, ImaginaryResult}; + +use num::{ + bigint::BigInt, pow::Pow, rational::BigRational, traits::Inv, BigUint, Integer, Signed, Zero, +}; /// `BigRational` only seems to support fractional string conversion, but we want to support decimal /// output as well. @@ -69,12 +73,154 @@ pub fn make_decimal_string( } } +pub fn exponentiate( + mut base: BigRational, + exponent: BigRational, + precision: u8, + radix: u8, +) -> Result { + // Step 1: If necessary, convert `b^-(n/d)` to `(1/b)^(n/d)`. + if exponent.is_negative() { + base = base.inv(); + } + + let (exp_num, exp_denom) = match exponent.into_raw() { + (num, denom) => ( + num.abs().to_biguint().unwrap(), + denom.abs().to_biguint().unwrap(), + ), + }; + + // Step 2: Convert `b^(n/d)` to `(b^n)^(1/d)` and compute `r = b^n` so we are left with + // `r^(1/d)`. + let radicand = base.pow(exp_num); + + // Step 3: Newton's Method + // Given `r` and `d`, we want to compute `r^(1/d)`. We will call the result `x`. + // So `r^(1/d) = x`, or `r = x^d`. + // Newton's method concerns finding when a function reaches 0. So we will make our function + // `f(x) = x^d - r`. + // Newton's method can be summarized as iterations of `x_n+1 = x_n - f(x_n)/f'(x_n)`. + // (Using `x` for `x_n` below, for brevity) + // `x_n+1 = x - (x^d - r)/(d * x^(d - 1))` + // `x_n+1 = x + (r - x^d)/(d * x^(d - 1))` + // `x_n+1 = (r - x^d + (x * d * x^(d - 1)))/(d * x^(d - 1))` + // `x_n+1 = (r - x^d + d*x^d)/(d * x^(d - 1)) + // `x_n+1 = (r + (d - 1)*x^d)/(d * x^(d - 1)) + + // Step 3.1: Rename, convert, and pre-calculate a few things. + let degree = exp_denom; + let one = BigUint::from(1u8); + let one_signed = BigInt::from(1); + let degree_ratio: BigRational = BigRational::from(BigInt::from(degree.clone())); + let degree_dec: BigUint = °ree - &one; + let degree_dec_ratio: BigRational = BigRational::from(BigInt::from(degree_dec.clone())); + // We are actually going to add one additional digit of precision. This prevents a rounding + // error from making our last guaranteed digit wrong. + let precision = BigUint::from(precision + 1); + let radix = BigInt::from(radix); + // The largest amount we are okay with being wrong by. + let max_error = BigRational::new(one_signed.clone(), radix.pow(precision).into()); + let f_magnitude = |x: &BigInt| -> BigRational { + (BigRational::from(x.clone()).pow(°ree) - &radicand).abs() + }; + let next_x = |x: BigRational| -> BigRational { + (&radicand + °ree_dec_ratio * x.clone().pow(°ree)) + / (°ree_ratio * x.pow(°ree_dec)) + }; + + // We are already done. + if degree == one { + return Ok(radicand); + } + + // Step 3.2: Input validation. This function currently cannot output complex numbers. + if radicand.is_negative() && degree.is_even() { + return Err(ImaginaryResult); + } + + // Step 3.3: Use a binary search to find a good starting point for Newton's method. Otherwise + // it takes forever. + let mut x = { + let (mut lower_bound, mut upper_bound) = if radicand.is_negative() { + (radicand.to_integer(), BigInt::from(0)) + } else { + (BigInt::from(0), radicand.to_integer()) + }; + + // We are going to unroll a bit to make the loop less confusing. + let mut guess: BigInt = (&upper_bound - &lower_bound) / 2 + &lower_bound; + let mut error = f_magnitude(&guess); + let (mut last_guess_was_lower, mut last_error) = { + let next_guess = &guess + 1; + let next_error = f_magnitude(&next_guess); + if next_error < error { + // We want to head towards the upper bound + lower_bound = next_guess; + (true, next_error) + } else { + // We want to head towards the lower bound + upper_bound = guess; + (false, error) + } + }; + + let mut span = &upper_bound - &lower_bound; + while span > one_signed { + guess = span / 2 + &lower_bound; + error = f_magnitude(&guess); + if last_guess_was_lower == (error < last_error) { + // Error is decreasing in the positive direction, we want to head towards the + // upper bound. + last_guess_was_lower = true; + lower_bound = guess; + } else { + // Error is decreasing in the negative direction. We want to head towards the lower + // bound. + last_guess_was_lower = false; + upper_bound = guess; + } + + last_error = error; + span = &upper_bound - &lower_bound; + } + + guess = if span.is_zero() || f_magnitude(&upper_bound) < f_magnitude(&lower_bound) { + upper_bound + } else { + lower_bound + }; + + let guess_error = f_magnitude(&guess); + let guess_ratio = BigRational::from(guess); + // Return early if it's an exact integer. + if guess_error.is_zero() { + return Ok(guess_ratio); + } + + guess_ratio + }; + + // Step 3.4: Newton's method + loop { + let prev_x = x.clone(); + x = next_x(x); + let error = (&x - prev_x).abs(); + if error <= max_error { + break; + } + } + + Ok(x) +} + #[cfg(test)] mod operation_tests { use crate::{ operations::make_decimal_string, syntax_tree::SyntaxTree, token::{ParsedInput, Tokenizer}, + Args, }; fn evaluate_to_string( @@ -85,13 +231,25 @@ mod operation_tests { commas: bool, upper: bool, ) -> String { + let args = Args { + radix: parse_radix, + input: None, + alternate_screen: false, + no_db: true, + convert_to_radix: Some(result_radix), + precision, + extra_precision: 0, + fractional: false, + commas, + upper, + }; let tokenizer = Tokenizer::new(); let tokens = match tokenizer.tokenize(input, parse_radix).unwrap() { ParsedInput::Tokens(t) => t, ParsedInput::Command((_, _)) => panic!(), }; let st = SyntaxTree::new(tokens.into()).unwrap(); - let result = st.execute(None, None, None).unwrap(); + let result = st.execute(None, None, None, &args).unwrap(); make_decimal_string(&result, result_radix, precision, commas, upper) } @@ -208,4 +366,82 @@ mod operation_tests { let result = evaluate_to_string("-1.1", 10, 10, 0, false, false); assert_eq!(result, "-1".to_string()); } + + #[test] + fn root_integer_result_1() { + let result = evaluate_to_string("100^(1/2)", 10, 10, 10, false, false); + assert_eq!(result, "10".to_string()); + } + + #[test] + fn root_integer_result_2() { + let result = evaluate_to_string("59049^(1/10)", 10, 10, 10, false, false); + assert_eq!(result, "3".to_string()); + } + + #[test] + fn exponentiate_integer_result_1() { + let result = evaluate_to_string("9^10", 10, 10, 10, false, false); + assert_eq!(result, "3486784401".to_string()); + } + + #[test] + fn exponentiate_integer_result_2() { + let result = evaluate_to_string("-3^9", 10, 10, 10, false, false); + assert_eq!(result, "-19683".to_string()); + } + + #[test] + fn exponentiate_integer_result_3() { + let result = evaluate_to_string("-3^10", 10, 10, 10, false, false); + assert_eq!(result, "59049".to_string()); + } + + #[test] + fn exponentiate_fractional_result_1() { + let result = evaluate_to_string("2^(1/2)", 10, 10, 10, false, false); + assert_eq!(result, "1.4142135624".to_string()); + } + + #[test] + fn exponentiate_fractional_result_2() { + let result = evaluate_to_string("-2^(1/3)", 10, 10, 10, false, false); + assert_eq!(result, "-1.2599210499".to_string()); + } + + #[test] + fn exponentiate_fractional_result_3() { + let result = evaluate_to_string("-100^(7/3)", 10, 10, 10, false, false); + assert_eq!(result, "-46415.8883361278".to_string()); + } + + #[test] + fn exponentiate_by_zero() { + let result = evaluate_to_string("-100^0", 10, 10, 10, false, false); + assert_eq!(result, "1".to_string()); + } + + #[test] + fn exponentiate_zero() { + let result = evaluate_to_string("0^2", 10, 10, 10, false, false); + assert_eq!(result, "0".to_string()); + } + + #[test] + fn exponentiate_zero_by_zero() { + let result = evaluate_to_string("0^0", 10, 10, 10, false, false); + assert_eq!(result, "1".to_string()); + } + + #[test] + fn exponentiate_by_negative() { + let result = evaluate_to_string("10^-2", 10, 10, 10, false, false); + assert_eq!(result, "0.01".to_string()); + } + + #[test] + fn exponentiate_one() { + let result = evaluate_to_string("1^(999/998)", 10, 10, 10, false, false); + assert_eq!(result, "1".to_string()); + } } diff --git a/src/syntax_tree.rs b/src/syntax_tree.rs index 0bb3a40..8153819 100644 --- a/src/syntax_tree.rs +++ b/src/syntax_tree.rs @@ -1,9 +1,7 @@ use crate::{ error::{ CalculatorFailure, - MathExecutionError::{ - DivisionByZero, FunctionNeedsArguments, Unimplemented, UnknownVariable, - }, + MathExecutionError::{DivisionByZero, FunctionNeedsArguments, UnknownVariable}, MissingCapabilityError::NoVariableStore, SyntaxError::{ self, CommaWithoutOperandAfter, CommaWithoutOperandBefore, EmptyParens, @@ -11,14 +9,20 @@ use crate::{ MissingOperand, MissingOperator, NoInput, UnexpectedToken, }, }, + operations::exponentiate, position::{Position, Positioned}, saved_data::SavedData, token::{ BinaryOperatorToken, FunctionNameToken, Token, UnaryOperatorToken, ORDERED_BINARY_OPERATORS, }, variable::{Variable, VariableStore}, + Args, +}; +use num::{ + bigint::{BigInt, ToBigInt}, + rational::BigRational, + Signed, }; -use num::{bigint::BigInt, pow::Pow, rational::BigRational, Signed}; use std::{ cmp::{max, min}, collections::VecDeque, @@ -30,6 +34,7 @@ trait OperationNode { self: Box, maybe_vars: Option<&mut VariableStore>, maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result; fn position(&self) -> Position; @@ -46,6 +51,7 @@ impl OperationNode for NumericNode { self: Box, _maybe_vars: Option<&mut VariableStore>, _maybe_db: Option<&mut SavedData>, + _args: &Args, ) -> Result { Ok(self.value) } @@ -66,6 +72,7 @@ impl OperationNode for VariableNode { self: Box, maybe_vars: Option<&mut VariableStore>, maybe_db: Option<&mut SavedData>, + _args: &Args, ) -> Result { let vars = match maybe_vars { Some(v) => v, @@ -94,14 +101,20 @@ impl OperationNode for UnaryNode { self: Box, mut maybe_vars: Option<&mut VariableStore>, mut maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { - let operand = self - .operand - .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut())?; + let operand = + self.operand + .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut(), args)?; match self.operator { UnaryOperatorToken::SquareRoot => { - // TODO: Implement - return Err(Positioned::new(Unimplemented, self.operator_position).into()); + let total_precision = args.precision + args.extra_precision; + let one_half = BigRational::new( + ToBigInt::to_bigint(&1).unwrap(), + ToBigInt::to_bigint(&2).unwrap(), + ); + exponentiate(operand, one_half, total_precision, args.radix) + .map_err(|e| Positioned::new(e, self.operator_position.clone()).into()) } UnaryOperatorToken::Negate => Ok(-operand), UnaryOperatorToken::AbsoluteValue => Ok(operand.abs()), @@ -126,13 +139,14 @@ impl OperationNode for BinaryNode { self: Box, mut maybe_vars: Option<&mut VariableStore>, mut maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { - let operand_1 = self - .operand_1 - .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut())?; - let operand_2 = self - .operand_2 - .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut())?; + let operand_1 = + self.operand_1 + .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut(), args)?; + let operand_2 = + self.operand_2 + .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut(), args)?; match self.operator { BinaryOperatorToken::Add => Ok(operand_1 + operand_2), BinaryOperatorToken::Subtract => Ok(operand_1 - operand_2), @@ -145,11 +159,9 @@ impl OperationNode for BinaryNode { } BinaryOperatorToken::Modulus => Ok(operand_1 % operand_2), BinaryOperatorToken::Exponent => { - if operand_2.is_integer() { - return Ok(Pow::pow(operand_1, operand_2.numer())); - } - // TODO: Implement - return Err(Positioned::new(Unimplemented, self.operator_position).into()); + let total_precision = args.precision + args.extra_precision; + exponentiate(operand_1, operand_2, total_precision, args.radix) + .map_err(|e| Positioned::new(e, self.operator_position.clone()).into()) } } } @@ -175,10 +187,15 @@ impl OperationNode for FunctionNode { self: Box, mut maybe_vars: Option<&mut VariableStore>, mut maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { let mut operands: Vec = Vec::new(); for operand in self.operands { - operands.push(operand.execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut())?); + operands.push(operand.execute( + maybe_vars.as_deref_mut(), + maybe_db.as_deref_mut(), + args, + )?); } match self.function_name { FunctionNameToken::Max => { @@ -232,8 +249,9 @@ impl OperationNode for ParenthesizedNode { self: Box, maybe_vars: Option<&mut VariableStore>, maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { - self.node.execute(maybe_vars, maybe_db) + self.node.execute(maybe_vars, maybe_db, args) } fn position(&self) -> Position { @@ -278,8 +296,10 @@ impl SyntaxTreeNode { self, maybe_vars: Option<&mut VariableStore>, maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { - self.into_operation_node().execute(maybe_vars, maybe_db) + self.into_operation_node() + .execute(maybe_vars, maybe_db, args) } fn position(&self) -> Position { @@ -765,10 +785,11 @@ impl SyntaxTree { maybe_input_history_id: Option, mut maybe_vars: Option<&mut VariableStore>, mut maybe_db: Option<&mut SavedData>, + args: &Args, ) -> Result { let result = self .root - .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut())?; + .execute(maybe_vars.as_deref_mut(), maybe_db.as_deref_mut(), args)?; if let Some(result_var) = self.maybe_result_var { let var = Variable { name: result_var.value,