diff --git a/README.md b/README.md index c6e0bfcb..191c95fc 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,8 @@ EOF A VRPLIB instance contains problem **specifications** and problem **data**. - Specifications are key-value pairs separated by a colon. In the example above, `NAME` and `EDGE_WEIGHT_TYPE` are the two data specifications. - Data are explicit array-like values such as customer coordinates or service times. -Each data section should start with a header name that ends with `_SECTION`, e.g., `NODE_COORD_SECTION` and `SERVICE_TIME_SECTION`. It is then followed by rows of values and each row must start with an index representing the depot or customer. -There are two exceptions: values in `EDGE_WEIGHT_SECTION` and `DEPOT_SECTION` should not start with an index. +Each data section should start with a header name that ends with `_SECTION`, e.g., `NODE_COORD_SECTION` and `SERVICE_TIME_SECTION`. Section names may also end with a colon, e.g., `DEMAND_SECTION:`. The name must be followed by rows of values and each row must start with an index representing the depot or customer. +There are two exceptions to this rule: values in `EDGE_WEIGHT_SECTION` and `DEPOT_SECTION` should not start with an index. Besides the rules outlined above, `vrplib` is not strict about the naming of specifications or sections. This means that you can use `vrplib` to read VRPLIB instances with custom specifications like `MY_SPECIFICATION: SOME_VALUE` and custom section names like `MY_SECTION`. diff --git a/tests/parse/test_parse_vrplib.py b/tests/parse/test_parse_vrplib.py index 8daef486..4a589d7f 100644 --- a/tests/parse/test_parse_vrplib.py +++ b/tests/parse/test_parse_vrplib.py @@ -1,4 +1,5 @@ from pathlib import Path +from textwrap import dedent import numpy as np from numpy.testing import assert_, assert_equal, assert_raises @@ -275,3 +276,30 @@ def test_empty_text(): """ actual = parse_vrplib("") assert_equal(actual, {}) + + +def test_section_with_colon(): + """ + Tests if a section name containing a colon is parsed correctly. + See https://github.com/PyVRP/VRPLIB/issues/132 for details. + """ + instance = dedent( + """ + DIMENSION: 1 + EDGE_WEIGHT_TYPE: EXACT_2D + NODE_COORD_SECTION: + 1 1 1 + DEMAND_SECTION : + 1 10 + DEPOT_SECTION + 1 + EOF + """ + ) + + result = parse_vrplib(instance) # should not raise + assert_equal(result["dimension"], 1) + assert_equal(result["edge_weight_type"], "EXACT_2D") + assert_equal(result["demand"], [10]) + assert_equal(result["node_coord"], [[1, 1]]) + assert_equal(result["depot"], [0]) diff --git a/vrplib/parse/parse_vrplib.py b/vrplib/parse/parse_vrplib.py index d24a0d68..b48a7318 100644 --- a/vrplib/parse/parse_vrplib.py +++ b/vrplib/parse/parse_vrplib.py @@ -71,14 +71,12 @@ def group_specifications_and_sections(lines: list[str]): if idx < end_section: # Skip all lines of the current section continue - if ":" in line: - specs.append(line) - elif "_SECTION" in line: - start = lines.index(line) + if "_SECTION" in line: + start = idx end_section = start + 1 for next_line in lines[start + 1 :]: - if ":" in next_line: + if ":" in next_line and "_SECTION" not in next_line: raise ValueError("Specification presented after section.") # The current section ends when a next section or an EOF token @@ -89,6 +87,8 @@ def group_specifications_and_sections(lines: list[str]): end_section += 1 sections.append(lines[start:end_section]) + elif ":" in line: + specs.append(line) else: msg = "Instance does not conform to the VRPLIB format." raise RuntimeError(msg) @@ -111,7 +111,8 @@ def parse_section( """ Parses the data section lines. """ - name = lines[0].strip().removesuffix("_SECTION").lower() + # Some section names include colons, so we strip those as well. + name = lines[0].strip(" :").removesuffix("_SECTION").lower() rows = [[infer_type(n) for n in line.split()] for line in lines[1:]] if name == "edge_weight":