From 80f64149ad7d9ccd8f6503d1f44d77a73637e51e Mon Sep 17 00:00:00 2001 From: Tarun Bansal Date: Wed, 29 May 2024 19:01:43 +0530 Subject: [PATCH 1/3] Add support for parsing and analysing negative values --- README.md | 2 +- src/analyzer.rs | 132 ++++++++++++++++++++++++++++++- src/analyzer/analyze_rounding.rs | 12 +-- src/analyzer/ast.rs | 5 +- src/analyzer/simplify_expr.rs | 97 +++++++++++++++++++++++ src/parser/arithmetic.lalrpop | 15 ++-- src/parser/formula_config.rs | 6 ++ src/parser/input.rs | 2 +- src/printer/latex_generator.rs | 1 + 9 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 src/analyzer/simplify_expr.rs diff --git a/README.md b/README.md index d585cb4..4b1099d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ round_up: true less_than_one: ["a * b"] # optional greater_than_one: ["c"] # optional ``` -- `formula` contains the formula to be analyze +- `formula` contains the formula to be analyze. All the numeric literals and identifiers are parsed as positive values, use the `-` sign to include negative values in the formula - `round_up` determines if the result of the formula should round up or down - `less_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* - `greater_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* diff --git a/src/analyzer.rs b/src/analyzer.rs index 080c1fb..f5d8c2d 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,4 +1,5 @@ mod analyze_rounding; +mod simplify_expr; pub mod ast; use crate::parser::arithmetic; @@ -13,7 +14,134 @@ pub fn analyze(formula_config: &mut FormulaConfig) -> anyhow::Result> anyhow::anyhow!("Error occured while parsing the formula {}: {}", formula, e) })?; - analyze_rounding::analyze(&ast, formula_config.round_up, formula_config)?; + let simplified_ast = simplify_expr::simplify_sign(ast); - Ok(ast) + analyze_rounding::analyze(&simplified_ast, formula_config.round_up, formula_config)?; + + Ok(simplified_ast) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mul_up() { + let mut formula_config = FormulaConfig::new( + "a * b".to_string(), + true, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a *↑ b)"); + } + + #[test] + fn test_mul_down() { + let mut formula_config = FormulaConfig::new( + "a * b".to_string(), + false, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a *↓ b)"); + } + + #[test] + fn test_div_up() { + let mut formula_config = FormulaConfig::new( + "a / b".to_string(), + true, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a /↑ b)"); + } + + #[test] + fn test_div_down() { + let mut formula_config = FormulaConfig::new( + "a / b".to_string(), + false, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a /↓ b)"); + } + + #[test] + fn test_pow_greater_than_one_up() { + let mut formula_config = FormulaConfig::new( + "a ** (b * c)".to_string(), + true, + None, + Some(vec!["a".to_string()]) + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a ** (b *↑ c))"); + } + + #[test] + fn test_pow_less_than_one_up() { + let mut formula_config = FormulaConfig::new( + "a ** (b * c)".to_string(), + true, + Some(vec!["a".to_string()]), + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a ** (b *↓ c))"); + } + + #[test] + fn test_pow_greater_than_one_down() { + let mut formula_config = FormulaConfig::new( + "a ** (b * c)".to_string(), + false, + None, + Some(vec!["a".to_string()]) + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a ** (b *↓ c))"); + } + + #[test] + fn test_pow_less_than_one_down() { + let mut formula_config = FormulaConfig::new( + "a ** (b * c)".to_string(), + false, + Some(vec!["a".to_string()]), + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(a ** (b *↑ c))"); + } + + #[test] + fn test_negative() { + let mut formula_config = FormulaConfig::new( + "-(-(-a * b)) + c".to_string(), + true, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(c - (a *↓ b))"); + } + + #[test] + fn test_double_negative() { + let mut formula_config = FormulaConfig::new( + "-(-(a * b)) + c".to_string(), + true, + None, + None + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "((a *↑ b) + c)"); + } +} \ No newline at end of file diff --git a/src/analyzer/analyze_rounding.rs b/src/analyzer/analyze_rounding.rs index 1ed2f41..c1a5025 100644 --- a/src/analyzer/analyze_rounding.rs +++ b/src/analyzer/analyze_rounding.rs @@ -76,20 +76,10 @@ fn handle_pow( /// # Returns /// /// This function returns a `Result` object with an empty Ok value if the operation is successful. -/// -/// # Example -/// -/// ``` -/// use analyze_rounding::visit; -/// use FormulaConfig; -/// -/// let expr = Expr::Number(5); -/// let formula_config = FormulaConfig::new(); -/// visit(&expr, true, &formula_config); -/// ``` fn visit(expr: &Expr, rounding_direction: bool, formula_config: &mut FormulaConfig) -> Result<()> { match expr { Expr::Number(_) | Expr::Id(_) | Expr::Error => (), + Expr::Negative(nexpr) => visit(nexpr, !rounding_direction, formula_config)?, Expr::Op(left, op, right) => { let (left_rounding, right_rounding) = match op { Opcode::Add => (rounding_direction, rounding_direction), diff --git a/src/analyzer/ast.rs b/src/analyzer/ast.rs index 18a438c..ab7911b 100644 --- a/src/analyzer/ast.rs +++ b/src/analyzer/ast.rs @@ -7,9 +7,11 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Expr { /// A numeric literal. - Number(i32), + Number(u32), /// An identifier. Id(String), + /// An identifier for a negative number + Negative(Box), /// An operation with two operands. Op(Box, Opcode, Box), /// An error expression. @@ -63,6 +65,7 @@ impl Display for Expr { match &self { Expr::Number(n) => write!(fmt, "{n}"), Expr::Id(ref n) => write!(fmt, "{n}"), + Expr::Negative(ref n) => write!(fmt, "(-{n})"), Expr::Op(ref l, op, ref r) => write!(fmt, "({l} {op} {r})"), Expr::Error => write!(fmt, "error"), } diff --git a/src/analyzer/simplify_expr.rs b/src/analyzer/simplify_expr.rs new file mode 100644 index 0000000..66f0211 --- /dev/null +++ b/src/analyzer/simplify_expr.rs @@ -0,0 +1,97 @@ +use std::cell::RefCell; + +use super::ast::{Expr, Opcode, Rounding}; + +fn is_negative_value(expr: Box) -> (bool, Box) { + match *expr { + Expr::Negative(ne) => match *ne { + Expr::Negative(internal) => is_negative_value(internal), + _ => (true, ne), + } + // Need to handle this case separately because in case of nested negatives the + // inner most operation will not be simplified + Expr::Op(l, o, r) => { + let simplified_internal_expr = simplify_sign(Box::new(Expr::Op(l, o, r))); + + match *simplified_internal_expr { + Expr::Negative(ne) => (true, ne), + _ => (false, simplified_internal_expr), + } + }, + _ => (false, expr), + } +} + +fn simplify_add(lnv: (bool, Box), rnv: (bool, Box)) -> Box { + let expr = match (lnv.0, rnv.0) { + (true, false) => Expr::Op(rnv.1, Opcode::Sub, lnv.1), + (true, true) => Expr::Negative(Box::new(Expr::Op(lnv.1, Opcode::Add, rnv.1))), + (false, true) => Expr::Op(lnv.1, Opcode::Sub, rnv.1), + (false, false) => Expr::Op(lnv.1, Opcode::Add, rnv.1), + }; + + Box::new(expr) +} + +fn simplify_sub(lnv: (bool, Box), rnv: (bool, Box)) -> Box { + let expr = match (lnv.0, rnv.0) { + (true, false) => Expr::Negative(Box::new(Expr::Op(lnv.1, Opcode::Add, rnv.1))), + (true, true) => Expr::Op(rnv.1, Opcode::Sub, lnv.1), + (false, true) => Expr::Op(lnv.1, Opcode::Add, rnv.1), + (false, false) => Expr::Op(lnv.1, Opcode::Sub, rnv.1), + }; + + Box::new(expr) +} + +fn simplify_mul(lnv: (bool, Box), rnv: (bool, Box), r: RefCell) -> Box { + let expr = if lnv.0 ^ rnv.0 { + Expr::Negative(Box::new(Expr::Op(lnv.1, Opcode::Mul(RefCell::new(Rounding::Init)), rnv.1))) + } else { + Expr::Op(lnv.1, Opcode::Mul(r), rnv.1) + }; + + Box::new(expr) +} + +fn simplify_div(lnv: (bool, Box), rnv: (bool, Box), r: RefCell) -> Box { + let expr = if lnv.0 ^ rnv.0 { + Expr::Negative(Box::new(Expr::Op(lnv.1, Opcode::Div(RefCell::new(Rounding::Init)), rnv.1))) + } else { + Expr::Op(lnv.1, Opcode::Div(r), rnv.1) + }; + + Box::new(expr) +} + +/// Simplifies the signs to bring the negative sign from values to the operations +/// and finally bring it out to the expression level if possible +/// It also reagganges the addition and substration formula to make them look better +/// with the sign. For example (-a + b) will be re-arranged to (b - a) +pub fn simplify_sign(expr: Box) -> Box { + match *expr { + Expr::Op(left, op, right) => { + let simplified_left = match *left { + Expr::Op(..) => simplify_sign(left), + _ => left + }; + + let simplified_right = match *right { + Expr::Op(..) => simplify_sign(right), + _ => right + }; + + let lnv = is_negative_value(simplified_left); + let rnv = is_negative_value(simplified_right); + + match op { + Opcode::Add => simplify_add(lnv, rnv), + Opcode::Sub => simplify_sub(lnv, rnv), + Opcode::Mul(r) => simplify_mul(lnv, rnv, r), + Opcode::Div(r) => simplify_div(lnv, rnv, r), + Opcode::Pow => Box::new(Expr::Op(lnv.1, op, rnv.1)) + } + }, + _ => expr + } +} \ No newline at end of file diff --git a/src/parser/arithmetic.lalrpop b/src/parser/arithmetic.lalrpop index d3db3d4..e98fccf 100644 --- a/src/parser/arithmetic.lalrpop +++ b/src/parser/arithmetic.lalrpop @@ -6,15 +6,15 @@ use lalrpop_util::ParseError; grammar; -Tier: Box = { - Tier Op NextTier => Box::new(Expr::Op(<>)), +Tier: Box = { + Tier Op NextTier => Box::new(Expr::Op(<>)), NextTier }; pub Expr = Tier; Factor = Tier; -ExprOp: Opcode = { // (3) +ExprOp: Opcode = { "+" => Opcode::Add, "-" => Opcode::Sub, }; @@ -27,12 +27,17 @@ FactorOp: Opcode = { Term: Box = { Num => Box::new(Expr::Number(<>)), + "-" => Box::new(Expr::Negative(Box::new(Expr::Number(<>)))), + Id => Box::new(Expr::Id(<>)), + "-" => Box::new(Expr::Negative(Box::new(Expr::Id(<>)))), + + "-(" ")" => Box::new(Expr::Negative(<>)), "(" ")" }; -Num: i32 = { - r"[0-9]+" =>? i32::from_str(<>) +Num: u32 = { + r"[0-9]+" =>? u32::from_str(<>) .map_err(|_| ParseError::User { error: "number is too big" }) diff --git a/src/parser/formula_config.rs b/src/parser/formula_config.rs index af19273..f153d46 100644 --- a/src/parser/formula_config.rs +++ b/src/parser/formula_config.rs @@ -27,6 +27,12 @@ impl Default for FormulaConfig { } } +impl FormulaConfig { + pub fn new(formula: String, round_up: bool, less_than_one: Option>, greater_than_one: Option>) -> FormulaConfig { + FormulaConfig { formula, round_up, less_than_one, greater_than_one } + } +} + impl FormulaConfig { // Add a value to the `less_than_one` list pub fn add_less_than_one(&mut self, value: String) { diff --git a/src/parser/input.rs b/src/parser/input.rs index a7286bc..57c9c20 100644 --- a/src/parser/input.rs +++ b/src/parser/input.rs @@ -89,7 +89,7 @@ fn find_less_greater_than_one(expr: &Expr, formula_config: &mut FormulaConfig) { fn visit(expr: &Expr, formula_config: &mut FormulaConfig) { match expr { - Expr::Number(_) | Expr::Id(_) | Expr::Error => (), + Expr::Number(_) | Expr::Id(_) | Expr::Negative(_) | Expr::Error => (), Expr::Op(left, op, right) => { if let Opcode::Pow = op { // We ignore if the following fail diff --git a/src/printer/latex_generator.rs b/src/printer/latex_generator.rs index d7c6f40..f33a803 100644 --- a/src/printer/latex_generator.rs +++ b/src/printer/latex_generator.rs @@ -25,6 +25,7 @@ fn visit(expr: &Expr) -> String { match expr { Expr::Number(n) => n.to_string(), Expr::Id(n) => n.to_string(), + Expr::Negative(e) => format!("(-{e})"), Expr::Op(left, op, right) => { let left_str = visit(left); let right_str = visit(right); From 81a8bf2eabef52c1f50268bf7109a03a3c1f0366 Mon Sep 17 00:00:00 2001 From: Tarun Bansal Date: Thu, 30 May 2024 13:58:56 +0530 Subject: [PATCH 2/3] Handle exponenet in the pow operation --- README.md | 2 +- src/analyzer.rs | 14 ++++++++++++++ src/analyzer/simplify_expr.rs | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b1099d..aae3508 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ round_up: true less_than_one: ["a * b"] # optional greater_than_one: ["c"] # optional ``` -- `formula` contains the formula to be analyze. All the numeric literals and identifiers are parsed as positive values, use the `-` sign to include negative values in the formula +- `formula` contains the formula to be analyze. All the numeric literals and identifiers are parsed as positive values, use the `-` sign to include negative values in the formula. The negative sign of the base in the exponential operations are ignored. - `round_up` determines if the result of the formula should round up or down - `less_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* - `greater_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* diff --git a/src/analyzer.rs b/src/analyzer.rs index f5d8c2d..1c56f5a 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -14,7 +14,9 @@ pub fn analyze(formula_config: &mut FormulaConfig) -> anyhow::Result> anyhow::anyhow!("Error occured while parsing the formula {}: {}", formula, e) })?; + println!("parsed : {ast}"); let simplified_ast = simplify_expr::simplify_sign(ast); + println!("simplified: {simplified_ast}"); analyze_rounding::analyze(&simplified_ast, formula_config.round_up, formula_config)?; @@ -144,4 +146,16 @@ mod tests { let ast = analyze(&mut formula_config).unwrap().to_string(); assert_eq!(ast, "((a *↑ b) + c)"); } + + #[test] + fn test_negative_exponent() { + let mut formula_config = FormulaConfig::new( + "a ** (-b * c)".to_string(), + true, + None, + Some(vec!["a".to_string()]) + ); + let ast = analyze(&mut formula_config).unwrap().to_string(); + assert_eq!(ast, "(1 /↑ (a ** (b *↓ c)))"); + } } \ No newline at end of file diff --git a/src/analyzer/simplify_expr.rs b/src/analyzer/simplify_expr.rs index 66f0211..afb762a 100644 --- a/src/analyzer/simplify_expr.rs +++ b/src/analyzer/simplify_expr.rs @@ -64,6 +64,20 @@ fn simplify_div(lnv: (bool, Box), rnv: (bool, Box), r: RefCell), rnv: (bool, Box)) -> Box { + let expr = if rnv.0 { + Expr::Op( + Box::new(Expr::Number(1)), + Opcode::Div(RefCell::new(Rounding::Init)), + Box::new(Expr::Op(lnv.1, Opcode::Pow, rnv.1)) + ) + } else { + Expr::Op(lnv.1, Opcode::Pow, rnv.1) + }; + + Box::new(expr) +} + /// Simplifies the signs to bring the negative sign from values to the operations /// and finally bring it out to the expression level if possible /// It also reagganges the addition and substration formula to make them look better @@ -89,7 +103,7 @@ pub fn simplify_sign(expr: Box) -> Box { Opcode::Sub => simplify_sub(lnv, rnv), Opcode::Mul(r) => simplify_mul(lnv, rnv, r), Opcode::Div(r) => simplify_div(lnv, rnv, r), - Opcode::Pow => Box::new(Expr::Op(lnv.1, op, rnv.1)) + Opcode::Pow => simplify_pow(lnv, rnv), } }, _ => expr From fca4cca2f14bf350ee2e7660c1653b383eb1b0e8 Mon Sep 17 00:00:00 2001 From: Tarun Bansal Date: Fri, 31 May 2024 18:13:11 +0530 Subject: [PATCH 3/3] Remove prints and fix readme --- README.md | 2 +- src/analyzer.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index aae3508..9f930df 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ round_up: true less_than_one: ["a * b"] # optional greater_than_one: ["c"] # optional ``` -- `formula` contains the formula to be analyze. All the numeric literals and identifiers are parsed as positive values, use the `-` sign to include negative values in the formula. The negative sign of the base in the exponential operations are ignored. +- `formula` contains the formula to be analyzed. All the numeric literals and identifiers are parsed as positive values, use the `-` sign to include negative values in the formula. The negative sign of the base in the exponential operations are ignored. - `round_up` determines if the result of the formula should round up or down - `less_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* - `greater_than_one` is used for the `**` [rules](#rules) *(raw string comparison and sensible to space)* diff --git a/src/analyzer.rs b/src/analyzer.rs index 1c56f5a..107e2b8 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -14,9 +14,7 @@ pub fn analyze(formula_config: &mut FormulaConfig) -> anyhow::Result> anyhow::anyhow!("Error occured while parsing the formula {}: {}", formula, e) })?; - println!("parsed : {ast}"); let simplified_ast = simplify_expr::simplify_sign(ast); - println!("simplified: {simplified_ast}"); analyze_rounding::analyze(&simplified_ast, formula_config.round_up, formula_config)?;