Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2deabb148e |
|||
|
35da9a0d40 |
|||
|
d2368adc8f |
|||
|
ae9e6604e8 |
|||
|
4363916608 |
|||
|
18a777c449 |
|||
|
396deba4e1 |
|||
|
9bacccfb71 |
|||
|
ca9a522d2b |
|||
|
06b6c38f30 |
|||
|
15534793fd |
|||
|
1bb69e7357 |
|||
|
c3526bf161 |
|||
|
5f25c92404 |
9 changed files with 242 additions and 185 deletions
|
|
@ -10,3 +10,7 @@ thiserror = "2.0.18"
|
|||
version = "1.30"
|
||||
default-features = false
|
||||
features = ["std", "float"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@
|
|||
let
|
||||
pkgs = import inputs.nixpkgs { inherit system; };
|
||||
|
||||
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
||||
src = ./.;
|
||||
cargoToml = fromTOML (builtins.readFile "${src}/Cargo.toml");
|
||||
name = cargoToml.package.name;
|
||||
|
||||
crate = pkgs.rustPlatform.buildRustPackage {
|
||||
inherit name;
|
||||
src = ./.;
|
||||
inherit name src;
|
||||
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
cargoLock.lockFile = "${src}/Cargo.lock";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
m4
|
||||
|
|
|
|||
100
src/formatting.rs
Normal file
100
src/formatting.rs
Normal 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
4
src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod formatting;
|
||||
pub mod nodes;
|
||||
|
||||
pub use nodes::eval;
|
||||
105
src/main.rs
105
src/main.rs
|
|
@ -1,14 +1,13 @@
|
|||
use std::{
|
||||
env,
|
||||
io::{self, Read},
|
||||
str::FromStr,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use rug::Float;
|
||||
|
||||
mod formatting;
|
||||
mod nodes;
|
||||
|
||||
fn main() {
|
||||
fn main() -> ExitCode {
|
||||
let input = match env::args().skip(1).rev().reduce(|x, a| a + &x) {
|
||||
Some(s) => s,
|
||||
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 {
|
||||
let should_add_back_equals = if let Some((i, _)) = input
|
||||
.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;
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
|
|
|||
188
src/nodes/eval.rs
Normal file
188
src/nodes/eval.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
|
|
@ -2,13 +2,29 @@
|
|||
//! 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`).
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use rug::Float;
|
||||
|
||||
mod evaluate;
|
||||
mod from_str;
|
||||
mod eval;
|
||||
pub mod parse;
|
||||
#[cfg(test)]
|
||||
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;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
|
|
@ -18,18 +34,28 @@ impl AsRef<[Node]> for Nodes {
|
|||
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)]
|
||||
enum Node {
|
||||
pub enum Node {
|
||||
Function(Function),
|
||||
ParenthesisStart,
|
||||
ParenthesisEnd,
|
||||
Number(Float),
|
||||
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)]
|
||||
enum Operator {
|
||||
pub enum Operator {
|
||||
Plus,
|
||||
Minus,
|
||||
#[default]
|
||||
|
|
@ -39,7 +65,7 @@ enum Operator {
|
|||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
enum Function {
|
||||
pub enum Function {
|
||||
Sqrt,
|
||||
|
||||
Sine,
|
||||
|
|
|
|||
205
src/nodes/parse.rs
Normal file
205
src/nodes/parse.rs
Normal 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))
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
@ -1,78 +1,40 @@
|
|||
use core::f64;
|
||||
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]
|
||||
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]
|
||||
fn implicit_mult() {
|
||||
assert_eq!(
|
||||
Nodes::from_str("10 sin(10)").unwrap().evaluate().to_f64(),
|
||||
-5.440211108893698
|
||||
);
|
||||
assert_eq!(eval("10 sin(10)"), -5.440211108893698);
|
||||
|
||||
assert_eq!(
|
||||
Nodes::from_str("(10)(10)").unwrap().evaluate().to_f64(),
|
||||
100.,
|
||||
)
|
||||
assert_eq!(eval("(10)(10)"), 100.,)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constants() {
|
||||
assert_eq!(
|
||||
Nodes::from_str("c").unwrap().evaluate().to_f64(),
|
||||
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
|
||||
);
|
||||
assert_eq!(eval("c"), 299792458.);
|
||||
assert_eq!(eval("A"), 6.02214076e23);
|
||||
assert_eq!(eval("pi"), f64::consts::PI);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unary_operators() {
|
||||
assert_eq!(Nodes::from_str("-10").unwrap().evaluate().to_f64(), -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))
|
||||
],
|
||||
)
|
||||
assert_eq!(eval("-10"), -10.);
|
||||
assert_eq!(eval("+10"), 10.);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue