Skip to content

Commit

Permalink
feat: Adding Binary, Generals and Integer parsing fn
Browse files Browse the repository at this point in the history
refactor: Updating the infinity implementation
refactor: Adding Nom helper functions
  • Loading branch information
dandxy89 committed Dec 31, 2024
1 parent 649bc51 commit bdd95d8
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 44 deletions.
19 changes: 13 additions & 6 deletions src/nom/decoder/number.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case},
bytes::complete::{tag, tag_no_case, take},
character::complete::{char, digit1, multispace0, one_of},
combinator::{all_consuming, complete, map, opt, recognize, value},
combinator::{complete, eof, map, opt, peek, recognize, value, verify},
error::ErrorKind,
sequence::{pair, preceded, tuple},
Err, IResult,
Expand All @@ -12,10 +12,17 @@ use crate::nom::model::ComparisonOp;

#[inline]
fn infinity(input: &str) -> IResult<&str, f64> {
all_consuming(map(tuple((opt(one_of("+-")), alt((tag_no_case("infinity"), tag_no_case("inf"))))), |(sign, _)| match sign {
Some('-') => f64::NEG_INFINITY,
_ => f64::INFINITY,
}))(input)
map(
tuple((
opt(one_of("+-")),
alt((tag_no_case("infinity"), tag_no_case("inf"))),
peek(alt((eof, verify(take(1_usize), |c: &str| !c.chars().next().unwrap().is_alphanumeric())))),
)),
|(sign, _, _)| match sign {
Some('-') => f64::NEG_INFINITY,
_ => f64::INFINITY,
},
)(input)
}

#[inline]
Expand Down
141 changes: 139 additions & 2 deletions src/nom/decoder/variable.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,145 @@
use nom::{bytes::complete::take_while1, IResult};
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while1},
character::complete::{char, multispace0, space0},
combinator::{map, opt},
error::ErrorKind,
multi::many0,
sequence::{preceded, tuple},
IResult,
};

use crate::nom::decoder::is_valid_lp_char;
use crate::nom::{
decoder::{is_valid_lp_char, number::parse_num_value},
model::VariableType,
};

const SECTION_HEADERS: [&str; 12] =
["integers", "integer", "general", "generals", "gen", "binaries", "binary", "bin", "bounds", "bound", "sos", "end"];

#[inline]
pub fn parse_variable(input: &str) -> IResult<&str, &str> {
take_while1(is_valid_lp_char)(input)
}

#[inline]
pub fn single_bound(input: &str) -> IResult<&str, (&str, VariableType)> {
preceded(
multispace0,
alt((
// Free variable: `x1 free`
map(tuple((parse_variable, preceded(space0, tag_no_case("free")))), |(var_name, _)| (var_name, VariableType::Free)),
// Double bound: `0 <= x1 <= 5`
map(
tuple((
parse_num_value,
preceded(space0, alt((tag("<="), tag("<")))),
preceded(space0, parse_variable),
preceded(space0, alt((tag("<="), tag("<")))),
preceded(space0, parse_num_value),
)),
|(lower, _, var_name, _, upper)| (var_name, VariableType::DoubleBound(lower, upper)),
),
// Lower bound: `x1 >= 5` or `5 <= x1`
alt((
map(tuple((parse_variable, preceded(space0, tag(">=")), preceded(space0, parse_num_value))), |(var_name, _, bound)| {
(var_name, VariableType::LowerBound(bound))
}),
map(tuple((parse_num_value, preceded(space0, tag("<=")), preceded(space0, parse_variable))), |(bound, _, var_name)| {
(var_name, VariableType::LowerBound(bound))
}),
)),
// Upper bound: `x1 <= 5` or `5 >= x1`
alt((
map(tuple((parse_variable, preceded(space0, tag("<=")), preceded(space0, parse_num_value))), |(var_name, _, bound)| {
(var_name, VariableType::UpperBound(bound))
}),
map(tuple((parse_num_value, preceded(space0, tag(">=")), preceded(space0, parse_variable))), |(bound, _, var_name)| {
(var_name, VariableType::UpperBound(bound))
}),
)),
)),
)(input)
}

#[inline]
pub fn parse_bounds_section(input: &str) -> IResult<&str, Vec<(&str, VariableType)>> {
preceded(tuple((multispace0, tag_no_case("bounds"), opt(preceded(space0, char(':'))), multispace0)), many0(single_bound))(input)
}

#[inline]
fn is_section_header(input: &str) -> bool {
let lower_input = input.trim().to_lowercase();
SECTION_HEADERS.iter().any(|&header| lower_input.starts_with(header))
}

#[inline]
fn variable_not_header(input: &str) -> IResult<&str, &str> {
let (input, _) = multispace0(input)?;
if is_section_header(input) {
return Err(nom::Err::Error(nom::error::Error::new(input, ErrorKind::Not)));
}
parse_variable(input)
}

