keepset 0.36.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{bundled-BrTVG8j1.js → bundled-CRr7GPD3.js} +921 -109
- package/dist/{categories-D9r42M9E.d.ts → categories-Djnk3zwg.d.ts} +349 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node/fetch-dataset.d.ts +2 -2
- package/dist/node/fetch-dataset.js +487 -156
- package/dist-cli/keepset.mjs +2 -2
- package/package.json +1 -1
- package/schemas/entry.schema.json +1 -1
- package/schemas/manifest.schema.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-
|
|
1
|
+
import { $ as ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-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
|
-
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
161
|
-
if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
165
|
-
for (const coordinate of coordinates) if (!NUMBER$9.test(coordinate.text)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
416
|
-
if (edges.length === 0) throw new NativeParseError(FORMAT$
|
|
417
|
-
if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$
|
|
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$
|
|
436
|
-
if (!WEIGHTED_TYPES.has(weights) && !UNWEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$
|
|
437
|
-
if (options.weighted !== WEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
464
|
-
if (third !== void 0 && options.weighted && !DECIMAL$3.test(third)) throw new NativeParseError(FORMAT$
|
|
465
|
-
if (time !== void 0 && !DECIMAL$3.test(time)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1123
|
-
if (required.length === 0 || required.length > MOST_SERVICES) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1291
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1363
|
-
if (!/^\d+$/.test(id)) throw new NativeParseError(FORMAT$
|
|
1364
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
1377
|
-
if (nodes.length === 0) throw new NativeParseError(FORMAT$
|
|
1378
|
-
if (meta.vehicles === void 0 || meta.capacity === void 0) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
1424
|
-
if (rows.length !== points) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1505
|
-
if (seen.has(id)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1688
|
-
if (type === "POP" && prizes.size !== dimension) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1893
|
-
if (dataLine === void 0) throw new NativeParseError(FORMAT$
|
|
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$
|
|
1896
|
-
if (header.diagonal === "ABSENT") throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1926
|
-
for (const cell of cells) if (!NUMBER$1.test(cell.text)) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2070
|
-
if (length !== void 0 && row.fields.length !== length) throw new NativeParseError(FORMAT$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2155
|
-
if (!Number.isInteger(machine) || machine < 1 || machine > machines.length) throw new NativeParseError(FORMAT$
|
|
2156
|
-
if (fromDay < 1 || fromDay > toDay || toDay > days) throw new NativeParseError(FORMAT$
|
|
2157
|
-
if (amount < 1) throw new NativeParseError(FORMAT$
|
|
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
|
|
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
|
-
|
|
2223
|
-
|
|
2224
|
-
type: USER_TYPE
|
|
2496
|
+
type: USER_TYPE,
|
|
2497
|
+
id: graph.set(USER_PREFIX, USER_TYPE)
|
|
2225
2498
|
},
|
|
2226
2499
|
item: {
|
|
2227
|
-
|
|
2228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3736
|
-
const chosen = new Set(
|
|
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
|
|
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
|
-
|
|
4059
|
+
...choice,
|
|
3743
4060
|
bytes: file
|
|
3744
4061
|
};
|
|
3745
4062
|
});
|
|
3746
4063
|
}
|
|
3747
|
-
const members = readTar(untarred(bytes,
|
|
3748
|
-
return
|
|
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
|
-
|
|
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 {
|