Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Calc operator precedence #838

Merged
merged 5 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 97 additions & 39 deletions crates/torin/src/values/size.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::ops::Deref;
use std::{
ops::Deref,
slice::Iter,
};

pub use euclid::Rect;

Expand Down Expand Up @@ -70,7 +73,7 @@ impl Size {
Size::Pixels(px) => Some(px.get() + parent_margin),
Size::Percentage(per) => Some(parent_value / 100.0 * per.get()),
Size::DynamicCalculations(calculations) => {
Some(run_calculations(calculations.deref(), parent_value))
Some(run_calculations(calculations.deref(), parent_value).unwrap_or(0.0))
}
Size::Fill => Some(available_parent_value),
Size::FillMinimum => {
Expand Down Expand Up @@ -193,50 +196,105 @@ impl std::fmt::Display for DynamicCalculation {
}
}

/// Calculate some chained operations with a given value.
/// This value could be for example the width of a node's parent area.
pub fn run_calculations(calcs: &[DynamicCalculation], value: f32) -> f32 {
let mut prev_number: Option<f32> = None;
let mut prev_op: Option<DynamicCalculation> = None;

let mut calc_with_op = |val: f32, prev_op: Option<DynamicCalculation>| {
if let Some(op) = prev_op {
match op {
DynamicCalculation::Sub => {
prev_number = Some(prev_number.unwrap() - val);
}
DynamicCalculation::Add => {
prev_number = Some(prev_number.unwrap() + val);
}
DynamicCalculation::Mul => {
prev_number = Some(prev_number.unwrap() * val);
}
DynamicCalculation::Div => {
prev_number = Some(prev_number.unwrap() / val);
}
_ => {}
}
} else {
prev_number = Some(val);
/// [Operator-precedence parser](https://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method)
struct DynamicCalculationEvaluator<'a> {
calcs: Iter<'a, DynamicCalculation>,
parent_value: f32,
current: Option<&'a DynamicCalculation>,
}

impl<'a> DynamicCalculationEvaluator<'a> {
pub fn new(calcs: Iter<'a, DynamicCalculation>, parent_value: f32) -> Self {
Self {
calcs,
parent_value,
current: None,
}
};
}

pub fn evaluate(&mut self) -> Option<f32> {
// Parse and evaluate the expression
let value = self.parse_expression(0);

// Return the result if there are no more tokens
match self.current {
Some(_) => None,
None => value,
}
}

for calc in calcs {
match calc {
DynamicCalculation::Percentage(per) => {
let val = (value / 100.0 * per).round();
/// Parse and evaluate the expression with operator precedence and following grammar:
/// ```ebnf
/// expression = value, { operator, value } ;
/// operator = "+" | "-" | "*" | "/" ;
/// ```
fn parse_expression(&mut self, min_precedence: usize) -> Option<f32> {
// Parse left-hand side value
self.current = self.calcs.next();
let mut lhs = self.parse_value()?;

calc_with_op(val, prev_op);
while let Some(operator_precedence) = self.operator_precedence() {
// Return if minimal precedence is reached.
if operator_precedence < min_precedence {
return Some(lhs);
}

// Save operator to apply after parsing right-hand side value.
let operator = self.current?;

// Parse right-hand side value.
//
// Next precedence is the current precedence + 1
// because all operators are left associative.
let rhs = self.parse_expression(operator_precedence + 1)?;

// Apply operator
match operator {
DynamicCalculation::Add => lhs += rhs,
DynamicCalculation::Sub => lhs -= rhs,
DynamicCalculation::Mul => lhs *= rhs,
DynamicCalculation::Div => lhs /= rhs,
// Precedence will return None for other tokens
// and loop will break if it's not an operator
_ => unreachable!(),
}
}

Some(lhs)
}

prev_op = None;
/// Parse and evaluate the value with the following grammar:
/// ```ebnf
/// value = percentage | pixels ;
/// percentage = number, "%" ;
/// pixels = number ;
/// ```
fn parse_value(&mut self) -> Option<f32> {
match self.current? {
DynamicCalculation::Percentage(value) => {
self.current = self.calcs.next();
Some((self.parent_value / 100.0 * value).round())
}
DynamicCalculation::Pixels(val) => {
calc_with_op(*val, prev_op);
prev_op = None;
DynamicCalculation::Pixels(value) => {
self.current = self.calcs.next();
Some(*value)
}
_ => prev_op = Some(*calc),
_ => None,
}
}

prev_number.unwrap()
/// Get the precedence of the operator if current token is an operator or None otherwise.
fn operator_precedence(&self) -> Option<usize> {
match self.current? {
DynamicCalculation::Add | DynamicCalculation::Sub => Some(1),
DynamicCalculation::Mul | DynamicCalculation::Div => Some(2),
_ => None,
}
}
}

/// Calculate dynamic expression with operator precedence.
/// This value could be for example the width of a node's parent area.
pub fn run_calculations(calcs: &[DynamicCalculation], value: f32) -> Option<f32> {
DynamicCalculationEvaluator::new(calcs.iter(), value).evaluate()
}
89 changes: 89 additions & 0 deletions crates/torin/tests/size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,92 @@ pub fn inner_percentage() {
Rect::new(Point2D::new(0.0, 300.0), Size2D::new(100.0, 100.0)),
);
}

#[test]
pub fn test_calc() {
const PARENT_VALUE: f32 = 500.0;

assert_eq!(
run_calculations(&vec![DynamicCalculation::Pixels(10.0)], PARENT_VALUE),
Some(10.0)
);

assert_eq!(
run_calculations(&vec![DynamicCalculation::Percentage(87.5)], PARENT_VALUE),
Some((87.5 / 100.0 * PARENT_VALUE).round())
);

assert_eq!(
run_calculations(
&vec![
DynamicCalculation::Pixels(10.0),
DynamicCalculation::Add,
DynamicCalculation::Pixels(20.0),
DynamicCalculation::Mul,
DynamicCalculation::Percentage(50.0),
],
PARENT_VALUE
),
Some(10.0 + 20.0 * (50.0 / 100.0 * PARENT_VALUE).round())
);

assert_eq!(
run_calculations(
&vec![
DynamicCalculation::Pixels(10.0),
DynamicCalculation::Add,
DynamicCalculation::Percentage(10.0),
DynamicCalculation::Add,
DynamicCalculation::Pixels(30.0),
DynamicCalculation::Mul,
DynamicCalculation::Pixels(10.0),
DynamicCalculation::Add,
DynamicCalculation::Pixels(75.0),
DynamicCalculation::Mul,
DynamicCalculation::Pixels(2.0),
],
PARENT_VALUE
),
Some(10.0 + (10.0 / 100.0 * PARENT_VALUE).round() + 30.0 * 10.0 + 75.0 * 2.0)
);

assert_eq!(
run_calculations(
&vec![
DynamicCalculation::Pixels(10.0),
DynamicCalculation::Pixels(20.0),
],
PARENT_VALUE
),
None
);

assert_eq!(
run_calculations(
&vec![DynamicCalculation::Pixels(10.0), DynamicCalculation::Add],
PARENT_VALUE
),
None
);

assert_eq!(
run_calculations(
&vec![DynamicCalculation::Add, DynamicCalculation::Pixels(10.0)],
PARENT_VALUE
),
None
);

assert_eq!(
run_calculations(
&vec![
DynamicCalculation::Pixels(10.0),
DynamicCalculation::Add,
DynamicCalculation::Add,
DynamicCalculation::Pixels(10.0)
],
PARENT_VALUE
),
None
);
}
Loading