@graphty/graph-io 0.3.4 → 0.3.5
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/dist/chunks/{escape-DXbOoLKl.js → escape-BjmIaFFo.js} +2 -2
- package/dist/chunks/{escape-DXbOoLKl.js.map → escape-BjmIaFFo.js.map} +1 -1
- package/dist/chunks/{importer-B9qF4e32.js → importer-BddBW3sf.js} +2 -2
- package/dist/chunks/{importer-B9qF4e32.js.map → importer-BddBW3sf.js.map} +1 -1
- package/dist/chunks/{importer-xoHpRG5x.js → importer-BtHFICnf.js} +2 -2
- package/dist/chunks/{importer-xoHpRG5x.js.map → importer-BtHFICnf.js.map} +1 -1
- package/dist/chunks/{importer-C2caVvl6.js → importer-DfI8POXb.js} +272 -63
- package/dist/chunks/importer-DfI8POXb.js.map +1 -0
- package/dist/csv.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +1 -1
- package/dist/gml.js +1 -1
- package/dist/graph-io.js +9 -8
- package/dist/graphml.js +1 -1
- package/dist/neo4j.js +51 -16
- package/dist/neo4j.js.map +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/escape.d.ts +4 -3
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +5 -4
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts +3 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +34 -6
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts +16 -3
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +49 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/neo4j/index.d.ts +3 -3
- package/dist/src/formats/neo4j/index.d.ts.map +1 -1
- package/dist/src/formats/neo4j/index.js +3 -3
- package/dist/src/formats/neo4j/index.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +3 -2
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +14 -8
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +233 -61
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts +39 -8
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +81 -11
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/importer-C2caVvl6.js.map +0 -1
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* mixed-direction policy; the third column of a line is the weight unless `weightFrom` says
|
|
12
12
|
* otherwise; `*Arcslist` / `*Edgeslist` adjacency lists and `*Matrix` rows are read too; time
|
|
13
13
|
* interval tokens `[1-5,7-*]` become the spells role. A second network in one file is refused
|
|
14
|
-
* with an issue
|
|
14
|
+
* with an issue. A project file's `*Partition` and `*Vector` objects become the node columns
|
|
15
|
+
* `partition` (i32) and `vector` (f64), wherever they stand in the file; other project-file
|
|
16
|
+
* sections (`*Events`, `*Permutation`, ...) are skipped with a warning.
|
|
15
17
|
*/
|
|
16
18
|
import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
|
|
17
|
-
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
|
|
19
|
+
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE, uniqueColumnName } from "../../common/attributes.js";
|
|
18
20
|
import { DUPLICATE_NODE_CODE, ENCODING_FALLBACK_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, UNKNOWN_ENCODING_CODE, } from "../../common/codes.js";
|
|
19
21
|
import { DirectionResolver } from "../../common/direction.js";
|
|
20
22
|
import { coerceIdText, ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
@@ -23,7 +25,7 @@ import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTI
|
|
|
23
25
|
import { ImportReportBuilder } from "../../common/report.js";
|
|
24
26
|
import { isNumericText, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
|
|
25
27
|
import { isWeightField, parseWeightText } from "../../common/weights.js";
|
|
26
|
-
import { isCommentOrBlank, isIntervalToken, isSectionLine, isVertexNumber, LABEL_COLUMN, ORIGINAL_ID_KEY, parseIntervals, parseSectionHeader, POSITION_COLUMN, RELATION_COLUMN, SHAPE_COLUMN,
|
|
28
|
+
import { decodeCharacterReferences, isCommentOrBlank, isIntervalToken, isSectionLine, isShapeKeyword, isVertexNumber, LABEL_COLUMN, ORIGINAL_ID_KEY, parseIntervals, parseSectionHeader, PARTITION_COLUMN, POSITION_COLUMN, RELATION_COLUMN, SHAPE_COLUMN, SPELLS_COLUMN, tokenize, VALUE_COLUMN, VALUE_FIELD, VECTOR_COLUMN, } from "./syntax.js";
|
|
27
29
|
/** Issue codes of the Pajek importer. */
|
|
28
30
|
export const PAJEK_ISSUE = Object.freeze({
|
|
29
31
|
/** The input holds invalid UTF-8 (fatal). */
|
|
@@ -36,7 +38,7 @@ export const PAJEK_ISSUE = Object.freeze({
|
|
|
36
38
|
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
37
39
|
/** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
|
|
38
40
|
NO_VERTICES: "E_PAJEK_NO_VERTICES",
|
|
39
|
-
/** Fatal: `*Vertices` without a vertex count,
|
|
41
|
+
/** Fatal: `*Vertices` without a vertex count, one the sink cannot hold, or a first-mode count outside 0..N. */
|
|
40
42
|
VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
|
|
41
43
|
/** A project file holds several networks; import() reads the first, importAll() reads every one. */
|
|
42
44
|
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
@@ -50,20 +52,24 @@ export const PAJEK_ISSUE = Object.freeze({
|
|
|
50
52
|
VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
|
|
51
53
|
/** A vertex number outside the declared range. */
|
|
52
54
|
VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
|
|
53
|
-
/** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
|
|
54
|
-
VERTEX_COUNT: "
|
|
55
|
+
/** Fewer vertex lines than `*Vertices` declares, which the Pajek manual allows (vertices without a line have no label). */
|
|
56
|
+
VERTEX_COUNT: "W_PAJEK_VERTEX_COUNT",
|
|
55
57
|
/** A second line for the same vertex; the later values overwrite. */
|
|
56
58
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
57
|
-
/** A line (arc, edge, list or
|
|
59
|
+
/** A line (arc, edge, list, matrix row, partition or vector value) the grammar does not accept. */
|
|
58
60
|
LINE: "E_PAJEK_LINE",
|
|
59
61
|
/** A line endpoint outside the declared vertex range (the core's code, forwarded). */
|
|
60
62
|
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
61
63
|
/** A malformed time interval token. */
|
|
62
64
|
INTERVAL: "E_PAJEK_INTERVAL",
|
|
63
|
-
/** A `*Matrix` section with the wrong number of rows. */
|
|
65
|
+
/** A `*Matrix` section with the wrong number of rows (N, or N1 in a two-mode network). */
|
|
64
66
|
MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
+
/** `*Matrix` rows longer than the column count; the extra values are ignored. */
|
|
68
|
+
MATRIX_EXTRA: "W_PAJEK_MATRIX_EXTRA",
|
|
69
|
+
/** A `*Partition` or `*Vector` whose value count differs from the network's vertex count; values beyond it are dropped, vertices without one are unset. */
|
|
70
|
+
OBJECT_COUNT: "E_PAJEK_OBJECT_COUNT",
|
|
71
|
+
/** A project-file section (`*Events`, `*Permutation`, ...) the importer does not read; its lines are skipped. */
|
|
72
|
+
UNSUPPORTED_SECTION: "W_PAJEK_UNSUPPORTED_SECTION",
|
|
67
73
|
/** Tokens after a section header the grammar does not account for. */
|
|
68
74
|
HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
|
|
69
75
|
/** The file declares vertices but no line section. */
|
|
@@ -176,6 +182,18 @@ class LineError extends Error {
|
|
|
176
182
|
this.code = code;
|
|
177
183
|
}
|
|
178
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether a number is an integer the i32 partition column can hold.
|
|
187
|
+
* @param value - the number
|
|
188
|
+
* @returns true for an i32 integer
|
|
189
|
+
*/
|
|
190
|
+
function isI32(value) {
|
|
191
|
+
return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
|
|
192
|
+
}
|
|
193
|
+
/** A vertex number of an adjacency list: Pajek reads a negative one as its absolute value. */
|
|
194
|
+
const LIST_VERTEX = /^-?[0-9]+$/;
|
|
195
|
+
/** The relation prefix `k:` of a line in a line section. */
|
|
196
|
+
const RELATION_PREFIX = /^([0-9]+):$/;
|
|
179
197
|
/**
|
|
180
198
|
* One import call's state machine: the sections, the vertex range, the columns declared so far
|
|
181
199
|
* and the direction resolver.
|
|
@@ -204,6 +222,16 @@ class PajekParser {
|
|
|
204
222
|
this.nodesCreated = false;
|
|
205
223
|
this.verticesDone = false;
|
|
206
224
|
this.sawLineSection = false;
|
|
225
|
+
/** The direction of the first line section, for a network whose line sections are all empty. */
|
|
226
|
+
this.firstKind = "directed";
|
|
227
|
+
/** Set once the first line has fixed the sink's direction. */
|
|
228
|
+
this.directionSet = false;
|
|
229
|
+
/** The names of the numbered relations (`*Arcs :k "name"`), for the `k:` line prefix. */
|
|
230
|
+
this.relationNames = new Map();
|
|
231
|
+
/** The `*Partition` / `*Vector` objects read so far, written by finish(). */
|
|
232
|
+
this.objects = [];
|
|
233
|
+
/** Set when the previous line was a project-object header, whose `*Vertices N` line comes next. */
|
|
234
|
+
this.objectHeader = false;
|
|
207
235
|
/** Node index per vertex position (vertex number minus base). */
|
|
208
236
|
this.indexOfPos = new Uint32Array(0);
|
|
209
237
|
/**
|
|
@@ -251,9 +279,11 @@ class PajekParser {
|
|
|
251
279
|
if (isCommentOrBlank(text)) {
|
|
252
280
|
return false;
|
|
253
281
|
}
|
|
282
|
+
const afterObjectHeader = this.objectHeader;
|
|
283
|
+
this.objectHeader = false;
|
|
254
284
|
try {
|
|
255
285
|
if (isSectionLine(text)) {
|
|
256
|
-
this.header(text, line);
|
|
286
|
+
this.header(text, line, afterObjectHeader);
|
|
257
287
|
return this.ended;
|
|
258
288
|
}
|
|
259
289
|
switch (this.section) {
|
|
@@ -271,6 +301,9 @@ class PajekParser {
|
|
|
271
301
|
case "matrix":
|
|
272
302
|
this.matrixLine(text, line);
|
|
273
303
|
return false;
|
|
304
|
+
case "object":
|
|
305
|
+
this.objectLine(text);
|
|
306
|
+
return false;
|
|
274
307
|
case "skip":
|
|
275
308
|
return false;
|
|
276
309
|
default: {
|
|
@@ -302,24 +335,69 @@ class PajekParser {
|
|
|
302
335
|
this.report.warning("validation-error", PAJEK_ISSUE.NO_LINES, "the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section");
|
|
303
336
|
this.resolver.setHeader(this.options.defaultDirected);
|
|
304
337
|
}
|
|
338
|
+
else if (!this.directionSet) {
|
|
339
|
+
// every line section was empty: the first header still says which way the network goes
|
|
340
|
+
this.resolver.setHeader(this.firstKind === "directed");
|
|
341
|
+
}
|
|
342
|
+
const objects = this.writeObjects();
|
|
305
343
|
if (this.coercer.mergeCount > 0) {
|
|
306
344
|
this.report.warning("coercion", PAJEK_ISSUE.ID_MERGED, `${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`);
|
|
307
345
|
}
|
|
346
|
+
const pajek = {
|
|
347
|
+
...(this.firstMode === null ? {} : { firstMode: this.firstMode }),
|
|
348
|
+
...(objects.length === 0 ? {} : { objects }),
|
|
349
|
+
};
|
|
308
350
|
const meta = {
|
|
309
351
|
sourceFormat: "pajek",
|
|
310
352
|
...(this.networkName === null ? {} : { name: this.networkName }),
|
|
311
|
-
...(
|
|
353
|
+
...(Object.keys(pajek).length === 0 ? {} : { extra: { pajek } }),
|
|
312
354
|
};
|
|
313
355
|
this.sink.setMeta(meta);
|
|
314
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Write every `*Partition` / `*Vector` object into its node column: `partition` (i32) or
|
|
359
|
+
* `vector` (f64), a later object of the same kind in `partition#2`, `vector#2`, ... The value
|
|
360
|
+
* of vertex k is the object's k-th value.
|
|
361
|
+
* @returns the objects' Pajek names and the columns they went to, for meta.extra.pajek.objects
|
|
362
|
+
*/
|
|
363
|
+
writeObjects() {
|
|
364
|
+
const used = new Set();
|
|
365
|
+
const written = [];
|
|
366
|
+
for (const object of this.objects) {
|
|
367
|
+
const where = { line: object.line };
|
|
368
|
+
const partition = object.kind === "partition";
|
|
369
|
+
const name = uniqueColumnName(partition ? PARTITION_COLUMN : VECTOR_COLUMN, null, (n) => used.has(n));
|
|
370
|
+
used.add(name);
|
|
371
|
+
const { handle, decl } = declareResolved(this.sink, "node", {
|
|
372
|
+
name,
|
|
373
|
+
dtype: partition ? "i32" : "f64",
|
|
374
|
+
nullable: true,
|
|
375
|
+
origin: { format: "pajek", id: object.kind },
|
|
376
|
+
}, this.report, where);
|
|
377
|
+
const n = Math.min(object.values.length, this.vertexCount);
|
|
378
|
+
for (let pos = 0; pos < n; pos++) {
|
|
379
|
+
const value = object.values[pos];
|
|
380
|
+
if (value !== null) {
|
|
381
|
+
this.sink.setNodeValue(handle, this.indexOfPos[pos], value);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const declared = object.count ?? object.values.length;
|
|
385
|
+
if (declared !== this.vertexCount || object.values.length !== declared) {
|
|
386
|
+
this.report.error("validation-error", PAJEK_ISSUE.OBJECT_COUNT, `*${partition ? "Partition" : "Vector"} "${object.name}" declares ${declared} vertices and holds ${object.values.length} value(s); the network has ${this.vertexCount}`, where);
|
|
387
|
+
}
|
|
388
|
+
written.push({ kind: object.kind, name: object.name, column: decl.name });
|
|
389
|
+
}
|
|
390
|
+
return written;
|
|
391
|
+
}
|
|
315
392
|
// ============================================================ headers
|
|
316
393
|
/**
|
|
317
394
|
* A section header line.
|
|
318
395
|
* @param text - the line
|
|
319
396
|
* @param line - the line number
|
|
397
|
+
* @param afterObjectHeader - the previous line was a `*Partition`, `*Vector` or skipped section header
|
|
320
398
|
*/
|
|
321
|
-
header(text, line) {
|
|
322
|
-
const
|
|
399
|
+
header(text, line, afterObjectHeader) {
|
|
400
|
+
const previous = this.section;
|
|
323
401
|
this.endSection();
|
|
324
402
|
const h = parseSectionHeader(text);
|
|
325
403
|
if (h === null) {
|
|
@@ -339,9 +417,13 @@ class PajekParser {
|
|
|
339
417
|
this.section = "none";
|
|
340
418
|
return;
|
|
341
419
|
case "vertices":
|
|
342
|
-
if (
|
|
343
|
-
// the `*Vertices N` line of a
|
|
344
|
-
|
|
420
|
+
if (afterObjectHeader) {
|
|
421
|
+
// the `*Vertices N` line of a project object (*Partition, *Vector, a skipped
|
|
422
|
+
// section), wherever it stands: never the network's own
|
|
423
|
+
this.section = previous;
|
|
424
|
+
if (previous === "object") {
|
|
425
|
+
this.objects[this.objects.length - 1].count = h.count;
|
|
426
|
+
}
|
|
345
427
|
return;
|
|
346
428
|
}
|
|
347
429
|
if (this.vertexCount >= 0) {
|
|
@@ -363,9 +445,16 @@ class PajekParser {
|
|
|
363
445
|
this.lineSection(h, line, "matrix", "directed");
|
|
364
446
|
this.matrixRows = 0;
|
|
365
447
|
return;
|
|
448
|
+
case "partition":
|
|
449
|
+
case "vector":
|
|
450
|
+
this.objects.push({ kind: h.kind, name: h.name ?? "", line, count: null, values: [] });
|
|
451
|
+
this.section = "object";
|
|
452
|
+
this.objectHeader = true;
|
|
453
|
+
return;
|
|
366
454
|
case "unsupported":
|
|
367
455
|
this.section = "skip";
|
|
368
|
-
this.
|
|
456
|
+
this.objectHeader = true;
|
|
457
|
+
this.report.warning("unsupported", PAJEK_ISSUE.UNSUPPORTED_SECTION, `the *${h.keyword} section is not supported; its lines are skipped`, { line });
|
|
369
458
|
return;
|
|
370
459
|
default: {
|
|
371
460
|
const name = h.kind;
|
|
@@ -385,6 +474,9 @@ class PajekParser {
|
|
|
385
474
|
if (!Number.isSafeInteger(h.count)) {
|
|
386
475
|
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
|
|
387
476
|
}
|
|
477
|
+
if (h.secondCount !== null && (h.secondCount < 0 || h.secondCount > h.count)) {
|
|
478
|
+
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count} ${h.secondCount}: the first-mode count is not within 0..${h.count}`, { line });
|
|
479
|
+
}
|
|
388
480
|
try {
|
|
389
481
|
// the declared count must be reservable (the sink is the authority on what it can hold,
|
|
390
482
|
// E_TOO_LARGE beyond MAX_COUNT) before any vertex is materialised
|
|
@@ -412,8 +504,9 @@ class PajekParser {
|
|
|
412
504
|
}
|
|
413
505
|
}
|
|
414
506
|
/**
|
|
415
|
-
* A line section header: the vertex set is completed
|
|
416
|
-
* (rule 1 of design section 8.4)
|
|
507
|
+
* A line section header: the vertex set is completed and the kind switched. The sink's
|
|
508
|
+
* direction (rule 1 of design section 8.4) is set by the first LINE, not the first header, so
|
|
509
|
+
* an empty `*Arcs` before the `*Edges` of an undirected network does not make it directed.
|
|
417
510
|
* @param h - the header
|
|
418
511
|
* @param line - the line number
|
|
419
512
|
* @param section - the parser state for the section
|
|
@@ -426,18 +519,22 @@ class PajekParser {
|
|
|
426
519
|
this.finishVertices();
|
|
427
520
|
if (!this.sawLineSection) {
|
|
428
521
|
this.sawLineSection = true;
|
|
429
|
-
this.
|
|
522
|
+
this.firstKind = kind;
|
|
430
523
|
}
|
|
431
524
|
this.section = section;
|
|
432
525
|
this.kind = kind;
|
|
433
526
|
this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
|
|
527
|
+
if (h.relation !== null) {
|
|
528
|
+
this.relationNames.set(h.relation, this.relation ?? String(h.relation));
|
|
529
|
+
}
|
|
434
530
|
}
|
|
435
531
|
/**
|
|
436
532
|
* Checks that run when a section ends.
|
|
437
533
|
*/
|
|
438
534
|
endSection() {
|
|
439
|
-
|
|
440
|
-
|
|
535
|
+
const rows = this.firstMode ?? this.vertexCount;
|
|
536
|
+
if (this.section === "matrix" && this.matrixRows !== rows) {
|
|
537
|
+
this.report.error("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${rows}`);
|
|
441
538
|
}
|
|
442
539
|
this.section = "none";
|
|
443
540
|
}
|
|
@@ -529,7 +626,7 @@ class PajekParser {
|
|
|
529
626
|
// a partial list: fewer lines than declared, counting the lines skipped after an error
|
|
530
627
|
const lines = this.seenCount + this.report.counts.skippedNodes;
|
|
531
628
|
if (this.seenCount > 0 && lines < n) {
|
|
532
|
-
this.report.
|
|
629
|
+
this.report.warning("validation-error", PAJEK_ISSUE.VERTEX_COUNT, `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`);
|
|
533
630
|
}
|
|
534
631
|
}
|
|
535
632
|
/**
|
|
@@ -554,8 +651,9 @@ class PajekParser {
|
|
|
554
651
|
}
|
|
555
652
|
// parse everything before writing anything
|
|
556
653
|
let i = 1;
|
|
557
|
-
|
|
558
|
-
if (
|
|
654
|
+
let label = null;
|
|
655
|
+
if (tokens.length > 1 && !startsWithCoordinates(text, tokens)) {
|
|
656
|
+
label = decodeCharacterReferences(tokens[1]);
|
|
559
657
|
i = 2;
|
|
560
658
|
}
|
|
561
659
|
let x = 0;
|
|
@@ -578,8 +676,8 @@ class PajekParser {
|
|
|
578
676
|
throw new LineError("parse-error", PAJEK_ISSUE.VERTEX_LINE, `vertex ${k}: a single coordinate "${tokens[i]}"; coordinates are x y [z]`);
|
|
579
677
|
}
|
|
580
678
|
let shape = null;
|
|
581
|
-
if (i < tokens.length &&
|
|
582
|
-
shape = tokens[i];
|
|
679
|
+
if (i < tokens.length && isShapeKeyword(tokens[i])) {
|
|
680
|
+
shape = tokens[i].toLowerCase();
|
|
583
681
|
i++;
|
|
584
682
|
}
|
|
585
683
|
const extras = this.extras(tokens, i, "node");
|
|
@@ -732,7 +830,15 @@ class PajekParser {
|
|
|
732
830
|
* @param line - the line number
|
|
733
831
|
*/
|
|
734
832
|
edgeLine(text, line) {
|
|
735
|
-
|
|
833
|
+
let tokens = this.tokens(text, "edge");
|
|
834
|
+
// a `k:` prefix puts this one line in relation k
|
|
835
|
+
let { relation } = this;
|
|
836
|
+
const prefix = RELATION_PREFIX.exec(tokens[0]);
|
|
837
|
+
if (prefix !== null) {
|
|
838
|
+
const k = Number(prefix[1]);
|
|
839
|
+
relation = this.relationNames.get(k) ?? String(k);
|
|
840
|
+
tokens = tokens.slice(1);
|
|
841
|
+
}
|
|
736
842
|
if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
|
|
737
843
|
this.report.counts.skippedEdges++;
|
|
738
844
|
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `a line starts with two vertex numbers, not "${tokens.slice(0, 2).join(" ")}"`);
|
|
@@ -768,30 +874,33 @@ class PajekParser {
|
|
|
768
874
|
// inferred like every parameter, so a re-import of the exporter's `value` parameter agrees
|
|
769
875
|
this.writeParameter("edge", VALUE_COLUMN, e, valueText);
|
|
770
876
|
}
|
|
771
|
-
this.writeEdgeExtras(e, extras);
|
|
877
|
+
this.writeEdgeExtras(e, extras, relation);
|
|
772
878
|
}
|
|
773
879
|
/**
|
|
774
|
-
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2
|
|
880
|
+
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`; a negative vertex
|
|
881
|
+
* number is read as its absolute value, as Pajek does.
|
|
775
882
|
* @param text - the line
|
|
776
883
|
* @param line - the line number
|
|
777
884
|
*/
|
|
778
885
|
listLine(text, line) {
|
|
779
886
|
const tokens = this.tokens(text, "edge");
|
|
780
887
|
for (const token of tokens) {
|
|
781
|
-
if (!
|
|
888
|
+
if (!LIST_VERTEX.test(token)) {
|
|
782
889
|
this.report.counts.skippedEdges++;
|
|
783
890
|
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `an adjacency list holds vertex numbers only, not "${token}"`);
|
|
784
891
|
}
|
|
785
892
|
}
|
|
786
|
-
const u = this.endpoint(Number(tokens[0]));
|
|
893
|
+
const u = this.endpoint(Math.abs(Number(tokens[0])));
|
|
787
894
|
for (let i = 1; i < tokens.length; i++) {
|
|
788
|
-
const v = this.endpoint(Number(tokens[i]));
|
|
895
|
+
const v = this.endpoint(Math.abs(Number(tokens[i])));
|
|
789
896
|
const e = this.pushEdge(u, v, undefined, line);
|
|
790
897
|
this.writeRelation(e);
|
|
791
898
|
}
|
|
792
899
|
}
|
|
793
900
|
/**
|
|
794
|
-
* A `*Matrix` row
|
|
901
|
+
* A `*Matrix` row, every non-zero entry an arc from the row's vertex with that value: N values
|
|
902
|
+
* in a one-mode network; in a two-mode network (`*Vertices N N1`) the matrix is N1 rows of
|
|
903
|
+
* N - N1 values, entry j of row i an arc from vertex i to vertex N1 + j.
|
|
795
904
|
* @param text - the line
|
|
796
905
|
* @param line - the line number
|
|
797
906
|
*/
|
|
@@ -799,26 +908,51 @@ class PajekParser {
|
|
|
799
908
|
const tokens = this.tokens(text, "edge");
|
|
800
909
|
const row = this.matrixRows;
|
|
801
910
|
this.matrixRows++;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
if (
|
|
806
|
-
throw new LineError("
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
911
|
+
const offset = this.firstMode ?? 0;
|
|
912
|
+
const rows = this.firstMode ?? this.vertexCount;
|
|
913
|
+
const columns = this.vertexCount - offset;
|
|
914
|
+
if (row >= rows) {
|
|
915
|
+
throw new LineError("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix row ${row + 1} is beyond the ${rows} declared row(s)`);
|
|
916
|
+
}
|
|
917
|
+
if (tokens.length < columns) {
|
|
918
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1} has ${tokens.length} value(s); the matrix has ${columns} column(s)`);
|
|
919
|
+
}
|
|
920
|
+
for (let j = 0; j < columns; j++) {
|
|
921
|
+
if (!isNumericText(tokens[j])) {
|
|
922
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1}: "${tokens[j]}" is not a number`);
|
|
811
923
|
}
|
|
812
924
|
}
|
|
813
|
-
|
|
925
|
+
if (tokens.length > columns) {
|
|
926
|
+
this.report.warnOnce("validation-error", PAJEK_ISSUE.MATRIX_EXTRA, `*Matrix row ${row + 1} has ${tokens.length} value(s) for ${columns} column(s); the extra values are ignored`, { line });
|
|
927
|
+
}
|
|
928
|
+
for (let j = 0; j < columns; j++) {
|
|
814
929
|
const value = Number(tokens[j]);
|
|
815
930
|
if (value === 0) {
|
|
816
931
|
continue;
|
|
817
932
|
}
|
|
818
|
-
const e = this.pushEdge(row, j, value, line);
|
|
933
|
+
const e = this.pushEdge(row, offset + j, value, line);
|
|
819
934
|
this.writeRelation(e);
|
|
820
935
|
}
|
|
821
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* A value line of a `*Partition` (integers) or `*Vector` (numbers): the values of the next
|
|
939
|
+
* vertices, usually one per line.
|
|
940
|
+
* @param text - the line
|
|
941
|
+
*/
|
|
942
|
+
objectLine(text) {
|
|
943
|
+
const object = this.objects[this.objects.length - 1];
|
|
944
|
+
const tokens = this.tokens(text, "node");
|
|
945
|
+
for (const token of tokens) {
|
|
946
|
+
if (!isNumericText(token) || (object.kind === "partition" && !isI32(Number(token)))) {
|
|
947
|
+
// keep the later values on their vertices
|
|
948
|
+
object.values.push(null);
|
|
949
|
+
throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*${object.kind === "partition" ? "Partition" : "Vector"} "${object.name}": "${token}" is not ${object.kind === "partition" ? "an integer" : "a number"}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
for (const token of tokens) {
|
|
953
|
+
object.values.push(Number(token));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
822
956
|
/**
|
|
823
957
|
* Push one line through the direction resolver, counting the logical edges it produced.
|
|
824
958
|
* @param u - the source position
|
|
@@ -829,6 +963,10 @@ class PajekParser {
|
|
|
829
963
|
*/
|
|
830
964
|
pushEdge(u, v, weight, line) {
|
|
831
965
|
const { sink } = this;
|
|
966
|
+
if (!this.directionSet) {
|
|
967
|
+
this.directionSet = true;
|
|
968
|
+
this.resolver.setHeader(this.kind === "directed", { line });
|
|
969
|
+
}
|
|
832
970
|
const before = sink.edgeCount;
|
|
833
971
|
const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
|
|
834
972
|
this.report.counts.edges += sink.edgeCount - before;
|
|
@@ -838,10 +976,11 @@ class PajekParser {
|
|
|
838
976
|
* Write the relation, parameters and spells of a line on its primary half.
|
|
839
977
|
* @param e - the edge index
|
|
840
978
|
* @param extras - the parsed extras
|
|
979
|
+
* @param relation - the line's relation name, or null
|
|
841
980
|
*/
|
|
842
|
-
writeEdgeExtras(e, extras) {
|
|
981
|
+
writeEdgeExtras(e, extras, relation) {
|
|
843
982
|
const { sink } = this;
|
|
844
|
-
this.writeRelation(e);
|
|
983
|
+
this.writeRelation(e, relation);
|
|
845
984
|
for (let p = 0; p < extras.keys.length; p++) {
|
|
846
985
|
this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
|
|
847
986
|
}
|
|
@@ -853,17 +992,18 @@ class PajekParser {
|
|
|
853
992
|
}
|
|
854
993
|
}
|
|
855
994
|
/**
|
|
856
|
-
* Write
|
|
995
|
+
* Write a relation name on an edge: the current section's, unless a line prefix named another.
|
|
857
996
|
* @param e - the edge index
|
|
997
|
+
* @param relation - the relation name, or null for none
|
|
858
998
|
*/
|
|
859
|
-
writeRelation(e) {
|
|
860
|
-
if (
|
|
999
|
+
writeRelation(e, relation = this.relation) {
|
|
1000
|
+
if (relation === null) {
|
|
861
1001
|
return;
|
|
862
1002
|
}
|
|
863
1003
|
if (this.relationHandle === INVALID_INDEX) {
|
|
864
1004
|
this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
|
|
865
1005
|
}
|
|
866
|
-
this.sink.setEdgeValue(this.relationHandle, e,
|
|
1006
|
+
this.sink.setEdgeValue(this.relationHandle, e, relation);
|
|
867
1007
|
}
|
|
868
1008
|
/**
|
|
869
1009
|
* A line endpoint: the vertex number checked against the declared range; a reference outside
|
|
@@ -928,7 +1068,9 @@ class PajekParser {
|
|
|
928
1068
|
const token = tokens[i];
|
|
929
1069
|
if (isIntervalToken(token)) {
|
|
930
1070
|
try {
|
|
931
|
-
|
|
1071
|
+
const spells = parseIntervals(token);
|
|
1072
|
+
// an empty `[]` is no spell at all
|
|
1073
|
+
extras.spells = spells.length === 0 ? null : spells;
|
|
932
1074
|
}
|
|
933
1075
|
catch (err) {
|
|
934
1076
|
this.skip(domain);
|
|
@@ -983,6 +1125,25 @@ class PajekParser {
|
|
|
983
1125
|
writer.write(row, text);
|
|
984
1126
|
}
|
|
985
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Whether a vertex line has coordinates but no label: the token after the vertex number is bare
|
|
1130
|
+
* (not quoted) and starts a run of exactly two numbers, which cannot be a label and one
|
|
1131
|
+
* coordinate. A run of three stays a numeric label and x y (the NetworkX writer's `1 0 0.5 0.5`),
|
|
1132
|
+
* the one reading of it that keeps real files' labels.
|
|
1133
|
+
* @param text - the line
|
|
1134
|
+
* @param tokens - its tokens
|
|
1135
|
+
* @returns true when tokens[1] is the first coordinate
|
|
1136
|
+
*/
|
|
1137
|
+
function startsWithCoordinates(text, tokens) {
|
|
1138
|
+
if (text.trimStart().slice(tokens[0].length).trimStart().startsWith('"')) {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
let run = 0;
|
|
1142
|
+
while (1 + run < tokens.length && isNumericText(tokens[1 + run])) {
|
|
1143
|
+
run++;
|
|
1144
|
+
}
|
|
1145
|
+
return run === 2;
|
|
1146
|
+
}
|
|
986
1147
|
/**
|
|
987
1148
|
* Resolve the firstVertex option.
|
|
988
1149
|
* @param value - the caller's value
|
|
@@ -1061,7 +1222,8 @@ export const pajekImporter = Object.freeze({
|
|
|
1061
1222
|
},
|
|
1062
1223
|
/**
|
|
1063
1224
|
* Read every network of a Pajek project file (`.paj`), each into its own sink. A network starts
|
|
1064
|
-
* at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
|
|
1225
|
+
* at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
|
|
1226
|
+
* (the `*Vertices N` line right after an object's header).
|
|
1065
1227
|
* @param input - the text, bytes or stream
|
|
1066
1228
|
* @param sinkFor - the sink of the network with this index, called before its first push
|
|
1067
1229
|
* @param options - common and Pajek options
|
|
@@ -1102,7 +1264,8 @@ export const pajekImporter = Object.freeze({
|
|
|
1102
1264
|
/**
|
|
1103
1265
|
* Counts the networks of the rest of a project file by their headers alone, with the parser's
|
|
1104
1266
|
* rule: a network starts at `*Network` after a named or populated one, or at a `*Vertices` after a
|
|
1105
|
-
* populated one unless it
|
|
1267
|
+
* populated one unless it is the line right after a project object's header (`*Partition`,
|
|
1268
|
+
* `*Vector`, a skipped section).
|
|
1106
1269
|
*/
|
|
1107
1270
|
class NetworkCounter {
|
|
1108
1271
|
constructor() {
|
|
@@ -1110,28 +1273,37 @@ class NetworkCounter {
|
|
|
1110
1273
|
this.count = 0;
|
|
1111
1274
|
this.named = false;
|
|
1112
1275
|
this.populated = false;
|
|
1113
|
-
this.
|
|
1276
|
+
this.afterObjectHeader = false;
|
|
1114
1277
|
}
|
|
1115
1278
|
/**
|
|
1116
1279
|
* Feed one line.
|
|
1117
1280
|
* @param text - the line
|
|
1118
1281
|
*/
|
|
1119
1282
|
line(text) {
|
|
1283
|
+
if (isCommentOrBlank(text)) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const { afterObjectHeader } = this;
|
|
1287
|
+
this.afterObjectHeader = false;
|
|
1120
1288
|
if (!isSectionLine(text)) {
|
|
1121
1289
|
return;
|
|
1122
1290
|
}
|
|
1123
1291
|
const h = parseSectionHeader(text);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1292
|
+
if (h === null) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const { kind } = h;
|
|
1296
|
+
if (kind === "partition" || kind === "vector" || kind === "unsupported") {
|
|
1297
|
+
this.afterObjectHeader = true;
|
|
1298
|
+
}
|
|
1299
|
+
else if (kind === "network") {
|
|
1128
1300
|
if (this.count === 0 || this.named || this.populated) {
|
|
1129
1301
|
this.count++;
|
|
1130
1302
|
this.populated = false;
|
|
1131
1303
|
}
|
|
1132
1304
|
this.named = true;
|
|
1133
1305
|
}
|
|
1134
|
-
else if (kind === "vertices" && !
|
|
1306
|
+
else if (kind === "vertices" && !afterObjectHeader) {
|
|
1135
1307
|
if (this.count === 0 || this.populated) {
|
|
1136
1308
|
this.count++;
|
|
1137
1309
|
this.named = false;
|