diff --git a/src/app/lang.ts b/src/app/lang.ts
index 4b1f2d1..d390de0 100644
--- a/src/app/lang.ts
+++ b/src/app/lang.ts
@@ -24,6 +24,8 @@ export default function text(lang: string, input: string): string {
return "Listen Sie alle Ihre Variablen auf. Eine pro Zeile (mit der 'Enter'-Taste trennen). Erlaubte Symbole sind a-z, A-Z.\nBeispiel:\nx\ny";
case "boxExportLP":
return "Als LP exportieren";
+ case "boxExportMPS":
+ return "Als MPS exportieren";
case "boxOut":
return "Geben Sie ein Problem ein und drücken Sie eine Aktionstaste, um die Ausgabe anzuzeigen...";
case "buttonCalc":
@@ -125,6 +127,8 @@ export default function text(lang: string, input: string): string {
return "List all your variables. One per line (divide by 'return' button). Allowed symbols are a-z, A-Z.\nExample:\nx\ny";
case "boxExportLP":
return "Export as LP";
+ case "boxExportMPS":
+ return "Export as MPS";
case "boxOut":
return "Input a problem and an action button to display output...";
case "buttonCalc":
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3375c12..fdf95e8 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { Box, Button, Output } from "./modules";
-import { calculate_clickMaximize, calculate_clickMinimize, downloadLP, import_click } from "./scripts"
+import { calculate_clickMaximize, calculate_clickMinimize, downloadLP, downloadMPS, import_click } from "./scripts"
import text from "./lang"
export default function Home() {
@@ -19,6 +19,7 @@ export default function Home() {
const tr_boxVarsDesc = text(language, "boxVarsDesc");
const tr_boxOut = text(language, "boxOut");
const tr_boxExportLP = text(language, "boxExportLP");
+ const tr_boxExoprtMPS = text(language, "boxExportMPS");
const tr_calc_max = text(language, "maximize");
const tr_calc_min = text(language, "minimize");
@@ -71,6 +72,10 @@ export default function Home() {
title={tr_boxExportLP}
className={"button"}
onClickFunc={downloadLP} />
+
{
+ if (constraint.bounds.type === GLP_UP) { // <=
+ mpsString += ` L ${constraint.name}\n`;
+ } else if (constraint.bounds.type === GLP_LO) { // >=
+ mpsString += ` G ${constraint.name}\n`;
+ } else if (constraint.bounds.type === GLP_FX) { // =
+ mpsString += ` E ${constraint.name}\n`;
+ }
+ });
-// function parseFunction(toParse: string) {
-// var regex = toParse.match(/([a-zA-Z][a-zA-Z0-9]*):/);
-// if (regex == null)
-// return;
-// var name = regex[1];
+ // COLUMNS section
+ mpsString += 'COLUMNS\n';
+ const variableMap: { [key: string]: { row: string; coef: number }[] } = {};
+ lp.objective.vars.forEach(varObj => {
+ if (!variableMap[varObj.name]) {
+ variableMap[varObj.name] = [];
+ }
+ variableMap[varObj.name].push({ row: lp.objective.name, coef: varObj.coef });
+ });
+ lp.subjectTo.forEach(constraint => {
+ constraint.vars.forEach(varObj => {
+ if (!variableMap[varObj.name]) {
+ variableMap[varObj.name] = [];
+ }
+ variableMap[varObj.name].push({ row: constraint.name, coef: varObj.coef });
+ });
+ });
-// regex = toParse.match(/(?:([0-9]*) *\* *([a-zA-Z][a-zA-Z0-9]*))/g);
+ for (const [variable, rows] of Object.entries(variableMap)) {
+ rows.forEach(entry => {
+ mpsString += ` ${variable} ${entry.row} ${entry.coef}\n`;
+ });
+ }
-// let coefs:number[] = [];
-// let vars:string[] = [];
+ // RHS section
+ mpsString += 'RHS\n';
+ lp.subjectTo.forEach(constraint => {
+ if (constraint.bounds.type === GLP_UP ) { // <= or =
+ mpsString += ` RHS1 ${constraint.name} ${constraint.bounds.ub}\n`;
+ } else if (constraint.bounds.type === GLP_LO || constraint.bounds.type === GLP_FX) { // >=
+ mpsString += ` RHS1 ${constraint.name} ${constraint.bounds.lb}\n`;
+ }
+ });
-// for (const rg of regex) {
-// coefs.push(+rg.match(/([0-9]+)/g));
-// vars.push(rg.match(/([a-zA-Z][a-zA-Z0-9]*)/g)[0]);
-// }
-// return {name, coefs, vars};
-// }
+ // BOUNDS section
+ if (lp.bounds && lp.bounds.length > 0) {
+ mpsString += 'BOUNDS\n';
+ lp.bounds.forEach(bound => {
+ if (bound.lb !== -Infinity) {
+ mpsString += ` LO BND1 ${bound.name} ${bound.lb}\n`;
+ }
+ if (bound.ub !== Infinity) {
+ mpsString += ` UP BND1 ${bound.name} ${bound.ub}\n`;
+ }
+ });
+ }
+
+ // BINARY section
+ if (lp.binaries && lp.binaries.length > 0) {
+ mpsString += 'BINARY\n';
+ lp.binaries.forEach(bin => {
+ mpsString += ` ${bin}\n`;
+ });
+ }
+
+ // GENERAL section
+ if (lp.generals && lp.generals.length > 0) {
+ mpsString += 'GENERAL\n';
+ lp.generals.forEach(gen => {
+ mpsString += ` ${gen}\n`;
+ });
+ }
+
+ // ENDATA section
+ mpsString += 'ENDATA\n';
+
+ return mpsString;
+}
+
+// read LP format from string
+function parseLP(lpString: string): LP {
+ const lines = lpString.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
+ let mode: string = '';
+ let lp: LP = {
+ name: '',
+ objective: {
+ direction: GLP_MAX, // 1 for maximize, -1 for minimize
+ name: '',
+ vars: []
+ },
+ subjectTo: [],
+ bounds: [],
+ binaries: [],
+ generals: []
+ };
+
+ let objectiveExpression = '';
+ let constraintExpression = '';
+ let currentConstraintName = '';
+
+ for (let line of lines) {
+
+ // handle each block differently
+ // set mode for each to determine parsing path
+ if (line.startsWith("Maximize")) {
+ lp.objective.direction = GLP_MAX;
+ mode = 'objective';
+ continue;
+ } else if (line.startsWith("Minimize")) {
+ lp.objective.direction = GLP_MIN;
+ mode = 'objective';
+ continue;
+ } else if (line.startsWith("Subject To")) {
+ // Constraint section
+ if (objectiveExpression.length > 0) {
+ lp.objective.vars = parseLPExpression(objectiveExpression.trim());
+ objectiveExpression = '';
+ }
+ mode = 'subjectTo';
+ continue;
+ } else if (line.startsWith("Bounds")) {
+ // Bound section
+ if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
+ const { vars, bound } = parseLPConstraint(constraintExpression.trim());
+ lp.subjectTo.push({
+ name: currentConstraintName,
+ vars: vars,
+ bounds: bound
+ });
+ constraintExpression = '';
+ currentConstraintName = '';
+ }
+ mode = 'bounds';
+ continue;
+ } else if (line.startsWith("Binary")) {
+ mode = 'binaries';
+ continue;
+ } else if (line.startsWith("General")) {
+ mode = 'generals';
+ continue;
+ } else if (line.startsWith("End")) {
+ mode = 'end';
+ continue;
+ }
+
+ // Parse based on current mode
+ if (mode === 'objective') {
+ const splitLine = line.split(":");
+ if (splitLine.length === 2) {
+ lp.objective.name = splitLine[0].trim(); // if name in first line
+ objectiveExpression += splitLine[1].trim() + ' ';
+ } else {
+ objectiveExpression += line.trim() + ' '; // multiline expansion of objective
+ }
+
+ } else if (mode === 'subjectTo') {
+ const splitLine = line.split(":");
+ if (splitLine.length === 2) {
+ if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
+ // new Constraint -> add previous to the subjects
+ const { vars, bound } = parseLPConstraint(constraintExpression.trim());
+ lp.subjectTo.push({
+ name: currentConstraintName,
+ vars: vars,
+ bounds: bound
+ });
+ }
+ // start collecting the new constraint
+ currentConstraintName = splitLine[0].trim();
+ constraintExpression = splitLine[1].trim() + ' ';
+ } else {
+ // continue collecting the expression if it's multi-line
+ constraintExpression += line.trim() + ' ';
+ }
+ } else if (mode === 'bounds') {
+ const bound = parseLPBound(line.trim());
+ if (bound && lp.bounds) {
+ lp.bounds.push(bound);
+ }
+ } else if (mode === 'binaries') {
+ lp.binaries?.push(line.trim());
+ } else if (mode === 'generals') {
+ lp.generals?.push(line.trim());
+ }
+ }
+
+ // if no "subject to" previously, do it here
+ if (objectiveExpression.length > 0) {
+ lp.objective.vars = parseLPExpression(objectiveExpression.trim());
+ }
+
+ // same for "bounds" and "end"
+ if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
+ const { vars, bound } = parseLPConstraint(constraintExpression.trim());
+ lp.subjectTo.push({
+ name: currentConstraintName,
+ vars: vars,
+ bounds: bound
+ });
+ }
+
+ return lp;
+}
+
+// Helper for expressions
+function parseLPExpression(expr: string): Variable[] {
+ const regex = /([+-]?\s*\d*\.?\d*)\s*([a-zA-Z_][a-zA-Z_0-9]*)/g;
+ let match;
+ let vars: Variable[] = [];
+
+ // loop over all variables in expresion
+ while ((match = regex.exec(expr)) !== null) {
+ let temp_coef = match[1].replace(/\s+/g, '').trim() || '1';
+ temp_coef = temp_coef === "-" || temp_coef === '+' ? `${temp_coef}1` : temp_coef;
+ const coef = parseFloat(temp_coef);
+ const name = match[2];
+ vars.push({ name: name, coef: coef });
+ }
+
+ return vars;
+}
+
+// Helper for Constraint section
+function parseLPConstraint(constraint: string): { vars: Variable[], bound: Bounds } {
+ // Valid operators in Constraints
+ const operators = ["<=", ">=", "="];
+ let operator = operators.find(op => constraint.includes(op));
+ if (!operator) {
+ throw new Error("Invalid constraint format");
+ }
+
+ const [expr, boundStr] = constraint.split(operator);
+ const vars: Variable[] = parseLPExpression(expr.trim());
+ const boundValue = parseFloat(boundStr.trim());
+
+ // determine bound type
+ let boundType = GLP_UP;
+ if (operator === "<=") {
+ boundType = GLP_UP;
+ } else if (operator === ">=") {
+ boundType = GLP_LO;
+ } else if (operator === "=") {
+ boundType = GLP_FX;
+ }
+ let lb = boundType === GLP_FX ? boundValue : boundType === GLP_LO ? boundValue : -Infinity;
+ let ub = boundType === GLP_UP ? boundValue : Infinity;
+
+ const bound: Bounds = {
+ type: boundType,
+ lb: lb,
+ ub: ub
+ } as Bounds;
+
+ return { vars, bound };
+}
+
+// Helper for Bound section
+function parseLPBound(boundStr: string): Bound | null {
+ // Regex to handle various bound formats
+ const regex = /^([-]?\d*\.?\d*)?\s*(<=|>=|=)?\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*(<=|>=|=)?\s*([-]?\d*\.?\d*)?$/;
+ const match = regex.exec(boundStr.trim());
+
+ if (match) {
+ const [, lbStr, leftOperator, varName, rightOperator, ubStr] = match;
+ let lb = lbStr ? parseFloat(lbStr) : undefined;
+ let ub = ubStr ? parseFloat(ubStr) : undefined;
+ let type: number;
+
+ // Handle free "edgecase"
+ if (boundStr.toLowerCase().includes('free')) {
+ return {
+ type: GLP_FR,
+ name: varName,
+ lb: -Infinity,
+ ub: Infinity
+ } as Bound;
+ }
+
+ // Determine bound type
+ if (leftOperator && rightOperator) {
+ if (leftOperator === '<=' && rightOperator === '<=') {
+ type = GLP_DB; // Double bound
+ } else if (leftOperator === '>=' && rightOperator === '>=') {
+ type = GLP_DB; // Double bound (reverse order)
+ [lb, ub] = [ub, lb]; // Swap lb and ub
+ } else {
+ return null; // Invalid combination
+ }
+ // detect one-sided bounds
+ } else if (leftOperator === '<=') {
+ type = GLP_UP;
+ ub = lb;
+ lb = undefined;
+ } else if (rightOperator === '<=') {
+ type = GLP_UP;
+ } else if (leftOperator === '>=') {
+ type = GLP_LO;
+ } else if (rightOperator === '>=') {
+ type = GLP_LO;
+ lb = ub;
+ ub = undefined;
+ } else if (leftOperator === '=' || rightOperator === '=') {
+ type = GLP_FX;
+ if (leftOperator === '=') ub = lb;
+ else lb = ub;
+ } else {
+ type = GLP_FR; // No bounds specified, assume free
+ }
+
+ return {
+ type,
+ name: varName,
+ lb: lb !== undefined ? lb : -Infinity,
+ ub: ub !== undefined ? ub : Infinity
+ } as Bound;
+ }
+ return null;
+}
+
+export function downloadMPS() {
+ let inputs = getInputsForLPAsString();
+ let lp = parseLP(inputs);
+ let mps = convertLPToMPS(lp);
+
+ downloadProblemDownload(mps);
+
+ console.log(mps);
+}
\ No newline at end of file
diff --git a/src/solver/jvail/Bound.tsx b/src/solver/jvail/Bound.tsx
new file mode 100644
index 0000000..70c905d
--- /dev/null
+++ b/src/solver/jvail/Bound.tsx
@@ -0,0 +1,6 @@
+export interface Bound{
+ name: string;
+ type: number; // 1 for lower bound, 2 for upper bound, 3 for equal
+ ub: number;
+ lb: number;
+}
\ No newline at end of file
diff --git a/src/solver/jvail/Bounds.tsx b/src/solver/jvail/Bounds.tsx
new file mode 100644
index 0000000..0b8ed75
--- /dev/null
+++ b/src/solver/jvail/Bounds.tsx
@@ -0,0 +1,9 @@
+export interface Bounds{ type: number, ub: number, lb: number }
+
+export const GLP_FR = 1; /* free (unbounded) variable */
+export const GLP_LO = 2; /* variable with lower bound */
+export const GLP_UP = 3; /* variable with upper bound */
+export const GLP_DB = 4; /* double-bounded variable */
+export const GLP_FX = 5; /* fixed variable */
+export const GLP_MAX = 2;
+export const GLP_MIN = 1
\ No newline at end of file
diff --git a/src/solver/jvail/Constraint.tsx b/src/solver/jvail/Constraint.tsx
new file mode 100644
index 0000000..70bc38b
--- /dev/null
+++ b/src/solver/jvail/Constraint.tsx
@@ -0,0 +1,8 @@
+import {Variable} from "./Variable";
+import {Bounds} from "./Bounds";
+
+export interface Constraint {
+ name: string;
+ vars: Variable[];
+ bounds: Bounds;
+}
\ No newline at end of file
diff --git a/src/solver/jvail/LP.tsx b/src/solver/jvail/LP.tsx
new file mode 100644
index 0000000..7109fe5
--- /dev/null
+++ b/src/solver/jvail/LP.tsx
@@ -0,0 +1,18 @@
+import {Variable} from "./Variable";
+import {Bound} from "./Bound";
+import {Options} from "./Options";
+import {Constraint} from "./Constraint";
+
+export interface LP {
+ name: string,
+ objective: {
+ direction: number,
+ name: string,
+ vars: Variable[]
+ },
+ subjectTo: Constraint[],
+ bounds?: Bound[],
+ binaries?: string[],
+ generals?: string[],
+ options?: Options
+}
\ No newline at end of file
diff --git a/src/solver/jvail/Options.tsx b/src/solver/jvail/Options.tsx
new file mode 100644
index 0000000..311b22a
--- /dev/null
+++ b/src/solver/jvail/Options.tsx
@@ -0,0 +1,12 @@
+import {Result} from "./Result";
+
+export interface Options {
+ mipgap?: number, /* set relative mip gap tolerance to mipgap, default 0.0 */
+ tmlim?: number, /* limit solution time to tmlim seconds, default INT_MAX */
+ msglev?: number, /* message level for terminal output, default GLP_MSG_ERR */
+ presol?: boolean, /* use presolver, default true */
+ cb?: { /* a callback called at each 'each' iteration (only simplex) */
+ call(result: Result):Result,
+ each: number
+ }
+}
\ No newline at end of file
diff --git a/src/solver/jvail/Result.tsx b/src/solver/jvail/Result.tsx
new file mode 100644
index 0000000..1530fb2
--- /dev/null
+++ b/src/solver/jvail/Result.tsx
@@ -0,0 +1,10 @@
+export interface Result {
+ name: string;
+ time: number;
+ result: {
+ status: number;
+ z: number;
+ vars: {[key:string]: number};
+ dual?: { [key: string]: number }; /* simplex only */
+ };
+}
\ No newline at end of file
diff --git a/src/solver/jvail/Variable.tsx b/src/solver/jvail/Variable.tsx
new file mode 100644
index 0000000..664e3c7
--- /dev/null
+++ b/src/solver/jvail/Variable.tsx
@@ -0,0 +1,4 @@
+export interface Variable {
+ name: string;
+ coef: number
+}
\ No newline at end of file