Compare commits

..
Author SHA1 Message Date
2deabb148e
refactor: rename modules to nicer names 2026-09-30 19:48:14 -07:00
35da9a0d40
feat: return Result instead of panicking
this doesn't always work still;
there are a lot of panicking index operations there.

a feature was stabilized very recently that would help fix that:
https://github.com/rust-lang/rust/issues/146954
2026-09-30 19:39:34 -07:00
d2368adc8f
feat: create lib.rs API 2026-09-30 18:25:46 -07:00
ae9e6604e8
refactor: fix unused lint 2026-09-30 18:18:35 -07:00
4363916608
refactor: split main.rs to support planned lib.rs 2026-09-30 17:40:31 -07:00
18a777c449
refactor: sort unit tests 2026-09-30 15:40:26 -07:00
396deba4e1
test: add test for backslash re-addition 2026-09-30 15:30:40 -07:00
9bacccfb71
feat: preserve leading_spaces 2026-09-30 15:18:15 -07:00
ca9a522d2b
refactor: drastically simplify should_add_back_equals logic 2026-09-29 16:18:50 -07:00
06b6c38f30
feat: ignore and add back trailing backslash 2026-09-29 16:04:15 -07:00
15534793fd
build: enable thick lto and minimize codegen-units
binary size goes from 1224 to 1068
(as reported by `du result/bin/tylc`)
2026-09-28 13:08:32 -07:00
1bb69e7357
build: don't duplicate cargo files in the store 2026-09-28 13:08:17 -07:00
c3526bf161
refactor: add and use testing helper fn eval 2026-09-28 13:01:24 -07:00
5f25c92404
fix: plus evaluated before minus 2026-09-28 12:58:12 -07:00
9 changed files with 242 additions and 185 deletions

View file

@ -10,3 +10,7 @@ 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

View file

@ -12,14 +12,14 @@
let let
pkgs = import inputs.nixpkgs { inherit system; }; pkgs = import inputs.nixpkgs { inherit system; };
cargoToml = fromTOML (builtins.readFile ./Cargo.toml); src = ./.;
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; inherit name src;
src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = "${src}/Cargo.lock";
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
m4 m4

100
src/formatting.rs Normal file
View file

@ -0,0 +1,100 @@
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);
}

4
src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod formatting;
pub mod nodes;
pub use nodes::eval;

View file

@ -1,14 +1,13 @@
use std::{ use std::{
env, env,
io::{self, Read}, io::{self, Read},
str::FromStr, process::ExitCode,
}; };
use rug::Float; mod formatting;
mod nodes; mod nodes;
fn main() { fn main() -> ExitCode {
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 => {
@ -18,100 +17,14 @@ fn main() {
} }
}; };
println!("{}", parse_and_calc_humanreadable(input)); match formatting::pretty_eval(input) {
} Ok(s) => println!("{s}"),
pub fn parse_and_calc_humanreadable(mut input: String) -> String { Err(e) => {
let should_add_back_equals = if let Some((i, _)) = input eprintln!("{e}");
.chars() return ExitCode::FAILURE;
.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 ExitCode::SUCCESS
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);
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);
} }

188
src/nodes/eval.rs Normal file
View file

@ -0,0 +1,188 @@
use rug::{
Float,
ops::{AddFrom, DivFrom, MulFrom, PowFrom, SubFrom},
};
use crate::nodes::{Function, Node, Nodes, Operator};
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error {
#[error("invalid operand: {0}")]
InvalidOperand(Node),
#[error("missing argument, got: {0}")]
MissingArgument(Node),
#[error("collapse failed, left with: {0}")]
CollapseFailed(Nodes),
}
impl Nodes {
/// Consume the `Nodes`, collapsing them down to a single number
pub fn evaluate(mut self) -> Result<Float, Error> {
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) => Ok(n),
_ => Err(Error::CollapseFailed(self)),
}
}
}
fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) -> Result<(), Error> {
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 => return Err(Error::MissingArgument(n.clone())),
};
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 => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.pow_from(lhs),
n => return Err(Error::InvalidOperand(n.clone())),
};
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 => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.div_from(lhs),
n => return Err(Error::InvalidOperand(n.clone())),
};
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 => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.mul_from(lhs),
n => return Err(Error::InvalidOperand(n.clone())),
};
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 => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.sub_from(lhs),
n => return Err(Error::InvalidOperand(n.clone())),
};
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 => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.add_from(lhs),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
end -= 1;
}
if nodes.len() > 1 {
nodes.remove(end);
nodes.remove(start);
}
Ok(())
}
#[cfg(test)]
use super::PREC;
#[test]
fn test() {
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()
.unwrap()
.to_f64(),
1.9092974268256817,
);
}

View file

@ -2,13 +2,29 @@
//! 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 evaluate; mod eval;
mod from_str; pub mod parse;
#[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)]
@ -18,18 +34,28 @@ 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)]
enum Node { pub 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)]
enum Operator { pub enum Operator {
Plus, Plus,
Minus, Minus,
#[default] #[default]
@ -39,7 +65,7 @@ enum Operator {
} }
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
enum Function { pub enum Function {
Sqrt, Sqrt,
Sine, Sine,

205
src/nodes/parse.rs Normal file
View file

@ -0,0 +1,205 @@
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 they are 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))
}
}
#[test]
fn test() {
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))
],
)
}

View file

@ -1,78 +1,40 @@
use core::f64; use core::f64;
use std::str::FromStr; use std::str::FromStr;
use rug::Float; use crate::nodes::Nodes;
use crate::nodes::{Function, Node, Nodes, Operator, PREC}; fn eval(s: &str) -> f64 {
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!(Nodes::from_str("1e-1").unwrap().evaluate().to_f64(), 0.1) assert_eq!(eval("1e-1"), 0.1)
} }
#[test] #[test]
fn implicit_mult() { fn implicit_mult() {
assert_eq!( assert_eq!(eval("10 sin(10)"), -5.440211108893698);
Nodes::from_str("10 sin(10)").unwrap().evaluate().to_f64(),
-5.440211108893698
);
assert_eq!( assert_eq!(eval("(10)(10)"), 100.,)
Nodes::from_str("(10)(10)").unwrap().evaluate().to_f64(),
100.,
)
} }
#[test] #[test]
fn constants() { fn constants() {
assert_eq!( assert_eq!(eval("c"), 299792458.);
Nodes::from_str("c").unwrap().evaluate().to_f64(), assert_eq!(eval("A"), 6.02214076e23);
299792458. assert_eq!(eval("pi"), f64::consts::PI);
);
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!(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.); assert_eq!(eval("+10"), 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))
],
)
} }