keepset 0.32.0 → 0.34.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,9 +1,10 @@
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";
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-CPSe9InV.js";
2
2
  import { RoundTripMismatchError, assertRoundTrip, encode } from "crossmap";
3
3
  import { z } from "zod";
4
4
  import { createHash } from "node:crypto";
5
5
  import { gunzipSync } from "node:zlib";
6
6
  import { unzipSync } from "fflate";
7
+ import unbzip2 from "unbzip2-stream";
7
8
  import { request } from "node:http";
8
9
  import { request as request$1 } from "node:https";
9
10
  //#region src/parsers/native.ts
@@ -36,6 +37,30 @@ var NodeOrder = class {
36
37
  function lines(text) {
37
38
  return text.split(/\r?\n/);
38
39
  }
40
+ const CARRIAGE_RETURN$1 = 13;
41
+ /**
42
+ * Visits the lines of a file read in pieces, accepting both LF and CRLF endings, holding only the unfinished end of a piece between pieces so a file larger than one string can be read.
43
+ * @param pieces - The file's text, in order, split anywhere (even between a carriage return and its line feed).
44
+ * @param visit - Called with each line, without its terminator, and its 1-based number. A final line without a terminator is visited; the empty text after a final terminator is not a line.
45
+ */
46
+ function forEachLine(pieces, visit) {
47
+ let carry = "";
48
+ let number = 0;
49
+ const emit = (text, start, end) => {
50
+ number += 1;
51
+ visit(end > start && text.charCodeAt(end - 1) === CARRIAGE_RETURN$1 ? text.slice(start, end - 1) : text.slice(start, end), number);
52
+ };
53
+ for (const piece of pieces) {
54
+ const text = carry + piece;
55
+ let start = 0;
56
+ for (let end = text.indexOf("\n", start); end !== -1; end = text.indexOf("\n", start)) {
57
+ emit(text, start, end);
58
+ start = end + 1;
59
+ }
60
+ carry = text.slice(start);
61
+ }
62
+ if (carry !== "") emit(carry, 0, carry.length);
63
+ }
39
64
  //#endregion
40
65
  //#region src/parsers/edge-list.ts
41
66
  /** How each fixed delimiter option splits a line into fields. */
@@ -53,7 +78,7 @@ const splitters = {
53
78
  * @returns The endpoint id.
54
79
  * @throws NativeParseError when the column is absent or empty.
55
80
  */
56
- function endpoint(fields, column, lineNumber) {
81
+ function endpoint$1(fields, column, lineNumber) {
57
82
  const value = fields[column];
58
83
  if (value === void 0 || value.trim() === "") throw new NativeParseError("edge-list", lineNumber, `column ${String(column)} holds no node id`);
59
84
  return value.trim();
@@ -86,8 +111,8 @@ function parseEdgeList(text, options) {
86
111
  split ??= splitters[detect(line)];
87
112
  const fields = split(line);
88
113
  if (fields.length < needed) throw new NativeParseError("edge-list", lineNumber, `expected at least ${String(needed)} fields, found ${String(fields.length)}`);
89
- const source = endpoint(fields, options.sourceColumn, lineNumber);
90
- const target = endpoint(fields, options.targetColumn, lineNumber);
114
+ const source = endpoint$1(fields, options.sourceColumn, lineNumber);
115
+ const target = endpoint$1(fields, options.targetColumn, lineNumber);
91
116
  order.add(source);
92
117
  order.add(target);
93
118
  edges.push({
@@ -103,7 +128,7 @@ function parseEdgeList(text, options) {
103
128
  }
104
129
  //#endregion
105
130
  //#region src/parsers/gd-contest.ts
106
- const FORMAT$8 = "gd-contest";
131
+ const FORMAT$10 = "gd-contest";
107
132
  const NUMBER$8 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
108
133
  /**
109
134
  * Reads a vertex number, which the format numbers from 0 to N - 1.
@@ -112,7 +137,7 @@ const NUMBER$8 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
112
137
  * @returns The vertex id as text.
113
138
  */
114
139
  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)}`);
140
+ if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$10, token.line, `expected a vertex number from 0 to ${String(count - 1)}, found ${JSON.stringify(token.text)}`);
116
141
  return token.text;
117
142
  }
118
143
  /**
@@ -132,17 +157,17 @@ function parseGdContest(text) {
132
157
  });
133
158
  }
134
159
  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)}`);
160
+ if (first === void 0) throw new NativeParseError(FORMAT$10, 1, "the file holds no vertex count");
161
+ if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$10, first.line, `the vertex count must be a non-negative integer, found ${JSON.stringify(first.text)}`);
137
162
  const count = Number(first.text);
138
163
  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`);
164
+ if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$10, lines(text).length, `expected ${String(2 * count)} coordinates for ${String(count)} vertices, found ${String(coordinates.length)}`);
165
+ for (const coordinate of coordinates) if (!NUMBER$8.test(coordinate.text)) throw new NativeParseError(FORMAT$10, coordinate.line, `coordinate ${JSON.stringify(coordinate.text)} is not a number`);
141
166
  const ends = tokens.slice(1 + 2 * count);
142
167
  const edges = [];
143
168
  for (let source = ends.shift(); source !== void 0; source = ends.shift()) {
144
169
  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`);
170
+ if (target === void 0) throw new NativeParseError(FORMAT$10, source.line, `edge from vertex ${source.text} has no second vertex`);
146
171
  edges.push({
147
172
  source: vertex$1(source, count),
148
173
  target: vertex$1(target, count)
@@ -311,6 +336,155 @@ function parseGml(text) {
311
336
  };
312
337
  }
313
338
  //#endregion
339
+ //#region src/parsers/konect.ts
340
+ const FORMAT$9 = "konect";
341
+ /** The network formats of KONECT's first line (Kunegis, "The KONECT Handbook", table 1): undirected, directed and bipartite. */
342
+ const NETWORK_FORMATS = /* @__PURE__ */ new Set([
343
+ "sym",
344
+ "asym",
345
+ "bip"
346
+ ]);
347
+ /** The weight types whose edges carry no weight: a third column, where the file has one, counts the edge's repetitions and is 1 for a single edge. */
348
+ const UNWEIGHTED_TYPES = /* @__PURE__ */ new Set(["unweighted", "positive"]);
349
+ /** The weight types whose third column is the edge's weight (handbook table 2). `dynamic` is not among them: its third column is +1 for an added edge and -1 for a removed one, which is not a weight. */
350
+ const WEIGHTED_TYPES = /* @__PURE__ */ new Set([
351
+ "posweighted",
352
+ "signed",
353
+ "multisigned",
354
+ "weighted",
355
+ "multiweighted",
356
+ "multiposweighted"
357
+ ]);
358
+ /** The first line, `% FORMAT WEIGHTS`. */
359
+ const FORMAT_LINE = /^%\s+(\S+)\s+(\S+)\s*$/;
360
+ /** The optional second line, `% EDGES SUBJECTS OBJECTS`: the number of edges and the number of nodes in each of the two sets (equal in a network that is not bipartite). */
361
+ const COUNT_LINE = /^%\s+(\d+)\s+(\d+)\s+(\d+)\s*$/;
362
+ /** A node id: KONECT numbers nodes from 1. */
363
+ const NODE_ID = /^[1-9]\d*$/;
364
+ /** A number as a decimal literal with an optional sign and exponent, so `Number` cannot read hex, an empty string or infinity. */
365
+ const DECIMAL$3 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
366
+ /** The columns of an edge line (handbook section 9.1): the two nodes, then optionally the weight, and the time only when the weight is also given. */
367
+ const ENDPOINT_COLUMNS = 2;
368
+ const WEIGHTED_COLUMNS = 3;
369
+ const TIMESTAMPED_COLUMNS = 4;
370
+ /** The prefix of the node ids in a bipartite network's left and right set, whose numbering each starts from 1. */
371
+ const LEFT_PREFIX = "l:";
372
+ const RIGHT_PREFIX = "r:";
373
+ /**
374
+ * Parses a KONECT TSV file (`out.<network>` in a KONECT download, in the format of section 9.1 of the KONECT Handbook, Kunegis 2017): a first line `% FORMAT WEIGHTS`, an optional second line `% EDGES SUBJECTS OBJECTS`, then one edge per line as two to four whitespace-separated numbers, the source node, the target node, the weight and the time. Other `%` lines are comments.
375
+ *
376
+ * Nodes are the endpoints in order of first appearance, as they are numbered in the file, except in a bipartite network, whose two node sets are numbered independently from 1: there a left node is `l:<n>` and a right node `r:<n>`, so left node 1 and right node 1 are two nodes. Edges are in file order. An edge carries `weight` when the recipe says the file's weights are read, and `timestamp` when it says the file has a time column. The recipe's `weighted` must agree with the file's weight type (`posweighted`, `signed`, `weighted` and their multi-edge forms are weighted; `unweighted` and `positive` are not, and an edge's repetition count in their third column must be 1) and `timestamped` with the file having four columns, so a recipe cannot silently drop a weight or a time the file holds. When the second line is present, its edge count must equal the number of edge lines and every node id must lie within its declared set.
377
+ * @param pieces - The file's text, in order.
378
+ * @param options - The recipe's parser.
379
+ * @returns The native graph.
380
+ * @throws NativeParseError naming the line for a missing or unknown first line, a `dynamic` network, a recipe that disagrees with the file, a malformed edge line, a node id out of its declared range, an edge count that differs from the declared one, and when the file holds no edges.
381
+ */
382
+ function parseKonect(pieces, options) {
383
+ const order = new NodeOrder();
384
+ const edges = [];
385
+ const state = {};
386
+ let lastLine = 0;
387
+ forEachLine(pieces, (line, number) => {
388
+ lastLine = number;
389
+ const content = line.trim();
390
+ if (content === "") return;
391
+ const { header } = state;
392
+ if (header === void 0) {
393
+ state.header = readHeader(content, number, options);
394
+ return;
395
+ }
396
+ if (content.startsWith("%")) {
397
+ const counts = number === header.line + 1 ? COUNT_LINE.exec(content) : null;
398
+ if (counts?.[1] !== void 0 && counts[2] !== void 0 && counts[3] !== void 0) state.header = {
399
+ ...header,
400
+ counts: {
401
+ edges: Number(counts[1]),
402
+ left: Number(counts[2]),
403
+ right: Number(counts[3])
404
+ }
405
+ };
406
+ return;
407
+ }
408
+ edges.push(readEdge(content, number, {
409
+ header,
410
+ options,
411
+ order
412
+ }));
413
+ });
414
+ const { header } = state;
415
+ if (header === void 0) throw new NativeParseError(FORMAT$9, lastLine, "the file has no first line naming its format and weights");
416
+ if (edges.length === 0) throw new NativeParseError(FORMAT$9, lastLine, "the file holds no edges");
417
+ if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$9, lastLine, `the second line declares ${String(header.counts.edges)} edges, the file holds ${String(edges.length)}`);
418
+ return {
419
+ nodes: order.nodes(),
420
+ edges
421
+ };
422
+ }
423
+ /**
424
+ * Reads the first line and checks the recipe against it.
425
+ * @param content - The trimmed first non-blank line.
426
+ * @param number - Its 1-based number.
427
+ * @param options - The recipe's parser.
428
+ * @returns The format and weight type it declares.
429
+ * @throws NativeParseError when it is not `% FORMAT WEIGHTS` with a known format, when the network is dynamic, or when the recipe's `weighted` disagrees with the weight type.
430
+ */
431
+ function readHeader(content, number, options) {
432
+ const match = FORMAT_LINE.exec(content);
433
+ const network = match?.[1];
434
+ const weights = match?.[2];
435
+ if (network === void 0 || weights === void 0 || !NETWORK_FORMATS.has(network)) throw new NativeParseError(FORMAT$9, number, `expected a first line "% FORMAT WEIGHTS" with FORMAT one of ${[...NETWORK_FORMATS].join(", ")}, found ${JSON.stringify(content)}`);
436
+ if (!WEIGHTED_TYPES.has(weights) && !UNWEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$9, number, `the weight type ${JSON.stringify(weights)} is not one this parser reads (its third column is not an edge weight or a repetition count)`);
437
+ if (options.weighted !== WEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$9, number, `the file's weight type is ${weights}, so a recipe with weighted ${String(WEIGHTED_TYPES.has(weights))} reads it, not one with weighted ${String(options.weighted)}`);
438
+ return {
439
+ line: number,
440
+ network
441
+ };
442
+ }
443
+ /**
444
+ * Reads one edge line.
445
+ * @param content - The trimmed line.
446
+ * @param number - Its 1-based number.
447
+ * @param context - What the first lines declare (`header`), the recipe's parser (`options`) and the `order` that receives the edge's endpoints.
448
+ * @returns The edge.
449
+ * @throws NativeParseError when the line has the wrong number of columns, a node id that is not a positive integer or is outside its declared set, a weight or time that is not a number, or a repetition count other than 1.
450
+ */
451
+ function readEdge(content, number, context) {
452
+ const { header, options, order } = context;
453
+ const fields = content.split(/\s+/);
454
+ const [from, to, third, time] = fields;
455
+ const expected = options.timestamped ? TIMESTAMPED_COLUMNS : options.weighted ? WEIGHTED_COLUMNS : void 0;
456
+ const fits = expected === void 0 ? fields.length === ENDPOINT_COLUMNS || fields.length === WEIGHTED_COLUMNS : fields.length === expected;
457
+ if (from === void 0 || to === void 0 || !fits) throw new NativeParseError(FORMAT$9, number, `expected ${expected === void 0 ? `${String(ENDPOINT_COLUMNS)} or ${String(WEIGHTED_COLUMNS)}` : String(expected)} columns for this recipe, found ${String(fields.length)}`);
458
+ const bipartite = header.network === "bip";
459
+ const source = endpoint(from, bipartite ? LEFT_PREFIX : "", header.counts?.left, number);
460
+ const target = endpoint(to, bipartite ? RIGHT_PREFIX : "", header.counts?.right, number);
461
+ order.add(source);
462
+ order.add(target);
463
+ if (third !== void 0 && !options.weighted && (!DECIMAL$3.test(third) || Number(third) !== 1)) throw new NativeParseError(FORMAT$9, number, `the third column ${JSON.stringify(third)} is not 1; an edge that stands for several would be read as one`);
464
+ if (third !== void 0 && options.weighted && !DECIMAL$3.test(third)) throw new NativeParseError(FORMAT$9, number, `the weight ${JSON.stringify(third)} is not a number`);
465
+ if (time !== void 0 && !DECIMAL$3.test(time)) throw new NativeParseError(FORMAT$9, number, `the time ${JSON.stringify(time)} is not a number`);
466
+ const edge = {
467
+ source,
468
+ target
469
+ };
470
+ if (options.weighted && third !== void 0) edge.weight = Number(third);
471
+ if (options.timestamped && time !== void 0) edge.timestamp = Number(time);
472
+ return edge;
473
+ }
474
+ /**
475
+ * Reads a node id.
476
+ * @param token - The id as written.
477
+ * @param prefix - The prefix of the node's set in a bipartite network, empty otherwise.
478
+ * @param size - The declared size of the node's set, when the file declares one.
479
+ * @param number - The 1-based line, for the error.
480
+ * @returns The node id.
481
+ * @throws NativeParseError when the token is not a positive integer or exceeds the set's size.
482
+ */
483
+ function endpoint(token, prefix, size, number) {
484
+ if (!NODE_ID.test(token) || size !== void 0 && Number(token) > size) throw new NativeParseError(FORMAT$9, number, `${JSON.stringify(token)} is not a node id from 1${size === void 0 ? "" : ` to ${String(size)}`}`);
485
+ return `${prefix}${token}`;
486
+ }
487
+ //#endregion
314
488
  //#region src/parsers/matrix-market.ts
315
489
  /** How many tokens an entry line of each field type holds: the coordinates, then the value's parts (none for a pattern, a real and an imaginary part for a complex value). */
316
490
  const tokensPerEntry = /* @__PURE__ */ new Map([
@@ -885,7 +1059,7 @@ function parsePajek(text) {
885
1059
  }
886
1060
  //#endregion
887
1061
  //#region src/parsers/customer-points.ts
888
- const FORMAT$7 = "customer-points";
1062
+ const FORMAT$8 = "customer-points";
889
1063
  const NUMBER$6 = /^-?\d+(?:\.\d+)?$/;
890
1064
  const WHOLE$2 = /^\d+$/;
891
1065
  /**
@@ -901,12 +1075,12 @@ function parseCustomerPoints(text) {
901
1075
  })).filter((row) => row.fields.length > 0);
902
1076
  const [first, ...body] = rows;
903
1077
  const [key, count, ...extra] = first?.fields ?? [];
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(" "))}`);
1078
+ if (first === void 0 || key !== "customers" || count === void 0 || !WHOLE$2.test(count) || extra.length > 0) throw new NativeParseError(FORMAT$8, first?.line ?? 1, `expected "customers <count>", found ${JSON.stringify(first?.fields.join(" "))}`);
905
1079
  const seen = /* @__PURE__ */ new Set();
906
1080
  const nodes = body.map((row) => {
907
1081
  const [id, x, y, ...rest] = row.fields;
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`);
1082
+ 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$8, row.line, `expected a customer id and two numbers, found ${JSON.stringify(row.fields.join(" "))}`);
1083
+ if (seen.has(id)) throw new NativeParseError(FORMAT$8, row.line, `customer ${id} appears twice`);
910
1084
  seen.add(id);
911
1085
  return {
912
1086
  id,
@@ -914,7 +1088,7 @@ function parseCustomerPoints(text) {
914
1088
  y: Number(y)
915
1089
  };
916
1090
  });
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)}`);
1091
+ if (nodes.length !== Number(count)) throw new NativeParseError(FORMAT$8, rows[rows.length - 1]?.line ?? first.line, `the file announces ${count} customers, found ${String(nodes.length)}`);
918
1092
  return {
919
1093
  meta: { customers: Number(count) },
920
1094
  nodes
@@ -922,7 +1096,7 @@ function parseCustomerPoints(text) {
922
1096
  }
923
1097
  //#endregion
924
1098
  //#region src/parsers/solomon.ts
925
- const FORMAT$6 = "solomon";
1099
+ const FORMAT$7 = "solomon";
926
1100
  /** 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). */
927
1101
  const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
928
1102
  /**
@@ -934,7 +1108,7 @@ const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
934
1108
  * @throws NativeParseError when the token is not a decimal number.
935
1109
  */
936
1110
  function numberOf$3(token, line, what) {
937
- if (!NUMBER$5.test(token)) throw new NativeParseError(FORMAT$6, line, `expected ${what}, found ${JSON.stringify(token)}`);
1111
+ if (!NUMBER$5.test(token)) throw new NativeParseError(FORMAT$7, line, `expected ${what}, found ${JSON.stringify(token)}`);
938
1112
  return Number(token);
939
1113
  }
940
1114
  /**
@@ -947,7 +1121,7 @@ function parseSolomon(text) {
947
1121
  const all = lines(text);
948
1122
  const present = (index) => (all[index] ?? "").trim();
949
1123
  const name = present(0);
950
- if (name === "") throw new NativeParseError(FORMAT$6, 1, "expected the instance name on the first line");
1124
+ if (name === "") throw new NativeParseError(FORMAT$7, 1, "expected the instance name on the first line");
951
1125
  const meta = { name };
952
1126
  const nodes = [];
953
1127
  const seen = /* @__PURE__ */ new Set();
@@ -963,7 +1137,7 @@ function parseSolomon(text) {
963
1137
  awaitingHeader = true;
964
1138
  continue;
965
1139
  }
966
- if (section === "preamble") throw new NativeParseError(FORMAT$6, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
1140
+ if (section === "preamble") throw new NativeParseError(FORMAT$7, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
967
1141
  if (awaitingHeader) {
968
1142
  awaitingHeader = false;
969
1143
  continue;
@@ -971,15 +1145,15 @@ function parseSolomon(text) {
971
1145
  const tokens = trimmed.split(/\s+/);
972
1146
  if (section === "vehicle") {
973
1147
  const [vehicles, capacity, ...extra] = tokens;
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)}`);
1148
+ if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$7, line, `expected the vehicle count and capacity, found ${JSON.stringify(trimmed)}`);
975
1149
  meta.vehicles = numberOf$3(vehicles, line, "the vehicle count");
976
1150
  meta.capacity = numberOf$3(capacity, line, "the vehicle capacity");
977
1151
  continue;
978
1152
  }
979
1153
  const [id, x, y, demand, ready, due, service, ...extra] = tokens;
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`);
1154
+ 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$7, line, `expected seven numbers (id, x, y, demand, ready, due, service), found ${JSON.stringify(trimmed)}`);
1155
+ if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$7, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1156
+ if (seen.has(id)) throw new NativeParseError(FORMAT$7, line, `node ${id} appears twice`);
983
1157
  seen.add(id);
984
1158
  nodes.push({
985
1159
  id,
@@ -991,9 +1165,9 @@ function parseSolomon(text) {
991
1165
  service: numberOf$3(service, line, "the service time")
992
1166
  });
993
1167
  }
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");
1168
+ if (section !== "customer") throw new NativeParseError(FORMAT$7, all.length, "expected a CUSTOMER section");
1169
+ if (nodes.length === 0) throw new NativeParseError(FORMAT$7, all.length, "the CUSTOMER section has no rows");
1170
+ if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$7, all.length, "expected a VEHICLE section with the vehicle count and capacity");
997
1171
  return {
998
1172
  meta: {
999
1173
  name: meta.name,
@@ -1005,7 +1179,7 @@ function parseSolomon(text) {
1005
1179
  }
1006
1180
  //#endregion
1007
1181
  //#region src/parsers/top.ts
1008
- const FORMAT$5 = "top";
1182
+ const FORMAT$6 = "top";
1009
1183
  /** The header lines before the first point: `n`, `m` and `tmax`. */
1010
1184
  const HEADER_LINES = 3;
1011
1185
  const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
@@ -1019,7 +1193,7 @@ const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
1019
1193
  */
1020
1194
  function headerOf(text, key, line) {
1021
1195
  const [name, value, ...extra] = (text ?? "").trim().split(/\s+/);
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())}`);
1196
+ if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$4.test(value)) throw new NativeParseError(FORMAT$6, line, `expected "${key} <number>", found ${JSON.stringify((text ?? "").trim())}`);
1023
1197
  return Number(value);
1024
1198
  }
1025
1199
  /**
@@ -1038,11 +1212,11 @@ function parseTop(text) {
1038
1212
  const points = headerOf(first?.text, "n", first?.line ?? 1);
1039
1213
  const paths = headerOf(second?.text, "m", second?.line ?? 2);
1040
1214
  const budget = headerOf(third?.text, "tmax", third?.line ?? HEADER_LINES);
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`);
1215
+ if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$6, first?.line ?? 1, `expected at least a start and an end point, found n ${String(points)}`);
1216
+ if (rows.length !== points) throw new NativeParseError(FORMAT$6, all[all.length - 1]?.line ?? 1, `the header gives ${String(points)} points, found ${String(rows.length)} rows`);
1043
1217
  const nodes = rows.map((row, index) => {
1044
1218
  const [x, y, score, ...extra] = row.text.split(/\s+/);
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)}`);
1219
+ 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$6, row.line, `expected three numbers (x, y, score), found ${JSON.stringify(row.text)}`);
1046
1220
  return {
1047
1221
  id: String(index),
1048
1222
  x: Number(x),
@@ -1062,7 +1236,7 @@ function parseTop(text) {
1062
1236
  }
1063
1237
  //#endregion
1064
1238
  //#region src/parsers/toptw.ts
1065
- const FORMAT$4 = "toptw";
1239
+ const FORMAT$5 = "toptw";
1066
1240
  /** How many numbers the first header line holds (k, paths, customers, t) and the second (D, capacity). */
1067
1241
  const FIRST_HEADER_NUMBERS = 4;
1068
1242
  const SECOND_HEADER_NUMBERS = 2;
@@ -1080,7 +1254,7 @@ const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
1080
1254
  * @throws NativeParseError when the token is not a decimal number.
1081
1255
  */
1082
1256
  function numberOf$2(token, line, what) {
1083
- if (token === void 0 || !NUMBER$3.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
1257
+ if (token === void 0 || !NUMBER$3.test(token)) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(token)}`);
1084
1258
  return Number(token);
1085
1259
  }
1086
1260
  /**
@@ -1094,7 +1268,7 @@ function numberOf$2(token, line, what) {
1094
1268
  */
1095
1269
  function numbersOf(text, count, line, what) {
1096
1270
  const tokens = text.trim().split(/\s+/);
1097
- if (tokens.length !== count) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
1271
+ if (tokens.length !== count) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
1098
1272
  return tokens.map((token) => numberOf$2(token, line, what));
1099
1273
  }
1100
1274
  /**
@@ -1109,18 +1283,18 @@ function parseToptw(text) {
1109
1283
  text: raw.trim()
1110
1284
  })).filter((entry) => entry.text !== "");
1111
1285
  const [first, second, ...rows] = all;
1112
- if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$4, 1, "expected the two header lines");
1286
+ if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$5, 1, "expected the two header lines");
1113
1287
  const [, paths, customers] = numbersOf(first.text, FIRST_HEADER_NUMBERS, first.line, "four numbers (k, paths, customers, t)");
1114
1288
  const [, capacity] = numbersOf(second.text, SECOND_HEADER_NUMBERS, second.line, "two numbers (D, capacity)");
1115
- if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$4, first.line, "the header lacks a number");
1289
+ if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$5, first.line, "the header lacks a number");
1116
1290
  const seen = /* @__PURE__ */ new Set();
1117
1291
  const nodes = rows.map((row) => {
1118
1292
  const tokens = row.text.split(/\s+/);
1119
1293
  const listLength = numberOf$2(tokens[LIST_LENGTH_AT], row.line, "the length of the list after the unused number");
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)}`);
1294
+ if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT$5, row.line, `expected ${String(ROW_NUMBERS_BESIDE_LIST + listLength)} numbers for a list of ${String(listLength)}, found ${String(tokens.length)}`);
1121
1295
  const [id] = tokens;
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`);
1296
+ if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$5, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1297
+ if (seen.has(id)) throw new NativeParseError(FORMAT$5, row.line, `node ${id} appears twice`);
1124
1298
  seen.add(id);
1125
1299
  return {
1126
1300
  id,
@@ -1132,7 +1306,7 @@ function parseToptw(text) {
1132
1306
  close: numberOf$2(tokens[8 + listLength], row.line, "the closing of the window")
1133
1307
  };
1134
1308
  });
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)}`);
1309
+ if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$5, 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)}`);
1136
1310
  return {
1137
1311
  meta: {
1138
1312
  paths,
@@ -1144,7 +1318,7 @@ function parseToptw(text) {
1144
1318
  }
1145
1319
  //#endregion
1146
1320
  //#region src/parsers/tsplib.ts
1147
- const FORMAT$3 = "tsplib";
1321
+ const FORMAT$4 = "tsplib";
1148
1322
  /** The instance types the parser reads: the probabilistic and the clustered orienteering problem. */
1149
1323
  const TYPES = /* @__PURE__ */ new Set(["POP", "COP"]);
1150
1324
  /** 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. */
@@ -1175,7 +1349,7 @@ function degrees(value, line) {
1175
1349
  const whole = Math.trunc(magnitude);
1176
1350
  const hundredths = (magnitude - whole) * MINUTE_SCALE;
1177
1351
  const minutes = Math.round(hundredths);
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)}`);
1352
+ if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT$4, line, `expected a DDD.MM coordinate with whole minutes below 60, found ${String(value)}`);
1179
1353
  return Math.sign(value) * (whole + minutes / MINUTES_PER_DEGREE);
1180
1354
  }
1181
1355
  /**
@@ -1187,7 +1361,7 @@ function degrees(value, line) {
1187
1361
  * @throws NativeParseError when the token is not a decimal number.
1188
1362
  */
1189
1363
  function numberOf$1(token, line, what) {
1190
- if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(token)}`);
1364
+ if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
1191
1365
  return Number(token);
