keepset 0.31.0 → 0.32.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-DV_kxyz2.js → bundled-3xBd8sP5.js} +344 -70
- package/dist/{categories-DepJEi4o.d.ts → categories-HwqAn-Lc.d.ts} +174 -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 +347 -58
- package/dist-cli/keepset.mjs +5 -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 ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-
|
|
1
|
+
import { $ as ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-3xBd8sP5.js";
|
|
2
2
|
import { RoundTripMismatchError, assertRoundTrip, encode } from "crossmap";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -102,9 +102,61 @@ function parseEdgeList(text, options) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
//#endregion
|
|
105
|
+
//#region src/parsers/gd-contest.ts
|
|
106
|
+
const FORMAT$8 = "gd-contest";
|
|
107
|
+
const NUMBER$8 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
|
|
108
|
+
/**
|
|
109
|
+
* Reads a vertex number, which the format numbers from 0 to N - 1.
|
|
110
|
+
* @param token - The token.
|
|
111
|
+
* @param count - The declared vertex count.
|
|
112
|
+
* @returns The vertex id as text.
|
|
113
|
+
*/
|
|
114
|
+
function vertex$1(token, count) {
|
|
115
|
+
if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$8, token.line, `expected a vertex number from 0 to ${String(count - 1)}, found ${JSON.stringify(token.text)}`);
|
|
116
|
+
return token.text;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parses a Graph Drawing contest file (the `.gd4` format of the GD 2004 contest rules, http://vlado.fmf.uni-lj.si/pub/networks/data/GD/gd04/gd04.zip, `GD04/rules04.htm`) as a graph. The file is a sequence of values, with `#` starting a comment that runs to the end of its line: the vertex count N, then an X and a Y coordinate for each vertex in turn, then pairs of vertex numbers, one pair per undirected edge. Nodes are `0` to `N - 1`, isolated ones included, and edges are the pairs in file order. The coordinates are not carried: the rules give them as random starting positions for the contestants to move, not as data about the graph.
|
|
120
|
+
* @param text - The file text.
|
|
121
|
+
* @returns The native graph.
|
|
122
|
+
* @throws NativeParseError naming the line for a missing or malformed vertex count, a coordinate that is not a number, too few coordinates, a vertex number outside 0 to N - 1, or an edge missing its second vertex.
|
|
123
|
+
*/
|
|
124
|
+
function parseGdContest(text) {
|
|
125
|
+
const tokens = [];
|
|
126
|
+
for (const [index, raw] of lines(text).entries()) {
|
|
127
|
+
const comment = raw.indexOf("#");
|
|
128
|
+
const content = comment === -1 ? raw : raw.slice(0, comment);
|
|
129
|
+
for (const token of content.split(/\s+/).filter((part) => part !== "")) tokens.push({
|
|
130
|
+
text: token,
|
|
131
|
+
line: index + 1
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const [first] = tokens;
|
|
135
|
+
if (first === void 0) throw new NativeParseError(FORMAT$8, 1, "the file holds no vertex count");
|
|
136
|
+
if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$8, first.line, `the vertex count must be a non-negative integer, found ${JSON.stringify(first.text)}`);
|
|
137
|
+
const count = Number(first.text);
|
|
138
|
+
const coordinates = tokens.slice(1, 1 + 2 * count);
|
|
139
|
+
if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$8, lines(text).length, `expected ${String(2 * count)} coordinates for ${String(count)} vertices, found ${String(coordinates.length)}`);
|
|
140
|
+
for (const coordinate of coordinates) if (!NUMBER$8.test(coordinate.text)) throw new NativeParseError(FORMAT$8, coordinate.line, `coordinate ${JSON.stringify(coordinate.text)} is not a number`);
|
|
141
|
+
const ends = tokens.slice(1 + 2 * count);
|
|
142
|
+
const edges = [];
|
|
143
|
+
for (let source = ends.shift(); source !== void 0; source = ends.shift()) {
|
|
144
|
+
const target = ends.shift();
|
|
145
|
+
if (target === void 0) throw new NativeParseError(FORMAT$8, source.line, `edge from vertex ${source.text} has no second vertex`);
|
|
146
|
+
edges.push({
|
|
147
|
+
source: vertex$1(source, count),
|
|
148
|
+
target: vertex$1(target, count)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
nodes: Array.from({ length: count }, (_, id) => ({ id: String(id) })),
|
|
153
|
+
edges
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
105
157
|
//#region src/parsers/gml.ts
|
|
106
158
|
const KEY = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
107
|
-
const NUMBER$
|
|
159
|
+
const NUMBER$7 = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?/y;
|
|
108
160
|
/**
|
|
109
161
|
* Matches a sticky pattern at a position.
|
|
110
162
|
* @param pattern - A sticky regular expression.
|
|
@@ -155,7 +207,7 @@ function tokenise(text) {
|
|
|
155
207
|
position = end + 1;
|
|
156
208
|
} else {
|
|
157
209
|
const key = matchAt(KEY, text, position);
|
|
158
|
-
const number = key === void 0 ? matchAt(NUMBER$
|
|
210
|
+
const number = key === void 0 ? matchAt(NUMBER$7, text, position) : void 0;
|
|
159
211
|
const match = key ?? number;
|
|
160
212
|
if (match === void 0) throw new NativeParseError("gml", line, `unexpected character ${JSON.stringify(char)}`);
|
|
161
213
|
tokens.push({
|
|
@@ -833,8 +885,8 @@ function parsePajek(text) {
|
|
|
833
885
|
}
|
|
834
886
|
//#endregion
|
|
835
887
|
//#region src/parsers/customer-points.ts
|
|
836
|
-
const FORMAT$
|
|
837
|
-
const NUMBER$
|
|
888
|
+
const FORMAT$7 = "customer-points";
|
|
889
|
+
const NUMBER$6 = /^-?\d+(?:\.\d+)?$/;
|
|
838
890
|
const WHOLE$2 = /^\d+$/;
|
|
839
891
|
/**
|
|
840
892
|
* 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.
|
|
@@ -849,12 +901,12 @@ function parseCustomerPoints(text) {
|
|
|
849
901
|
})).filter((row) => row.fields.length > 0);
|
|
850
902
|
const [first, ...body] = rows;
|
|
851
903
|
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$
|
|
904
|
+
if (first === void 0 || key !== "customers" || count === void 0 || !WHOLE$2.test(count) || extra.length > 0) throw new NativeParseError(FORMAT$7, first?.line ?? 1, `expected "customers <count>", found ${JSON.stringify(first?.fields.join(" "))}`);
|
|
853
905
|
const seen = /* @__PURE__ */ new Set();
|
|
854
906
|
const nodes = body.map((row) => {
|
|
855
907
|
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$
|
|
857
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
908
|
+
if (id === void 0 || x === void 0 || y === void 0 || rest.length > 0 || !WHOLE$2.test(id) || !NUMBER$6.test(x) || !NUMBER$6.test(y)) throw new NativeParseError(FORMAT$7, row.line, `expected a customer id and two numbers, found ${JSON.stringify(row.fields.join(" "))}`);
|
|
909
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$7, row.line, `customer ${id} appears twice`);
|
|
858
910
|
seen.add(id);
|
|
859
911
|
return {
|
|
860
912
|
id,
|
|
@@ -862,7 +914,7 @@ function parseCustomerPoints(text) {
|
|
|
862
914
|
y: Number(y)
|
|
863
915
|
};
|
|
864
916
|
});
|
|
865
|
-
if (nodes.length !== Number(count)) throw new NativeParseError(FORMAT$
|
|
917
|
+
if (nodes.length !== Number(count)) throw new NativeParseError(FORMAT$7, rows[rows.length - 1]?.line ?? first.line, `the file announces ${count} customers, found ${String(nodes.length)}`);
|
|
866
918
|
return {
|
|
867
919
|
meta: { customers: Number(count) },
|
|
868
920
|
nodes
|
|
@@ -870,9 +922,9 @@ function parseCustomerPoints(text) {
|
|
|
870
922
|
}
|
|
871
923
|
//#endregion
|
|
872
924
|
//#region src/parsers/solomon.ts
|
|
873
|
-
const FORMAT$
|
|
925
|
+
const FORMAT$6 = "solomon";
|
|
874
926
|
/** 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). */
|
|
875
|
-
const NUMBER$
|
|
927
|
+
const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
|
|
876
928
|
/**
|
|
877
929
|
* Reads a token as a finite number.
|
|
878
930
|
* @param token - The token.
|
|
@@ -882,7 +934,7 @@ const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
|
|
|
882
934
|
* @throws NativeParseError when the token is not a decimal number.
|
|
883
935
|
*/
|
|
884
936
|
function numberOf$3(token, line, what) {
|
|
885
|
-
if (!NUMBER$
|
|
937
|
+
if (!NUMBER$5.test(token)) throw new NativeParseError(FORMAT$6, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
886
938
|
return Number(token);
|
|
887
939
|
}
|
|
888
940
|
/**
|
|
@@ -895,7 +947,7 @@ function parseSolomon(text) {
|
|
|
895
947
|
const all = lines(text);
|
|
896
948
|
const present = (index) => (all[index] ?? "").trim();
|
|
897
949
|
const name = present(0);
|
|
898
|
-
if (name === "") throw new NativeParseError(FORMAT$
|
|
950
|
+
if (name === "") throw new NativeParseError(FORMAT$6, 1, "expected the instance name on the first line");
|
|
899
951
|
const meta = { name };
|
|
900
952
|
const nodes = [];
|
|
901
953
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -911,7 +963,7 @@ function parseSolomon(text) {
|
|
|
911
963
|
awaitingHeader = true;
|
|
912
964
|
continue;
|
|
913
965
|
}
|
|
914
|
-
if (section === "preamble") throw new NativeParseError(FORMAT$
|
|
966
|
+
if (section === "preamble") throw new NativeParseError(FORMAT$6, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
|
|
915
967
|
if (awaitingHeader) {
|
|
916
968
|
awaitingHeader = false;
|
|
917
969
|
continue;
|
|
@@ -919,15 +971,15 @@ function parseSolomon(text) {
|
|
|
919
971
|
const tokens = trimmed.split(/\s+/);
|
|
920
972
|
if (section === "vehicle") {
|
|
921
973
|
const [vehicles, capacity, ...extra] = tokens;
|
|
922
|
-
if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$
|
|
974
|
+
if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$6, line, `expected the vehicle count and capacity, found ${JSON.stringify(trimmed)}`);
|
|
923
975
|
meta.vehicles = numberOf$3(vehicles, line, "the vehicle count");
|
|
924
976
|
meta.capacity = numberOf$3(capacity, line, "the vehicle capacity");
|
|
925
977
|
continue;
|
|
926
978
|
}
|
|
927
979
|
const [id, x, y, demand, ready, due, service, ...extra] = tokens;
|
|
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$
|
|
929
|
-
if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$
|
|
930
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
980
|
+
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$6, line, `expected seven numbers (id, x, y, demand, ready, due, service), found ${JSON.stringify(trimmed)}`);
|
|
981
|
+
if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$6, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
|
|
982
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$6, line, `node ${id} appears twice`);
|
|
931
983
|
seen.add(id);
|
|
932
984
|
nodes.push({
|
|
933
985
|
id,
|
|
@@ -939,9 +991,9 @@ function parseSolomon(text) {
|
|
|
939
991
|
service: numberOf$3(service, line, "the service time")
|
|
940
992
|
});
|
|
941
993
|
}
|
|
942
|
-
if (section !== "customer") throw new NativeParseError(FORMAT$
|
|
943
|
-
if (nodes.length === 0) throw new NativeParseError(FORMAT$
|
|
944
|
-
if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$
|
|
994
|
+
if (section !== "customer") throw new NativeParseError(FORMAT$6, all.length, "expected a CUSTOMER section");
|
|
995
|
+
if (nodes.length === 0) throw new NativeParseError(FORMAT$6, all.length, "the CUSTOMER section has no rows");
|
|
996
|
+
if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$6, all.length, "expected a VEHICLE section with the vehicle count and capacity");
|
|
945
997
|
return {
|
|
946
998
|
meta: {
|
|
947
999
|
name: meta.name,
|
|
@@ -953,10 +1005,10 @@ function parseSolomon(text) {
|
|
|
953
1005
|
}
|
|
954
1006
|
//#endregion
|
|
955
1007
|
//#region src/parsers/top.ts
|
|
956
|
-
const FORMAT$
|
|
1008
|
+
const FORMAT$5 = "top";
|
|
957
1009
|
/** The header lines before the first point: `n`, `m` and `tmax`. */
|
|
958
1010
|
const HEADER_LINES = 3;
|
|
959
|
-
const NUMBER$
|
|
1011
|
+
const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
|
|
960
1012
|
/**
|
|
961
1013
|
* Reads the value of a header line `<key> <number>`.
|
|
962
1014
|
* @param text - The line.
|
|
@@ -967,7 +1019,7 @@ const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
|
|
|
967
1019
|
*/
|
|
968
1020
|
function headerOf(text, key, line) {
|
|
969
1021
|
const [name, value, ...extra] = (text ?? "").trim().split(/\s+/);
|
|
970
|
-
if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$
|
|
1022
|
+
if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$4.test(value)) throw new NativeParseError(FORMAT$5, line, `expected "${key} <number>", found ${JSON.stringify((text ?? "").trim())}`);
|
|
971
1023
|
return Number(value);
|
|
972
1024
|
}
|
|
973
1025
|
/**
|
|
@@ -986,11 +1038,11 @@ function parseTop(text) {
|
|
|
986
1038
|
const points = headerOf(first?.text, "n", first?.line ?? 1);
|
|
987
1039
|
const paths = headerOf(second?.text, "m", second?.line ?? 2);
|
|
988
1040
|
const budget = headerOf(third?.text, "tmax", third?.line ?? HEADER_LINES);
|
|
989
|
-
if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$
|
|
990
|
-
if (rows.length !== points) throw new NativeParseError(FORMAT$
|
|
1041
|
+
if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$5, first?.line ?? 1, `expected at least a start and an end point, found n ${String(points)}`);
|
|
1042
|
+
if (rows.length !== points) throw new NativeParseError(FORMAT$5, all[all.length - 1]?.line ?? 1, `the header gives ${String(points)} points, found ${String(rows.length)} rows`);
|
|
991
1043
|
const nodes = rows.map((row, index) => {
|
|
992
1044
|
const [x, y, score, ...extra] = row.text.split(/\s+/);
|
|
993
|
-
if (x === void 0 || y === void 0 || score === void 0 || extra.length > 0 || !NUMBER$
|
|
1045
|
+
if (x === void 0 || y === void 0 || score === void 0 || extra.length > 0 || !NUMBER$4.test(x) || !NUMBER$4.test(y) || !NUMBER$4.test(score)) throw new NativeParseError(FORMAT$5, row.line, `expected three numbers (x, y, score), found ${JSON.stringify(row.text)}`);
|
|
994
1046
|
return {
|
|
995
1047
|
id: String(index),
|
|
996
1048
|
x: Number(x),
|
|
@@ -1010,7 +1062,7 @@ function parseTop(text) {
|
|
|
1010
1062
|
}
|
|
1011
1063
|
//#endregion
|
|
1012
1064
|
//#region src/parsers/toptw.ts
|
|
1013
|
-
const FORMAT$
|
|
1065
|
+
const FORMAT$4 = "toptw";
|
|
1014
1066
|
/** How many numbers the first header line holds (k, paths, customers, t) and the second (D, capacity). */
|
|
1015
1067
|
const FIRST_HEADER_NUMBERS = 4;
|
|
1016
1068
|
const SECOND_HEADER_NUMBERS = 2;
|
|
@@ -1018,7 +1070,7 @@ const SECOND_HEADER_NUMBERS = 2;
|
|
|
1018
1070
|
const LIST_LENGTH_AT = 6;
|
|
1019
1071
|
const LIST_STARTS_AT = 7;
|
|
1020
1072
|
const ROW_NUMBERS_BESIDE_LIST = 9;
|
|
1021
|
-
const NUMBER$
|
|
1073
|
+
const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
|
|
1022
1074
|
/**
|
|
1023
1075
|
* Reads a token as a finite number.
|
|
1024
1076
|
* @param token - The token.
|
|
@@ -1028,7 +1080,7 @@ const NUMBER$2 = /^-?\d+(?:\.\d+)?$/;
|
|
|
1028
1080
|
* @throws NativeParseError when the token is not a decimal number.
|
|
1029
1081
|
*/
|
|
1030
1082
|
function numberOf$2(token, line, what) {
|
|
1031
|
-
if (token === void 0 || !NUMBER$
|
|
1083
|
+
if (token === void 0 || !NUMBER$3.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1032
1084
|
return Number(token);
|
|
1033
1085
|
}
|
|
1034
1086
|
/**
|
|
@@ -1042,7 +1094,7 @@ function numberOf$2(token, line, what) {
|
|
|
1042
1094
|
*/
|
|
1043
1095
|
function numbersOf(text, count, line, what) {
|
|
1044
1096
|
const tokens = text.trim().split(/\s+/);
|
|
1045
|
-
if (tokens.length !== count) throw new NativeParseError(FORMAT$
|
|
1097
|
+
if (tokens.length !== count) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
|
|
1046
1098
|
return tokens.map((token) => numberOf$2(token, line, what));
|
|
1047
1099
|
}
|
|
1048
1100
|
/**
|
|
@@ -1057,18 +1109,18 @@ function parseToptw(text) {
|
|
|
1057
1109
|
text: raw.trim()
|
|
1058
1110
|
})).filter((entry) => entry.text !== "");
|
|
1059
1111
|
const [first, second, ...rows] = all;
|
|
1060
|
-
if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$
|
|
1112
|
+
if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$4, 1, "expected the two header lines");
|
|
1061
1113
|
const [, paths, customers] = numbersOf(first.text, FIRST_HEADER_NUMBERS, first.line, "four numbers (k, paths, customers, t)");
|
|
1062
1114
|
const [, capacity] = numbersOf(second.text, SECOND_HEADER_NUMBERS, second.line, "two numbers (D, capacity)");
|
|
1063
|
-
if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$
|
|
1115
|
+
if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$4, first.line, "the header lacks a number");
|
|
1064
1116
|
const seen = /* @__PURE__ */ new Set();
|
|
1065
1117
|
const nodes = rows.map((row) => {
|
|
1066
1118
|
const tokens = row.text.split(/\s+/);
|
|
1067
1119
|
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$
|
|
1120
|
+
if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT$4, row.line, `expected ${String(ROW_NUMBERS_BESIDE_LIST + listLength)} numbers for a list of ${String(listLength)}, found ${String(tokens.length)}`);
|
|
1069
1121
|
const [id] = tokens;
|
|
1070
|
-
if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$
|
|
1071
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
1122
|
+
if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$4, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
|
|
1123
|
+
if (seen.has(id)) throw new NativeParseError(FORMAT$4, row.line, `node ${id} appears twice`);
|
|
1072
1124
|
seen.add(id);
|
|
1073
1125
|
return {
|
|
1074
1126
|
id,
|
|
@@ -1080,7 +1132,7 @@ function parseToptw(text) {
|
|
|
1080
1132
|
close: numberOf$2(tokens[8 + listLength], row.line, "the closing of the window")
|
|
1081
1133
|
};
|
|
1082
1134
|
});
|
|
1083
|
-
if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$
|
|
1135
|
+
if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$4, 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)}`);
|
|
1084
1136
|
return {
|
|
1085
1137
|
meta: {
|
|
1086
1138
|
paths,
|
|
@@ -1092,7 +1144,7 @@ function parseToptw(text) {
|
|
|
1092
1144
|
}
|
|
1093
1145
|
//#endregion
|
|
1094
1146
|
//#region src/parsers/tsplib.ts
|
|
1095
|
-
const FORMAT$
|
|
1147
|
+
const FORMAT$3 = "tsplib";
|
|
1096
1148
|
/** The instance types the parser reads: the probabilistic and the clustered orienteering problem. */
|
|
1097
1149
|
const TYPES = /* @__PURE__ */ new Set(["POP", "COP"]);
|
|
1098
1150
|
/** 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. */
|
|
@@ -1101,7 +1153,7 @@ const PLANAR = /* @__PURE__ */ new Set([
|
|
|
1101
1153
|
"CEIL_2D",
|
|
1102
1154
|
"ATT"
|
|
1103
1155
|
]);
|
|
1104
|
-
const NUMBER$
|
|
1156
|
+
const NUMBER$2 = /^-?\d+(?:\.\d+)?$/;
|
|
1105
1157
|
const WHOLE$1 = /^\d+$/;
|
|
1106
1158
|
const HEADER$2 = /^([A-Z][A-Z_]*)\s*:\s*(.*)$/;
|
|
1107
1159
|
const SECTION = /^[A-Z][A-Z_]*$/;
|
|
@@ -1123,7 +1175,7 @@ function degrees(value, line) {
|
|
|
1123
1175
|
const whole = Math.trunc(magnitude);
|
|
1124
1176
|
const hundredths = (magnitude - whole) * MINUTE_SCALE;
|
|
1125
1177
|
const minutes = Math.round(hundredths);
|
|
1126
|
-
if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT$
|
|
1178
|
+
if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT$3, line, `expected a DDD.MM coordinate with whole minutes below 60, found ${String(value)}`);
|
|
1127
1179
|
return Math.sign(value) * (whole + minutes / MINUTES_PER_DEGREE);
|
|
1128
1180
|
}
|
|
1129
1181
|
/**
|
|
@@ -1135,7 +1187,7 @@ function degrees(value, line) {
|
|
|
1135
1187
|
* @throws NativeParseError when the token is not a decimal number.
|
|
1136
1188
|
*/
|
|
1137
1189
|
function numberOf$1(token, line, what) {
|
|
1138
|
-
if (token === void 0 || !NUMBER$
|
|
1190
|
+
if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1139
1191
|
return Number(token);
|
|
1140
1192
|
}
|
|
1141
1193
|
/**
|
|
@@ -1147,7 +1199,7 @@ function numberOf$1(token, line, what) {
|
|
|
1147
1199
|
* @throws NativeParseError when the token is not a whole number.
|
|
1148
1200
|
*/
|
|
1149
1201
|
function wholeOf(token, line, what) {
|
|
1150
|
-
if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$
|
|
1202
|
+
if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(token)}`);
|
|
1151
1203
|
return Number(token);
|
|
1152
1204
|
}
|
|
1153
1205
|
/**
|
|
@@ -1175,7 +1227,7 @@ function collect(text) {
|
|
|
1175
1227
|
continue;
|
|
1176
1228
|
}
|
|
1177
1229
|
if (keyed !== null && key !== void 0) {
|
|
1178
|
-
if (collected.header.has(key)) throw new NativeParseError(FORMAT$
|
|
1230
|
+
if (collected.header.has(key)) throw new NativeParseError(FORMAT$3, line, `${key} appears twice`);
|
|
1179
1231
|
collected.header.set(key, (keyed[2] ?? "").trim());
|
|
1180
1232
|
section = void 0;
|
|
1181
1233
|
continue;
|
|
@@ -1187,9 +1239,9 @@ function collect(text) {
|
|
|
1187
1239
|
const tokens = trimmed.split(/\s+/);
|
|
1188
1240
|
if (section === "coords") {
|
|
1189
1241
|
const [id, x, y, ...extra] = tokens;
|
|
1190
|
-
if (extra.length > 0) throw new NativeParseError(FORMAT$
|
|
1242
|
+
if (extra.length > 0) throw new NativeParseError(FORMAT$3, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
|
|
1191
1243
|
const node = wholeOf(id, line, "a node id");
|
|
1192
|
-
if (collected.coords.has(node)) throw new NativeParseError(FORMAT$
|
|
1244
|
+
if (collected.coords.has(node)) throw new NativeParseError(FORMAT$3, line, `node ${String(node)} has two coordinate rows`);
|
|
1193
1245
|
collected.coords.set(node, {
|
|
1194
1246
|
x: numberOf$1(x, line, "x"),
|
|
1195
1247
|
y: numberOf$1(y, line, "y"),
|
|
@@ -1197,9 +1249,9 @@ function collect(text) {
|
|
|
1197
1249
|
});
|
|
1198
1250
|
} else if (section === "prizes") {
|
|
1199
1251
|
const [id, prize, probability, ...extra] = tokens;
|
|
1200
|
-
if (extra.length > 0) throw new NativeParseError(FORMAT$
|
|
1252
|
+
if (extra.length > 0) throw new NativeParseError(FORMAT$3, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
|
|
1201
1253
|
const node = wholeOf(id, line, "a node id");
|
|
1202
|
-
if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$
|
|
1254
|
+
if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$3, line, `node ${String(node)} has two prize rows`);
|
|
1203
1255
|
collected.prizes.set(node, {
|
|
1204
1256
|
prize: numberOf$1(prize, line, "the prize"),
|
|
1205
1257
|
probability: numberOf$1(probability, line, "the probability")
|
|
@@ -1207,13 +1259,13 @@ function collect(text) {
|
|
|
1207
1259
|
} else if (section === "sets") {
|
|
1208
1260
|
const [id, prize, ...members] = tokens;
|
|
1209
1261
|
const setId = wholeOf(id, line, "a set id");
|
|
1210
|
-
if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$
|
|
1262
|
+
if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$3, line, `set ${String(setId)} appears twice`);
|
|
1211
1263
|
collected.sets.push({
|
|
1212
1264
|
id: setId,
|
|
1213
1265
|
prize: numberOf$1(prize, line, "the set prize"),
|
|
1214
1266
|
members: members.map((member) => wholeOf(member, line, "a node id in a set"))
|
|
1215
1267
|
});
|
|
1216
|
-
} else if (section === void 0) throw new NativeParseError(FORMAT$
|
|
1268
|
+
} else if (section === void 0) throw new NativeParseError(FORMAT$3, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
|
|
1217
1269
|
}
|
|
1218
1270
|
return collected;
|
|
1219
1271
|
}
|
|
@@ -1226,7 +1278,7 @@ function collect(text) {
|
|
|
1226
1278
|
*/
|
|
1227
1279
|
function required(header, key) {
|
|
1228
1280
|
const value = header.get(key);
|
|
1229
|
-
if (value === void 0) throw new NativeParseError(FORMAT$
|
|
1281
|
+
if (value === void 0) throw new NativeParseError(FORMAT$3, 1, `expected a ${key} line`);
|
|
1230
1282
|
return value;
|
|
1231
1283
|
}
|
|
1232
1284
|
/**
|
|
@@ -1238,23 +1290,23 @@ function required(header, key) {
|
|
|
1238
1290
|
function parseTsplib(text) {
|
|
1239
1291
|
const { header, coords, prizes, sets } = collect(text);
|
|
1240
1292
|
const type = required(header, "TYPE");
|
|
1241
|
-
if (!TYPES.has(type)) throw new NativeParseError(FORMAT$
|
|
1293
|
+
if (!TYPES.has(type)) throw new NativeParseError(FORMAT$3, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
|
|
1242
1294
|
const weightType = required(header, "EDGE_WEIGHT_TYPE");
|
|
1243
1295
|
const geographic = weightType === "GEO";
|
|
1244
|
-
if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT$
|
|
1296
|
+
if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT$3, 1, `expected EDGE_WEIGHT_TYPE EUC_2D, CEIL_2D, ATT, GEO or EXPLICIT, found ${JSON.stringify(weightType)}`);
|
|
1245
1297
|
const dimension = wholeOf(required(header, "DIMENSION"), 1, "DIMENSION");
|
|
1246
1298
|
const budget = numberOf$1(required(header, "TMAX"), 1, "TMAX");
|
|
1247
|
-
if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT$
|
|
1299
|
+
if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT$3, 1, weightType === "EXPLICIT" ? "an EXPLICIT file has no NODE_COORD_SECTION" : `expected ${String(dimension)} coordinate rows, found ${String(coords.size)}`);
|
|
1248
1300
|
const inRange = (id) => id >= 1 && id <= dimension;
|
|
1249
1301
|
for (const id of [
|
|
1250
1302
|
...coords.keys(),
|
|
1251
1303
|
...prizes.keys(),
|
|
1252
1304
|
...sets.flatMap((set) => set.members)
|
|
1253
|
-
]) if (!inRange(id)) throw new NativeParseError(FORMAT$
|
|
1254
|
-
if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$
|
|
1305
|
+
]) if (!inRange(id)) throw new NativeParseError(FORMAT$3, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
|
|
1306
|
+
if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$3, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
|
|
1255
1307
|
if (type === "COP") {
|
|
1256
1308
|
const declared = wholeOf(required(header, "SETS"), 1, "SETS");
|
|
1257
|
-
if (sets.length !== declared) throw new NativeParseError(FORMAT$
|
|
1309
|
+
if (sets.length !== declared) throw new NativeParseError(FORMAT$3, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
|
|
1258
1310
|
}
|
|
1259
1311
|
const clustersOf = (node) => {
|
|
1260
1312
|
const ids = sets.filter((set) => set.members.includes(node)).map((set) => String(set.id));
|
|
@@ -1291,6 +1343,221 @@ function parseTsplib(text) {
|
|
|
1291
1343
|
};
|
|
1292
1344
|
}
|
|
1293
1345
|
//#endregion
|
|
1346
|
+
//#region src/parsers/ucinet-dl.ts
|
|
1347
|
+
const FORMAT$2 = "ucinet-dl";
|
|
1348
|
+
/** A label section's keyword, upper-cased with its spacing collapsed, mapped to which labels it gives: `LABELS:` names the rows and columns of a one-mode matrix at once, `LEVEL LABELS:` (or `MATRIX LABELS:`) names the matrices. */
|
|
1349
|
+
const LABEL_SECTIONS = /* @__PURE__ */ new Map([
|
|
1350
|
+
["LABELS", "both"],
|
|
1351
|
+
["ROW LABELS", "rows"],
|
|
1352
|
+
["COLUMN LABELS", "columns"],
|
|
1353
|
+
["COL LABELS", "columns"],
|
|
1354
|
+
["LEVEL LABELS", "matrices"],
|
|
1355
|
+
["MATRIX LABELS", "matrices"]
|
|
1356
|
+
]);
|
|
1357
|
+
/** The FORMAT values that name the full matrix layout, the only one this version reads. */
|
|
1358
|
+
const FULL_MATRIX = /* @__PURE__ */ new Set(["FULLMATRIX", "FM"]);
|
|
1359
|
+
const NUMBER$1 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
|
|
1360
|
+
/**
|
|
1361
|
+
* Splits label text into labels: separated by commas, spaces or line breaks, and a double-quoted label may hold spaces or commas.
|
|
1362
|
+
* @param text - The text.
|
|
1363
|
+
* @returns The labels.
|
|
1364
|
+
*/
|
|
1365
|
+
function labelsOf(text) {
|
|
1366
|
+
return [...text.matchAll(/"([^"]*)"|[^\s,]+/g)].map((match) => match[1] ?? match[0]);
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Reads a dimension parameter's value.
|
|
1370
|
+
* @param value - The text after `=`.
|
|
1371
|
+
* @param key - The parameter, for the error.
|
|
1372
|
+
* @param line - The 1-based line, for the error.
|
|
1373
|
+
* @returns The dimension.
|
|
1374
|
+
*/
|
|
1375
|
+
function dimension(value, key, line) {
|
|
1376
|
+
if (!/^[1-9]\d*$/.test(value)) throw new NativeParseError(FORMAT$2, line, `${key} must be a positive integer, found ${JSON.stringify(value)}`);
|
|
1377
|
+
return Number(value);
|
|
1378
|
+
}
|
|
1379
|
+
/** The dimension parameters, upper-cased, mapped to the header field each sets: N actors of a one-mode matrix, NR rows and NC columns of a two-mode one, NM matrices. */
|
|
1380
|
+
const DIMENSIONS = /* @__PURE__ */ new Map([
|
|
1381
|
+
["N", "n"],
|
|
1382
|
+
["NR", "nr"],
|
|
1383
|
+
["NC", "nc"],
|
|
1384
|
+
["NM", "nm"]
|
|
1385
|
+
]);
|
|
1386
|
+
/**
|
|
1387
|
+
* Reads the parameters on one header line (`DL N=58 NM=2`, `FORMAT = FULLMATRIX DIAGONAL PRESENT`), separated by spaces or commas, with any spacing around `=`.
|
|
1388
|
+
* @param text - The line, after any leading `DL`.
|
|
1389
|
+
* @param line - The 1-based line, for the error.
|
|
1390
|
+
* @returns The parameters, in line order.
|
|
1391
|
+
*/
|
|
1392
|
+
function readParameters(text, line) {
|
|
1393
|
+
const parameters = [];
|
|
1394
|
+
const tokens = text.replace(/\s*=\s*/g, "=").split(/[\s,]+/).filter((token) => token !== "");
|
|
1395
|
+
for (let token = tokens.shift(); token !== void 0; token = tokens.shift()) {
|
|
1396
|
+
const upper = token.toUpperCase();
|
|
1397
|
+
const equals = upper.indexOf("=");
|
|
1398
|
+
if (equals === -1) {
|
|
1399
|
+
const next = tokens[0]?.toUpperCase();
|
|
1400
|
+
if (upper === "DIAGONAL" && (next === "PRESENT" || next === "ABSENT")) {
|
|
1401
|
+
parameters.push({
|
|
1402
|
+
kind: "diagonal",
|
|
1403
|
+
value: next
|
|
1404
|
+
});
|
|
1405
|
+
tokens.shift();
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
throw new NativeParseError(FORMAT$2, line, `unknown header token ${JSON.stringify(token)}`);
|
|
1409
|
+
}
|
|
1410
|
+
const key = upper.slice(0, equals);
|
|
1411
|
+
const value = token.slice(equals + 1);
|
|
1412
|
+
const field = DIMENSIONS.get(key);
|
|
1413
|
+
if (field !== void 0) parameters.push({
|
|
1414
|
+
kind: "dimension",
|
|
1415
|
+
field,
|
|
1416
|
+
key,
|
|
1417
|
+
value: dimension(value, key, line)
|
|
1418
|
+
});
|
|
1419
|
+
else if (key === "FORMAT") parameters.push({
|
|
1420
|
+
kind: "format",
|
|
1421
|
+
value: value.toUpperCase()
|
|
1422
|
+
});
|
|
1423
|
+
else throw new NativeParseError(FORMAT$2, line, `unknown header parameter ${key}`);
|
|
1424
|
+
}
|
|
1425
|
+
return parameters;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Checks a label list's length and uniqueness.
|
|
1429
|
+
* @param labels - The section, when the file has one.
|
|
1430
|
+
* @param count - How many labels it must hold.
|
|
1431
|
+
* @param what - What the labels name, for the error.
|
|
1432
|
+
* @returns The labels, or undefined when the file has none.
|
|
1433
|
+
*/
|
|
1434
|
+
function checked(labels, count, what) {
|
|
1435
|
+
if (labels === void 0) return;
|
|
1436
|
+
if (labels.values.length !== count) throw new NativeParseError(FORMAT$2, labels.line, `expected ${String(count)} ${what} labels, found ${String(labels.values.length)}`);
|
|
1437
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1438
|
+
for (const label of labels.values) {
|
|
1439
|
+
if (seen.has(label)) throw new NativeParseError(FORMAT$2, labels.line, `${what} label ${JSON.stringify(label)} appears twice`);
|
|
1440
|
+
seen.add(label);
|
|
1441
|
+
}
|
|
1442
|
+
return labels.values;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Parses a UCINet DL file (the Data Language of Borgatti, Everett and Freeman's UCINET, as its help describes it) in the full matrix layout as a graph, reading the one matrix the recipe names by its 1-based position among the file's `NM` matrices.
|
|
1446
|
+
*
|
|
1447
|
+
* A one-mode file (`N=n`) has nodes for its n actors, named by their `LABELS:`, `ROW LABELS:` or `COLUMN LABELS:` (which must agree where more than one is given) and otherwise numbered `1` to `n`. A two-mode file (`NR=r NC=c`) has nodes for its r rows followed by its c columns, named by `ROW LABELS:` and `COLUMN LABELS:` and otherwise `r1` to `rR` and `c1` to `cC`; a row and a column may not share a name, because they are different entities. Every non-zero cell of the chosen matrix is one edge from its row to its column, in row-major order, so a symmetric matrix gives each tie in both directions; cell values (tie strengths) are not carried in this version, nor are the `LEVEL LABELS:` that name the matrices, which are only checked to number `NM`.
|
|
1448
|
+
* @param text - The file text.
|
|
1449
|
+
* @param options - The recipe's parser, which names the matrix to read.
|
|
1450
|
+
* @returns The native graph.
|
|
1451
|
+
* @throws NativeParseError naming the line for a file that does not start with `DL`, a missing or contradictory dimension, a layout other than the full matrix, an absent diagonal, embedded labels, a label list of the wrong length or with a repeat, a matrix position beyond `NM`, a non-numeric cell, or a data section that does not hold exactly NM full matrices.
|
|
1452
|
+
*/
|
|
1453
|
+
function parseUcinetDl(text, options) {
|
|
1454
|
+
const all = lines(text);
|
|
1455
|
+
const header = { labels: {} };
|
|
1456
|
+
const setParameters = (parameterText, line) => {
|
|
1457
|
+
for (const parameter of readParameters(parameterText, line)) if (parameter.kind === "dimension") {
|
|
1458
|
+
if (header[parameter.field] !== void 0) throw new NativeParseError(FORMAT$2, line, `${parameter.key} is declared twice`);
|
|
1459
|
+
header[parameter.field] = parameter.value;
|
|
1460
|
+
} else if (parameter.kind === "format") header.format = parameter.value;
|
|
1461
|
+
else header.diagonal = parameter.value;
|
|
1462
|
+
};
|
|
1463
|
+
let started = false;
|
|
1464
|
+
let section;
|
|
1465
|
+
let dataLine;
|
|
1466
|
+
const cells = [];
|
|
1467
|
+
for (const [index, raw] of all.entries()) {
|
|
1468
|
+
const lineNumber = index + 1;
|
|
1469
|
+
const content = raw.trim();
|
|
1470
|
+
if (dataLine !== void 0) {
|
|
1471
|
+
for (const cell of content.split(/\s+/).filter((token) => token !== "")) cells.push({
|
|
1472
|
+
text: cell,
|
|
1473
|
+
line: lineNumber
|
|
1474
|
+
});
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (content === "") continue;
|
|
1478
|
+
if (!started) {
|
|
1479
|
+
const match = /^DL(?:\s+(.*))?$/i.exec(content);
|
|
1480
|
+
if (match === null) throw new NativeParseError(FORMAT$2, lineNumber, "a DL file starts with the keyword DL");
|
|
1481
|
+
started = true;
|
|
1482
|
+
if (match[1] !== void 0) setParameters(match[1], lineNumber);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
const colon = content.indexOf(":");
|
|
1486
|
+
const keyword = colon === -1 ? void 0 : content.slice(0, colon).trim().toUpperCase().replace(/\s+/g, " ");
|
|
1487
|
+
const rest = content.slice(colon + 1);
|
|
1488
|
+
if (keyword === "DATA") {
|
|
1489
|
+
dataLine = lineNumber;
|
|
1490
|
+
for (const cell of rest.split(/\s+/).filter((token) => token !== "")) cells.push({
|
|
1491
|
+
text: cell,
|
|
1492
|
+
line: lineNumber
|
|
1493
|
+
});
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const target = keyword === void 0 ? void 0 : LABEL_SECTIONS.get(keyword);
|
|
1497
|
+
if (target !== void 0) {
|
|
1498
|
+
if (header.labels[target] !== void 0) throw new NativeParseError(FORMAT$2, lineNumber, `${String(keyword)} is given twice`);
|
|
1499
|
+
section = target;
|
|
1500
|
+
header.labels[target] = {
|
|
1501
|
+
values: labelsOf(rest),
|
|
1502
|
+
line: lineNumber
|
|
1503
|
+
};
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (/^LABELS\s+EMBEDDED\b/i.test(content)) throw new NativeParseError(FORMAT$2, lineNumber, "labels embedded in the data are not read in this version");
|
|
1507
|
+
if (section === void 0) setParameters(content, lineNumber);
|
|
1508
|
+
else header.labels[section]?.values.push(...labelsOf(content));
|
|
1509
|
+
}
|
|
1510
|
+
if (!started) throw new NativeParseError(FORMAT$2, 1, "the file is empty");
|
|
1511
|
+
if (dataLine === void 0) throw new NativeParseError(FORMAT$2, all.length, "the file has no DATA: section");
|
|
1512
|
+
const format = header.format ?? "FULLMATRIX";
|
|
1513
|
+
if (!FULL_MATRIX.has(format)) throw new NativeParseError(FORMAT$2, dataLine, `format ${format} is not read in this version; only the full matrix layout is`);
|
|
1514
|
+
if (header.diagonal === "ABSENT") throw new NativeParseError(FORMAT$2, dataLine, "a full matrix with its diagonal absent is not read in this version");
|
|
1515
|
+
const twoMode = header.nr !== void 0 || header.nc !== void 0;
|
|
1516
|
+
if (twoMode === (header.n !== void 0) || twoMode && (header.nr === void 0 || header.nc === void 0)) throw new NativeParseError(FORMAT$2, dataLine, "the header must give either N, or both NR and NC");
|
|
1517
|
+
const rows = header.n ?? header.nr ?? 0;
|
|
1518
|
+
const columns = header.n ?? header.nc ?? 0;
|
|
1519
|
+
const matrices = header.nm ?? 1;
|
|
1520
|
+
checked(header.labels.matrices, matrices, "matrix");
|
|
1521
|
+
if (options.matrix > matrices) throw new NativeParseError(FORMAT$2, dataLine, `the recipe reads matrix ${String(options.matrix)} but the file holds ${String(matrices)}`);
|
|
1522
|
+
let rowIds;
|
|
1523
|
+
let columnIds;
|
|
1524
|
+
if (twoMode) {
|
|
1525
|
+
if (header.labels.both !== void 0) throw new NativeParseError(FORMAT$2, header.labels.both.line, "a two-mode file names its rows and columns with ROW LABELS: and COLUMN LABELS:, not LABELS:");
|
|
1526
|
+
rowIds = checked(header.labels.rows, rows, "row") ?? Array.from({ length: rows }, (_, row) => `r${String(row + 1)}`);
|
|
1527
|
+
columnIds = checked(header.labels.columns, columns, "column") ?? Array.from({ length: columns }, (_, column) => `c${String(column + 1)}`);
|
|
1528
|
+
const rowSet = new Set(rowIds);
|
|
1529
|
+
const shared = columnIds.find((id) => rowSet.has(id));
|
|
1530
|
+
if (shared !== void 0) throw new NativeParseError(FORMAT$2, dataLine, `the row and column label ${JSON.stringify(shared)} names two different entities`);
|
|
1531
|
+
} else {
|
|
1532
|
+
const given = [
|
|
1533
|
+
checked(header.labels.both, rows, "actor"),
|
|
1534
|
+
checked(header.labels.rows, rows, "row"),
|
|
1535
|
+
checked(header.labels.columns, columns, "column")
|
|
1536
|
+
].filter((labels) => labels !== void 0);
|
|
1537
|
+
const [first] = given;
|
|
1538
|
+
if (first !== void 0 && given.some((labels) => labels.some((label, position) => label !== first[position]))) throw new NativeParseError(FORMAT$2, dataLine, "a one-mode file's row and column labels must be the same actors in the same order");
|
|
1539
|
+
rowIds = first ?? Array.from({ length: rows }, (_, row) => String(row + 1));
|
|
1540
|
+
columnIds = rowIds;
|
|
1541
|
+
}
|
|
1542
|
+
const perMatrix = rows * columns;
|
|
1543
|
+
if (cells.length !== perMatrix * matrices) throw new NativeParseError(FORMAT$2, cells.at(-1)?.line ?? dataLine, `expected ${String(matrices)} full ${String(rows)} by ${String(columns)} matrices (${String(perMatrix * matrices)} cells), found ${String(cells.length)} cells`);
|
|
1544
|
+
for (const cell of cells) if (!NUMBER$1.test(cell.text)) throw new NativeParseError(FORMAT$2, cell.line, `cell ${JSON.stringify(cell.text)} is not a number`);
|
|
1545
|
+
const chosen = cells.slice((options.matrix - 1) * perMatrix, options.matrix * perMatrix);
|
|
1546
|
+
const edges = [];
|
|
1547
|
+
for (const [row, source] of rowIds.entries()) for (const [column, target] of columnIds.entries()) {
|
|
1548
|
+
const cell = chosen[row * columns + column];
|
|
1549
|
+
if (cell === void 0) throw new NativeParseError(FORMAT$2, dataLine, `matrix ${String(options.matrix)} has no cell at row ${String(row + 1)}, column ${String(column + 1)}`);
|
|
1550
|
+
if (Number(cell.text) !== 0) edges.push({
|
|
1551
|
+
source,
|
|
1552
|
+
target
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
nodes: (twoMode ? [...rowIds, ...columnIds] : rowIds).map((id) => ({ id })),
|
|
1557
|
+
edges
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
//#endregion
|
|
1294
1561
|
//#region src/parsers/verolog-2019.ts
|
|
1295
1562
|
const FORMAT$1 = "verolog-2019";
|
|
1296
1563
|
/** The header lines, in the order the file has them, with the key of `meta` each fills and whether its value is text. */
|
|
@@ -1889,6 +2156,8 @@ function parseNative(bytes, parser) {
|
|
|
1889
2156
|
if (parser.format === "edge-list") return parseEdgeList(text, parser);
|
|
1890
2157
|
if (parser.format === "matrix-market") return parseMatrixMarket(text);
|
|
1891
2158
|
if (parser.format === "gml") return parseGml(text);
|
|
2159
|
+
if (parser.format === "ucinet-dl") return parseUcinetDl(text, parser);
|
|
2160
|
+
if (parser.format === "gd-contest") return parseGdContest(text);
|
|
1892
2161
|
if (parser.format === "customer-points") return parseCustomerPoints(text);
|
|
1893
2162
|
if (parser.format === "solomon") return parseSolomon(text);
|
|
1894
2163
|
if (parser.format === "top") return parseTop(text);
|
|
@@ -2330,11 +2599,11 @@ function pick(paths, member) {
|
|
|
2330
2599
|
return only;
|
|
2331
2600
|
}
|
|
2332
2601
|
/**
|
|
2333
|
-
* Unpacks downloaded bytes into the one file the parser reads.
|
|
2602
|
+
* Unpacks downloaded bytes into the one file the parser reads. A member with an `inner` archive is unpacked again by that layout, and the path reported is the outer member's, joined by `/` to the inner member's when the inner layout selects one, as though the inner archive were a directory.
|
|
2334
2603
|
* @param bytes - The downloaded bytes.
|
|
2335
2604
|
* @param archive - The recipe's archive layout.
|
|
2336
2605
|
* @returns The file's bytes, and its member path for a multi-file archive.
|
|
2337
|
-
* @throws ArchiveError when the bytes are not a well-formed archive of the stated kind or
|
|
2606
|
+
* @throws ArchiveError when the bytes, or a member's bytes that an `inner` layout reads, are not a well-formed archive of the stated kind or a selector does not match exactly one member.
|
|
2338
2607
|
*/
|
|
2339
2608
|
function unpack(bytes, archive) {
|
|
2340
2609
|
if (archive.kind === "none") return { bytes };
|
|
@@ -2343,6 +2612,26 @@ function unpack(bytes, archive) {
|
|
|
2343
2612
|
bytes: sheetToCsv(bytes, archive.sheet),
|
|
2344
2613
|
member: archive.sheet
|
|
2345
2614
|
};
|
|
2615
|
+
const outer = selectMember(bytes, archive);
|
|
2616
|
+
if (archive.inner === void 0) return outer;
|
|
2617
|
+
if (archive.inner.kind === "gzip") return {
|
|
2618
|
+
bytes: gunzip(outer.bytes),
|
|
2619
|
+
member: outer.member
|
|
2620
|
+
};
|
|
2621
|
+
const inner = selectMember(outer.bytes, archive.inner);
|
|
2622
|
+
return {
|
|
2623
|
+
bytes: inner.bytes,
|
|
2624
|
+
member: `${outer.member}/${inner.member}`
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Unpacks the one member a file archive's selector matches.
|
|
2629
|
+
* @param bytes - The archive's bytes.
|
|
2630
|
+
* @param archive - A file archive layout and its selector.
|
|
2631
|
+
* @returns The member's bytes and path.
|
|
2632
|
+
* @throws ArchiveError when the bytes are not a well-formed archive of the stated kind or the selector does not match exactly one member.
|
|
2633
|
+
*/
|
|
2634
|
+
function selectMember(bytes, archive) {
|
|
2346
2635
|
if (archive.kind === "zip") {
|
|
2347
2636
|
const names = [];
|
|
2348
2637
|
unzip(bytes, (file) => {
|