@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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* otherwise introduce out of index order gets a row of its own first, and a neighbor id holding a
|
|
22
22
|
* colon is written `id:` when it has no weight so the importer does not read its tail as one.
|
|
23
23
|
*/
|
|
24
|
-
import { GraphFormatError } from "@graphty/graph-format";
|
|
24
|
+
import { GraphFormatError, } from "@graphty/graph-format";
|
|
25
25
|
import { DICT_SAMPLE_ROWS, DictHeuristic } from "../../common/attributes.js";
|
|
26
26
|
import { pairFolding } from "../../common/direction.js";
|
|
27
27
|
import { quoteCsvCell } from "../../common/escape.js";
|
|
@@ -261,16 +261,6 @@ function cellText(column, row) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
/**
|
|
265
|
-
* Build the plan of an export: resolve options, choose the rows and columns, and collect the
|
|
266
|
-
* CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
|
|
267
|
-
* every written cell and are collected for check() only; export() needs the plan and the fatal
|
|
268
|
-
* id-collision note alone.
|
|
269
|
-
* @param snapshot - the snapshot
|
|
270
|
-
* @param options - the caller's options
|
|
271
|
-
* @param values - whether to scan the values for notes
|
|
272
|
-
* @returns the plan
|
|
273
|
-
*/
|
|
274
264
|
/**
|
|
275
265
|
* W_CSV_NODE_TABLE: the node columns an edge-table or adjacency export leaves for a second, node-table export.
|
|
276
266
|
* @param names - the node columns, label first
|
|
@@ -293,6 +283,25 @@ function nodeTableNote(names, table, notes) {
|
|
|
293
283
|
}));
|
|
294
284
|
}
|
|
295
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The number of node ids whose text an earlier id already has (a number and a string).
|
|
288
|
+
* @param ids - the snapshot's ids
|
|
289
|
+
* @returns the collisions
|
|
290
|
+
*/
|
|
291
|
+
function idTextCollisions(ids) {
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
let collisions = 0;
|
|
294
|
+
for (let i = 0; i < ids.size; i++) {
|
|
295
|
+
const text = String(ids.idOf(i));
|
|
296
|
+
if (seen.has(text)) {
|
|
297
|
+
collisions++;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
seen.add(text);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return collisions;
|
|
304
|
+
}
|
|
296
305
|
/**
|
|
297
306
|
* The id notes: text collisions are fatal (export() throws E_INVALID_ID); type changes under the
|
|
298
307
|
* canonical re-read are reported.
|
|
@@ -303,17 +312,7 @@ function nodeTableNote(names, table, notes) {
|
|
|
303
312
|
function idNotes(snapshot, values, note) {
|
|
304
313
|
const { ids } = snapshot;
|
|
305
314
|
if (ids.kind === "mixed") {
|
|
306
|
-
const
|
|
307
|
-
let collisions = 0;
|
|
308
|
-
for (let i = 0; i < ids.size; i++) {
|
|
309
|
-
const text = String(ids.idOf(i));
|
|
310
|
-
if (seen.has(text)) {
|
|
311
|
-
collisions++;
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
seen.add(text);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
315
|
+
const collisions = idTextCollisions(ids);
|
|
317
316
|
if (collisions > 0) {
|
|
318
317
|
note(CSV_LOSS.ID_TEXT_COLLISION, `${collisions} node id${plural(collisions)} ${agree(collisions, "shares", "share")} their text with another id (a number and a string); the save fails with E_INVALID_ID`, null, collisions);
|
|
319
318
|
}
|
|
@@ -331,6 +330,16 @@ function idNotes(snapshot, values, note) {
|
|
|
331
330
|
}
|
|
332
331
|
}
|
|
333
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Build the plan of an export: resolve options, choose the rows and columns, and collect the
|
|
335
|
+
* CSV-specific loss notes (check() adds the generic ones). The value-level notes need a pass over
|
|
336
|
+
* every written cell and are collected for check() only; export() needs the plan and the fatal
|
|
337
|
+
* id-collision note alone.
|
|
338
|
+
* @param snapshot - the snapshot
|
|
339
|
+
* @param options - the caller's options
|
|
340
|
+
* @param values - whether to scan the values for notes
|
|
341
|
+
* @returns the plan
|
|
342
|
+
*/
|
|
334
343
|
function planExport(snapshot, options, values) {
|
|
335
344
|
const common = resolveExportOptions(options);
|
|
336
345
|
const csv = resolveCsvExportOptions(options);
|
|
@@ -349,7 +358,45 @@ function planExport(snapshot, options, values) {
|
|
|
349
358
|
edgeRows.push(e);
|
|
350
359
|
}
|
|
351
360
|
}
|
|
352
|
-
|
|
361
|
+
directionNotes(snapshot, csv, edgeRows.length, note);
|
|
362
|
+
if (folding.mutualCount > 0 && csv.table !== "nodes") {
|
|
363
|
+
note(CSV_LOSS.MUTUAL_EXPANDED, `${folding.mutualCount} mutual pair${plural(folding.mutualCount)} ${agree(folding.mutualCount, "is", "are")} written as two directed rows; the mutual mark is lost`, null, folding.mutualCount);
|
|
364
|
+
}
|
|
365
|
+
if (csv.table === "edges") {
|
|
366
|
+
noteNodeCoverage(snapshot, edgeRows, note);
|
|
367
|
+
}
|
|
368
|
+
// weights
|
|
369
|
+
const weights = explicitWeights(snapshot);
|
|
370
|
+
const { edgeId, edgeLabel, edgeColumns } = planEdgeColumns(snapshot, csv, edgeRows, values, note);
|
|
371
|
+
const { nodeLabel, nodeColumns } = planNodeColumns(snapshot, csv, values, note);
|
|
372
|
+
if (csv.table !== "nodes") {
|
|
373
|
+
nodeTableNote([...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)], csv.table, notes);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
csv,
|
|
377
|
+
common,
|
|
378
|
+
notes,
|
|
379
|
+
edgeRows,
|
|
380
|
+
folding,
|
|
381
|
+
edgeId,
|
|
382
|
+
edgeLabel,
|
|
383
|
+
weights,
|
|
384
|
+
edgeColumns,
|
|
385
|
+
nodeLabel,
|
|
386
|
+
nodeColumns,
|
|
387
|
+
idText,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* W_CSV_DIRECTION_DROPPED: a table with no direction column (the generic dialect, an adjacency
|
|
392
|
+
* table) loses the direction of undirected edges, and the Gephi edge table of an undirected graph
|
|
393
|
+
* without edges has no Type cell to carry it. A node table holds no edge, so it gets no note.
|
|
394
|
+
* @param snapshot - the snapshot
|
|
395
|
+
* @param csv - the resolved CSV options
|
|
396
|
+
* @param edgeRowCount - how many edge rows are written
|
|
397
|
+
* @param note - the recorder
|
|
398
|
+
*/
|
|
399
|
+
function directionNotes(snapshot, csv, edgeRowCount, note) {
|
|
353
400
|
if (csv.table !== "nodes" && (csv.dialect.type === null || csv.table === "adjacency")) {
|
|
354
401
|
const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
|
|
355
402
|
const mixed = countMixedEdges(snapshot);
|
|
@@ -360,18 +407,21 @@ function planExport(snapshot, options, values) {
|
|
|
360
407
|
note(CSV_LOSS.DIRECTION_DROPPED, `${where} has no direction column; ${mixed} undirected edge${plural(mixed)} of a mixed graph read back as one directed edge each`, null, mixed);
|
|
361
408
|
}
|
|
362
409
|
}
|
|
363
|
-
else if (csv.table === "edges" && !snapshot.directed &&
|
|
410
|
+
else if (csv.table === "edges" && !snapshot.directed && edgeRowCount === 0) {
|
|
364
411
|
note(CSV_LOSS.DIRECTION_DROPPED, "the direction lives in the Type cell of each edge row; an undirected graph without edges reads back as directed", null, 0);
|
|
365
412
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The edge table's columns, with their notes: the attribute columns' names (edge table only), the
|
|
416
|
+
* columns an adjacency table drops, and the values of every column the edge table writes.
|
|
417
|
+
* @param snapshot - the snapshot
|
|
418
|
+
* @param csv - the resolved CSV options
|
|
419
|
+
* @param edgeRows - the edge rows written
|
|
420
|
+
* @param values - whether the values are inspected
|
|
421
|
+
* @param note - the recorder
|
|
422
|
+
* @returns the id and label role columns and the attribute columns
|
|
423
|
+
*/
|
|
424
|
+
function planEdgeColumns(snapshot, csv, edgeRows, values, note) {
|
|
375
425
|
const edgeId = snapshot.edges.byRole("id");
|
|
376
426
|
const edgeLabel = snapshot.edges.byRole("label");
|
|
377
427
|
const edgeReserved = new Set([csv.dialect.source, csv.dialect.target, csv.dialect.weight].map(lower));
|
|
@@ -392,7 +442,17 @@ function planExport(snapshot, options, values) {
|
|
|
392
442
|
checkValues(column, "edge", edgeRows, note);
|
|
393
443
|
}
|
|
394
444
|
}
|
|
395
|
-
|
|
445
|
+
return { edgeId, edgeLabel, edgeColumns };
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* The node table's columns, with their notes when the node table is the one written.
|
|
449
|
+
* @param snapshot - the snapshot
|
|
450
|
+
* @param csv - the resolved CSV options
|
|
451
|
+
* @param values - whether the values are inspected
|
|
452
|
+
* @param note - the recorder
|
|
453
|
+
* @returns the label role column and the attribute columns
|
|
454
|
+
*/
|
|
455
|
+
function planNodeColumns(snapshot, csv, values, note) {
|
|
396
456
|
const nodeLabel = snapshot.nodes.byRole("label");
|
|
397
457
|
const nodeReserved = new Set([lower(csv.dialect.id)]);
|
|
398
458
|
const nodeRows = [];
|
|
@@ -402,31 +462,13 @@ function planExport(snapshot, options, values) {
|
|
|
402
462
|
}
|
|
403
463
|
}
|
|
404
464
|
const nodeColumns = attributeColumns(snapshot.nodes, "node", nodeReserved, null, nodeLabel, csv.table === "nodes" ? note : null);
|
|
405
|
-
if (csv.table === "nodes") {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
checkValues(column, "node", nodeRows, note);
|
|
410
|
-
}
|
|
465
|
+
if (csv.table === "nodes" && values) {
|
|
466
|
+
checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
|
|
467
|
+
for (const { column } of nodeColumns) {
|
|
468
|
+
checkValues(column, "node", nodeRows, note);
|
|
411
469
|
}
|
|
412
470
|
}
|
|
413
|
-
|
|
414
|
-
nodeTableNote([...(nodeLabel === null ? [] : [nodeLabel.meta.name]), ...nodeColumns.map((c) => c.column.meta.name)], csv.table, notes);
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
csv,
|
|
418
|
-
common,
|
|
419
|
-
notes,
|
|
420
|
-
edgeRows,
|
|
421
|
-
folding,
|
|
422
|
-
edgeId,
|
|
423
|
-
edgeLabel,
|
|
424
|
-
weights,
|
|
425
|
-
edgeColumns,
|
|
426
|
-
nodeLabel,
|
|
427
|
-
nodeColumns,
|
|
428
|
-
idText,
|
|
429
|
-
};
|
|
471
|
+
return { nodeLabel, nodeColumns };
|
|
430
472
|
}
|
|
431
473
|
/**
|
|
432
474
|
* The edge table is not a node carrier (research note 07 section 2.6: node attributes and node
|
|
@@ -498,24 +540,55 @@ function attributeColumns(table, domain, reserved, idColumn, labelColumn, note)
|
|
|
498
540
|
if (role !== null && SKIPPED_ROLES.has(role)) {
|
|
499
541
|
continue;
|
|
500
542
|
}
|
|
501
|
-
const low = lower(name);
|
|
502
543
|
const set = column.length - column.nullCount;
|
|
503
|
-
|
|
504
|
-
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
505
|
-
note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`, name, set);
|
|
544
|
+
if (isReservedName(name, domain, reserved, idColumn, set, note)) {
|
|
506
545
|
continue;
|
|
507
546
|
}
|
|
508
|
-
if (findColumn([name], LABEL_NAMES) >= 0) {
|
|
509
|
-
|
|
510
|
-
note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the label column`, name, set);
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
|
|
547
|
+
if (findColumn([name], LABEL_NAMES) >= 0 && !labelNameWritten(name, domain, labelColumn, set, note)) {
|
|
548
|
+
continue;
|
|
514
549
|
}
|
|
515
550
|
out.push({ column, header: name });
|
|
516
551
|
}
|
|
517
552
|
return out;
|
|
518
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Whether an attribute column's name is reserved for a structural column (a dialect header, an
|
|
556
|
+
* edge id name, the id column's name), with its note.
|
|
557
|
+
* @param name - the column name
|
|
558
|
+
* @param domain - node or edge, for messages
|
|
559
|
+
* @param reserved - lower-case header names the dialect writes for structure
|
|
560
|
+
* @param idColumn - the table's id role column, or null
|
|
561
|
+
* @param set - the column's set cells
|
|
562
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
563
|
+
* @returns true when the column is not written
|
|
564
|
+
*/
|
|
565
|
+
function isReservedName(name, domain, reserved, idColumn, set, note) {
|
|
566
|
+
const low = lower(name);
|
|
567
|
+
const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
|
|
568
|
+
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
569
|
+
note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`, name, set);
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Whether an attribute column with a label header name is written, with its note: not when the
|
|
576
|
+
* table has a label column (the name is reserved for it), else it reads back with the label role.
|
|
577
|
+
* @param name - the column name
|
|
578
|
+
* @param domain - node or edge, for messages
|
|
579
|
+
* @param labelColumn - the table's label role column, or null
|
|
580
|
+
* @param set - the column's set cells
|
|
581
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
582
|
+
* @returns true when the column is written
|
|
583
|
+
*/
|
|
584
|
+
function labelNameWritten(name, domain, labelColumn, set, note) {
|
|
585
|
+
if (labelColumn !== null) {
|
|
586
|
+
note?.(CSV_LOSS.RESERVED_NAME, `${domain} column "${name}" is not written: the name is reserved for the label column`, name, set);
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
519
592
|
/**
|
|
520
593
|
* Notes about a role column (id or label): a name the importer does not map to the role, and a
|
|
521
594
|
* dtype other than string / dict (read back as string).
|
|
@@ -571,20 +644,7 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
|
|
|
571
644
|
const { name, dtype } = column.meta;
|
|
572
645
|
const label = `${domain} column "${name}"`;
|
|
573
646
|
if (column.dtype === "f32" || column.dtype === "f64") {
|
|
574
|
-
|
|
575
|
-
const { components } = column.meta;
|
|
576
|
-
const { data } = column;
|
|
577
|
-
for (const row of rows) {
|
|
578
|
-
if (!column.isSet(row)) {
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
for (let k = 0; k < components; k++) {
|
|
582
|
-
if (!Number.isFinite(data[row * components + k])) {
|
|
583
|
-
nonFinite++;
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
647
|
+
const nonFinite = countNonFinite(column, rows);
|
|
588
648
|
if (nonFinite > 0) {
|
|
589
649
|
note(CSV_LOSS.NONFINITE, `${label}: ${nonFinite} non-finite value${plural(nonFinite)} ${agree(nonFinite, "reads", "read")} back as text`, name, nonFinite);
|
|
590
650
|
}
|
|
@@ -593,15 +653,59 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
|
|
|
593
653
|
if (dtype !== "string" && dtype !== "dict") {
|
|
594
654
|
return;
|
|
595
655
|
}
|
|
656
|
+
const scan = scanText(column, rows);
|
|
657
|
+
if (roleColumn || scan.widest < 0) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const readsAsDict = scan.candidate && scan.heuristic.decide() === "dict";
|
|
661
|
+
if (scan.widest < WIDENING.string && !readsAsDict) {
|
|
662
|
+
note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[scan.widest]}`, name, scan.set);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (dtype === "dict" && !readsAsDict) {
|
|
666
|
+
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as string (cardinality too high for a dict)`, name, null);
|
|
667
|
+
}
|
|
668
|
+
else if (dtype === "string" && readsAsDict) {
|
|
669
|
+
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* The rows of a float column holding at least one non-finite component.
|
|
674
|
+
* @param column - the f32 or f64 column
|
|
675
|
+
* @param rows - the rows written
|
|
676
|
+
* @returns how many rows hold one
|
|
677
|
+
*/
|
|
678
|
+
function countNonFinite(column, rows) {
|
|
679
|
+
let nonFinite = 0;
|
|
680
|
+
const { components } = column.meta;
|
|
681
|
+
const { data } = column;
|
|
682
|
+
for (const row of rows) {
|
|
683
|
+
if (!column.isSet(row)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
for (let k = 0; k < components; k++) {
|
|
687
|
+
if (!Number.isFinite(data[row * components + k])) {
|
|
688
|
+
nonFinite++;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return nonFinite;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Infer the dtype every written value of a text column reads back as, and sample the values for
|
|
697
|
+
* the dict heuristic.
|
|
698
|
+
* @param column - the string or dict column
|
|
699
|
+
* @param rows - the rows written
|
|
700
|
+
* @returns the scan
|
|
701
|
+
*/
|
|
702
|
+
function scanText(column, rows) {
|
|
596
703
|
let widest = -1;
|
|
597
704
|
let candidate = true;
|
|
598
705
|
let set = 0;
|
|
599
706
|
const heuristic = new DictHeuristic(DICT_SAMPLE_ROWS);
|
|
600
707
|
for (const row of rows) {
|
|
601
|
-
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
const text = column.value(row);
|
|
708
|
+
const text = column.isSet(row) ? column.value(row) : null;
|
|
605
709
|
if (typeof text !== "string") {
|
|
606
710
|
continue;
|
|
607
711
|
}
|
|
@@ -609,26 +713,11 @@ function checkValues(column, domain, rows, note, roleColumn = false) {
|
|
|
609
713
|
const kind = inferTextDtype(text);
|
|
610
714
|
widest = Math.max(widest, WIDENING[kind]);
|
|
611
715
|
if (!heuristic.decided) {
|
|
612
|
-
|
|
613
|
-
candidate = false;
|
|
614
|
-
}
|
|
716
|
+
candidate && (candidate = kind === "string");
|
|
615
717
|
heuristic.observe(text);
|
|
616
718
|
}
|
|
617
719
|
}
|
|
618
|
-
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
const readsAsDict = candidate && heuristic.decide() === "dict";
|
|
622
|
-
if (widest < WIDENING.string && !readsAsDict) {
|
|
623
|
-
note(CSV_LOSS.TEXT_INFERRED, `${label}: every value reads back as ${TEXT_DTYPES[widest]}`, name, set);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
if (dtype === "dict" && !readsAsDict) {
|
|
627
|
-
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as string (cardinality too high for a dict)`, name, null);
|
|
628
|
-
}
|
|
629
|
-
else if (dtype === "string" && readsAsDict) {
|
|
630
|
-
note(CSV_LOSS.STORAGE_CLASS_CHANGED, `${label}: reads back as dict (low cardinality)`, name, null);
|
|
631
|
-
}
|
|
720
|
+
return { widest, candidate, set, heuristic };
|
|
632
721
|
}
|
|
633
722
|
/**
|
|
634
723
|
* The Type cell of an edge row.
|
|
@@ -657,6 +746,21 @@ function neighbourCell(id, weight) {
|
|
|
657
746
|
}
|
|
658
747
|
return id.includes(":") ? `${id}:` : id;
|
|
659
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* The end of the run of consecutive edge rows that share the source of row `r`.
|
|
751
|
+
* @param src - the edge sources by edge index
|
|
752
|
+
* @param edgeRows - the edges in write order
|
|
753
|
+
* @param r - the run's first row
|
|
754
|
+
* @returns the index after the run's last row
|
|
755
|
+
*/
|
|
756
|
+
function sourceRunEnd(src, edgeRows, r) {
|
|
757
|
+
const source = src[edgeRows[r]];
|
|
758
|
+
let end = r;
|
|
759
|
+
while (end < edgeRows.length && src[edgeRows[end]] === source) {
|
|
760
|
+
end++;
|
|
761
|
+
}
|
|
762
|
+
return end;
|
|
763
|
+
}
|
|
660
764
|
/**
|
|
661
765
|
* The rows of the adjacency table (design section 8.5: the order survives the importer's first-
|
|
662
766
|
* appearance numbering). Consecutive edges with one source share a row, so the edge order is kept;
|
|
@@ -697,10 +801,7 @@ function* adjacencyLines(snapshot, plan) {
|
|
|
697
801
|
let r = 0;
|
|
698
802
|
while (r < edgeRows.length) {
|
|
699
803
|
const source = list.src[edgeRows[r]];
|
|
700
|
-
|
|
701
|
-
while (end < edgeRows.length && list.src[edgeRows[end]] === source) {
|
|
702
|
-
end++;
|
|
703
|
-
}
|
|
804
|
+
const end = sourceRunEnd(list.src, edgeRows, r);
|
|
704
805
|
// the nodes this row introduces, in the order the importer meets them; in index order from
|
|
705
806
|
// `next` they need nothing more
|
|
706
807
|
const fresh = [];
|
|
@@ -743,59 +844,75 @@ function* adjacencyLines(snapshot, plan) {
|
|
|
743
844
|
* @returns nothing
|
|
744
845
|
*/
|
|
745
846
|
function* lines(snapshot, plan) {
|
|
746
|
-
|
|
747
|
-
if (csv.table === "adjacency") {
|
|
847
|
+
if (plan.csv.table === "adjacency") {
|
|
748
848
|
yield* adjacencyLines(snapshot, plan);
|
|
749
|
-
return;
|
|
750
849
|
}
|
|
751
|
-
|
|
850
|
+
else if (plan.csv.table === "nodes") {
|
|
851
|
+
yield* nodeLines(snapshot, plan);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
yield* edgeLines(snapshot, plan);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The cell writers of a delimiter.
|
|
859
|
+
* @param delimiter - the field delimiter
|
|
860
|
+
* @returns `quote` for a text and `cell` for a column value (an unset value is the empty cell)
|
|
861
|
+
*/
|
|
862
|
+
function cellWriters(delimiter) {
|
|
752
863
|
const quote = (text) => quoteCsvCell(text, delimiter);
|
|
753
864
|
const cell = (column, row) => {
|
|
754
865
|
const text = cellText(column, row);
|
|
755
866
|
return text === null ? "" : quote(text);
|
|
756
867
|
};
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
for (const { column } of plan.nodeColumns) {
|
|
774
|
-
cells.push(cell(column, i));
|
|
775
|
-
}
|
|
776
|
-
yield cells.join(delimiter) + newline;
|
|
777
|
-
}
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
const headers = [csv.dialect.source, csv.dialect.target];
|
|
781
|
-
if (csv.dialect.type !== null) {
|
|
782
|
-
headers.push(csv.dialect.type);
|
|
783
|
-
}
|
|
784
|
-
if (plan.edgeId !== null) {
|
|
785
|
-
headers.push(plan.edgeId.meta.name);
|
|
786
|
-
}
|
|
787
|
-
if (plan.edgeLabel !== null) {
|
|
788
|
-
headers.push(plan.edgeLabel.meta.name);
|
|
789
|
-
}
|
|
790
|
-
if (plan.weights.weighted) {
|
|
791
|
-
headers.push(csv.dialect.weight);
|
|
868
|
+
return { quote, cell };
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The lines of the node table: the id, the label and the attribute columns of every node.
|
|
872
|
+
* @param snapshot - the snapshot
|
|
873
|
+
* @param plan - the plan
|
|
874
|
+
* @yields one line at a time
|
|
875
|
+
* @returns nothing
|
|
876
|
+
*/
|
|
877
|
+
function* nodeLines(snapshot, plan) {
|
|
878
|
+
const { csv } = plan;
|
|
879
|
+
const { delimiter, newline } = csv;
|
|
880
|
+
const { quote, cell } = cellWriters(delimiter);
|
|
881
|
+
const headers = [csv.dialect.id];
|
|
882
|
+
if (plan.nodeLabel !== null) {
|
|
883
|
+
headers.push(plan.nodeLabel.meta.name);
|
|
792
884
|
}
|
|
793
|
-
for (const { header } of plan.
|
|
885
|
+
for (const { header } of plan.nodeColumns) {
|
|
794
886
|
headers.push(header);
|
|
795
887
|
}
|
|
796
888
|
if (csv.header) {
|
|
797
889
|
yield headers.map(quote).join(delimiter) + newline;
|
|
798
890
|
}
|
|
891
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
892
|
+
const cells = [quote(plan.idText(i))];
|
|
893
|
+
if (plan.nodeLabel !== null) {
|
|
894
|
+
cells.push(cell(plan.nodeLabel, i));
|
|
895
|
+
}
|
|
896
|
+
for (const { column } of plan.nodeColumns) {
|
|
897
|
+
cells.push(cell(column, i));
|
|
898
|
+
}
|
|
899
|
+
yield cells.join(delimiter) + newline;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The lines of the edge table: one row per written edge.
|
|
904
|
+
* @param snapshot - the snapshot
|
|
905
|
+
* @param plan - the plan
|
|
906
|
+
* @yields one line at a time
|
|
907
|
+
* @returns nothing
|
|
908
|
+
*/
|
|
909
|
+
function* edgeLines(snapshot, plan) {
|
|
910
|
+
const { csv } = plan;
|
|
911
|
+
const { delimiter, newline } = csv;
|
|
912
|
+
const { quote, cell } = cellWriters(delimiter);
|
|
913
|
+
if (csv.header) {
|
|
914
|
+
yield edgeHeaders(plan).map(quote).join(delimiter) + newline;
|
|
915
|
+
}
|
|
799
916
|
const list = snapshot.edgeList();
|
|
800
917
|
for (const e of plan.edgeRows) {
|
|
801
918
|
const cells = [quote(plan.idText(list.src[e])), quote(plan.idText(list.dst[e]))];
|
|
@@ -817,6 +934,31 @@ function* lines(snapshot, plan) {
|
|
|
817
934
|
yield cells.join(delimiter) + newline;
|
|
818
935
|
}
|
|
819
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* The header of the edge table.
|
|
939
|
+
* @param plan - the plan
|
|
940
|
+
* @returns the header names, unquoted
|
|
941
|
+
*/
|
|
942
|
+
function edgeHeaders(plan) {
|
|
943
|
+
const { dialect } = plan.csv;
|
|
944
|
+
const headers = [dialect.source, dialect.target];
|
|
945
|
+
if (dialect.type !== null) {
|
|
946
|
+
headers.push(dialect.type);
|
|
947
|
+
}
|
|
948
|
+
if (plan.edgeId !== null) {
|
|
949
|
+
headers.push(plan.edgeId.meta.name);
|
|
950
|
+
}
|
|
951
|
+
if (plan.edgeLabel !== null) {
|
|
952
|
+
headers.push(plan.edgeLabel.meta.name);
|
|
953
|
+
}
|
|
954
|
+
if (plan.weights.weighted) {
|
|
955
|
+
headers.push(dialect.weight);
|
|
956
|
+
}
|
|
957
|
+
for (const { header } of plan.edgeColumns) {
|
|
958
|
+
headers.push(header);
|
|
959
|
+
}
|
|
960
|
+
return headers;
|
|
961
|
+
}
|
|
820
962
|
/**
|
|
821
963
|
* Plan an export and refuse it when a fatal note is present (E_INVALID_ID for id text collisions),
|
|
822
964
|
* before anything is written.
|