1use crate::base::split_areas;
2
3use super::{
4 CMD, CellRef, DEFAULT_EVALS, DefinedName, EngineError, Json, MAX_CONSTRAINTS, MAX_EVALS, MAX_POPULATION, MAX_VARS, MIN_POPULATION, RangeRef,
5 Result, Session, Workbook, bad, bool_param, f64_param, json, split_sheet, str_param,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
11pub(super) enum Rel {
12 Le,
13 Eq,
14 Ge,
15 Int,
16 Bin,
17}
18
19impl Rel {
20 fn parse(s: &str) -> Option<Rel> {
21 match s.trim().to_ascii_lowercase().as_str() {
22 "<=" | "≤" | "=<" | "le" => Some(Rel::Le),
23 ">=" | "≥" | "=>" | "ge" => Some(Rel::Ge),
24 "=" | "==" | "eq" => Some(Rel::Eq),
25 "int" | "integer" => Some(Rel::Int),
26 "bin" | "binary" => Some(Rel::Bin),
27 _ => None,
28 }
29 }
30 pub(super) fn text(self) -> &'static str {
31 match self {
32 Rel::Le => "<=",
33 Rel::Eq => "=",
34 Rel::Ge => ">=",
35 Rel::Int => "int",
36 Rel::Bin => "bin",
37 }
38 }
39 fn code(self) -> u8 {
41 match self {
42 Rel::Le => 1,
43 Rel::Eq => 2,
44 Rel::Ge => 3,
45 Rel::Int => 4,
46 Rel::Bin => 5,
47 }
48 }
49 fn from_code(c: &str) -> Option<Rel> {
50 match c.trim() {
51 "1" => Some(Rel::Le),
52 "2" => Some(Rel::Eq),
53 "3" => Some(Rel::Ge),
54 "4" => Some(Rel::Int),
55 "5" => Some(Rel::Bin),
56 _ => None,
57 }
58 }
59}
60
61#[derive(Clone, Debug)]
62pub(super) enum Rhs {
63 None,
64 Const(f64),
65 Cells(Vec<CellRef>),
66}
67
68#[derive(Clone, Debug)]
69pub(super) struct Constraint {
70 pub(super) lhs: Vec<CellRef>,
71 lhs_text: String,
72 pub(super) rel: Rel,
73 pub(super) rhs: Rhs,
74 rhs_text: String,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub(super) enum Goal {
79 Max,
80 Min,
81 Value(f64),
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub(super) enum Method {
86 Simplex,
87 Grg,
88 Evolutionary,
89}
90
91#[derive(Clone, Copy, Debug, PartialEq)]
94pub(super) struct EvoOptions {
95 pub(super) population: usize,
96 pub(super) mutation: f64,
97 pub(super) seed: u64,
98 pub(super) convergence: f64,
99 pub(super) stall: u64,
100 pub(super) require_bounds: bool,
101}
102
103impl Default for EvoOptions {
104 fn default() -> EvoOptions {
105 EvoOptions { population: 100, mutation: 0.075, seed: 0, convergence: 1e-4, stall: 50, require_bounds: true }
106 }
107}
108
109fn evo_options(p: &Json) -> EvoOptions {
110 let d = EvoOptions::default();
111 let num = |k: &str| p.get(k).and_then(Json::as_f64).or_else(|| str_param(p, k).and_then(num_text)).filter(|v| v.is_finite());
112 EvoOptions {
113 population: num("populationSize").map_or(d.population, |v| v.clamp(MIN_POPULATION as f64, MAX_POPULATION as f64) as usize),
114 mutation: num("mutationRate").map_or(d.mutation, |v| v.clamp(0.0, 1.0)),
115 seed: num("randomSeed").map_or(d.seed, |v| v.clamp(0.0, f64::from(u32::MAX)) as u64),
116 convergence: num("convergence").map_or(d.convergence, |v| v.clamp(0.0, 1.0)),
117 stall: num("generationsWithoutImprovement").map_or(d.stall, |v| v.clamp(1.0, 100_000.0) as u64),
118 require_bounds: bool_param(p, "requireBounds").unwrap_or(d.require_bounds),
119 }
120}
121
122#[derive(Clone, Debug)]
123pub(super) struct Model {
124 pub(super) sheet: usize,
125 pub(super) objective: CellRef,
126 pub(super) goal: Goal,
127 pub(super) vars: Vec<CellRef>,
128 vars_text: String,
129 pub(super) cons: Vec<Constraint>,
130 pub(super) non_negative: bool,
131 pub(super) method: Method,
132 pub(super) evo: EvoOptions,
133 pub(super) max_evals: u64,
135 pub(super) gap: f64,
137}
138
139fn cells_of(wb: &Workbook, t: &str, default_sheet: usize, cap: usize) -> Result<(usize, Vec<CellRef>)> {
141 let mut sheet = None;
142 let mut out = Vec::new();
143 for part in split_areas(t).into_iter().map(str::trim).filter(|x| !x.is_empty()) {
145 let (sh, body) = split_sheet(part);
146 let si = match sh {
147 Some(n) => wb.sheet_index(&n).ok_or_else(|| bad(CMD, format!("no sheet named `{n}`")))?,
148 None => default_sheet,
149 };
150 if *sheet.get_or_insert(si) != si {
151 return Err(bad(CMD, "all cells of a reference must be on one sheet"));
152 }
153 let r = RangeRef::parse(body.trim()).ok_or_else(|| bad(CMD, format!("not a reference: `{part}`")))?;
154 if r.count() > cap as u64 || out.len() + r.count() as usize > cap {
155 return Err(bad(CMD, format!("too many cells (at most {cap})")));
156 }
157 out.extend(r.iter());
158 }
159 if out.is_empty() {
160 return Err(bad(CMD, format!("not a reference: `{t}`")));
161 }
162 Ok((sheet.unwrap_or(default_sheet), out))
163}
164
165fn num_text(t: &str) -> Option<f64> {
166 crate::base::parse::parse_number_text(t.trim()).filter(|v| v.is_finite())
167}
168
169fn split_unquoted<'a>(t: &'a str, pat: &str) -> Option<(&'a str, &'a str)> {
173 let mut quoted = false;
174 for (i, c) in t.char_indices() {
175 if c == '\'' {
176 quoted = !quoted;
177 } else if !quoted && t.get(i..).is_some_and(|rest| rest.starts_with(pat)) {
178 return Some((t.get(..i)?, t.get(i.saturating_add(pat.len())..)?));
179 }
180 }
181 None
182}
183
184fn split_constraint(t: &str) -> Option<(String, Rel, String)> {
187 for op in ["<=", ">=", "=<", "=>", "≤", "≥", "=="] {
188 if let Some((a, b)) = split_unquoted(t, op) {
189 return Some((a.trim().into(), Rel::parse(op)?, b.trim().into()));
190 }
191 }
192 if let Some((a, b)) = split_unquoted(t, "=") {
193 return Some((a.trim().into(), Rel::Eq, b.trim().into()));
194 }
195 let lower = t.trim().to_ascii_lowercase();
196 for (suffix, rel) in [("integer", Rel::Int), ("binary", Rel::Bin), ("int", Rel::Int), ("bin", Rel::Bin)] {
197 if let Some(a) = lower.strip_suffix(suffix) {
198 let a = a.trim();
199 return Some((t.trim().get(..a.len())?.trim().to_string(), rel, String::new()));
200 }
201 }
202 None
203}
204
205fn parse_constraint(wb: &Workbook, sheet: usize, c: &Json) -> Result<Constraint> {
206 let (lhs_text, rel, rhs_json) = match c {
207 Json::String(t) => {
208 let (l, r, rhs) = split_constraint(t).ok_or_else(|| bad(CMD, format!("not a constraint: `{t}`")))?;
209 (l, r, Json::String(rhs))
210 }
211 Json::Object(_) => {
212 let lhs = str_param(c, "lhs").or_else(|| str_param(c, "cell")).or_else(|| str_param(c, "range")).unwrap_or("").to_string();
213 let op = str_param(c, "op").or_else(|| str_param(c, "relation")).unwrap_or("");
214 let rel = Rel::parse(op).ok_or_else(|| bad(CMD, format!("unknown constraint relation `{op}`")))?;
215 (lhs, rel, c.get("rhs").or_else(|| c.get("value")).cloned().unwrap_or(Json::Null))
216 }
217 _ => return Err(bad(CMD, "a constraint is a string like \"B5 <= 10\" or {lhs, op, rhs}")),
218 };
219 let (lsi, lhs) = cells_of(wb, &lhs_text, sheet, MAX_CONSTRAINTS)?;
220 if lsi != sheet {
221 return Err(EngineError::Other("Constraint cells must be on the objective cell's sheet.".into()));
222 }
223 let (rhs, rhs_text) = match rel {
224 Rel::Int | Rel::Bin => (Rhs::None, String::new()),
225 _ => match &rhs_json {
226 Json::Number(n) => {
227 let v = n.as_f64().filter(|v| v.is_finite()).ok_or_else(|| bad(CMD, "the constraint's right side must be a number"))?;
228 (Rhs::Const(v), v.to_string())
229 }
230 Json::String(t) => {
231 let t = t.trim().trim_start_matches('=').trim();
232 if let Some(v) = num_text(t) {
233 (Rhs::Const(v), t.to_string())
234 } else {
235 let (rsi, cells) = cells_of(wb, t, sheet, MAX_CONSTRAINTS)?;
236 if rsi != sheet {
237 return Err(EngineError::Other("Constraint cells must be on the objective cell's sheet.".into()));
238 }
239 if cells.len() != 1 && cells.len() != lhs.len() {
240 return Err(EngineError::Other("The constraint's two sides must be the same size (or the right side one cell).".into()));
241 }
242 (Rhs::Cells(cells), t.to_string())
243 }
244 }
245 _ => return Err(bad(CMD, "missing the constraint's right side")),
246 },
247 };
248 Ok(Constraint { lhs, lhs_text, rel, rhs, rhs_text })
249}
250
251pub(super) fn parse_model(s: &Session, p: &Json) -> Result<Model> {
252 let d = s.doc()?;
253 let wb = &d.wb;
254 let obj_text = str_param(p, "objective").or_else(|| str_param(p, "setObjective")).or_else(|| str_param(p, "set")).unwrap_or("");
255 let (sheet, obj) = cells_of(wb, obj_text, wb.active_sheet(), 1).map_err(|_| EngineError::Other("The objective must be a single cell.".into()))?;
256 let objective = *obj.first().ok_or_else(|| EngineError::Other("The objective must be a single cell.".into()))?;
257 let sh = wb.sheet(sheet).ok_or(EngineError::NoDocument)?;
258 if sh.cell(objective).is_none_or(|c| c.formula.is_none()) {
259 return Err(EngineError::Other("The objective cell must contain a formula.".into()));
260 }
261 let goal = match str_param(p, "goal").or_else(|| str_param(p, "to")).unwrap_or("max").to_ascii_lowercase().as_str() {
262 "max" | "maximize" | "maximum" => Goal::Max,
263 "min" | "minimize" | "minimum" => Goal::Min,
264 "value" | "valueof" | "value of" => {
265 let v = f64_param(p, "value")
266 .or_else(|| str_param(p, "value").and_then(num_text))
267 .filter(|v| v.is_finite())
268 .ok_or_else(|| bad(CMD, "goal `value` needs a number in `value`"))?;
269 Goal::Value(v)
270 }
271 g => return Err(bad(CMD, format!("unknown goal `{g}` (max, min or value)"))),
272 };
273 let vars_text = str_param(p, "variables").or_else(|| str_param(p, "changing")).or_else(|| str_param(p, "byChanging")).unwrap_or("").to_string();
274 let (vsi, mut vars) = cells_of(wb, &vars_text, sheet, MAX_VARS).map_err(|e| match e {
275 EngineError::BadParams { msg, .. } if msg.starts_with("too many") => {
276 EngineError::Other(format!("Too many variable cells (at most {MAX_VARS})."))
277 }
278 _ => EngineError::Other("The variable cells must be a reference like B2:B4.".into()),
279 })?;
280 if vsi != sheet {
281 return Err(EngineError::Other("The variable cells must be on the objective cell's sheet.".into()));
282 }
283 let mut seen = std::collections::HashSet::new();
284 vars.retain(|c| seen.insert(*c));
285 if vars.iter().any(|c| sh.cell(*c).is_some_and(|c| c.formula.is_some() || c.py.is_some())) {
287 return Err(EngineError::Other("Variable cells must hold constants, not formulas.".into()));
288 }
289 let mut cons = Vec::new();
290 let mut scalar = 0usize;
291 if let Some(list) = p.get("constraints") {
292 let list = list.as_array().ok_or_else(|| bad(CMD, "`constraints` must be an array"))?;
293 if list.len() > MAX_CONSTRAINTS {
294 return Err(EngineError::Other(format!("Too many constraints (at most {MAX_CONSTRAINTS}).")));
295 }
296 for c in list {
297 let c = parse_constraint(wb, sheet, c)?;
298 scalar = scalar.saturating_add(c.lhs.len());
299 if scalar > MAX_CONSTRAINTS {
300 return Err(EngineError::Other(format!("Too many constraints (at most {MAX_CONSTRAINTS} cells).")));
301 }
302 if matches!(c.rel, Rel::Int | Rel::Bin) && c.lhs.iter().any(|x| !vars.contains(x)) {
303 return Err(EngineError::Other("Integer and binary constraints must be on variable cells.".into()));
304 }
305 cons.push(c);
306 }
307 }
308 let non_negative = bool_param(p, "nonNegative").unwrap_or(true);
309 let method = match str_param(p, "method").unwrap_or("grg").to_ascii_lowercase().replace([' ', '_', '-'], "").as_str() {
310 "grg" | "grgnonlinear" | "nonlinear" => Method::Grg,
311 "simplex" | "simplexlp" | "lp" | "linear" => Method::Simplex,
312 "evolutionary" | "evo" | "genetic" => Method::Evolutionary,
313 m => return Err(bad(CMD, format!("unknown method `{m}` (grg, simplex or evolutionary)"))),
314 };
315 let num = |k: &str| p.get(k).and_then(Json::as_f64).or_else(|| str_param(p, k).and_then(num_text)).filter(|v| v.is_finite());
316 let max_evals = num("maxEvaluations").filter(|v| *v >= 1.0).map_or(DEFAULT_EVALS, |v| (v as u64).min(MAX_EVALS));
317 let percent = p
320 .get("integerOptimality")
321 .and_then(Json::as_f64)
322 .or_else(|| str_param(p, "integerOptimality").and_then(|t| num_text(t.trim().trim_end_matches('%'))))
323 .filter(|v| v.is_finite());
324 let gap = percent.map_or(0.0, |v| v.clamp(0.0, 100.0) / 100.0);
325 Ok(Model { sheet, objective, goal, vars, vars_text, cons, non_negative, method, evo: evo_options(p), max_evals, gap })
326}
327
328pub fn sensitivity_status(status: &str) -> bool {
332 matches!(status, "optimal" | "converged")
333}
334
335pub fn sensitivity_applies(p: &Json) -> bool {
339 let method = str_param(p, "method").unwrap_or("grg").to_ascii_lowercase().replace([' ', '_', '-'], "");
340 let goal = str_param(p, "goal").or_else(|| str_param(p, "to")).unwrap_or("max").to_ascii_lowercase();
341 let integer = p.get("constraints").and_then(Json::as_array).is_some_and(|list| {
342 list.iter().any(|c| {
343 let rel = match c {
344 Json::String(t) => split_constraint(t).map(|(_, rel, _)| rel),
345 Json::Object(_) => str_param(c, "op").or_else(|| str_param(c, "relation")).and_then(Rel::parse),
346 _ => None,
347 };
348 matches!(rel, Some(Rel::Int | Rel::Bin))
349 })
350 });
351 !integer && !matches!(method.as_str(), "evolutionary" | "evo" | "genetic") && !matches!(goal.as_str(), "value" | "valueof" | "value of")
352}
353
354fn abs_text(sheet_name: Option<&str>, cells: &str) -> String {
358 let q = sheet_name.map(|n| format!("{}!", crate::engine::pivot::quote_sheet(n))).unwrap_or_default();
359 split_areas(cells)
360 .into_iter()
361 .map(str::trim)
362 .filter(|x| !x.is_empty())
363 .filter_map(|part| {
364 let r = RangeRef::parse(split_sheet(part).1.trim())?;
365 let abs = |c: CellRef| format!("${}${}", crate::base::col_to_letters(c.col), c.row as u64 + 1);
366 Some(if r.is_single() { format!("{q}{}", abs(r.start)) } else { format!("{q}{}:{}", abs(r.start), abs(r.end)) })
367 })
368 .collect::<Vec<_>>()
369 .join(",")
370}
371
372fn ref_text(sheet_name: &str, cells: &str) -> String {
373 abs_text(Some(sheet_name), cells)
374}
375
376const EVALS_NAME: &str = "gridcraft_solver_evals";
381const STALL_NAME: &str = "gridcraft_solver_stall";
382
383const SOLVER_KEYS: [&str; 28] = [
386 "adj", "cvg", "drv", "eng", "est", "itr", "lin", "mip", "mni", "mrt", "msl", "neg", "nod", "num", "nwt", "opt", "pre", "rbv", "rlx", "rsd",
387 "scl", "sho", "ssz", "tim", "tol", "typ", "val", "ver",
388];
389
390pub(super) fn is_model_name(name: &str) -> bool {
393 let name = name.to_ascii_lowercase();
394 if name == EVALS_NAME || name == STALL_NAME {
395 return true;
396 }
397 let Some(key) = name.strip_prefix("solver_") else { return false };
398 if SOLVER_KEYS.contains(&key) {
399 return true;
400 }
401 ["lhs", "rel", "rhs"].iter().filter_map(|p| key.strip_prefix(p)).any(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
403}
404
405pub(super) fn save_model(wb: &mut Workbook, m: &Model) {
407 let Some(sheet_name) = wb.sheet(m.sheet).map(|s| s.name.clone()) else { return };
408 wb.names.retain(|n| !(n.scope == Some(m.sheet) && is_model_name(&n.name)));
409 let mut put = |name: String, formula: String| {
410 wb.names.push(DefinedName { name, scope: Some(m.sheet), formula, comment: String::new(), hidden: true });
411 };
412 put("solver_opt".into(), ref_text(&sheet_name, &m.objective.a1()));
413 let (typ, val) = match m.goal {
414 Goal::Max => (1, 0.0),
415 Goal::Min => (2, 0.0),
416 Goal::Value(v) => (3, v),
417 };
418 put("solver_typ".into(), typ.to_string());
419 put("solver_val".into(), val.to_string());
420 put("solver_adj".into(), ref_text(&sheet_name, &m.vars_text));
421 for (i, c) in m.cons.iter().enumerate() {
422 let k = i + 1;
423 put(format!("solver_lhs{k}"), ref_text(&sheet_name, &c.lhs_text));
424 put(format!("solver_rel{k}"), c.rel.code().to_string());
425 let rhs = match &c.rhs {
426 Rhs::None => {
427 if c.rel == Rel::Bin {
428 "\"binary\"".to_string()
429 } else {
430 "\"integer\"".to_string()
431 }
432 }
433 Rhs::Const(v) => v.to_string(),
434 Rhs::Cells(_) => ref_text(&sheet_name, &c.rhs_text),
435 };
436 put(format!("solver_rhs{k}"), rhs);
437 }
438 put("solver_num".into(), m.cons.len().to_string());
439 put("solver_neg".into(), if m.non_negative { "1" } else { "2" }.into());
440 let eng = match m.method {
441 Method::Grg => "1",
442 Method::Simplex => "2",
443 Method::Evolutionary => "3",
444 };
445 put("solver_eng".into(), eng.into());
446 put(EVALS_NAME.into(), m.max_evals.to_string());
447 put("solver_tol".into(), m.gap.to_string());
448 put("solver_ssz".into(), m.evo.population.to_string());
451 put("solver_mrt".into(), m.evo.mutation.to_string());
452 put("solver_rsd".into(), m.evo.seed.to_string());
453 put("solver_rbv".into(), if m.evo.require_bounds { "1" } else { "2" }.into());
454 put("solver_cvg".into(), m.evo.convergence.to_string());
455 put(STALL_NAME.into(), m.evo.stall.to_string());
456 put("solver_ver".into(), "3".into());
457}
458
459pub fn saved_model(wb: &Workbook, sheet: usize) -> Option<Json> {
465 let named = |n: &str, scope: Option<usize>| wb.names.iter().find(|d| d.name.eq_ignore_ascii_case(n) && d.scope == scope);
466 let scope = if named("solver_opt", Some(sheet)).is_some() { Some(sheet) } else { None };
467 let get = |n: &str| named(n, scope).map(|d| d.formula.trim().trim_start_matches('=').to_string());
468 let local = |t: String| local_refs(&t);
469 let objective = local(get("solver_opt")?);
470 if objective.trim_matches('"').trim().is_empty() {
471 return None;
472 }
473 let goal = match get("solver_typ").as_deref().map(str::trim) {
474 Some("2") => "min",
475 Some("3") => "value",
476 _ => "max",
477 };
478 let value = get("solver_val").and_then(|v| num_text(&v)).unwrap_or(0.0);
479 let count = get("solver_num").and_then(|v| num_text(&v)).unwrap_or(0.0).clamp(0.0, MAX_CONSTRAINTS as f64) as usize;
480 let mut constraints = Vec::new();
481 for k in 1..=count {
482 let (Some(lhs), Some(rel)) = (get(&format!("solver_lhs{k}")), get(&format!("solver_rel{k}")).and_then(|r| Rel::from_code(&r))) else {
483 continue;
484 };
485 let lhs = local(lhs);
486 constraints.push(match rel {
487 Rel::Int | Rel::Bin => json!(format!("{lhs} {}", rel.text())),
488 _ => {
489 let rhs = get(&format!("solver_rhs{k}")).unwrap_or_default();
490 let rhs = if num_text(&rhs).is_some() { rhs } else { local(rhs) };
491 json!(format!("{lhs} {} {rhs}", rel.text()))
492 }
493 });
494 }
495 let method = match get("solver_eng").as_deref().map(str::trim) {
496 Some("2") => "simplex",
497 Some("3") => "evolutionary",
498 _ => "grg",
499 };
500 let mut model = json!({
501 "objective": objective,
502 "goal": goal,
503 "value": value,
504 "variables": get("solver_adj").map(local).unwrap_or_default(),
505 "constraints": constraints,
506 "nonNegative": get("solver_neg").as_deref().map(str::trim) != Some("2"),
507 "method": method,
508 });
509 for (name, key) in [
510 ("solver_ssz", "populationSize"),
511 ("solver_mrt", "mutationRate"),
512 ("solver_rsd", "randomSeed"),
513 ("solver_cvg", "convergence"),
514 (STALL_NAME, "generationsWithoutImprovement"),
515 (EVALS_NAME, "maxEvaluations"),
516 ] {
517 if let Some(v) = get(name).and_then(|v| num_text(&v)) {
518 model[key] = number(v);
519 }
520 }
521 if let Some(v) = get("solver_tol").and_then(|v| num_text(&v)) {
522 model["integerOptimality"] = number(percent(v));
523 }
524 if let Some(v) = get("solver_rbv") {
525 model["requireBounds"] = json!(v.trim() != "2");
526 }
527 Some(model)
528}
529
530pub(super) fn model_params(wb: &Workbook, m: &Model) -> Json {
533 let sheet = wb.sheet(m.sheet).map(|s| crate::engine::pivot::quote_sheet(&s.name)).unwrap_or_default();
534 let (goal, value) = match m.goal {
535 Goal::Max => ("max", 0.0),
536 Goal::Min => ("min", 0.0),
537 Goal::Value(v) => ("value", v),
538 };
539 let constraints: Vec<Json> = m
540 .cons
541 .iter()
542 .map(|c| match c.rel {
543 Rel::Int | Rel::Bin => json!(format!("{} {}", c.lhs_text, c.rel.text())),
544 _ => json!(format!("{} {} {}", c.lhs_text, c.rel.text(), c.rhs_text)),
545 })
546 .collect();
547 let method = match m.method {
548 Method::Simplex => "simplex",
549 Method::Grg => "grg",
550 Method::Evolutionary => "evolutionary",
551 };
552 let mut out = json!({
553 "objective": format!("{sheet}!{}", m.objective.a1()),
554 "goal": goal,
555 "value": value,
556 "variables": m.vars_text,
557 "constraints": constraints,
558 "nonNegative": m.non_negative,
559 "method": method,
560 "maxEvaluations": m.max_evals,
561 "integerOptimality": number(percent(m.gap)),
562 });
563 let e = &m.evo;
564 out["populationSize"] = json!(e.population);
565 out["mutationRate"] = json!(e.mutation);
566 out["randomSeed"] = json!(e.seed);
567 out["convergence"] = json!(e.convergence);
568 out["generationsWithoutImprovement"] = json!(e.stall);
569 out["requireBounds"] = json!(e.require_bounds);
570 out
571}
572
573pub(super) fn model_formulas(m: &Model) -> Result<Vec<String>> {
577 let one_area = |t: &str| split_areas(t).into_iter().filter(|a| !a.trim().is_empty()).count() <= 1;
579 let mut rows = Vec::with_capacity(m.cons.len() + 3);
580 let obj = abs_text(None, &m.objective.a1());
581 rows.push(match m.goal {
582 Goal::Max => format!("=MAX({obj})"),
583 Goal::Min => format!("=MIN({obj})"),
584 Goal::Value(v) => format!("={obj}={}", crate::base::number_to_text(v)),
585 });
586 rows.push(format!("=COUNT({})", abs_text(None, &m.vars_text)));
587 for c in &m.cons {
588 if !one_area(&c.lhs_text) || (matches!(c.rhs, Rhs::Cells(_)) && !one_area(&c.rhs_text)) {
590 return Err(EngineError::Other("Save Model needs each side of a constraint to be one range.".into()));
591 }
592 let lhs = abs_text(None, &c.lhs_text);
593 rows.push(match (c.rel, &c.rhs) {
594 (Rel::Int, _) => format!("=AND({lhs}=INT({lhs}))"),
595 (Rel::Bin, _) => format!("=AND(({lhs}=0)+({lhs}=1))"),
596 (rel, Rhs::Cells(_)) => format!("=AND({lhs}{}{})", rel.text(), abs_text(None, &c.rhs_text)),
597 (rel, Rhs::Const(v)) => format!("=AND({lhs}{}{})", rel.text(), crate::base::number_to_text(*v)),
598 (rel, Rhs::None) => format!("=AND({lhs}{}0)", rel.text()),
599 });
600 }
601 let e = &m.evo;
602 let n = crate::base::number_to_text;
603 let eng = match m.method {
604 Method::Grg => 1,
605 Method::Simplex => 2,
606 Method::Evolutionary => 3,
607 };
608 let yes = |b: bool| if b { 1 } else { 2 };
609 rows.push(format!(
610 "={{{eng},{},{},{},{},{},{},{},{},{}}}",
611 yes(m.non_negative),
612 e.population,
613 n(e.mutation),
614 e.seed,
615 yes(e.require_bounds),
616 n(e.convergence),
617 e.stall,
618 m.max_evals,
619 n(percent(m.gap))
620 ));
621 Ok(rows)
622}
623
624fn call_args(t: &str, name: &str) -> Option<String> {
626 let head = t.get(..name.len() + 1)?;
627 (head.eq_ignore_ascii_case(&format!("{name}(")) && t.ends_with(')')).then(|| t.get(name.len() + 1..t.len() - 1).map(str::to_string))?
628}
629
630fn local_refs(t: &str) -> String {
632 split_areas(t).into_iter().map(|p| split_sheet(p.trim()).1.replace('$', "")).collect::<Vec<_>>().join(",")
633}
634
635fn not_a_model() -> EngineError {
636 EngineError::Other("The range doesn't hold a Solver model saved with Save Model.".into())
637}
638
639const OPTIONS_FIELDS: usize = 8;
643
644fn read_options_row(model: &mut Json, list: &str) -> Result<()> {
648 let incomplete = || {
649 EngineError::Other(format!(
650 "The model's options row doesn't hold the {OPTIONS_FIELDS} or more numbers Save Model writes, so the range isn't a complete model saved with Save Model."
651 ))
652 };
653 let mut v: Vec<f64> = Vec::with_capacity(16);
656 let mut count = 0usize;
657 for t in list.split([',', ';']) {
658 let x = num_text(t).ok_or_else(incomplete)?;
659 count = count.saturating_add(1);
660 if v.len() < 16 {
661 v.push(x);
662 }
663 }
664 if count < OPTIONS_FIELDS {
665 return Err(incomplete());
666 }
667 let get = |k: usize| v.get(k).copied();
668 model["method"] = json!(match get(0).map(|x| x as i64) {
669 Some(2) => "simplex",
670 Some(3) => "evolutionary",
671 _ => "grg",
672 });
673 model["nonNegative"] = json!(get(1) != Some(2.0));
674 for (k, key) in [
675 (2, "populationSize"),
676 (3, "mutationRate"),
677 (4, "randomSeed"),
678 (6, "convergence"),
679 (7, "generationsWithoutImprovement"),
680 (8, "maxEvaluations"),
681 (9, "integerOptimality"),
682 ] {
683 if let Some(x) = get(k) {
684 model[key] = json!(x);
685 }
686 }
687 model["requireBounds"] = json!(get(5) != Some(2.0));
688 Ok(())
689}
690
691fn read_constraint_row(t: String) -> Result<String> {
693 let t = call_args(&t, "AND").unwrap_or(t);
694 if let Some((x, y)) = split_unquoted(&t, "=INT(").or_else(|| split_unquoted(&t, "=int(")) {
695 if y.strip_suffix(')') != Some(x) {
696 return Err(not_a_model());
697 }
698 return Ok(format!("{} int", local_refs(x)));
699 }
700 if let Some(x) = t.strip_prefix('(').and_then(|x| x.strip_suffix("=1)")).and_then(|x| split_unquoted(x, "=0)+(")).filter(|(a, b)| a == b) {
701 return Ok(format!("{} bin", local_refs(x.0)));
702 }
703 let (l, rel, r) = split_constraint(&t).ok_or_else(not_a_model)?;
704 let r = r.trim_matches('"');
705 Ok(match r.to_ascii_lowercase().as_str() {
706 "integer" => format!("{} int", local_refs(&l)),
707 "binary" => format!("{} bin", local_refs(&l)),
708 _ if num_text(r).is_some() => format!("{} {} {r}", local_refs(&l), rel.text()),
709 _ => format!("{} {} {}", local_refs(&l), rel.text(), local_refs(r)),
710 })
711}
712
713pub(super) fn parse_model_formulas(rows: &[String]) -> Result<Json> {
715 let body = |t: &str| t.trim().trim_start_matches('=').replace(' ', "");
716 let first = body(rows.first().ok_or_else(not_a_model)?);
717 let (goal, objective, value) = if let Some(o) = call_args(&first, "MAX") {
718 ("max", o, 0.0)
719 } else if let Some(o) = call_args(&first, "MIN") {
720 ("min", o, 0.0)
721 } else {
722 let (o, v) = split_unquoted(&first, "=").ok_or_else(not_a_model)?;
723 ("value", o.to_string(), num_text(v).ok_or_else(not_a_model)?)
724 };
725 let variables = call_args(&body(rows.get(1).ok_or_else(not_a_model)?), "COUNT").ok_or_else(not_a_model)?;
726 let mut model = json!({"objective": local_refs(&objective), "goal": goal, "value": value, "variables": local_refs(&variables)});
727 let mut constraints = Vec::new();
728 let rest = rows.get(2..).unwrap_or(&[]);
729 for (i, row) in rest.iter().enumerate() {
730 let t = body(row);
731 if let Some(list) = t.strip_prefix('{').and_then(|x| x.strip_suffix('}')) {
732 if i + 1 != rest.len() {
734 return Err(not_a_model());
735 }
736 read_options_row(&mut model, list)?;
737 break;
738 }
739 constraints.push(json!(read_constraint_row(t)?));
740 }
741 model["constraints"] = json!(constraints);
742 Ok(model)
743}
744
745fn percent(fraction: f64) -> f64 {
748 let p = fraction * 100.0;
749 let rounded = (p * 1e10).round() / 1e10;
750 if rounded.is_finite() { rounded } else { p }
751}
752
753fn number(v: f64) -> Json {
756 if v.fract() == 0.0 && v.abs() < 9_007_199_254_740_992.0 { json!(v as i64) } else { json!(v) }
758}