#[inline]
pub fn parse_variable_list(input: &str) -> IResult<&str, Vec<&str>> {
many0(variable_not_header)(input)
}

#[inline]
pub fn parse_generals_section(input: &str) -> IResult<&str, Vec<&str>> {
if input.is_empty() || input == "\n" {
return Ok(("", vec![]));
}
preceded(
tuple((multispace0, alt((tag_no_case("generals"), tag_no_case("general"))), opt(preceded(space0, char(':'))), multispace0)),
parse_variable_list,
)(input)
}

#[inline]
pub fn parse_integer_section(input: &str) -> IResult<&str, Vec<&str>> {
if input.is_empty() || input == "\n" {
return Ok(("", vec![]));
}
preceded(
tuple((multispace0, alt((tag_no_case("integers"), tag_no_case("integer"))), opt(preceded(space0, char(':'))), multispace0)),
parse_variable_list,
)(input)
}

#[inline]
pub fn parse_binary_section(input: &str) -> IResult<&str, Vec<&str>> {
if input.is_empty() || input == "\n" {
return Ok(("", vec![]));
}
preceded(
tuple((
multispace0,
alt((tag_no_case("binaries"), tag_no_case("binary"), tag_no_case("bin"))),
opt(preceded(space0, char(':'))),
multispace0,
)),
parse_variable_list,
)(input)
}

#[cfg(test)]
mod test {
use crate::nom::decoder::variable::parse_bounds_section;

#[test]
fn test_bounds() {
let input = "
bounds
x1 free
x2 >= 1
x2 >= inf
100 <= x2dfsdf <= -1
-infinity <= qwer < +inf";
let (remaining, bounds) = parse_bounds_section(input).unwrap();
assert_eq!(remaining, "");
assert_eq!(bounds.len(), 5);
}
}
137 changes: 102 additions & 35 deletions src/nom/lp_problem.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
use std::{borrow::Cow, collections::HashMap};
use std::{
borrow::Cow,
collections::{hash_map::Entry, HashMap},
};

use nom::{branch::alt, error::ErrorKind, sequence::tuple, IResult};
use nom::{combinator::opt, sequence::tuple};

use crate::nom::{
decoder::{
constraint::{parse_constraint_header, parse_constraints},
objective::parse_objectives,
problem_name::parse_problem_name,
sense::parse_sense,
variable::{parse_binary_section, parse_bounds_section, parse_generals_section, parse_integer_section},
},
model::{Constraint, Objective, Sense, Variable},
};

const CONSTRAINT_HEADERS: [&str; 4] = ["subject to", "such that", "s.t.", "st:"];

const BINARY_HEADERS: [&str; 3] = ["binaries", "binary", "bin"];
const ALL_VAR_BOUND_HEADERS: [&str; 11] =
["generals", "general", "integers", "integer", "binaries", "binary", "bin", "semi-continuous", "semis", "semi", "end"];
const GENERAL_HEADERS: [&str; 2] = ["generals", "general"];
const SEMI_HEADERS: [&str; 3] = ["semi-continuous", "semis", "semi"];

#[cfg_attr(feature = "diff", derive(diff::Diff), diff(attr(#[derive(Debug, PartialEq)])))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, PartialEq)]
Expand Down Expand Up @@ -49,21 +61,10 @@ impl LPProblem<'_> {
}
}

