keepset 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as 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-BrTVG8j1.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-CRr7GPD3.js";
2
2
  import { RoundTripMismatchError, assertRoundTrip, encode } from "crossmap";
3
3
  import { z } from "zod";
4
4
  import { createHash } from "node:crypto";
@@ -12,13 +12,25 @@ import { request as request$1 } from "node:https";
12
12
  * Collects node ids in order of first appearance, so a parser that learns its nodes from its edges emits them deterministically.
13
13
  */
14
14
  var NodeOrder = class {
15
- #ids = /* @__PURE__ */ new Set();
15
+ /** Each id, keyed by itself, so that `intern` can hand back the one string held for it. */
16
+ #ids = /* @__PURE__ */ new Map();
16
17
  /**
17
18
  * Records an id; a repeat keeps its first position.
18
19
  * @param id - The node id.
19
20
  */
20
21
  add(id) {
21
- this.#ids.add(id);
22
+ this.intern(id);
23
+ }
24
+ /**
25
+ * Records an id and returns the string held for it, so that every edge naming a node shares one string instead of each holding a copy cut from its own line.
26
+ * @param id - The node id.
27
+ * @returns The first string recorded with this value.
28
+ */
29
+ intern(id) {
30
+ const held = this.#ids.get(id);
31
+ if (held !== void 0) return held;
32
+ this.#ids.set(id, id);
33
+ return id;
22
34
  }
23
35
  /** @returns Whether the id has been recorded. */
24
36
  has(id) {
@@ -26,7 +38,38 @@ var NodeOrder = class {
26
38
  }
27
39
  /** @returns The nodes, in first-appearance order. */
28
40
  nodes() {
29
- return [...this.#ids].map((id) => ({ id }));
41
+ return [...this.#ids.keys()].map((id) => ({ id }));
42
+ }
43
+ };
44
+ /**
45
+ * Collects the nodes of a bipartite graph whose two sets a source numbers independently, giving each set's ids its own prefix (`u:1` and `m:1` are a user and a movie) and type. Nodes are kept in order of first appearance across both sets, and each distinct id is held once however many edges name it.
46
+ */
47
+ var BipartiteNodes = class {
48
+ #nodes = [];
49
+ /**
50
+ * Opens one of the graph's node sets.
51
+ * @param prefix - The prefix its node ids take.
52
+ * @param type - Its nodes' type.
53
+ * @returns A function from an id as the source writes it to the node id, adding the node the first time the id appears in this set.
54
+ */
55
+ set(prefix, type) {
56
+ const known = /* @__PURE__ */ new Map();
57
+ return (raw) => {
58
+ let id = known.get(raw);
59
+ if (id === void 0) {
60
+ id = `${prefix}${raw}`;
61
+ known.set(raw, id);
62
+ this.#nodes.push({
63
+ id,
64
+ type
65
+ });
66
+ }
67
+ return id;
68
+ };
69
+ }
70
+ /** @returns The nodes, in first-appearance order. */
71
+ nodes() {
72
+ return this.#nodes;
30
73
  }
31
74
  };
32
75
  /**
@@ -128,7 +171,7 @@ function parseEdgeList(text, options) {
128
171
  }
129
172
  //#endregion
130
173
  //#region src/parsers/gd-contest.ts
131
- const FORMAT$11 = "gd-contest";
174
+ const FORMAT$14 = "gd-contest";
132
175
  const NUMBER$9 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
133
176
  /**
134
177
  * Reads a vertex number, which the format numbers from 0 to N - 1.
@@ -137,7 +180,7 @@ const NUMBER$9 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
137
180
  * @returns The vertex id as text.
138
181
  */
139
182
  function vertex$1(token, count) {
140
- if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$11, token.line, `expected a vertex number from 0 to ${String(count - 1)}, found ${JSON.stringify(token.text)}`);
183
+ if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$14, token.line, `expected a vertex number from 0 to ${String(count - 1)}, found ${JSON.stringify(token.text)}`);
141
184
  return token.text;
142
185
  }
143
186
  /**
@@ -157,17 +200,17 @@ function parseGdContest(text) {
157
200
  });
158
201
  }
159
202
  const [first] = tokens;
160
- if (first === void 0) throw new NativeParseError(FORMAT$11, 1, "the file holds no vertex count");
161
- if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$11, first.line, `the vertex count must be a non-negative integer, found ${JSON.stringify(first.text)}`);
203
+ if (first === void 0) throw new NativeParseError(FORMAT$14, 1, "the file holds no vertex count");
204
+ if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$14, first.line, `the vertex count must be a non-negative integer, found ${JSON.stringify(first.text)}`);
162
205
  const count = Number(first.text);
163
206
  const coordinates = tokens.slice(1, 1 + 2 * count);
164
- if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$11, lines(text).length, `expected ${String(2 * count)} coordinates for ${String(count)} vertices, found ${String(coordinates.length)}`);
165
- for (const coordinate of coordinates) if (!NUMBER$9.test(coordinate.text)) throw new NativeParseError(FORMAT$11, coordinate.line, `coordinate ${JSON.stringify(coordinate.text)} is not a number`);
207
+ if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$14, lines(text).length, `expected ${String(2 * count)} coordinates for ${String(count)} vertices, found ${String(coordinates.length)}`);
208
+ for (const coordinate of coordinates) if (!NUMBER$9.test(coordinate.text)) throw new NativeParseError(FORMAT$14, coordinate.line, `coordinate ${JSON.stringify(coordinate.text)} is not a number`);
166
209
  const ends = tokens.slice(1 + 2 * count);
167
210
  const edges = [];
168
211
  for (let source = ends.shift(); source !== void 0; source = ends.shift()) {
169
212
  const target = ends.shift();
170
- if (target === void 0) throw new NativeParseError(FORMAT$11, source.line, `edge from vertex ${source.text} has no second vertex`);
213
+ if (target === void 0) throw new NativeParseError(FORMAT$14, source.line, `edge from vertex ${source.text} has no second vertex`);
171
214
  edges.push({
172
215
  source: vertex$1(source, count),
173
216
  target: vertex$1(target, count)
@@ -336,8 +379,191 @@ function parseGml(text) {
336
379
  };
337
380
  }
338
381
  //#endregion
382
+ //#region src/parsers/hgb.ts
383
+ const FORMAT$13 = "hgb";
384
+ /** A type code or node id as the HGB files write them: a non-negative decimal integer with no sign or leading zero. */
385
+ const CODE = /^(?:0|[1-9]\d*)$/;
386
+ /** A number as a decimal literal with an optional sign and exponent, so `Number` cannot read hex, an empty string or infinity. */
387
+ const DECIMAL$4 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
388
+ /** `node.dat` lines are the id, the name and the type, then the attributes on node types that have them. */
389
+ const NODE_FIELDS = 3;
390
+ const NODE_FIELDS_WITH_ATTRIBUTES = 4;
391
+ /** `link.dat` lines are the source, the target, the link type and the weight. */
392
+ const LINK_FIELDS = 4;
393
+ /**
394
+ * Reads a type code and resolves it against the types the recipe gives, in code order.
395
+ * @param field - The field.
396
+ * @param types - The recipe's types.
397
+ * @param where - Where the field is, for the error.
398
+ * @returns The type the code names.
399
+ * @throws NativeParseError when the field is not a code or names no type of the recipe.
400
+ */
401
+ function resolve(field, types, where) {
402
+ const type = types[CODE.test(field) ? Number(field) : -1];
403
+ if (type === void 0) throw new NativeParseError(FORMAT$13, where.line, `${where.file}: the ${where.kind} type ${JSON.stringify(field)} is not one of the ${String(types.length)} the recipe names`);
404
+ return type;
405
+ }
406
+ /**
407
+ * Parses a Heterogeneous Graph Benchmark dataset (Lv et al., "Are we really making much progress? Revisiting, benchmarking, and refining heterogeneous graph neural networks", KDD 2021): `node.dat`, one node per line as `id<TAB>name<TAB>type`, with a fourth field of attributes on the node types that have them, and `link.dat`, one link per line as `source<TAB>target<TAB>type<TAB>weight`. The type codes number the types the recipe names, which are the ones the dataset's `info.dat` gives, in code order, each link type with the node types it runs from and to.
408
+ *
409
+ * Nodes are the lines of `node.dat` in file order, each with its id and `type` the name of its node type; names and attributes are not read. Edges are the lines of `link.dat` in file order, each from its source to its target with `type` the name of its link type. Every link's endpoints must be nodes of the node types its link type joins, and its weight must be 1, the only weight the benchmark's files hold, so the weight column carries nothing the graph drops.
410
+ * @param parts - The two files.
411
+ * @param options - The recipe's parser.
412
+ * @returns The native graph.
413
+ * @throws NativeParseError naming the file and line for a line with the wrong number of fields, an id that is not a code or repeats, a type code the recipe does not name, a link whose endpoint is not a node or is of another node type than its link type joins, a weight other than 1, and a file with no lines.
414
+ */
415
+ function parseHgb(parts, options) {
416
+ const nodes = [];
417
+ const typeOf = /* @__PURE__ */ new Map();
418
+ forEachLine(parts.nodes, (line, number) => {
419
+ const fields = line.split(" ");
420
+ const [id, , type] = fields;
421
+ if (fields.length !== NODE_FIELDS && fields.length !== NODE_FIELDS_WITH_ATTRIBUTES || id === void 0 || type === void 0) throw new NativeParseError(FORMAT$13, number, `node.dat: expected ${String(NODE_FIELDS)} or ${String(NODE_FIELDS_WITH_ATTRIBUTES)} fields separated by tabs, found ${String(fields.length)}`);
422
+ if (!CODE.test(id)) throw new NativeParseError(FORMAT$13, number, `node.dat: the node id ${JSON.stringify(id)} is not a non-negative integer`);
423
+ if (typeOf.has(id)) throw new NativeParseError(FORMAT$13, number, `node.dat: the node id ${id} is repeated`);
424
+ const nodeType = resolve(type, options.nodeTypes, {
425
+ file: "node.dat",
426
+ line: number,
427
+ kind: "node"
428
+ });
429
+ typeOf.set(id, nodeType);
430
+ nodes.push({
431
+ id,
432
+ type: nodeType
433
+ });
434
+ });
435
+ if (nodes.length === 0) throw new NativeParseError(FORMAT$13, 1, "node.dat: the file holds no nodes");
436
+ const edges = [];
437
+ forEachLine(parts.links, (line, number) => {
438
+ const fields = line.split(" ");
439
+ const [source, target, type, weight] = fields;
440
+ if (fields.length !== LINK_FIELDS || source === void 0 || target === void 0 || type === void 0 || weight === void 0) throw new NativeParseError(FORMAT$13, number, `link.dat: expected ${String(LINK_FIELDS)} fields separated by tabs, found ${String(fields.length)}`);
441
+ const linkType = resolve(type, options.linkTypes, {
442
+ file: "link.dat",
443
+ line: number,
444
+ kind: "link"
445
+ });
446
+ for (const [end, id, expected] of [[
447
+ "source",
448
+ source,
449
+ linkType.source
450
+ ], [
451
+ "target",
452
+ target,
453
+ linkType.target
454
+ ]]) {
455
+ const actual = typeOf.get(id);
456
+ if (actual === void 0) throw new NativeParseError(FORMAT$13, number, `link.dat: the ${end} ${JSON.stringify(id)} is not a node of node.dat`);
457
+ if (actual !== expected) throw new NativeParseError(FORMAT$13, number, `link.dat: a ${linkType.name} link's ${end} must be a ${expected} node, and ${id} is a ${actual} node`);
458
+ }
459
+ if (!DECIMAL$4.test(weight) || Number(weight) !== 1) throw new NativeParseError(FORMAT$13, number, `link.dat: the weight ${JSON.stringify(weight)} is not 1; a weighted HGB file would lose its weights`);
460
+ edges.push({
461
+ source,
462
+ target,
463
+ type: linkType.name
464
+ });
465
+ });
466
+ if (edges.length === 0) throw new NativeParseError(FORMAT$13, 1, "link.dat: the file holds no links");
467
+ return {
468
+ nodes,
469
+ edges
470
+ };
471
+ }
472
+ //#endregion
473
+ //#region src/parsers/kg-triples.ts
474
+ const FORMAT$12 = "kg-triples";
475
+ /** The character each delimiter option splits a line on. A line is split on exactly that character, so a field cannot hide a second delimiter. */
476
+ const DELIMITERS$1 = {
477
+ tab: " ",
478
+ space: " "
479
+ };
480
+ /** The fields of a triple line: the head, the relation and the tail, in one of two orders. */
481
+ const TRIPLE_FIELDS = 3;
482
+ /** A count line: a non-negative decimal integer with no sign or leading zero. */
483
+ const COUNT$1 = /^(?:0|[1-9]\d*)$/;
484
+ /** The double quote that wraps every field of a quoted file. */
485
+ const QUOTE$2 = "\"";
486
+ /**
487
+ * Reads one field: unwraps the quotes of a quoted file, and refuses an empty field.
488
+ * @param field - The field as the line has it.
489
+ * @param quoted - Whether every field of the file is wrapped in double quotes.
490
+ * @param where - Where the field is, for the error.
491
+ * @returns The field's value.
492
+ * @throws NativeParseError for an empty field, or in a quoted file a field not wrapped in quotes or holding one.
493
+ */
494
+ function value(field, quoted, where) {
495
+ const inner = quoted ? field.slice(1, -1) : field;
496
+ if (quoted && (field.length < 2 || !field.startsWith(QUOTE$2) || !field.endsWith(QUOTE$2) || inner.includes(QUOTE$2))) throw new NativeParseError(FORMAT$12, where.line, `${where.part}the field ${JSON.stringify(field)} is not one value wrapped in double quotes`);
497
+ if (inner === "") throw new NativeParseError(FORMAT$12, where.line, `${where.part}a field is empty`);
498
+ return inner;
499
+ }
500
+ /**
501
+ * Parses knowledge graph triple files into one graph: each line of each part is one triple, a head, a relation and a tail separated by the recipe's delimiter, in the recipe's field order. The part's first line is a triple, a header naming the columns (skipped), or the number of triples the part holds (checked), as the recipe's `header` says.
502
+ *
503
+ * Nodes are the heads and tails, in order of first appearance across the parts in the order given, with no type: the files type their relations, not their entities. Every line is an edge from its head to its tail, in part order then file order, with `type` the relation as written; a triple that appears twice is two edges. When the parts carry split names, each edge's `split` is the name of the part it came from; a single file has no split and its edges carry none.
504
+ * @param parts - The files, in the order the recipe names them: every one named by its split, or a single file with none.
505
+ * @param options - The recipe's parser.
506
+ * @returns The native graph.
507
+ * @throws NativeParseError naming the part and line for a line without exactly three fields, an empty or wrongly quoted field, a blank line, a count line that is not a count or differs from the part's triples, and a part with no triples.
508
+ */
509
+ function parseKgTriples(parts, options) {
510
+ const delimiter = DELIMITERS$1[options.delimiter];
511
+ const order = new NodeOrder();
512
+ const relations = /* @__PURE__ */ new Map();
513
+ const edges = [];
514
+ for (const part of parts) {
515
+ const label = part.split === void 0 ? "" : `${part.split}: `;
516
+ let declared;
517
+ let triples = 0;
518
+ let lastLine = 0;
519
+ forEachLine(part.pieces, (line, number) => {
520
+ lastLine = number;
521
+ if (line.trim() === "") throw new NativeParseError(FORMAT$12, number, `${label}the line is blank`);
522
+ if (number === 1 && options.header === "names") return;
523
+ if (number === 1 && options.header === "count") {
524
+ if (!COUNT$1.test(line)) throw new NativeParseError(FORMAT$12, number, `${label}expected the number of triples on the first line, found ${JSON.stringify(line)}`);
525
+ declared = Number(line);
526
+ return;
527
+ }
528
+ const fields = line.split(delimiter);
529
+ const [head, middle, last] = fields;
530
+ if (fields.length !== TRIPLE_FIELDS || head === void 0 || middle === void 0 || last === void 0) throw new NativeParseError(FORMAT$12, number, `${label}expected ${String(TRIPLE_FIELDS)} fields separated by ${options.delimiter}, found ${String(fields.length)}`);
531
+ const where = {
532
+ line: number,
533
+ part: label
534
+ };
535
+ const [relationField, tailField] = options.order === "head-relation-tail" ? [middle, last] : [last, middle];
536
+ const source = order.intern(value(head, options.quoted, where));
537
+ const relation = value(relationField, options.quoted, where);
538
+ const target = order.intern(value(tailField, options.quoted, where));
539
+ let type = relations.get(relation);
540
+ if (type === void 0) {
541
+ type = relation;
542
+ relations.set(relation, relation);
543
+ }
544
+ edges.push(part.split === void 0 ? {
545
+ source,
546
+ target,
547
+ type
548
+ } : {
549
+ source,
550
+ target,
551
+ type,
552
+ split: part.split
553
+ });
554
+ triples += 1;
555
+ });
556
+ if (triples === 0) throw new NativeParseError(FORMAT$12, lastLine, `${label}the file holds no triples`);
557
+ if (declared !== void 0 && declared !== triples) throw new NativeParseError(FORMAT$12, lastLine, `${label}the first line declares ${String(declared)} triples, the file holds ${String(triples)}`);
558
+ }
559
+ return {
560
+ nodes: order.nodes(),
561
+ edges
562
+ };
563
+ }
564
+ //#endregion
339
565
  //#region src/parsers/konect.ts