1192
1366
  }
1193
1367
  /**
@@ -1199,7 +1373,7 @@ function numberOf$1(token, line, what) {
1199
1373
  * @throws NativeParseError when the token is not a whole number.
1200
1374
  */
1201
1375
  function wholeOf(token, line, what) {
1202
- if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$3, line, `expected ${what}, found ${JSON.stringify(token)}`);
1376
+ if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
1203
1377
  return Number(token);
1204
1378
  }
1205
1379
  /**
@@ -1227,7 +1401,7 @@ function collect(text) {
1227
1401
  continue;
1228
1402
  }
1229
1403
  if (keyed !== null && key !== void 0) {
1230
- if (collected.header.has(key)) throw new NativeParseError(FORMAT$3, line, `${key} appears twice`);
1404
+ if (collected.header.has(key)) throw new NativeParseError(FORMAT$4, line, `${key} appears twice`);
1231
1405
  collected.header.set(key, (keyed[2] ?? "").trim());
1232
1406
  section = void 0;
1233
1407
  continue;
@@ -1239,9 +1413,9 @@ function collect(text) {
1239
1413
  const tokens = trimmed.split(/\s+/);
1240
1414
  if (section === "coords") {
1241
1415
  const [id, x, y, ...extra] = tokens;
1242
- if (extra.length > 0) throw new NativeParseError(FORMAT$3, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
1416
+ if (extra.length > 0) throw new NativeParseError(FORMAT$4, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
1243
1417
  const node = wholeOf(id, line, "a node id");
1244
- if (collected.coords.has(node)) throw new NativeParseError(FORMAT$3, line, `node ${String(node)} has two coordinate rows`);
1418
+ if (collected.coords.has(node)) throw new NativeParseError(FORMAT$4, line, `node ${String(node)} has two coordinate rows`);
1245
1419
  collected.coords.set(node, {
1246
1420
  x: numberOf$1(x, line, "x"),
1247
1421
  y: numberOf$1(y, line, "y"),
@@ -1249,9 +1423,9 @@ function collect(text) {
1249
1423
  });
1250
1424
  } else if (section === "prizes") {
1251
1425
  const [id, prize, probability, ...extra] = tokens;
1252
- if (extra.length > 0) throw new NativeParseError(FORMAT$3, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
1426
+ if (extra.length > 0) throw new NativeParseError(FORMAT$4, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
1253
1427
  const node = wholeOf(id, line, "a node id");
1254
- if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$3, line, `node ${String(node)} has two prize rows`);
1428
+ if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$4, line, `node ${String(node)} has two prize rows`);
1255
1429
  collected.prizes.set(node, {
1256
1430
  prize: numberOf$1(prize, line, "the prize"),
1257
1431
  probability: numberOf$1(probability, line, "the probability")
@@ -1259,13 +1433,13 @@ function collect(text) {
1259
1433
  } else if (section === "sets") {
1260
1434
  const [id, prize, ...members] = tokens;
1261
1435
  const setId = wholeOf(id, line, "a set id");
1262
- if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$3, line, `set ${String(setId)} appears twice`);
1436
+ if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$4, line, `set ${String(setId)} appears twice`);
1263
1437
  collected.sets.push({
1264
1438
  id: setId,
1265
1439
  prize: numberOf$1(prize, line, "the set prize"),
1266
1440
  members: members.map((member) => wholeOf(member, line, "a node id in a set"))
1267
1441
  });
1268
- } else if (section === void 0) throw new NativeParseError(FORMAT$3, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
1442
+ } else if (section === void 0) throw new NativeParseError(FORMAT$4, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
1269
1443
  }
1270
1444
  return collected;
1271
1445
  }
@@ -1278,7 +1452,7 @@ function collect(text) {
1278
1452
  */
