keepset 0.25.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.
@@ -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-Dkvod55t.js";
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$3 = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?/y;
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$3, text, position) : void 0;
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$1 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
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$1.test(cell)) throw new NativeParseError("csv-table", line, `column ${column} is declared numeric but holds ${JSON.stringify(cell)}`);
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$2 = "solomon";
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$2 = /^-?\d+(?:\.\d+)?$/;
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$2 = /^-?\d+(?:\.\d+)?$/;
844
881
  * @returns The number.
845
882
  * @throws NativeParseError when the token is not a decimal number.
846
883
  */
847
- function numberOf$1(token, line, what) {
848
- if (!NUMBER$2.test(token)) throw new NativeParseError(FORMAT$2, line, `expected ${what}, found ${JSON.stringify(token)}`);
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$2, 1, "expected the instance name on the first line");
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$2, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
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$2, line, `expected the vehicle count and capacity, found ${JSON.stringify(trimmed)}`);
886
- meta.vehicles = numberOf$1(vehicles, line, "the vehicle count");
887
- meta.capacity = numberOf$1(capacity, line, "the vehicle capacity");
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$2, line, `expected seven numbers (id, x, y, demand, ready, due, service), found ${JSON.stringify(trimmed)}`);
892
- if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$2, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
893
- if (seen.has(id)) throw new NativeParseError(FORMAT$2, line, `node ${id} appears twice`);
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$1(x, line, "x"),
898
- y: numberOf$1(y, line, "y"),
899
- demand: numberOf$1(demand, line, "the demand"),
900
- ready: numberOf$1(ready, line, "the ready time"),
901
- due: numberOf$1(due, line, "the due date"),
902
- service: numberOf$1(service, line, "the service time")
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$2, all.length, "expected a CUSTOMER section");
906
- if (nodes.length === 0) throw new NativeParseError(FORMAT$2, all.length, "the CUSTOMER section has no rows");
907
- if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$2, all.length, "expected a VEHICLE section with the vehicle count and capacity");
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$1 = "top";
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$1 = /^-?\d+(?:\.\d+)?$/;
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$1 = /^-?\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$1.test(value)) throw new NativeParseError(FORMAT$1, line, `expected "${key} <number>", found ${JSON.stringify((text ?? "").trim())}`);
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$1, first?.line ?? 1, `expected at least a start and an end point, found n ${String(points)}`);
953
- if (rows.length !== points) throw new NativeParseError(FORMAT$1, all[all.length - 1]?.line ?? 1, `the header gives ${String(points)} points, found ${String(rows.length)} rows`);
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$1.test(x) || !NUMBER$1.test(y) || !NUMBER$1.test(score)) throw new NativeParseError(FORMAT$1, row.line, `expected three numbers (x, y, score), found ${JSON.stringify(row.text)}`);
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 = "toptw";
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 = /^-?\d+(?:\.\d+)?$/;
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 = /^-?\d+(?:\.\d+)?$/;
990
1027
  * @returns The number.
991
1028
  * @throws NativeParseError when the token is not a decimal number.
992
1029
  */
