use std::borrow::Cow;
use crate::lexer::{LexerError, RawCoefficient, RawConstraint, RawObjective};
use crate::model::ComparisonOp;
pub type SpannedElem<'input> = (usize, Elem<'input>);
#[derive(Debug, Clone, PartialEq)]
pub enum Elem<'input> {
Name(&'input str),
Var(&'input str),
Num(f64),
Plus,
Minus,
Op(ComparisonOp),
}
fn err(position: usize, message: impl Into<String>) -> LexerError {
LexerError { position, message: Some(message.into()) }
}
fn pos_at(elems: &[SpannedElem<'_>], i: usize) -> usize {
elems.get(i).or_else(|| elems.last()).map_or(0, |&(loc, _)| loc)
}
#[derive(Debug, Default)]
struct Segment<'input> {
coefficients: Vec<RawCoefficient<'input>>,
constant: f64,
}
fn parse_segment<'input>(elems: &[SpannedElem<'input>], mut i: usize) -> Result<(Segment<'input>, usize), LexerError> {
let mut segment = Segment::default();
let mut first = true;
loop {
let sign = match elems.get(i) {
Some((_, Elem::Plus)) => {
i += 1;
1.0
}
Some((_, Elem::Minus)) => {
i += 1;
-1.0
}
Some((_, Elem::Var(_) | Elem::Num(_))) if first => 1.0,
_ => return Ok((segment, i)),
};
match elems.get(i) {
Some(&(_, Elem::Var(name))) => {
segment.coefficients.push(RawCoefficient { name, value: sign });
i += 1;
}
Some(&(_, Elem::Num(value))) => match elems.get(i + 1) {
Some(&(_, Elem::Var(name))) => {
segment.coefficients.push(RawCoefficient { name, value: sign * value });
i += 2;
}
Some((loc, Elem::Num(_))) => {
return Err(err(*loc, "adjacent numeric literals; expected '+', '-', or a variable name"));
}
_ => {
segment.constant += sign * value;
i += 1;
}
},
Some((loc, Elem::Plus | Elem::Minus)) => return Err(err(*loc, "consecutive signs; expected a number or variable")),
_ => return Err(err(pos_at(elems, i), "dangling sign; expected a number or variable")),
}
first = false;
}
}
fn parse_signed_number(elems: &[SpannedElem<'_>], mut i: usize, context: &str) -> Result<(f64, usize), LexerError> {
let sign = match elems.get(i) {
Some((_, Elem::Plus)) => {
i += 1;
1.0
}
Some((_, Elem::Minus)) => {
i += 1;
-1.0
}
_ => 1.0,
};
match elems.get(i) {
Some(&(_, Elem::Num(value))) => Ok((sign * value, i + 1)),
_ => Err(err(pos_at(elems, i), format!("{context} must be a numeric value"))),
}
}
pub fn assemble_objectives<'input>(elems: &[SpannedElem<'input>]) -> Result<Vec<RawObjective<'input>>, LexerError> {
let mut objectives: Vec<RawObjective<'input>> = Vec::new();
let mut current: Option<RawObjective<'input>> = None;
let mut i = 0;
while i < elems.len() {
let (loc, ref elem) = elems[i];
if let Elem::Name(name) = elem {
if let Some(obj) = current.take() {
objectives.push(obj);
}
current = Some(RawObjective { name: Cow::Borrowed(name), coefficients: Vec::new(), constant: 0.0, byte_offset: Some(loc) });
i += 1;
} else {
let obj = current.get_or_insert_with(|| RawObjective {
name: Cow::Borrowed("__obj__"),
coefficients: Vec::new(),
constant: 0.0,
byte_offset: Some(loc),
});
let (segment, next) = parse_segment(elems, i)?;
debug_assert!(next > i, "parse_segment must consume at least one element here");
obj.coefficients.extend(segment.coefficients);
obj.constant += segment.constant;
if next < elems.len() && !matches!(elems[next].1, Elem::Name(_)) {
return Err(err(pos_at(elems, next), "expected '+', '-', or a new objective in the objective section"));
}
i = next;
}
}
if let Some(obj) = current.take() {
objectives.push(obj);
}
Ok(objectives)
}
const fn flip(op: ComparisonOp) -> ComparisonOp {
match op {
ComparisonOp::LT => ComparisonOp::GT,
ComparisonOp::LTE => ComparisonOp::GTE,
ComparisonOp::GT => ComparisonOp::LT,
ComparisonOp::GTE => ComparisonOp::LTE,
ComparisonOp::EQ => ComparisonOp::EQ,
}
}
pub fn assemble_constraints<'input>(elems: &[SpannedElem<'input>]) -> Result<Vec<RawConstraint<'input>>, LexerError> {
let mut constraints = Vec::new();
let mut i = 0;
while i < elems.len() {
let entry_loc = elems[i].0;
let name: Option<&'input str> = if let Elem::Name(n) = elems[i].1 {
i += 1;
Some(n)
} else {
None
};
let (lhs, next) = parse_segment(elems, i)?;
if next == i {
return Err(err(pos_at(elems, i), "expected an expression before the comparison operator"));
}
i = next;
let Some(&(_, Elem::Op(op1))) = elems.get(i) else {
return Err(err(pos_at(elems, i), "expected a comparison operator in constraint"));
};
i += 1;
if lhs.coefficients.is_empty() {
let (mid, next) = parse_segment(elems, i)?;
if next == i {
return Err(err(pos_at(elems, i), "expected an expression after the comparison operator"));
}
i = next;
if let Some(&(_, Elem::Op(op2))) = elems.get(i) {
i += 1;
let (rhs, next) = parse_signed_number(elems, i, "range bound")?;
i = next;
let lower_name: Cow<'input, str> = name.map_or(Cow::Borrowed("__c__"), Cow::Borrowed);
let upper_name: Cow<'input, str> = name.map_or(Cow::Borrowed("__c__"), |n| Cow::Owned(format!("{n}_rng")));
constraints.push(RawConstraint::Standard {
name: lower_name,
coefficients: mid.coefficients.clone(),
operator: flip(op1),
rhs: lhs.constant - mid.constant,
byte_offset: Some(entry_loc),
});
constraints.push(RawConstraint::Standard {
name: upper_name,
coefficients: mid.coefficients,
operator: op2,
rhs: rhs - mid.constant,
byte_offset: Some(entry_loc),
});
} else {
constraints.push(RawConstraint::Standard {
name: name.map_or(Cow::Borrowed("__c__"), Cow::Borrowed),
coefficients: mid.coefficients,
operator: flip(op1),
rhs: lhs.constant - mid.constant,
byte_offset: Some(entry_loc),
});
}
} else {
let (rhs, next) = parse_signed_number(elems, i, "constraint right-hand side")?;
i = next;
constraints.push(RawConstraint::Standard {
name: name.map_or(Cow::Borrowed("__c__"), Cow::Borrowed),
coefficients: lhs.coefficients,
operator: op1,
rhs: rhs - lhs.constant,
byte_offset: Some(entry_loc),
});
}
}
Ok(constraints)
}