#[derive(Debug, Clone, PartialEq)]
pub enum OperandValue {
Tag(TagPath),
Literal(String),
Expression(Expression),
}
#[derive(Debug, Clone, PartialEq)]
pub struct TagPath {
pub base: String,
pub full_path: String,
pub indices: Vec<OperandValue>,
}
impl TagPath {
pub fn simple(name: impl Into<String>) -> Self {
let name = name.into();
Self {
base: name.clone(),
full_path: name,
indices: Vec::new(),
}
}
pub fn new(base: impl Into<String>, full_path: impl Into<String>) -> Self {
Self {
base: base.into(),
full_path: full_path.into(),
indices: Vec::new(),
}
}
pub fn with_indices(base: impl Into<String>, full_path: impl Into<String>, indices: Vec<OperandValue>) -> Self {
Self {
base: base.into(),
full_path: full_path.into(),
indices,
}
}
pub fn all_tags(&self) -> Vec<String> {
let mut tags = vec![self.base.clone()];
for idx in &self.indices {
tags.extend(idx.all_tags());
}
tags
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Expression {
pub text: String,
pub terms: Vec<OperandValue>,
}
impl Expression {
pub fn new(text: impl Into<String>, terms: Vec<OperandValue>) -> Self {
Self {
text: text.into(),
terms,
}
}
}
impl OperandValue {
pub fn all_tags(&self) -> Vec<String> {
match self {
OperandValue::Tag(path) => path.all_tags(),
OperandValue::Literal(_) => Vec::new(),
OperandValue::Expression(expr) => {
let mut tags = Vec::new();
for term in &expr.terms {
tags.extend(term.all_tags());
}
tags
}
}
}
pub fn base_tag(&self) -> Option<&str> {
match self {
OperandValue::Tag(path) => Some(&path.base),
_ => None,
}
}
}
pub fn parse_operand_value(input: &str) -> OperandValue {
let trimmed = input.trim();
if trimmed.is_empty() {
return OperandValue::Literal(String::new());
}
if is_numeric_literal(trimmed) {
return OperandValue::Literal(trimmed.to_string());
}
if looks_like_expression(trimmed) {
return parse_expression(trimmed);
}
parse_tag_path(trimmed)
}
fn is_numeric_literal(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.starts_with("16#") || s.starts_with("8#") || s.starts_with("2#") {
return true;
}
let s = s.strip_prefix('-').unwrap_or(s);
let s = s.strip_prefix('+').unwrap_or(s);
if s.is_empty() {
return false;
}
let first = s.chars().next().unwrap();
if !first.is_ascii_digit() {
return false;
}
s.chars().all(|c| {
c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-' || c == '_'
})
}
fn looks_like_expression(s: &str) -> bool {
let mut paren_depth = 0;
let mut bracket_depth = 0;
let chars: Vec<char> = s.chars().collect();
let mut seen_term = false;
for &c in chars.iter() {
match c {
'(' => paren_depth += 1,
')' => paren_depth -= 1,
'[' => bracket_depth += 1,
']' => bracket_depth -= 1,
'+' | '*' | '/' | '>' | '<' | '=' => {
if paren_depth == 0 && bracket_depth == 0 {
return true;
}
}
'-' => {
if paren_depth == 0 && bracket_depth == 0 && seen_term {
return true;
}
}
' ' | '\t' | '\n' => {
}
_ => {
if paren_depth == 0 && bracket_depth == 0 {
seen_term = true;
}
}
}
}
false
}
fn parse_tag_path(input: &str) -> OperandValue {
let mut chars = input.chars().peekable();
let mut base = String::new();
let mut indices = Vec::new();
while let Some(&c) = chars.peek() {
if c == '.' || c == '[' || c == ':' {
break;
}
base.push(c);
chars.next();
}
while let Some(&c) = chars.peek() {
if c == ':' {
chars.next(); while let Some(&nc) = chars.peek() {
if nc == ':' || nc == '.' || nc == '[' {
break;
}
chars.next();
}
} else {
break;
}
}
while let Some(&c) = chars.peek() {
if c == '[' {
chars.next(); let idx_str = collect_until_balanced(&mut chars, '[', ']');
let trimmed = idx_str.trim();
if !trimmed.is_empty() {
let first_char = trimmed.chars().next().unwrap();
if first_char.is_ascii_alphabetic() || first_char == '_' {
let idx_value = parse_operand_value(trimmed);
indices.push(idx_value);
}
}
} else if c == '.' {
chars.next(); if let Some(&nc) = chars.peek() {
if nc == '[' {
chars.next(); let idx_str = collect_until_balanced(&mut chars, '[', ']');
let idx_value = parse_operand_value(&idx_str);
indices.push(idx_value);
}
}
} else {
break;
}
}
OperandValue::Tag(TagPath::with_indices(base, input.to_string(), indices))
}
fn parse_expression(input: &str) -> OperandValue {
let mut terms = Vec::new();
extract_terms_from_expression(input, &mut terms);
OperandValue::Expression(Expression::new(input, terms))
}
fn extract_terms_from_expression(input: &str, terms: &mut Vec<OperandValue>) {
let mut current = String::new();
let mut paren_depth = 0;
let mut bracket_depth = 0;
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'(' => {
paren_depth += 1;
current.push(c);
}
')' => {
paren_depth -= 1;
current.push(c);
}
'[' => {
bracket_depth += 1;
current.push(c);
}
']' => {
bracket_depth -= 1;
current.push(c);
}
'+' | '*' | '/' | '>' | '<' | '=' => {
if paren_depth == 0 && bracket_depth == 0 {
process_term(¤t, terms);
current.clear();
} else {
current.push(c);
}
}
'-' => {
if paren_depth == 0 && bracket_depth == 0 && !current.trim().is_empty() {
process_term(¤t, terms);
current.clear();
} else {
current.push(c);
}
}
' ' => {
if paren_depth == 0 && bracket_depth == 0 {
} else {
current.push(c);
}
}
_ => {
current.push(c);
}
}
i += 1;
}
process_term(¤t, terms);
}
fn process_term(term: &str, terms: &mut Vec<OperandValue>) {
let trimmed = term.trim();
if trimmed.is_empty() {
return;
}
if let Some(paren_pos) = trimmed.find('(') {
let func_name = &trimmed[..paren_pos];
if is_known_function(func_name) {
if trimmed.ends_with(')') {
let args = &trimmed[paren_pos + 1..trimmed.len() - 1];
for arg in split_args(args) {
let arg_trimmed = arg.trim();
if !arg_trimmed.is_empty() {
let value = parse_operand_value(arg_trimmed);
match &value {
OperandValue::Literal(_) => {}
OperandValue::Tag(_) => terms.push(value),
OperandValue::Expression(e) => terms.extend(e.terms.clone()),
}
}
}
}
return;
}
}
let stripped = strip_outer_parens(trimmed);
if looks_like_expression(stripped) {
extract_terms_from_expression(stripped, terms);
} else {
let value = parse_operand_value(stripped);
match &value {
OperandValue::Literal(_) => {} OperandValue::Tag(_) => terms.push(value),
OperandValue::Expression(e) => terms.extend(e.terms.clone()),
}
}
}
fn is_known_function(name: &str) -> bool {
matches!(name.to_uppercase().as_str(),
"ABS" | "SQRT" | "LN" | "LOG" | "EXP" |
"SIN" | "COS" | "TAN" | "ASN" | "ACS" | "ATN" |
"DEG" | "RAD" | "TRUNC" | "NOT" | "AND" | "OR" | "XOR" |
"MOD" | "FRD" | "TOD"
)
}
fn split_args(args: &str) -> Vec<&str> {
let mut result = Vec::new();
let mut start = 0;
let mut paren_depth = 0;
let mut bracket_depth = 0;
for (i, c) in args.char_indices() {
match c {
'(' => paren_depth += 1,
')' => paren_depth -= 1,
'[' => bracket_depth += 1,
']' => bracket_depth -= 1,
',' if paren_depth == 0 && bracket_depth == 0 => {
result.push(&args[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < args.len() {
result.push(&args[start..]);
}
result
}
fn strip_outer_parens(s: &str) -> &str {
let trimmed = s.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
let inner = &trimmed[1..trimmed.len()-1];
let mut depth = 0;
for c in inner.chars() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth < 0 {
return trimmed; }
}
_ => {}
}
}
if depth == 0 {
return strip_outer_parens(inner); }
}
trimmed
}
fn collect_until_balanced(chars: &mut std::iter::Peekable<std::str::Chars>, open: char, close: char) -> String {
let mut result = String::new();
let mut depth = 1;
while let Some(&c) = chars.peek() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
chars.next(); break;
}
}
result.push(c);
chars.next();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_tag() {
let val = parse_operand_value("Motor");
assert_eq!(val.all_tags(), vec!["Motor"]);
}
#[test]
fn test_parse_structured_tag() {
let val = parse_operand_value("Timer1.DN");
assert_eq!(val.all_tags(), vec!["Timer1"]);
if let OperandValue::Tag(path) = val {
assert_eq!(path.base, "Timer1");
assert_eq!(path.full_path, "Timer1.DN");
} else {
panic!("Expected Tag");
}
}
#[test]
fn test_parse_io_tag() {
let val = parse_operand_value("Local:1:I.Data.0");
assert_eq!(val.all_tags(), vec!["Local"]);
if let OperandValue::Tag(path) = val {
assert_eq!(path.base, "Local");
assert_eq!(path.full_path, "Local:1:I.Data.0");
} else {
panic!("Expected Tag");
}
}
#[test]
fn test_parse_flexio_tag() {
let val = parse_operand_value("FlexIO:3:I.Pt01.Data");
assert_eq!(val.all_tags(), vec!["FlexIO"]);
}
#[test]
fn test_parse_array_literal_index() {
let val = parse_operand_value("Array[0]");
assert_eq!(val.all_tags(), vec!["Array"]);
}
#[test]
fn test_parse_array_tag_index() {
let val = parse_operand_value("Array[idx]");
let tags = val.all_tags();
assert!(tags.contains(&"Array".to_string()));
assert!(tags.contains(&"idx".to_string()));
}
#[test]
fn test_parse_numeric_literal() {
let val = parse_operand_value("123.456");
assert!(matches!(val, OperandValue::Literal(_)));
assert!(val.all_tags().is_empty());
}
#[test]
fn test_parse_hex_literal() {
let val = parse_operand_value("16#FF00");
assert!(matches!(val, OperandValue::Literal(_)));
assert!(val.all_tags().is_empty());
}
#[test]
fn test_parse_negative_literal() {
let val = parse_operand_value("-2147483648");
assert!(matches!(val, OperandValue::Literal(_)));
assert!(val.all_tags().is_empty());
}
#[test]
fn test_parse_expression() {
let val = parse_operand_value("((1.0 - x) * y) + z");
let tags = val.all_tags();
assert!(tags.contains(&"x".to_string()), "Expected 'x' in {:?}", tags);
assert!(tags.contains(&"y".to_string()), "Expected 'y' in {:?}", tags);
assert!(tags.contains(&"z".to_string()), "Expected 'z' in {:?}", tags);
assert_eq!(tags.len(), 3);
}
#[test]
fn test_parse_expression_with_structured_tags() {
let val = parse_operand_value("Timer1.ACC / Timer1.PRE * 100");
let tags = val.all_tags();
assert!(tags.iter().all(|t| t == "Timer1"));
}
#[test]
fn test_parse_complex_expression() {
let val = parse_operand_value("((SP_In[10]-SP_In[9])*SRun_Tmr[10].ACC/SRun_Tmr[10].PRE)+SP_In[9]");
let tags = val.all_tags();
assert!(tags.contains(&"SP_In".to_string()));
assert!(tags.contains(&"SRun_Tmr".to_string()));
}
#[test]
fn test_parse_aoi_first_operand() {
let val = parse_operand_value("A_URNG");
assert_eq!(val.all_tags(), vec!["A_URNG"]);
}
#[test]
fn test_negative_in_expression() {
let val = parse_operand_value("-2.0");
assert!(matches!(val, OperandValue::Literal(_)));
}
#[test]
fn test_expression_with_negative() {
let val = parse_operand_value("x * -2.0");
let tags = val.all_tags();
assert_eq!(tags, vec!["x"]);
}
#[test]
fn test_indirect_addressing() {
let val = parse_operand_value("SimpleDint.[TestTag.IntMember]");
let tags = val.all_tags();
assert!(tags.contains(&"SimpleDint".to_string()), "Expected SimpleDint in {:?}", tags);
assert!(tags.contains(&"TestTag".to_string()), "Expected TestTag in {:?}", tags);
}
#[test]
fn test_multi_dimensional_array() {
let val = parse_operand_value("MultiDimArray[1,3].Member");
let tags = val.all_tags();
assert_eq!(tags, vec!["MultiDimArray"]);
}
#[test]
fn test_atn_function_in_expression() {
let val = parse_operand_value("ATN(_Test) > 1.0");
let tags = val.all_tags();
assert!(tags.contains(&"_Test".to_string()), "Expected _Test in {:?}", tags);
}
#[test]
fn test_sin_function_in_expression() {
let val = parse_operand_value("SIN(Angle) + COS(Angle)");
let tags = val.all_tags();
assert!(tags.contains(&"Angle".to_string()), "Expected Angle in {:?}", tags);
}
#[test]
fn test_complex_cmp_expression() {
let val = parse_operand_value("ATN(_Test) > 1.0");
let tags = val.all_tags();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0], "_Test");
}
}