@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.
Files changed (210) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
  3. package/dist/chunks/children-CA4coBi0.js.map +1 -0
  4. package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
  5. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
  6. package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
  7. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
  8. package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
  9. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
  10. package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
  11. package/dist/chunks/export-rKLeGQMo.js.map +1 -0
  12. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
  13. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
  14. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
  15. package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
  16. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
  17. package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
  18. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
  19. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
  20. package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
  21. package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
  22. package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
  23. package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
  24. package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
  25. package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
  26. package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
  27. package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
  28. package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
  29. package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
  30. package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
  31. package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
  32. package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
  33. package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
  34. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
  35. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
  36. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
  37. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
  38. package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
  39. package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
  40. package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
  41. package/dist/chunks/text-DHOJXnED.js.map +1 -0
  42. package/dist/csv.js +323 -241
  43. package/dist/csv.js.map +1 -1
  44. package/dist/cx.js +1 -1
  45. package/dist/cx2.js +2 -2
  46. package/dist/cys.js +1 -1
  47. package/dist/dot.js +1 -1
  48. package/dist/gexf.js +195 -97
  49. package/dist/gexf.js.map +1 -1
  50. package/dist/gml.js +170 -149
  51. package/dist/gml.js.map +1 -1
  52. package/dist/graph-io.js +27 -27
  53. package/dist/graphml.js +2 -2
  54. package/dist/json.js +1 -1
  55. package/dist/neo4j.js +103 -65
  56. package/dist/neo4j.js.map +1 -1
  57. package/dist/obo.js +1 -1
  58. package/dist/pajek.js +1 -1
  59. package/dist/src/children.d.ts.map +1 -1
  60. package/dist/src/children.js +50 -27
  61. package/dist/src/children.js.map +1 -1
  62. package/dist/src/common/attributes.d.ts.map +1 -1
  63. package/dist/src/common/attributes.js +86 -42
  64. package/dist/src/common/attributes.js.map +1 -1
  65. package/dist/src/common/input.d.ts.map +1 -1
  66. package/dist/src/common/input.js +358 -184
  67. package/dist/src/common/input.js.map +1 -1
  68. package/dist/src/common/lists.js +30 -15
  69. package/dist/src/common/lists.js.map +1 -1
  70. package/dist/src/common/text.d.ts +7 -0
  71. package/dist/src/common/text.d.ts.map +1 -1
  72. package/dist/src/common/text.js +15 -7
  73. package/dist/src/common/text.js.map +1 -1
  74. package/dist/src/common/zip.d.ts.map +1 -1
  75. package/dist/src/common/zip.js +36 -19
  76. package/dist/src/common/zip.js.map +1 -1
  77. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  78. package/dist/src/formats/csv/exporter.js +288 -146
  79. package/dist/src/formats/csv/exporter.js.map +1 -1
  80. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  81. package/dist/src/formats/csv/importer.js +129 -80
  82. package/dist/src/formats/csv/importer.js.map +1 -1
  83. package/dist/src/formats/csv/records.d.ts.map +1 -1
  84. package/dist/src/formats/csv/records.js +78 -36
  85. package/dist/src/formats/csv/records.js.map +1 -1
  86. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  87. package/dist/src/formats/cys/tables.js +11 -8
  88. package/dist/src/formats/cys/tables.js.map +1 -1
  89. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  90. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  91. package/dist/src/formats/dot/tokenizer.js +112 -66
  92. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  93. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  94. package/dist/src/formats/gexf/exporter.js +65 -37
  95. package/dist/src/formats/gexf/exporter.js.map +1 -1
  96. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/importer.js +138 -50
  98. package/dist/src/formats/gexf/importer.js.map +1 -1
  99. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  100. package/dist/src/formats/gml/exporter.js +51 -35
  101. package/dist/src/formats/gml/exporter.js.map +1 -1
  102. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  103. package/dist/src/formats/gml/importer.js +103 -58
  104. package/dist/src/formats/gml/importer.js.map +1 -1
  105. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  106. package/dist/src/formats/gml/syntax.js +17 -8
  107. package/dist/src/formats/gml/syntax.js.map +1 -1
  108. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  109. package/dist/src/formats/graphml/exporter.js +34 -24
  110. package/dist/src/formats/graphml/exporter.js.map +1 -1
  111. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/importer.js +17 -13
  113. package/dist/src/formats/graphml/importer.js.map +1 -1
  114. package/dist/src/formats/graphml/tree.js +23 -14
  115. package/dist/src/formats/graphml/tree.js.map +1 -1
  116. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  117. package/dist/src/formats/graphml/yfiles.js +22 -10
  118. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  119. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  120. package/dist/src/formats/json/dialect.js +54 -20
  121. package/dist/src/formats/json/dialect.js.map +1 -1
  122. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  123. package/dist/src/formats/json/exporter.js +38 -21
  124. package/dist/src/formats/json/exporter.js.map +1 -1
  125. package/dist/src/formats/json/importer.d.ts.map +1 -1
  126. package/dist/src/formats/json/importer.js +536 -343
  127. package/dist/src/formats/json/importer.js.map +1 -1
  128. package/dist/src/formats/json/obographs.js +29 -17
  129. package/dist/src/formats/json/obographs.js.map +1 -1
  130. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  131. package/dist/src/formats/neo4j/exporter.js +73 -43
  132. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  133. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  134. package/dist/src/formats/neo4j/header.js +37 -16
  135. package/dist/src/formats/neo4j/header.js.map +1 -1
  136. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  137. package/dist/src/formats/neo4j/importer.js +21 -11
  138. package/dist/src/formats/neo4j/importer.js.map +1 -1
  139. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  140. package/dist/src/formats/obo/exporter.js +18 -7
  141. package/dist/src/formats/obo/exporter.js.map +1 -1
  142. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  143. package/dist/src/formats/pajek/exporter.js +142 -73
  144. package/dist/src/formats/pajek/exporter.js.map +1 -1
  145. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  146. package/dist/src/formats/pajek/importer.js +26 -15
  147. package/dist/src/formats/pajek/importer.js.map +1 -1
  148. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  149. package/dist/src/formats/pajek/syntax.js +85 -58
  150. package/dist/src/formats/pajek/syntax.js.map +1 -1
  151. package/dist/src/formats/xgmml/emit.d.ts +7 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +79 -40
  154. package/dist/src/formats/xgmml/emit.js.map +1 -1
  155. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  156. package/dist/src/formats/xgmml/exporter.js +38 -12
  157. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  158. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  159. package/dist/src/formats/xgmml/importer.js +38 -18
  160. package/dist/src/formats/xgmml/importer.js.map +1 -1
  161. package/dist/src/sniff.js +109 -57
  162. package/dist/src/sniff.js.map +1 -1
  163. package/dist/xgmml.js +2 -2
  164. package/package.json +2 -2
  165. package/src/children.ts +54 -27
  166. package/src/common/attributes.ts +118 -49
  167. package/src/common/input.ts +439 -197
  168. package/src/common/lists.ts +29 -15
  169. package/src/common/text.ts +24 -15
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +377 -175
  172. package/src/formats/csv/importer.ts +155 -84
  173. package/src/formats/csv/records.ts +90 -35
  174. package/src/formats/cys/tables.ts +11 -10
  175. package/src/formats/dot/tokenizer.ts +118 -68
  176. package/src/formats/gexf/exporter.ts +67 -36
  177. package/src/formats/gexf/importer.ts +173 -63
  178. package/src/formats/gml/exporter.ts +78 -82
  179. package/src/formats/gml/importer.ts +119 -68
  180. package/src/formats/gml/syntax.ts +18 -8
  181. package/src/formats/graphml/exporter.ts +44 -24
  182. package/src/formats/graphml/importer.ts +18 -11
  183. package/src/formats/graphml/tree.ts +22 -12
  184. package/src/formats/graphml/yfiles.ts +23 -10
  185. package/src/formats/json/dialect.ts +61 -22
  186. package/src/formats/json/exporter.ts +40 -21
  187. package/src/formats/json/importer.ts +701 -385
  188. package/src/formats/json/obographs.ts +35 -15
  189. package/src/formats/neo4j/exporter.ts +79 -41
  190. package/src/formats/neo4j/header.ts +50 -16
  191. package/src/formats/neo4j/importer.ts +21 -10
  192. package/src/formats/obo/exporter.ts +34 -17
  193. package/src/formats/pajek/exporter.ts +180 -117
  194. package/src/formats/pajek/importer.ts +27 -15
  195. package/src/formats/pajek/syntax.ts +99 -58
  196. package/src/formats/xgmml/emit.ts +88 -40
  197. package/src/formats/xgmml/exporter.ts +40 -12
  198. package/src/formats/xgmml/importer.ts +45 -23
  199. package/src/sniff.ts +115 -59
  200. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  201. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  202. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  203. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  204. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  205. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  206. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  207. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  208. package/dist/chunks/importer-DcKB97kW.js.map +0 -1
  209. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  210. 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 seen = new Set();
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
- // A node table holds no edge: the direction and mutual notes are the edge table's.
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 && edgeRows.length === 0) {
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
- if (folding.mutualCount > 0 && csv.table !== "nodes") {
367
- 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);
368
- }
369
- if (csv.table === "edges") {
370
- noteNodeCoverage(snapshot, edgeRows, note);
371
- }
372
- // weights
373
- const weights = explicitWeights(snapshot);
374
- // the edge table's columns
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
- // the node table's columns
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
- if (values) {
407
- checkRoleColumn(nodeLabel, "label", LABEL_NAMES, "node", nodeRows, note);
408
- for (const { column } of nodeColumns) {
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
- else {
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
- const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
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
- if (labelColumn !== null) {
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
- let nonFinite = 0;
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
- if (!column.isSet(row)) {
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
- if (kind !== "string") {
613
- candidate = false;
614
- }
716
+ candidate && (candidate = kind === "string");
615
717
  heuristic.observe(text);
616
718
  }
617
719
  }
618
- if (roleColumn || widest < 0) {
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
- let end = r;
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
- const { csv } = plan;
747
- if (csv.table === "adjacency") {
847
+ if (plan.csv.table === "adjacency") {
748
848
  yield* adjacencyLines(snapshot, plan);
749
- return;
750
849
  }
751
- const { delimiter, newline } = csv;
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
- if (csv.table === "nodes") {
758
- const headers = [csv.dialect.id];
759
- if (plan.nodeLabel !== null) {
760
- headers.push(plan.nodeLabel.meta.name);
761
- }
762
- for (const { header } of plan.nodeColumns) {
763
- headers.push(header);
764
- }
765
- if (csv.header) {
766
- yield headers.map(quote).join(delimiter) + newline;
767
- }
768
- for (let i = 0; i < snapshot.nodeCount; i++) {
769
- const cells = [quote(plan.idText(i))];
770
- if (plan.nodeLabel !== null) {
771
- cells.push(cell(plan.nodeLabel, i));
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.edgeColumns) {
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.