diff --git a/.gitignore b/.gitignore index 96a4b3b..38461ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /bin/** +Cargo.lock +Cargo.lock diff --git a/.swp b/.swp new file mode 100644 index 0000000..06b0357 Binary files /dev/null and b/.swp differ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6dcd831 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'rclc'", + "cargo": { + "args": [ + "run", + "--bin=rclc", + "--package=rust_command_line_calculator", + ], + "filter": { + "name": "rclc", + "kind": "bin" + } + }, + "args": ["(())()"], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'rclc'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=rclc", + "--package=rust_command_line_calculator", + ], + "filter": { + "name": "rclc", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 1c25e25..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,452 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "clap" -version = "4.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" -dependencies = [ - "bitflags", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "io-lifetimes" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "is-terminal" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" -dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys", -] - -[[package]] -name = "libc" -version = "0.2.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" - -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "rust_command_line_calculator" -version = "0.1.1" -dependencies = [ - "anyhow", - "clap", - "num", - "regex", -] - -[[package]] -name = "rustix" -version = "0.36.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "unicode-ident" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml index 6aa94c1..f5d1369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust_command_line_calculator" -version = "0.1.1" +version = "0.2.0" edition = "2021" authors = ["Christoph J. Scherr "] license = "GPL3" diff --git a/src/expression_parser.rs b/src/expression_parser.rs index a8c4fed..79d0e81 100644 --- a/src/expression_parser.rs +++ b/src/expression_parser.rs @@ -1,15 +1,24 @@ -use std::fmt; +use std::{fmt, error::Error, num::IntErrorKind}; use regex::Regex; +pub mod shunting_yard; + +fn normalize_string(to_normalize: String) -> String { + let mut normalized_text = to_normalize; + normalized_text.retain(|c| !c.is_whitespace()); + normalized_text = normalized_text.to_string(); + normalized_text +} + // In an expression like `sqrt(25)` the Task would correspond to `sqrt`. This is the enum to // configure possible Tasks. // None means, the Expression doesn't send it's Value to a Task Handler #[derive(Debug)] // automatically generate Debug Formatter pub enum Task { None, - Sqrt, - Power, - Log(u64), + Root(u64), + Power(f64), + Log(f64), } // How to clone a Task, i was supprised I had to do it myself. @@ -18,39 +27,83 @@ impl Clone for Task { // This can probably be done cleaner than with a verbose match. FIXME match self { Task::None => Task::None, - Task::Sqrt => Task::Sqrt, - Task::Power => Task::Power, + Task::Root(depth) => Task::Root(*depth), + Task::Power(exp) => Task::Power(*exp), Task::Log(base) => Task::Log(*base), // TODO add base for log } } } impl Task { - pub fn new(task_text: &str) -> Task { - match task_text { + pub fn new(task_text: &str, task_param: &str) -> Task { + if task_text.is_empty() { + return Task::None; + } + let task_text = task_text.to_lowercase(); + match task_text.as_str() { "none" => Task::None, - "sqrt" => Task::Sqrt, - "power"|"pow" => Task::Power, - "log"|"ln" => Task::Log(10), // TODO add base + "sqrt"|"root" => { + if task_param.is_empty() { + return Task::Root(2); + } + let pot_param = task_param.parse::(); + match pot_param { + Ok(value) => {Task::Root(value)}, + Err(error) => { + eprintln!("could not parse task parameter: {error}"); + std::process::exit(1); + }, + } + }, + "power"|"pow"|"sq" => { + if task_param.is_empty() { + return Task::Power(2.0); + } + let pot_param = task_param.parse::(); + match pot_param { + Ok(value) => {Task::Power(value)}, + Err(error) => { + eprintln!("could not parse task parameter: {error}"); + std::process::exit(1); + }, + } + }, + "log"|"ln" => { + if task_param.is_empty() { + return Task::Log(10.0); + } + let pot_param = task_param.parse::(); + match pot_param { + Ok(value) => {Task::Log(value)}, + Err(error) => { + eprintln!("could not parse task parameter: {error}"); + std::process::exit(1); + }, + } + }, // what to do if a bad task was given: - &_ => {eprintln!("Bad Task: {}", task_text); std::process::exit(1); }, + // this would be throwing an error and aborting + //&_ => {eprintln!("Bad Task: {}", task_text); std::process::exit(1); }, + _ => Task::None, } } } // An Expression is something that can be calculated. 20+5 is an expression. Expressions can // contain other -// Expressions and have tasks: 20+sqrt(20+5) +// Expressions and have tasks: 20+log_10(20+5) +// Tasks may have parameters, denoted using an underscore '_' // Expressions are marked down with braces and a task before those braces: // task(Expression) // once the Value of the Expression got calculated, the calculated value should be sent to the // TaskHandler, if the Task of the Expression is not Task::None pub struct Expression { - text: String, + pub text: String, + full_text: String, task: Task, complex: bool, - inner_value: f64, - outer_value: f64, + outer_value: Result, children: Vec, + depth: u8, } // Debug Formatter for Expression @@ -58,11 +111,12 @@ impl fmt::Debug for Expression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Expression") .field("text", &self.text) + .field("full text", &self.full_text) .field("task", &self.task) .field("is complex?", &self.complex) - .field("inner value", &self.inner_value) .field("outer value", &self.outer_value) .field("children", &self.children) + .field("depth", &self.depth) .finish() } } @@ -72,15 +126,56 @@ impl Clone for Expression{ fn clone(&self) -> Self { Expression { text: self.text.clone(), + full_text: self.full_text.clone(), task: self.task.clone(), complex: self.complex.clone(), // TODO add support for complex numbers - inner_value: self.inner_value.clone(), outer_value: self.outer_value.clone(), children: self.children.clone(), + depth: self.depth.clone(), } } } +fn find_brace_groups(haystack: String) -> Vec> { + + // TODO add support for diffrent braces + // TODO add error if not all braces are closed + let mut parenthesis_group: Vec<(usize, usize)> = Vec::new(); + let mut parenthesis_open: usize = 0; + let mut parenthesis_open_processed: usize = 0; + let mut parenthesis_closed_processed: usize = 0; + let mut parenthesis_last_opened: Vec = Vec::new(); + //let mut brackets_group: Vec<(usize, usize)> = Vec::new(); + //let mut brackets_open: usize = 0; + //let mut square_braces_group: Vec<(usize, usize)> = Vec::new(); + //let mut square_braces_open: usize = 0; + // first open stuff + for (index, char) in haystack.chars().enumerate() { + match char { + '(' => { + parenthesis_group.push((index, 0)); + parenthesis_open = parenthesis_open + 1; + parenthesis_last_opened.push(parenthesis_open_processed); + parenthesis_open_processed = parenthesis_open_processed + 1; + }, + ')' => { + parenthesis_group[parenthesis_last_opened[parenthesis_last_opened.len() - 1]].1 = index; + parenthesis_open = parenthesis_open - 1; + parenthesis_closed_processed = parenthesis_closed_processed + 1; + parenthesis_last_opened.pop(); + // TODO add error if no parenthesis is open yet. + }, + _ => (), + } + } + // now iterate backwards and search for closing things + + let brace_groups = vec![parenthesis_group/*, square_braces_group, brackets_group*/]; + #[cfg(debug_assertions)] + dbg!(&brace_groups); + return brace_groups; +} + /* * Main logic for the Expression struct */ @@ -90,66 +185,133 @@ impl Expression { * example: "12 + log_10(10 + 15) + 3" * has a sub expression log_10(10 + 5), which has Task::Log with base 10 */ - pub fn new(expression_text: String, task: Task) -> Expression { + pub fn new(expression_text: String, expression_full_text: String, task: Task, depth: u8) -> Expression { - // find children - // TODO add error for unused task parameters - let re_sub_expression = Regex::new(r"\w+\(.+?\)").unwrap(); - if re_sub_expression.is_match(&expression_text) { - let mut children: Vec = Vec::new(); - for sub_expression_text in re_sub_expression.captures_iter(&expression_text) { - // if any task parameters are set ( syntax: task_para(expression) ) - if sub_expression_text[0].contains('_') { - let task_and_expr: Vec<&str> = sub_expression_text[0].split(['_', '(']).collect(); + // check if we are too deep + if depth > 254 { + eprintln!("Expression '{}' has a too deep family tree. Maximum generations are 254.", expression_text); + std::process::exit(1); + } - let task_text = task_and_expr[0].clone().to_lowercase(); - let task_param = task_and_expr[1].clone().to_string(); - let task = match task_text.as_str() { - "none" => Task::None, - "sqrt" => Task::Sqrt, - "power" => Task::Power, - "log" => {let base: u64 = task_param.parse().unwrap(); Task::Log(base)}, - // what to do if a bad task was given: - &_ => {eprintln!("Bad Task: {}", task_text); std::process::exit(1); }, - }; - let expression_inner = task_and_expr[2].clone().to_string(); - children.push(Expression::new(expression_inner, task)); - } - // if there are no parameters we need to do diffrent splitting and assume defaults - else { - let task_and_expr: Vec<&str> = sub_expression_text[0].split(['(']).collect(); + let expression_text = normalize_string(expression_text); + let mut task_text_full: String; + let mut children: Vec = Vec::new(); - let task_text = task_and_expr[0].clone().to_lowercase(); - let task = match task_text.as_str() { - "none" => Task::None, - "sqrt" => Task::Sqrt, - "power" => Task::Power, - "log" => Task::Log(10), - // what to do if a bad task was given: - &_ => {eprintln!("Bad Task: {}", task_text); std::process::exit(1); }, - }; - let expression_inner = task_and_expr[1].clone().to_string(); - children.push(Expression::new(expression_inner, task)); + let re_contains_sub_expression= Regex::new(r"(\(.*\))|(\[.*\])|(\{.*\})").unwrap(); + + if re_contains_sub_expression.is_match(expression_text.as_str()) { + let brace_groups: Vec> = find_brace_groups(expression_text.clone()); + + let mut brace_groups_texts: Vec = Vec::new(); + + // 1 brace group per possible combination, by default, this is only (), so 1 iteration. + // This is still O(n¹) + for brace_group in brace_groups { + for pair in brace_group { + let text = &expression_text[pair.0..pair.1 + 1]; + let text = &text[1..text.len() - 1]; + brace_groups_texts.push(text.to_string()); + // we have the expression_text, now we just need to get the task until we can + // pass these parameters into Expression::new(). This is the recursive part. + let possible_task = &expression_text[..pair.0].chars().rev().collect::(); + let mut stop_at: usize = 0; + for (index, char) in possible_task.chars().enumerate() { + if !(char.is_alphanumeric()) { + break; + } + stop_at = index; + } + dbg!(&stop_at); + // needed for none task: '1 + (1 + 1)' + let fixup = if stop_at == 0 { 0 } else { 1 }; + task_text_full = possible_task.clone()[..stop_at+ fixup].chars().rev().collect::(); + let task: Task; + if task_text_full.contains('_') { + let split: Vec<&str> = task_text_full.split('_').collect(); + task = Task::new(split[0], split[1]); + } + else { + task = Task::new(task_text_full.as_str(), ""); + } + let child_full_text = task_text_full + "(" + text + ")"; + let child = Expression::new(text.to_string(), child_full_text, task, depth+1); + children.push(child); } } - #[cfg(debug_assertions)] - dbg!(children); - } + } let expression = Expression { text: expression_text, - // TODO generate these from the text! - task: task, + full_text: normalize_string(expression_full_text), + task, complex: false, - inner_value: 0.0, - outer_value: 0.0, - children: Vec::new(), + outer_value: Err("Value not yet calculated.".to_string()), + children, + depth, }; expression } - pub fn process(&self) { - println!("{}", self.text); + // calculate value for expression. + pub fn process(self) -> Result { + let mut normalized_text = self.normalize_text(); + //let re_numeric = Regex::new(r"\d+(\.\d+)?"); + /* + * Algorithm: + * + * First, search child expressions in normalized_text by searching for the text of all + * children in normalized_text. If an expression is found, a value for it should be + * calculated (recursive!) and the text should be substituted with the calculated value. + * If a child expression is not found in the normalized_text, throw an error, as an + * expression has a child but does not contain it's text. (note: a childs child + * expressions are not the child expression of the original expression, so no need to + * worry about the order of substituting texts for values.) + * + * Once there are no more child expressions in the normalized_text, we can use the + * shunting yards algorithm to calculate the result. I'm not yet sure, if I want to use + * another developers shunting yard algorithm or implement it by myself. + */ + + // TODO check if we have any unknown values. + + // iterate through children, substitute childrens text with childrens results (as string + // slice). + for child in self.children { + //normalized_text = normalized_text.replace(child.full_text.clone().as_str(), child.process().expect(self.text).as_str()); + let child_full_text = match child.clone().process() { + Ok(result) => result.to_string(), + Err(err) => { + eprintln!( + "Could not calculate result of child expression '{}': {}", + child.text, + "error placeholder TODO" + ); + std::process::exit(2); + } + }; + dbg!(&child.full_text); + dbg!(&child_full_text); + normalized_text = normalized_text.replace(child.full_text.as_str(), child_full_text.as_str()); + } + dbg!(&normalized_text); + // TODO Shunting yards algorithm, as we now have only calculatable values left. + // Implement this as public module in shunting_yard.rs + // self.result = MYRESULT + let rpn = shunting_yard::form_reverse_polish_notation(&normalized_text); + match rpn { + Ok(valid_rpn) => { + dbg!(&valid_rpn); + return shunting_yard::calc_reverse_polish_notation(valid_rpn); + }, + Err(err) => { + eprintln!("Could not calculate a result for expression '{}': {err}", self.text); + std::process::exit(2); + }, + } + } + + // wrapper for normalize_string() + fn normalize_text(&self) -> String { + normalize_string(self.text.clone()) } } - diff --git a/src/linear_algebra.rs b/src/expression_parser/linear_algebra.rs similarity index 100% rename from src/linear_algebra.rs rename to src/expression_parser/linear_algebra.rs diff --git a/src/expression_parser/shunting_yard.rs b/src/expression_parser/shunting_yard.rs new file mode 100644 index 0000000..ccc03fd --- /dev/null +++ b/src/expression_parser/shunting_yard.rs @@ -0,0 +1,184 @@ + +/* + * Custom made implementation of the shunting yard algorithm. + * Makes a regular mathmatical expression into reverse polish notation, + * a + b -> a b + + * a * b + c -> a b * c + + * and so on. + * these can be easily interpreted by an algorithm to calculate the value of any given term. + * + * note: this version of shunting yard does not implement functions. They are handled by the + * expression parser. + */ + +#[derive(PartialEq)] +enum Associativity { + Right, + Left +} + +#[derive(PartialEq)] +pub struct Operator { + character: char, + precedence: u8, + associativity: Associativity +} + +impl Operator { + pub fn is_operator(c: char) -> bool { + for op in OPERATORS { + if c == op.character { return true; } + } + return false; + } + + pub fn get_operator(c: char) -> Option { + match c { + '+' => Some(ADDITION), + '-' => Some(SUBTRACTION), + '*' => Some(MULTIPLICATION), + '/' => Some(DIVISION), + '^' => Some(EXPONENTIATION), + _ => None + } + } +} + +const ADDITION: Operator = Operator { + character: '+', + precedence: 2, + associativity: Associativity::Left +}; + +const SUBTRACTION: Operator = Operator { + character: '-', + precedence: 2, + associativity: Associativity::Left +}; + +const MULTIPLICATION: Operator = Operator { + character: '*', + precedence: 2, + associativity: Associativity::Left +}; + +const DIVISION: Operator = Operator { + character: '/', + precedence: 2, + associativity: Associativity::Left +}; + +const EXPONENTIATION: Operator = Operator { + character: '*', + precedence: 2, + associativity: Associativity::Right +}; + +const OPERATORS: [Operator; 5] = [ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION, EXPONENTIATION]; + +pub fn form_reverse_polish_notation(regular_math: &str) -> Result, String> { + let mut output_queue: Vec> = Vec::new(); + let mut input_queue: Vec = regular_math.chars().rev().collect(); + let mut operator_stack: Vec = Vec::new(); + let mut currently_processing_numeric_group = false; + let mut current_numeric_group: Vec = Vec::new(); + let mut current_numeric_group_has_point = false; + + // while there are tokens to br read: + while !(input_queue.is_empty()) { + // read a token + let token: char = input_queue.pop().unwrap(); + dbg!(&token); + + // if the token is: + // a number: + if token.is_numeric() | (token == '.') { + // put it into the output_queue + current_numeric_group.push(token); + currently_processing_numeric_group = true; + if (token == '.') & (!current_numeric_group_has_point) { + current_numeric_group_has_point = true; + } + else if (token == '.') & (current_numeric_group_has_point) { + return Err("Numeric group contains too many '.' Only one is allowed.".to_string()); + } + } + // a function + // handled by the expression parser + + // a operator o1 + else if Operator::is_operator(token) { + + // numeric group is done, push it. + if currently_processing_numeric_group { + output_queue.push(current_numeric_group); + current_numeric_group = Vec::new(); + currently_processing_numeric_group = false; + current_numeric_group_has_point = false; + } + + // (get the constant Operator (which is a struct) that fits to that token.) + let o1 = match Operator::get_operator(token) { + Some(valid_op) => valid_op, + None => {panic!("Operator '{}' not found.", token);}, + }; + + // while there is an operator o2 at the top of the stack + if !operator_stack.is_empty() { + dbg!(&operator_stack); + let o2 = match Operator::get_operator(*(operator_stack.clone().last().clone().unwrap())) { + Some(valid_op) => valid_op, + None => {panic!("Operator '{}' not found.", token);}, + }; + // and + // (o2 has greater precedence than o1 or (o1 and o2 have the same precedence and o1 + // is left associative)) + while ((operator_stack.last().is_some()) & ((o2.precedence > o1.precedence) | ((o1.precedence == o2.precedence) & (o1.associativity == Associativity::Left)))) { + // pop o2 from the operator stack into the output queue. + // after this debug statement, the operator_stack is empty for no reason!!!! + // FIXME + let my_c = match operator_stack.pop() { + Some(c) => c, + None => {panic!("weirdly gone!")}, + }; + output_queue.push(vec![my_c]); + } + } + operator_stack.push(o1.character); + } + /* + // Unnessecary, will be processed by the expression parser + else if '(' == token { + println!("("); + } + else if ')' == token { + println!(")"); + } + */ + else { + return Err(("Unrecognized token: '".to_string() + token.to_string().as_str() + "'").to_string()); + } + } + // numeric group is done, push it. + if currently_processing_numeric_group { + output_queue.push(current_numeric_group); + } + dbg!(&output_queue); + + // afterwards, process any operators still on the operator_stack + while !(operator_stack.is_empty()) { + output_queue.push(vec![operator_stack.pop().unwrap()]); + } + + dbg!(&output_queue); + let mut rpn: Vec = Vec::new(); + for group in output_queue { + rpn.push(group.iter().cloned().collect::()); + } + Ok(rpn) +} + +// after we have the rpn, we may want to calculate the values with it. +pub fn calc_reverse_polish_notation(rpn: Vec) -> Result { + Ok(0.0) +} diff --git a/src/main.rs b/src/main.rs index fc8e00d..c9d9782 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -use clap::{Parser, Subcommand}; +use clap::Parser; mod expression_parser; -mod linear_algebra; use expression_parser::Expression; use expression_parser::Task; @@ -10,25 +9,30 @@ use expression_parser::Task; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Arg { -// /// Optional subcommand -// #[command(subcommand)] -// command: Option, - - /// Show verbose output - #[arg(short, long)] - verbose: bool, - - /// An expression that should be used to calculate something + ///Syntax: '1 + task_param(inner) + 1 '{n} + ///{n} + ///Specify an expression, any expression may contain child expressions, which can be denoted{n} + ///with parenthesis '(child)'. Expressions may have a task applied to them, such as a + ///logarithm{n} or drawing a root. To apply a task to a expression, simply write the name of{n} + ///the task before denoting an expression: 'myTask(myExpression)'. You can apply a parameter{n} + ///to some tasks by using an underscore '_': 'myTask_myParameter(myExpression)'.{n} + ///{n} + ///List of Tasks:{n} + ///{n} + ///"none" explicitly set no task for expression{n} + /// parameter: none + ///{n} + ///"root" or "sqrt" draw the root of the expression{n} + /// parameter: draw n'th root of expression, default is 2.0{n} + ///{n} + ///"power" or "pow" or "sq" apply an exponent to the expression{n} + /// parameter: specify exponent n, default is 2.0{n} + ///{n} + ///"log" or "ln" apply a logarithm to the expression{n} + /// parameter: specify base n, default is 10{n} expressions: Vec, } -//#[derive(Subcommand)] -//enum Commands { -// /// Assert if two expressions are equal to each other -// Equal { -// } -//} - fn main() { let args = Arg::parse(); let mut expression_vec: Vec = Vec::new(); @@ -41,11 +45,21 @@ fn main() { expression_texts_concat.push(args.expressions.join(" ").trim().to_string()); for expression_text in expression_texts_concat { - expression_vec.push(Expression::new(expression_text, Task::None)); + expression_vec.push(Expression::new(expression_text.clone(), expression_text, Task::None, 0)); + } + #[cfg(debug_assertions)] + { + dbg!(&expression_vec); } - for expression in expression_vec { - expression.process(); + match expression.clone().process() { + Ok(result) => { + println!("{result}"); + }, + Err(err) => { + eprintln!("Could not calculate expression '{}': {}", &expression.text, err); + } + } } - + } diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..ae08274 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +13 + 2525 + sqrt(15 + log_10(100)) + power_10(10)