1279
1453
  function required(header, key) {
1280
1454
  const value = header.get(key);
1281
- if (value === void 0) throw new NativeParseError(FORMAT$3, 1, `expected a ${key} line`);
1455
+ if (value === void 0) throw new NativeParseError(FORMAT$4, 1, `expected a ${key} line`);
1282
1456
  return value;
1283
1457
  }
1284
1458
  /**
@@ -1290,23 +1464,23 @@ function required(header, key) {
1290
1464
  function parseTsplib(text) {
1291
1465
  const { header, coords, prizes, sets } = collect(text);
1292
1466
  const type = required(header, "TYPE");
1293
- if (!TYPES.has(type)) throw new NativeParseError(FORMAT$3, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
1467
+ if (!TYPES.has(type)) throw new NativeParseError(FORMAT$4, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
1294
1468
  const weightType = required(header, "EDGE_WEIGHT_TYPE");
1295
1469
  const geographic = weightType === "GEO";
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)}`);
1470
+ if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT$4, 1, `expected EDGE_WEIGHT_TYPE EUC_2D, CEIL_2D, ATT, GEO or EXPLICIT, found ${JSON.stringify(weightType)}`);
1297
1471
  const dimension = wholeOf(required(header, "DIMENSION"), 1, "DIMENSION");
1298
1472
  const budget = numberOf$1(required(header, "TMAX"), 1, "TMAX");
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)}`);
1473
+ if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT$4, 1, weightType === "EXPLICIT" ? "an EXPLICIT file has no NODE_COORD_SECTION" : `expected ${String(dimension)} coordinate rows, found ${String(coords.size)}`);
1300
1474
  const inRange = (id) => id >= 1 && id <= dimension;
1301
1475
  for (const id of [
1302
1476
  ...coords.keys(),
1303
1477
  ...prizes.keys(),
1304
1478
  ...sets.flatMap((set) => set.members)
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)}`);
1479
+ ]) if (!inRange(id)) throw new NativeParseError(FORMAT$4, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
1480
+ if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$4, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
1307
1481
  if (type === "COP") {
1308
1482
  const declared = wholeOf(required(header, "SETS"), 1, "SETS");
1309
- if (sets.length !== declared) throw new NativeParseError(FORMAT$3, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
1483
+ if (sets.length !== declared) throw new NativeParseError(FORMAT$4, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
1310
1484
  }
1311
1485
  const clustersOf = (node) => {
1312
1486
  const ids = sets.filter((set) => set.members.includes(node)).map((set) => String(set.id));
@@ -1344,7 +1518,7 @@ function parseTsplib(text) {
1344
1518
  }
1345
1519
  //#endregion
1346
1520
  //#region src/parsers/ucinet-dl.ts
1347
- const FORMAT$2 = "ucinet-dl";
1521
+ const FORMAT$3 = "ucinet-dl";
1348
1522
  /** 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
1523
  const LABEL_SECTIONS = /* @__PURE__ */ new Map([
1350
1524
  ["LABELS", "both"],
@@ -1373,7 +1547,7 @@ function labelsOf(text) {
1373
1547
  * @returns The dimension.
1374
1548
  */
1375
1549
  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)}`);
1550
+ if (!/^[1-9]\d*$/.test(value)) throw new NativeParseError(FORMAT$3, line, `${key} must be a positive integer, found ${JSON.stringify(value)}`);
1377
1551
  return Number(value);
1378
1552
  }
1379
1553
  /** 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. */
@@ -1405,7 +1579,7 @@ function readParameters(text, line) {
1405
1579
  tokens.shift();
1406
1580
  continue;
1407
1581
  }
1408
- throw new NativeParseError(FORMAT$2, line, `unknown header token ${JSON.stringify(token)}`);
1582
+ throw new NativeParseError(FORMAT$3, line, `unknown header token ${JSON.stringify(token)}`);
1409
1583
  }
1410
1584
  const key = upper.slice(0, equals);
1411
1585
  const value = token.slice(equals + 1);
@@ -1420,7 +1594,7 @@ function readParameters(text, line) {
1420
1594
  kind: "format",
1421
1595
  value: value.toUpperCase()
1422
1596
  });
1423
- else throw new NativeParseError(FORMAT$2, line, `unknown header parameter ${key}`);
1597
+ else throw new NativeParseError(FORMAT$3, line, `unknown header parameter ${key}`);
1424
1598
  }
1425
1599
  return parameters;
1426
1600
  }
@@ -1433,10 +1607,10 @@ function readParameters(text, line) {
1433
1607
  */
1434
1608
  function checked(labels, count, what) {
1435
1609
  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)}`);