993
- function numberOf(token, line, what) {
994
- if (token === void 0 || !NUMBER.test(token)) throw new NativeParseError(FORMAT, line, `expected ${what}, found ${JSON.stringify(token)}`);
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(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, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
1009
- return tokens.map((token) => numberOf(token, line, what));
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, 1, "expected the two header lines");
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, first.line, "the header lacks a number");
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(tokens[LIST_LENGTH_AT], row.line, "the length of the list after the unused number");
1031
- if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT, row.line, `expected ${String(ROW_NUMBERS_BESIDE_LIST + listLength)} numbers for a list of ${String(listLength)}, found ${String(tokens.length)}`);
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, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1034
- if (seen.has(id)) throw new NativeParseError(FORMAT, row.line, `node ${id} appears twice`);
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(tokens[1], row.line, "x"),
1039
- y: numberOf(tokens[2], row.line, "y"),
1040
- service: numberOf(tokens[3], row.line, "the service duration"),
1041
- profit: numberOf(tokens[4], row.line, "the profit"),
1042
- open: numberOf(tokens[LIST_STARTS_AT + listLength], row.line, "the opening of the window"),
1043
- close: numberOf(tokens[8 + listLength], row.line, "the closing of the window")
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, 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)}`);
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,
@@ -1054,6 +1091,749 @@ function parseToptw(text) {
1054
1091
  };
1055
1092
  }
1056
1093
  //#endregion
1094
+ //#region src/parsers/tsplib.ts
1095
+ const FORMAT$2 = "tsplib";
1096
+ /** The instance types the parser reads: the probabilistic and the clustered orienteering problem. */
1097
+ const TYPES = /* @__PURE__ */ new Set(["POP", "COP"]);
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. */
1099
+ const PLANAR = /* @__PURE__ */ new Set([
1100
+ "EUC_2D",
1101
+ "CEIL_2D",
1102
+ "ATT"
1103
+ ]);
1104
+ const NUMBER$1 = /^-?\d+(?:\.\d+)?$/;
1105
+ const WHOLE$1 = /^\d+$/;
1106
+ const HEADER$2 = /^([A-Z][A-Z_]*)\s*:\s*(.*)$/;
1107
+ const SECTION = /^[A-Z][A-Z_]*$/;
1108
+ /** Minutes in a degree, which a `DDD.MM` coordinate counts in. */
1109
+ const MINUTES_PER_DEGREE = 60;
1110
+ /** `DDD.MM` has two digits of minutes, so the fraction is read in hundredths. */
1111
+ const MINUTE_SCALE = 100;
1112
+ /** How far a coordinate's hundredths may lie from a whole number of minutes: float error in the text's value, not a third digit. */
1113
+ const MINUTE_TOLERANCE = 1e-6;
1114
+ /**
1115
+ * Converts a TSPLIB `GEO` coordinate to decimal degrees. The value is `DDD.MM`, degrees then minutes in one number (`48.23` is 48 degrees 23 minutes), so the fraction is minutes out of a hundred, not a decimal fraction of a degree. The degrees are truncated, which is what makes the conversion agree with the optimal tour lengths the instances record where the specification's `nint` would round minutes of 50 or more up to the next degree.
1116
+ * @param value - The coordinate as the file writes it.
1117
+ * @param line - The 1-based line, for the error.
1118
+ * @returns Decimal degrees, signed like the input.
1119
+ * @throws NativeParseError when the minutes are not a whole number below sixty.
1120
+ */
1121
+ function degrees(value, line) {
1122
+ const magnitude = Math.abs(value);
1123
+ const whole = Math.trunc(magnitude);
1124
+ const hundredths = (magnitude - whole) * MINUTE_SCALE;
1125
+ const minutes = Math.round(hundredths);
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)}`);
1127
+ return Math.sign(value) * (whole + minutes / MINUTES_PER_DEGREE);
1128
+ }
1129
+ /**
1130
+ * Reads a token as a finite number.
1131
+ * @param token - The token.
1132
+ * @param line - The 1-based line, for the error.
1133
+ * @param what - What the token should be, for the error.
1134
+ * @returns The number.
1135
+ * @throws NativeParseError when the token is not a decimal number.
1136
+ */
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)}`);
1139
+ return Number(token);
1140
+ }
1141
+ /**
1142
+ * Reads a token as a whole number.
1143
+ * @param token - The token.
1144
+ * @param line - The 1-based line, for the error.
1145
+ * @param what - What the token should be, for the error.
1146
+ * @returns The number.
1147
+ * @throws NativeParseError when the token is not a whole number.
1148
+ */
1149
+ function wholeOf(token, line, what) {
1150
+ if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$2, line, `expected ${what}, found ${JSON.stringify(token)}`);
1151
+ return Number(token);
1152
+ }
1153
+ /**
1154
+ * Reads the header, the coordinate section, the prize and probability section and the set section of a file, skipping every other section.
1155
+ * @param text - The file text.
1156
+ * @returns What it holds.
1157
+ * @throws NativeParseError naming the line for a repeated key, a malformed row, a repeated node or set id, or a row outside any section.
1158
+ */
1159
+ function collect(text) {
1160
+ const collected = {
1161
+ header: /* @__PURE__ */ new Map(),
1162
+ coords: /* @__PURE__ */ new Map(),
1163
+ prizes: /* @__PURE__ */ new Map(),
1164
+ sets: []
1165
+ };
1166
+ let section;
1167
+ for (const [index, raw] of lines(text).entries()) {
1168
+ const line = index + 1;
1169
+ const trimmed = raw.trim();
1170
+ if (trimmed === "" || trimmed === "EOF") continue;
1171
+ const keyed = HEADER$2.exec(trimmed);
1172
+ const key = keyed?.[1];
1173
+ if (key === "CONSTRAINING_SET_SECTION") {
1174
+ section = "sets";
1175
+ continue;
1176
+ }
1177
+ if (keyed !== null && key !== void 0) {
1178
+ if (collected.header.has(key)) throw new NativeParseError(FORMAT$2, line, `${key} appears twice`);
1179
+ collected.header.set(key, (keyed[2] ?? "").trim());
1180
+ section = void 0;
1181
+ continue;
1182
+ }
1183
+ if (SECTION.test(trimmed)) {
1184
+ section = trimmed === "NODE_COORD_SECTION" ? "coords" : trimmed === "NODE_PRIZE_PROBABILITY_SECTION" ? "prizes" : "skipped";
1185
+ continue;
1186
+ }
1187
+ const tokens = trimmed.split(/\s+/);
1188
+ if (section === "coords") {
1189
+ const [id, x, y, ...extra] = tokens;
1190
+ if (extra.length > 0) throw new NativeParseError(FORMAT$2, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
1191
+ const node = wholeOf(id, line, "a node id");
1192
+ if (collected.coords.has(node)) throw new NativeParseError(FORMAT$2, line, `node ${String(node)} has two coordinate rows`);
1193
+ collected.coords.set(node, {
1194
+ x: numberOf$1(x, line, "x"),
1195
+ y: numberOf$1(y, line, "y"),
1196
+ line
1197
+ });
1198
+ } else if (section === "prizes") {
1199
+ const [id, prize, probability, ...extra] = tokens;
1200
+ if (extra.length > 0) throw new NativeParseError(FORMAT$2, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
1201
+ const node = wholeOf(id, line, "a node id");
1202
+ if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$2, line, `node ${String(node)} has two prize rows`);
1203
+ collected.prizes.set(node, {
1204
+ prize: numberOf$1(prize, line, "the prize"),
1205
+ probability: numberOf$1(probability, line, "the probability")
1206
+ });
1207
+ } else if (section === "sets") {
1208
+ const [id, prize, ...members] = tokens;
1209
+ const setId = wholeOf(id, line, "a set id");
1210
+ if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$2, line, `set ${String(setId)} appears twice`);
1211
+ collected.sets.push({
1212
+ id: setId,
1213
+ prize: numberOf$1(prize, line, "the set prize"),
1214
+ members: members.map((member) => wholeOf(member, line, "a node id in a set"))
1215
+ });
1216
+ } else if (section === void 0) throw new NativeParseError(FORMAT$2, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
1217
+ }
1218
+ return collected;
1219
+ }
1220
+ /**
1221
+ * Reads a header value that must be there.
1222
+ * @param header - The header.
1223
+ * @param key - The key.
1224
+ * @returns Its text.
1225
+ * @throws NativeParseError when the file has no such key.
1226
+ */
1227
+ function required(header, key) {
1228
+ const value = header.get(key);
1229
+ if (value === void 0) throw new NativeParseError(FORMAT$2, 1, `expected a ${key} line`);
1230
+ return value;
1231
+ }
1232
+ /**
1233
+ * Parses a TSPLIB style orienteering instance of type `POP` (probabilistic orienteering, Angelelli and others) or `COP` (clustered orienteering), in the layout TSPLIB95 (Reinelt) defines: `KEY : value` header lines and bare keyword sections. The nodes are 1 to `DIMENSION`. The planar weight types keep their coordinates as `x` and `y` and `GEO` converts them to `lat` and `lon`; an `EXPLICIT` file has no coordinates, so its nodes have only the prize data. A `POP` file's `NODE_PRIZE_PROBABILITY_SECTION` gives each node its prize and probability. A `COP` file has no prize on a node: its prizes belong to sets of nodes that may overlap, so each node lists the sets it is in under `clusters` and the sets' prizes are the `setPrizes` metadata, and nothing is divided among the members. The weight matrix is not read, as or/v1 has no arcs.
1234
+ * @param text - The file text.
1235
+ * @returns The instance: its header values as `meta` and its nodes in id order.
1236
+ * @throws NativeParseError naming the line for an unsupported type or weight type, a missing key or section, a node id outside 1 to `DIMENSION` or missing from a section that must cover every node, or a set that names a node outside that range.
1237
+ */
1238
+ function parseTsplib(text) {
1239
+ const { header, coords, prizes, sets } = collect(text);
1240
+ const type = required(header, "TYPE");
1241
+ if (!TYPES.has(type)) throw new NativeParseError(FORMAT$2, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
1242
+ const weightType = required(header, "EDGE_WEIGHT_TYPE");
1243
+ const geographic = weightType === "GEO";
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)}`);
1245
+ const dimension = wholeOf(required(header, "DIMENSION"), 1, "DIMENSION");
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)}`);
1248
+ const inRange = (id) => id >= 1 && id <= dimension;
1249
+ for (const id of [
1250
+ ...coords.keys(),
1251
+ ...prizes.keys(),
1252
+ ...sets.flatMap((set) => set.members)
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)}`);
1255
+ if (type === "COP") {
1256
+ const declared = wholeOf(required(header, "SETS"), 1, "SETS");
1257
+ if (sets.length !== declared) throw new NativeParseError(FORMAT$2, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
1258
+ }
1259
+ const clustersOf = (node) => {
1260
+ const ids = sets.filter((set) => set.members.includes(node)).map((set) => String(set.id));
1261
+ return ids.length === 0 ? null : ids.join(",");
1262
+ };
1263
+ const nodes = Array.from({ length: dimension }, (_, index) => {
1264
+ const id = index + 1;
1265
+ const position = coords.get(id);
1266
+ const prize = prizes.get(id);
1267
+ const lat = geographic && position !== void 0 ? degrees(position.x, position.line) : null;
1268
+ const lon = geographic && position !== void 0 ? degrees(position.y, position.line) : null;
1269
+ return {
1270
+ id: String(id),
1271
+ x: geographic ? null : position?.x ?? null,
1272
+ y: geographic ? null : position?.y ?? null,
1273
+ lat,
1274
+ lon,
1275
+ prize: prize?.prize ?? null,
1276
+ probability: prize?.probability ?? null,
1277
+ clusters: clustersOf(id)
1278
+ };
1279
+ });
1280
+ const total = header.get("TPRIZE");
1281
+ return {
1282
+ meta: {
1283
+ name: header.get("NAME") ?? null,
1284
+ type,
1285
+ budget,
1286
+ edgeWeightType: weightType,
1287
+ totalPrize: total === void 0 ? null : numberOf$1(total, 1, "TPRIZE"),
1288
+ setPrizes: type === "COP" ? sets.map((set) => `${String(set.id)}:${String(set.prize)}`).join(",") : null
1289
+ },
1290
+ nodes
1291
+ };
1292
+ }
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
1057
1837
  //#region src/parsers/parse-native.ts
