@graphty/graph-io 0.3.39 → 0.3.41

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 (213) hide show
  1. package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
  2. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
  3. package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
  4. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
  5. package/dist/chunks/{escape-BpYMQZVa.js → escape-DBb8zuNE.js} +4 -3
  6. package/dist/chunks/escape-DBb8zuNE.js.map +1 -0
  7. package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
  8. package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
  9. package/dist/chunks/{exporter-CNJZF_BN.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-BqKs1Q12.js → exporter-CS35R6Fg.js} +6 -5
  12. package/dist/chunks/exporter-CS35R6Fg.js.map +1 -0
  13. package/dist/chunks/{exporter-DXqCPNrI.js → exporter-D8r1N2dU.js} +67 -42
  14. package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
  15. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
  16. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
  17. package/dist/chunks/{importer-CSt-gaYE.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-DvFznt5-.js → importer-BdWHmlmG.js} +112 -62
  20. package/dist/chunks/importer-BdWHmlmG.js.map +1 -0
  21. package/dist/chunks/{importer-CqgYq4Ld.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-PVXkeoaS.js → importer-BmaZcskq.js} +79 -66
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-B7Ir269L.js → importer-DviEsHy4.js} +118 -72
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer--tQQsY4W.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer--tQQsY4W.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BfJ1jnNr.js → importer-r29cWnEf.js} +28 -13
  30. package/dist/chunks/importer-r29cWnEf.js.map +1 -0
  31. package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DdF-etPT.js} +7 -5
  32. package/dist/chunks/{json-elements-CXs4z1iL.js.map → json-elements-DdF-etPT.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
  34. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
  35. package/dist/chunks/{records-C9jd7yLD.js → records-Uc3LkABx.js} +150 -106
  36. package/dist/chunks/records-Uc3LkABx.js.map +1 -0
  37. package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
  38. package/dist/chunks/text-Bh9SaAob.js.map +1 -0
  39. package/dist/csv.js +67 -49
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +199 -99
  46. package/dist/gexf.js.map +1 -1
  47. package/dist/gml.js +170 -149
  48. package/dist/gml.js.map +1 -1
  49. package/dist/graph-io.js +26 -26
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +103 -65
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/attributes.d.ts.map +1 -1
  57. package/dist/src/common/attributes.js +34 -9
  58. package/dist/src/common/attributes.js.map +1 -1
  59. package/dist/src/common/input.d.ts.map +1 -1
  60. package/dist/src/common/input.js +180 -102
  61. package/dist/src/common/input.js.map +1 -1
  62. package/dist/src/common/json-elements.d.ts.map +1 -1
  63. package/dist/src/common/json-elements.js +4 -2
  64. package/dist/src/common/json-elements.js.map +1 -1
  65. package/dist/src/common/text.d.ts +7 -0
  66. package/dist/src/common/text.d.ts.map +1 -1
  67. package/dist/src/common/text.js +15 -7
  68. package/dist/src/common/text.js.map +1 -1
  69. package/dist/src/common/xml.d.ts.map +1 -1
  70. package/dist/src/common/xml.js +2 -1
  71. package/dist/src/common/xml.js.map +1 -1
  72. package/dist/src/common/zip.d.ts.map +1 -1
  73. package/dist/src/common/zip.js +36 -19
  74. package/dist/src/common/zip.js.map +1 -1
  75. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  76. package/dist/src/formats/csv/exporter.js +77 -25
  77. package/dist/src/formats/csv/exporter.js.map +1 -1
  78. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  79. package/dist/src/formats/csv/importer.js +16 -6
  80. package/dist/src/formats/csv/importer.js.map +1 -1
  81. package/dist/src/formats/csv/records.d.ts.map +1 -1
  82. package/dist/src/formats/csv/records.js +156 -110
  83. package/dist/src/formats/csv/records.js.map +1 -1
  84. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  85. package/dist/src/formats/cx/importer.js +108 -53
  86. package/dist/src/formats/cx/importer.js.map +1 -1
  87. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/cx2/exporter.js +2 -1
  89. package/dist/src/formats/cx2/exporter.js.map +1 -1
  90. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  91. package/dist/src/formats/cx2/importer.js +28 -6
  92. package/dist/src/formats/cx2/importer.js.map +1 -1
  93. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  94. package/dist/src/formats/cys/tables.js +11 -8
  95. package/dist/src/formats/cys/tables.js.map +1 -1
  96. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/exporter.js +69 -39
  98. package/dist/src/formats/gexf/exporter.js.map +1 -1
  99. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  100. package/dist/src/formats/gexf/importer.js +138 -50
  101. package/dist/src/formats/gexf/importer.js.map +1 -1
  102. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/gml/exporter.js +51 -35
  104. package/dist/src/formats/gml/exporter.js.map +1 -1
  105. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  106. package/dist/src/formats/gml/importer.js +103 -58
  107. package/dist/src/formats/gml/importer.js.map +1 -1
  108. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  109. package/dist/src/formats/gml/syntax.js +17 -8
  110. package/dist/src/formats/gml/syntax.js.map +1 -1
  111. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/exporter.js +36 -25
  113. package/dist/src/formats/graphml/exporter.js.map +1 -1
  114. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  115. package/dist/src/formats/graphml/importer.js +20 -15
  116. package/dist/src/formats/graphml/importer.js.map +1 -1
  117. package/dist/src/formats/graphml/tree.js +23 -14
  118. package/dist/src/formats/graphml/tree.js.map +1 -1
  119. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  120. package/dist/src/formats/graphml/yfiles.js +22 -10
  121. package/dist/src/formats/graphml/yfiles.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 +13 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +112 -50
  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 +92 -34
  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/common/attributes.ts +59 -19
  166. package/src/common/input.ts +216 -102
  167. package/src/common/json-elements.ts +4 -2
  168. package/src/common/text.ts +24 -15
  169. package/src/common/xml.ts +2 -1
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +103 -39
  172. package/src/formats/csv/importer.ts +17 -6
  173. package/src/formats/csv/records.ts +162 -109
  174. package/src/formats/cx/importer.ts +140 -58
  175. package/src/formats/cx2/exporter.ts +2 -1
  176. package/src/formats/cx2/importer.ts +34 -10
  177. package/src/formats/cys/tables.ts +11 -10
  178. package/src/formats/gexf/exporter.ts +71 -38
  179. package/src/formats/gexf/importer.ts +173 -63
  180. package/src/formats/gml/exporter.ts +78 -82
  181. package/src/formats/gml/importer.ts +119 -68
  182. package/src/formats/gml/syntax.ts +18 -8
  183. package/src/formats/graphml/exporter.ts +46 -27
  184. package/src/formats/graphml/importer.ts +21 -13
  185. package/src/formats/graphml/tree.ts +22 -12
  186. package/src/formats/graphml/yfiles.ts +23 -10
  187. package/src/formats/json/exporter.ts +40 -21
  188. package/src/formats/json/importer.ts +701 -385
  189. package/src/formats/json/obographs.ts +35 -15
  190. package/src/formats/neo4j/exporter.ts +79 -41
  191. package/src/formats/neo4j/header.ts +50 -16
  192. package/src/formats/neo4j/importer.ts +21 -10
  193. package/src/formats/obo/exporter.ts +34 -17
  194. package/src/formats/pajek/exporter.ts +180 -117
  195. package/src/formats/pajek/importer.ts +27 -15
  196. package/src/formats/pajek/syntax.ts +99 -58
  197. package/src/formats/xgmml/emit.ts +122 -50
  198. package/src/formats/xgmml/exporter.ts +103 -35
  199. package/src/formats/xgmml/importer.ts +45 -23
  200. package/src/sniff.ts +115 -59
  201. package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
  202. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  203. package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
  204. package/dist/chunks/exporter-CNJZF_BN.js.map +0 -1
  205. package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
  206. package/dist/chunks/importer-B7Ir269L.js.map +0 -1
  207. package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
  208. package/dist/chunks/importer-CSt-gaYE.js.map +0 -1
  209. package/dist/chunks/importer-CqgYq4Ld.js.map +0 -1
  210. package/dist/chunks/importer-DvFznt5-.js.map +0 -1
  211. package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
  212. package/dist/chunks/records-C9jd7yLD.js.map +0 -1
  213. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -503,21 +503,7 @@ function writeMeta(
503
503
  element: string,
504
504
  ): void {
505
505
  const path = `${element}.meta`;
506
- const { definition } = meta;
507
- if (isJsonObject(definition)) {
508
- const xrefs = strings(ctx, definition.xrefs, `${path}.definition.xrefs`);
509
- if (typeof definition.val === "string") {
510
- columns.node("def", row, definition.val);
511
- columns.node("def.xrefs", row, xrefs);
512
- } else {
513
- badValue(ctx, `${path}.definition has no string val; it is dropped`, `${path}.definition.val`);
514
- }
515
- if (definition.meta !== undefined) {
516
- unrecognized["meta.definition.meta"] = definition.meta;
517
- }
518
- } else if (definition !== undefined && definition !== null) {
519
- badValue(ctx, `${path}.definition must be an object with a val; it is dropped`, `${path}.definition`);
520
- }
506
+ writeDefinition(ctx, columns, meta.definition, row, unrecognized, path);
521
507
  const comments = strings(ctx, meta.comments, `${path}.comments`);
522
508
  if (comments.length > 0) {
523
509
  columns.node("comment", row, comments.join("\n"));
@@ -547,6 +533,40 @@ function writeMeta(
547
533
  }
548
534
  }
549
535
 
536
+ /**
537
+ * Write a node's `meta.definition`: the text and its xrefs; a nested `meta` is kept in
538
+ * `obo.unrecognized`.
539
+ * @param ctx - the context
540
+ * @param columns - the column writer
541
+ * @param definition - the `definition` field
542
+ * @param row - the node index
543
+ * @param unrecognized - where keys without a column go
544
+ * @param path - the meta path in messages
545
+ */
546
+ function writeDefinition(
547
+ ctx: ImportContext,
548
+ columns: ColumnWriter,
549
+ definition: unknown,
550
+ row: number,
551
+ unrecognized: JsonRecord,
552
+ path: string,
553
+ ): void {
554
+ if (isJsonObject(definition)) {
555
+ const xrefs = strings(ctx, definition.xrefs, `${path}.definition.xrefs`);
556
+ if (typeof definition.val === "string") {
557
+ columns.node("def", row, definition.val);
558
+ columns.node("def.xrefs", row, xrefs);
559
+ } else {
560
+ badValue(ctx, `${path}.definition has no string val; it is dropped`, `${path}.definition.val`);
561
+ }
562
+ if (definition.meta !== undefined) {
563
+ unrecognized["meta.definition.meta"] = definition.meta;
564
+ }
565
+ } else if (definition !== undefined && definition !== null) {
566
+ badValue(ctx, `${path}.definition must be an object with a val; it is dropped`, `${path}.definition`);
567
+ }
568
+ }
569
+
550
570
  /**
551
571
  * The synonyms of a node's meta as the OBO `synonym` items: each entry that is not an object or
552
572
  * has no string val reported and dropped, a predicate that is not one of the four scopes kept with
@@ -400,6 +400,17 @@ class ExportPlan {
400
400
  this.checkStoredIds();
401
401
  this.directionNotes();
402
402
  const slots = new Set([this.kind, this.labels, this.idSpace].flatMap((c) => (c === null ? [] : [c.meta.name])));
403
+ this.addGenericNotes(generic, slots);
404
+ this.notes.push(...this.ownNotes);
405
+ this.filterNotesByPart();
406
+ }
407
+
408
+ /**
409
+ * Add the generic capability notes, leaving out the ones this format reads back as they were.
410
+ * @param generic - the notes the capability check found
411
+ * @param slots - the names of the :TYPE, :LABEL and :ID(Space) columns
412
+ */
413
+ private addGenericNotes(generic: ReturnType<typeof checkCapabilities>, slots: ReadonlySet<string>): void {
403
414
  for (const gen of generic) {
404
415
  if (gen.code === LOSS.JSON && gen.column !== null && this.writtenAsPoint(gen.column)) {
405
416
  continue;
@@ -425,8 +436,6 @@ class ExportPlan {
425
436
  }
426
437
  this.notes.push(gen);
427
438
  }
428
- this.notes.push(...this.ownNotes);
429
- this.filterNotesByPart();
430
439
  }
431
440
 
432
441
  /**
@@ -617,22 +626,7 @@ class ExportPlan {
617
626
  * @returns the scalar plan
618
627
  */
619
628
  private integralOrDouble(column: Column, declared: string, label: string): ScalarPlan {
620
- let integral = true;
621
- if (column.dtype === "f64") {
622
- for (let r = 0; r < column.length && integral; r++) {
623
- if (column.isSet(r) && !Number.isInteger(column.data[r])) {
624
- integral = false;
625
- }
626
- }
627
- } else if (column.dtype === "list" && column.child.dtype === "f64") {
628
- const { data } = column.child;
629
- for (let i = 0; i < data.length && integral; i++) {
630
- if (!Number.isInteger(data[i])) {
631
- integral = false;
632
- }
633
- }
634
- }
635
- if (integral) {
629
+ if (holdsOnlyIntegers(column)) {
636
630
  return { type: declared, kind: "integer", temporal: null };
637
631
  }
638
632
  this.note(
@@ -1186,23 +1180,34 @@ class ExportPlan {
1186
1180
  return null;
1187
1181
  }
1188
1182
  if (plan.list) {
1189
- const items = column.dtype === "list" ? column.sliceOf(row) : (column.value(row) as ArrayLike<unknown>);
1190
- const texts: string[] = [];
1191
- for (const item of Array.from(items)) {
1192
- // an item of several components (itemComponents > 1) is flattened into the array
1193
- if (Array.isArray(item) || ArrayBuffer.isView(item)) {
1194
- for (const component of Array.from(item as ArrayLike<unknown>)) {
1195
- texts.push(formatScalar(component, scalar, null));
1196
- }
1197
- } else {
1198
- texts.push(formatScalar(item, scalar, null));
1199
- }
1200
- }
1201
- return texts.join(this.options.arrayDelimiter);
1183
+ return this.listText(plan, row);
1202
1184
  }
1203
1185
  const text = companion?.isSet(row) ? textOf(companion, row) : null;
1204
1186
  return formatScalar(column.dtype === "json" ? column.values[row] : column.value(row), scalar, text);
1205
1187
  }
1188
+
1189
+ /**
1190
+ * The text of one list cell: its items joined by the array delimiter.
1191
+ * @param plan - the column plan
1192
+ * @param row - the row, which is set
1193
+ * @returns the text
1194
+ */
1195
+ private listText(plan: ColumnPlan, row: number): string {
1196
+ const { column, scalar } = plan;
1197
+ const items = column.dtype === "list" ? column.sliceOf(row) : (column.value(row) as ArrayLike<unknown>);
1198
+ const texts: string[] = [];
1199
+ for (const item of Array.from(items)) {
1200
+ // an item of several components (itemComponents > 1) is flattened into the array
1201
+ if (Array.isArray(item) || ArrayBuffer.isView(item)) {
1202
+ for (const component of Array.from(item as ArrayLike<unknown>)) {
1203
+ texts.push(formatScalar(component, scalar, null));
1204
+ }
1205
+ } else {
1206
+ texts.push(formatScalar(item, scalar, null));
1207
+ }
1208
+ }
1209
+ return texts.join(this.options.arrayDelimiter);
1210
+ }
1206
1211
  }
1207
1212
 
1208
1213
  /**
@@ -1319,6 +1324,16 @@ function defaultScalar(dtype: Dtype | ScalarDtype): ScalarPlan {
1319
1324
  }
1320
1325
  }
1321
1326
 
1327
+ /**
1328
+ * Format a number with a formatter; any other value is written as String(value).
1329
+ * @param value - the value
1330
+ * @param format - the number formatter
1331
+ * @returns the text
1332
+ */
1333
+ function numberOrString(value: unknown, format: (n: number) => string): string {
1334
+ return typeof value === "number" ? format(value) : String(value);
1335
+ }
1336
+
1322
1337
  /**
1323
1338
  * Format one scalar value for a cell.
1324
1339
  * @param value - the value
@@ -1329,22 +1344,22 @@ function defaultScalar(dtype: Dtype | ScalarDtype): ScalarPlan {
1329
1344
  function formatScalar(value: unknown, scalar: ScalarPlan, text: string | null): string {
1330
1345
  switch (scalar.kind) {
1331
1346
  case "integer":
1332
- return typeof value === "number" ? formatInteger(value) : String(value);
1347
+ return numberOrString(value, formatInteger);
1333
1348
  case "float":
1334
- return typeof value === "number" ? formatF32(value) : String(value);
1349
+ return numberOrString(value, formatF32);
1335
1350
  case "double":
1336
- return typeof value === "number" ? formatF64(value) : String(value);
1351
+ return numberOrString(value, formatF64);
1337
1352
  case "boolean":
1338
1353
  return value === true ? "true" : "false";
1339
1354
  case "string":
1340
- return typeof value === "string" ? value : String(value);
1355
+ return String(value);
1341
1356
  case "temporal":
1342
- if (text !== null) {
1343
- return text;
1344
- }
1345
- return typeof value === "number" && scalar.temporal !== null
1346
- ? formatTemporal(value, scalar.temporal)
1347
- : String(value);
1357
+ return (
1358
+ text ??
1359
+ numberOrString(value, (n) =>
1360
+ scalar.temporal === null ? String(n) : formatTemporal(n, scalar.temporal),
1361
+ )
1362
+ );
1348
1363
  case "point":
1349
1364
  return formatPoint(value);
1350
1365
  case "json":
@@ -1539,3 +1554,26 @@ function weightPropertyOf(snapshot: GraphSnapshot): string | null {
1539
1554
  const name = neo4j?.weightProperty;
1540
1555
  return typeof name === "string" && name !== "" ? name : null;
1541
1556
  }
1557
+
1558
+ /**
1559
+ * Whether every set value of an f64 column, or of an f64 list column's items, is integral.
1560
+ * @param column - the column
1561
+ * @returns false when a value is not an integer
1562
+ */
1563
+ function holdsOnlyIntegers(column: Column): boolean {
1564
+ if (column.dtype === "f64") {
1565
+ for (let r = 0; r < column.length; r++) {
1566
+ if (column.isSet(r) && !Number.isInteger(column.data[r])) {
1567
+ return false;
1568
+ }
1569
+ }
1570
+ } else if (column.dtype === "list" && column.child.dtype === "f64") {
1571
+ const { data } = column.child;
1572
+ for (let i = 0; i < data.length; i++) {
1573
+ if (!Number.isInteger(data[i])) {
1574
+ return false;
1575
+ }
1576
+ }
1577
+ }
1578
+ return true;
1579
+ }
@@ -108,28 +108,14 @@ export function parseHeaderField(text: string): HeaderField {
108
108
  const space = m[3] === undefined ? null : m[3].trim();
109
109
  const options = parseOptions(text, m[4]);
110
110
  if (rawType === null) {
111
- // `name` alone: a string property
112
- if (name.length === 0) {
113
- throw headerError(text, "an empty header cell");
114
- }
115
- if (space !== null || options.size > 0) {
116
- throw headerError(text, "an id space or options need a field type");
117
- }
118
- return { text, name, kind: "PROPERTY", type: null, space: null, options };
111
+ return untypedField(text, name, space, options);
119
112
  }
120
113
  if (rawType.length === 0) {
121
114
  throw headerError(text, "an empty field type");
122
115
  }
123
116
  const upper = rawType.toUpperCase();
124
117
  if (STRUCTURAL.has(upper)) {
125
- const kind = upper as Exclude<FieldKind, "PROPERTY">;
126
- if (space !== null && !SPACE_KINDS.has(kind)) {
127
- throw headerError(text, `an id space is only allowed on ID, START_ID and END_ID, not ${kind}`);
128
- }
129
- if (name.length > 0 && kind !== "ID" && kind !== "IGNORE") {
130
- throw headerError(text, `a ${kind} field cannot be stored as a property`);
131
- }
132
- return { text, name, kind, type: null, space: space === "" ? null : space, options };
118
+ return structuralField(text, name, upper as Exclude<FieldKind, "PROPERTY">, space, options);
133
119
  }
134
120
  if (name.length === 0) {
135
121
  throw headerError(text, "a property column needs a name");
@@ -140,6 +126,54 @@ export function parseHeaderField(text: string): HeaderField {
140
126
  return { text, name, kind: "PROPERTY", type: rawType, space: null, options };
141
127
  }
142
128
 
129
+ /**
130
+ * The field of a cell with no type: `name` alone is a string property.
131
+ * @param text - the whole cell, for errors
132
+ * @param name - the property name
133
+ * @param space - the id space, or null
134
+ * @param options - the options
135
+ * @returns the field
136
+ */
137
+ function untypedField(
138
+ text: string,
139
+ name: string,
140
+ space: string | null,
141
+ options: ReadonlyMap<string, string>,
142
+ ): HeaderField {
143
+ if (name.length === 0) {
144
+ throw headerError(text, "an empty header cell");
145
+ }
146
+ if (space !== null || options.size > 0) {
147
+ throw headerError(text, "an id space or options need a field type");
148
+ }
149
+ return { text, name, kind: "PROPERTY", type: null, space: null, options };
150
+ }
151
+
152
+ /**
153
+ * The field of a cell with a structural type (ID, START_ID, END_ID, LABEL, TYPE, IGNORE).
154
+ * @param text - the whole cell, for errors
155
+ * @param name - the property name, or empty
156
+ * @param kind - the structural kind
157
+ * @param space - the id space, or null
158
+ * @param options - the options
159
+ * @returns the field
160
+ */
161
+ function structuralField(
162
+ text: string,
163
+ name: string,
164
+ kind: Exclude<FieldKind, "PROPERTY">,
165
+ space: string | null,
166
+ options: ReadonlyMap<string, string>,
167
+ ): HeaderField {
168
+ if (space !== null && !SPACE_KINDS.has(kind)) {
169
+ throw headerError(text, `an id space is only allowed on ID, START_ID and END_ID, not ${kind}`);
170
+ }
171
+ if (name.length > 0 && kind !== "ID" && kind !== "IGNORE") {
172
+ throw headerError(text, `a ${kind} field cannot be stored as a property`);
173
+ }
174
+ return { text, name, kind, type: null, space: space === "" ? null : space, options };
175
+ }
176
+
143
177
  /**
144
178
  * Parse the `{key:value, key:value}` options of a header cell.
145
179
  * @param text - the whole cell, for errors
@@ -1385,16 +1385,7 @@ class Neo4jImportSession {
1385
1385
  }
1386
1386
  const { spec } = slot;
1387
1387
  try {
1388
- if (spec.temporal !== null && !spec.list) {
1389
- const zoned = withZoneOffset(text, spec);
1390
- const parsed = parseDeclaredTemporal(zoned, spec);
1391
- values[slot.cell] = parsed.value;
1392
- // a zone name is kept in the companion text, the value is its instant
1393
- texts[slot.cell] = zoned === text ? parsed.text : text;
1394
- } else {
1395
- values[slot.cell] = parseDeclaredValue(text, spec, this.options.listSyntax);
1396
- checkNeo4jRange(spec, values[slot.cell], text);
1397
- }
1388
+ this.parseCell(slot, text);
1398
1389
  } catch (err) {
1399
1390
  this.report.recordError(err, { line, element: `${element} ${slot.name}` });
1400
1391
  return false;
@@ -1409,6 +1400,26 @@ class Neo4jImportSession {
1409
1400
  return true;
1410
1401
  }
1411
1402
 
1403
+ /**
1404
+ * Parse one non-empty property cell into the scratch arrays; throws on a malformed value.
1405
+ * @param slot - the cell's property slot
1406
+ * @param text - the cell text
1407
+ */
1408
+ private parseCell(slot: PropertySlot, text: string): void {
1409
+ const { values, texts } = this;
1410
+ const { spec } = slot;
1411
+ if (spec.temporal !== null && !spec.list) {
1412
+ const zoned = withZoneOffset(text, spec);
1413
+ const parsed = parseDeclaredTemporal(zoned, spec);
1414
+ values[slot.cell] = parsed.value;
1415
+ // a zone name is kept in the companion text, the value is its instant
1416
+ texts[slot.cell] = zoned === text ? parsed.text : text;
1417
+ } else {
1418
+ values[slot.cell] = parseDeclaredValue(text, spec, this.options.listSyntax);
1419
+ checkNeo4jRange(spec, values[slot.cell], text);
1420
+ }
1421
+ }
1422
+
1412
1423
  /**
1413
1424
  * Write the parsed property values of a row.
1414
1425
  * @param domain - node or edge
@@ -972,6 +972,39 @@ function keptObo(snapshot: GraphSnapshot): {
972
972
  return { header: clauses(record.header), typedefs, unknownFrames };
973
973
  }
974
974
 
975
+ /**
976
+ * Note the frame kinds the written nodes read back as: the type column gained, and the nodes
977
+ * written as [Typedef] frames.
978
+ * @param snapshot - the snapshot
979
+ * @param rows - the node indices written as frames
980
+ * @param kinds - the frame kind of each node
981
+ * @param note - receives the notes
982
+ */
983
+ function noteFrameKinds(
984
+ snapshot: GraphSnapshot,
985
+ rows: readonly number[],
986
+ kinds: readonly string[],
987
+ note: NoteFn,
988
+ ): void {
989
+ if (rows.length > 0 && !snapshot.nodes.has("type")) {
990
+ note(
991
+ OBO_LOSS.TYPE_GAINED,
992
+ `${rows.length} node${plural(rows.length)} ${agree(rows.length, "is", "are")} written as [Term] frames and ${agree(rows.length, "reads", "read")} back with the type Term in a new node column "type"`,
993
+ null,
994
+ rows.length,
995
+ );
996
+ }
997
+ const typedefNodes = rows.filter((i) => kinds[i] === "Typedef").length;
998
+ if (typedefNodes > 0) {
999
+ note(
1000
+ OBO_LOSS.TYPEDEF_NODES,
1001
+ `${typedefNodes} node${plural(typedefNodes)} ${agree(typedefNodes, "is", "are")} written as [Typedef] frames; they read back as nodes only when imported with typedefs: "nodes", and then after the other nodes, with the [Typedef] frames that declare the relations`,
1002
+ "type",
1003
+ typedefNodes,
1004
+ );
1005
+ }
1006
+ }
1007
+
975
1008
  /**
976
1009
  * Plan an export: the notes, the fatal condition and what is written.
977
1010
  * @param snapshot - the snapshot
@@ -1014,23 +1047,7 @@ function plan(snapshot: GraphSnapshot, options: (OboExportOptions & CommonExport
1014
1047
  }
1015
1048
  }
1016
1049
  const { kinds, type } = planKinds(snapshot, frameless);
1017
- if (rows.length > 0 && !snapshot.nodes.has("type")) {
1018
- note(
1019
- OBO_LOSS.TYPE_GAINED,
1020
- `${rows.length} node${plural(rows.length)} ${agree(rows.length, "is", "are")} written as [Term] frames and ${agree(rows.length, "reads", "read")} back with the type Term in a new node column "type"`,
1021
- null,
1022
- rows.length,
1023
- );
1024
- }
1025
- const typedefNodes = rows.filter((i) => kinds[i] === "Typedef").length;
1026
- if (typedefNodes > 0) {
1027
- note(
1028
- OBO_LOSS.TYPEDEF_NODES,
1029
- `${typedefNodes} node${plural(typedefNodes)} ${agree(typedefNodes, "is", "are")} written as [Typedef] frames; they read back as nodes only when imported with typedefs: "nodes", and then after the other nodes, with the [Typedef] frames that declare the relations`,
1030
- "type",
1031
- typedefNodes,
1032
- );
1033
- }
1050
+ noteFrameKinds(snapshot, rows, kinds, note);
1034
1051
 
1035
1052
  const kept = keptObo(snapshot);
1036
1053
  const scopes = synonymTypeScopes(kept.header);