@graphty/graph-io 0.3.40 → 0.3.42
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 -0
- package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
- package/dist/chunks/children-CA4coBi0.js.map +1 -0
- package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
- package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
- package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
- package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
- package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
- package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
- package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
- package/dist/chunks/text-DHOJXnED.js.map +1 -0
- package/dist/csv.js +323 -241
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +195 -97
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +27 -27
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +50 -27
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +86 -42
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +358 -184
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/lists.js +30 -15
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +288 -146
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +129 -80
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +78 -36
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +39 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +112 -66
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +65 -37
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +34 -24
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +17 -13
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +54 -20
- 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 +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +7 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +79 -40
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +38 -12
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/children.ts +54 -27
- package/src/common/attributes.ts +118 -49
- package/src/common/input.ts +439 -197
- package/src/common/lists.ts +29 -15
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +377 -175
- package/src/formats/csv/importer.ts +155 -84
- package/src/formats/csv/records.ts +90 -35
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/dot/tokenizer.ts +118 -68
- package/src/formats/gexf/exporter.ts +67 -36
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +44 -24
- package/src/formats/graphml/importer.ts +18 -11
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/dialect.ts +61 -22
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +88 -40
- package/src/formats/xgmml/exporter.ts +40 -12
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
- package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
- package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
- package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
- package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
- package/dist/chunks/importer-DcKB97kW.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
|
@@ -22,7 +22,14 @@
|
|
|
22
22
|
* colon is written `id:` when it has no weight so the importer does not read its tail as one.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type Column,
|
|
27
|
+
type F32Column,
|
|
28
|
+
type F64Column,
|
|
29
|
+
GraphFormatError,
|
|
30
|
+
type GraphSnapshot,
|
|
31
|
+
type NodeId,
|
|
32
|
+
} from "@graphty/graph-format";
|
|
26
33
|
|
|
27
34
|
import { DICT_SAMPLE_ROWS, DictHeuristic } from "../../common/attributes.js";
|
|
28
35
|
import { type PairFolding, pairFolding } from "../../common/direction.js";
|
|
@@ -371,16 +378,6 @@ function cellText(column: Column, row: number): string | null {
|
|
|
371
378
|
}
|
|
372
379
|
}
|
|
373
380
|
|
|
374
|
-
/**
|
|
375
|
-
* Build the plan of an export: resolve options, choose the rows and columns, and collect the
|
|
376
|
-
* CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
|
|
377
|
-
* every written cell and are collected for check() only; export() needs the plan and the fatal
|
|
378
|
-
* id-collision note alone.
|
|
379
|
-
* @param snapshot - the snapshot
|
|
380
|
-
* @param options - the caller's options
|
|
381
|
-
* @param values - whether to scan the values for notes
|
|
382
|
-
* @returns the plan
|
|
383
|
-
*/
|
|
384
381
|
/**
|
|
385
382
|
* W_CSV_NODE_TABLE: the node columns an edge-table or adjacency export leaves for a second, node-table export.
|
|
386
383
|
* @param names - the node columns, label first
|
|
@@ -407,6 +404,25 @@ function nodeTableNote(names: readonly string[], table: string, notes: LossNote[
|
|
|
407
404
|
}
|
|
408
405
|
}
|
|
409
406
|
|
|
407
|
+
/**
|
|
408
|
+
* The number of node ids whose text an earlier id already has (a number and a string).
|
|
409
|
+
* @param ids - the snapshot's ids
|
|
410
|
+
* @returns the collisions
|
|
411
|
+
*/
|
|
412
|
+
function idTextCollisions(ids: GraphSnapshot["ids"]): number {
|
|
413
|
+
const seen = new Set<string>();
|
|
414
|
+
let collisions = 0;
|
|
415
|
+
for (let i = 0; i < ids.size; i++) {
|
|
416
|
+
const text = String(ids.idOf(i));
|
|
417
|
+
if (seen.has(text)) {
|
|
418
|
+
collisions++;
|
|
419
|
+
} else {
|
|
420
|
+
seen.add(text);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return collisions;
|
|
424
|
+
}
|
|
425
|
+
|
|
410
426
|
/**
|
|
411
427
|
* The id notes: text collisions are fatal (export() throws E_INVALID_ID); type changes under the
|
|
412
428
|
* canonical re-read are reported.
|
|
@@ -421,16 +437,7 @@ function idNotes(
|
|
|
421
437
|
): void {
|
|
422
438
|
const { ids } = snapshot;
|
|
423
439
|
if (ids.kind === "mixed") {
|
|
424
|
-
const
|
|
425
|
-
let collisions = 0;
|
|
426
|
-
for (let i = 0; i < ids.size; i++) {
|
|
427
|
-
const text = String(ids.idOf(i));
|
|
428
|
-
if (seen.has(text)) {
|
|
429
|
-
collisions++;
|
|
430
|
-
} else {
|
|
431
|
-
seen.add(text);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
440
|
+
const collisions = idTextCollisions(ids);
|
|
434
441
|
if (collisions > 0) {
|
|
435
442
|
note(
|
|
436
443
|
CSV_LOSS.ID_TEXT_COLLISION,
|
|
@@ -459,6 +466,16 @@ function idNotes(
|
|
|
459
466
|
}
|
|
460
467
|
}
|
|
461
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Build the plan of an export: resolve options, choose the rows and columns, and collect the
|
|
471
|
+
* CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
|
|
472
|
+
* every written cell and are collected for check() only; export() needs the plan and the fatal
|
|
473
|
+
* id-collision note alone.
|
|
474
|
+
* @param snapshot - the snapshot
|
|
475
|
+
* @param options - the caller's options
|
|
476
|
+
* @param values - whether to scan the values for notes
|
|
477
|
+
* @returns the plan
|
|
478
|
+
*/
|
|
462
479
|
function planExport(
|
|
463
480
|
snapshot: GraphSnapshot,
|
|
464
481
|
options: (CsvExportOptions & CommonExportOptions) | undefined,
|
|
@@ -482,7 +499,66 @@ function planExport(
|
|
|
482
499
|
edgeRows.push(e);
|
|
483
500
|
}
|
|
484
501
|
}
|
|
485
|
-
|
|
502
|
+
directionNotes(snapshot, csv, edgeRows.length, note);
|
|
503
|
+
if (folding.mutualCount > 0 && csv.table !== "nodes") {
|
|
504
|
+
note(
|
|
505
|
+
CSV_LOSS.MUTUAL_EXPANDED,
|
|
506
|
+
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
|
|
507
|
+
null,
|
|
508
|
+
folding.mutualCount,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (csv.table === "edges") {
|
|
512
|
+
noteNodeCoverage(snapshot, edgeRows, note);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// weights
|
|
516
|
+
const weights = explicitWeights(snapshot);
|
|
517
|
+
|
|
518
|
+
const { edgeId, edgeLabel, edgeColumns } = planEdgeColumns(snapshot, csv, edgeRows, values, note);
|
|
519
|
+
const { nodeLabel, nodeColumns } = planNodeColumns(snapshot, csv, values, note);
|
|
520
|
+
if (csv.table !== "nodes") {
|
|
521
|
+
nodeTableNote(
|
|
522
|
+
[...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
|
|
523
|
+
csv.table,
|
|
524
|
+
notes,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
csv,
|
|
530
|
+
common,
|
|
531
|
+
notes,
|
|
532
|
+
edgeRows,
|
|
533
|
+
folding,
|
|
534
|
+
edgeId,
|
|
535
|
+
edgeLabel,
|
|
536
|
+
weights,
|
|
537
|
+
edgeColumns,
|
|
538
|
+
nodeLabel,
|
|
539
|
+
nodeColumns,
|
|
540
|
+
idText,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Records one loss note. */
|
|
545
|
+
type NoteFn = (code: string, message: string, column?: string | null, count?: number | null) => void;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* W_CSV_DIRECTION_DROPPED: a table with no direction column (the generic dialect, an adjacency
|
|
549
|
+
* table) loses the direction of undirected edges, and the Gephi edge table of an undirected graph
|
|
550
|
+
* without edges has no Type cell to carry it. A node table holds no edge, so it gets no note.
|
|
551
|
+
* @param snapshot - the snapshot
|
|
552
|
+
* @param csv - the resolved CSV options
|
|
553
|
+
* @param edgeRowCount - how many edge rows are written
|
|
554
|
+
* @param note - the recorder
|
|
555
|
+
*/
|
|
556
|
+
function directionNotes(
|
|
557
|
+
snapshot: GraphSnapshot,
|
|
558
|
+
csv: ResolvedCsvExportOptions,
|
|
559
|
+
edgeRowCount: number,
|
|
560
|
+
note: NoteFn,
|
|
561
|
+
): void {
|
|
486
562
|
if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
|
|
487
563
|
const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
|
|
488
564
|
const mixed = countMixedEdges(snapshot);
|
|
@@ -501,7 +577,7 @@ function planExport(
|
|
|
501
577
|
mixed,
|
|
502
578
|
);
|
|
503
579
|
}
|
|
504
|
-
} else if (csv.table === "edges" && !snapshot.directed &&
|
|
580
|
+
} else if (csv.table === "edges" && !snapshot.directed && edgeRowCount === 0) {
|
|
505
581
|
note(
|
|
506
582
|
CSV_LOSS.DIRECTION_DROPPED,
|
|
507
583
|
"the direction lives in the Type cell of each edge row; an undirected graph without edges reads back as directed",
|
|
@@ -509,22 +585,25 @@ function planExport(
|
|
|
509
585
|
0,
|
|
510
586
|
);
|
|
511
587
|
}
|
|
512
|
-
|
|
513
|
-
note(
|
|
514
|
-
CSV_LOSS.MUTUAL_EXPANDED,
|
|
515
|
-
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
|
|
516
|
-
null,
|
|
517
|
-
folding.mutualCount,
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
if (csv.table === "edges") {
|
|
521
|
-
noteNodeCoverage(snapshot, edgeRows, note);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// weights
|
|
525
|
-
const weights = explicitWeights(snapshot);
|
|
588
|
+
}
|
|
526
589
|
|
|
527
|
-
|
|
590
|
+
/**
|
|
591
|
+
* The edge table's columns, with their notes: the attribute columns' names (edge table only), the
|
|
592
|
+
* columns an adjacency table drops, and the values of every column the edge table writes.
|
|
593
|
+
* @param snapshot - the snapshot
|
|
594
|
+
* @param csv - the resolved CSV options
|
|
595
|
+
* @param edgeRows - the edge rows written
|
|
596
|
+
* @param values - whether the values are inspected
|
|
597
|
+
* @param note - the recorder
|
|
598
|
+
* @returns the id and label role columns and the attribute columns
|
|
599
|
+
*/
|
|
600
|
+
function planEdgeColumns(
|
|
601
|
+
snapshot: GraphSnapshot,
|
|
602
|
+
csv: ResolvedCsvExportOptions,
|
|
603
|
+
edgeRows: readonly number[],
|
|
604
|
+
values: boolean,
|
|
605
|
+
note: NoteFn,
|
|
606
|
+
): { edgeId: Column | null; edgeLabel: Column | null; edgeColumns: ColumnOut[] } {
|
|
528
607
|
const edgeId = snapshot.edges.byRole("id");
|
|
529
608
|
const edgeLabel = snapshot.edges.byRole("label");
|
|
530
609
|
const edgeReserved = new Set<string>([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
|
|
@@ -557,8 +636,23 @@ function planExport(
|
|
|
557
636
|
checkValues(column, "edge", edgeRows, note);
|
|
558
637
|
}
|
|
559
638
|
}
|
|
639
|
+
return { edgeId, edgeLabel, edgeColumns };
|
|
640
|
+
}
|
|
560
641
|
|
|
561
|
-
|
|
642
|
+
/**
|
|
643
|
+
* The node table's columns, with their notes when the node table is the one written.
|
|
644
|
+
* @param snapshot - the snapshot
|
|
645
|
+
* @param csv - the resolved CSV options
|
|
646
|
+
* @param values - whether the values are inspected
|
|
647
|
+
* @param note - the recorder
|
|
648
|
+
* @returns the label role column and the attribute columns
|
|
649
|
+
*/
|
|
650
|
+
function planNodeColumns(
|
|
651
|
+
snapshot: GraphSnapshot,
|
|
652
|
+
csv: ResolvedCsvExportOptions,
|
|
653
|
+
values: boolean,
|
|
654
|
+
note: NoteFn,
|
|
655
|
+
): { nodeLabel: Column | null; nodeColumns: ColumnOut[] } {
|
|
562
656
|
const nodeLabel = snapshot.nodes.byRole("label");
|
|
563
657
|
const nodeReserved = new Set<string>([lower(csv.dialect.id)]);
|
|
564
658
|
const nodeRows: number[] = [];
|
|
@@ -575,35 +669,13 @@ function planExport(
|
|
|
575
669
|
nodeLabel,
|
|
576
670
|
csv.table === "nodes" ? note : null,
|
|
577
671
|
);
|
|
578
|
-
if (csv.table === "nodes") {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
checkValues(column, "node", nodeRows, note);
|
|
583
|
-
}
|
|
672
|
+
if (csv.table === "nodes" && values) {
|
|
673
|
+
checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
|
|
674
|
+
for (const { column } of nodeColumns) {
|
|
675
|
+
checkValues(column, "node", nodeRows, note);
|
|
584
676
|
}
|
|
585
|
-
} else {
|
|
586
|
-
nodeTableNote(
|
|
587
|
-
[...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
|
|
588
|
-
csv.table,
|
|
589
|
-
notes,
|
|
590
|
-
);
|
|
591
677
|
}
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
csv,
|
|
595
|
-
common,
|
|
596
|
-
notes,
|
|
597
|
-
edgeRows,
|
|
598
|
-
folding,
|
|
599
|
-
edgeId,
|
|
600
|
-
edgeLabel,
|
|
601
|
-
weights,
|
|
602
|
-
edgeColumns,
|
|
603
|
-
nodeLabel,
|
|
604
|
-
nodeColumns,
|
|
605
|
-
idText,
|
|
606
|
-
};
|
|
678
|
+
return { nodeLabel, nodeColumns };
|
|
607
679
|
}
|
|
608
680
|
|
|
609
681
|
/**
|
|
@@ -699,40 +771,81 @@ function attributeColumns(
|
|
|
699
771
|
if (role !== null && SKIPPED_ROLES.has(role)) {
|
|
700
772
|
continue;
|
|
701
773
|
}
|
|
702
|
-
const low = lower(name);
|
|
703
774
|
const set = column.length - column.nullCount;
|
|
704
|
-
|
|
705
|
-
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
706
|
-
note?.(
|
|
707
|
-
CSV_LOSS.RESERVED_NAME,
|
|
708
|
-
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
709
|
-
name,
|
|
710
|
-
set,
|
|
711
|
-
);
|
|
775
|
+
if (isReservedName(name, domain, reserved, idColumn, set, note)) {
|
|
712
776
|
continue;
|
|
713
777
|
}
|
|
714
|
-
if (findColumn([name], LABEL_NAMES) >= 0) {
|
|
715
|
-
|
|
716
|
-
note?.(
|
|
717
|
-
CSV_LOSS.RESERVED_NAME,
|
|
718
|
-
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
719
|
-
name,
|
|
720
|
-
set,
|
|
721
|
-
);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
note?.(
|
|
725
|
-
CSV_LOSS.ROLE_ASSUMED,
|
|
726
|
-
`${domain} column "${name}" reads back with the label role (string)`,
|
|
727
|
-
name,
|
|
728
|
-
set,
|
|
729
|
-
);
|
|
778
|
+
if (findColumn([name], LABEL_NAMES) >= 0 && !labelNameWritten(name, domain, labelColumn, set, note)) {
|
|
779
|
+
continue;
|
|
730
780
|
}
|
|
731
781
|
out.push({ column, header: name });
|
|
732
782
|
}
|
|
733
783
|
return out;
|
|
734
784
|
}
|
|
735
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Whether an attribute column's name is reserved for a structural column (a dialect header, an
|
|
788
|
+
* edge id name, the id column's name), with its note.
|
|
789
|
+
* @param name - the column name
|
|
790
|
+
* @param domain - node or edge, for messages
|
|
791
|
+
* @param reserved - lower-case header names the dialect writes for structure
|
|
792
|
+
* @param idColumn - the table's id role column, or null
|
|
793
|
+
* @param set - the column's set cells
|
|
794
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
795
|
+
* @returns true when the column is not written
|
|
796
|
+
*/
|
|
797
|
+
function isReservedName(
|
|
798
|
+
name: string,
|
|
799
|
+
domain: "node" | "edge",
|
|
800
|
+
reserved: ReadonlySet<string>,
|
|
801
|
+
idColumn: Column | null,
|
|
802
|
+
set: number,
|
|
803
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
804
|
+
): boolean {
|
|
805
|
+
const low = lower(name);
|
|
806
|
+
const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
|
|
807
|
+
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
808
|
+
note?.(
|
|
809
|
+
CSV_LOSS.RESERVED_NAME,
|
|
810
|
+
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
811
|
+
name,
|
|
812
|
+
set,
|
|
813
|
+
);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Whether an attribute column with a label header name is written, with its note: not when the
|
|
821
|
+
* table has a label column (the name is reserved for it), else it reads back with the label role.
|
|
822
|
+
* @param name - the column name
|
|
823
|
+
* @param domain - node or edge, for messages
|
|
824
|
+
* @param labelColumn - the table's label role column, or null
|
|
825
|
+
* @param set - the column's set cells
|
|
826
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
827
|
+
* @returns true when the column is written
|
|
828
|
+
*/
|
|
829
|
+
function labelNameWritten(
|
|
830
|
+
name: string,
|
|
831
|
+
domain: "node" | "edge",
|
|
832
|
+
labelColumn: Column | null,
|
|
833
|
+
set: number,
|
|
834
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
835
|
+
): boolean {
|
|
836
|
+
if (labelColumn !== null) {
|
|
837
|
+
note?.(
|
|
838
|
+
CSV_LOSS.RESERVED_NAME,
|
|
839
|
+
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
840
|
+
name,
|
|
841
|
+
set,
|
|
842
|
+
);
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
|
|
736
849
|
/**
|
|
737
850
|
* Notes about a role column (id or label): a name the importer does not map to the role, and a
|
|
738
851
|
* dtype other than string / dict (read back as string).
|
|
@@ -808,26 +921,13 @@ function checkValues(
|
|
|
808
921
|
column: Column,
|
|
809
922
|
domain: "node" | "edge",
|
|
810
923
|
rows: readonly number[],
|
|
811
|
-
note:
|
|
924
|
+
note: NoteFn,
|
|
812
925
|
roleColumn = false,
|
|
813
926
|
): void {
|
|
814
927
|
const { name, dtype } = column.meta;
|
|
815
928
|
const label = `${domain} column "${name}"`;
|
|
816
929
|
if (column.dtype === "f32" || column.dtype === "f64") {
|
|
817
|
-
|
|
818
|
-
const { components } = column.meta;
|
|
819
|
-
const { data } = column;
|
|
820
|
-
for (const row of rows) {
|
|
821
|
-
if (!column.isSet(row)) {
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
for (let k = 0; k < components; k++) {
|
|
825
|
-
if (!Number.isFinite(data[row * components + k])) {
|
|
826
|
-
nonFinite++;
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
930
|
+
const nonFinite = countNonFinite(column, rows);
|
|
831
931
|
if (nonFinite > 0) {
|
|
832
932
|
note(
|
|
833
933
|
CSV_LOSS.NONFINITE,
|
|
@@ -841,15 +941,77 @@ function checkValues(
|
|
|
841
941
|
if (dtype !== "string" && dtype !== "dict") {
|
|
842
942
|
return;
|
|
843
943
|
}
|
|
944
|
+
const scan = scanText(column, rows);
|
|
945
|
+
if (roleColumn || scan.widest < 0) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const readsAsDict = scan.candidate && scan.heuristic.decide() === "dict";
|
|
949
|
+
if (scan.widest < WIDENING.string && !readsAsDict) {
|
|
950
|
+
note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[scan.widest]}`, name, scan.set);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (dtype === "dict" && !readsAsDict) {
|
|
954
|
+
note(
|
|
955
|
+
CSV_LOSS.STORAGE_CLASS_CHANGED,
|
|
956
|
+
`${label}: reads back as string (cardinality too high for a dict)`,
|
|
957
|
+
name,
|
|
958
|
+
null,
|
|
959
|
+
);
|
|
960
|
+
} else if (dtype === "string" && readsAsDict) {
|
|
961
|
+
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* The rows of a float column holding at least one non-finite component.
|
|
967
|
+
* @param column - the f32 or f64 column
|
|
968
|
+
* @param rows - the rows written
|
|
969
|
+
* @returns how many rows hold one
|
|
970
|
+
*/
|
|
971
|
+
function countNonFinite(column: F32Column | F64Column, rows: readonly number[]): number {
|
|
972
|
+
let nonFinite = 0;
|
|
973
|
+
const { components } = column.meta;
|
|
974
|
+
const { data } = column;
|
|
975
|
+
for (const row of rows) {
|
|
976
|
+
if (!column.isSet(row)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
for (let k = 0; k < components; k++) {
|
|
980
|
+
if (!Number.isFinite(data[row * components + k])) {
|
|
981
|
+
nonFinite++;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return nonFinite;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** What a text column's written values read back as. */
|
|
990
|
+
interface TextScan {
|
|
991
|
+
/** The widest dtype the values infer to (WIDENING), -1 when no value is set. */
|
|
992
|
+
readonly widest: number;
|
|
993
|
+
/** Whether every value the dict heuristic sampled infers to string. */
|
|
994
|
+
readonly candidate: boolean;
|
|
995
|
+
/** How many rows hold a string. */
|
|
996
|
+
readonly set: number;
|
|
997
|
+
/** The dict heuristic over the sampled values. */
|
|
998
|
+
readonly heuristic: DictHeuristic;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Infer the dtype every written value of a text column reads back as, and sample the values for
|
|
1003
|
+
* the dict heuristic.
|
|
1004
|
+
* @param column - the string or dict column
|
|
1005
|
+
* @param rows - the rows written
|
|
1006
|
+
* @returns the scan
|
|
1007
|
+
*/
|
|
1008
|
+
function scanText(column: Column, rows: readonly number[]): TextScan {
|
|
844
1009
|
let widest = -1;
|
|
845
1010
|
let candidate = true;
|
|
846
1011
|
let set = 0;
|
|
847
1012
|
const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
|
|
848
1013
|
for (const row of rows) {
|
|
849
|
-
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
const text = column.value(row);
|
|
1014
|
+
const text = column.isSet(row) ? column.value(row) : null;
|
|
853
1015
|
if (typeof text !== "string") {
|
|
854
1016
|
continue;
|
|
855
1017
|
}
|
|
@@ -857,30 +1019,11 @@ function checkValues(
|
|
|
857
1019
|
const kind = inferTextDtype(text);
|
|
858
1020
|
widest = Math.max(widest, WIDENING[kind]);
|
|
859
1021
|
if (!heuristic.decided) {
|
|
860
|
-
|
|
861
|
-
candidate = false;
|
|
862
|
-
}
|
|
1022
|
+
candidate &&= kind === "string";
|
|
863
1023
|
heuristic.observe(text);
|
|
864
1024
|
}
|
|
865
1025
|
}
|
|
866
|
-
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const readsAsDict = candidate && heuristic.decide() === "dict";
|
|
870
|
-
if (widest < WIDENING.string && !readsAsDict) {
|
|
871
|
-
note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (dtype === "dict" && !readsAsDict) {
|
|
875
|
-
note(
|
|
876
|
-
CSV_LOSS.STORAGE_CLASS_CHANGED,
|
|
877
|
-
`${label}: reads back as string (cardinality too high for a dict)`,
|
|
878
|
-
name,
|
|
879
|
-
null,
|
|
880
|
-
);
|
|
881
|
-
} else if (dtype === "string" && readsAsDict) {
|
|
882
|
-
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
|
|
883
|
-
}
|
|
1026
|
+
return { widest, candidate, set, heuristic };
|
|
884
1027
|
}
|
|
885
1028
|
|
|
886
1029
|
/**
|
|
@@ -912,6 +1055,22 @@ function neighbourCell(id: string, weight: string | null): string {
|
|
|
912
1055
|
return id.includes(":") ? `${id}:` : id;
|
|
913
1056
|
}
|
|
914
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* The end of the run of consecutive edge rows that share the source of row `r`.
|
|
1060
|
+
* @param src - the edge sources by edge index
|
|
1061
|
+
* @param edgeRows - the edges in write order
|
|
1062
|
+
* @param r - the run's first row
|
|
1063
|
+
* @returns the index after the run's last row
|
|
1064
|
+
*/
|
|
1065
|
+
function sourceRunEnd(src: ArrayLike<number>, edgeRows: ArrayLike<number>, r: number): number {
|
|
1066
|
+
const source = src[edgeRows[r]];
|
|
1067
|
+
let end = r;
|
|
1068
|
+
while (end < edgeRows.length && src[edgeRows[end]] === source) {
|
|
1069
|
+
end++;
|
|
1070
|
+
}
|
|
1071
|
+
return end;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
915
1074
|
/**
|
|
916
1075
|
* The rows of the adjacency table (design section 8.5: the order survives the importer's first-
|
|
917
1076
|
* appearance numbering). Consecutive edges with one source share a row, so the edge order is kept;
|
|
@@ -953,10 +1112,7 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
|
|
|
953
1112
|
let r = 0;
|
|
954
1113
|
while (r < edgeRows.length) {
|
|
955
1114
|
const source = list.src[edgeRows[r]];
|
|
956
|
-
|
|
957
|
-
while (end < edgeRows.length && list.src[edgeRows[end]] === source) {
|
|
958
|
-
end++;
|
|
959
|
-
}
|
|
1115
|
+
const end = sourceRunEnd(list.src, edgeRows, r);
|
|
960
1116
|
// the nodes this row introduces, in the order the importer meets them; in index order from
|
|
961
1117
|
// `next` they need nothing more
|
|
962
1118
|
const fresh: number[] = [];
|
|
@@ -1000,59 +1156,79 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
|
|
|
1000
1156
|
* @returns nothing
|
|
1001
1157
|
*/
|
|
1002
1158
|
function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
|
|
1003
|
-
|
|
1004
|
-
if (csv.table === "adjacency") {
|
|
1159
|
+
if (plan.csv.table === "adjacency") {
|
|
1005
1160
|
yield* adjacencyLines(snapshot, plan);
|
|
1006
|
-
|
|
1161
|
+
} else if (plan.csv.table === "nodes") {
|
|
1162
|
+
yield* nodeLines(snapshot, plan);
|
|
1163
|
+
} else {
|
|
1164
|
+
yield* edgeLines(snapshot, plan);
|
|
1007
1165
|
}
|
|
1008
|
-
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* The cell writers of a delimiter.
|
|
1170
|
+
* @param delimiter - the field delimiter
|
|
1171
|
+
* @returns `quote` for a text and `cell` for a column value (an unset value is the empty cell)
|
|
1172
|
+
*/
|
|
1173
|
+
function cellWriters(delimiter: string): {
|
|
1174
|
+
quote: (text: string) => string;
|
|
1175
|
+
cell: (column: Column, row: number) => string;
|
|
1176
|
+
} {
|
|
1009
1177
|
const quote = (text: string): string => quoteCsvCell(text, delimiter);
|
|
1010
1178
|
const cell = (column: Column, row: number): string => {
|
|
1011
1179
|
const text = cellText(column, row);
|
|
1012
1180
|
return text === null ? "" : quote(text);
|
|
1013
1181
|
};
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
cells.push(cell(column, i));
|
|
1032
|
-
}
|
|
1033
|
-
yield cells.join(delimiter) + newline;
|
|
1034
|
-
}
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const headers = [csv.dialect.source, csv.dialect.target];
|
|
1038
|
-
if (csv.dialect.type !== null) {
|
|
1039
|
-
headers.push(csv.dialect.type);
|
|
1040
|
-
}
|
|
1041
|
-
if (plan.edgeId !== null) {
|
|
1042
|
-
headers.push(plan.edgeId.meta.name);
|
|
1043
|
-
}
|
|
1044
|
-
if (plan.edgeLabel !== null) {
|
|
1045
|
-
headers.push(plan.edgeLabel.meta.name);
|
|
1046
|
-
}
|
|
1047
|
-
if (plan.weights.weighted) {
|
|
1048
|
-
headers.push(csv.dialect.weight);
|
|
1182
|
+
return { quote, cell };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* The lines of the node table: the id, the label and the attribute columns of every node.
|
|
1187
|
+
* @param snapshot - the snapshot
|
|
1188
|
+
* @param plan - the plan
|
|
1189
|
+
* @yields one line at a time
|
|
1190
|
+
* @returns nothing
|
|
1191
|
+
*/
|
|
1192
|
+
function* nodeLines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
|
|
1193
|
+
const { csv } = plan;
|
|
1194
|
+
const { delimiter, newline } = csv;
|
|
1195
|
+
const { quote, cell } = cellWriters(delimiter);
|
|
1196
|
+
const headers = [csv.dialect.id];
|
|
1197
|
+
if (plan.nodeLabel !== null) {
|
|
1198
|
+
headers.push(plan.nodeLabel.meta.name);
|
|
1049
1199
|
}
|
|
1050
|
-
for (const { header } of plan.
|
|
1200
|
+
for (const { header } of plan.nodeColumns) {
|
|
1051
1201
|
headers.push(header);
|
|
1052
1202
|
}
|
|
1053
1203
|
if (csv.header) {
|
|
1054
1204
|
yield headers.map(quote).join(delimiter) + newline;
|
|
1055
1205
|
}
|
|
1206
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1207
|
+
const cells = [quote(plan.idText(i))];
|
|
1208
|
+
if (plan.nodeLabel !== null) {
|
|
1209
|
+
cells.push(cell(plan.nodeLabel, i));
|
|
1210
|
+
}
|
|
1211
|
+
for (const { column } of plan.nodeColumns) {
|
|
1212
|
+
cells.push(cell(column, i));
|
|
1213
|
+
}
|
|
1214
|
+
yield cells.join(delimiter) + newline;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* The lines of the edge table: one row per written edge.
|
|
1220
|
+
* @param snapshot - the snapshot
|
|
1221
|
+
* @param plan - the plan
|
|
1222
|
+
* @yields one line at a time
|
|
1223
|
+
* @returns nothing
|
|
1224
|
+
*/
|
|
1225
|
+
function* edgeLines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
|
|
1226
|
+
const { csv } = plan;
|
|
1227
|
+
const { delimiter, newline } = csv;
|
|
1228
|
+
const { quote, cell } = cellWriters(delimiter);
|
|
1229
|
+
if (csv.header) {
|
|
1230
|
+
yield edgeHeaders(plan).map(quote).join(delimiter) + newline;
|
|
1231
|
+
}
|
|
1056
1232
|
const list = snapshot.edgeList();
|
|
1057
1233
|
for (const e of plan.edgeRows) {
|
|
1058
1234
|
const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
|
|
@@ -1075,6 +1251,32 @@ function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, un
|
|
|
1075
1251
|
}
|
|
1076
1252
|
}
|
|
1077
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* The header of the edge table.
|
|
1256
|
+
* @param plan - the plan
|
|
1257
|
+
* @returns the header names, unquoted
|
|
1258
|
+
*/
|
|
1259
|
+
function edgeHeaders(plan: Plan): string[] {
|
|
1260
|
+
const { dialect } = plan.csv;
|
|
1261
|
+
const headers = [dialect.source, dialect.target];
|
|
1262
|
+
if (dialect.type !== null) {
|
|
1263
|
+
headers.push(dialect.type);
|
|
1264
|
+
}
|
|
1265
|
+
if (plan.edgeId !== null) {
|
|
1266
|
+
headers.push(plan.edgeId.meta.name);
|
|
1267
|
+
}
|
|
1268
|
+
if (plan.edgeLabel !== null) {
|
|
1269
|
+
headers.push(plan.edgeLabel.meta.name);
|
|
1270
|
+
}
|
|
1271
|
+
if (plan.weights.weighted) {
|
|
1272
|
+
headers.push(dialect.weight);
|
|
1273
|
+
}
|
|
1274
|
+
for (const { header } of plan.edgeColumns) {
|
|
1275
|
+
headers.push(header);
|
|
1276
|
+
}
|
|
1277
|
+
return headers;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1078
1280
|
/**
|
|
1079
1281
|
* Plan an export and refuse it when a fatal note is present (E_INVALID_ID for id text collisions),
|
|
1080
1282
|
* before anything is written.
|