Skip to main content

cnvx_parse/
gmpl.rs

1use super::LanguageParser;
2use cnvx_core::{LinExpr, Model, Objective, VarId};
3
4#[derive(Default)]
5pub struct GMPLLanguage;
6
7impl GMPLLanguage {
8    pub fn new() -> Self {
9        Self {}
10    }
11}
12
13impl LanguageParser for GMPLLanguage {
14    fn parse(&self, src: &str) -> Result<Model, String> {
15        let mut model = Model::new();
16        let mut vars: Vec<VarId> = vec![];
17
18        for line in src.lines() {
19            let line = line.trim();
20            if line.is_empty() || line.starts_with('#') {
21                continue;
22            }
23
24            if line.starts_with("var ") {
25                let parts: Vec<_> = line.split_whitespace().collect();
26                if parts.len() >= 2 {
27                    let v = model.add_var().finish();
28                    vars.push(v);
29                }
30            } else if line.to_lowercase().starts_with("maximize") {
31                let after_colon = line.split(':').nth(1).unwrap_or("").trim();
32                let expr = parse_expression(after_colon, &vars)?;
33                model.add_objective(Objective::maximize(expr).name("Z"));
34            } else if line.to_lowercase().starts_with("subject to") {
35                let rest = line.split(':').nth(1).unwrap_or("").trim();
36                let (lhs, rhs, cmp) = parse_constraint(rest, &vars)?;
37                match cmp {
38                    "<=" => model += lhs.leq(rhs),
39                    ">=" => model += lhs.geq(rhs),
40                    "=" => model += lhs.eq(rhs),
41                    _ => return Err(format!("unknown constraint type '{}'", cmp)),
42                }
43            }
44        }
45
46        Ok(model)
47    }
48}
49
50fn parse_expression(expr: &str, vars: &[VarId]) -> Result<LinExpr, String> {
51    let mut le = LinExpr::constant(0.0);
52
53    for tok in expr.split('+') {
54        let tok = tok.trim();
55        if tok.is_empty() {
56            continue;
57        }
58
59        let (coef, varname) = if tok.contains('*') {
60            let parts: Vec<_> = tok.split('*').collect();
61            let coef = parts[0]
62                .parse::<f64>()
63                .map_err(|_| format!("invalid coefficient '{}'", parts[0]))?;
64            (coef, parts[1])
65        } else if let Some(rest) = tok.strip_prefix('-') {
66            (-1.0, rest)
67        } else {
68            (1.0, tok)
69        };
70
71        let varname = varname.trim().trim_end_matches(';');
72
73        let idx = varname[1..]
74            .parse::<usize>()
75            .map_err(|_| format!("invalid variable '{}'", varname))?;
76        if idx == 0 || idx > vars.len() {
77            return Err(format!("unknown variable '{}'", varname));
78        }
79        le += coef * vars[idx - 1];
80    }
81
82    Ok(le)
83}
84
85fn parse_constraint<'a>(
86    line: &'a str,
87    vars: &[VarId],
88) -> Result<(LinExpr, f64, &'a str), String> {
89    let cmp: &'a str = if line.contains("<=") {
90        "<="
91    } else if line.contains(">=") {
92        ">="
93    } else if line.contains('=') {
94        "="
95    } else {
96        return Err("invalid constraint".into());
97    };
98    let parts: Vec<&str> = line.split(cmp).collect();
99    if parts.len() != 2 {
100        return Err("invalid constraint format".into());
101    }
102
103    let lhs = parse_expression(parts[0].trim(), vars)?;
104    let rhs = parts[1]
105        .trim()
106        .trim_end_matches(';') // <-- remove trailing semicolon
107        .parse::<f64>()
108        .map_err(|_| "invalid RHS".to_string())?;
109
110    Ok((lhs, rhs, cmp))
111}