@graphty/graph-io 0.3.41 → 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-CwFQamH9.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CwFQamH9.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-Cf4Nn6tw.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-Cf4Nn6tw.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-DBb8zuNE.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-DBb8zuNE.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-DD3Lvw-F.js → export-rKLeGQMo.js} +232 -128
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-CS35R6Fg.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-CS35R6Fg.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-D8r1N2dU.js → exporter-BWwEuOPT.js} +5 -5
- package/dist/chunks/{exporter-D8r1N2dU.js.map → exporter-BWwEuOPT.js.map} +1 -1
- package/dist/chunks/{exporter-BqtmlxNr.js → exporter-CpRyHZ3H.js} +34 -21
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-NMplSnYn.js → exporter-yLxKBvLE.js} +3 -3
- package/dist/chunks/{exporter-NMplSnYn.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-S-abxJHn.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-BdWHmlmG.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-BdWHmlmG.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-r29cWnEf.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-r29cWnEf.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-BfbzWk6k.js → importer-Dz7tI8Sa.js} +5 -5
- package/dist/chunks/{importer-BfbzWk6k.js.map → importer-Dz7tI8Sa.js.map} +1 -1
- package/dist/chunks/{importer-DviEsHy4.js → importer-Ht7LNg8L.js} +4 -4
- package/dist/chunks/{importer-DviEsHy4.js.map → importer-Ht7LNg8L.js.map} +1 -1
- package/dist/chunks/{importer-BKC7AmCA.js → importer-WrGRZb0N.js} +6 -6
- package/dist/chunks/{importer-BKC7AmCA.js.map → importer-WrGRZb0N.js.map} +1 -1
- package/dist/chunks/{importer-BmaZcskq.js → importer-xZ-3suY1.js} +5 -5
- package/dist/chunks/{importer-BmaZcskq.js.map → importer-xZ-3suY1.js.map} +1 -1
- package/dist/chunks/{json-elements-DdF-etPT.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DdF-etPT.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-BC0zyPhj.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-BC0zyPhj.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-Uc3LkABx.js → records-Cu4bxYOm.js} +56 -26
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bh9SaAob.js → text-DHOJXnED.js} +2 -2
- package/dist/chunks/{text-Bh9SaAob.js.map → text-DHOJXnED.js.map} +1 -1
- package/dist/csv.js +260 -196
- 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 +2 -2
- package/dist/gml.js +3 -3
- package/dist/graph-io.js +27 -27
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +3 -3
- 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 +52 -33
- 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 +178 -82
- 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/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +211 -121
- 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 +113 -74
- 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 +60 -26
- package/dist/src/formats/csv/records.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/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/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/children.ts +54 -27
- package/src/common/attributes.ts +59 -30
- package/src/common/input.ts +223 -95
- package/src/common/lists.ts +29 -15
- package/src/formats/csv/exporter.ts +274 -136
- package/src/formats/csv/importer.ts +138 -78
- package/src/formats/csv/records.ts +71 -25
- package/src/formats/dot/tokenizer.ts +118 -68
- package/src/formats/json/dialect.ts +61 -22
- package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
- package/dist/chunks/export-DD3Lvw-F.js.map +0 -1
- package/dist/chunks/exporter-BqtmlxNr.js.map +0 -1
- package/dist/chunks/importer-S-abxJHn.js.map +0 -1
- package/dist/chunks/records-Uc3LkABx.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
|
|
@@ -469,6 +466,16 @@ function idNotes(
|
|
|
469
466
|
}
|
|
470
467
|
}
|
|
471
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
|
+
*/
|
|
472
479
|
function planExport(
|
|
473
480
|
snapshot: GraphSnapshot,
|
|
474
481
|
options: (CsvExportOptions & CommonExportOptions) | undefined,
|
|
@@ -492,7 +499,66 @@ function planExport(
|
|
|
492
499
|
edgeRows.push(e);
|
|
493
500
|
}
|
|
494
501
|
}
|
|
495
|
-
|
|
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 {
|
|
496
562
|
if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
|
|
497
563
|
const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
|
|
498
564
|
const mixed = countMixedEdges(snapshot);
|
|
@@ -511,7 +577,7 @@ function planExport(
|
|
|
511
577
|
mixed,
|
|
512
578
|
);
|
|
513
579
|
}
|
|
514
|
-
} else if (csv.table === "edges" && !snapshot.directed &&
|
|
580
|
+
} else if (csv.table === "edges" && !snapshot.directed && edgeRowCount === 0) {
|
|
515
581
|
note(
|
|
516
582
|
CSV_LOSS.DIRECTION_DROPPED,
|
|
517
583
|
"the direction lives in the Type cell of each edge row; an undirected graph without edges reads back as directed",
|
|
@@ -519,22 +585,25 @@ function planExport(
|
|
|
519
585
|
0,
|
|
520
586
|
);
|
|
521
587
|
}
|
|
522
|
-
|
|
523
|
-
note(
|
|
524
|
-
CSV_LOSS.MUTUAL_EXPANDED,
|
|
525
|
-
`${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`,
|
|
526
|
-
null,
|
|
527
|
-
folding.mutualCount,
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
if (csv.table === "edges") {
|
|
531
|
-
noteNodeCoverage(snapshot, edgeRows, note);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// weights
|
|
535
|
-
const weights = explicitWeights(snapshot);
|
|
588
|
+
}
|
|
536
589
|
|
|
537
|
-
|
|
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[] } {
|
|
538
607
|
const edgeId = snapshot.edges.byRole("id");
|
|
539
608
|
const edgeLabel = snapshot.edges.byRole("label");
|
|
540
609
|
const edgeReserved = new Set<string>([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
|
|
@@ -567,8 +636,23 @@ function planExport(
|
|
|
567
636
|
checkValues(column, "edge", edgeRows, note);
|
|
568
637
|
}
|
|
569
638
|
}
|
|
639
|
+
return { edgeId, edgeLabel, edgeColumns };
|
|
640
|
+
}
|
|
570
641
|
|
|
571
|
-
|
|
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[] } {
|
|
572
656
|
const nodeLabel = snapshot.nodes.byRole("label");
|
|
573
657
|
const nodeReserved = new Set<string>([lower(csv.dialect.id)]);
|
|
574
658
|
const nodeRows: number[] = [];
|
|
@@ -585,35 +669,13 @@ function planExport(
|
|
|
585
669
|
nodeLabel,
|
|
586
670
|
csv.table === "nodes" ? note : null,
|
|
587
671
|
);
|
|
588
|
-
if (csv.table === "nodes") {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
checkValues(column, "node", nodeRows, note);
|
|
593
|
-
}
|
|
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);
|
|
594
676
|
}
|
|
595
|
-
} else {
|
|
596
|
-
nodeTableNote(
|
|
597
|
-
[...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)],
|
|
598
|
-
csv.table,
|
|
599
|
-
notes,
|
|
600
|
-
);
|
|
601
677
|
}
|
|
602
|
-
|
|
603
|
-
return {
|
|
604
|
-
csv,
|
|
605
|
-
common,
|
|
606
|
-
notes,
|
|
607
|
-
edgeRows,
|
|
608
|
-
folding,
|
|
609
|
-
edgeId,
|
|
610
|
-
edgeLabel,
|
|
611
|
-
weights,
|
|
612
|
-
edgeColumns,
|
|
613
|
-
nodeLabel,
|
|
614
|
-
nodeColumns,
|
|
615
|
-
idText,
|
|
616
|
-
};
|
|
678
|
+
return { nodeLabel, nodeColumns };
|
|
617
679
|
}
|
|
618
680
|
|
|
619
681
|
/**
|
|
@@ -859,26 +921,13 @@ function checkValues(
|
|
|
859
921
|
column: Column,
|
|
860
922
|
domain: "node" | "edge",
|
|
861
923
|
rows: readonly number[],
|
|
862
|
-
note:
|
|
924
|
+
note: NoteFn,
|
|
863
925
|
roleColumn = false,
|
|
864
926
|
): void {
|
|
865
927
|
const { name, dtype } = column.meta;
|
|
866
928
|
const label = `${domain} column "${name}"`;
|
|
867
929
|
if (column.dtype === "f32" || column.dtype === "f64") {
|
|
868
|
-
|
|
869
|
-
const { components } = column.meta;
|
|
870
|
-
const { data } = column;
|
|
871
|
-
for (const row of rows) {
|
|
872
|
-
if (!column.isSet(row)) {
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
for (let k = 0; k < components; k++) {
|
|
876
|
-
if (!Number.isFinite(data[row * components + k])) {
|
|
877
|
-
nonFinite++;
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
930
|
+
const nonFinite = countNonFinite(column, rows);
|
|
882
931
|
if (nonFinite > 0) {
|
|
883
932
|
note(
|
|
884
933
|
CSV_LOSS.NONFINITE,
|
|
@@ -892,15 +941,77 @@ function checkValues(
|
|
|
892
941
|
if (dtype !== "string" && dtype !== "dict") {
|
|
893
942
|
return;
|
|
894
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 {
|
|
895
1009
|
let widest = -1;
|
|
896
1010
|
let candidate = true;
|
|
897
1011
|
let set = 0;
|
|
898
1012
|
const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
|
|
899
1013
|
for (const row of rows) {
|
|
900
|
-
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
903
|
-
const text = column.value(row);
|
|
1014
|
+
const text = column.isSet(row) ? column.value(row) : null;
|
|
904
1015
|
if (typeof text !== "string") {
|
|
905
1016
|
continue;
|
|
906
1017
|
}
|
|
@@ -908,30 +1019,11 @@ function checkValues(
|
|
|
908
1019
|
const kind = inferTextDtype(text);
|
|
909
1020
|
widest = Math.max(widest, WIDENING[kind]);
|
|
910
1021
|
if (!heuristic.decided) {
|
|
911
|
-
|
|
912
|
-
candidate = false;
|
|
913
|
-
}
|
|
1022
|
+
candidate &&= kind === "string";
|
|
914
1023
|
heuristic.observe(text);
|
|
915
1024
|
}
|
|
916
1025
|
}
|
|
917
|
-
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
const readsAsDict = candidate && heuristic.decide() === "dict";
|
|
921
|
-
if (widest < WIDENING.string && !readsAsDict) {
|
|
922
|
-
note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
if (dtype === "dict" && !readsAsDict) {
|
|
926
|
-
note(
|
|
927
|
-
CSV_LOSS.STORAGE_CLASS_CHANGED,
|
|
928
|
-
`${label}: reads back as string (cardinality too high for a dict)`,
|
|
929
|
-
name,
|
|
930
|
-
null,
|
|
931
|
-
);
|
|
932
|
-
} else if (dtype === "string" && readsAsDict) {
|
|
933
|
-
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
|
|
934
|
-
}
|
|
1026
|
+
return { widest, candidate, set, heuristic };
|
|
935
1027
|
}
|
|
936
1028
|
|
|
937
1029
|
/**
|
|
@@ -1064,59 +1156,79 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
|
|
|
1064
1156
|
* @returns nothing
|
|
1065
1157
|
*/
|
|
1066
1158
|
function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, undefined> {
|
|
1067
|
-
|
|
1068
|
-
if (csv.table === "adjacency") {
|
|
1159
|
+
if (plan.csv.table === "adjacency") {
|
|
1069
1160
|
yield* adjacencyLines(snapshot, plan);
|
|
1070
|
-
|
|
1161
|
+
} else if (plan.csv.table === "nodes") {
|
|
1162
|
+
yield* nodeLines(snapshot, plan);
|
|
1163
|
+
} else {
|
|
1164
|
+
yield* edgeLines(snapshot, plan);
|
|
1071
1165
|
}
|
|
1072
|
-
|
|
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
|
+
} {
|
|
1073
1177
|
const quote = (text: string): string => quoteCsvCell(text, delimiter);
|
|
1074
1178
|
const cell = (column: Column, row: number): string => {
|
|
1075
1179
|
const text = cellText(column, row);
|
|
1076
1180
|
return text === null ? "" : quote(text);
|
|
1077
1181
|
};
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
cells.push(cell(column, i));
|
|
1096
|
-
}
|
|
1097
|
-
yield cells.join(delimiter) + newline;
|
|
1098
|
-
}
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const headers = [csv.dialect.source, csv.dialect.target];
|
|
1102
|
-
if (csv.dialect.type !== null) {
|
|
1103
|
-
headers.push(csv.dialect.type);
|
|
1104
|
-
}
|
|
1105
|
-
if (plan.edgeId !== null) {
|
|
1106
|
-
headers.push(plan.edgeId.meta.name);
|
|
1107
|
-
}
|
|
1108
|
-
if (plan.edgeLabel !== null) {
|
|
1109
|
-
headers.push(plan.edgeLabel.meta.name);
|
|
1110
|
-
}
|
|
1111
|
-
if (plan.weights.weighted) {
|
|
1112
|
-
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);
|
|
1113
1199
|
}
|
|
1114
|
-
for (const { header } of plan.
|
|
1200
|
+
for (const { header } of plan.nodeColumns) {
|
|
1115
1201
|
headers.push(header);
|
|
1116
1202
|
}
|
|
1117
1203
|
if (csv.header) {
|
|
1118
1204
|
yield headers.map(quote).join(delimiter) + newline;
|
|
1119
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
|
+
}
|
|
1120
1232
|
const list = snapshot.edgeList();
|
|
1121
1233
|
for (const e of plan.edgeRows) {
|
|
1122
1234
|
const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
|
|
@@ -1139,6 +1251,32 @@ function* lines(snapshot: GraphSnapshot, plan: Plan): Generator<string, void, un
|
|
|
1139
1251
|
}
|
|
1140
1252
|
}
|
|
1141
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
|
+
|
|
1142
1280
|
/**
|
|
1143
1281
|
* Plan an export and refuse it when a fatal note is present (E_INVALID_ID for id text collisions),
|
|
1144
1282
|
* before anything is written.
|