From e34325bcff2cc333ac77ff177a2cde97953535ca Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Tue, 9 Dec 2025 16:58:12 +0100 Subject: [PATCH 1/5] Add test and fix --- tests/parse/test_parse_vrplib.py | 27 +++++++++++++++++++++++++++ vrplib/parse/parse_vrplib.py | 11 ++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/parse/test_parse_vrplib.py b/tests/parse/test_parse_vrplib.py index 8daef486..aeed9982 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,29 @@ 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..ceba0829 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: + if "_SECTION" in line: start = lines.index(line) 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": From 5e739215edc2a8478bd27f423e0023b82a818370 Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Tue, 9 Dec 2025 16:58:42 +0100 Subject: [PATCH 2/5] Dont do lookup --- vrplib/parse/parse_vrplib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vrplib/parse/parse_vrplib.py b/vrplib/parse/parse_vrplib.py index ceba0829..b48a7318 100644 --- a/vrplib/parse/parse_vrplib.py +++ b/vrplib/parse/parse_vrplib.py @@ -72,7 +72,7 @@ def group_specifications_and_sections(lines: list[str]): continue if "_SECTION" in line: - start = lines.index(line) + start = idx end_section = start + 1 for next_line in lines[start + 1 :]: From 6878eadead5177e5e0c3a175b7a8d331a9eef00a Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Tue, 9 Dec 2025 17:00:01 +0100 Subject: [PATCH 3/5] Format nicely --- tests/parse/test_parse_vrplib.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/parse/test_parse_vrplib.py b/tests/parse/test_parse_vrplib.py index aeed9982..4a589d7f 100644 --- a/tests/parse/test_parse_vrplib.py +++ b/tests/parse/test_parse_vrplib.py @@ -285,15 +285,16 @@ def test_section_with_colon(): """ instance = dedent( """ - DIMENSION: 1 - EDGE_WEIGHT_TYPE: EXACT_2D - NODE_COORD_SECTION: - 1 1 1 - DEMAND_SECTION : - 1 10 - DEPOT_SECTION - 1 - EOF""" + 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 From 36dcb0dfa2523dd2601f4ec193c22e7b19754f43 Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Tue, 9 Dec 2025 17:01:38 +0100 Subject: [PATCH 4/5] Add a note in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6e0bfcb..406f6097 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ 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. +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:`. 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. Besides the rules outlined above, `vrplib` is not strict about the naming of specifications or sections. From 59b2ba37bcd8d2c2fe8c547ff82a969d6cd8314a Mon Sep 17 00:00:00 2001 From: Leon Lan Date: Tue, 9 Dec 2025 17:02:57 +0100 Subject: [PATCH 5/5] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 406f6097..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`. Section names may also end with a colon, e.g., `DEMAND_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`.