From c2577ef4777864b770ba1d7a14ee4c07d773980e Mon Sep 17 00:00:00 2001 From: bakk Date: Tue, 18 May 2021 19:48:13 +0200 Subject: [PATCH] Estimation/rounding for final results --- README.md | 49 +++++----- kalk/src/calculus.rs | 44 ++------- kalk/src/kalk_num/mod.rs | 198 +++++++++++++++++++++++++++++++++++++++ kalk_cli/src/output.rs | 11 ++- 4 files changed, 244 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 9d6eb61..70f451e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ # kalk -[![Crates.io](https://img.shields.io/crates/v/kalk_cli)](https://crates.io/crates/kalk_cli) -![npm](https://img.shields.io/npm/v/@paddim8/kalk) -[![GitHub](https://img.shields.io/github/license/PaddiM8/kalk)](https://github.com/PaddiM8/kalk/blob/master/LICENSE) -[![Docs.rs](https://docs.rs/kalk/badge.svg)](https://docs.rs/kalk/latest/kalk/) -![Build status](https://img.shields.io/github/workflow/status/PaddiM8/kalk/Rust?event=push&label=build%20%26%20test) + [![Crates.io](https://img.shields.io/crates/v/kalk_cli)](https://crates.io/crates/kalk_cli)![npm](https://img.shields.io/npm/v/@paddim8/kalk) [![GitHub](https://img.shields.io/github/license/PaddiM8/kalk)](https://github.com/PaddiM8/kalk/blob/master/LICENSE) [![Docs.rs](https://docs.rs/kalk/badge.svg)](https://docs.rs/kalk/latest/kalk/) ![Build status](https://img.shields.io/github/workflow/status/PaddiM8/kalk/Rust?event=push&label=build%20%26%20test) -Kalk is a calculator (both program and library) that supports user-defined variables, functions, and units (experimental, limited). It runs on Windows, macOS, Linux, Android, and in web browsers (with WebAssembly). +Kalk is a calculator (both program and library) that supports user-defined variables, functions, and units (experimental, limited). It runs on Windows, macOS, Linux, Android, and in web browsers (with WebAssembly). [Kanban](https://kolan.strct.net/Board/4RAdMjLDz) | [Website](https://kalk.strct.net) ![](example.png) ## Features + * Operators: +, -, \*, /, ! * Groups: (), ⌈⌉, ⌋⌊ * [Pre-defined functions and constants](https://github.com/PaddiM8/kalk/blob/master/kalk/src/prelude.rs) @@ -28,48 +25,58 @@ Kalk is a calculator (both program and library) that supports user-defined varia ## Libraries There are currently three different libraries related to kalk. + * [kalk](https://crates.io/crates/kalk): The Rust crate that powers it all. * [@paddim8/kalk](https://www.npmjs.com/package/@paddim8/kalk): JavaScript bindings to `kalk`. This lets you use it in the browser, thanks to WebAssembly. * [@paddim8/kalk-component](https://www.npmjs.com/package/@paddim8/kalk-component): A web component that acts as a frontend to `@paddim8/kalk`, which lets you use kalk in the browser with a command line-like interface. ## Installation + ### Binaries + Pre-compiled binaries for Linux, Windows, and macOS (64-bit) are available in the [releases page](https://github.com/PaddiM8/kalk/releases). + ### Compiling + **Minimum rust version: v1.36.0**. Make sure you have `diffutils` `gcc` `make` and `m4` installed. **If you use windows:** [follow the instructions here](https://docs.rs/gmp-mpfr-sys/1.2.3/gmp_mpfr_sys/index.html#building-on-windows) (don't forget to install `mingw-w64-x86_64-rust` in MSYS2). #### Cargo + Run `cargo install kalk_cli` #### Manually + 1. Go into the `kalk_cli` directory. 2. Run `cargo build --release` 3. Grab the binary from `targets/release` ## Syntax + A more complete reference can be found on [the website](https://kalk.strct.net) ### Functions -__Defining:__ name(parameter1, parameter2, ...) = expression -**Example:** `f(x) = 2x+3` -__Using:__ name(argument1, argument2) -**Example:** `f(2)` +**Defining:** name(parameter1, parameter2, ...) = expression\ +**Example:** `f(x) = 2x+3` + +**Using:** name(argument1, argument2)\ +**Example:** `f(2)` ### Variables -__Defining:__ name = expression -**Example:** `x = 3` -__Using:__ name -**Example:** `x` +**Defining:** name = expression\ +**Example:** `x = 3` + +**Using:** name\ +**Example:** `x` ### Units (experimental, are likely to not work properly) -*Note: You only need to define the relationship between two units once. You will be able to convert between both of them.* -__Defining:__ `unit` name = expression -**Example:** `unit deg = (rad*180)/π` -__Using:__ Use them freely in expressions. -**Example:** `2m/50cm` +*Note: You only need to define the relationship between two units once. You will be able to convert between both of them.* **Defining:** `unit` name = expression\ +**Example:** `unit deg = (rad*180)/π` -__Converting:__ expression `to` unit -**Example:** `2 m to cm` +**Using:** Use them freely in expressions.\ +**Example:** `2m/50cm` + +**Converting:** expression `to` unit\ +**Example:** `2 m to cm` \ No newline at end of file diff --git a/kalk/src/calculus.rs b/kalk/src/calculus.rs index 64271f7..a37a406 100644 --- a/kalk/src/calculus.rs +++ b/kalk/src/calculus.rs @@ -22,9 +22,10 @@ pub fn derive_func( let f_x = interpreter::eval_fn_call_expr(context, &new_identifier, &[argument_without_h], unit)?; - Ok(round( - f_x_h.sub(context, f_x).div(context, (2f64 * H).into()), - )) + Ok(f_x_h + .sub(context, f_x) + .div(context, (2f64 * H).into()) + .round_if_needed()) } pub fn integrate( @@ -55,13 +56,7 @@ pub fn integrate( Box::new(Expr::Literal(1f64)), )); - Ok(round(simpsons_rule( - context, - a, - b, - expr, - integration_variable.unwrap(), - )?)) + Ok(simpsons_rule(context, a, b, expr, integration_variable.unwrap())?.round_if_needed()) } /// Composite Simpson's 3/8 rule @@ -75,8 +70,8 @@ fn simpsons_rule( let mut result = KalkNum::default(); const N: i32 = 900; - let a = interpreter::eval_expr(context, a_expr, "")?.value.to_f64(); - let b = interpreter::eval_expr(context, b_expr, "")?.value.to_f64(); + let a = interpreter::eval_expr(context, a_expr, "")?.to_f64(); + let b = interpreter::eval_expr(context, b_expr, "")?.to_f64(); let h = (b - a) / N as f64; for i in 0..=N { context.symbol_table.set(Stmt::VarDecl( @@ -91,33 +86,10 @@ fn simpsons_rule( }; // factor * f(x_n) - result.value += factor * interpreter::eval_expr(context, expr, "")?.value; + result.value += factor as f64 * interpreter::eval_expr(context, expr, "")?.value; } result.value *= (3f64 * h) / 8f64; Ok(result) } - -/// Basic up/down rounding from 0.00xxx or 0.999xxx or xx.000xxx, etc. -fn round(num: KalkNum) -> KalkNum { - let fract = num.value.clone().fract(); - let floored = num.value.clone().floor(); - - // If it's zero something, don't do the rounding as aggressively. - let (limit_floor, limit_ceil) = if floored.clone() == 0 { - (-15, -5) - } else { - (-4, -6) - }; - - if fract.clone().log10() < limit_floor { - // If eg. 0.00xxx - return KalkNum::new(floored, &num.unit); - } else if (1f64 - fract).log10() < limit_ceil { - // If eg. 0.999 - return KalkNum::new(num.value.clone().ceil(), &num.unit); - } else { - return num; - } -} diff --git a/kalk/src/kalk_num/mod.rs b/kalk/src/kalk_num/mod.rs index 613e6ca..2049efd 100644 --- a/kalk/src/kalk_num/mod.rs +++ b/kalk/src/kalk_num/mod.rs @@ -7,3 +7,201 @@ pub use with_rug::*; pub mod regular; #[cfg(not(feature = "rug"))] pub use regular::*; + +use lazy_static::lazy_static; +use std::collections::HashMap; + +lazy_static! { + static ref CONSTANTS: HashMap<&'static str, &'static str> = { + let mut m = HashMap::new(); + m.insert("3.141592", "π"); + m.insert("2.718281", "e"); + m.insert("6.283185", "tau"); + m.insert("6.283185", "τ"); + m.insert("1.618033", "phi"); + m.insert("1.618033", "ϕ"); + m.insert("1.414213", "√2"); + // Radian values for common angles + m.insert("0.523598", "π/6"); + m.insert("0.785398", "π/4"); + m.insert("1.047197", "π/3"); + m.insert("1.570796", "π/2"); + m.insert("2.094395", "2π/3"); + m.insert("2.356194", "3π/4"); + m.insert("2.617993", "5π/6"); + m.insert("3.665191", "7π/6"); + m.insert("3.926990", "5π/4"); + m.insert("4.188790", "4π/3"); + m.insert("4.712388", "3π/2"); + m.insert("5.23598", "5π/3"); + m.insert("5.497787", "7π/4"); + m.insert("5.759586", "11π/6"); + m.insert("6.283185", "2π"); + m.insert("0.866025", "√3/2"); + m + }; +} + +impl KalkNum { + // Get an estimate of what the number is, eg. 3.141592 => π + pub fn estimate(&self) -> Option { + let fract = self.value.clone().fract().abs(); + let integer = self.value.clone().trunc(); + + // If it's an integer, there's nothing that would be done to it. + if fract == 0 { + return None; + } + + // Eg. 0.5 to 1/2 + let as_abs_string = self.to_string().trim_start_matches("-").to_string(); + let sign = if self.value < 0 { "-" } else { "" }; + let fract_as_string = fract.to_string(); + if as_abs_string.starts_with("0.5") { + if as_abs_string.len() == 3 || (as_abs_string.len() > 6 && &as_abs_string[3..5] == "00") + { + return Some(format!("{}1/2", sign)); + } + } + + // Eg. 1.33333333 to 1 + 1/3 + if fract_as_string.len() >= 5 { + let first_five_decimals = &fract_as_string[2..7]; + if first_five_decimals == "33333" || first_five_decimals == "66666" { + let fraction = match first_five_decimals.as_ref() { + "33333" => "1/3", + "66666" => "2/3", + _ => "?", + }; + + if integer == 0 { + return Some(format!("{}{}", sign, fraction)); + } else { + let explicit_sign = if sign == "" { "+" } else { "-" }; + return Some(format!( + "{} {} {}", + trim_zeroes(&integer.to_string()), + explicit_sign, + fraction + )); + } + } + } + + // Match with common numbers, eg. π, 2π/3, √2 + if as_abs_string.len() >= 8 { + if let Some(constant) = CONSTANTS.get(&as_abs_string[0..8]) { + return Some(format!("{}{}", sign, constant.to_string())); + } + } + + // If nothing above was relevant, simply round it off a bit, eg. from 0.99999 to 1 + let rounded = self.round()?.to_string(); + Some(trim_zeroes(&rounded)) + } + + /// Basic up/down rounding from 0.00xxx or 0.999xxx or xx.000xxx, etc. + pub fn round(&self) -> Option { + let sign = if self.value < 0 { -1 } else { 1 }; + let fract = self.value.clone().abs().fract(); + let integer = self.value.clone().abs().trunc(); + + // If it's zero something, don't do the rounding as aggressively. + let (limit_floor, limit_ceil) = if integer == 0f64 { + (-8f64, -5f64) + } else { + (-4f64, -6f64) + }; + + if fract.clone().log10() < limit_floor { + // If eg. 0.00xxx + Some(KalkNum::new(integer * sign, &self.unit)) + } else if (1f64 - fract.clone()).log10() < limit_ceil { + // If eg. 0.999 + // .abs() this before ceiling to make sure it rounds correctly. The sign is re-added afterwards. + Some(KalkNum::new( + self.value.clone().abs().ceil() * sign, + &self.unit, + )) + } else { + None + } + } + + pub fn round_if_needed(&self) -> KalkNum { + if let Some(value) = self.round() { + value + } else { + self.clone() // Hmm + } + } +} + +fn trim_zeroes(input: &str) -> String { + if input.contains(".") { + input + .trim_end_matches("0") + .trim_end_matches(".") + .to_string() + } else { + input.into() + } +} + +#[cfg(test)] +mod tests { + use crate::kalk_num::KalkNum; + + #[test] + fn test_estimate() { + let in_out = vec![ + (0.99999999, Some(String::from("1"))), + (-0.9999999, Some(String::from("-1"))), + (0.0000000001, Some(String::from("0"))), + (-0.000000001, Some(String::from("0"))), + (1.99999999, Some(String::from("2"))), + (-1.9999999, Some(String::from("-2"))), + (1.000000001, Some(String::from("1"))), + (-1.000001, Some(String::from("-1"))), + (0.5, Some(String::from("1/2"))), + (-0.5, Some(String::from("-1/2"))), + (0.3333333333, Some(String::from("1/3"))), + (1.3333333333, Some(String::from("1 + 1/3"))), + (-0.666666666, Some(String::from("-2/3"))), + (-1.666666666, Some(String::from("-1 - 2/3"))), + (-1.666666666, Some(String::from("-1 - 2/3"))), + (100.33333333, Some(String::from("100 + 1/3"))), + (-100.6666666, Some(String::from("-100 - 2/3"))), + (0.9932611, None), + (-0.9932611, None), + (-0.00001, None), + (1.9932611, None), + (-1.9932611, None), + (24f64, None), + (-24f64, None), + (1.23456f64, None), + (-1.23456f64, None), + (1.98, None), + (-1.98, None), + (9999999999f64, None), + (-9999999999f64, None), + (1000000001f64, None), + (-1000000001f64, None), + (0.53f64, None), + (-0.53f64, None), + (-1.51f64, None), + (0.335f64, None), + (-0.335f64, None), + (0.665f64, None), + (-0.665f64, None), + (100f64, None), + (-100f64, None), + ]; + + for (input, output) in in_out { + let result = KalkNum::from(input).estimate(); + println!("{}", input); + assert_eq!(output, result); + } + } +} diff --git a/kalk_cli/src/output.rs b/kalk_cli/src/output.rs index c75beb7..c439755 100644 --- a/kalk_cli/src/output.rs +++ b/kalk_cli/src/output.rs @@ -14,7 +14,16 @@ pub fn eval(parser: &mut parser::Context, input: &str, precision: u32) { result.to_string_big() }; - println!("{} {}", result_str, result.get_unit()); + let unit = result.get_unit(); + if let Some(estimate) = result.estimate() { + if unit == "" { + println!("{} ≈ {}", result_str, estimate); + } else { + println!("{} {} ≈ {}", result_str, unit, estimate); + } + } else { + println!("{} {}", result_str, unit); + } } Ok(None) => print!(""), Err(err) => print_err(&err.to_string()),