1610
+ if (labels.values.length !== count) throw new NativeParseError(FORMAT$3, labels.line, `expected ${String(count)} ${what} labels, found ${String(labels.values.length)}`);
1437
1611
  const seen = /* @__PURE__ */ new Set();
1438
1612
  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`);
1613
+ if (seen.has(label)) throw new NativeParseError(FORMAT$3, labels.line, `${what} label ${JSON.stringify(label)} appears twice`);
1440
1614
  seen.add(label);
1441
1615
  }
1442
1616
  return labels.values;
@@ -1455,7 +1629,7 @@ function parseUcinetDl(text, options) {
1455
1629
  const header = { labels: {} };
1456
1630
  const setParameters = (parameterText, line) => {
1457
1631
  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`);
1632
+ if (header[parameter.field] !== void 0) throw new NativeParseError(FORMAT$3, line, `${parameter.key} is declared twice`);
1459
1633
  header[parameter.field] = parameter.value;
1460
1634
  } else if (parameter.kind === "format") header.format = parameter.value;
1461
1635
  else header.diagonal = parameter.value;
@@ -1477,7 +1651,7 @@ function parseUcinetDl(text, options) {
1477
1651
  if (content === "") continue;
1478
1652
  if (!started) {
1479
1653
  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");
1654
+ if (match === null) throw new NativeParseError(FORMAT$3, lineNumber, "a DL file starts with the keyword DL");
1481
1655
  started = true;
1482
1656
  if (match[1] !== void 0) setParameters(match[1], lineNumber);
1483
1657
  continue;
@@ -1495,7 +1669,7 @@ function parseUcinetDl(text, options) {
1495
1669
  }
1496
1670
  const target = keyword === void 0 ? void 0 : LABEL_SECTIONS.get(keyword);
1497
1671
  if (target !== void 0) {
1498
- if (header.labels[target] !== void 0) throw new NativeParseError(FORMAT$2, lineNumber, `${String(keyword)} is given twice`);
1672
+ if (header.labels[target] !== void 0) throw new NativeParseError(FORMAT$3, lineNumber, `${String(keyword)} is given twice`);
1499
1673
  section = target;
1500
1674
  header.labels[target] = {
1501
1675
  values: labelsOf(rest),
@@ -1503,31 +1677,31 @@ function parseUcinetDl(text, options) {
1503
1677
  };
1504
1678
  continue;
1505
1679
  }
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");
1680
+ if (/^LABELS\s+EMBEDDED\b/i.test(content)) throw new NativeParseError(FORMAT$3, lineNumber, "labels embedded in the data are not read in this version");
1507
1681
  if (section === void 0) setParameters(content, lineNumber);
1508
1682
  else header.labels[section]?.values.push(...labelsOf(content));
1509
1683
  }
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");
1684
+ if (!started) throw new NativeParseError(FORMAT$3, 1, "the file is empty");
1685
+ if (dataLine === void 0) throw new NativeParseError(FORMAT$3, all.length, "the file has no DATA: section");
1512
1686
  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");
1687
+ if (!FULL_MATRIX.has(format)) throw new NativeParseError(FORMAT$3, dataLine, `format ${format} is not read in this version; only the full matrix layout is`);
1688
+ if (header.diagonal === "ABSENT") throw new NativeParseError(FORMAT$3, dataLine, "a full matrix with its diagonal absent is not read in this version");
1515
1689
  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");
1690
+ if (twoMode === (header.n !== void 0) || twoMode && (header.nr === void 0 || header.nc === void 0)) throw new NativeParseError(FORMAT$3, dataLine, "the header must give either N, or both NR and NC");
1517
1691
  const rows = header.n ?? header.nr ?? 0;
1518
1692
  const columns = header.n ?? header.nc ?? 0;
1519
1693
  const matrices = header.nm ?? 1;
1520
1694
  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)}`);
1695
+ if (options.matrix > matrices) throw new NativeParseError(FORMAT$3, dataLine, `the recipe reads matrix ${String(options.matrix)} but the file holds ${String(matrices)}`);
1522
1696
  let rowIds;
1523
1697
  let columnIds;
1524
1698
  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:");
1699
+ if (header.labels.both !== void 0) throw new NativeParseError(FORMAT$3, header.labels.both.line, "a two-mode file names its rows and columns with ROW LABELS: and COLUMN LABELS:, not LABELS:");
1526
1700
  rowIds = checked(header.labels.rows, rows, "row") ?? Array.from({ length: rows }, (_, row) => `r${String(row + 1)}`);
1527
1701
  columnIds = checked(header.labels.columns, columns, "column") ?? Array.from({ length: columns }, (_, column) => `c${String(column + 1)}`);
1528
1702
  const rowSet = new Set(rowIds);
1529
1703
  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`);