1058
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. */
1059
1839
  const LATIN1_CHUNK = 32768;
@@ -1109,9 +1889,13 @@ function parseNative(bytes, parser) {
1109
1889
  if (parser.format === "edge-list") return parseEdgeList(text, parser);
1110
1890
  if (parser.format === "matrix-market") return parseMatrixMarket(text);
1111
1891
  if (parser.format === "gml") return parseGml(text);
1892
+ if (parser.format === "customer-points") return parseCustomerPoints(text);
1112
1893
  if (parser.format === "solomon") return parseSolomon(text);
1113
1894
  if (parser.format === "top") return parseTop(text);
1114
1895
  if (parser.format === "toptw") return parseToptw(text);
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);
1115
1899
  return parsePajek(text);
1116
1900
  }
1117
1901
  /** The ustar header layout (POSIX.1-1988, pax(1) "ustar Interchange Format"): each field's byte offset and length. */
@@ -1499,7 +2283,7 @@ function isResourceFork(path) {
1499
2283
  return path.startsWith("__MACOSX/") || path.split("/").some((segment) => segment.startsWith("._"));
1500
2284
  }
1501
2285
  /**
1502
- * 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.
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.
1503
2287
  * @param path - The member path.
1504
2288
  * @param member - The recipe's selector.
1505
2289
  * @returns True on a match.
@@ -1507,9 +2291,20 @@ function isResourceFork(path) {
1507
2291
  function selects(path, member) {
1508
2292
  if (isResourceFork(path) || path.endsWith("/")) return false;
1509
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);
1510
2295
  return member.extensions.some((extension) => path.toLowerCase().endsWith(extension.toLowerCase()));
1511
2296
  }
1512
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
+ /**
1513
2308
  * Picks every member a selector matches, in path order, so a collection reads in the same order wherever it is unpacked.
1514
2309
  * @param paths - Every member path.
1515
2310
  * @param member - The recipe's selector.
@@ -1518,7 +2313,7 @@ function selects(path, member) {
1518
2313
  */
