Estimation/rounding for final results

This commit is contained in:
bakk 2021-05-18 19:48:13 +02:00
parent 8399d810d8
commit c2577ef477
4 changed files with 244 additions and 58 deletions

View File

@ -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`

View File

@ -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;
}
}

View File

@ -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", "");
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<String> {
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<KalkNum> {
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);
}
}
}

View File

@ -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()),