1704
+ if (shared !== void 0) throw new NativeParseError(FORMAT$3, dataLine, `the row and column label ${JSON.stringify(shared)} names two different entities`);
1531
1705
  } else {
1532
1706
  const given = [
1533
1707
  checked(header.labels.both, rows, "actor"),
@@ -1535,18 +1709,18 @@ function parseUcinetDl(text, options) {
1535
1709
  checked(header.labels.columns, columns, "column")
1536
1710
  ].filter((labels) => labels !== void 0);
1537
1711
  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");
1712
+ if (first !== void 0 && given.some((labels) => labels.some((label, position) => label !== first[position]))) throw new NativeParseError(FORMAT$3, dataLine, "a one-mode file's row and column labels must be the same actors in the same order");
1539
1713
  rowIds = first ?? Array.from({ length: rows }, (_, row) => String(row + 1));
1540
1714
  columnIds = rowIds;
1541
1715
  }
1542
1716
  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`);
1717
+ if (cells.length !== perMatrix * matrices) throw new NativeParseError(FORMAT$3, cells.at(-1)?.line ?? dataLine, `expected ${String(matrices)} full ${String(rows)} by ${String(columns)} matrices (${String(perMatrix * matrices)} cells), found ${String(cells.length)} cells`);
1718
+ for (const cell of cells) if (!NUMBER$1.test(cell.text)) throw new NativeParseError(FORMAT$3, cell.line, `cell ${JSON.stringify(cell.text)} is not a number`);
1545
1719
  const chosen = cells.slice((options.matrix - 1) * perMatrix, options.matrix * perMatrix);
1546
1720
  const edges = [];
1547
1721
  for (const [row, source] of rowIds.entries()) for (const [column, target] of columnIds.entries()) {
1548
1722
  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)}`);
