keepset 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{bundled-V1EJ0k9e.js → bundled-qRweq3SB.js} +369 -26
- package/dist/{categories-HeWbVPhc.d.ts → categories-Dwo8gSp_.d.ts} +106 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/fetch-dataset.d.ts +1 -1
- package/dist/node/fetch-dataset.js +678 -87
- package/dist-cli/keepset.mjs +13 -3
- package/package.json +1 -1
- package/schemas/entry.schema.json +1 -1
- package/schemas/manifest.schema.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-
|
|
1
|
+
import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-qRweq3SB.js";
|
|
2
2
|
import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -104,7 +104,7 @@ function parseEdgeList(text, options) {
|
|
|
104
104
|
//#endregion
|
|
105
105
|
//#region src/parsers/gml.ts
|
|
106
106
|
const KEY = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
107
|
-
const NUMBER$
|
|
107
|
+
const NUMBER$6 = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?/y;
|
|
108
108
|
/**
|
|
109
109
|
* Matches a sticky pattern at a position.
|
|
110
110
|
* @param pattern - A sticky regular expression.
|
|
@@ -155,7 +155,7 @@ function tokenise(text) {
|
|
|
155
155
|
position = end + 1;
|
|
156
156
|
} else {
|
|
157
157
|
const key = matchAt(KEY, text, position);
|
|
158
|
-
const number = key === void 0 ? matchAt(NUMBER$
|
|
158
|
+
const number = key === void 0 ? matchAt(NUMBER$6, text, position) : void 0;
|
|
159
159
|
const match = key ?? number;
|
|
160
160
|
if (match === void 0) throw new NativeParseError("gml", line, `unexpected character ${JSON.stringify(char)}`);
|
|
161
161
|
tokens.push({
|
|
@@ -356,7 +356,7 @@ const DELIMITERS = {
|
|
|
356
356
|
semicolon: ";"
|
|
357
357
|
};
|
|
358
358
|
/** A number as a decimal literal, with an optional sign and exponent. `Number` alone would also accept hex, binary, `Infinity` and the empty string. */
|
|
359
|
-
const DECIMAL$
|
|
359
|
+
const DECIMAL$2 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
360
360
|
/** The byte order mark some exporters put before the first header. */
|
|
361
361
|
const BYTE_ORDER_MARK = "";
|
|
362
362
|
/**
|
|
@@ -559,7 +559,7 @@ function* withoutByteOrderMark(text) {
|
|
|
559
559
|
*/
|
|
560
560
|
function numeric$1(cell, factor, column, line) {
|
|
561
561
|
if (cell === "") return null;
|
|
562
|
-
if (!DECIMAL$
|
|
562
|
+
if (!DECIMAL$2.test(cell)) throw new NativeParseError("csv-table", line, `column ${column} is declared numeric but holds ${JSON.stringify(cell)}`);
|
|
563
563
|
return Number(cell) * factor;
|
|
564
564
|
}
|
|
565
565
|
/**
|
|
@@ -832,10 +832,47 @@ function parsePajek(text) {
|
|
|
832
832
|
};
|
|
833
833
|
}
|
|
834
834
|
//#endregion
|
|
835
|
+
//#region src/parsers/customer-points.ts
|
|
836
|
+
const FORMAT$6 = "customer-points";
|
|
837
|
+
const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
|
|
838
|
+
const WHOLE$2 = /^\d+$/;
|
|
839
|
+
/**
|
|
840
|
+
* Parses a customer points file, as the OAR Bench arc routing benchmark generator writes the customers of an instance: the line `customers <count>`, then one row per customer of its id, x and y, separated by tabs or spaces, with blank lines skipped. The positions are planar coordinates in the instance's own unit, which the file does not name. The row count must be the announced count, which catches a truncated file.
|
|
841
|
+
* @param text - The file text.
|
|
842
|
+
* @returns The instance: the customer count as `meta` and the customers as nodes, in file order.
|
|
843
|
+
* @throws NativeParseError naming the line for a first line that is not `customers <count>`, a row that is not an id and two numbers, a repeated id, or a row count that is not the announced one.
|
|
844
|
+
*/
|
|
845
|
+
function parseCustomerPoints(text) {
|
|
846
|
+
const rows = lines(text).map((raw, index) => ({
|
|
847
|
+
line: index + 1,
|
|
848
|
+
fields: raw.trim().split(/\s+/).filter((field) => field !== "")
|
|
849
|
+
})).filter((row) => row.fields.length > 0);
|
|
850
|
+
const [first, ...body] = rows;
|
|
851
|
+
const [key, count, ...extra] = first?.fields ?? [];
|
|
852
|
+
if (first === void 0 || key !== "customers" || count === void 0 || !WHOLE$2.test(count) || extra.length > 0) throw new NativeParseError(FORMAT$6, first?.line ?? 1, `expected "customers <count>", found ${JSON.stringify(first?.fields.join(" "))}`);
|
|
853
|
+
const seen = /* @__PURE__ */ new Set();
|
|
854
|
+
const nodes = body.map((row) => {
|
|
855
|
+
const [id, x, y, ...rest] = row.fields;
|
|
856
|
+
if (id === void 0 || x === void 0 || y === void 0 || rest.length > 0 || !WHOLE$2.test(id) || !NUMBER$5.test(x) || !NUMBER$5.test(y)) throw new NativeParseError(FORMAT$6, row.line, `expected a customer id and two numbers, found ${JSON.stringify(row.fields.join(" "))}`);
|
|
857
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$6, row.line, `customer ${id} appears twice`);
|
|
858
|
+
seen.add(id);
|
|
859
|
+
return {
|
|
860
|
+
id,
|
|
861
|
+
x: Number(x),
|
|
862
|
+
y: Number(y)
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
if (nodes.length !== Number(count)) throw new NativeParseError(FORMAT$6, rows[rows.length - 1]?.line ?? first.line, `the file announces ${count} customers, found ${String(nodes.length)}`);
|
|
866
|
+
return {
|
|
867
|
+
meta: { customers: Number(count) },
|
|
868
|
+
nodes
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
835
872
|
//#region src/parsers/solomon.ts
|
|
836
|
-
const FORMAT$
|
|
873
|
+
const FORMAT$5 = "solomon";
|
|
837
874
|
/** A line of integers or decimals separated by any run of spaces and tabs, as the files write rows (Solomon's with padded spaces, the Tenerife set with tabs). */
|
|
838
|
-
const NUMBER$
|
|
875
|
+
const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
|
|
839
876
|
/**
|
|
840
877
|
* Reads a token as a finite number.
|
|
841
878
|
* @param token - The token.
|
|
@@ -844,8 +881,8 @@ const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
|
|
|
844
881
|
* @returns The number.
|
|
845
882
|
* @throws NativeParseError when the token is not a decimal number.
|
|
846
883
|
*/
|
|
847
|
-
function numberOf$
|
|
848
|
-
if (!NUMBER$
|
|
884
|
+
function numberOf$3(token, line, what) {
|
|
885
|
+
if (!NUMBER$4.test(token)) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
849
886
|
return Number(token);
|
|
850
887
|
}
|
|
851
888
|
/**
|
|
@@ -858,7 +895,7 @@ function parseSolomon(text) {
|
|
|
858
895
|
const all = lines(text);
|
|
859
896
|
const present = (index) => (all[index] ?? "").trim();
|
|
860
897
|
const name = present(0);
|
|
861
|
-
if (name === "") throw new NativeParseError(FORMAT$
|
|
898
|
+
if (name === "") throw new NativeParseError(FORMAT$5, 1, "expected the instance name on the first line");
|
|
862
899
|
const meta = { name };
|
|
863
900
|
const nodes = [];
|
|
864
901
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -874,7 +911,7 @@ function parseSolomon(text) {
|
|
|
874
911
|
awaitingHeader = true;
|
|
875
912
|
continue;
|
|
876
913
|
}
|
|
877
|
-
if (section === "preamble") throw new NativeParseError(FORMAT$
|
|
914
|
+
if (section === "preamble") throw new NativeParseError(FORMAT$5, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
|
|
878
915
|
if (awaitingHeader) {
|
|
879
916
|
awaitingHeader = false;
|
|
880
917
|
continue;
|
|
@@ -882,29 +919,29 @@ function parseSolomon(text) {
|
|
|
882
919
|
const tokens = trimmed.split(/\s+/);
|
|
883
920
|
if (section === "vehicle") {
|
|
884
921
|
const [vehicles, capacity, ...extra] = tokens;
|
|
885
|
-
if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$
|
|
886
|
-
meta.vehicles = numberOf$
|
|
887
|
-
meta.capacity = numberOf$
|
|
922
|
+
if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$5, line, `expected the vehicle count and capacity, found ${JSON.stringify(trimmed)}`);
|
|
923
|
+
meta.vehicles = numberOf$3(vehicles, line, "the vehicle count");
|
|
924
|
+
meta.capacity = numberOf$3(capacity, line, "the vehicle capacity");
|
|
888
925
|
continue;
|
|
889
926
|
}
|
|
890
927
|
const [id, x, y, demand, ready, due, service, ...extra] = tokens;
|
|
891
|
-
if (id === void 0 || x === void 0 || y === void 0 || demand === void 0 || ready === void 0 || due === void 0 || service === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$
|
|
892
|
-
if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$
|
|
893
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
928
|
+
if (id === void 0 || x === void 0 || y === void 0 || demand === void 0 || ready === void 0 || due === void 0 || service === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$5, line, `expected seven numbers (id, x, y, demand, ready, due, service), found ${JSON.stringify(trimmed)}`);
|
|
929
|
+
if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$5, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
|
|
930
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$5, line, `node ${id} appears twice`);
|
|
894
931
|
seen.add(id);
|
|
895
932
|
nodes.push({
|
|
896
933
|
id,
|
|
897
|
-
x: numberOf$
|
|
898
|
-
y: numberOf$
|
|
899
|
-
demand: numberOf$
|
|
900
|
-
ready: numberOf$
|
|
901
|
-
due: numberOf$
|
|
902
|
-
service: numberOf$
|
|
934
|
+
x: numberOf$3(x, line, "x"),
|
|
935
|
+
y: numberOf$3(y, line, "y"),
|
|
936
|
+
demand: numberOf$3(demand, line, "the demand"),
|
|
937
|
+
ready: numberOf$3(ready, line, "the ready time"),
|
|
938
|
+
due: numberOf$3(due, line, "the due date"),
|
|
939
|
+
service: numberOf$3(service, line, "the service time")
|
|
903
940
|
});
|
|
904
941
|
}
|
|
905
|
-
if (section !== "customer") throw new NativeParseError(FORMAT$
|
|
906
|
-
if (nodes.length === 0) throw new NativeParseError(FORMAT$
|
|
907
|
-
if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$
|
|
942
|
+
if (section !== "customer") throw new NativeParseError(FORMAT$5, all.length, "expected a CUSTOMER section");
|
|
943
|
+
if (nodes.length === 0) throw new NativeParseError(FORMAT$5, all.length, "the CUSTOMER section has no rows");
|
|
944
|
+
if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$5, all.length, "expected a VEHICLE section with the vehicle count and capacity");
|
|
908
945
|
return {
|
|
909
946
|
meta: {
|
|
910
947
|
name: meta.name,
|
|
@@ -916,10 +953,10 @@ function parseSolomon(text) {
|
|
|
916
953
|
}
|
|
917
954
|
//#endregion
|
|
918
955
|
//#region src/parsers/top.ts
|
|
919
|
-
const FORMAT$
|
|
956
|
+
const FORMAT$4 = "top";
|
|
920
957
|
/** The header lines before the first point: `n`, `m` and `tmax`. */
|
|
921
958
|
const HEADER_LINES = 3;
|
|
922
|
-
const NUMBER$
|
|
959
|
+
const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
|
|
923
960
|
/**
|
|
924
961
|
* Reads the value of a header line `<key> <number>`.
|
|
925
962
|
* @param text - The line.
|
|
@@ -930,7 +967,7 @@ const NUMBER$2 = /^-?\d+(?:\.\d+)?$/;
|
|
|
930
967
|
*/
|
|
931
968
|
function headerOf(text, key, line) {
|
|
932
969
|
const [name, value, ...extra] = (text ?? "").trim().split(/\s+/);
|
|
933
|
-
if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$
|
|
970
|
+
if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$3.test(value)) throw new NativeParseError(FORMAT$4, line, `expected "${key} <number>", found ${JSON.stringify((text ?? "").trim())}`);
|
|
934
971
|
return Number(value);
|
|
935
972
|
}
|
|
936
973
|
/**
|
|
@@ -949,11 +986,11 @@ function parseTop(text) {
|
|
|
949
986
|
const points = headerOf(first?.text, "n", first?.line ?? 1);
|
|
950
987
|
const paths = headerOf(second?.text, "m", second?.line ?? 2);
|
|
951
988
|
const budget = headerOf(third?.text, "tmax", third?.line ?? HEADER_LINES);
|
|
952
|
-
if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$
|
|
953
|
-
if (rows.length !== points) throw new NativeParseError(FORMAT$
|
|
989
|
+
if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$4, first?.line ?? 1, `expected at least a start and an end point, found n ${String(points)}`);
|
|
990
|
+
if (rows.length !== points) throw new NativeParseError(FORMAT$4, all[all.length - 1]?.line ?? 1, `the header gives ${String(points)} points, found ${String(rows.length)} rows`);
|
|
954
991
|
const nodes = rows.map((row, index) => {
|
|
955
992
|
const [x, y, score, ...extra] = row.text.split(/\s+/);
|
|
956
|
-
if (x === void 0 || y === void 0 || score === void 0 || extra.length > 0 || !NUMBER$
|
|
993
|
+
if (x === void 0 || y === void 0 || score === void 0 || extra.length > 0 || !NUMBER$3.test(x) || !NUMBER$3.test(y) || !NUMBER$3.test(score)) throw new NativeParseError(FORMAT$4, row.line, `expected three numbers (x, y, score), found ${JSON.stringify(row.text)}`);
|
|
957
994
|
return {
|
|
958
995
|
id: String(index),
|
|
959
996
|
x: Number(x),
|
|
@@ -973,7 +1010,7 @@ function parseTop(text) {
|
|
|
973
1010
|
}
|
|
974
1011
|
//#endregion
|
|
975
1012
|
//#region src/parsers/toptw.ts
|
|
976
|
-
const FORMAT$
|
|
1013
|
+
const FORMAT$3 = "toptw";
|
|
977
1014
|
/** How many numbers the first header line holds (k, paths, customers, t) and the second (D, capacity). */
|
|
978
1015
|
const FIRST_HEADER_NUMBERS = 4;
|
|
979
1016
|
const SECOND_HEADER_NUMBERS = 2;
|
|
@@ -981,7 +1018,7 @@ const SECOND_HEADER_NUMBERS = 2;
|
|
|
981
1018
|
const LIST_LENGTH_AT = 6;
|
|
982
1019
|
const LIST_STARTS_AT = 7;
|
|
983
1020
|
const ROW_NUMBERS_BESIDE_LIST = 9;
|
|
984
|
-
const NUMBER$
|
|
1021
|
+
const NUMBER$2 = /^-?\d+(?:\.\d+)?$/;
|
|
985
1022
|
/**
|
|
986
1023
|
* Reads a token as a finite number.
|
|
987
1024
|
* @param token - The token.
|
|
@@ -990,8 +1027,8 @@ const NUMBER$1 = /^-?\d+(?:\.\d+)?$/;
|
|
|
990
1027
|
* @returns The number.
|
|
991
1028
|
* @throws NativeParseError when the token is not a decimal number.
|
|
992
1029
|
*/
|
|
993
|
-
function numberOf$
|
|
994
|
-
if (token === void 0 || !NUMBER$
|
|
1030
|
+
function numberOf$2(token, line, what) {
|
|
1031
|
+
if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
995
1032
|
return Number(token);
|
|
996
1033
|
}
|
|
997
1034
|
/**
|
|
@@ -1005,8 +1042,8 @@ function numberOf$1(token, line, what) {
|
|
|
1005
1042
|
*/
|
|
1006
1043
|
function numbersOf(text, count, line, what) {
|
|
1007
1044
|
const tokens = text.trim().split(/\s+/);
|
|
1008
|
-
if (tokens.length !== count) throw new NativeParseError(FORMAT$
|
|
1009
|
-
return tokens.map((token) => numberOf$
|
|
1045
|
+
if (tokens.length !== count) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
|
|
1046
|
+
return tokens.map((token) => numberOf$2(token, line, what));
|
|
1010
1047
|
}
|
|
1011
1048
|
/**
|
|
1012
1049
|
* Parses a team orienteering problem with time windows instance (TOPTW, in the layout of Righini and Salani, as the KU Leuven pages describe it). The first line holds four numbers, of which the second is the number of paths and the third the number of customers, the second line two, of which the second is the vehicle capacity, and then one row per node, the depot first: its id, position, service duration, profit, an unused number, the length of a list of unused numbers, that list, and the opening and closing of its time window. Columns are separated by any run of spaces, and blank or whitespace only lines are skipped. The row count must be the customer count plus one, which catches a truncated file. The time quantities are in the unit of the source's coordinate grid, which the format does not name.
|
|
@@ -1020,30 +1057,30 @@ function parseToptw(text) {
|
|
|
1020
1057
|
text: raw.trim()
|
|
1021
1058
|
})).filter((entry) => entry.text !== "");
|
|
1022
1059
|
const [first, second, ...rows] = all;
|
|
1023
|
-
if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$
|
|
1060
|
+
if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$3, 1, "expected the two header lines");
|
|
1024
1061
|
const [, paths, customers] = numbersOf(first.text, FIRST_HEADER_NUMBERS, first.line, "four numbers (k, paths, customers, t)");
|
|
1025
1062
|
const [, capacity] = numbersOf(second.text, SECOND_HEADER_NUMBERS, second.line, "two numbers (D, capacity)");
|
|
1026
|
-
if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$
|
|
1063
|
+
if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$3, first.line, "the header lacks a number");
|
|
1027
1064
|
const seen = /* @__PURE__ */ new Set();
|
|
1028
1065
|
const nodes = rows.map((row) => {
|
|
1029
1066
|
const tokens = row.text.split(/\s+/);
|
|
1030
|
-
const listLength = numberOf$
|
|
1031
|
-
if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT$
|
|
1067
|
+
const listLength = numberOf$2(tokens[LIST_LENGTH_AT], row.line, "the length of the list after the unused number");
|
|
1068
|
+
if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT$3, row.line, `expected ${String(ROW_NUMBERS_BESIDE_LIST + listLength)} numbers for a list of ${String(listLength)}, found ${String(tokens.length)}`);
|
|
1032
1069
|
const [id] = tokens;
|
|
1033
|
-
if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$
|
|
1034
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
1070
|
+
if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$3, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
|
|
1071
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$3, row.line, `node ${id} appears twice`);
|
|
1035
1072
|
seen.add(id);
|
|
1036
1073
|
return {
|
|
1037
1074
|
id,
|
|
1038
|
-
x: numberOf$
|
|
1039
|
-
y: numberOf$
|
|
1040
|
-
service: numberOf$
|
|
1041
|
-
profit: numberOf$
|
|
1042
|
-
open: numberOf$
|
|
1043
|
-
close: numberOf$
|
|
1075
|
+
x: numberOf$2(tokens[1], row.line, "x"),
|
|
1076
|
+
y: numberOf$2(tokens[2], row.line, "y"),
|
|
1077
|
+
service: numberOf$2(tokens[3], row.line, "the service duration"),
|
|
1078
|
+
profit: numberOf$2(tokens[4], row.line, "the profit"),
|
|
1079
|
+
open: numberOf$2(tokens[LIST_STARTS_AT + listLength], row.line, "the opening of the window"),
|
|
1080
|
+
close: numberOf$2(tokens[8 + listLength], row.line, "the closing of the window")
|
|
1044
1081
|
};
|
|
1045
1082
|
});
|
|
1046
|
-
if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$
|
|
1083
|
+
if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$3, all.length === 0 ? 1 : all[all.length - 1]?.line ?? 1, `the header gives ${String(customers)} customers, so expected ${String(customers + 1)} rows with the depot, found ${String(nodes.length)}`);
|
|
1047
1084
|
return {
|
|
1048
1085
|
meta: {
|
|
1049
1086
|
paths,
|
|
@@ -1055,7 +1092,7 @@ function parseToptw(text) {
|
|
|
1055
1092
|
}
|
|
1056
1093
|
//#endregion
|
|
1057
1094
|
//#region src/parsers/tsplib.ts
|
|
1058
|
-
const FORMAT = "tsplib";
|
|
1095
|
+
const FORMAT$2 = "tsplib";
|
|
1059
1096
|
/** The instance types the parser reads: the probabilistic and the clustered orienteering problem. */
|
|
1060
1097
|
const TYPES = /* @__PURE__ */ new Set(["POP", "COP"]);
|
|
1061
1098
|
/** The edge weight types a coordinate section is read under: planar ones keep their coordinates as `x` and `y`, `GEO` converts them to degrees, and `EXPLICIT` has no coordinates. */
|
|
@@ -1064,9 +1101,9 @@ const PLANAR = /* @__PURE__ */ new Set([
|
|
|
1064
1101
|
"CEIL_2D",
|
|
1065
1102
|
"ATT"
|
|
1066
1103
|
]);
|
|
1067
|
-
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
1068
|
-
const WHOLE = /^\d+$/;
|
|
1069
|
-
const HEADER$
|
|
1104
|
+
const NUMBER$1 = /^-?\d+(?:\.\d+)?$/;
|
|
1105
|
+
const WHOLE$1 = /^\d+$/;
|
|
1106
|
+
const HEADER$2 = /^([A-Z][A-Z_]*)\s*:\s*(.*)$/;
|
|
1070
1107
|
const SECTION = /^[A-Z][A-Z_]*$/;
|
|
1071
1108
|
/** Minutes in a degree, which a `DDD.MM` coordinate counts in. */
|
|
1072
1109
|
const MINUTES_PER_DEGREE = 60;
|
|
@@ -1086,7 +1123,7 @@ function degrees(value, line) {
|
|
|
1086
1123
|
const whole = Math.trunc(magnitude);
|
|
1087
1124
|
const hundredths = (magnitude - whole) * MINUTE_SCALE;
|
|
1088
1125
|
const minutes = Math.round(hundredths);
|
|
1089
|
-
if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT, line, `expected a DDD.MM coordinate with whole minutes below 60, found ${String(value)}`);
|
|
1126
|
+
if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT$2, line, `expected a DDD.MM coordinate with whole minutes below 60, found ${String(value)}`);
|
|
1090
1127
|
return Math.sign(value) * (whole + minutes / MINUTES_PER_DEGREE);
|
|
1091
1128
|
}
|
|
1092
1129
|
/**
|
|
@@ -1097,8 +1134,8 @@ function degrees(value, line) {
|
|
|
1097
1134
|
* @returns The number.
|
|
1098
1135
|
* @throws NativeParseError when the token is not a decimal number.
|
|
1099
1136
|
*/
|
|
1100
|
-
function numberOf(token, line, what) {
|
|
1101
|
-
if (token === void 0 || !NUMBER.test(token)) throw new NativeParseError(FORMAT, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1137
|
+
function numberOf$1(token, line, what) {
|
|
1138
|
+
if (token === void 0 || !NUMBER$1.test(token)) throw new NativeParseError(FORMAT$2, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1102
1139
|
return Number(token);
|
|
1103
1140
|
}
|
|
1104
1141
|
/**
|
|
@@ -1110,7 +1147,7 @@ function numberOf(token, line, what) {
|
|
|
1110
1147
|
* @throws NativeParseError when the token is not a whole number.
|
|
1111
1148
|
*/
|
|
1112
1149
|
function wholeOf(token, line, what) {
|
|
1113
|
-
if (token === void 0 || !WHOLE.test(token)) throw new NativeParseError(FORMAT, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1150
|
+
if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$2, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1114
1151
|
return Number(token);
|
|
1115
1152
|
}
|
|
1116
1153
|
/**
|
|
@@ -1131,14 +1168,14 @@ function collect(text) {
|
|
|
1131
1168
|
const line = index + 1;
|
|
1132
1169
|
const trimmed = raw.trim();
|
|
1133
1170
|
if (trimmed === "" || trimmed === "EOF") continue;
|
|
1134
|
-
const keyed = HEADER$
|
|
1171
|
+
const keyed = HEADER$2.exec(trimmed);
|
|
1135
1172
|
const key = keyed?.[1];
|
|
1136
1173
|
if (key === "CONSTRAINING_SET_SECTION") {
|
|
1137
1174
|
section = "sets";
|
|
1138
1175
|
continue;
|
|
1139
1176
|
}
|
|
1140
1177
|
if (keyed !== null && key !== void 0) {
|
|
1141
|
-
if (collected.header.has(key)) throw new NativeParseError(FORMAT, line, `${key} appears twice`);
|
|
1178
|
+
if (collected.header.has(key)) throw new NativeParseError(FORMAT$2, line, `${key} appears twice`);
|
|
1142
1179
|
collected.header.set(key, (keyed[2] ?? "").trim());
|
|
1143
1180
|
section = void 0;
|
|
1144
1181
|
continue;
|
|
@@ -1150,33 +1187,33 @@ function collect(text) {
|
|
|
1150
1187
|
const tokens = trimmed.split(/\s+/);
|
|
1151
1188
|
if (section === "coords") {
|
|
1152
1189
|
const [id, x, y, ...extra] = tokens;
|
|
1153
|
-
if (extra.length > 0) throw new NativeParseError(FORMAT, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
|
|
1190
|
+
if (extra.length > 0) throw new NativeParseError(FORMAT$2, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
|
|
1154
1191
|
const node = wholeOf(id, line, "a node id");
|
|
1155
|
-
if (collected.coords.has(node)) throw new NativeParseError(FORMAT, line, `node ${String(node)} has two coordinate rows`);
|
|
1192
|
+
if (collected.coords.has(node)) throw new NativeParseError(FORMAT$2, line, `node ${String(node)} has two coordinate rows`);
|
|
1156
1193
|
collected.coords.set(node, {
|
|
1157
|
-
x: numberOf(x, line, "x"),
|
|
1158
|
-
y: numberOf(y, line, "y"),
|
|
1194
|
+
x: numberOf$1(x, line, "x"),
|
|
1195
|
+
y: numberOf$1(y, line, "y"),
|
|
1159
1196
|
line
|
|
1160
1197
|
});
|
|
1161
1198
|
} else if (section === "prizes") {
|
|
1162
1199
|
const [id, prize, probability, ...extra] = tokens;
|
|
1163
|
-
if (extra.length > 0) throw new NativeParseError(FORMAT, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
|
|
1200
|
+
if (extra.length > 0) throw new NativeParseError(FORMAT$2, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
|
|
1164
1201
|
const node = wholeOf(id, line, "a node id");
|
|
1165
|
-
if (collected.prizes.has(node)) throw new NativeParseError(FORMAT, line, `node ${String(node)} has two prize rows`);
|
|
1202
|
+
if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$2, line, `node ${String(node)} has two prize rows`);
|
|
1166
1203
|
collected.prizes.set(node, {
|
|
1167
|
-
prize: numberOf(prize, line, "the prize"),
|
|
1168
|
-
probability: numberOf(probability, line, "the probability")
|
|
1204
|
+
prize: numberOf$1(prize, line, "the prize"),
|
|
1205
|
+
probability: numberOf$1(probability, line, "the probability")
|
|
1169
1206
|
});
|
|
1170
1207
|
} else if (section === "sets") {
|
|
1171
1208
|
const [id, prize, ...members] = tokens;
|
|
1172
1209
|
const setId = wholeOf(id, line, "a set id");
|
|
1173
|
-
if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT, line, `set ${String(setId)} appears twice`);
|
|
1210
|
+
if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$2, line, `set ${String(setId)} appears twice`);
|
|
1174
1211
|
collected.sets.push({
|
|
1175
1212
|
id: setId,
|
|
1176
|
-
prize: numberOf(prize, line, "the set prize"),
|
|
1213
|
+
prize: numberOf$1(prize, line, "the set prize"),
|
|
1177
1214
|
members: members.map((member) => wholeOf(member, line, "a node id in a set"))
|
|
1178
1215
|
});
|
|
1179
|
-
} else if (section === void 0) throw new NativeParseError(FORMAT, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
|
|
1216
|
+
} else if (section === void 0) throw new NativeParseError(FORMAT$2, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
|
|
1180
1217
|
}
|
|
1181
1218
|
return collected;
|
|
1182
1219
|
}
|
|
@@ -1189,7 +1226,7 @@ function collect(text) {
|
|
|
1189
1226
|
*/
|
|
1190
1227
|
function required(header, key) {
|
|
1191
1228
|
const value = header.get(key);
|
|
1192
|
-
if (value === void 0) throw new NativeParseError(FORMAT, 1, `expected a ${key} line`);
|
|
1229
|
+
if (value === void 0) throw new NativeParseError(FORMAT$2, 1, `expected a ${key} line`);
|
|
1193
1230
|
return value;
|
|
1194
1231
|
}
|
|
1195
1232
|
/**
|
|
@@ -1201,23 +1238,23 @@ function required(header, key) {
|
|
|
1201
1238
|
function parseTsplib(text) {
|
|
1202
1239
|
const { header, coords, prizes, sets } = collect(text);
|
|
1203
1240
|
const type = required(header, "TYPE");
|
|
1204
|
-
if (!TYPES.has(type)) throw new NativeParseError(FORMAT, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
|
|
1241
|
+
if (!TYPES.has(type)) throw new NativeParseError(FORMAT$2, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
|
|
1205
1242
|
const weightType = required(header, "EDGE_WEIGHT_TYPE");
|
|
1206
1243
|
const geographic = weightType === "GEO";
|
|
1207
|
-
if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT, 1, `expected EDGE_WEIGHT_TYPE EUC_2D, CEIL_2D, ATT, GEO or EXPLICIT, found ${JSON.stringify(weightType)}`);
|
|
1244
|
+
if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT$2, 1, `expected EDGE_WEIGHT_TYPE EUC_2D, CEIL_2D, ATT, GEO or EXPLICIT, found ${JSON.stringify(weightType)}`);
|
|
1208
1245
|
const dimension = wholeOf(required(header, "DIMENSION"), 1, "DIMENSION");
|
|
1209
|
-
const budget = numberOf(required(header, "TMAX"), 1, "TMAX");
|
|
1210
|
-
if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT, 1, weightType === "EXPLICIT" ? "an EXPLICIT file has no NODE_COORD_SECTION" : `expected ${String(dimension)} coordinate rows, found ${String(coords.size)}`);
|
|
1246
|
+
const budget = numberOf$1(required(header, "TMAX"), 1, "TMAX");
|
|
1247
|
+
if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT$2, 1, weightType === "EXPLICIT" ? "an EXPLICIT file has no NODE_COORD_SECTION" : `expected ${String(dimension)} coordinate rows, found ${String(coords.size)}`);
|
|
1211
1248
|
const inRange = (id) => id >= 1 && id <= dimension;
|
|
1212
1249
|
for (const id of [
|
|
1213
1250
|
...coords.keys(),
|
|
1214
1251
|
...prizes.keys(),
|
|
1215
1252
|
...sets.flatMap((set) => set.members)
|
|
1216
|
-
]) if (!inRange(id)) throw new NativeParseError(FORMAT, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
|
|
1217
|
-
if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
|
|
1253
|
+
]) if (!inRange(id)) throw new NativeParseError(FORMAT$2, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
|
|
1254
|
+
if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$2, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
|
|
1218
1255
|
if (type === "COP") {
|
|
1219
1256
|
const declared = wholeOf(required(header, "SETS"), 1, "SETS");
|
|
1220
|
-
if (sets.length !== declared) throw new NativeParseError(FORMAT, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
|
|
1257
|
+
if (sets.length !== declared) throw new NativeParseError(FORMAT$2, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
|
|
1221
1258
|
}
|
|
1222
1259
|
const clustersOf = (node) => {
|
|
1223
1260
|
const ids = sets.filter((set) => set.members.includes(node)).map((set) => String(set.id));
|
|
@@ -1247,13 +1284,556 @@ function parseTsplib(text) {
|
|
|
1247
1284
|
type,
|
|
1248
1285
|
budget,
|
|
1249
1286
|
edgeWeightType: weightType,
|
|
1250
|
-
totalPrize: total === void 0 ? null : numberOf(total, 1, "TPRIZE"),
|
|
1287
|
+
totalPrize: total === void 0 ? null : numberOf$1(total, 1, "TPRIZE"),
|
|
1251
1288
|
setPrizes: type === "COP" ? sets.map((set) => `${String(set.id)}:${String(set.prize)}`).join(",") : null
|
|
1252
1289
|
},
|
|
1253
1290
|
nodes
|
|
1254
1291
|
};
|
|
1255
1292
|
}
|
|
1256
1293
|
//#endregion
|
|
1294
|
+
//#region src/parsers/verolog-2019.ts
|
|
1295
|
+
const FORMAT$1 = "verolog-2019";
|
|
1296
|
+
/** The header lines, in the order the file has them, with the key of `meta` each fills and whether its value is text. */
|
|
1297
|
+
const HEADER$1 = [
|
|
1298
|
+
[
|
|
1299
|
+
"DATASET",
|
|
1300
|
+
"dataset",
|
|
1301
|
+
"text"
|
|
1302
|
+
],
|
|
1303
|
+
[
|
|
1304
|
+
"NAME",
|
|
1305
|
+
"name",
|
|
1306
|
+
"text"
|
|
1307
|
+
],
|
|
1308
|
+
[
|
|
1309
|
+
"DAYS",
|
|
1310
|
+
"days",
|
|
1311
|
+
"number"
|
|
1312
|
+
],
|
|
1313
|
+
[
|
|
1314
|
+
"TRUCK_CAPACITY",
|
|
1315
|
+
"truckCapacity",
|
|
1316
|
+
"number"
|
|
1317
|
+
],
|
|
1318
|
+
[
|
|
1319
|
+
"TRUCK_MAX_DISTANCE",
|
|
1320
|
+
"truckMaxDistance",
|
|
1321
|
+
"number"
|
|
1322
|
+
],
|
|
1323
|
+
[
|
|
1324
|
+
"TRUCK_DISTANCE_COST",
|
|
1325
|
+
"truckDistanceCost",
|
|
1326
|
+
"number"
|
|
1327
|
+
],
|
|
1328
|
+
[
|
|
1329
|
+
"TRUCK_DAY_COST",
|
|
1330
|
+
"truckDayCost",
|
|
1331
|
+
"number"
|
|
1332
|
+
],
|
|
1333
|
+
[
|
|
1334
|
+
"TRUCK_COST",
|
|
1335
|
+
"truckCost",
|
|
1336
|
+
"number"
|
|
1337
|
+
],
|
|
1338
|
+
[
|
|
1339
|
+
"TECHNICIAN_DISTANCE_COST",
|
|
1340
|
+
"technicianDistanceCost",
|
|
1341
|
+
"number"
|
|
1342
|
+
],
|
|
1343
|
+
[
|
|
1344
|
+
"TECHNICIAN_DAY_COST",
|
|
1345
|
+
"technicianDayCost",
|
|
1346
|
+
"number"
|
|
1347
|
+
],
|
|
1348
|
+
[
|
|
1349
|
+
"TECHNICIAN_COST",
|
|
1350
|
+
"technicianCost",
|
|
1351
|
+
"number"
|
|
1352
|
+
]
|
|
1353
|
+
];
|
|
1354
|
+
/** The sections that hold rows, and how many numbers a row has before the per-machine flags of a technician. */
|
|
1355
|
+
const SECTIONS = [
|
|
1356
|
+
"MACHINES",
|
|
1357
|
+
"LOCATIONS",
|
|
1358
|
+
"REQUESTS",
|
|
1359
|
+
"TECHNICIANS"
|
|
1360
|
+
];
|
|
1361
|
+
const MACHINE_NUMBERS = 3;
|
|
1362
|
+
const LOCATION_NUMBERS = 3;
|
|
1363
|
+
const REQUEST_NUMBERS = 6;
|
|
1364
|
+
/** Where a request row keeps its fields after the id. */
|
|
1365
|
+
const REQUEST_AT = {
|
|
1366
|
+
location: 1,
|
|
1367
|
+
fromDay: 2,
|
|
1368
|
+
toDay: 3,
|
|
1369
|
+
machine: 4,
|
|
1370
|
+
amount: 5
|
|
1371
|
+
};
|
|
1372
|
+
const TECHNICIAN_NUMBERS_BEFORE_FLAGS = 4;
|
|
1373
|
+
const WHOLE = /^\d+$/;
|
|
1374
|
+
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
1375
|
+
/**
|
|
1376
|
+
* Splits the file into non-empty rows of fields. The three layouts the files come in (space padded, tab separated with trailing tabs, and semicolon separated with trailing semicolons) differ only in separator and padding, so any run of whitespace and semicolons separates fields and empty fields are dropped.
|
|
1377
|
+
* @param text - The file text.
|
|
1378
|
+
* @returns The rows, in file order.
|
|
1379
|
+
*/
|
|
1380
|
+
function rowsOf(text) {
|
|
1381
|
+
return lines(text).map((raw, index) => ({
|
|
1382
|
+
line: index + 1,
|
|
1383
|
+
fields: raw.split(/[;\s]+/).filter((field) => field !== "")
|
|
1384
|
+
})).filter((row) => row.fields.length > 0);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Reads a field as a finite number.
|
|
1388
|
+
* @param row - The row.
|
|
1389
|
+
* @param index - The field's position in it.
|
|
1390
|
+
* @param what - What the field should be, for the error.
|
|
1391
|
+
* @returns The number.
|
|
1392
|
+
* @throws NativeParseError when the field is absent or not a decimal number.
|
|
1393
|
+
*/
|
|
1394
|
+
function numberAt(row, index, what) {
|
|
1395
|
+
const field = row.fields[index];
|
|
1396
|
+
if (field === void 0 || !NUMBER.test(field)) throw new NativeParseError(FORMAT$1, row.line, `expected ${what}, found ${JSON.stringify(field)}`);
|
|
1397
|
+
return Number(field);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Reads a `KEY = value` row: the key, and the fields after the equals sign.
|
|
1401
|
+
* @param row - The row.
|
|
1402
|
+
* @returns The key and the value fields, or undefined for a row that is not of that form.
|
|
1403
|
+
*/
|
|
1404
|
+
function keyed(row) {
|
|
1405
|
+
const [key, equals, ...value] = row.fields;
|
|
1406
|
+
return key !== void 0 && equals === "=" ? {
|
|
1407
|
+
key,
|
|
1408
|
+
value
|
|
1409
|
+
} : void 0;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Checks that the rows of a section run from 1 with consecutive ids and have the length they should.
|
|
1413
|
+
* @param name - The section.
|
|
1414
|
+
* @param rows - Its rows.
|
|
1415
|
+
* @param length - How many fields a row must have, or undefined when only a minimum applies.
|
|
1416
|
+
* @throws NativeParseError naming the line of a row whose id is not the next one or whose length is wrong.
|
|
1417
|
+
*/
|
|
1418
|
+
function checkRows(name, rows, length) {
|
|
1419
|
+
for (const [index, row] of rows.entries()) {
|
|
1420
|
+
if (row.fields[0] !== String(index + 1)) throw new NativeParseError(FORMAT$1, row.line, `expected ${name} row ${String(index + 1)} to start with that id, found ${JSON.stringify(row.fields[0])}`);
|
|
1421
|
+
if (length !== void 0 && row.fields.length !== length) throw new NativeParseError(FORMAT$1, row.line, `expected ${String(length)} fields in a ${name} row, found ${String(row.fields.length)}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Splits the rows after the header into the rows of each section, each announced by `NAME = count`.
|
|
1426
|
+
* @param rows - The rows after the header.
|
|
1427
|
+
* @returns The rows of each section.
|
|
1428
|
+
* @throws NativeParseError naming the line for a section that is out of order, announced twice or short of its count, or a row outside any section.
|
|
1429
|
+
*/
|
|
1430
|
+
function sectionsOf(rows) {
|
|
1431
|
+
const found = {};
|
|
1432
|
+
let at = 0;
|
|
1433
|
+
for (const name of SECTIONS) {
|
|
1434
|
+
const start = rows[at];
|
|
1435
|
+
const announced = start === void 0 ? void 0 : keyed(start);
|
|
1436
|
+
if (start === void 0 || announced?.key !== name || announced.value.length !== 1 || !WHOLE.test(announced.value[0] ?? "")) throw new NativeParseError(FORMAT$1, start?.line ?? 1, `expected "${name} = <count>", found ${JSON.stringify(start?.fields.join(" "))}`);
|
|
1437
|
+
const count = Number(announced.value[0]);
|
|
1438
|
+
const body = rows.slice(at + 1, at + 1 + count);
|
|
1439
|
+
if (body.length !== count) throw new NativeParseError(FORMAT$1, start.line, `${name} announces ${String(count)} rows, found ${String(body.length)}`);
|
|
1440
|
+
found[name] = body;
|
|
1441
|
+
at += 1 + count;
|
|
1442
|
+
}
|
|
1443
|
+
const extra = rows[at];
|
|
1444
|
+
if (extra !== void 0) throw new NativeParseError(FORMAT$1, extra.line, `expected the end of the file after TECHNICIANS, found ${JSON.stringify(extra.fields.join(" "))}`);
|
|
1445
|
+
return {
|
|
1446
|
+
MACHINES: found.MACHINES ?? [],
|
|
1447
|
+
LOCATIONS: found.LOCATIONS ?? [],
|
|
1448
|
+
REQUESTS: found.REQUESTS ?? [],
|
|
1449
|
+
TECHNICIANS: found.TECHNICIANS ?? []
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Parses a VeRoLog Solver Challenge 2019 instance (the technician and truck routing problem of ORTEC's challenge): `KEY = value` header lines, then the sections MACHINES (id, size, idle penalty), LOCATIONS (id, x, y), REQUESTS (id, location, first day, last day, machine, amount) and TECHNICIANS (id, home location, maximum distance per day, maximum installations per day, then a 0 or 1 flag per machine for the machines the technician can install). Location 1 is the depot. The nodes are the depot, then one node per request at its location's position, carrying the request's day window, machine and amount; the machines and technicians stay unread beyond the technician count, as or/v1 has no place for a fleet. The days are day numbers, not times, and distances are the integer coordinates' own units.
|
|
1454
|
+
* @param text - The file text.
|
|
1455
|
+
* @returns The instance: its header values as `meta` and its nodes.
|
|
1456
|
+
* @throws NativeParseError naming the line for a header line that is missing, out of order or not of its type, a section that is out of order or short, a row whose ids or length are wrong, or a request whose location, machine or days are outside the file's.
|
|
1457
|
+
*/
|
|
1458
|
+
function parseVerolog2019(text) {
|
|
1459
|
+
const rows = rowsOf(text);
|
|
1460
|
+
const meta = {};
|
|
1461
|
+
let days = 0;
|
|
1462
|
+
for (const [position, [key, name, kind]] of HEADER$1.entries()) {
|
|
1463
|
+
const row = rows[position];
|
|
1464
|
+
const header = row === void 0 ? void 0 : keyed(row);
|
|
1465
|
+
if (row === void 0 || header?.key !== key || header.value.length === 0) throw new NativeParseError(FORMAT$1, row?.line ?? 1, `expected "${key} = <value>", found ${JSON.stringify(row?.fields.join(" "))}`);
|
|
1466
|
+
meta[name] = kind === "text" ? header.value.join(" ") : numberAt({
|
|
1467
|
+
line: row.line,
|
|
1468
|
+
fields: header.value
|
|
1469
|
+
}, 0, `a number for ${key}`);
|
|
1470
|
+
if (key === "DAYS") days = Number(meta[name]);
|
|
1471
|
+
}
|
|
1472
|
+
const sections = sectionsOf(rows.slice(HEADER$1.length));
|
|
1473
|
+
const machines = sections.MACHINES;
|
|
1474
|
+
const locations = sections.LOCATIONS;
|
|
1475
|
+
checkRows("MACHINES", machines, MACHINE_NUMBERS);
|
|
1476
|
+
checkRows("LOCATIONS", locations, LOCATION_NUMBERS);
|
|
1477
|
+
checkRows("REQUESTS", sections.REQUESTS, REQUEST_NUMBERS);
|
|
1478
|
+
checkRows("TECHNICIANS", sections.TECHNICIANS, TECHNICIAN_NUMBERS_BEFORE_FLAGS + machines.length);
|
|
1479
|
+
const position = (id) => {
|
|
1480
|
+
const row = locations[id - 1];
|
|
1481
|
+
return row === void 0 ? {
|
|
1482
|
+
x: NaN,
|
|
1483
|
+
y: NaN
|
|
1484
|
+
} : {
|
|
1485
|
+
x: numberAt(row, 1, "x"),
|
|
1486
|
+
y: numberAt(row, 2, "y")
|
|
1487
|
+
};
|
|
1488
|
+
};
|
|
1489
|
+
const depot = {
|
|
1490
|
+
id: "depot",
|
|
1491
|
+
...position(1),
|
|
1492
|
+
kind: "depot",
|
|
1493
|
+
location: 1,
|
|
1494
|
+
fromDay: null,
|
|
1495
|
+
toDay: null,
|
|
1496
|
+
machine: null,
|
|
1497
|
+
amount: null
|
|
1498
|
+
};
|
|
1499
|
+
const requests = sections.REQUESTS.map((row) => {
|
|
1500
|
+
const location = numberAt(row, REQUEST_AT.location, "a location id");
|
|
1501
|
+
const fromDay = numberAt(row, REQUEST_AT.fromDay, "the first day");
|
|
1502
|
+
const toDay = numberAt(row, REQUEST_AT.toDay, "the last day");
|
|
1503
|
+
const machine = numberAt(row, REQUEST_AT.machine, "a machine id");
|
|
1504
|
+
const amount = numberAt(row, REQUEST_AT.amount, "the amount");
|
|
1505
|
+
if (!Number.isInteger(location) || location < 1 || location > locations.length) throw new NativeParseError(FORMAT$1, row.line, `location ${String(location)} is outside 1 to ${String(locations.length)}`);
|
|
1506
|
+
if (!Number.isInteger(machine) || machine < 1 || machine > machines.length) throw new NativeParseError(FORMAT$1, row.line, `machine ${String(machine)} is outside 1 to ${String(machines.length)}`);
|
|
1507
|
+
if (fromDay < 1 || fromDay > toDay || toDay > days) throw new NativeParseError(FORMAT$1, row.line, `the days ${String(fromDay)} to ${String(toDay)} are not within 1 to ${String(days)}`);
|
|
1508
|
+
if (amount < 1) throw new NativeParseError(FORMAT$1, row.line, `expected an amount of at least 1, found ${String(amount)}`);
|
|
1509
|
+
return {
|
|
1510
|
+
id: row.fields[0] ?? "",
|
|
1511
|
+
...position(location),
|
|
1512
|
+
kind: "request",
|
|
1513
|
+
location,
|
|
1514
|
+
fromDay,
|
|
1515
|
+
toDay,
|
|
1516
|
+
machine: String(machine),
|
|
1517
|
+
amount
|
|
1518
|
+
};
|
|
1519
|
+
});
|
|
1520
|
+
return {
|
|
1521
|
+
meta: {
|
|
1522
|
+
...meta,
|
|
1523
|
+
technicians: sections.TECHNICIANS.length
|
|
1524
|
+
},
|
|
1525
|
+
nodes: [depot, ...requests]
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/parsers/xml.ts
|
|
1530
|
+
/** The characters an XML name may contain, narrowed to what the formats read use. */
|
|
1531
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;
|
|
1532
|
+
const ATTRIBUTE = /\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;
|
|
1533
|
+
const PREDEFINED = {
|
|
1534
|
+
amp: "&",
|
|
1535
|
+
lt: "<",
|
|
1536
|
+
gt: ">",
|
|
1537
|
+
quot: "\"",
|
|
1538
|
+
apos: "'"
|
|
1539
|
+
};
|
|
1540
|
+
const CODE_POINT = /^#(?:x([0-9A-Fa-f]+)|(\d+))$/;
|
|
1541
|
+
const HEXADECIMAL$1 = 16;
|
|
1542
|
+
const DECIMAL$1 = 10;
|
|
1543
|
+
/** The markup that runs from an opening marker to a closing one and holds no elements: comments, CDATA sections and processing instructions. */
|
|
1544
|
+
const COMMENT = {
|
|
1545
|
+
open: "<!--",
|
|
1546
|
+
close: "-->"
|
|
1547
|
+
};
|
|
1548
|
+
const CDATA = {
|
|
1549
|
+
open: "<![CDATA[",
|
|
1550
|
+
close: "]]>"
|
|
1551
|
+
};
|
|
1552
|
+
const INSTRUCTION = {
|
|
1553
|
+
open: "<?",
|
|
1554
|
+
close: "?>"
|
|
1555
|
+
};
|
|
1556
|
+
const DECLARATION = "<!";
|
|
1557
|
+
/** Reads an XML document into a tree; see `readXml`. */
|
|
1558
|
+
var XmlReader = class {
|
|
1559
|
+
#text;
|
|
1560
|
+
#format;
|
|
1561
|
+
#stack = [];
|
|
1562
|
+
#root;
|
|
1563
|
+
#position = 0;
|
|
1564
|
+
constructor(text, format) {
|
|
1565
|
+
this.#text = text;
|
|
1566
|
+
this.#format = format;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Fails at a position of the document.
|
|
1570
|
+
* @param index - Where in the text the fault is.
|
|
1571
|
+
* @param message - What is wrong.
|
|
1572
|
+
* @throws NativeParseError naming the line of the position.
|
|
1573
|
+
*/
|
|
1574
|
+
#fail(index, message) {
|
|
1575
|
+
throw new NativeParseError(this.#format, this.#text.slice(0, index).split("\n").length, message);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Replaces the character references and the five predefined entities in text.
|
|
1579
|
+
* @param text - Text from an element or an attribute value.
|
|
1580
|
+
* @param index - Where the text starts, for the error.
|
|
1581
|
+
* @returns The text with references resolved.
|
|
1582
|
+
*/
|
|
1583
|
+
#unescape(text, index) {
|
|
1584
|
+
return text.replace(/&([^;&\s]*);/g, (whole, reference) => {
|
|
1585
|
+
const predefined = Object.hasOwn(PREDEFINED, reference) ? PREDEFINED[reference] : void 0;
|
|
1586
|
+
if (predefined !== void 0) return predefined;
|
|
1587
|
+
const code = CODE_POINT.exec(reference);
|
|
1588
|
+
if (code === null) return this.#fail(index, `the entity ${whole} is not one of the five predefined ones`);
|
|
1589
|
+
return String.fromCodePoint(code[1] === void 0 ? Number.parseInt(code[2] ?? "", DECIMAL$1) : Number.parseInt(code[1], HEXADECIMAL$1));
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Finishes an element: it becomes a child of the open element, or the root.
|
|
1594
|
+
* @param open - The element.
|
|
1595
|
+
*/
|
|
1596
|
+
#finish(open) {
|
|
1597
|
+
const element = {
|
|
1598
|
+
name: open.name,
|
|
1599
|
+
attributes: open.attributes,
|
|
1600
|
+
children: open.children,
|
|
1601
|
+
text: open.text
|
|
1602
|
+
};
|
|
1603
|
+
const parent = this.#stack[this.#stack.length - 1];
|
|
1604
|
+
if (parent !== void 0) parent.children.push(element);
|
|
1605
|
+
else if (this.#root === void 0) this.#root = element;
|
|
1606
|
+
else this.#fail(open.index, "the document has more than one root element");
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Reads the `name="value"` pairs of a tag.
|
|
1610
|
+
* @param text - The tag's text after its name.
|
|
1611
|
+
* @param index - Where the tag starts, for errors.
|
|
1612
|
+
* @returns The attributes.
|
|
1613
|
+
*/
|
|
1614
|
+
#attributes(text, index) {
|
|
1615
|
+
const attributes = {};
|
|
1616
|
+
ATTRIBUTE.lastIndex = 0;
|
|
1617
|
+
let consumed = 0;
|
|
1618
|
+
for (let match = ATTRIBUTE.exec(text); match !== null; match = ATTRIBUTE.exec(text)) {
|
|
1619
|
+
const [, key = "", doubled, single] = match;
|
|
1620
|
+
if (Object.hasOwn(attributes, key)) this.#fail(index, `the attribute ${key} appears twice`);
|
|
1621
|
+
attributes[key] = this.#unescape(doubled ?? single ?? "", index);
|
|
1622
|
+
consumed = ATTRIBUTE.lastIndex;
|
|
1623
|
+
}
|
|
1624
|
+
if (text.slice(consumed).trim() !== "") this.#fail(index, `the attributes ${JSON.stringify(text.trim())} are not name="value" pairs`);
|
|
1625
|
+
return attributes;
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Reads a start tag, an empty-element tag or an end tag that begins at the position.
|
|
1629
|
+
* @returns The position after it.
|
|
1630
|
+
*/
|
|
1631
|
+
#tag() {
|
|
1632
|
+
const start = this.#position;
|
|
1633
|
+
const stop = this.#text.indexOf(">", start);
|
|
1634
|
+
if (stop === -1) return this.#fail(start, "a tag is never closed");
|
|
1635
|
+
const tag = this.#text.slice(start + 1, stop);
|
|
1636
|
+
if (tag.startsWith("/")) {
|
|
1637
|
+
const finished = this.#stack.pop();
|
|
1638
|
+
if (finished?.name !== tag.slice(1).trim()) return this.#fail(start, `the closing tag ${JSON.stringify(tag)} does not close ${finished === void 0 ? "any element" : JSON.stringify(finished.name)}`);
|
|
1639
|
+
this.#finish(finished);
|
|
1640
|
+
return stop + 1;
|
|
1641
|
+
}
|
|
1642
|
+
const selfClosing = tag.endsWith("/");
|
|
1643
|
+
const body = selfClosing ? tag.slice(0, -1) : tag;
|
|
1644
|
+
const name = /^\S+/.exec(body)?.[0] ?? "";
|
|
1645
|
+
if (!NAME.test(name)) return this.#fail(start, `${JSON.stringify(name)} is not an element name`);
|
|
1646
|
+
const element = {
|
|
1647
|
+
name,
|
|
1648
|
+
attributes: this.#attributes(body.slice(name.length), start),
|
|
1649
|
+
children: [],
|
|
1650
|
+
text: "",
|
|
1651
|
+
index: start
|
|
1652
|
+
};
|
|
1653
|
+
if (selfClosing) this.#finish(element);
|
|
1654
|
+
else this.#stack.push(element);
|
|
1655
|
+
return stop + 1;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Skips markup that runs to a closing marker, or reads it as text.
|
|
1659
|
+
* @param markers - The opening and closing markers.
|
|
1660
|
+
* @param what - What the markup is, for the error.
|
|
1661
|
+
* @returns The position after it and its content.
|
|
1662
|
+
*/
|
|
1663
|
+
#delimited(markers, what) {
|
|
1664
|
+
const from = this.#position + markers.open.length;
|
|
1665
|
+
const stop = this.#text.indexOf(markers.close, from);
|
|
1666
|
+
if (stop === -1) return this.#fail(this.#position, `${what} is never closed`);
|
|
1667
|
+
return {
|
|
1668
|
+
next: stop + markers.close.length,
|
|
1669
|
+
content: this.#text.slice(from, stop)
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Reads the markup at the position, which starts with `<`.
|
|
1674
|
+
* @returns The position after it.
|
|
1675
|
+
*/
|
|
1676
|
+
#markup() {
|
|
1677
|
+
const rest = this.#text.startsWith.bind(this.#text);
|
|
1678
|
+
const open = this.#stack[this.#stack.length - 1];
|
|
1679
|
+
if (rest(COMMENT.open, this.#position)) return this.#delimited(COMMENT, "a comment").next;
|
|
1680
|
+
if (rest(CDATA.open, this.#position)) {
|
|
1681
|
+
const { next, content } = this.#delimited(CDATA, "a CDATA section");
|
|
1682
|
+
if (open === void 0) return this.#fail(this.#position, "a CDATA section outside the root element");
|
|
1683
|
+
open.text += content;
|
|
1684
|
+
return next;
|
|
1685
|
+
}
|
|
1686
|
+
if (rest(INSTRUCTION.open, this.#position)) return this.#delimited(INSTRUCTION, "a processing instruction").next;
|
|
1687
|
+
if (rest(DECLARATION, this.#position)) return this.#fail(this.#position, "a document type declaration is not read: it can define entities, and the formats here have none");
|
|
1688
|
+
return this.#tag();
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Reads the whole document.
|
|
1692
|
+
* @returns The root element.
|
|
1693
|
+
*/
|
|
1694
|
+
read() {
|
|
1695
|
+
while (this.#position < this.#text.length) {
|
|
1696
|
+
const lessThan = this.#text.indexOf("<", this.#position);
|
|
1697
|
+
const end = lessThan === -1 ? this.#text.length : lessThan;
|
|
1698
|
+
const content = this.#text.slice(this.#position, end);
|
|
1699
|
+
const open = this.#stack[this.#stack.length - 1];
|
|
1700
|
+
if (open !== void 0) open.text += this.#unescape(content, this.#position);
|
|
1701
|
+
else if (content.trim() !== "") this.#fail(this.#position, "text outside the root element");
|
|
1702
|
+
if (lessThan === -1) break;
|
|
1703
|
+
this.#position = lessThan;
|
|
1704
|
+
this.#position = this.#markup();
|
|
1705
|
+
}
|
|
1706
|
+
const unclosed = this.#stack[this.#stack.length - 1];
|
|
1707
|
+
if (unclosed !== void 0) this.#fail(this.#text.length, `the element ${JSON.stringify(unclosed.name)} is never closed`);
|
|
1708
|
+
return this.#root ?? this.#fail(0, "the document has no root element");
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
/**
|
|
1712
|
+
* Reads an XML document into a tree. It reads what data files use: elements, attributes in single or double quotes, text, CDATA sections, comments, processing instructions and the XML declaration, and the predefined entities and character references. A document type declaration is refused, because it can define entities that expand without bound and nothing here reads one. Namespaces are not interpreted: a prefix is part of the name.
|
|
1713
|
+
* @param text - The document text.
|
|
1714
|
+
* @param format - The name of the format being parsed, for errors.
|
|
1715
|
+
* @returns The root element.
|
|
1716
|
+
* @throws NativeParseError naming the line for markup that is malformed, a tag that does not close the open one, an undeclared entity, a document type declaration, text outside the root or more than one root.
|
|
1717
|
+
*/
|
|
1718
|
+
function readXml(text, format) {
|
|
1719
|
+
return new XmlReader(text, format).read();
|
|
1720
|
+
}
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/parsers/vrp-rep.ts
|
|
1723
|
+
const FORMAT = "vrp-rep";
|
|
1724
|
+
/**
|
|
1725
|
+
* The child elements of an element with a name.
|
|
1726
|
+
* @param element - The parent.
|
|
1727
|
+
* @param name - The child element name.
|
|
1728
|
+
* @returns Them, in document order.
|
|
1729
|
+
*/
|
|
1730
|
+
function childrenOf(element, name) {
|
|
1731
|
+
return element.children.filter((child) => child.name === name);
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* The one child element of an element with a name.
|
|
1735
|
+
* @param element - The parent.
|
|
1736
|
+
* @param name - The child element name.
|
|
1737
|
+
* @returns It.
|
|
1738
|
+
* @throws NativeParseError when the parent has none or several.
|
|
1739
|
+
*/
|
|
1740
|
+
function childOf(element, name) {
|
|
1741
|
+
const found = childrenOf(element, name);
|
|
1742
|
+
const [only] = found;
|
|
1743
|
+
if (found.length !== 1 || only === void 0) throw new NativeParseError(FORMAT, 1, `expected one <${name}> in <${element.name}>, found ${String(found.length)}`);
|
|
1744
|
+
return only;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Reads an attribute that must be there.
|
|
1748
|
+
* @param element - The element.
|
|
1749
|
+
* @param name - The attribute name.
|
|
1750
|
+
* @returns Its value.
|
|
1751
|
+
* @throws NativeParseError when the element has no such attribute.
|
|
1752
|
+
*/
|
|
1753
|
+
function attributeOf(element, name) {
|
|
1754
|
+
const value = Object.hasOwn(element.attributes, name) ? element.attributes[name] : void 0;
|
|
1755
|
+
if (value === void 0) throw new NativeParseError(FORMAT, 1, `expected <${element.name}> to have the attribute ${name}`);
|
|
1756
|
+
return value;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Reads the text of an element as a finite number.
|
|
1760
|
+
* @param element - The element.
|
|
1761
|
+
* @returns The number.
|
|
1762
|
+
* @throws NativeParseError when the text is not a decimal number.
|
|
1763
|
+
*/
|
|
1764
|
+
function numberOf(element) {
|
|
1765
|
+
const text = element.text.trim();
|
|
1766
|
+
if (!/^-?\d+(?:\.\d+)?$/.test(text)) throw new NativeParseError(FORMAT, 1, `expected <${element.name}> to hold a number, found ${JSON.stringify(text)}`);
|
|
1767
|
+
return Number(text);
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Joins ids into the list text a column holds.
|
|
1771
|
+
* @param ids - The ids, in document order.
|
|
1772
|
+
* @returns The distinct ids joined by commas, or null when there are none.
|
|
1773
|
+
*/
|
|
1774
|
+
function listOf(ids) {
|
|
1775
|
+
const distinct = [...new Set(ids)];
|
|
1776
|
+
return distinct.length === 0 ? null : distinct.join(",");
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Parses a VRP-REP instance (the XML format of the VRP-REP repository) in the layout its technician routing instances use: an `instance` with `info`, a `network` of `node` elements with `cx` and `cy`, a `fleet` of vehicle profiles that each depart from a node, `requests` that each sit at a node and name the skill and resources they need, and `drivers` whose profiles list the skills a vehicle type's driver has. Each node becomes a row with its position and `type`; a node's request skills and resources, and the skills of the drivers whose vehicles depart from it, are attached to it. Fleet costs and resource stocks are not read, as or/v1 has no place for them.
|
|
1780
|
+
* @param text - The file text.
|
|
1781
|
+
* @returns The instance: its dataset and name as `meta`, and its nodes in document order.
|
|
1782
|
+
* @throws NativeParseError for malformed XML, a root that is not `instance`, a missing element or attribute, a coordinate that is not a number, a repeated node id, or a request or vehicle that names a node the network lacks.
|
|
1783
|
+
*/
|
|
1784
|
+
function parseVrpRep(text) {
|
|
1785
|
+
const root = readXml(text, FORMAT);
|
|
1786
|
+
if (root.name !== "instance") throw new NativeParseError(FORMAT, 1, `expected the root element <instance>, found <${root.name}>`);
|
|
1787
|
+
const info = childOf(root, "info");
|
|
1788
|
+
const nodeElements = childrenOf(childOf(childOf(root, "network"), "nodes"), "node");
|
|
1789
|
+
const known = /* @__PURE__ */ new Set();
|
|
1790
|
+
for (const node of nodeElements) {
|
|
1791
|
+
const id = attributeOf(node, "id");
|
|
1792
|
+
if (known.has(id)) throw new NativeParseError(FORMAT, 1, `node ${id} appears twice`);
|
|
1793
|
+
known.add(id);
|
|
1794
|
+
}
|
|
1795
|
+
const needs = /* @__PURE__ */ new Map();
|
|
1796
|
+
for (const request of childrenOf(childOf(root, "requests"), "request")) {
|
|
1797
|
+
const node = attributeOf(request, "node");
|
|
1798
|
+
if (!known.has(node)) throw new NativeParseError(FORMAT, 1, `request ${attributeOf(request, "id")} is at node ${node}, which the network lacks`);
|
|
1799
|
+
const entry = needs.get(node) ?? {
|
|
1800
|
+
skills: [],
|
|
1801
|
+
resources: []
|
|
1802
|
+
};
|
|
1803
|
+
entry.skills.push(...childrenOf(request, "skill").map((skill) => skill.text.trim()));
|
|
1804
|
+
entry.resources.push(...childrenOf(request, "resource").map((resource) => attributeOf(resource, "id")));
|
|
1805
|
+
needs.set(node, entry);
|
|
1806
|
+
}
|
|
1807
|
+
const driverSkills = /* @__PURE__ */ new Map();
|
|
1808
|
+
for (const profile of childrenOf(childOf(root, "drivers"), "driver_profile")) driverSkills.set(attributeOf(profile, "type"), childrenOf(profile, "skill").map((skill) => attributeOf(skill, "id")));
|
|
1809
|
+
const offered = /* @__PURE__ */ new Map();
|
|
1810
|
+
for (const vehicle of childrenOf(childOf(root, "fleet"), "vehicle_profile")) {
|
|
1811
|
+
const home = childOf(vehicle, "departure_node").text.trim();
|
|
1812
|
+
if (!known.has(home)) throw new NativeParseError(FORMAT, 1, `vehicle type ${attributeOf(vehicle, "type")} departs from node ${home}, which the network lacks`);
|
|
1813
|
+
offered.set(home, [...offered.get(home) ?? [], ...driverSkills.get(attributeOf(vehicle, "type")) ?? []]);
|
|
1814
|
+
}
|
|
1815
|
+
const nodes = nodeElements.map((node) => {
|
|
1816
|
+
const id = attributeOf(node, "id");
|
|
1817
|
+
const need = needs.get(id);
|
|
1818
|
+
return {
|
|
1819
|
+
id,
|
|
1820
|
+
x: numberOf(childOf(node, "cx")),
|
|
1821
|
+
y: numberOf(childOf(node, "cy")),
|
|
1822
|
+
type: attributeOf(node, "type"),
|
|
1823
|
+
skill: listOf(need?.skills ?? []),
|
|
1824
|
+
skills: listOf(offered.get(id) ?? []),
|
|
1825
|
+
resources: listOf(need?.resources ?? [])
|
|
1826
|
+
};
|
|
1827
|
+
});
|
|
1828
|
+
return {
|
|
1829
|
+
meta: {
|
|
1830
|
+
dataset: childOf(info, "dataset").text.trim(),
|
|
1831
|
+
name: childOf(info, "name").text.trim()
|
|
1832
|
+
},
|
|
1833
|
+
nodes
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
//#endregion
|
|
1257
1837
|
//#region src/parsers/parse-native.ts
|
|
1258
1838
|
/** Bytes converted per String.fromCharCode call: spreading a whole file would exceed the engine's argument-count limit, and this many stays well inside every engine's. */
|
|
1259
1839
|
const LATIN1_CHUNK = 32768;
|
|
@@ -1309,10 +1889,13 @@ function parseNative(bytes, parser) {
|
|
|
1309
1889
|
if (parser.format === "edge-list") return parseEdgeList(text, parser);
|
|
1310
1890
|
if (parser.format === "matrix-market") return parseMatrixMarket(text);
|
|
1311
1891
|
if (parser.format === "gml") return parseGml(text);
|
|
1892
|
+
if (parser.format === "customer-points") return parseCustomerPoints(text);
|
|
1312
1893
|
if (parser.format === "solomon") return parseSolomon(text);
|
|
1313
1894
|
if (parser.format === "top") return parseTop(text);
|
|
1314
1895
|
if (parser.format === "toptw") return parseToptw(text);
|
|
1315
1896
|
if (parser.format === "tsplib") return parseTsplib(text);
|
|
1897
|
+
if (parser.format === "verolog-2019") return parseVerolog2019(text);
|
|
1898
|
+
if (parser.format === "vrp-rep") return parseVrpRep(text);
|
|
1316
1899
|
return parsePajek(text);
|
|
1317
1900
|
}
|
|
1318
1901
|
/** The ustar header layout (POSIX.1-1988, pax(1) "ustar Interchange Format"): each field's byte offset and length. */
|
|
@@ -1700,7 +2283,7 @@ function isResourceFork(path) {
|
|
|
1700
2283
|
return path.startsWith("__MACOSX/") || path.split("/").some((segment) => segment.startsWith("._"));
|
|
1701
2284
|
}
|
|
1702
2285
|
/**
|
|
1703
|
-
* Whether a member path matches a selector: a path selector matches the whole member path or its trailing segments, an extensions selector the end of the member's name
|
|
2286
|
+
* Whether a member path matches a selector: a path selector matches the whole member path or its trailing segments, an extensions selector the end of the member's name (ignoring case), a pattern selector the whole path.
|
|
1704
2287
|
* @param path - The member path.
|
|
1705
2288
|
* @param member - The recipe's selector.
|
|
1706
2289
|
* @returns True on a match.
|
|
@@ -1708,9 +2291,20 @@ function isResourceFork(path) {
|
|
|
1708
2291
|
function selects(path, member) {
|
|
1709
2292
|
if (isResourceFork(path) || path.endsWith("/")) return false;
|
|
1710
2293
|
if ("path" in member) return path === member.path || path.endsWith(`/${member.path}`);
|
|
2294
|
+
if ("pattern" in member) return new RegExp(`^(?:${member.pattern})$`, "u").test(path);
|
|
1711
2295
|
return member.extensions.some((extension) => path.toLowerCase().endsWith(extension.toLowerCase()));
|
|
1712
2296
|
}
|
|
1713
2297
|
/**
|
|
2298
|
+
* Describes what a selector asks for, for an error message.
|
|
2299
|
+
* @param member - The selector.
|
|
2300
|
+
* @param noun - How to name the member or members.
|
|
2301
|
+
* @returns The member path, extensions or pattern it names.
|
|
2302
|
+
*/
|
|
2303
|
+
function describeSelector(member, noun) {
|
|
2304
|
+
if ("path" in member) return `${noun === "one member" ? "member" : noun} ${member.path}`;
|
|
2305
|
+
return "pattern" in member ? `${noun} matching ${member.pattern}` : `${noun} ending ${member.extensions.join(" or ")}`;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
1714
2308
|
* Picks every member a selector matches, in path order, so a collection reads in the same order wherever it is unpacked.
|
|
1715
2309
|
* @param paths - Every member path.
|
|
1716
2310
|
* @param member - The recipe's selector.
|
|
@@ -1719,7 +2313,7 @@ function selects(path, member) {
|
|
|
1719
2313
|
*/
|
|
1720
2314
|
function pickAll(paths, member) {
|
|
1721
2315
|
const matched = paths.filter((path) => selects(path, member)).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
1722
|
-
if (matched.length === 0) throw new ArchiveError(`expected at least one ${
|
|
2316
|
+
if (matched.length === 0) throw new ArchiveError(`expected at least one ${describeSelector(member, "member")}, found none among: ${paths.join(", ")}`);
|
|
1723
2317
|
return matched;
|
|
1724
2318
|
}
|
|
1725
2319
|
/**
|
|
@@ -1732,10 +2326,7 @@ function pickAll(paths, member) {
|
|
|
1732
2326
|
function pick(paths, member) {
|
|
1733
2327
|
const matched = paths.filter((path) => selects(path, member));
|
|
1734
2328
|
const [only] = matched;
|
|
1735
|
-
if (matched.length !== 1 || only === void 0) {
|
|
1736
|
-
const wanted = "path" in member ? `member ${member.path}` : `one member ending ${member.extensions.join(" or ")}`;
|
|
1737
|
-
throw new ArchiveError(`expected ${wanted}, found ${String(matched.length)} among: ${paths.join(", ")}`);
|
|
1738
|
-
}
|
|
2329
|
+
if (matched.length !== 1 || only === void 0) throw new ArchiveError(`expected ${describeSelector(member, "one member")}, found ${String(matched.length)} among: ${paths.join(", ")}`);
|
|
1739
2330
|
return only;
|
|
1740
2331
|
}
|
|
1741
2332
|
/**
|