@graphty/graph-io 0.3.3 → 0.3.4
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 +17 -4
- package/dist/chunks/{importer-BELrICWM.js → importer-BOxwmef_.js} +352 -19
- package/dist/chunks/importer-BOxwmef_.js.map +1 -0
- package/dist/graph-io.js +2 -2
- package/dist/graph-io.js.map +1 -1
- package/dist/json.js +1 -1
- package/dist/src/formats/json/dialect.d.ts +21 -4
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +29 -4
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +27 -7
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +17 -4
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +356 -8
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/index.d.ts +1 -1
- package/dist/src/formats/json/index.d.ts.map +1 -1
- package/dist/src/formats/json/index.js +1 -1
- package/dist/src/formats/json/index.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.map +1 -1
- package/dist/src/sniff.d.ts +3 -3
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js.map +1 -1
- package/package.json +1 -1
- package/src/common/escape.ts +5 -4
- package/src/formats/json/dialect.ts +40 -6
- package/src/formats/json/exporter.ts +28 -7
- package/src/formats/json/importer.ts +411 -21
- package/src/formats/json/index.ts +7 -1
- package/src/formats/neo4j/exporter.ts +43 -6
- package/src/formats/neo4j/importer.ts +56 -11
- package/src/formats/neo4j/index.ts +10 -3
- package/src/formats/pajek/exporter.ts +3 -1
- package/src/formats/pajek/importer.ts +284 -60
- package/src/formats/pajek/syntax.ts +98 -12
- package/src/index.ts +2 -0
- package/src/sniff.ts +3 -3
- package/dist/chunks/importer-BELrICWM.js.map +0 -1
|
@@ -11,7 +11,9 @@
|
|
|
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
|
|
|
17
19
|
import {
|
|
@@ -25,7 +27,7 @@ import {
|
|
|
25
27
|
type U32,
|
|
26
28
|
} from "@graphty/graph-format";
|
|
27
29
|
|
|
28
|
-
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
|
|
30
|
+
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE, uniqueColumnName } from "../../common/attributes.js";
|
|
29
31
|
import {
|
|
30
32
|
DUPLICATE_NODE_CODE,
|
|
31
33
|
ENCODING_FALLBACK_CODE,
|
|
@@ -52,23 +54,26 @@ import { isNumericText, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../
|
|
|
52
54
|
import { isWeightField, parseWeightText } from "../../common/weights.js";
|
|
53
55
|
import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
|
|
54
56
|
import {
|
|
57
|
+
decodeCharacterReferences,
|
|
55
58
|
isCommentOrBlank,
|
|
56
59
|
isIntervalToken,
|
|
57
60
|
isSectionLine,
|
|
61
|
+
isShapeKeyword,
|
|
58
62
|
isVertexNumber,
|
|
59
63
|
LABEL_COLUMN,
|
|
60
64
|
ORIGINAL_ID_KEY,
|
|
61
65
|
parseIntervals,
|
|
62
66
|
parseSectionHeader,
|
|
67
|
+
PARTITION_COLUMN,
|
|
63
68
|
POSITION_COLUMN,
|
|
64
69
|
RELATION_COLUMN,
|
|
65
70
|
type SectionHeader,
|
|
66
71
|
SHAPE_COLUMN,
|
|
67
|
-
SHAPES,
|
|
68
72
|
SPELLS_COLUMN,
|
|
69
73
|
tokenize,
|
|
70
74
|
VALUE_COLUMN,
|
|
71
75
|
VALUE_FIELD,
|
|
76
|
+
VECTOR_COLUMN,
|
|
72
77
|
} from "./syntax.js";
|
|
73
78
|
|
|
74
79
|
/** The format-specific options of the Pajek importer. */
|
|
@@ -92,7 +97,7 @@ export const PAJEK_ISSUE = Object.freeze({
|
|
|
92
97
|
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
93
98
|
/** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
|
|
94
99
|
NO_VERTICES: "E_PAJEK_NO_VERTICES",
|
|
95
|
-
/** Fatal: `*Vertices` without a vertex count,
|
|
100
|
+
/** Fatal: `*Vertices` without a vertex count, one the sink cannot hold, or a first-mode count outside 0..N. */
|
|
96
101
|
VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
|
|
97
102
|
/** A project file holds several networks; import() reads the first, importAll() reads every one. */
|
|
98
103
|
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
@@ -106,20 +111,24 @@ export const PAJEK_ISSUE = Object.freeze({
|
|
|
106
111
|
VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
|
|
107
112
|
/** A vertex number outside the declared range. */
|
|
108
113
|
VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
|
|
109
|
-
/** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
|
|
110
|
-
VERTEX_COUNT: "
|
|
114
|
+
/** Fewer vertex lines than `*Vertices` declares, which the Pajek manual allows (vertices without a line have no label). */
|
|
115
|
+
VERTEX_COUNT: "W_PAJEK_VERTEX_COUNT",
|
|
111
116
|
/** A second line for the same vertex; the later values overwrite. */
|
|
112
117
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
113
|
-
/** A line (arc, edge, list or
|
|
118
|
+
/** A line (arc, edge, list, matrix row, partition or vector value) the grammar does not accept. */
|
|
114
119
|
LINE: "E_PAJEK_LINE",
|
|
115
120
|
/** A line endpoint outside the declared vertex range (the core's code, forwarded). */
|
|
116
121
|
UNKNOWN_NODE: "E_UNKNOWN_NODE",
|
|
117
122
|
/** A malformed time interval token. */
|
|
118
123
|
INTERVAL: "E_PAJEK_INTERVAL",
|
|
119
|
-
/** A `*Matrix` section with the wrong number of rows. */
|
|
124
|
+
/** A `*Matrix` section with the wrong number of rows (N, or N1 in a two-mode network). */
|
|
120
125
|
MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
|
|
121
|
-
/**
|
|
122
|
-
|
|
126
|
+
/** `*Matrix` rows longer than the column count; the extra values are ignored. */
|
|
127
|
+
MATRIX_EXTRA: "W_PAJEK_MATRIX_EXTRA",
|
|
128
|
+
/** A `*Partition` or `*Vector` whose value count differs from the network's vertex count; values beyond it are dropped, vertices without one are unset. */
|
|
129
|
+
OBJECT_COUNT: "E_PAJEK_OBJECT_COUNT",
|
|
130
|
+
/** A project-file section (`*Events`, `*Permutation`, ...) the importer does not read; its lines are skipped. */
|
|
131
|
+
UNSUPPORTED_SECTION: "W_PAJEK_UNSUPPORTED_SECTION",
|
|
123
132
|
/** Tokens after a section header the grammar does not account for. */
|
|
124
133
|
HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
|
|
125
134
|
/** The file declares vertices but no line section. */
|
|
@@ -258,7 +267,38 @@ interface RowExtras {
|
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
/** The section the parser is in. */
|
|
261
|
-
type Section = "none" | "vertices" | "lines" | "list" | "matrix" | "skip";
|
|
270
|
+
type Section = "none" | "vertices" | "lines" | "list" | "matrix" | "object" | "skip";
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A `*Partition` or `*Vector` object of a project file, buffered until the end of the network:
|
|
274
|
+
* one may come before the network whose vertices it describes.
|
|
275
|
+
*/
|
|
276
|
+
interface ProjectObject {
|
|
277
|
+
readonly kind: "partition" | "vector";
|
|
278
|
+
/** The object's name from its header. */
|
|
279
|
+
readonly name: string;
|
|
280
|
+
/** The header's line number. */
|
|
281
|
+
readonly line: number;
|
|
282
|
+
/** The object's own `*Vertices N`, or null without one. */
|
|
283
|
+
count: number | null;
|
|
284
|
+
/** The value per vertex position; null for a line that could not be read. */
|
|
285
|
+
readonly values: (number | null)[];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Whether a number is an integer the i32 partition column can hold.
|
|
290
|
+
* @param value - the number
|
|
291
|
+
* @returns true for an i32 integer
|
|
292
|
+
*/
|
|
293
|
+
function isI32(value: number): boolean {
|
|
294
|
+
return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A vertex number of an adjacency list: Pajek reads a negative one as its absolute value. */
|
|
298
|
+
const LIST_VERTEX = /^-?[0-9]+$/;
|
|
299
|
+
|
|
300
|
+
/** The relation prefix `k:` of a line in a line section. */
|
|
301
|
+
const RELATION_PREFIX = /^([0-9]+):$/;
|
|
262
302
|
|
|
263
303
|
/**
|
|
264
304
|
* One import call's state machine: the sections, the vertex range, the columns declared so far
|
|
@@ -303,6 +343,21 @@ class PajekParser {
|
|
|
303
343
|
|
|
304
344
|
private sawLineSection = false;
|
|
305
345
|
|
|
346
|
+
/** The direction of the first line section, for a network whose line sections are all empty. */
|
|
347
|
+
private firstKind: EdgeKind = "directed";
|
|
348
|
+
|
|
349
|
+
/** Set once the first line has fixed the sink's direction. */
|
|
350
|
+
private directionSet = false;
|
|
351
|
+
|
|
352
|
+
/** The names of the numbered relations (`*Arcs :k "name"`), for the `k:` line prefix. */
|
|
353
|
+
private readonly relationNames = new Map<number, string>();
|
|
354
|
+
|
|
355
|
+
/** The `*Partition` / `*Vector` objects read so far, written by finish(). */
|
|
356
|
+
private readonly objects: ProjectObject[] = [];
|
|
357
|
+
|
|
358
|
+
/** Set when the previous line was a project-object header, whose `*Vertices N` line comes next. */
|
|
359
|
+
private objectHeader = false;
|
|
360
|
+
|
|
306
361
|
/** Node index per vertex position (vertex number minus base). */
|
|
307
362
|
private indexOfPos: U32 = new Uint32Array(0);
|
|
308
363
|
|
|
@@ -380,9 +435,11 @@ class PajekParser {
|
|
|
380
435
|
if (isCommentOrBlank(text)) {
|
|
381
436
|
return false;
|
|
382
437
|
}
|
|
438
|
+
const afterObjectHeader = this.objectHeader;
|
|
439
|
+
this.objectHeader = false;
|
|
383
440
|
try {
|
|
384
441
|
if (isSectionLine(text)) {
|
|
385
|
-
this.header(text, line);
|
|
442
|
+
this.header(text, line, afterObjectHeader);
|
|
386
443
|
return this.ended;
|
|
387
444
|
}
|
|
388
445
|
switch (this.section) {
|
|
@@ -404,6 +461,9 @@ class PajekParser {
|
|
|
404
461
|
case "matrix":
|
|
405
462
|
this.matrixLine(text, line);
|
|
406
463
|
return false;
|
|
464
|
+
case "object":
|
|
465
|
+
this.objectLine(text);
|
|
466
|
+
return false;
|
|
407
467
|
case "skip":
|
|
408
468
|
return false;
|
|
409
469
|
default: {
|
|
@@ -438,7 +498,11 @@ class PajekParser {
|
|
|
438
498
|
"the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section",
|
|
439
499
|
);
|
|
440
500
|
this.resolver.setHeader(this.options.defaultDirected);
|
|
501
|
+
} else if (!this.directionSet) {
|
|
502
|
+
// every line section was empty: the first header still says which way the network goes
|
|
503
|
+
this.resolver.setHeader(this.firstKind === "directed");
|
|
441
504
|
}
|
|
505
|
+
const objects = this.writeObjects();
|
|
442
506
|
if (this.coercer.mergeCount > 0) {
|
|
443
507
|
this.report.warning(
|
|
444
508
|
"coercion",
|
|
@@ -446,23 +510,75 @@ class PajekParser {
|
|
|
446
510
|
`${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`,
|
|
447
511
|
);
|
|
448
512
|
}
|
|
513
|
+
const pajek = {
|
|
514
|
+
...(this.firstMode === null ? {} : { firstMode: this.firstMode }),
|
|
515
|
+
...(objects.length === 0 ? {} : { objects }),
|
|
516
|
+
};
|
|
449
517
|
const meta: GraphMetaPatch = {
|
|
450
518
|
sourceFormat: "pajek",
|
|
451
519
|
...(this.networkName === null ? {} : { name: this.networkName }),
|
|
452
|
-
...(
|
|
520
|
+
...(Object.keys(pajek).length === 0 ? {} : { extra: { pajek } }),
|
|
453
521
|
};
|
|
454
522
|
this.sink.setMeta(meta);
|
|
455
523
|
}
|
|
456
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Write every `*Partition` / `*Vector` object into its node column: `partition` (i32) or
|
|
527
|
+
* `vector` (f64), a later object of the same kind in `partition#2`, `vector#2`, ... The value
|
|
528
|
+
* of vertex k is the object's k-th value.
|
|
529
|
+
* @returns the objects' Pajek names and the columns they went to, for meta.extra.pajek.objects
|
|
530
|
+
*/
|
|
531
|
+
private writeObjects(): { kind: string; name: string; column: string }[] {
|
|
532
|
+
const used = new Set<string>();
|
|
533
|
+
const written: { kind: string; name: string; column: string }[] = [];
|
|
534
|
+
for (const object of this.objects) {
|
|
535
|
+
const where = { line: object.line };
|
|
536
|
+
const partition = object.kind === "partition";
|
|
537
|
+
const name = uniqueColumnName(partition ? PARTITION_COLUMN : VECTOR_COLUMN, null, (n) => used.has(n));
|
|
538
|
+
used.add(name);
|
|
539
|
+
const { handle, decl } = declareResolved(
|
|
540
|
+
this.sink,
|
|
541
|
+
"node",
|
|
542
|
+
{
|
|
543
|
+
name,
|
|
544
|
+
dtype: partition ? "i32" : "f64",
|
|
545
|
+
nullable: true,
|
|
546
|
+
origin: { format: "pajek", id: object.kind },
|
|
547
|
+
},
|
|
548
|
+
this.report,
|
|
549
|
+
where,
|
|
550
|
+
);
|
|
551
|
+
const n = Math.min(object.values.length, this.vertexCount);
|
|
552
|
+
for (let pos = 0; pos < n; pos++) {
|
|
553
|
+
const value = object.values[pos];
|
|
554
|
+
if (value !== null) {
|
|
555
|
+
this.sink.setNodeValue(handle, this.indexOfPos[pos], value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const declared = object.count ?? object.values.length;
|
|
559
|
+
if (declared !== this.vertexCount || object.values.length !== declared) {
|
|
560
|
+
this.report.error(
|
|
561
|
+
"validation-error",
|
|
562
|
+
PAJEK_ISSUE.OBJECT_COUNT,
|
|
563
|
+
`*${partition ? "Partition" : "Vector"} "${object.name}" declares ${declared} vertices and holds ${object.values.length} value(s); the network has ${this.vertexCount}`,
|
|
564
|
+
where,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
written.push({ kind: object.kind, name: object.name, column: decl.name });
|
|
568
|
+
}
|
|
569
|
+
return written;
|
|
570
|
+
}
|
|
571
|
+
|
|
457
572
|
// ============================================================ headers
|
|
458
573
|
|
|
459
574
|
/**
|
|
460
575
|
* A section header line.
|
|
461
576
|
* @param text - the line
|
|
462
577
|
* @param line - the line number
|
|
578
|
+
* @param afterObjectHeader - the previous line was a `*Partition`, `*Vector` or skipped section header
|
|
463
579
|
*/
|
|
464
|
-
private header(text: string, line: number): void {
|
|
465
|
-
const
|
|
580
|
+
private header(text: string, line: number, afterObjectHeader: boolean): void {
|
|
581
|
+
const previous = this.section;
|
|
466
582
|
this.endSection();
|
|
467
583
|
const h = parseSectionHeader(text);
|
|
468
584
|
if (h === null) {
|
|
@@ -487,9 +603,13 @@ class PajekParser {
|
|
|
487
603
|
this.section = "none";
|
|
488
604
|
return;
|
|
489
605
|
case "vertices":
|
|
490
|
-
if (
|
|
491
|
-
// the `*Vertices N` line of a
|
|
492
|
-
|
|
606
|
+
if (afterObjectHeader) {
|
|
607
|
+
// the `*Vertices N` line of a project object (*Partition, *Vector, a skipped
|
|
608
|
+
// section), wherever it stands: never the network's own
|
|
609
|
+
this.section = previous;
|
|
610
|
+
if (previous === "object") {
|
|
611
|
+
this.objects[this.objects.length - 1].count = h.count;
|
|
612
|
+
}
|
|
493
613
|
return;
|
|
494
614
|
}
|
|
495
615
|
if (this.vertexCount >= 0) {
|
|
@@ -511,9 +631,16 @@ class PajekParser {
|
|
|
511
631
|
this.lineSection(h, line, "matrix", "directed");
|
|
512
632
|
this.matrixRows = 0;
|
|
513
633
|
return;
|
|
634
|
+
case "partition":
|
|
635
|
+
case "vector":
|
|
636
|
+
this.objects.push({ kind: h.kind, name: h.name ?? "", line, count: null, values: [] });
|
|
637
|
+
this.section = "object";
|
|
638
|
+
this.objectHeader = true;
|
|
639
|
+
return;
|
|
514
640
|
case "unsupported":
|
|
515
641
|
this.section = "skip";
|
|
516
|
-
this.
|
|
642
|
+
this.objectHeader = true;
|
|
643
|
+
this.report.warning(
|
|
517
644
|
"unsupported",
|
|
518
645
|
PAJEK_ISSUE.UNSUPPORTED_SECTION,
|
|
519
646
|
`the *${h.keyword} section is not supported; its lines are skipped`,
|
|
@@ -539,6 +666,13 @@ class PajekParser {
|
|
|
539
666
|
if (!Number.isSafeInteger(h.count)) {
|
|
540
667
|
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
|
|
541
668
|
}
|
|
669
|
+
if (h.secondCount !== null && (h.secondCount < 0 || h.secondCount > h.count)) {
|
|
670
|
+
this.report.fail(
|
|
671
|
+
PAJEK_ISSUE.VERTICES_COUNT,
|
|
672
|
+
`*Vertices ${h.count} ${h.secondCount}: the first-mode count is not within 0..${h.count}`,
|
|
673
|
+
{ line },
|
|
674
|
+
);
|
|
675
|
+
}
|
|
542
676
|
try {
|
|
543
677
|
// the declared count must be reservable (the sink is the authority on what it can hold,
|
|
544
678
|
// E_TOO_LARGE beyond MAX_COUNT) before any vertex is materialised
|
|
@@ -570,8 +704,9 @@ class PajekParser {
|
|
|
570
704
|
}
|
|
571
705
|
|
|
572
706
|
/**
|
|
573
|
-
* A line section header: the vertex set is completed
|
|
574
|
-
* (rule 1 of design section 8.4)
|
|
707
|
+
* A line section header: the vertex set is completed and the kind switched. The sink's
|
|
708
|
+
* direction (rule 1 of design section 8.4) is set by the first LINE, not the first header, so
|
|
709
|
+
* an empty `*Arcs` before the `*Edges` of an undirected network does not make it directed.
|
|
575
710
|
* @param h - the header
|
|
576
711
|
* @param line - the line number
|
|
577
712
|
* @param section - the parser state for the section
|
|
@@ -588,22 +723,26 @@ class PajekParser {
|
|
|
588
723
|
this.finishVertices();
|
|
589
724
|
if (!this.sawLineSection) {
|
|
590
725
|
this.sawLineSection = true;
|
|
591
|
-
this.
|
|
726
|
+
this.firstKind = kind;
|
|
592
727
|
}
|
|
593
728
|
this.section = section;
|
|
594
729
|
this.kind = kind;
|
|
595
730
|
this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
|
|
731
|
+
if (h.relation !== null) {
|
|
732
|
+
this.relationNames.set(h.relation, this.relation ?? String(h.relation));
|
|
733
|
+
}
|
|
596
734
|
}
|
|
597
735
|
|
|
598
736
|
/**
|
|
599
737
|
* Checks that run when a section ends.
|
|
600
738
|
*/
|
|
601
739
|
private endSection(): void {
|
|
602
|
-
|
|
740
|
+
const rows = this.firstMode ?? this.vertexCount;
|
|
741
|
+
if (this.section === "matrix" && this.matrixRows !== rows) {
|
|
603
742
|
this.report.error(
|
|
604
743
|
"validation-error",
|
|
605
744
|
PAJEK_ISSUE.MATRIX_ROWS,
|
|
606
|
-
`*Matrix has ${this.matrixRows} row(s); *Vertices declares ${
|
|
745
|
+
`*Matrix has ${this.matrixRows} row(s); *Vertices declares ${rows}`,
|
|
607
746
|
);
|
|
608
747
|
}
|
|
609
748
|
this.section = "none";
|
|
@@ -707,7 +846,7 @@ class PajekParser {
|
|
|
707
846
|
// a partial list: fewer lines than declared, counting the lines skipped after an error
|
|
708
847
|
const lines = this.seenCount + this.report.counts.skippedNodes;
|
|
709
848
|
if (this.seenCount > 0 && lines < n) {
|
|
710
|
-
this.report.
|
|
849
|
+
this.report.warning(
|
|
711
850
|
"validation-error",
|
|
712
851
|
PAJEK_ISSUE.VERTEX_COUNT,
|
|
713
852
|
`*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`,
|
|
@@ -745,8 +884,9 @@ class PajekParser {
|
|
|
745
884
|
}
|
|
746
885
|
// parse everything before writing anything
|
|
747
886
|
let i = 1;
|
|
748
|
-
|
|
749
|
-
if (
|
|
887
|
+
let label: string | null = null;
|
|
888
|
+
if (tokens.length > 1 && !startsWithCoordinates(text, tokens)) {
|
|
889
|
+
label = decodeCharacterReferences(tokens[1]);
|
|
750
890
|
i = 2;
|
|
751
891
|
}
|
|
752
892
|
let x = 0;
|
|
@@ -772,8 +912,8 @@ class PajekParser {
|
|
|
772
912
|
);
|
|
773
913
|
}
|
|
774
914
|
let shape: string | null = null;
|
|
775
|
-
if (i < tokens.length &&
|
|
776
|
-
shape = tokens[i];
|
|
915
|
+
if (i < tokens.length && isShapeKeyword(tokens[i])) {
|
|
916
|
+
shape = tokens[i].toLowerCase();
|
|
777
917
|
i++;
|
|
778
918
|
}
|
|
779
919
|
const extras = this.extras(tokens, i, "node");
|
|
@@ -954,7 +1094,15 @@ class PajekParser {
|
|
|
954
1094
|
* @param line - the line number
|
|
955
1095
|
*/
|
|
956
1096
|
private edgeLine(text: string, line: number): void {
|
|
957
|
-
|
|
1097
|
+
let tokens = this.tokens(text, "edge");
|
|
1098
|
+
// a `k:` prefix puts this one line in relation k
|
|
1099
|
+
let { relation } = this;
|
|
1100
|
+
const prefix = RELATION_PREFIX.exec(tokens[0]);
|
|
1101
|
+
if (prefix !== null) {
|
|
1102
|
+
const k = Number(prefix[1]);
|
|
1103
|
+
relation = this.relationNames.get(k) ?? String(k);
|
|
1104
|
+
tokens = tokens.slice(1);
|
|
1105
|
+
}
|
|
958
1106
|
if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
|
|
959
1107
|
this.report.counts.skippedEdges++;
|
|
960
1108
|
throw new LineError(
|
|
@@ -993,18 +1141,19 @@ class PajekParser {
|
|
|
993
1141
|
// inferred like every parameter, so a re-import of the exporter's `value` parameter agrees
|
|
994
1142
|
this.writeParameter("edge", VALUE_COLUMN, e, valueText);
|
|
995
1143
|
}
|
|
996
|
-
this.writeEdgeExtras(e, extras);
|
|
1144
|
+
this.writeEdgeExtras(e, extras, relation);
|
|
997
1145
|
}
|
|
998
1146
|
|
|
999
1147
|
/**
|
|
1000
|
-
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2
|
|
1148
|
+
* An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`; a negative vertex
|
|
1149
|
+
* number is read as its absolute value, as Pajek does.
|
|
1001
1150
|
* @param text - the line
|
|
1002
1151
|
* @param line - the line number
|
|
1003
1152
|
*/
|
|
1004
1153
|
private listLine(text: string, line: number): void {
|
|
1005
1154
|
const tokens = this.tokens(text, "edge");
|
|
1006
1155
|
for (const token of tokens) {
|
|
1007
|
-
if (!
|
|
1156
|
+
if (!LIST_VERTEX.test(token)) {
|
|
1008
1157
|
this.report.counts.skippedEdges++;
|
|
1009
1158
|
throw new LineError(
|
|
1010
1159
|
"parse-error",
|
|
@@ -1013,16 +1162,18 @@ class PajekParser {
|
|
|
1013
1162
|
);
|
|
1014
1163
|
}
|
|
1015
1164
|
}
|
|
1016
|
-
const u = this.endpoint(Number(tokens[0]));
|
|
1165
|
+
const u = this.endpoint(Math.abs(Number(tokens[0])));
|
|
1017
1166
|
for (let i = 1; i < tokens.length; i++) {
|
|
1018
|
-
const v = this.endpoint(Number(tokens[i]));
|
|
1167
|
+
const v = this.endpoint(Math.abs(Number(tokens[i])));
|
|
1019
1168
|
const e = this.pushEdge(u, v, undefined, line);
|
|
1020
1169
|
this.writeRelation(e);
|
|
1021
1170
|
}
|
|
1022
1171
|
}
|
|
1023
1172
|
|
|
1024
1173
|
/**
|
|
1025
|
-
* A `*Matrix` row
|
|
1174
|
+
* A `*Matrix` row, every non-zero entry an arc from the row's vertex with that value: N values
|
|
1175
|
+
* in a one-mode network; in a two-mode network (`*Vertices N N1`) the matrix is N1 rows of
|
|
1176
|
+
* N - N1 values, entry j of row i an arc from vertex i to vertex N1 + j.
|
|
1026
1177
|
* @param text - the line
|
|
1027
1178
|
* @param line - the line number
|
|
1028
1179
|
*/
|
|
@@ -1030,39 +1181,74 @@ class PajekParser {
|
|
|
1030
1181
|
const tokens = this.tokens(text, "edge");
|
|
1031
1182
|
const row = this.matrixRows;
|
|
1032
1183
|
this.matrixRows++;
|
|
1033
|
-
|
|
1184
|
+
const offset = this.firstMode ?? 0;
|
|
1185
|
+
const rows = this.firstMode ?? this.vertexCount;
|
|
1186
|
+
const columns = this.vertexCount - offset;
|
|
1187
|
+
if (row >= rows) {
|
|
1034
1188
|
throw new LineError(
|
|
1035
1189
|
"validation-error",
|
|
1036
1190
|
PAJEK_ISSUE.MATRIX_ROWS,
|
|
1037
|
-
`*Matrix row ${row + 1} is beyond the ${
|
|
1191
|
+
`*Matrix row ${row + 1} is beyond the ${rows} declared row(s)`,
|
|
1038
1192
|
);
|
|
1039
1193
|
}
|
|
1040
|
-
if (tokens.length
|
|
1194
|
+
if (tokens.length < columns) {
|
|
1041
1195
|
throw new LineError(
|
|
1042
1196
|
"parse-error",
|
|
1043
1197
|
PAJEK_ISSUE.LINE,
|
|
1044
|
-
`*Matrix row ${row + 1} has ${tokens.length} value(s);
|
|
1198
|
+
`*Matrix row ${row + 1} has ${tokens.length} value(s); the matrix has ${columns} column(s)`,
|
|
1045
1199
|
);
|
|
1046
1200
|
}
|
|
1047
|
-
for (
|
|
1048
|
-
if (!isNumericText(
|
|
1201
|
+
for (let j = 0; j < columns; j++) {
|
|
1202
|
+
if (!isNumericText(tokens[j])) {
|
|
1049
1203
|
throw new LineError(
|
|
1050
1204
|
"parse-error",
|
|
1051
1205
|
PAJEK_ISSUE.LINE,
|
|
1052
|
-
`*Matrix row ${row + 1}: "${
|
|
1206
|
+
`*Matrix row ${row + 1}: "${tokens[j]}" is not a number`,
|
|
1053
1207
|
);
|
|
1054
1208
|
}
|
|
1055
1209
|
}
|
|
1056
|
-
|
|
1210
|
+
if (tokens.length > columns) {
|
|
1211
|
+
this.report.warnOnce(
|
|
1212
|
+
"validation-error",
|
|
1213
|
+
PAJEK_ISSUE.MATRIX_EXTRA,
|
|
1214
|
+
`*Matrix row ${row + 1} has ${tokens.length} value(s) for ${columns} column(s); the extra values are ignored`,
|
|
1215
|
+
{ line },
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
for (let j = 0; j < columns; j++) {
|
|
1057
1219
|
const value = Number(tokens[j]);
|
|
1058
1220
|
if (value === 0) {
|
|
1059
1221
|
continue;
|
|
1060
1222
|
}
|
|
1061
|
-
const e = this.pushEdge(row, j, value, line);
|
|
1223
|
+
const e = this.pushEdge(row, offset + j, value, line);
|
|
1062
1224
|
this.writeRelation(e);
|
|
1063
1225
|
}
|
|
1064
1226
|
}
|
|
1065
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* A value line of a `*Partition` (integers) or `*Vector` (numbers): the values of the next
|
|
1230
|
+
* vertices, usually one per line.
|
|
1231
|
+
* @param text - the line
|
|
1232
|
+
*/
|
|
1233
|
+
private objectLine(text: string): void {
|
|
1234
|
+
const object = this.objects[this.objects.length - 1];
|
|
1235
|
+
const tokens = this.tokens(text, "node");
|
|
1236
|
+
for (const token of tokens) {
|
|
1237
|
+
if (!isNumericText(token) || (object.kind === "partition" && !isI32(Number(token)))) {
|
|
1238
|
+
// keep the later values on their vertices
|
|
1239
|
+
object.values.push(null);
|
|
1240
|
+
throw new LineError(
|
|
1241
|
+
"parse-error",
|
|
1242
|
+
PAJEK_ISSUE.LINE,
|
|
1243
|
+
`*${object.kind === "partition" ? "Partition" : "Vector"} "${object.name}": "${token}" is not ${object.kind === "partition" ? "an integer" : "a number"}`,
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
for (const token of tokens) {
|
|
1248
|
+
object.values.push(Number(token));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1066
1252
|
/**
|
|
1067
1253
|
* Push one line through the direction resolver, counting the logical edges it produced.
|
|
1068
1254
|
* @param u - the source position
|
|
@@ -1073,6 +1259,10 @@ class PajekParser {
|
|
|
1073
1259
|
*/
|
|
1074
1260
|
private pushEdge(u: number, v: number, weight: number | undefined, line: number): number {
|
|
1075
1261
|
const { sink } = this;
|
|
1262
|
+
if (!this.directionSet) {
|
|
1263
|
+
this.directionSet = true;
|
|
1264
|
+
this.resolver.setHeader(this.kind === "directed", { line });
|
|
1265
|
+
}
|
|
1076
1266
|
const before = sink.edgeCount;
|
|
1077
1267
|
const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
|
|
1078
1268
|
this.report.counts.edges += sink.edgeCount - before;
|
|
@@ -1083,10 +1273,11 @@ class PajekParser {
|
|
|
1083
1273
|
* Write the relation, parameters and spells of a line on its primary half.
|
|
1084
1274
|
* @param e - the edge index
|
|
1085
1275
|
* @param extras - the parsed extras
|
|
1276
|
+
* @param relation - the line's relation name, or null
|
|
1086
1277
|
*/
|
|
1087
|
-
private writeEdgeExtras(e: number, extras: RowExtras): void {
|
|
1278
|
+
private writeEdgeExtras(e: number, extras: RowExtras, relation: string | null): void {
|
|
1088
1279
|
const { sink } = this;
|
|
1089
|
-
this.writeRelation(e);
|
|
1280
|
+
this.writeRelation(e, relation);
|
|
1090
1281
|
for (let p = 0; p < extras.keys.length; p++) {
|
|
1091
1282
|
this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
|
|
1092
1283
|
}
|
|
@@ -1099,17 +1290,18 @@ class PajekParser {
|
|
|
1099
1290
|
}
|
|
1100
1291
|
|
|
1101
1292
|
/**
|
|
1102
|
-
* Write
|
|
1293
|
+
* Write a relation name on an edge: the current section's, unless a line prefix named another.
|
|
1103
1294
|
* @param e - the edge index
|
|
1295
|
+
* @param relation - the relation name, or null for none
|
|
1104
1296
|
*/
|
|
1105
|
-
private writeRelation(e: number): void {
|
|
1106
|
-
if (
|
|
1297
|
+
private writeRelation(e: number, relation: string | null = this.relation): void {
|
|
1298
|
+
if (relation === null) {
|
|
1107
1299
|
return;
|
|
1108
1300
|
}
|
|
1109
1301
|
if (this.relationHandle === INVALID_INDEX) {
|
|
1110
1302
|
this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
|
|
1111
1303
|
}
|
|
1112
|
-
this.sink.setEdgeValue(this.relationHandle, e,
|
|
1304
|
+
this.sink.setEdgeValue(this.relationHandle, e, relation);
|
|
1113
1305
|
}
|
|
1114
1306
|
|
|
1115
1307
|
/**
|
|
@@ -1181,7 +1373,9 @@ class PajekParser {
|
|
|
1181
1373
|
const token = tokens[i];
|
|
1182
1374
|
if (isIntervalToken(token)) {
|
|
1183
1375
|
try {
|
|
1184
|
-
|
|
1376
|
+
const spells = parseIntervals(token);
|
|
1377
|
+
// an empty `[]` is no spell at all
|
|
1378
|
+
extras.spells = spells.length === 0 ? null : spells;
|
|
1185
1379
|
} catch (err) {
|
|
1186
1380
|
this.skip(domain);
|
|
1187
1381
|
throw new LineError(
|
|
@@ -1245,6 +1439,26 @@ class PajekParser {
|
|
|
1245
1439
|
}
|
|
1246
1440
|
}
|
|
1247
1441
|
|
|
1442
|
+
/**
|
|
1443
|
+
* Whether a vertex line has coordinates but no label: the token after the vertex number is bare
|
|
1444
|
+
* (not quoted) and starts a run of exactly two numbers, which cannot be a label and one
|
|
1445
|
+
* coordinate. A run of three stays a numeric label and x y (the NetworkX writer's `1 0 0.5 0.5`),
|
|
1446
|
+
* the one reading of it that keeps real files' labels.
|
|
1447
|
+
* @param text - the line
|
|
1448
|
+
* @param tokens - its tokens
|
|
1449
|
+
* @returns true when tokens[1] is the first coordinate
|
|
1450
|
+
*/
|
|
1451
|
+
function startsWithCoordinates(text: string, tokens: readonly string[]): boolean {
|
|
1452
|
+
if (text.trimStart().slice(tokens[0].length).trimStart().startsWith('"')) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
let run = 0;
|
|
1456
|
+
while (1 + run < tokens.length && isNumericText(tokens[1 + run])) {
|
|
1457
|
+
run++;
|
|
1458
|
+
}
|
|
1459
|
+
return run === 2;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1248
1462
|
/**
|
|
1249
1463
|
* Resolve the firstVertex option.
|
|
1250
1464
|
* @param value - the caller's value
|
|
@@ -1340,7 +1554,8 @@ export const pajekImporter: GraphImporter<PajekImportOptions> = Object.freeze({
|
|
|
1340
1554
|
|
|
1341
1555
|
/**
|
|
1342
1556
|
* Read every network of a Pajek project file (`.paj`), each into its own sink. A network starts
|
|
1343
|
-
* at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
|
|
1557
|
+
* at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
|
|
1558
|
+
* (the `*Vertices N` line right after an object's header).
|
|
1344
1559
|
* @param input - the text, bytes or stream
|
|
1345
1560
|
* @param sinkFor - the sink of the network with this index, called before its first push
|
|
1346
1561
|
* @param options - common and Pajek options
|
|
@@ -1386,7 +1601,8 @@ export const pajekImporter: GraphImporter<PajekImportOptions> = Object.freeze({
|
|
|
1386
1601
|
/**
|
|
1387
1602
|
* Counts the networks of the rest of a project file by their headers alone, with the parser's
|
|
1388
1603
|
* rule: a network starts at `*Network` after a named or populated one, or at a `*Vertices` after a
|
|
1389
|
-
* populated one unless it
|
|
1604
|
+
* populated one unless it is the line right after a project object's header (`*Partition`,
|
|
1605
|
+
* `*Vector`, a skipped section).
|
|
1390
1606
|
*/
|
|
1391
1607
|
class NetworkCounter {
|
|
1392
1608
|
/** Networks started so far (the first header fed opens the first). */
|
|
@@ -1396,27 +1612,35 @@ class NetworkCounter {
|
|
|
1396
1612
|
|
|
1397
1613
|
private populated = false;
|
|
1398
1614
|
|
|
1399
|
-
private
|
|
1615
|
+
private afterObjectHeader = false;
|
|
1400
1616
|
|
|
1401
1617
|
/**
|
|
1402
1618
|
* Feed one line.
|
|
1403
1619
|
* @param text - the line
|
|
1404
1620
|
*/
|
|
1405
1621
|
line(text: string): void {
|
|
1622
|
+
if (isCommentOrBlank(text)) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const { afterObjectHeader } = this;
|
|
1626
|
+
this.afterObjectHeader = false;
|
|
1406
1627
|
if (!isSectionLine(text)) {
|
|
1407
1628
|
return;
|
|
1408
1629
|
}
|
|
1409
1630
|
const h = parseSectionHeader(text);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1631
|
+
if (h === null) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const { kind } = h;
|
|
1635
|
+
if (kind === "partition" || kind === "vector" || kind === "unsupported") {
|
|
1636
|
+
this.afterObjectHeader = true;
|
|
1637
|
+
} else if (kind === "network") {
|
|
1414
1638
|
if (this.count === 0 || this.named || this.populated) {
|
|
1415
1639
|
this.count++;
|
|
1416
1640
|
this.populated = false;
|
|
1417
1641
|
}
|
|
1418
1642
|
this.named = true;
|
|
1419
|
-
} else if (kind === "vertices" && !
|
|
1643
|
+
} else if (kind === "vertices" && !afterObjectHeader) {
|
|
1420
1644
|
if (this.count === 0 || this.populated) {
|
|
1421
1645
|
this.count++;
|
|
1422
1646
|
this.named = false;
|