1723
+ if (cell === void 0) throw new NativeParseError(FORMAT$3, dataLine, `matrix ${String(options.matrix)} has no cell at row ${String(row + 1)}, column ${String(column + 1)}`);
1550
1724
  if (Number(cell.text) !== 0) edges.push({
1551
1725
  source,
1552
1726
  target
@@ -1559,7 +1733,7 @@ function parseUcinetDl(text, options) {
1559
1733
  }
1560
1734
  //#endregion
1561
1735
  //#region src/parsers/verolog-2019.ts
1562
- const FORMAT$1 = "verolog-2019";
1736
+ const FORMAT$2 = "verolog-2019";
1563
1737
  /** The header lines, in the order the file has them, with the key of `meta` each fills and whether its value is text. */
1564
1738
  const HEADER$1 = [
1565
1739
  [
@@ -1660,7 +1834,7 @@ function rowsOf(text) {
1660
1834
  */
1661
1835
  function numberAt(row, index, what) {
1662
1836
  const field = row.fields[index];
1663
- if (field === void 0 || !NUMBER.test(field)) throw new NativeParseError(FORMAT$1, row.line, `expected ${what}, found ${JSON.stringify(field)}`);
1837
+ if (field === void 0 || !NUMBER.test(field)) throw new NativeParseError(FORMAT$2, row.line, `expected ${what}, found ${JSON.stringify(field)}`);
1664
1838
  return Number(field);
1665
1839
  }
1666
1840
  /**
@@ -1684,8 +1858,8 @@ function keyed(row) {
1684
1858
  */
1685
1859
  function checkRows(name, rows, length) {
1686
1860
  for (const [index, row] of rows.entries()) {
1687
- 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])}`);
1688
- 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)}`);
1861
+ if (row.fields[0] !== String(index + 1)) throw new NativeParseError(FORMAT$2, row.line, `expected ${name} row ${String(index + 1)} to start with that id, found ${JSON.stringify(row.fields[0])}`);
1862
+ if (length !== void 0 && row.fields.length !== length) throw new NativeParseError(FORMAT$2, row.line, `expected ${String(length)} fields in a ${name} row, found ${String(row.fields.length)}`);
1689
1863
  }
1690
1864
  }
1691
1865
  /**
@@ -1700,15 +1874,15 @@ function sectionsOf(rows) {
1700
1874
  for (const name of SECTIONS) {
1701
1875
  const start = rows[at];
1702
1876
  const announced = start === void 0 ? void 0 : keyed(start);
1703
- 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(" "))}`);
1877
+ if (start === void 0 || announced?.key !== name || announced.value.length !== 1 || !WHOLE.test(announced.value[0] ?? "")) throw new NativeParseError(FORMAT$2, start?.line ?? 1, `expected "${name} = <count>", found ${JSON.stringify(start?.fields.join(" "))}`);
1704
1878
  const count = Number(announced.value[0]);
1705
1879
  const body = rows.slice(at + 1, at + 1 + count);
1706
- if (body.length !== count) throw new NativeParseError(FORMAT$1, start.line, `${name} announces ${String(count)} rows, found ${String(body.length)}`);
1880
+ if (body.length !== count) throw new NativeParseError(FORMAT$2, start.line, `${name} announces ${String(count)} rows, found ${String(body.length)}`);
1707
1881
  found[name] = body;
1708
1882
  at += 1 + count;
1709
1883
  }
1710
1884
  const extra = rows[at];
1711
- 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(" "))}`);
1885
+ if (extra !== void 0) throw new NativeParseError(FORMAT$2, extra.line, `expected the end of the file after TECHNICIANS, found ${JSON.stringify(extra.fields.join(" "))}`);
1712
1886
  return {
1713
1887
  MACHINES: found.MACHINES ?? [],
1714
1888
  LOCATIONS: found.LOCATIONS ?? [],
@@ -1729,7 +1903,7 @@ function parseVerolog2019(text) {
1729
1903
  for (const [position, [key, name, kind]] of HEADER$1.entries()) {
1730
1904
  const row = rows[position];
1731
1905
  const header = row === void 0 ? void 0 : keyed(row);
1732
- 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(" "))}`);
1906
+ if (row === void 0 || header?.key !== key || header.value.length === 0) throw new NativeParseError(FORMAT$2, row?.line ?? 1, `expected "${key} = <value>", found ${JSON.stringify(row?.fields.join(" "))}`);
1733
1907
  meta[name] = kind === "text" ? header.value.join(" ") : numberAt({
1734
1908
  line: row.line,
1735
1909
  fields: header.value
@@ -1769,10 +1943,10 @@ function parseVerolog2019(text) {
1769
1943
  const toDay = numberAt(row, REQUEST_AT.toDay, "the last day");
1770
1944
  const machine = numberAt(row, REQUEST_AT.machine, "a machine id");
1771
1945
  const amount = numberAt(row, REQUEST_AT.amount, "the amount");
1772
- 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)}`);
1773
- 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)}`);
1774
- 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)}`);
1775
- if (amount < 1) throw new NativeParseError(FORMAT$1, row.line, `expected an amount of at least 1, found ${String(amount)}`);
1946
+ if (!Number.isInteger(location) || location < 1 || location > locations.length) throw new NativeParseError(FORMAT$2, row.line, `location ${String(location)} is outside 1 to ${String(locations.length)}`);
1947
+ if (!Number.isInteger(machine) || machine < 1 || machine > machines.length) throw new NativeParseError(FORMAT$2, row.line, `machine ${String(machine)} is outside 1 to ${String(machines.length)}`);
1948
+ if (fromDay < 1 || fromDay > toDay || toDay > days) throw new NativeParseError(FORMAT$2, row.line, `the days ${String(fromDay)} to ${String(toDay)} are not within 1 to ${String(days)}`);
1949
+ if (amount < 1) throw new NativeParseError(FORMAT$2, row.line, `expected an amount of at least 1, found ${String(amount)}`);
1776
1950
  return {
1777
1951
  id: row.fields[0] ?? "",
1778
1952
  ...position(location),
@@ -1793,6 +1967,98 @@ function parseVerolog2019(text) {
1793
1967
  };
1794
1968
  }
1795
1969
  //#endregion
1970
+ //#region src/parsers/user-item-ratings.ts
1971
+ const FORMAT$1 = "user-item-ratings";
1972
+ /** The text each delimiter option separates a line's fields with. */
1973
+ const SEPARATORS = {
1974
+ comma: ",",
1975
+ tab: " ",
1976
+ "double-colon": "::"
1977
+ };
1978
+ /** The prefix an id takes from the set it is in, so that user 1 and movie 1, which a source numbers independently, are two nodes: `u:` for a user and the first letter of the item type and a colon for an item. */
1979
+ const USER_PREFIX = "u:";
1980
+ const ITEM_PREFIXES = {
1981
+ movie: "m:",
1982
+ product: "p:"
1983
+ };
1984
+ /** The node type of a user, and the edge type of a rating. */
1985
+ const USER_TYPE = "user";
1986
+ const RATING_TYPE = "rating";
1987
+ /** A rating as a decimal literal: digits with an optional fraction, no sign, so `Number` cannot read it as hex, as an empty string or as infinity. */
1988
+ const RATING = /^\d+(?:\.\d+)?$/;
1989
+ /** A time as a count of whole units since the epoch, in digits only. */
1990
+ const TIMESTAMP = /^\d+$/;
1991
+ /** The fields on a rating's line: the user, the item, the rating and when it was given. */
1992
+ const FIELDS = 4;
1993
+ /**
1994
+ * Parses a file of ratings, one per line as `user`, `item`, `rating` and `timestamp` separated by the recipe's delimiter, after `skipLines` header lines, into a bipartite graph. This is the layout of an Amazon ratings-only file (comma, no header) and of each MovieLens release (tab for `u.data`, `::` for `ratings.dat`, comma with a header line for `ratings.csv`).
1995
+ *
1996
+ * Nodes are the users and the items in order of first appearance, a user as `u:<id>` of type `user` and an item as `m:<id>` or `p:<id>` of the recipe's item type. Each line is one edge from its user to its item, in file order, of type `rating`, with `weight` the rating over the recipe's `maxRating` (so a weight is in (0, 1]) and `timestamp` the time field as the file writes it. Blank lines are skipped. A user who rates an item twice gives two edges.
1997
+ *
1998
+ * The file is read a piece of text at a time and each distinct id is held once, however many edges name it.
1999
+ * @param pieces - The file's text, in order.
2000
+ * @param options - The recipe's parser.
2001
+ * @returns The native graph.
2002
+ * @throws NativeParseError naming the line for a line without exactly four fields, an empty id, a rating that is not a positive number or exceeds `maxRating`, or a timestamp that is not a whole number, and when the file holds no ratings.
2003
+ */
2004
+ function parseUserItemRatings(pieces, options) {
2005
+ const separator = SEPARATORS[options.delimiter];
2006
+ const itemPrefix = ITEM_PREFIXES[options.itemType];
2007
+ const users = /* @__PURE__ */ new Map();
2008
+ const items = /* @__PURE__ */ new Map();
2009
+ const nodes = [];
2010
+ const edges = [];
2011
+ /** The ids of one node set seen so far, by source id, so each distinct id is held once however many edges name it. */
2012
+ const sets = {
2013
+ user: {
2014
+ known: users,
2015
+ prefix: USER_PREFIX,
2016
+ type: USER_TYPE
2017
+ },
2018
+ item: {
2019
+ known: items,
2020
+ prefix: itemPrefix,
2021
+ type: options.itemType
2022
+ }
2023
+ };
2024
+ /** Returns the node id of a source id in a set, adding the node when the id has not appeared in it. */
2025
+ const idOf = (set, raw, number) => {
2026
+ if (raw === "") throw new NativeParseError(FORMAT$1, number, `the ${set.type} id is empty`);
2027
+ let id = set.known.get(raw);
2028
+ if (id === void 0) {
2029
+ id = `${set.prefix}${raw}`;
2030
+ set.known.set(raw, id);
2031
+ nodes.push({
2032
+ id,
2033
+ type: set.type
2034
+ });
2035
+ }
2036
+ return id;
2037
+ };
2038
+ let lastLine = 0;
2039
+ forEachLine(pieces, (line, number) => {
2040
+ lastLine = number;
2041
+ if (number <= options.skipLines || line.trim() === "") return;
2042
+ const fields = line.split(separator);
2043
+ const [user, item, rating, timestamp] = fields;
2044
+ if (fields.length !== FIELDS || user === void 0 || item === void 0 || rating === void 0 || timestamp === void 0) throw new NativeParseError(FORMAT$1, number, `expected ${String(FIELDS)} fields (user, item, rating, timestamp), found ${String(fields.length)}`);
2045
+ if (!RATING.test(rating) || Number(rating) <= 0 || Number(rating) > options.maxRating) throw new NativeParseError(FORMAT$1, number, `the rating ${JSON.stringify(rating)} is not a number above 0 and at most ${String(options.maxRating)}`);
2046
+ if (!TIMESTAMP.test(timestamp) || !Number.isSafeInteger(Number(timestamp))) throw new NativeParseError(FORMAT$1, number, `the timestamp ${JSON.stringify(timestamp)} is not a whole number`);
2047
+ edges.push({
2048
+ source: idOf(sets.user, user, number),
2049
+ target: idOf(sets.item, item, number),
2050
+ type: RATING_TYPE,
2051
+ weight: Number(rating) / options.maxRating,
2052
+ timestamp: Number(timestamp)
2053
+ });
2054
+ });
2055
+ if (edges.length === 0) throw new NativeParseError(FORMAT$1, lastLine, "the file holds no ratings");
2056
+ return {
2057
+ nodes,
2058
+ edges
2059
+ };
2060
+ }
2061
+ //#endregion
1796
2062
  //#region src/parsers/xml.ts
1797
2063
  /** The characters an XML name may contain, narrowed to what the formats read use. */
1798
2064
  const NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;
@@ -2151,6 +2417,8 @@ function* decodePieces(bytes, parser, pieceBytes = DECODE_PIECE_BYTES) {
2151
2417
  }
2152
2418
  function parseNative(bytes, parser) {
2153
2419
  if (parser.format === "csv-table") return parseCsvTable(decodePieces(bytes, parser), parser);
2420
+ if (parser.format === "user-item-ratings") return parseUserItemRatings(decodePieces(bytes, parser), parser);
2421
+ if (parser.format === "konect") return parseKonect(decodePieces(bytes, parser), parser);
2154
2422
  const text = decode(bytes, parser);
2155
2423
  if (parser.format === "geojson") return parseGeojson(text, parser);
2156
2424
  if (parser.format === "edge-list") return parseEdgeList(text, parser);
@@ -2167,6 +2435,48 @@ function parseNative(bytes, parser) {
2167
2435
  if (parser.format === "vrp-rep") return parseVrpRep(text);
2168
2436
  return parsePajek(text);
2169
2437
  }
2438
+ //#endregion
2439
+ //#region src/node/bzip2.ts
2440
+ /** The four bytes every bzip2 stream starts with, as ASCII: `BZh` and the block size digit, `1` to `9`. */
2441
+ const MAGIC = /^BZh[1-9]$/;
2442
+ const MAGIC_LENGTH = 4;
2443
+ /**
2444
+ * Whether the bytes start a bzip2 stream.
2445
+ * @param bytes - The data.
2446
+ * @returns True when they begin with `BZh` and a block size digit.
2447
+ */
2448
+ function startsStream(bytes) {
2449
+ return MAGIC.test(String.fromCharCode(...bytes.subarray(0, MAGIC_LENGTH)));
2450
+ }
2451
+ /**
2452
+ * Decompresses bzip2 data: one stream or several concatenated ones, as `bzip2 -d` reads them. The decoder verifies every block's CRC and the stream's, and fails on input that ends inside a stream or holds anything after the last one, so a download cut short or corrupted never yields a prefix of the file.
2453
+ *
2454
+ * `unbzip2-stream` is a stream interface, but its decoder runs inside `write` and `end` and emits every `data`, `error` and `end` event before they return, so one write of the whole input and an `end` give the whole output synchronously; the check that `end` was emitted guards that.
2455
+ * @param bytes - The compressed data.
2456
+ * @returns The decompressed bytes.
2457
+ * @throws ArchiveError when the data does not start with a bzip2 header, fails a CRC, ends early or holds trailing bytes that are not another stream.
2458
+ */
2459
+ function bunzip2(bytes) {
2460
+ if (!startsStream(bytes)) throw new ArchiveError("not valid bzip2 data: it does not start with the BZh header and a block size digit");
2461
+ const decoder = unbzip2();
2462
+ const chunks = [];
2463
+ const outcome = { finished: false };
2464
+ decoder.on("data", (chunk) => {
2465
+ chunks.push(chunk);
2466
+ });
2467
+ decoder.on("error", (error) => {
2468
+ outcome.failure ??= error;
2469
+ });
2470
+ decoder.on("end", () => {
2471
+ outcome.finished = true;
2472
+ });
2473
+ decoder.write(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
2474
+ decoder.end();
2475
+ if (outcome.failure !== void 0) throw new ArchiveError(`not valid bzip2 data: ${outcome.failure instanceof Error ? outcome.failure.message : JSON.stringify(outcome.failure)}`, { cause: outcome.failure });
2476
+ if (!outcome.finished) throw new ArchiveError("the bzip2 decoder did not finish when its input ended");
2477
+ const joined = Buffer.concat(chunks);
2478
+ return new Uint8Array(joined.buffer, joined.byteOffset, joined.byteLength);
2479
+ }
2170
2480
  /** The ustar header layout (POSIX.1-1988, pax(1) "ustar Interchange Format"): each field's byte offset and length. */
2171
2481
  const HEADER = {
2172
2482
  name: {
@@ -2608,6 +2918,7 @@ function pick(paths, member) {
2608
2918
  function unpack(bytes, archive) {
2609
2919
  if (archive.kind === "none") return { bytes };
2610
2920
  if (archive.kind === "gzip") return { bytes: gunzip(bytes) };
2921
+ if (archive.kind === "bzip2") return { bytes: bunzip2(bytes) };
2611
2922
  if (archive.kind === "xlsx") return {
2612
2923
  bytes: sheetToCsv(bytes, archive.sheet),
2613
2924
  member: archive.sheet
@@ -2646,7 +2957,7 @@ function selectMember(bytes, archive) {
2646
2957
  member: path
2647
2958
  };
2648
2959
  }
2649
- const members = readTar(archive.kind === "tar" ? bytes : gunzip(bytes));
2960
+ const members = readTar(untarred(bytes, archive.kind));
2650
2961
  const path = pick(members.map((member) => member.path), archive.member);
2651
2962
  const found = members.find((member) => member.path === path);
2652
2963
  if (found === void 0) throw new ArchiveError(`tar member ${path} could not be read`);
@@ -2681,7 +2992,7 @@ function unpackMembers(bytes, archive) {
2681
2992
  };
2682
2993
  });
2683
2994
  }
2684
- const members = readTar(archive.kind === "tar" ? bytes : gunzip(bytes));
2995
+ const members = readTar(untarred(bytes, archive.kind));
2685
2996
  return pickAll(members.map((member) => member.path), archive.members).map((path) => {
2686
2997
  const found = members.find((member) => member.path === path);
2687
2998
  if (found === void 0) throw new ArchiveError(`tar member ${path} could not be read`);
@@ -2692,6 +3003,16 @@ function unpackMembers(bytes, archive) {
2692
3003
  });
2693
3004
  }
2694
3005
  /**
3006
+ * Removes the compression a tar archive kind states, leaving the tar bytes.
3007
+ * @param bytes - The downloaded bytes.
3008
+ * @param kind - The tar archive kind: `tar` is uncompressed, `tar.gz` is gzip and `tar.bz2` is bzip2.
3009
+ * @returns The tar archive.
3010
+ */
3011
+ function untarred(bytes, kind) {
3012
+ if (kind === "tar") return bytes;
3013
+ return kind === "tar.gz" ? gunzip(bytes) : bunzip2(bytes);
3014
+ }
3015
+ /**
2695
3016
  * Gunzips bytes, rethrowing zlib's failure in the package's taxonomy.
2696
3017
  * @param bytes - Gzip data.
2697
3018
  * @returns The decompressed bytes.