fn take_until_no_case<'a>(tag: &'a str) -> impl Fn(&'a str) -> IResult<&'a str, &'a str> {
move |input: &str| {
let mut index = 0;
let tag_lower = tag.to_lowercase();
let chars: Vec<char> = input.chars().collect();

while index <= chars.len() - tag.len() {
let window: String = chars[index..index + tag.len()].iter().collect();
if window.to_lowercase() == tag_lower {
return Ok((&input[index..], &input[..index]));
}
index += 1;
}

Err(nom::Err::Error(nom::error::Error::new(input, ErrorKind::TakeUntil)))
fn log_remaining(prefix: &str, remaining: &str) {
if !remaining.trim().is_empty() {
log::debug!("{prefix}: {remaining}");
println!("{prefix}: {remaining}");
}
}

Expand All @@ -72,31 +73,88 @@ impl<'a> TryFrom<&'a str> for LPProblem<'a> {

fn try_from(input: &'a str) -> Result<Self, Self::Error> {
// Extract the Sense, problem name and objectives slice
let (input, (name, sense, obj_section, _cons_header)) = tuple((
parse_problem_name,
parse_sense,
// First find where the constraint section starts by looking for any valid header
alt((take_until_no_case("subject to"), take_until_no_case("such that"), take_until_no_case("s.t."), take_until_no_case("st:"))),
parse_constraint_header,
))(input)?;

// Parse objectives from the section before constraints
let (_, (objs, mut variables)) = parse_objectives(obj_section)?;

// Parse the constraints
let (_remaining, (constraints, constraint_vars)) = parse_constraints(input)?;
let (input, (name, sense, obj_section, _cons_header)) =
tuple((parse_problem_name, parse_sense, nom_aux::make_take_until_parser(&CONSTRAINT_HEADERS), parse_constraint_header))(input)?;

let (remaining, (objs, mut variables)) = parse_objectives(obj_section)?;
log_remaining("Failed to parse objectives fully", remaining);

let (input, constraint_str) = nom_aux::make_take_until_parser(&["bounds", "bound"])(input)?;
let (remaining, (constraints, constraint_vars)) = parse_constraints(constraint_str)?;
log_remaining("Failed to parse constraints fully", remaining);
variables.extend(constraint_vars);

// Parse Variable Bounds (Integer, General, Bounded, Free and Semi-continuous)
//
let (input, bound_str) = nom_aux::make_take_until_parser(&ALL_VAR_BOUND_HEADERS)(input)?;
let (remaining, bounds) = parse_bounds_section(bound_str)?;
log_remaining("Failed to parse bounds fully", remaining);

for (bound_name, var_type) in bounds {
match variables.entry(bound_name) {
Entry::Occupied(mut occupied_entry) => {
occupied_entry.get_mut().set_vt(var_type);
}
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(Variable { name: bound_name, var_type });
}
}
}

// Parse SOS constraints
//
let (input, integer_str) = opt(nom_aux::make_take_until_parser(&GENERAL_HEADERS))(input)?;
if let Some(integer_str) = integer_str {
let (remaining, _integer_var) = parse_integer_section(integer_str)?;
log_remaining("Failed to parse integers fully", remaining);
}

let (input, generals_str) = opt(nom_aux::make_take_until_parser(&BINARY_HEADERS))(input)?;
if let Some(generals_str) = generals_str {
let (remaining, _general_var) = parse_generals_section(generals_str)?;
log_remaining("Failed to parse generals fully", remaining);
}

let (_input, binary_str) = opt(nom_aux::make_take_until_parser(&SEMI_HEADERS))(input)?;
if let Some(binary_str) = binary_str {
let (remaining, _binary_vars) = parse_binary_section(binary_str)?;
log_remaining("Failed to parse binaries fully", remaining);
}

Ok(LPProblem { name, sense, objectives: objs, constraints, variables })
}
}

mod nom_aux {
use nom::{error::ErrorKind, IResult};

fn take_until_cased<'a>(tag: &'a str) -> impl Fn(&'a str) -> IResult<&'a str, &'a str> {
move |input: &str| {
let mut index = 0;
let tag_lower = tag.to_lowercase();
let chars: Vec<char> = input.chars().collect();

while index <= chars.len() - tag.len() {
let window: String = chars[index..index + tag.len()].iter().collect();
if window.to_lowercase() == tag_lower {
return Ok((&input[index..], &input[..index]));
}
index += 1;
}

Err(nom::Err::Error(nom::error::Error::new(input, ErrorKind::TakeUntil)))
}
}

pub fn make_take_until_parser<'a>(tags: &'a [&'a str]) -> impl Fn(&'a str) -> IResult<&'a str, &'a str> + 'a {
move |input| {
tags.iter().map(|&tag| take_until_cased(tag)).fold(
Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Alt))),
|acc, parser| match acc {
Ok(ok) => Ok(ok),
Err(_) => parser(input),
},
)
}
}
}

#[cfg(test)]
mod test {
const SMALL_INPUT: &str = "\\ This file has been generated by Author
Expand All @@ -110,7 +168,16 @@ Minimize
subject to:
c1: 3 x1 + x2 + 2 x3 = 30
c2: 2 x1 + x2 + 3 x3 + x4 >= 15
c3: 2 x2 + 3 x4 <= 25";
c3: 2 x2 + 3 x4 <= 25
bounds
x1 free
x2 >= 1
100 <= x2dfsdf <= -1
Generals
Route_A_1
Route_A_2
Route_A_3
End";

#[cfg(feature = "serde")]
#[test]
Expand Down
4 changes: 4 additions & 0 deletions src/nom/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ impl<'a> Variable<'a> {
pub fn new(name: &'a str) -> Self {
Self { name, var_type: VariableType::default() }
}

pub fn set_vt(&mut self, var_type: VariableType) {
self.var_type = var_type;
}
}

#[cfg(feature = "serde")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,14 @@ variables:
var_type: Free
x2:
name: x2
var_type: Free
var_type:
LowerBound: 1
x2dfsdf:
name: x2dfsdf
var_type:
DoubleBound:
- 100
- -1
x3:
name: x3
var_type: Free
Expand Down

0 comments on commit bdd95d8

Please sign in to comment.