1519
2314
  function pickAll(paths, member) {
1520
2315
  const matched = paths.filter((path) => selects(path, member)).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
1521
- if (matched.length === 0) throw new ArchiveError(`expected at least one ${"path" in member ? `member ${member.path}` : `member ending ${member.extensions.join(" or ")}`}, found none among: ${paths.join(", ")}`);
2316
+ if (matched.length === 0) throw new ArchiveError(`expected at least one ${describeSelector(member, "member")}, found none among: ${paths.join(", ")}`);
1522
2317
  return matched;
1523
2318
  }
1524
2319
  /**
@@ -1531,10 +2326,7 @@ function pickAll(paths, member) {
1531
2326
  function pick(paths, member) {
1532
2327
  const matched = paths.filter((path) => selects(path, member));
1533
2328
  const [only] = matched;
1534
- if (matched.length !== 1 || only === void 0) {
1535
- const wanted = "path" in member ? `member ${member.path}` : `one member ending ${member.extensions.join(" or ")}`;
1536
- throw new ArchiveError(`expected ${wanted}, found ${String(matched.length)} among: ${paths.join(", ")}`);
1537
- }
2329
+ if (matched.length !== 1 || only === void 0) throw new ArchiveError(`expected ${describeSelector(member, "one member")}, found ${String(matched.length)} among: ${paths.join(", ")}`);
1538
2330
  return only;
1539
2331
  }
1540
2332
  /**
@@ -1648,6 +2440,8 @@ const REDIRECTS = new Set(Object.values({
1648
2440
  }));
1649
2441
  /** What the downloader says it is, so a server operator can tell where the traffic comes from. */
1650
2442
  const USER_AGENT = "keepset (+https://github.com/Mearman/keepset)";
2443
+ /** What the downloader accepts: anything, as the file is the recipe's to read. Node's HTTP client sends no `Accept` header of its own, and a server that content-negotiates a download (the VRP-REP host answers 406 to a request without one) refuses it; every other HTTP client sends this, and RFC 9110 reads an absent header as the same. */
2444
+ const ACCEPT = "*/*";
1651
2445
  /** The failure of a connection that sent nothing for the stall time. */
1652
2446
  var StalledError = class extends KeepsetError {};
1653
2447
  /**
@@ -1672,7 +2466,10 @@ async function get(url, signal, stall) {
1672
2466
  let answered;
1673
2467
  const request = send(url, {
1674
2468
  method: "GET",
1675
- headers: { "user-agent": USER_AGENT },
2469
+ headers: {
2470
+ accept: ACCEPT,
2471
+ "user-agent": USER_AGENT
2472
+ },
1676
2473
  ...signal === void 0 ? {} : { signal }
1677
2474
  }, (response) => {
1678
2475
  answered = response;