Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
dandxy89 committed Nov 18, 2024
1 parent 425df6e commit e6676c1
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ serde = { version = "1", features = ["derive"], optional = true }
thiserror = "2.0"
unique_id = "0.1"

nom = "7.1.3"

[dev-dependencies]
float_eq = "1.0"
insta = { version = "1.41", features = ["yaml", "redactions"] }
Expand Down
8 changes: 5 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@

use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "lp_file_format.pest"]
pub struct LParser;

pub mod common;
pub mod model;
pub mod parse;

#[derive(Parser)]
#[grammar = "lp_file_format.pest"]
pub struct LParser;
pub mod nom;
260 changes: 260 additions & 0 deletions src/nom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while1},
character::complete::{char, multispace0},
combinator::{map, opt, recognize, value},
multi::many1,
number::complete::double,
sequence::{delimited, pair, preceded, terminated, tuple},
IResult,
};

const VALID_LP_CHARS: &[char] = &['!', '#', '$', '%', '&', '(', ')', '_', ',', '.', ';', '?', '@', '\\', '{', '}', '~', '\''];

#[derive(Debug, Clone, PartialEq)]
pub struct Coefficient {
pub var_name: String,
pub coefficient: f64,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Objective {
pub name: Option<String>,
pub coefficients: Vec<Coefficient>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sense {
Minimize,
Maximize,
}

pub fn valid_lp_char(c: char) -> bool {
c.is_alphanumeric() || VALID_LP_CHARS.contains(&c)
}

pub fn scientific_number(input: &str) -> IResult<&str, f64> {
double(input)
}

#[inline]
pub fn sign(input: &str) -> IResult<&str, f64> {
alt((value(-1.0, char('-')), value(1.0, char('+')), value(1.0, multispace0)))(input)
}

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

#[inline]
pub fn problem_name_line(input: &str) -> IResult<&str, String> {
map(
delimited(
tuple((multispace0, tag("\\"), multispace0, tag("Problem"), multispace0, tag("name:"), multispace0)),
problem_name,
multispace0,
),
String::from,
)(input)
}

#[inline]
pub fn minimize(input: &str) -> IResult<&str, Sense> {
value(Sense::Minimize, alt((tag_no_case("minimize"), tag_no_case("minimum"), tag_no_case("min"))))(input)
}

#[inline]
pub fn maximize(input: &str) -> IResult<&str, Sense> {
value(Sense::Maximize, alt((tag_no_case("maximize"), tag_no_case("maximum"), tag_no_case("max"))))(input)
}

#[inline]
pub fn problem_sense(input: &str) -> IResult<&str, Sense> {
delimited(multispace0, alt((minimize, maximize)), multispace0)(input)
}

#[inline]
pub fn problem_header(input: &str) -> IResult<&str, (Option<String>, Sense)> {
pair(opt(problem_name_line), problem_sense)(input)
}

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

#[inline]
pub fn coefficient(input: &str) -> IResult<&str, Coefficient> {
preceded(
multispace0,
map(tuple((sign, opt(terminated(scientific_number, multispace0)), variable)), |(sign, coef_opt, var_name)| Coefficient {
var_name: var_name.to_string(),
coefficient: sign * coef_opt.unwrap_or(1.0),
}),
)(input)
}

#[inline]
pub fn objective_name(input: &str) -> IResult<&str, &str> {
terminated(take_while1(valid_lp_char), delimited(multispace0, char(':'), multispace0))(input)
}

#[inline]
pub fn term(input: &str) -> IResult<&str, Coefficient> {
preceded(
multispace0,
alt((
map(preceded(char('-'), coefficient), |mut coef| {
coef.coefficient = -coef.coefficient;
coef
}),
preceded(opt(char('+')), coefficient),
)),
)(input)
}

#[inline]
pub fn objective(input: &str) -> IResult<&str, Objective> {
map(pair(opt(preceded(multispace0, objective_name)), many1(term)), |(name, coefficients)| Objective {
name: name.map(String::from),
coefficients,
})(input)
}

#[cfg(test)]
mod tests {
use float_eq::assert_float_eq;

use super::*;

#[test]
fn test_problem_name() {
// Basic case
assert_eq!(problem_name("MyProblem123"), Ok(("", "MyProblem123")));
// With special characters
assert_eq!(problem_name("Test_Problem!#$"), Ok(("", "Test_Problem!#$")));
// With trailing content
assert_eq!(problem_name("Problem1 rest"), Ok((" rest", "Problem1")));
}

#[test]
fn test_problem_name_line() {
// Standard case
assert_eq!(problem_name_line("\\ Problem name: TestProblem"), Ok(("", "TestProblem".to_string())));
// With extra whitespace
assert_eq!(problem_name_line("\\ Problem name: SpacedProblem"), Ok(("", "SpacedProblem".to_string())));
// With special characters
assert_eq!(problem_name_line("\\ Problem name: Test_Problem!#$"), Ok(("", "Test_Problem!#$".to_string())));
// With leading whitespace
assert_eq!(problem_name_line(" \\ Problem name: LeadingSpace"), Ok(("", "LeadingSpace".to_string())));
}

#[test]
fn test_minimize() {
assert_eq!(minimize("minimize"), Ok(("", Sense::Minimize)));
assert_eq!(minimize("MINIMIZE"), Ok(("", Sense::Minimize)));
assert_eq!(minimize("min"), Ok(("", Sense::Minimize)));
assert_eq!(minimize("MIN"), Ok(("", Sense::Minimize)));
assert_eq!(minimize("minimum"), Ok(("", Sense::Minimize)));
assert_eq!(minimize("MINIMUM"), Ok(("", Sense::Minimize)));
}

#[test]
fn test_maximize() {
assert_eq!(maximize("maximize"), Ok(("", Sense::Maximize)));
assert_eq!(maximize("MAXIMIZE"), Ok(("", Sense::Maximize)));
assert_eq!(maximize("max"), Ok(("", Sense::Maximize)));
assert_eq!(maximize("MAX"), Ok(("", Sense::Maximize)));
assert_eq!(maximize("maximum"), Ok(("", Sense::Maximize)));
assert_eq!(maximize("MAXIMUM"), Ok(("", Sense::Maximize)));
}

#[test]
fn test_problem_sense() {
// With leading whitespace
assert_eq!(problem_sense(" minimize"), Ok(("", Sense::Minimize)));
assert_eq!(problem_sense("\nmaximize"), Ok(("", Sense::Maximize)));
// With trailing content
assert_eq!(problem_sense("minimize obj"), Ok(("obj", Sense::Minimize)));
assert_eq!(problem_sense("maximize obj"), Ok(("obj", Sense::Maximize)));
// Case insensitive
assert_eq!(problem_sense("MINIMIZE"), Ok(("", Sense::Minimize)));
assert_eq!(problem_sense("MAXIMIZE"), Ok(("", Sense::Maximize)));
}

#[test]
fn test_problem_header() {
assert_eq!(problem_header("minimize"), Ok(("", (None, Sense::Minimize))));
assert_eq!(problem_header("\\ Problem name: Test\nminimize"), Ok(("", (Some("Test".to_string()), Sense::Minimize))));
assert_eq!(problem_header(" \\ Problem name: Test \n maximize "), Ok(("", (Some("Test".to_string()), Sense::Maximize))));
}

#[test]
fn test_scientific_number() {
assert_float_eq!(scientific_number("1e-03").unwrap().1, 0.001, abs <= 1e-10);
assert_float_eq!(scientific_number("2.5e+02").unwrap().1, 250.0, abs <= 1e-10);
assert_float_eq!(scientific_number("3.14").unwrap().1, 3.14, abs <= 1e-10);
}

#[test]
fn test_coefficient() {
// Basic cases
let (_, coef1) = coefficient("x1").unwrap();
assert_eq!(coef1.var_name, "x1");
assert_float_eq!(coef1.coefficient, 1.0, abs <= 1e-10);

let (_, coef2) = coefficient("-2.5x2").unwrap();
assert_eq!(coef2.var_name, "x2");
assert_float_eq!(coef2.coefficient, -2.5, abs <= 1e-10);

// Scientific notation
let (_, coef3) = coefficient("1e-03x3").unwrap();
assert_eq!(coef3.var_name, "x3");
assert_float_eq!(coef3.coefficient, 0.001, abs <= 1e-10);

// With spaces
let (_, coef4) = coefficient(" +3.5 x4").unwrap();
assert_eq!(coef4.var_name, "x4");
assert_float_eq!(coef4.coefficient, 3.5, abs <= 1e-10);

// Just variable with sign
let (_, coef5) = coefficient(" -x5").unwrap();
assert_eq!(coef5.var_name, "x5");
assert_float_eq!(coef5.coefficient, -1.0, abs <= 1e-10);

// Spaced scientic notation
let (_, coef6) = coefficient(" +2.5e+02 x6").unwrap();
assert_eq!(coef6.var_name, "x6");
assert_float_eq!(coef6.coefficient, 250.0, abs <= 1e-10);
}

#[test]
fn test_objective() {
// Basic objective
let (rest, obj1) = objective("obj1: x1 + 2 x2").unwrap();
assert_eq!(obj1.name, Some("obj1".to_string()));
assert_eq!(obj1.coefficients.len(), 2);
assert_eq!(obj1.coefficients[0].var_name, "x1");
assert_float_eq!(obj1.coefficients[0].coefficient, 1.0, abs <= 1e-10);
assert_eq!(obj1.coefficients[1].var_name, "x2");
assert_float_eq!(obj1.coefficients[1].coefficient, 2.0, abs <= 1e-10);
assert_eq!(rest, "");

// Objective with scientific notation
let (_, obj2) = objective("obj: 1e-03x1 + 2.5e+02x2 - 3.14x3").unwrap();
assert_eq!(obj2.name, Some("obj".to_string()));
assert_eq!(obj2.coefficients.len(), 3);
assert_float_eq!(obj2.coefficients[0].coefficient, 0.001, abs <= 1e-10);
assert_float_eq!(obj2.coefficients[1].coefficient, 250.0, abs <= 1e-10);
assert_float_eq!(obj2.coefficients[2].coefficient, -3.14, abs <= 1e-10);

// Objective with spaces
let (_, obj3) = objective("obj: x1 - 2 x2 + 1.5 x3").unwrap();
assert_eq!(obj3.name, Some("obj".to_string()));
assert_eq!(obj3.coefficients.len(), 3);
assert_float_eq!(obj3.coefficients[0].coefficient, 1.0, abs <= 1e-10);
assert_float_eq!(obj3.coefficients[1].coefficient, -2.0, abs <= 1e-10);
assert_float_eq!(obj3.coefficients[2].coefficient, 1.5, abs <= 1e-10);
}
}
1 change: 1 addition & 0 deletions tests/nom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

0 comments on commit e6676c1

Please sign in to comment.