Compare commits
9 changed files with 185 additions and 242 deletions
|
|
@ -10,7 +10,3 @@ thiserror = "2.0.18"
|
||||||
version = "1.30"
|
version = "1.30"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["std", "float"]
|
features = ["std", "float"]
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@
|
||||||
let
|
let
|
||||||
pkgs = import inputs.nixpkgs { inherit system; };
|
pkgs = import inputs.nixpkgs { inherit system; };
|
||||||
|
|
||||||
src = ./.;
|
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
cargoToml = fromTOML (builtins.readFile "${src}/Cargo.toml");
|
|
||||||
name = cargoToml.package.name;
|
name = cargoToml.package.name;
|
||||||
|
|
||||||
crate = pkgs.rustPlatform.buildRustPackage {
|
crate = pkgs.rustPlatform.buildRustPackage {
|
||||||
inherit name src;
|
inherit name;
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
cargoLock.lockFile = "${src}/Cargo.lock";
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
m4
|
m4
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use rug::Float;
|
|
||||||
|
|
||||||
use crate::nodes::{Error, Nodes};
|
|
||||||
|
|
||||||
pub fn pretty_eval(mut input: String) -> Result<String, Error> {
|
|
||||||
// no need to remove the characters here,
|
|
||||||
// as we ignore the them in all other logic already
|
|
||||||
let should_add_back_alignment = input.contains('&');
|
|
||||||
let should_add_back_backslash = input.contains('\\');
|
|
||||||
let leading_spaces = input.chars().take_while(|c| *c == ' ').count();
|
|
||||||
|
|
||||||
let should_add_back_equals = input
|
|
||||||
.find('=')
|
|
||||||
.inspect(|i| {
|
|
||||||
input.remove(*i);
|
|
||||||
})
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
let result = Nodes::from_str(&input)?.evaluate()?;
|
|
||||||
let significant_digits = get_significant_digits(&result);
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
"{:leading_spaces$}{}{result:.significant_digits$}{}",
|
|
||||||
' ',
|
|
||||||
if should_add_back_equals && should_add_back_alignment {
|
|
||||||
"& = "
|
|
||||||
} else if should_add_back_equals {
|
|
||||||
"= "
|
|
||||||
} else if should_add_back_alignment {
|
|
||||||
"& "
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
if should_add_back_backslash { " \\" } else { "" }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_character_readdition() {
|
|
||||||
assert_eq!(pretty_eval(" = 11".into()).unwrap(), " = 11");
|
|
||||||
assert_eq!(pretty_eval(" &= 11".into()).unwrap(), " & = 11");
|
|
||||||
assert_eq!(pretty_eval(" & 11".into()).unwrap(), " & 11");
|
|
||||||
assert_eq!(pretty_eval(" & 11 \\".into()).unwrap(), " & 11 \\");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_significant_digits(n: &Float) -> usize {
|
|
||||||
let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect();
|
|
||||||
|
|
||||||
let mut consecutive_zeros = 0;
|
|
||||||
let mut consecutive_nines = 0;
|
|
||||||
for (i, c) in raw_string.char_indices() {
|
|
||||||
if c == '0' {
|
|
||||||
consecutive_zeros += 1;
|
|
||||||
} else {
|
|
||||||
consecutive_zeros = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '9' {
|
|
||||||
consecutive_nines += 1;
|
|
||||||
} else {
|
|
||||||
consecutive_nines = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if consecutive_zeros >= 10 {
|
|
||||||
return i + 1 - consecutive_zeros;
|
|
||||||
} else if consecutive_nines >= 10 {
|
|
||||||
return i + 1 - consecutive_nines;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// all digits are significant
|
|
||||||
raw_string.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn eval(s: &str) -> Float {
|
|
||||||
crate::nodes::eval(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_significant_digits() {
|
|
||||||
let many_zeros = eval("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4");
|
|
||||||
assert_eq!(many_zeros.to_f64(), 117.48139);
|
|
||||||
let many_nines = eval("12.0096 + 15.9994 * 2");
|
|
||||||
assert_eq!(many_nines.to_f64(), 44.0084);
|
|
||||||
let zero_nine_zero = eval("2+12321(5+123)");
|
|
||||||
assert_eq!(zero_nine_zero.to_f64(), 1577090.0);
|
|
||||||
let test_4 = eval("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784");
|
|
||||||
assert_eq!(test_4.to_f64(), 46.06564);
|
|
||||||
|
|
||||||
assert_eq!(get_significant_digits(&many_zeros), 8);
|
|
||||||
assert_eq!(get_significant_digits(&many_nines), 6);
|
|
||||||
assert_eq!(get_significant_digits(&zero_nine_zero), 6);
|
|
||||||
assert_eq!(get_significant_digits(&test_4), 7);
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod formatting;
|
|
||||||
pub mod nodes;
|
|
||||||
|
|
||||||
pub use nodes::eval;
|
|
||||||
105
src/main.rs
105
src/main.rs
|
|
@ -1,13 +1,14 @@
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
process::ExitCode,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod formatting;
|
use rug::Float;
|
||||||
|
|
||||||
mod nodes;
|
mod nodes;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() {
|
||||||
let input = match env::args().skip(1).rev().reduce(|x, a| a + &x) {
|
let input = match env::args().skip(1).rev().reduce(|x, a| a + &x) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -17,14 +18,100 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match formatting::pretty_eval(input) {
|
println!("{}", parse_and_calc_humanreadable(input));
|
||||||
Ok(s) => println!("{s}"),
|
}
|
||||||
|
|
||||||
Err(e) => {
|
pub fn parse_and_calc_humanreadable(mut input: String) -> String {
|
||||||
eprintln!("{e}");
|
let should_add_back_equals = if let Some((i, _)) = input
|
||||||
return ExitCode::FAILURE;
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, c)| !matches!(c, ' ' | '&' | '\n'))
|
||||||
|
.find(|(_, c)| *c == '=')
|
||||||
|
{
|
||||||
|
input.remove(i);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
// no need to remove the character here,
|
||||||
|
// as we ignore the ampersand in all other logic already
|
||||||
|
let should_add_back_alignment = input.contains('&');
|
||||||
|
|
||||||
|
let result = parse_and_calc(&input);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{0}{result:.1$}",
|
||||||
|
if should_add_back_equals && should_add_back_alignment {
|
||||||
|
"& = "
|
||||||
|
} else if should_add_back_equals {
|
||||||
|
"= "
|
||||||
|
} else if should_add_back_alignment {
|
||||||
|
"& "
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
get_significant_digits(&result),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_equals_readdition() {
|
||||||
|
assert_eq!(parse_and_calc_humanreadable(" = 11".into()), "= 11");
|
||||||
|
assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), "& = 11");
|
||||||
|
assert_eq!(parse_and_calc_humanreadable(" & 11".into()), "& 11");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_and_calc(s: &str) -> Float {
|
||||||
|
nodes::Nodes::from_str(s).unwrap().evaluate()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_significant_digits(n: &Float) -> usize {
|
||||||
|
let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect();
|
||||||
|
|
||||||
|
let mut consecutive_zeros = 0;
|
||||||
|
let mut consecutive_nines = 0;
|
||||||
|
for (i, c) in raw_string.char_indices() {
|
||||||
|
if c == '0' {
|
||||||
|
consecutive_zeros += 1;
|
||||||
|
} else {
|
||||||
|
consecutive_zeros = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '9' {
|
||||||
|
consecutive_nines += 1;
|
||||||
|
} else {
|
||||||
|
consecutive_nines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if consecutive_zeros >= 10 {
|
||||||
|
return i + 1 - consecutive_zeros;
|
||||||
|
} else if consecutive_nines >= 10 {
|
||||||
|
return i + 1 - consecutive_nines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) {
|
||||||
|
return i;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all digits are significant
|
||||||
|
raw_string.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_significant_digits() {
|
||||||
|
let many_zeros = parse_and_calc("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4");
|
||||||
|
assert_eq!(many_zeros.to_f64(), 117.48139);
|
||||||
|
let many_nines = parse_and_calc("12.0096 + 15.9994 * 2");
|
||||||
|
assert_eq!(many_nines.to_f64(), 44.0084);
|
||||||
|
let zero_nine_zero = parse_and_calc("2+12321(5+123)");
|
||||||
|
assert_eq!(zero_nine_zero.to_f64(), 1577090.0);
|
||||||
|
let test_4 =
|
||||||
|
parse_and_calc("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784");
|
||||||
|
assert_eq!(test_4.to_f64(), 46.06564);
|
||||||
|
|
||||||
ExitCode::SUCCESS
|
assert_eq!(get_significant_digits(&many_zeros), 8);
|
||||||
|
assert_eq!(get_significant_digits(&many_nines), 6);
|
||||||
|
assert_eq!(get_significant_digits(&zero_nine_zero), 6);
|
||||||
|
assert_eq!(get_significant_digits(&test_4), 7);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
155
src/nodes/evaluate.rs
Normal file
155
src/nodes/evaluate.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
use rug::{
|
||||||
|
Float,
|
||||||
|
ops::{AddFrom, DivFrom, MulFrom, PowFrom, SubFrom},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::nodes::{Function, Node, Nodes, Operator};
|
||||||
|
|
||||||
|
impl Nodes {
|
||||||
|
/// Consume the `Nodes`, collapsing them down to a single number
|
||||||
|
pub fn evaluate(mut self) -> Float {
|
||||||
|
let nodes = &mut self.0;
|
||||||
|
|
||||||
|
while let Some(end) = nodes.iter().position(|n| *n == Node::ParenthesisEnd) {
|
||||||
|
collapse_toplevel(nodes, end);
|
||||||
|
}
|
||||||
|
let len = nodes.len();
|
||||||
|
collapse_toplevel(nodes, len);
|
||||||
|
|
||||||
|
match nodes.pop().unwrap() {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
_ => panic!("failed to collapse nodes: {nodes:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
|
||||||
|
let start = nodes[0..end]
|
||||||
|
.iter()
|
||||||
|
.rposition(|n| *n == Node::ParenthesisStart)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
while let Some((index, variant)) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, n)| match n {
|
||||||
|
Node::Function(f) => Some((i, *f)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
match &mut nodes[start..end][index + 1] {
|
||||||
|
Node::Number(n) => match variant {
|
||||||
|
Function::Sqrt => n.sqrt_mut(),
|
||||||
|
|
||||||
|
Function::Sine => n.sin_mut(),
|
||||||
|
Function::ArcSine => n.asin_mut(),
|
||||||
|
Function::Cosine => n.cos_mut(),
|
||||||
|
Function::ArcCosine => n.acos_mut(),
|
||||||
|
Function::Tangent => n.tan_mut(),
|
||||||
|
Function::ArcTangent => n.atan_mut(),
|
||||||
|
Function::Secant => n.sec_mut(),
|
||||||
|
Function::CoSecant => n.csc_mut(),
|
||||||
|
},
|
||||||
|
n => panic!("function is missing an argument, got: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + index);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(operator) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == Node::Operator(Operator::Exp))
|
||||||
|
{
|
||||||
|
let lhs = match nodes.remove(start + operator - 1) {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
end -= 1;
|
||||||
|
match &mut nodes[start..end][operator] {
|
||||||
|
Node::Number(n) => n.pow_from(lhs),
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + operator - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(operator) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == Node::Operator(Operator::Div))
|
||||||
|
{
|
||||||
|
let lhs = match nodes.remove(start + operator - 1) {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
end -= 1;
|
||||||
|
match &mut nodes[start..end][operator] {
|
||||||
|
Node::Number(n) => n.div_from(lhs),
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + operator - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(operator) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == Node::Operator(Operator::Mult))
|
||||||
|
{
|
||||||
|
let lhs = match nodes.remove(start + operator - 1) {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
end -= 1;
|
||||||
|
match &mut nodes[start..end][operator] {
|
||||||
|
Node::Number(n) => n.mul_from(lhs),
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + operator - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(operator) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == Node::Operator(Operator::Plus))
|
||||||
|
{
|
||||||
|
let lhs = match nodes.remove(start + operator - 1) {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
end -= 1;
|
||||||
|
match &mut nodes[start..end][operator] {
|
||||||
|
Node::Number(n) => n.add_from(lhs),
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + operator - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(operator) = nodes[start..end]
|
||||||
|
.iter()
|
||||||
|
.position(|n| *n == Node::Operator(Operator::Minus))
|
||||||
|
{
|
||||||
|
let lhs = match nodes.remove(start + operator - 1) {
|
||||||
|
Node::Number(n) => n,
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
end -= 1;
|
||||||
|
match &mut nodes[start..end][operator] {
|
||||||
|
Node::Number(n) => n.sub_from(lhs),
|
||||||
|
n => panic!("operator has an invalid operand: {n:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.remove(start + operator - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes.len() > 1 {
|
||||||
|
nodes.remove(end);
|
||||||
|
nodes.remove(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/nodes/from_str.rs
Normal file
190
src/nodes/from_str.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
use core::f64;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use rug::{Float, float::ParseFloatError, ops::CompleteRound};
|
||||||
|
|
||||||
|
use crate::nodes::{Function, Node, Nodes, Operator, PREC};
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn from_name(s: &str) -> Result<Self, String> {
|
||||||
|
Ok(match s {
|
||||||
|
"c" => Self::Number(Float::with_val(PREC, 299_792_458)),
|
||||||
|
"A" => Self::Number(Float::with_val(PREC, 6.02214076e23)),
|
||||||
|
"pi" => Self::Number(Float::with_val(PREC, f64::consts::PI)),
|
||||||
|
// Planck's constant, in J*s (joule-seconds)
|
||||||
|
"h" => Self::Number(Float::with_val(PREC, 6.62607015e-34)),
|
||||||
|
_ => Self::Function(s.parse()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<char> for Operator {
|
||||||
|
type Error = char;
|
||||||
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
'+' => Ok(Self::Plus),
|
||||||
|
'-' | '−' => Ok(Self::Minus),
|
||||||
|
'*' | '×' => Ok(Self::Mult),
|
||||||
|
'/' | '÷' => Ok(Self::Div),
|
||||||
|
'^' => Ok(Self::Exp),
|
||||||
|
_ => Err(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for Function {
|
||||||
|
type Err = String;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"sqrt" => Ok(Self::Sqrt),
|
||||||
|
|
||||||
|
"sin" => Ok(Self::Sine),
|
||||||
|
"asin" => Ok(Self::ArcSine),
|
||||||
|
"cos" => Ok(Self::Cosine),
|
||||||
|
"acos" => Ok(Self::ArcCosine),
|
||||||
|
"tan" => Ok(Self::Tangent),
|
||||||
|
"atan" => Ok(Self::ArcTangent),
|
||||||
|
"sec" => Ok(Self::Secant),
|
||||||
|
"csc" => Ok(Self::CoSecant),
|
||||||
|
_ => Err(s.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("unrecognized function/constant: {0}")]
|
||||||
|
UnrecognizedName(String),
|
||||||
|
#[error("invalid operator: {0}")]
|
||||||
|
InvalidOperator(char),
|
||||||
|
#[error("failed to parse number '{0}': {1}")]
|
||||||
|
NumberParsing(String, ParseFloatError),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
enum ParsingState {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
ReadingFunctionName(usize),
|
||||||
|
ReadingNumber(usize),
|
||||||
|
}
|
||||||
|
impl FromStr for Nodes {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
/// A superset of `char::is_numeric`
|
||||||
|
/// that includes other characters parseable into numbers,
|
||||||
|
/// such as `.` (for decimal) and `e` for scientific notation.
|
||||||
|
fn is_number_char(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
'.' | 'e' => true,
|
||||||
|
c => c.is_numeric(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A function to check if the given `char` should be used by the parser.
|
||||||
|
///
|
||||||
|
/// We ignore whitespace since it has no semantic significance in typst
|
||||||
|
/// (aside from variables, which are not implemented)
|
||||||
|
/// and `&` because it is only used for visual alignment.
|
||||||
|
///
|
||||||
|
/// Newlines are also ignored because they're likely accidental;
|
||||||
|
/// like in the case of `echo 1+1 | tylc`.
|
||||||
|
fn is_not_ignored_char(c: &char) -> bool {
|
||||||
|
!matches!(c, ' ' | '&' | '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
let mut state = ParsingState::default();
|
||||||
|
|
||||||
|
let filtered_string: String = s.chars().filter(is_not_ignored_char).collect();
|
||||||
|
|
||||||
|
for (i, c) in filtered_string.char_indices() {
|
||||||
|
// check whether we're at the end of the bit we're parsing
|
||||||
|
match state {
|
||||||
|
ParsingState::Start => {}
|
||||||
|
ParsingState::ReadingFunctionName(start_index) => {
|
||||||
|
if !c.is_alphabetic() {
|
||||||
|
if matches!(nodes.last(), Some(Node::ParenthesisEnd | Node::Number(_))) {
|
||||||
|
nodes.push(Node::Operator(Default::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
Node::from_name(&filtered_string[start_index..i])
|
||||||
|
.map_err(Error::UnrecognizedName)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = ParsingState::Start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParsingState::ReadingNumber(start_index) => {
|
||||||
|
if !is_number_char(c)
|
||||||
|
// don't stop reading number on a special case, eg "1e-1"
|
||||||
|
&& !(filtered_string.chars().nth(i - 1) == Some('e') && c == '-')
|
||||||
|
{
|
||||||
|
nodes.push(Node::Number(
|
||||||
|
Float::parse(&filtered_string[start_index..i])
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::NumberParsing(
|
||||||
|
filtered_string[start_index..i].to_owned(),
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.complete(PREC),
|
||||||
|
));
|
||||||
|
|
||||||
|
state = ParsingState::Start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start parsing something new (if applicable)
|
||||||
|
if state == ParsingState::Start {
|
||||||
|
if is_number_char(c) {
|
||||||
|
state = ParsingState::ReadingNumber(i);
|
||||||
|
} else if c.is_alphabetic() {
|
||||||
|
state = ParsingState::ReadingFunctionName(i);
|
||||||
|
} else if c == '(' {
|
||||||
|
if matches!(nodes.last(), Some(Node::Number(_) | Node::ParenthesisEnd)) {
|
||||||
|
nodes.push(Node::Operator(Default::default()));
|
||||||
|
}
|
||||||
|
nodes.push(Node::ParenthesisStart);
|
||||||
|
} else if c == ')' {
|
||||||
|
nodes.push(Node::ParenthesisEnd);
|
||||||
|
} else {
|
||||||
|
if matches!(nodes.last(), Some(Node::Operator(_)) | None) {
|
||||||
|
state = ParsingState::ReadingNumber(i);
|
||||||
|
} else {
|
||||||
|
nodes.push(Node::Operator(
|
||||||
|
c.try_into().map_err(Error::InvalidOperator)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap up any unfinished parsing
|
||||||
|
let i = filtered_string.len();
|
||||||
|
match state {
|
||||||
|
ParsingState::Start => {}
|
||||||
|
ParsingState::ReadingFunctionName(start_index) => {
|
||||||
|
if matches!(nodes.last(), Some(Node::ParenthesisEnd | Node::Number(_))) {
|
||||||
|
nodes.push(Node::Operator(Default::default()));
|
||||||
|
}
|
||||||
|
nodes.push(
|
||||||
|
Node::from_name(&filtered_string[start_index..i])
|
||||||
|
.map_err(Error::UnrecognizedName)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ParsingState::ReadingNumber(start_index) => {
|
||||||
|
if matches!(nodes.last(), Some(Node::ParenthesisEnd)) {
|
||||||
|
nodes.push(Node::Operator(Default::default()));
|
||||||
|
}
|
||||||
|
nodes.push(Node::Number(
|
||||||
|
Float::parse(&filtered_string[start_index..i])
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::NumberParsing(filtered_string[start_index..i].to_owned(), e)
|
||||||
|
})?
|
||||||
|
.complete(PREC),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(nodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,29 +2,13 @@
|
||||||
//! as well as the logic to parse it from a string (`Node::from_str`),
|
//! as well as the logic to parse it from a string (`Node::from_str`),
|
||||||
//! and the logic to evaluate it into a single number (`Node::evaluate`).
|
//! and the logic to evaluate it into a single number (`Node::evaluate`).
|
||||||
|
|
||||||
use std::fmt::{Debug, Display};
|
|
||||||
|
|
||||||
use rug::Float;
|
use rug::Float;
|
||||||
|
|
||||||
mod eval;
|
mod evaluate;
|
||||||
pub mod parse;
|
mod from_str;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("{0}")]
|
|
||||||
Parsing(#[from] parse::Error),
|
|
||||||
#[error("{0}")]
|
|
||||||
Evaluation(#[from] eval::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// helper function to parse to Nodes and immediately evaluate
|
|
||||||
pub fn eval(s: &str) -> Result<rug::Float, Error> {
|
|
||||||
use std::str::FromStr;
|
|
||||||
Ok(Nodes::from_str(s)?.evaluate()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PREC: u32 = 512;
|
pub const PREC: u32 = 512;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
|
@ -34,28 +18,18 @@ impl AsRef<[Node]> for Nodes {
|
||||||
self.0.as_ref()
|
self.0.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Display for Nodes {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
Debug::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Node {
|
enum Node {
|
||||||
Function(Function),
|
Function(Function),
|
||||||
ParenthesisStart,
|
ParenthesisStart,
|
||||||
ParenthesisEnd,
|
ParenthesisEnd,
|
||||||
Number(Float),
|
Number(Float),
|
||||||
Operator(Operator),
|
Operator(Operator),
|
||||||
}
|
}
|
||||||
impl Display for Node {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
Debug::fmt(&self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||||
pub enum Operator {
|
enum Operator {
|
||||||
Plus,
|
Plus,
|
||||||
Minus,
|
Minus,
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -65,7 +39,7 @@ pub enum Operator {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum Function {
|
enum Function {
|
||||||
Sqrt,
|
Sqrt,
|
||||||
|
|
||||||
Sine,
|
Sine,
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,78 @@
|
||||||
use core::f64;
|
use core::f64;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::nodes::Nodes;
|
use rug::Float;
|
||||||
|
|
||||||
fn eval(s: &str) -> f64 {
|
use crate::nodes::{Function, Node, Nodes, Operator, PREC};
|
||||||
Nodes::from_str(s).unwrap().evaluate().unwrap().to_f64()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn plus_minus_order() {
|
|
||||||
assert_eq!(eval("1 - 4 - 5"), -8.);
|
|
||||||
assert_eq!(eval("1 - 4 + 5"), 2.);
|
|
||||||
assert_eq!(eval("1 + 4 - 5"), 0.);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn e_edge_case() {
|
fn e_edge_case() {
|
||||||
assert_eq!(eval("1e-1"), 0.1)
|
assert_eq!(Nodes::from_str("1e-1").unwrap().evaluate().to_f64(), 0.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn implicit_mult() {
|
fn implicit_mult() {
|
||||||
assert_eq!(eval("10 sin(10)"), -5.440211108893698);
|
assert_eq!(
|
||||||
|
Nodes::from_str("10 sin(10)").unwrap().evaluate().to_f64(),
|
||||||
|
-5.440211108893698
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(eval("(10)(10)"), 100.,)
|
assert_eq!(
|
||||||
|
Nodes::from_str("(10)(10)").unwrap().evaluate().to_f64(),
|
||||||
|
100.,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn constants() {
|
fn constants() {
|
||||||
assert_eq!(eval("c"), 299792458.);
|
assert_eq!(
|
||||||
assert_eq!(eval("A"), 6.02214076e23);
|
Nodes::from_str("c").unwrap().evaluate().to_f64(),
|
||||||
assert_eq!(eval("pi"), f64::consts::PI);
|
299792458.
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Nodes::from_str("A").unwrap().evaluate().to_f64(),
|
||||||
|
6.02214076e23
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Nodes::from_str("pi").unwrap().evaluate().to_f64(),
|
||||||
|
f64::consts::PI
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unary_operators() {
|
fn unary_operators() {
|
||||||
assert_eq!(eval("-10"), -10.);
|
assert_eq!(Nodes::from_str("-10").unwrap().evaluate().to_f64(), -10.);
|
||||||
assert_eq!(eval("+10"), 10.);
|
assert_eq!(Nodes::from_str("+10").unwrap().evaluate().to_f64(), 10.);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nodes_to_num() {
|
||||||
|
assert_eq!(
|
||||||
|
Nodes(vec![
|
||||||
|
Node::Function(Function::Sine),
|
||||||
|
Node::ParenthesisStart,
|
||||||
|
Node::Number(Float::with_val(PREC, 2)),
|
||||||
|
Node::ParenthesisEnd,
|
||||||
|
Node::Operator(Operator::Plus),
|
||||||
|
Node::Number(Float::with_val(PREC, 1))
|
||||||
|
])
|
||||||
|
.evaluate()
|
||||||
|
.to_f64(),
|
||||||
|
1.9092974268256817,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nodes_from_str() {
|
||||||
|
assert_eq!(
|
||||||
|
Nodes::from_str("sin(2) + 1").unwrap().as_ref(),
|
||||||
|
&[
|
||||||
|
Node::Function(Function::Sine),
|
||||||
|
Node::ParenthesisStart,
|
||||||
|
Node::Number(Float::with_val(PREC, 2)),
|
||||||
|
Node::ParenthesisEnd,
|
||||||
|
Node::Operator(Operator::Plus),
|
||||||
|
Node::Number(Float::with_val(PREC, 1))
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue