1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum ObjectiveType {
#[default]
Minimize,
Maximize,
}
#[derive(Debug)]
pub enum BoundType {
Equality,
LessThanOrEqual,
GreaterThanOrEqual,
}
#[derive(Debug)]
pub struct Bound {
_name: String,
pub lhs: (Vec<(f64, String)>, f64), // Coefficient and variable pairs for the left-hand side
pub rhs: (Vec<(f64, String)>, f64), // Right-hand side constant
pub bound_type: BoundType,
}
#[derive(Debug, Default, Clone)]
pub struct Objective {
pub objective_type: ObjectiveType,
pub terms: (Vec<(f64, String)>, f64), // Coefficient and variable pairs
}
#[derive(Debug)]
pub struct ModelParser {
pub objective: Objective,
pub bounds: Vec<Bound>,
pub binary_variables: Vec<String>,
}
impl ModelParser {
pub fn new() -> Self {
ModelParser {
objective: Objective::default(),
bounds: Vec::new(),
binary_variables: Vec::new(),
}
}
// Parses the text input into the ModelParser structure
pub fn parse(&mut self, text: &str) {
let mut lines = text.lines();
let mut mode = "";
let mut current_objective_terms = Vec::new();
let mut constant = 0.0;
while let Some(line) = lines.next() {
let line = line.trim();
if line.is_empty() {
continue;
} else if line.starts_with("Minimize") || line.starts_with("Maximize") {
mode = "objective";
let objective_type = if line.starts_with("Minimize") {
ObjectiveType::Minimize
} else {
ObjectiveType::Maximize
};
self.objective = Objective {
objective_type,
terms: (Vec::new(), 0.0),
};
} else if line.starts_with("Subject To") {
mode = "constraints";
// Store any objective terms collected so far
self.objective.terms = (current_objective_terms.clone(), constant);
} else if line.starts_with("Binary") {
mode = "binary";
} else if line.starts_with("End") {
break;
} else {
match mode {
"objective" => {
// Collects terms for the objective function across multiple lines
let obj_line = line
.split_whitespace()
.skip(1)
.collect::<Vec<&str>>()
.join(" ");
let expr = self.parse_expression(&obj_line);
current_objective_terms.extend(expr.0);
constant = expr.1;
}
"constraints" => {
// Parses each constraint line and adds it to the constraints vector
self.bounds.push(self.parse_constraint(line));
}
"binary" => {
// Adds each binary variable name to the binary_variables vector
self.binary_variables.push(line.to_string());
}
_ => {}
}
}
}
// Ensure any remaining terms for the objective are stored if they were parsed
self.objective.terms = (current_objective_terms, constant);
}
fn parse_constraint(&self, line: &str) -> Bound {
let name_split: Vec<&str> = line.split(':').collect();
let _name = name_split[0].trim().to_string();
let rest = name_split[1].trim();
let (lhs, bound_type, rhs) = if rest.contains("<=") {
let sides: Vec<&str> = rest.split("<=").collect();
(
self.parse_expression(sides[0]),
BoundType::LessThanOrEqual,
self.parse_expression(sides[1]),
)
} else if rest.contains(">=") {
let sides: Vec<&str> = rest.split(">=").collect();
(
self.parse_expression(sides[0]),
BoundType::GreaterThanOrEqual,
self.parse_expression(sides[1]),
)
} else if rest.contains("=") {
let sides: Vec<&str> = rest.split('=').collect();
(
self.parse_expression(sides[0]),
BoundType::Equality,
self.parse_expression(sides[1]),
)
} else {
panic!("Unrecognized constraint format");
};
Bound {
_name,
lhs,
rhs,
bound_type,
}
}
fn parse_expression(&self, expr: &str) -> (Vec<(f64, String)>, f64) {
let mut terms = Vec::new();
let mut constant = 0.0;
let mut current_sign = 1.0;
let mut buffer = String::new();
for (i, c) in expr.chars().enumerate() {
match c {
' ' => {
if !buffer.is_empty() {
self.parse_and_add_term(
&mut buffer,
&mut terms,
&mut constant,
current_sign,
);
buffer.clear();
}
}
'+' => {
if !buffer.is_empty() {
self.parse_and_add_term(
&mut buffer,
&mut terms,
&mut constant,
current_sign,
);
buffer.clear();
}
current_sign = 1.0;
}
'-' => {
if !buffer.is_empty() {
self.parse_and_add_term(
&mut buffer,
&mut terms,
&mut constant,
current_sign,
);
buffer.clear();
}
current_sign = -1.0;
}
_ => buffer.push(c), // Accumulate characters into buffer
}
// If it's the last character, parse the remaining buffer
if i == expr.len() - 1 && !buffer.is_empty() {
self.parse_and_add_term(&mut buffer, &mut terms, &mut constant, current_sign);
}
}
(terms, constant)
}
// Helper function to parse buffer content as either a constant or variable term
fn parse_and_add_term(
&self,
buffer: &mut String,
terms: &mut Vec<(f64, String)>,
constant: &mut f64,
sign: f64,
) {
// Check if buffer is empty (nothing to parse)
if buffer.is_empty() {
return;
}
// Case 1: If the buffer is purely numeric, treat it as a constant
if buffer.chars().all(|c| c.is_digit(10) || c == '.') {
*constant += sign * buffer.parse::<f64>().unwrap();
// Case 2: If buffer starts with a digit and contains letters, it's a variable with a coefficient
} else if buffer.chars().next().unwrap().is_digit(10) {
let mut coefficient_str = String::new();
// Extract leading numeric characters as the coefficient until we reach a letter
while let Some(c) = buffer.chars().next() {
if c.is_alphabetic() {
break;
}
coefficient_str.push(buffer.remove(0)); // Add to coefficient and remove from buffer
}
// Parse coefficient and add term
let coefficient = sign * coefficient_str.parse::<f64>().unwrap();
terms.push((coefficient, buffer.clone())); // Remainder of buffer is the variable name
// Case 3: If buffer starts with a letter, treat it as a variable with implicit coefficient 1
} else {
terms.push((sign, buffer.clone()));
}
// Clear buffer after parsing
buffer.clear();
}
}