diff --git a/check/TestFilereader.cpp b/check/TestFilereader.cpp index 14ad114b6e..a1b420d316 100644 --- a/check/TestFilereader.cpp +++ b/check/TestFilereader.cpp @@ -343,3 +343,17 @@ TEST_CASE("filereader-dD2e", "[highs_filereader]") { objective_value = highs.getInfo().objective_function_value; REQUIRE(objective_value == optimal_objective_value); } + +TEST_CASE("filereader-comment", "[highs_filereader]") { + // Check that comments - either whole line with * in first column, + // or rest of line following */$ are handled correctly + const double optimal_objective_value = -4; + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/comment.mps"; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + REQUIRE(highs.readModel(model_file) == HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + double objective_value = highs.getInfo().objective_function_value; + REQUIRE(objective_value == optimal_objective_value); +} diff --git a/check/instances/comment.mps b/check/instances/comment.mps new file mode 100644 index 0000000000..96d7c80985 --- /dev/null +++ b/check/instances/comment.mps @@ -0,0 +1,23 @@ +* Optimal objective is -4 +NAME Comment +* Comment +ROWS + N COST $Comment + G R0 + G R1 *Comment + G R2 $Comment +COLUMNS + C0 R0 1.0 COST -1.0 $Comment + C0 R1 -1.0 $Comment + C0 R2 -1.0 $Comment + C1 R0 1.0 COST -2.0 *Comment + C1 R1 -1.0 *Comment + C1 R2 -1.0 *Comment +RHS + DEMANDS R0 1.0 R1 -2.0 $Comment + DEMANDS R2 -2.0 $Comment +RANGES + test R0 1 $Comment +BOUNDS + UP SERVINGS C0 1.0 $Comment +ENDATA diff --git a/src/io/HMpsFF.cpp b/src/io/HMpsFF.cpp index 14f0bf5dc0..414e1fcbbb 100644 --- a/src/io/HMpsFF.cpp +++ b/src/io/HMpsFF.cpp @@ -210,6 +210,31 @@ HighsInt HMpsFF::fillHessian(const HighsLogOptions& log_options) { return 0; } +bool HMpsFF::timeout() { + return time_limit > 0 && getWallTime() - start_time > time_limit; +} + +bool HMpsFF::getMpsLine(std::istream& file, std::string& strline, bool& skip) { + skip = false; + if (!getline(file, strline)) return false; + if (is_empty(strline) || strline[0] == '*') { + skip = true; + } else { + // Remove any trailing comment + const size_t p = strline.find_first_of(mps_comment_chars); + if (p <= strline.length()) { + // A comment character has been found, so erase from it to the end + // of the line and check whether the line is now empty + strline.erase(p); + skip = is_empty(strline); + if (skip) return true; + } + strline = trim(strline); + skip = is_empty(strline); + } + return true; +} + FreeFormatParserReturnCode HMpsFF::parse(const HighsLogOptions& log_options, const std::string& filename) { HMpsFF::Parsekey keyword = HMpsFF::Parsekey::kNone; @@ -448,9 +473,11 @@ HighsInt HMpsFF::getColIdx(const std::string& colname, const bool add_if_new) { HMpsFF::Parsekey HMpsFF::parseDefault(const HighsLogOptions& log_options, std::istream& file) { std::string strline, word; - if (getline(file, strline)) { - strline = trim(strline); - if (strline.empty()) return HMpsFF::Parsekey::kComment; + bool skip; + if (getMpsLine(file, strline, skip)) { + if (skip) return HMpsFF::Parsekey::kComment; + if (timeout()) return HMpsFF::Parsekey::kTimeout; + size_t s, e; HMpsFF::Parsekey key = checkFirstWord(strline, s, e, word); if (key == HMpsFF::Parsekey::kName) { @@ -495,8 +522,10 @@ HMpsFF::Parsekey HMpsFF::parseObjsense(const HighsLogOptions& log_options, std::istream& file) { std::string strline, word; - while (getline(file, strline)) { - if (is_empty(strline) || strline[0] == '*') continue; + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t start = 0; size_t end = 0; @@ -532,11 +561,10 @@ HMpsFF::Parsekey HMpsFF::parseRows(const HighsLogOptions& log_options, assert(num_row == 0); assert(row_lower.size() == 0); assert(row_upper.size() == 0); - while (getline(file, strline)) { - if (is_empty(strline) || strline[0] == '*') continue; - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; bool isobj = false; bool isFreeRow = false; @@ -670,22 +698,10 @@ typename HMpsFF::Parsekey HMpsFF::parseCols(const HighsLogOptions& log_options, assert(-1 == rowidx || -2 == rowidx); }; - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; HMpsFF::Parsekey key = checkFirstWord(strline, start, end, word); @@ -977,22 +993,10 @@ HMpsFF::Parsekey HMpsFF::parseRhs(const HighsLogOptions& log_options, has_obj_entry_ = false; bool has_entry = false; - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin = 0; size_t end = 0; @@ -1143,22 +1147,10 @@ HMpsFF::Parsekey HMpsFF::parseBounds(const HighsLogOptions& log_options, has_lower.assign(num_col, false); has_upper.assign(num_col, false); - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin = 0; size_t end = 0; @@ -1422,22 +1414,10 @@ HMpsFF::Parsekey HMpsFF::parseRanges(const HighsLogOptions& log_options, // Initialise tracking for duplicate entries has_row_entry_.assign(num_row, false); - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin, end; std::string word; @@ -1575,21 +1555,10 @@ typename HMpsFF::Parsekey HMpsFF::parseHessian( size_t end_coeff_name; HighsInt colidx, rowidx; - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin = 0; size_t end = 0; @@ -1702,7 +1671,11 @@ typename HMpsFF::Parsekey HMpsFF::parseQuadRows( "Row name \"%s\" in %s section is not defined: ignored\n", rowname.c_str(), section_name.c_str()); // read lines until start of new section - while (getline(file, strline)) { + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; + size_t begin = 0; size_t end = 0; HMpsFF::Parsekey key = checkFirstWord(strline, begin, end, col_name); @@ -1725,21 +1698,10 @@ typename HMpsFF::Parsekey HMpsFF::parseQuadRows( auto& qentries = (rowidx == -1 ? q_entries : qrows_entries[rowidx]); - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin = 0; size_t end = 0; @@ -1878,22 +1840,10 @@ typename HMpsFF::Parsekey HMpsFF::parseCones(const HighsLogOptions& log_options, // now parse the cone entries: one column per line std::string strline; - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin; std::string colname; @@ -1921,22 +1871,10 @@ typename HMpsFF::Parsekey HMpsFF::parseSos(const HighsLogOptions& log_options, const HMpsFF::Parsekey keyword) { std::string strline, word; - while (getline(file, strline)) { - double current = getWallTime(); - if (time_limit > 0 && current - start_time > time_limit) - return HMpsFF::Parsekey::kTimeout; - - if (kAnyFirstNonBlankAsStarImpliesComment) { - trim(strline); - if (strline.size() == 0 || strline[0] == '*') continue; - } else { - if (strline.size() > 0) { - // Just look for comment character in column 1 - if (strline[0] == '*') continue; - } - trim(strline); - if (strline.size() == 0) continue; - } + bool skip; + while (getMpsLine(file, strline, skip)) { + if (skip) continue; + if (timeout()) return HMpsFF::Parsekey::kTimeout; size_t begin, end; std::string word; diff --git a/src/io/HMpsFF.h b/src/io/HMpsFF.h index 6c17e0c7ef..2426273ec2 100644 --- a/src/io/HMpsFF.h +++ b/src/io/HMpsFF.h @@ -38,6 +38,8 @@ using Triplet = std::tuple; +const std::string mps_comment_chars = "*$"; + enum class FreeFormatParserReturnCode { kSuccess, kParserError, @@ -124,7 +126,6 @@ class HMpsFF { HighsInt fillMatrix(const HighsLogOptions& log_options); HighsInt fillHessian(const HighsLogOptions& log_options); - const bool kAnyFirstNonBlankAsStarImpliesComment = false; /// how to treat variables that appear in COLUMNS section first /// assume them to be binary as in the original IBM interpretation /// or integer with default bounds @@ -189,6 +190,9 @@ class HMpsFF { mutable std::string section_args; + bool timeout(); + bool getMpsLine(std::istream& file, std::string& strline, bool& skip); + FreeFormatParserReturnCode parse(const HighsLogOptions& log_options, const std::string& filename); // Checks first word of strline and wraps it by it_begin and it_end diff --git a/src/util/stringutil.h b/src/util/stringutil.h index 4fa072485b..25ac72c0ef 100644 --- a/src/util/stringutil.h +++ b/src/util/stringutil.h @@ -27,14 +27,18 @@ void strTrim(char* str); void tolower(std::string& str); -const std::string non_chars = "\t\n\v\f\r "; -std::string& ltrim(std::string& str, const std::string& chars = non_chars); -std::string& rtrim(std::string& str, const std::string& chars = non_chars); -std::string& trim(std::string& str, const std::string& chars = non_chars); - -bool is_empty(std::string& str, const std::string& chars = non_chars); -bool is_empty(char c, const std::string& chars = non_chars); -bool is_end(std::string& str, size_t end, const std::string& chars = non_chars); +const std::string default_non_chars = "\t\n\v\f\r "; +std::string& ltrim(std::string& str, + const std::string& chars = default_non_chars); +std::string& rtrim(std::string& str, + const std::string& chars = default_non_chars); +std::string& trim(std::string& str, + const std::string& chars = default_non_chars); + +bool is_empty(std::string& str, const std::string& chars = default_non_chars); +bool is_empty(char c, const std::string& chars = default_non_chars); +bool is_end(std::string& str, size_t end, + const std::string& chars = default_non_chars); // todo: replace with pair of references rather than string ret value to avoid // copy and also using function below. or do it properly with iterators.