340
- const FORMAT$10 = "konect";
566
+ const FORMAT$11 = "konect";
341
567
  /** The network formats of KONECT's first line (Kunegis, "The KONECT Handbook", table 1): undirected, directed and bipartite. */
342
568
  const NETWORK_FORMATS = /* @__PURE__ */ new Set([
343
569
  "sym",
@@ -412,9 +638,9 @@ function parseKonect(pieces, options) {
412
638
  }));
413
639
  });
414
640
  const { header } = state;
415
- if (header === void 0) throw new NativeParseError(FORMAT$10, lastLine, "the file has no first line naming its format and weights");
416
- if (edges.length === 0) throw new NativeParseError(FORMAT$10, lastLine, "the file holds no edges");
417
- if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$10, lastLine, `the second line declares ${String(header.counts.edges)} edges, the file holds ${String(edges.length)}`);
641
+ if (header === void 0) throw new NativeParseError(FORMAT$11, lastLine, "the file has no first line naming its format and weights");
642
+ if (edges.length === 0) throw new NativeParseError(FORMAT$11, lastLine, "the file holds no edges");
643
+ if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$11, lastLine, `the second line declares ${String(header.counts.edges)} edges, the file holds ${String(edges.length)}`);
418
644
  return {
419
645
  nodes: order.nodes(),
420
646
  edges
@@ -432,9 +658,9 @@ function readHeader(content, number, options) {
432
658
  const match = FORMAT_LINE.exec(content);
433
659
  const network = match?.[1];
434
660
  const weights = match?.[2];
435
- if (network === void 0 || weights === void 0 || !NETWORK_FORMATS.has(network)) throw new NativeParseError(FORMAT$10, 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$10, 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$10, 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)}`);
661
+ if (network === void 0 || weights === void 0 || !NETWORK_FORMATS.has(network)) throw new NativeParseError(FORMAT$11, number, `expected a first line "% FORMAT WEIGHTS" with FORMAT one of ${[...NETWORK_FORMATS].join(", ")}, found ${JSON.stringify(content)}`);
662
+ if (!WEIGHTED_TYPES.has(weights) && !UNWEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$11, 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)`);
663
+ if (options.weighted !== WEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$11, 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
664
  return {
439
665
  line: number,
440
666
  network
@@ -454,15 +680,15 @@ function readEdge(content, number, context) {
454
680
  const [from, to, third, time] = fields;
455
681
  const expected = options.timestamped ? TIMESTAMPED_COLUMNS : options.weighted ? WEIGHTED_COLUMNS : void 0;
456
682
  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$10, number, `expected ${expected === void 0 ? `${String(ENDPOINT_COLUMNS)} or ${String(WEIGHTED_COLUMNS)}` : String(expected)} columns for this recipe, found ${String(fields.length)}`);
683
+ if (from === void 0 || to === void 0 || !fits) throw new NativeParseError(FORMAT$11, number, `expected ${expected === void 0 ? `${String(ENDPOINT_COLUMNS)} or ${String(WEIGHTED_COLUMNS)}` : String(expected)} columns for this recipe, found ${String(fields.length)}`);
458
684
  const bipartite = header.network === "bip";
459
685
  const source = endpoint(from, bipartite ? LEFT_PREFIX : "", header.counts?.left, number);
460
686
  const target = endpoint(to, bipartite ? RIGHT_PREFIX : "", header.counts?.right, number);
461
687
  order.add(source);
462
688
  order.add(target);
463
- if (third !== void 0 && !options.weighted && (!DECIMAL$3.test(third) || Number(third) !== 1)) throw new NativeParseError(FORMAT$10, 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$10, number, `the weight ${JSON.stringify(third)} is not a number`);
465
- if (time !== void 0 && !DECIMAL$3.test(time)) throw new NativeParseError(FORMAT$10, number, `the time ${JSON.stringify(time)} is not a number`);
689
+ if (third !== void 0 && !options.weighted && (!DECIMAL$3.test(third) || Number(third) !== 1)) throw new NativeParseError(FORMAT$11, number, `the third column ${JSON.stringify(third)} is not 1; an edge that stands for several would be read as one`);
690
+ if (third !== void 0 && options.weighted && !DECIMAL$3.test(third)) throw new NativeParseError(FORMAT$11, number, `the weight ${JSON.stringify(third)} is not a number`);
691
+ if (time !== void 0 && !DECIMAL$3.test(time)) throw new NativeParseError(FORMAT$11, number, `the time ${JSON.stringify(time)} is not a number`);
466
692
  const edge = {
467
693
  source,
468
694
  target
@@ -481,7 +707,7 @@ function readEdge(content, number, context) {
481
707
  * @throws NativeParseError when the token is not a positive integer or exceeds the set's size.
482
708
  */
483
709
  function endpoint(token, prefix, size, number) {
484
- if (!NODE_ID.test(token) || size !== void 0 && Number(token) > size) throw new NativeParseError(FORMAT$10, number, `${JSON.stringify(token)} is not a node id from 1${size === void 0 ? "" : ` to ${String(size)}`}`);
710
+ if (!NODE_ID.test(token) || size !== void 0 && Number(token) > size) throw new NativeParseError(FORMAT$11, number, `${JSON.stringify(token)} is not a node id from 1${size === void 0 ? "" : ` to ${String(size)}`}`);
485
711
  return `${prefix}${token}`;
486
712
  }
487
713
  //#endregion
@@ -956,7 +1182,7 @@ function parseGeojson(text, options) {
956
1182
  }
957
1183
  //#endregion
958
1184
  //#region src/parsers/hhcrsp.ts
959
- const FORMAT$9 = "hhcrsp";
1185
+ const FORMAT$10 = "hhcrsp";
960
1186
  /** The labels of the sheet's blocks, each the top-left cell of a block whose header runs to its right and whose rows run below it. */
961
1187
  const LABEL = {
962
1188
  patients: "Patients",
@@ -985,7 +1211,7 @@ function blockOf(grid, label, pastHeader = "empty") {
985
1211
  column
986
1212
  }] : []));
987
1213
  const [only] = found;
988
- if (only === void 0 || found.length !== 1) throw new NativeParseError(FORMAT$9, 1, `expected one block labelled ${JSON.stringify(label)}, found ${String(found.length)}`);
1214
+ if (only === void 0 || found.length !== 1) throw new NativeParseError(FORMAT$10, 1, `expected one block labelled ${JSON.stringify(label)}, found ${String(found.length)}`);
989
1215
  const headerRow = grid[only.line] ?? [];
990
1216
  const end = headerRow.findIndex((cell, column) => column > only.column && cell.trim() === "");
991
1217
  const header = headerRow.slice(only.column + 1, end === -1 ? headerRow.length : end).map((cell) => cell.trim());
@@ -994,7 +1220,7 @@ function blockOf(grid, label, pastHeader = "empty") {
994
1220
  const cells = (grid[line] ?? []).slice(only.column, only.column + 1 + header.length).map((cell) => cell.trim());
995
1221
  if (cells.every((cell) => cell === "")) break;
996
1222
  const past = (grid[line] ?? [])[only.column + 1 + header.length]?.trim();
997
- if (pastHeader === "empty" && past !== void 0 && past !== "") throw new NativeParseError(FORMAT$9, line + 1, `the ${label} block's row holds ${JSON.stringify(past)} past its header ${JSON.stringify(header)}, which does not label it`);
1223
+ if (pastHeader === "empty" && past !== void 0 && past !== "") throw new NativeParseError(FORMAT$10, line + 1, `the ${label} block's row holds ${JSON.stringify(past)} past its header ${JSON.stringify(header)}, which does not label it`);
998
1224
  rows.push({
999
1225
  line: line + 1,
1000
1226
  label: cells[0] ?? "",
@@ -1017,7 +1243,7 @@ function blockOf(grid, label, pastHeader = "empty") {
1017
1243
  * @throws NativeParseError when the cell is not a decimal number.
1018
1244
  */
1019
1245
  function numberOf$4(cell, line, what) {
1020
- if (cell === void 0 || !NUMBER$7.test(cell)) throw new NativeParseError(FORMAT$9, line, `expected ${what}, found ${JSON.stringify(cell)}`);
1246
+ if (cell === void 0 || !NUMBER$7.test(cell)) throw new NativeParseError(FORMAT$10, line, `expected ${what}, found ${JSON.stringify(cell)}`);
1021
1247
  return Number(cell);
1022
1248
  }
1023
1249
  /**
@@ -1029,7 +1255,7 @@ function numberOf$4(cell, line, what) {
1029
1255
  */
1030
1256
  function checkNumbered(block, noun, label) {
1031
1257
  block.rows.forEach((row, index) => {
1032
- if (row.label !== `${noun} ${String(index + 1)}`) throw new NativeParseError(FORMAT$9, row.line, `the ${label} block's row ${String(index + 1)} is labelled ${JSON.stringify(row.label)}, not ${noun} ${String(index + 1)}`);
1258
+ if (row.label !== `${noun} ${String(index + 1)}`) throw new NativeParseError(FORMAT$10, row.line, `the ${label} block's row ${String(index + 1)} is labelled ${JSON.stringify(row.label)}, not ${noun} ${String(index + 1)}`);
1033
1259
  });
1034
1260
  }
1035
1261
  /**
@@ -1041,7 +1267,7 @@ function checkNumbered(block, noun, label) {
1041
1267
  */
1042
1268
  function serviceCount(block, label) {
1043
1269
  block.header.forEach((cell, index) => {
1044
- if (SERVICE.exec(cell)?.[1] !== String(index + 1)) throw new NativeParseError(FORMAT$9, block.line, `the ${label} block's header cell ${String(index + 1)} is ${JSON.stringify(cell)}, not Service ${String(index + 1)}`);
1270
+ if (SERVICE.exec(cell)?.[1] !== String(index + 1)) throw new NativeParseError(FORMAT$10, block.line, `the ${label} block's header cell ${String(index + 1)} is ${JSON.stringify(cell)}, not Service ${String(index + 1)}`);
1045
1271
  });
1046
1272
  return block.header.length;
1047
1273
  }
@@ -1060,7 +1286,7 @@ function parseHhcrsp(text) {
1060
1286
  const caregiverWindows = blockOf(grid, LABEL.caregiverWindows);
1061
1287
  const windows = blockOf(grid, LABEL.patientWindows);
1062
1288
  const serviceKinds = serviceCount(requests, LABEL.requests);
1063
- if (serviceCount(times, LABEL.times) !== serviceKinds || serviceCount(skills, LABEL.skills) !== serviceKinds) throw new NativeParseError(FORMAT$9, times.line, `the ${LABEL.requests}, ${LABEL.times} and ${LABEL.skills} blocks name different numbers of services`);
1289
+ if (serviceCount(times, LABEL.times) !== serviceKinds || serviceCount(skills, LABEL.skills) !== serviceKinds) throw new NativeParseError(FORMAT$10, times.line, `the ${LABEL.requests}, ${LABEL.times} and ${LABEL.skills} blocks name different numbers of services`);
1064
1290
  checkNumbered(requests, "Patient", LABEL.requests);
1065
1291
  checkNumbered(windows, "Patient", LABEL.patientWindows);
1066
1292
  checkNumbered(skills, "Caregiver", LABEL.skills);
@@ -1075,11 +1301,11 @@ function parseHhcrsp(text) {
1075
1301
  "",
1076
1302
  "center",
1077
1303
  "centre"
1078
- ].includes(centreTimes.label.toLowerCase()) || windows.rows.length !== patients || patientTimes.length !== patients || skills.rows.length !== caregiverWindows.rows.length) throw new NativeParseError(FORMAT$9, requests.line, `the blocks disagree: ${String(patients)} patients requesting services, ${String(patientTimes.length)} with durations after the centre's row, ${String(windows.rows.length)} with time windows, and ${String(skills.rows.length)} caregivers with skills against ${String(caregiverWindows.rows.length)} with windows`);
1079
- if (positions.header.join(",") !== "X,Y" || positions.rows.length !== patients + 2 || positions.rows.some((row, index) => row.label !== String(index))) throw new NativeParseError(FORMAT$9, positions.line, `the ${LABEL.patients} block must give X and Y for nodes 0 to ${String(patients + 1)}, the centre, ${String(patients)} patients and the centre again`);
1304
+ ].includes(centreTimes.label.toLowerCase()) || windows.rows.length !== patients || patientTimes.length !== patients || skills.rows.length !== caregiverWindows.rows.length) throw new NativeParseError(FORMAT$10, requests.line, `the blocks disagree: ${String(patients)} patients requesting services, ${String(patientTimes.length)} with durations after the centre's row, ${String(windows.rows.length)} with time windows, and ${String(skills.rows.length)} caregivers with skills against ${String(caregiverWindows.rows.length)} with windows`);
1305
+ if (positions.header.join(",") !== "X,Y" || positions.rows.length !== patients + 2 || positions.rows.some((row, index) => row.label !== String(index))) throw new NativeParseError(FORMAT$10, positions.line, `the ${LABEL.patients} block must give X and Y for nodes 0 to ${String(patients + 1)}, the centre, ${String(patients)} patients and the centre again`);
1080
1306
  const position = (index) => {
1081
1307
  const row = positions.rows[index];
1082
- if (row === void 0) throw new NativeParseError(FORMAT$9, positions.line, `the ${LABEL.patients} block has no node ${String(index)}`);
1308
+ if (row === void 0) throw new NativeParseError(FORMAT$10, positions.line, `the ${LABEL.patients} block has no node ${String(index)}`);
1083
1309
  return {
1084
1310
  x: numberOf$4(row.cells[0], row.line, "an X"),
1085
1311
  y: numberOf$4(row.cells[1], row.line, "a Y"),
@@ -1088,7 +1314,7 @@ function parseHhcrsp(text) {
1088
1314
  };
1089
1315
  const { x, y } = position(0);
1090
1316
  const closing = position(patients + 1);
1091
- if (closing.x !== x || closing.y !== y) throw new NativeParseError(FORMAT$9, closing.line, `the last node of the ${LABEL.patients} block is not at the centre's position, so it is not the route's end`);
1317
+ if (closing.x !== x || closing.y !== y) throw new NativeParseError(FORMAT$10, closing.line, `the last node of the ${LABEL.patients} block is not at the centre's position, so it is not the route's end`);
1092
1318
  const windowColumn = (name) => {
1093
1319
  const index = windows.header.indexOf(name);
1094
1320
  return index === -1 ? void 0 : index;
@@ -1099,7 +1325,7 @@ function parseHhcrsp(text) {
1099
1325
  earliest2: windowColumn("ETW 2"),
1100
1326
  latest2: windowColumn("LTW 2")
1101
1327
  };
1102
- if (columns.earliest1 === void 0 || columns.latest1 === void 0 || columns.earliest2 === void 0 !== (columns.latest2 === void 0) || windows.header.length !== (columns.earliest2 === void 0 ? 2 : 4)) throw new NativeParseError(FORMAT$9, windows.line, `the ${LABEL.patientWindows} header ${JSON.stringify(windows.header)} is not ETW 1 and LTW 1, with or without ETW 2 and LTW 2`);
1328
+ if (columns.earliest1 === void 0 || columns.latest1 === void 0 || columns.earliest2 === void 0 !== (columns.latest2 === void 0) || windows.header.length !== (columns.earliest2 === void 0 ? 2 : 4)) throw new NativeParseError(FORMAT$10, windows.line, `the ${LABEL.patientWindows} header ${JSON.stringify(windows.header)} is not ETW 1 and LTW 1, with or without ETW 2 and LTW 2`);
1103
1329
  const nodes = [{
1104
1330
  id: "0",
1105
1331
  role: "centre",
@@ -1115,15 +1341,15 @@ function parseHhcrsp(text) {
1115
1341
  }];
1116
1342
  requests.rows.forEach((request, index) => {
1117
1343
  const required = request.cells.flatMap((cell, service) => {
1118
- if (cell !== "0" && cell !== "1" && cell !== "") throw new NativeParseError(FORMAT$9, request.line, `${request.label} has ${JSON.stringify(cell)} for Service ${String(service + 1)}, not 0, 1 or empty`);
1344
+ if (cell !== "0" && cell !== "1" && cell !== "") throw new NativeParseError(FORMAT$10, request.line, `${request.label} has ${JSON.stringify(cell)} for Service ${String(service + 1)}, not 0, 1 or empty`);
1119
1345
  return cell === "1" ? [service] : [];
1120
1346
  });
1121
1347
  const stated = (grid[request.line - 1] ?? [])[requests.column + serviceKinds + 1]?.trim();
1122
- if (stated !== String(required.length)) throw new NativeParseError(FORMAT$9, request.line, `${request.label} requires ${String(required.length)} services by its marks, and the count beside them is ${JSON.stringify(stated)}`);
1123
- if (required.length === 0 || required.length > MOST_SERVICES) throw new NativeParseError(FORMAT$9, request.line, `${request.label} requires ${String(required.length)} services, and a patient requires one or two`);
1348
+ if (stated !== String(required.length)) throw new NativeParseError(FORMAT$10, request.line, `${request.label} requires ${String(required.length)} services by its marks, and the count beside them is ${JSON.stringify(stated)}`);
1349
+ if (required.length === 0 || required.length > MOST_SERVICES) throw new NativeParseError(FORMAT$10, request.line, `${request.label} requires ${String(required.length)} services, and a patient requires one or two`);
1124
1350
  const timeRow = patientTimes[index];
1125
1351
  const windowRow = windows.rows[index];
1126
- if (timeRow === void 0 || windowRow === void 0) throw new NativeParseError(FORMAT$9, request.line, `${request.label} has no durations or time windows`);
1352
+ if (timeRow === void 0 || windowRow === void 0) throw new NativeParseError(FORMAT$10, request.line, `${request.label} has no durations or time windows`);
1127
1353
  const duration = (which) => {
1128
1354
  const service = required[which];
1129
1355
  return service === void 0 ? null : numberOf$4(timeRow.cells[service], timeRow.line, `the duration of Service ${String(service + 1)}`);
@@ -1267,7 +1493,7 @@ function parsePajek(text) {
1267
1493
  }
1268
1494
  //#endregion
1269
1495
  //#region src/parsers/customer-points.ts
1270
- const FORMAT$8 = "customer-points";
1496
+ const FORMAT$9 = "customer-points";
1271
1497
  const NUMBER$6 = /^-?\d+(?:\.\d+)?$/;
1272
1498
  const WHOLE$2 = /^\d+$/;
1273
1499
  /**
@@ -1283,12 +1509,12 @@ function parseCustomerPoints(text) {
1283
1509
  })).filter((row) => row.fields.length > 0);
1284
1510
  const [first, ...body] = rows;
1285
1511
  const [key, count, ...extra] = first?.fields ?? [];
1286
- 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(" "))}`);
1512
+ if (first === void 0 || key !== "customers" || count === void 0 || !WHOLE$2.test(count) || extra.length > 0) throw new NativeParseError(FORMAT$9, first?.line ?? 1, `expected "customers <count>", found ${JSON.stringify(first?.fields.join(" "))}`);
1287
1513
  const seen = /* @__PURE__ */ new Set();
1288
1514
  const nodes = body.map((row) => {
1289
1515
  const [id, x, y, ...rest] = row.fields;
1290
- 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(" "))}`);
1291
- if (seen.has(id)) throw new NativeParseError(FORMAT$8, row.line, `customer ${id} appears twice`);
1516
+ 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$9, row.line, `expected a customer id and two numbers, found ${JSON.stringify(row.fields.join(" "))}`);
1517
+ if (seen.has(id)) throw new NativeParseError(FORMAT$9, row.line, `customer ${id} appears twice`);
1292
1518
  seen.add(id);
1293
1519
  return {
1294
1520
  id,
@@ -1296,7 +1522,7 @@ function parseCustomerPoints(text) {
1296
1522
  y: Number(y)
1297
1523
  };
1298
1524
  });
1299
- 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)}`);
1525
+ if (nodes.length !== Number(count)) throw new NativeParseError(FORMAT$9, rows[rows.length - 1]?.line ?? first.line, `the file announces ${count} customers, found ${String(nodes.length)}`);
1300
1526
  return {
1301
1527
  meta: { customers: Number(count) },
1302
1528
  nodes
@@ -1304,7 +1530,7 @@ function parseCustomerPoints(text) {
1304
1530
  }
1305
1531
  //#endregion
1306
1532
  //#region src/parsers/solomon.ts
1307
- const FORMAT$7 = "solomon";
1533
+ const FORMAT$8 = "solomon";
1308
1534
  /** 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). */
1309
1535
  const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
1310
1536
  /**
@@ -1316,7 +1542,7 @@ const NUMBER$5 = /^-?\d+(?:\.\d+)?$/;
1316
1542
  * @throws NativeParseError when the token is not a decimal number.
1317
1543
  */
1318
1544
  function numberOf$3(token, line, what) {
1319
- if (!NUMBER$5.test(token)) throw new NativeParseError(FORMAT$7, line, `expected ${what}, found ${JSON.stringify(token)}`);
1545
+ if (!NUMBER$5.test(token)) throw new NativeParseError(FORMAT$8, line, `expected ${what}, found ${JSON.stringify(token)}`);
1320
1546
  return Number(token);
1321
1547
  }
1322
1548
  /**
@@ -1329,7 +1555,7 @@ function parseSolomon(text) {
1329
1555
  const all = lines(text);
1330
1556
  const present = (index) => (all[index] ?? "").trim();
1331
1557
  const name = present(0);
1332
- if (name === "") throw new NativeParseError(FORMAT$7, 1, "expected the instance name on the first line");
1558
+ if (name === "") throw new NativeParseError(FORMAT$8, 1, "expected the instance name on the first line");
1333
1559
  const meta = { name };
1334
1560
  const nodes = [];
1335
1561
  const seen = /* @__PURE__ */ new Set();
@@ -1345,7 +1571,7 @@ function parseSolomon(text) {
1345
1571
  awaitingHeader = true;
1346
1572
  continue;
1347
1573
  }
1348
- if (section === "preamble") throw new NativeParseError(FORMAT$7, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
1574
+ if (section === "preamble") throw new NativeParseError(FORMAT$8, line, `expected VEHICLE or CUSTOMER, found ${JSON.stringify(trimmed)}`);
1349
1575
  if (awaitingHeader) {
1350
1576
  awaitingHeader = false;
1351
1577
  continue;
@@ -1353,15 +1579,15 @@ function parseSolomon(text) {
1353
1579
  const tokens = trimmed.split(/\s+/);
1354
1580
  if (section === "vehicle") {
1355
1581
  const [vehicles, capacity, ...extra] = tokens;
1356
- 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)}`);
1582
+ if (vehicles === void 0 || capacity === void 0 || extra.length > 0) throw new NativeParseError(FORMAT$8, line, `expected the vehicle count and capacity, found ${JSON.stringify(trimmed)}`);
1357
1583
  meta.vehicles = numberOf$3(vehicles, line, "the vehicle count");
1358
1584
  meta.capacity = numberOf$3(capacity, line, "the vehicle capacity");
1359
1585
  continue;
1360
1586
  }
1361
1587
  const [id, x, y, demand, ready, due, service, ...extra] = tokens;
1362
- 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)}`);
1363
- if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$7, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1364
- if (seen.has(id)) throw new NativeParseError(FORMAT$7, line, `node ${id} appears twice`);
1588
+ 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$8, line, `expected seven numbers (id, x, y, demand, ready, due, service), found ${JSON.stringify(trimmed)}`);
1589
+ if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$8, line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1590
+ if (seen.has(id)) throw new NativeParseError(FORMAT$8, line, `node ${id} appears twice`);
1365
1591
  seen.add(id);
1366
1592
  nodes.push({
1367
1593
  id,
@@ -1373,9 +1599,9 @@ function parseSolomon(text) {
1373
1599
  service: numberOf$3(service, line, "the service time")
1374
1600
  });
1375
1601
  }
1376
- if (section !== "customer") throw new NativeParseError(FORMAT$7, all.length, "expected a CUSTOMER section");
1377
- if (nodes.length === 0) throw new NativeParseError(FORMAT$7, all.length, "the CUSTOMER section has no rows");
1378
- 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");
1602
+ if (section !== "customer") throw new NativeParseError(FORMAT$8, all.length, "expected a CUSTOMER section");
1603
+ if (nodes.length === 0) throw new NativeParseError(FORMAT$8, all.length, "the CUSTOMER section has no rows");
1604
+ if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$8, all.length, "expected a VEHICLE section with the vehicle count and capacity");
1379
1605
  return {
1380
1606
  meta: {
1381
1607
  name: meta.name,
@@ -1387,7 +1613,7 @@ function parseSolomon(text) {
1387
1613
  }
1388
1614
  //#endregion
1389
1615
  //#region src/parsers/top.ts
1390
- const FORMAT$6 = "top";
1616
+ const FORMAT$7 = "top";
1391
1617
  /** The header lines before the first point: `n`, `m` and `tmax`. */
1392
1618
  const HEADER_LINES = 3;
1393
1619
  const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
@@ -1401,7 +1627,7 @@ const NUMBER$4 = /^-?\d+(?:\.\d+)?$/;
1401
1627
  */
1402
1628
  function headerOf(text, key, line) {
1403
1629
  const [name, value, ...extra] = (text ?? "").trim().split(/\s+/);
1404
- 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())}`);
1630
+ if (name !== key || value === void 0 || extra.length > 0 || !NUMBER$4.test(value)) throw new NativeParseError(FORMAT$7, line, `expected "${key} <number>", found ${JSON.stringify((text ?? "").trim())}`);
1405
1631
  return Number(value);
1406
1632
  }
1407
1633
  /**
@@ -1420,11 +1646,11 @@ function parseTop(text) {
1420
1646
  const points = headerOf(first?.text, "n", first?.line ?? 1);
1421
1647
  const paths = headerOf(second?.text, "m", second?.line ?? 2);
1422
1648
  const budget = headerOf(third?.text, "tmax", third?.line ?? HEADER_LINES);
1423
- 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)}`);
1424
- 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`);
1649
+ if (!Number.isInteger(points) || points < 2) throw new NativeParseError(FORMAT$7, first?.line ?? 1, `expected at least a start and an end point, found n ${String(points)}`);
1650
+ if (rows.length !== points) throw new NativeParseError(FORMAT$7, all[all.length - 1]?.line ?? 1, `the header gives ${String(points)} points, found ${String(rows.length)} rows`);
1425
1651
  const nodes = rows.map((row, index) => {
1426
1652
  const [x, y, score, ...extra] = row.text.split(/\s+/);
1427
- 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)}`);
1653
+ 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$7, row.line, `expected three numbers (x, y, score), found ${JSON.stringify(row.text)}`);
1428
1654
  return {
1429
1655
  id: String(index),
1430
1656
  x: Number(x),
@@ -1444,7 +1670,7 @@ function parseTop(text) {
1444
1670
  }
1445
1671
  //#endregion
1446
1672
  //#region src/parsers/toptw.ts
1447
- const FORMAT$5 = "toptw";
1673
+ const FORMAT$6 = "toptw";
1448
1674
  /** How many numbers the first header line holds (k, paths, customers, t) and the second (D, capacity). */
1449
1675
  const FIRST_HEADER_NUMBERS = 4;
1450
1676
  const SECOND_HEADER_NUMBERS = 2;
@@ -1462,7 +1688,7 @@ const NUMBER$3 = /^-?\d+(?:\.\d+)?$/;
1462
1688
  * @throws NativeParseError when the token is not a decimal number.
1463
1689
  */
1464
1690
  function numberOf$2(token, line, what) {
1465
- if (token === void 0 || !NUMBER$3.test(token)) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(token)}`);
1691
+ if (token === void 0 || !NUMBER$3.test(token)) throw new NativeParseError(FORMAT$6, line, `expected ${what}, found ${JSON.stringify(token)}`);
1466
1692
  return Number(token);
1467
1693
  }
1468
1694
  /**
@@ -1476,7 +1702,7 @@ function numberOf$2(token, line, what) {
1476
1702
  */
1477
1703
  function numbersOf(text, count, line, what) {
1478
1704
  const tokens = text.trim().split(/\s+/);
1479
- if (tokens.length !== count) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
1705
+ if (tokens.length !== count) throw new NativeParseError(FORMAT$6, line, `expected ${what}, found ${JSON.stringify(text.trim())}`);
1480
1706
  return tokens.map((token) => numberOf$2(token, line, what));
1481
1707
  }
1482
1708
  /**
@@ -1491,18 +1717,18 @@ function parseToptw(text) {
1491
1717
  text: raw.trim()
1492
1718
  })).filter((entry) => entry.text !== "");
1493
1719
  const [first, second, ...rows] = all;
1494
- if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$5, 1, "expected the two header lines");
1720
+ if (first === void 0 || second === void 0) throw new NativeParseError(FORMAT$6, 1, "expected the two header lines");
1495
1721
  const [, paths, customers] = numbersOf(first.text, FIRST_HEADER_NUMBERS, first.line, "four numbers (k, paths, customers, t)");
1496
1722
  const [, capacity] = numbersOf(second.text, SECOND_HEADER_NUMBERS, second.line, "two numbers (D, capacity)");
1497
- if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$5, first.line, "the header lacks a number");
1723
+ if (paths === void 0 || customers === void 0 || capacity === void 0) throw new NativeParseError(FORMAT$6, first.line, "the header lacks a number");
1498
1724
  const seen = /* @__PURE__ */ new Set();
1499
1725
  const nodes = rows.map((row) => {
1500
1726
  const tokens = row.text.split(/\s+/);
1501
1727
  const listLength = numberOf$2(tokens[LIST_LENGTH_AT], row.line, "the length of the list after the unused number");
1502
- 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)}`);
1728
+ if (!Number.isInteger(listLength) || listLength < 0 || tokens.length !== ROW_NUMBERS_BESIDE_LIST + listLength) throw new NativeParseError(FORMAT$6, row.line, `expected ${String(ROW_NUMBERS_BESIDE_LIST + listLength)} numbers for a list of ${String(listLength)}, found ${String(tokens.length)}`);
1503
1729
  const [id] = tokens;
1504
- if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$5, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1505
- if (seen.has(id)) throw new NativeParseError(FORMAT$5, row.line, `node ${id} appears twice`);
1730
+ if (id === void 0 || !/^\d+$/.test(id)) throw new NativeParseError(FORMAT$6, row.line, `expected a whole number node id, found ${JSON.stringify(id)}`);
1731
+ if (seen.has(id)) throw new NativeParseError(FORMAT$6, row.line, `node ${id} appears twice`);
1506
1732
  seen.add(id);
1507
1733
  return {
1508
1734
  id,
@@ -1514,7 +1740,7 @@ function parseToptw(text) {
1514
1740
  close: numberOf$2(tokens[8 + listLength], row.line, "the closing of the window")
1515
1741
  };
1516
1742
  });
1517
- 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)}`);
1743
+ if (nodes.length !== customers + 1) throw new NativeParseError(FORMAT$6, 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)}`);
1518
1744
  return {
1519
1745
  meta: {
1520
1746
  paths,
@@ -1526,7 +1752,7 @@ function parseToptw(text) {
1526
1752
  }
1527
1753
  //#endregion
1528
1754
  //#region src/parsers/tsplib.ts
1529
- const FORMAT$4 = "tsplib";
1755
+ const FORMAT$5 = "tsplib";
1530
1756
  /** The instance types the parser reads: the probabilistic and the clustered orienteering problem. */
1531
1757
  const TYPES = /* @__PURE__ */ new Set(["POP", "COP"]);
1532
1758
  /** 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. */
@@ -1557,7 +1783,7 @@ function degrees(value, line) {
1557
1783
  const whole = Math.trunc(magnitude);
1558
1784
  const hundredths = (magnitude - whole) * MINUTE_SCALE;
1559
1785
  const minutes = Math.round(hundredths);
1560
- 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)}`);
1786
+ if (Math.abs(hundredths - minutes) > MINUTE_TOLERANCE || minutes >= MINUTES_PER_DEGREE) throw new NativeParseError(FORMAT$5, line, `expected a DDD.MM coordinate with whole minutes below 60, found ${String(value)}`);
1561
1787
  return Math.sign(value) * (whole + minutes / MINUTES_PER_DEGREE);
1562
1788
  }
1563
1789
  /**
@@ -1569,7 +1795,7 @@ function degrees(value, line) {
1569
1795
  * @throws NativeParseError when the token is not a decimal number.
1570
1796
  */
1571
1797
  function numberOf$1(token, line, what) {
1572
- if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
1798
+ if (token === void 0 || !NUMBER$2.test(token)) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(token)}`);
1573
1799
  return Number(token);
1574
1800
  }
1575
1801
  /**
@@ -1581,7 +1807,7 @@ function numberOf$1(token, line, what) {
1581
1807
  * @throws NativeParseError when the token is not a whole number.
1582
1808
  */
1583
1809
  function wholeOf(token, line, what) {
1584
- if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$4, line, `expected ${what}, found ${JSON.stringify(token)}`);
1810
+ if (token === void 0 || !WHOLE$1.test(token)) throw new NativeParseError(FORMAT$5, line, `expected ${what}, found ${JSON.stringify(token)}`);
1585
1811
  return Number(token);
1586
1812
  }
1587
1813
  /**
@@ -1609,7 +1835,7 @@ function collect(text) {
1609
1835
  continue;
1610
1836
  }
1611
1837
  if (keyed !== null && key !== void 0) {
1612
- if (collected.header.has(key)) throw new NativeParseError(FORMAT$4, line, `${key} appears twice`);
1838
+ if (collected.header.has(key)) throw new NativeParseError(FORMAT$5, line, `${key} appears twice`);
1613
1839
  collected.header.set(key, (keyed[2] ?? "").trim());
1614
1840
  section = void 0;
1615
1841
  continue;
@@ -1621,9 +1847,9 @@ function collect(text) {
1621
1847
  const tokens = trimmed.split(/\s+/);
1622
1848
  if (section === "coords") {
1623
1849
  const [id, x, y, ...extra] = tokens;
1624
- if (extra.length > 0) throw new NativeParseError(FORMAT$4, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
1850
+ if (extra.length > 0) throw new NativeParseError(FORMAT$5, line, `expected "id x y", found ${JSON.stringify(trimmed)}`);
1625
1851
  const node = wholeOf(id, line, "a node id");
1626
- if (collected.coords.has(node)) throw new NativeParseError(FORMAT$4, line, `node ${String(node)} has two coordinate rows`);
1852
+ if (collected.coords.has(node)) throw new NativeParseError(FORMAT$5, line, `node ${String(node)} has two coordinate rows`);
1627
1853
  collected.coords.set(node, {
1628
1854
  x: numberOf$1(x, line, "x"),
1629
1855
  y: numberOf$1(y, line, "y"),
@@ -1631,9 +1857,9 @@ function collect(text) {
1631
1857
  });
1632
1858
  } else if (section === "prizes") {
1633
1859
  const [id, prize, probability, ...extra] = tokens;
1634
- if (extra.length > 0) throw new NativeParseError(FORMAT$4, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
1860
+ if (extra.length > 0) throw new NativeParseError(FORMAT$5, line, `expected "id prize probability", found ${JSON.stringify(trimmed)}`);
1635
1861
  const node = wholeOf(id, line, "a node id");
1636
- if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$4, line, `node ${String(node)} has two prize rows`);
1862
+ if (collected.prizes.has(node)) throw new NativeParseError(FORMAT$5, line, `node ${String(node)} has two prize rows`);
1637
1863
  collected.prizes.set(node, {
1638
1864
  prize: numberOf$1(prize, line, "the prize"),
1639
1865
  probability: numberOf$1(probability, line, "the probability")
@@ -1641,13 +1867,13 @@ function collect(text) {
1641
1867
  } else if (section === "sets") {
1642
1868
  const [id, prize, ...members] = tokens;
1643
1869
  const setId = wholeOf(id, line, "a set id");
1644
- if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$4, line, `set ${String(setId)} appears twice`);
1870
+ if (collected.sets.some((set) => set.id === setId)) throw new NativeParseError(FORMAT$5, line, `set ${String(setId)} appears twice`);
1645
1871
  collected.sets.push({
1646
1872
  id: setId,
1647
1873
  prize: numberOf$1(prize, line, "the set prize"),
1648
1874
  members: members.map((member) => wholeOf(member, line, "a node id in a set"))
1649
1875
  });
1650
- } else if (section === void 0) throw new NativeParseError(FORMAT$4, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
1876
+ } else if (section === void 0) throw new NativeParseError(FORMAT$5, line, `expected a KEY : value line or a section, found ${JSON.stringify(trimmed)}`);
1651
1877
  }
1652
1878
  return collected;
1653
1879
  }
@@ -1660,7 +1886,7 @@ function collect(text) {
1660
1886
  */
1661
1887
  function required(header, key) {
1662
1888
  const value = header.get(key);
1663
- if (value === void 0) throw new NativeParseError(FORMAT$4, 1, `expected a ${key} line`);
1889
+ if (value === void 0) throw new NativeParseError(FORMAT$5, 1, `expected a ${key} line`);
1664
1890
  return value;
1665
1891
  }
1666
1892
  /**
@@ -1672,23 +1898,23 @@ function required(header, key) {
1672
1898
  function parseTsplib(text) {
1673
1899
  const { header, coords, prizes, sets } = collect(text);
1674
1900
  const type = required(header, "TYPE");
1675
- if (!TYPES.has(type)) throw new NativeParseError(FORMAT$4, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
1901
+ if (!TYPES.has(type)) throw new NativeParseError(FORMAT$5, 1, `expected TYPE POP or COP, found ${JSON.stringify(type)}`);
1676
1902
  const weightType = required(header, "EDGE_WEIGHT_TYPE");
1677
1903
  const geographic = weightType === "GEO";
1678
- 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)}`);
1904
+ if (!geographic && !PLANAR.has(weightType) && weightType !== "EXPLICIT") throw new NativeParseError(FORMAT$5, 1, `expected EDGE_WEIGHT_TYPE EUC_2D, CEIL_2D, ATT, GEO or EXPLICIT, found ${JSON.stringify(weightType)}`);
1679
1905
  const dimension = wholeOf(required(header, "DIMENSION"), 1, "DIMENSION");
1680
1906
  const budget = numberOf$1(required(header, "TMAX"), 1, "TMAX");
1681
- 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)}`);
1907
+ if (weightType === "EXPLICIT" ? coords.size > 0 : coords.size !== dimension) throw new NativeParseError(FORMAT$5, 1, weightType === "EXPLICIT" ? "an EXPLICIT file has no NODE_COORD_SECTION" : `expected ${String(dimension)} coordinate rows, found ${String(coords.size)}`);
1682
1908
  const inRange = (id) => id >= 1 && id <= dimension;
1683
1909
  for (const id of [
1684
1910
  ...coords.keys(),
1685
1911
  ...prizes.keys(),
1686
1912
  ...sets.flatMap((set) => set.members)
1687
- ]) if (!inRange(id)) throw new NativeParseError(FORMAT$4, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
1688
- if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$4, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
1913
+ ]) if (!inRange(id)) throw new NativeParseError(FORMAT$5, 1, `node ${String(id)} is outside 1 to ${String(dimension)}`);
1914
+ if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$5, 1, `expected ${String(dimension)} prize rows, found ${String(prizes.size)}`);
1689
1915
  if (type === "COP") {
1690
1916
  const declared = wholeOf(required(header, "SETS"), 1, "SETS");
1691
- if (sets.length !== declared) throw new NativeParseError(FORMAT$4, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
1917
+ if (sets.length !== declared) throw new NativeParseError(FORMAT$5, 1, `expected ${String(declared)} sets, found ${String(sets.length)}`);
1692
1918
  }
1693
1919
  const clustersOf = (node) => {
1694
1920
  const ids = sets.filter((set) => set.members.includes(node)).map((set) => String(set.id));
@@ -1726,7 +1952,7 @@ function parseTsplib(text) {
1726
1952
  }
1727
1953
  //#endregion
1728
1954
  //#region src/parsers/ucinet-dl.ts
1729
- const FORMAT$3 = "ucinet-dl";
1955
+ const FORMAT$4 = "ucinet-dl";
1730
1956
  /** 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. */
1731
1957
  const LABEL_SECTIONS = /* @__PURE__ */ new Map([
1732
1958
  ["LABELS", "both"],
@@ -1755,7 +1981,7 @@ function labelsOf(text) {
1755
1981
  * @returns The dimension.
1756
1982
  */
1757
1983
  function dimension(value, key, line) {
1758
- if (!/^[1-9]\d*$/.test(value)) throw new NativeParseError(FORMAT$3, line, `${key} must be a positive integer, found ${JSON.stringify(value)}`);
1984
+ if (!/^[1-9]\d*$/.test(value)) throw new NativeParseError(FORMAT$4, line, `${key} must be a positive integer, found ${JSON.stringify(value)}`);
1759
1985
  return Number(value);
1760
1986
  }
1761
1987
  /** 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. */
@@ -1787,7 +2013,7 @@ function readParameters(text, line) {
1787
2013
  tokens.shift();
1788
2014
  continue;
1789
2015
  }
1790
- throw new NativeParseError(FORMAT$3, line, `unknown header token ${JSON.stringify(token)}`);
2016
+ throw new NativeParseError(FORMAT$4, line, `unknown header token ${JSON.stringify(token)}`);
1791
2017
  }
1792
2018
  const key = upper.slice(0, equals);
1793
2019
  const value = token.slice(equals + 1);
@@ -1802,7 +2028,7 @@ function readParameters(text, line) {
1802
2028
  kind: "format",
1803
2029
  value: value.toUpperCase()
1804
2030
  });
1805
- else throw new NativeParseError(FORMAT$3, line, `unknown header parameter ${key}`);
2031
+ else throw new NativeParseError(FORMAT$4, line, `unknown header parameter ${key}`);
1806
2032
  }
1807
2033
  return parameters;
1808
2034
  }
@@ -1815,10 +2041,10 @@ function readParameters(text, line) {
1815
2041
  */
1816
2042
  function checked(labels, count, what) {
1817
2043
  if (labels === void 0) return;
1818
- if (labels.values.length !== count) throw new NativeParseError(FORMAT$3, labels.line, `expected ${String(count)} ${what} labels, found ${String(labels.values.length)}`);
2044
+ if (labels.values.length !== count) throw new NativeParseError(FORMAT$4, labels.line, `expected ${String(count)} ${what} labels, found ${String(labels.values.length)}`);
1819
2045
  const seen = /* @__PURE__ */ new Set();
1820
2046
  for (const label of labels.values) {
1821
- if (seen.has(label)) throw new NativeParseError(FORMAT$3, labels.line, `${what} label ${JSON.stringify(label)} appears twice`);
2047
+ if (seen.has(label)) throw new NativeParseError(FORMAT$4, labels.line, `${what} label ${JSON.stringify(label)} appears twice`);
1822
2048
  seen.add(label);
1823
2049
  }
1824
2050
  return labels.values;
@@ -1837,7 +2063,7 @@ function parseUcinetDl(text, options) {
1837
2063
  const header = { labels: {} };
1838
2064
  const setParameters = (parameterText, line) => {
1839
2065
  for (const parameter of readParameters(parameterText, line)) if (parameter.kind === "dimension") {
1840
- if (header[parameter.field] !== void 0) throw new NativeParseError(FORMAT$3, line, `${parameter.key} is declared twice`);
2066
+ if (header[parameter.field] !== void 0) throw new NativeParseError(FORMAT$4, line, `${parameter.key} is declared twice`);
1841
2067
  header[parameter.field] = parameter.value;
1842
2068
  } else if (parameter.kind === "format") header.format = parameter.value;
1843
2069
  else header.diagonal = parameter.value;
@@ -1859,7 +2085,7 @@ function parseUcinetDl(text, options) {
1859
2085
  if (content === "") continue;
1860
2086
  if (!started) {
1861
2087
  const match = /^DL(?:\s+(.*))?$/i.exec(content);
1862
- if (match === null) throw new NativeParseError(FORMAT$3, lineNumber, "a DL file starts with the keyword DL");
2088
+ if (match === null) throw new NativeParseError(FORMAT$4, lineNumber, "a DL file starts with the keyword DL");
1863
2089
  started = true;
1864
2090
  if (match[1] !== void 0) setParameters(match[1], lineNumber);
1865
2091
  continue;
@@ -1877,7 +2103,7 @@ function parseUcinetDl(text, options) {
1877
2103
  }
1878
2104
  const target = keyword === void 0 ? void 0 : LABEL_SECTIONS.get(keyword);
1879
2105
  if (target !== void 0) {
1880
- if (header.labels[target] !== void 0) throw new NativeParseError(FORMAT$3, lineNumber, `${String(keyword)} is given twice`);
2106
+ if (header.labels[target] !== void 0) throw new NativeParseError(FORMAT$4, lineNumber, `${String(keyword)} is given twice`);
1881
2107
  section = target;
1882
2108
  header.labels[target] = {
1883
2109
  values: labelsOf(rest),
@@ -1885,31 +2111,31 @@ function parseUcinetDl(text, options) {
1885
2111
  };
1886
2112
  continue;
1887
2113
  }
1888
- 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");
2114
+ if (/^LABELS\s+EMBEDDED\b/i.test(content)) throw new NativeParseError(FORMAT$4, lineNumber, "labels embedded in the data are not read in this version");
1889
2115
  if (section === void 0) setParameters(content, lineNumber);
1890
2116
  else header.labels[section]?.values.push(...labelsOf(content));
1891
2117
  }
1892
- if (!started) throw new NativeParseError(FORMAT$3, 1, "the file is empty");
1893
- if (dataLine === void 0) throw new NativeParseError(FORMAT$3, all.length, "the file has no DATA: section");
2118
+ if (!started) throw new NativeParseError(FORMAT$4, 1, "the file is empty");
2119
+ if (dataLine === void 0) throw new NativeParseError(FORMAT$4, all.length, "the file has no DATA: section");
1894
2120
  const format = header.format ?? "FULLMATRIX";
1895
- 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`);
1896
- if (header.diagonal === "ABSENT") throw new NativeParseError(FORMAT$3, dataLine, "a full matrix with its diagonal absent is not read in this version");
2121
+ if (!FULL_MATRIX.has(format)) throw new NativeParseError(FORMAT$4, dataLine, `format ${format} is not read in this version; only the full matrix layout is`);
2122
+ if (header.diagonal === "ABSENT") throw new NativeParseError(FORMAT$4, dataLine, "a full matrix with its diagonal absent is not read in this version");
1897
2123
  const twoMode = header.nr !== void 0 || header.nc !== void 0;
1898
- 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");
2124
+ if (twoMode === (header.n !== void 0) || twoMode && (header.nr === void 0 || header.nc === void 0)) throw new NativeParseError(FORMAT$4, dataLine, "the header must give either N, or both NR and NC");
1899
2125
  const rows = header.n ?? header.nr ?? 0;
1900
2126
  const columns = header.n ?? header.nc ?? 0;
1901
2127
  const matrices = header.nm ?? 1;
1902
2128
  checked(header.labels.matrices, matrices, "matrix");
1903
- if (options.matrix > matrices) throw new NativeParseError(FORMAT$3, dataLine, `the recipe reads matrix ${String(options.matrix)} but the file holds ${String(matrices)}`);
2129
+ if (options.matrix > matrices) throw new NativeParseError(FORMAT$4, dataLine, `the recipe reads matrix ${String(options.matrix)} but the file holds ${String(matrices)}`);
1904
2130
  let rowIds;
1905
2131
  let columnIds;
1906
2132
  if (twoMode) {
1907
- 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:");
2133
+ if (header.labels.both !== void 0) throw new NativeParseError(FORMAT$4, header.labels.both.line, "a two-mode file names its rows and columns with ROW LABELS: and COLUMN LABELS:, not LABELS:");
1908
2134
  rowIds = checked(header.labels.rows, rows, "row") ?? Array.from({ length: rows }, (_, row) => `r${String(row + 1)}`);
1909
2135
  columnIds = checked(header.labels.columns, columns, "column") ?? Array.from({ length: columns }, (_, column) => `c${String(column + 1)}`);
1910
2136
  const rowSet = new Set(rowIds);
1911
2137
  const shared = columnIds.find((id) => rowSet.has(id));
1912
- if (shared !== void 0) throw new NativeParseError(FORMAT$3, dataLine, `the row and column label ${JSON.stringify(shared)} names two different entities`);
2138
+ if (shared !== void 0) throw new NativeParseError(FORMAT$4, dataLine, `the row and column label ${JSON.stringify(shared)} names two different entities`);
1913
2139
  } else {
1914
2140
  const given = [
1915
2141
  checked(header.labels.both, rows, "actor"),
@@ -1917,18 +2143,18 @@ function parseUcinetDl(text, options) {
1917
2143
  checked(header.labels.columns, columns, "column")
1918
2144
  ].filter((labels) => labels !== void 0);
1919
2145
  const [first] = given;
1920
- 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");
2146
+ if (first !== void 0 && given.some((labels) => labels.some((label, position) => label !== first[position]))) throw new NativeParseError(FORMAT$4, dataLine, "a one-mode file's row and column labels must be the same actors in the same order");
1921
2147
  rowIds = first ?? Array.from({ length: rows }, (_, row) => String(row + 1));
1922
2148
  columnIds = rowIds;
1923
2149
  }
1924
2150
  const perMatrix = rows * columns;
1925
- 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`);
1926
- 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`);
2151
+ if (cells.length !== perMatrix * matrices) throw new NativeParseError(FORMAT$4, cells.at(-1)?.line ?? dataLine, `expected ${String(matrices)} full ${String(rows)} by ${String(columns)} matrices (${String(perMatrix * matrices)} cells), found ${String(cells.length)} cells`);
2152
+ for (const cell of cells) if (!NUMBER$1.test(cell.text)) throw new NativeParseError(FORMAT$4, cell.line, `cell ${JSON.stringify(cell.text)} is not a number`);
1927
2153
  const chosen = cells.slice((options.matrix - 1) * perMatrix, options.matrix * perMatrix);
1928
2154
  const edges = [];
1929
2155
  for (const [row, source] of rowIds.entries()) for (const [column, target] of columnIds.entries()) {
1930
2156
  const cell = chosen[row * columns + column];
1931
- 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)}`);
2157
+ if (cell === void 0) throw new NativeParseError(FORMAT$4, dataLine, `matrix ${String(options.matrix)} has no cell at row ${String(row + 1)}, column ${String(column + 1)}`);
1932
2158
  if (Number(cell.text) !== 0) edges.push({
1933
2159
  source,
1934
2160
  target
@@ -1941,7 +2167,7 @@ function parseUcinetDl(text, options) {
1941
2167
  }
1942
2168
  //#endregion
1943
2169
  //#region src/parsers/verolog-2019.ts
1944
- const FORMAT$2 = "verolog-2019";
2170
+ const FORMAT$3 = "verolog-2019";
1945
2171
  /** The header lines, in the order the file has them, with the key of `meta` each fills and whether its value is text. */
1946
2172
  const HEADER$1 = [
1947
2173
  [
@@ -2042,7 +2268,7 @@ function rowsOf(text) {
2042
2268
  */
2043
2269
  function numberAt$1(row, index, what) {
2044
2270
  const field = row.fields[index];
2045
- if (field === void 0 || !NUMBER.test(field)) throw new NativeParseError(FORMAT$2, row.line, `expected ${what}, found ${JSON.stringify(field)}`);
2271
+ if (field === void 0 || !NUMBER.test(field)) throw new NativeParseError(FORMAT$3, row.line, `expected ${what}, found ${JSON.stringify(field)}`);
2046
2272
  return Number(field);
2047
2273
  }
2048
2274
  /**
@@ -2066,8 +2292,8 @@ function keyed(row) {
2066
2292
  */
2067
2293
  function checkRows(name, rows, length) {
2068
2294
  for (const [index, row] of rows.entries()) {
2069
- 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])}`);
2070
- 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)}`);
2295
+ if (row.fields[0] !== String(index + 1)) throw new NativeParseError(FORMAT$3, row.line, `expected ${name} row ${String(index + 1)} to start with that id, found ${JSON.stringify(row.fields[0])}`);
2296
+ if (length !== void 0 && row.fields.length !== length) throw new NativeParseError(FORMAT$3, row.line, `expected ${String(length)} fields in a ${name} row, found ${String(row.fields.length)}`);
2071
2297
  }
2072
2298
  }
2073
2299
  /**
@@ -2082,15 +2308,15 @@ function sectionsOf(rows) {
2082
2308
  for (const name of SECTIONS) {
2083
2309
  const start = rows[at];
2084
2310
  const announced = start === void 0 ? void 0 : keyed(start);
2085
- 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(" "))}`);
2311
+ if (start === void 0 || announced?.key !== name || announced.value.length !== 1 || !WHOLE.test(announced.value[0] ?? "")) throw new NativeParseError(FORMAT$3, start?.line ?? 1, `expected "${name} = <count>", found ${JSON.stringify(start?.fields.join(" "))}`);
2086
2312
  const count = Number(announced.value[0]);
2087
2313
  const body = rows.slice(at + 1, at + 1 + count);
2088
- if (body.length !== count) throw new NativeParseError(FORMAT$2, start.line, `${name} announces ${String(count)} rows, found ${String(body.length)}`);
2314
+ if (body.length !== count) throw new NativeParseError(FORMAT$3, start.line, `${name} announces ${String(count)} rows, found ${String(body.length)}`);
2089
2315
  found[name] = body;
2090
2316
  at += 1 + count;
2091
2317
  }
2092
2318
  const extra = rows[at];
2093
- 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(" "))}`);
2319
+ if (extra !== void 0) throw new NativeParseError(FORMAT$3, extra.line, `expected the end of the file after TECHNICIANS, found ${JSON.stringify(extra.fields.join(" "))}`);
2094
2320
  return {
2095
2321
  MACHINES: found.MACHINES ?? [],
2096
2322
  LOCATIONS: found.LOCATIONS ?? [],
@@ -2111,7 +2337,7 @@ function parseVerolog2019(text) {
2111
2337
  for (const [position, [key, name, kind]] of HEADER$1.entries()) {
2112
2338
  const row = rows[position];
2113
2339
  const header = row === void 0 ? void 0 : keyed(row);
2114
- 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(" "))}`);
2340
+ if (row === void 0 || header?.key !== key || header.value.length === 0) throw new NativeParseError(FORMAT$3, row?.line ?? 1, `expected "${key} = <value>", found ${JSON.stringify(row?.fields.join(" "))}`);
2115
2341
  meta[name] = kind === "text" ? header.value.join(" ") : numberAt$1({
2116
2342
  line: row.line,
2117
2343
  fields: header.value
@@ -2151,10 +2377,10 @@ function parseVerolog2019(text) {
2151
2377
  const toDay = numberAt$1(row, REQUEST_AT.toDay, "the last day");
2152
2378
  const machine = numberAt$1(row, REQUEST_AT.machine, "a machine id");
2153
2379
  const amount = numberAt$1(row, REQUEST_AT.amount, "the amount");
2154
- 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)}`);
2155
- 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)}`);
2156
- 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)}`);
2157
- if (amount < 1) throw new NativeParseError(FORMAT$2, row.line, `expected an amount of at least 1, found ${String(amount)}`);
2380
+ if (!Number.isInteger(location) || location < 1 || location > locations.length) throw new NativeParseError(FORMAT$3, row.line, `location ${String(location)} is outside 1 to ${String(locations.length)}`);
2381
+ if (!Number.isInteger(machine) || machine < 1 || machine > machines.length) throw new NativeParseError(FORMAT$3, row.line, `machine ${String(machine)} is outside 1 to ${String(machines.length)}`);
2382
+ if (fromDay < 1 || fromDay > toDay || toDay > days) throw new NativeParseError(FORMAT$3, row.line, `the days ${String(fromDay)} to ${String(toDay)} are not within 1 to ${String(days)}`);
2383
+ if (amount < 1) throw new NativeParseError(FORMAT$3, row.line, `expected an amount of at least 1, found ${String(amount)}`);
2158
2384
  return {
2159
2385
  id: row.fields[0] ?? "",
2160
2386
  ...position(location),
@@ -2175,6 +2401,59 @@ function parseVerolog2019(text) {
2175
2401
  };
2176
2402
  }
2177
2403
  //#endregion
2404
+ //#region src/parsers/user-item-counts.ts
2405
+ const FORMAT$2 = "user-item-counts";
2406
+ /** The text each delimiter option separates a line's fields with. */
2407
+ const SEPARATORS$1 = { tab: " " };
2408
+ /** The prefix an id takes from the set it is in, so that user 2 and artist 2, which the source numbers independently, are two nodes. */
2409
+ const USER_PREFIX$1 = "u:";
2410
+ const ITEM_PREFIXES$1 = { artist: "a:" };
2411
+ /** The node type of a user, and the edge type of each item type's counts: a count of plays for an artist. */
2412
+ const USER_TYPE$1 = "user";
2413
+ const EDGE_TYPES = { artist: "listened" };
2414
+ /** A count: digits only, so `Number` cannot read hex, an empty string or infinity. */
2415
+ const COUNT = /^\d+$/;
2416
+ /** The fields on a count's line: the user, the item and the count. */
2417
+ const FIELDS$1 = 3;
2418
+ /**
2419
+ * Parses a file of user-item counts, one per line as `user`, `item` and `count` separated by the recipe's delimiter, after `skipLines` header lines, into a bipartite graph. This is the layout of the HetRec 2011 Last.fm release's `user_artists.dat` (tab, one header line, the count being how many times the user listened to the artist).
2420
+ *
2421
+ * Nodes are the users and the items in order of first appearance, a user as `u:<id>` of type `user` and an item with its type's prefix (`a:<id>` for an artist) and type. Each line is one edge from its user to its item, in file order, with the item type's edge type (`listened` for an artist) and `weight` the count. Blank lines are skipped.
2422
+ * @param pieces - The file's text, in order.
2423
+ * @param options - The recipe's parser.
2424
+ * @returns The native graph.
2425
+ * @throws NativeParseError naming the line for a line without exactly three fields, an empty id or a count that is not a positive whole number, and when the file holds no counts.
2426
+ */
2427
+ function parseUserItemCounts(pieces, options) {
2428
+ const separator = SEPARATORS$1[options.delimiter];
2429
+ const graph = new BipartiteNodes();
2430
+ const users = graph.set(USER_PREFIX$1, USER_TYPE$1);
2431
+ const items = graph.set(ITEM_PREFIXES$1[options.itemType], options.itemType);
2432
+ const type = EDGE_TYPES[options.itemType];
2433
+ const edges = [];
2434
+ let lastLine = 0;
2435
+ forEachLine(pieces, (line, number) => {
2436
+ lastLine = number;
2437
+ if (number <= options.skipLines || line.trim() === "") return;
2438
+ const fields = line.split(separator);
2439
+ const [user, item, count] = fields;
2440
+ if (fields.length !== FIELDS$1 || user === void 0 || item === void 0 || count === void 0) throw new NativeParseError(FORMAT$2, number, `expected ${String(FIELDS$1)} fields (user, item, count), found ${String(fields.length)}`);
2441
+ if (user === "" || item === "") throw new NativeParseError(FORMAT$2, number, `the ${user === "" ? USER_TYPE$1 : options.itemType} id is empty`);
2442
+ if (!COUNT.test(count) || Number(count) === 0 || !Number.isSafeInteger(Number(count))) throw new NativeParseError(FORMAT$2, number, `the count ${JSON.stringify(count)} is not a positive whole number`);
2443
+ edges.push({
2444
+ source: users(user),
2445
+ target: items(item),
2446
+ type,
2447
+ weight: Number(count)
2448
+ });
2449
+ });
2450
+ if (edges.length === 0) throw new NativeParseError(FORMAT$2, lastLine, "the file holds no counts");
2451
+ return {
2452
+ nodes: graph.nodes(),
2453
+ edges
2454
+ };
2455
+ }
2456
+ //#endregion
2178
2457
  //#region src/parsers/user-item-ratings.ts
2179
2458
  const FORMAT$1 = "user-item-ratings";
2180
2459
  /** The text each delimiter option separates a line's fields with. */
@@ -2211,37 +2490,22 @@ const FIELDS = 4;
2211
2490
  */
2212
2491
  function parseUserItemRatings(pieces, options) {
2213
2492
  const separator = SEPARATORS[options.delimiter];
2214
- const itemPrefix = ITEM_PREFIXES[options.itemType];
2215
- const users = /* @__PURE__ */ new Map();
2216
- const items = /* @__PURE__ */ new Map();
2217
- const nodes = [];
2218
- const edges = [];
2219
- /** The ids of one node set seen so far, by source id, so each distinct id is held once however many edges name it. */
2493
+ const graph = new BipartiteNodes();
2220
2494
  const sets = {
2221
2495
  user: {
2222
- known: users,
2223
- prefix: USER_PREFIX,
2224
- type: USER_TYPE
2496
+ type: USER_TYPE,
2497
+ id: graph.set(USER_PREFIX, USER_TYPE)
2225
2498
  },
2226
2499
  item: {
2227
- known: items,
2228
- prefix: itemPrefix,
2229
- type: options.itemType
2500
+ type: options.itemType,
2501
+ id: graph.set(ITEM_PREFIXES[options.itemType], options.itemType)
2230
2502
  }
2231
2503
  };
2504
+ const edges = [];
2232
2505
  /** Returns the node id of a source id in a set, adding the node when the id has not appeared in it. */
2233
2506
  const idOf = (set, raw, number) => {
2234
2507
  if (raw === "") throw new NativeParseError(FORMAT$1, number, `the ${set.type} id is empty`);
2235
- let id = set.known.get(raw);
2236
- if (id === void 0) {
2237
- id = `${set.prefix}${raw}`;
2238
- set.known.set(raw, id);
2239
- nodes.push({
2240
- id,
2241
- type: set.type
2242
- });
2243
- }
2244
- return id;
2508
+ return set.id(raw);
2245
2509
  };
2246
2510
  let lastLine = 0;
2247
2511
  forEachLine(pieces, (line, number) => {
@@ -2262,7 +2526,7 @@ function parseUserItemRatings(pieces, options) {
2262
2526
  });
2263
2527
  if (edges.length === 0) throw new NativeParseError(FORMAT$1, lastLine, "the file holds no ratings");
2264
2528
  return {
2265
- nodes,
2529
+ nodes: graph.nodes(),
2266
2530
  edges
2267
2531
  };
2268
2532
  }
@@ -2627,6 +2891,12 @@ function parseNative(bytes, parser) {
2627
2891
  if (parser.format === "csv-table") return parseCsvTable(decodePieces(bytes, parser), parser);
2628
2892
  if (parser.format === "user-item-ratings") return parseUserItemRatings(decodePieces(bytes, parser), parser);
2629
2893
  if (parser.format === "konect") return parseKonect(decodePieces(bytes, parser), parser);
2894
+ if (parser.format === "user-item-counts") return parseUserItemCounts(decodePieces(bytes, parser), parser);
2895
+ if (parser.format === "kg-triples") return parseKgTriples([{
2896
+ split: void 0,
2897
+ pieces: decodePieces(bytes, parser)
2898
+ }], parser);
2899
+ if (parser.format === "hgb") throw new NativeParseError(parser.format, 1, "an HGB dataset is two files, nodes and links, which a recipe reads from a parts archive");
2630
2900
  const text = decode(bytes, parser);
2631
2901
  if (parser.format === "geojson") return parseGeojson(text, parser);
2632
2902
  if (parser.format === "edge-list") return parseEdgeList(text, parser);
@@ -2644,6 +2914,29 @@ function parseNative(bytes, parser) {
2644
2914
  if (parser.format === "hhcrsp") return parseHhcrsp(text);
2645
2915
  return parsePajek(text);
2646
2916
  }
2917
+ /**
2918
+ * Reads the named parts of a parts archive into the one native graph of the recipe's parser: the split files of a triples download, each edge carrying its part's name as its split, or an HGB dataset's `nodes` and `links` files.
2919
+ * @param parts - The parts, in the order the recipe names them.
2920
+ * @param parser - The recipe's parser, one of the formats that read parts.
2921
+ * @returns The native graph.
2922
+ * @throws NativeParseError when a part is not valid in the stated encoding, its text is not in the stated format, the parser reads one file, or an HGB recipe lacks its `nodes` or `links` part.
2923
+ */
2924
+ function parseNativeParts(parts, parser) {
2925
+ if (parser.format === "kg-triples") return parseKgTriples(parts.map((part) => ({
2926
+ split: part.name,
2927
+ pieces: decodePieces(part.bytes, parser)
2928
+ })), parser);
2929
+ if (parser.format === "hgb") {
2930
+ const nodes = parts.find((part) => part.name === "nodes");
2931
+ const links = parts.find((part) => part.name === "links");
2932
+ if (nodes === void 0 || links === void 0) throw new NativeParseError(parser.format, 1, "an HGB recipe reads the parts nodes and links");
2933
+ return parseHgb({
2934
+ nodes: decodePieces(nodes.bytes, parser),
2935
+ links: decodePieces(links.bytes, parser)
2936
+ }, parser);
2937
+ }
2938
+ throw new NativeParseError(parser.format, 1, `the ${parser.format} parser reads one file, not named parts`);
2939
+ }
2647
2940
  //#endregion
2648
2941
  //#region src/node/bzip2.ts
2649
2942
  /** The four bytes every bzip2 stream starts with, as ASCII: `BZh` and the block size digit, `1` to `9`. */
@@ -3726,30 +4019,54 @@ function unpackMembers(bytes, archive) {
3726
4019
  * @throws ArchiveError when the bytes are not a well-formed archive of the stated kind or the selector matches no member.
3727
4020
  */
3728
4021
  function selectMembers(bytes, archive) {
3729
- if (archive.kind === "zip") {
4022
+ return readMembers(bytes, archive.kind, (paths) => pickAll(paths, archive.members).map((path) => ({ path })));
4023
+ }
4024
+ /**
4025
+ * Unpacks downloaded bytes into the files a parts archive names, each the one member its selector matches.
4026
+ * @param bytes - The downloaded bytes.
4027
+ * @param archive - The recipe's archive layout, which names its parts.
4028
+ * @returns Each part, in the order the recipe names them.
4029
+ * @throws ArchiveError when the bytes are not a well-formed archive of the stated kind or a part's selector does not match exactly one member.
4030
+ */
4031
+ function unpackParts(bytes, archive) {
4032
+ return readMembers(bytes, archive.kind, (paths) => archive.parts.map((part) => ({
4033
+ name: part.name,
4034
+ path: pick(paths, part.member)
4035
+ })));
4036
+ }
4037
+ /**
4038
+ * Reads the members of a file archive that a choice makes from its member paths, listing the archive once and decompressing it once however many members are chosen.
4039
+ * @param bytes - The downloaded bytes.
4040
+ * @param kind - The archive kind.
4041
+ * @param choose - Given every member path, the members to read, in the order wanted, each naming its path.
4042
+ * @returns Each choice with the bytes of the member it names, in the order chosen.
4043
+ * @throws ArchiveError when the bytes are not a well-formed archive of the stated kind or a chosen member cannot be read; whatever `choose` throws.
4044
+ */
4045
+ function readMembers(bytes, kind, choose) {
4046
+ if (kind === "zip") {
3730
4047
  const names = [];
3731
4048
  unzip(bytes, (file) => {
3732
4049
  names.push(file.name);
3733
4050
  return false;
3734
4051
  });
3735
- const paths = pickAll(names, archive.members);
3736
- const chosen = new Set(paths);
4052
+ const choices = choose(names);
4053
+ const chosen = new Set(choices.map((choice) => choice.path));
3737
4054
  const files = unzip(bytes, (candidate) => chosen.has(candidate.name));
3738
- return paths.map((path) => {
3739
- const file = getOwn(files, path);
3740
- if (file === void 0) throw new ArchiveError(`zip member ${path} could not be read`);
4055
+ return choices.map((choice) => {
4056
+ const file = getOwn(files, choice.path);
4057
+ if (file === void 0) throw new ArchiveError(`zip member ${choice.path} could not be read`);
3741
4058
  return {
3742
- path,
4059
+ ...choice,
3743
4060
  bytes: file
3744
4061
  };
3745
4062
  });
3746
4063
  }
3747
- const members = readTar(untarred(bytes, archive.kind));
3748
- return pickAll(members.map((member) => member.path), archive.members).map((path) => {
3749
- const found = members.find((member) => member.path === path);
3750
- if (found === void 0) throw new ArchiveError(`tar member ${path} could not be read`);
4064
+ const members = readTar(untarred(bytes, kind));
4065
+ return choose(members.map((member) => member.path)).map((choice) => {
4066
+ const found = members.find((member) => member.path === choice.path);
4067
+ if (found === void 0) throw new ArchiveError(`tar member ${choice.path} could not be read`);
3751
4068
  return {
3752
- path,
4069
+ ...choice,
3753
4070
  bytes: found.data
3754
4071
  };
3755
4072
  });
@@ -4289,6 +4606,20 @@ async function runToNative(dataset, options) {
4289
4606
  provenance: provenanceOf({ members: files.map((file) => file.path) })
4290
4607
  };
4291
4608
  }
4609
+ if ("parts" in archive) {
4610
+ const parts = await step(entry.id, "extract", () => unpackParts(bytes, archive));
4611
+ return {
4612
+ collection: false,
4613
+ documents: [{
4614
+ id: void 0,
4615
+ native: {
4616
+ ...await step(entry.id, "parse", () => parseNativeParts(parts, recipe.parser)),
4617
+ meta: metaOf(entry)
4618
+ }
4619
+ }],
4620
+ provenance: provenanceOf({ members: parts.map((part) => part.path) })
4621
+ };
4622
+ }
4292
4623
  const unpacked = await step(entry.id, "extract", () => unpack(bytes, archive));
4293
4624
  const parsed = await step(entry.id, "parse", () => parseNative(unpacked.bytes, recipe.parser));
4294
4625
  return {