@graphty/graph-io 0.0.0 → 0.2.1
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/LICENSE +21 -0
- package/README.md +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Pajek NET exporter (design section 8.5; research note 07 sections 2.5 and 9): vertices are
|
|
3
|
+
* numbered 1..N in index order (the `dense-1-based` id charset: an id that is not its 1-based
|
|
4
|
+
* index survives as the label only, which check() reports as W_ID_RENUMBERED; under
|
|
5
|
+
* `sanitizeIds: "mangle"` it is also written as a `graphty_originalId` parameter the importer
|
|
6
|
+
* restores under `restoreMangledIds`), the label role
|
|
7
|
+
* column is the label, the position role column the coordinates, a `shape` column the shape
|
|
8
|
+
* keyword, every other writable column a `key value` parameter and the spells role a time
|
|
9
|
+
* interval token. Logical edges are written in index order in runs of `*Arcs` (directed) and
|
|
10
|
+
* `*Edges` (undirected) sections, folding expanded pairs back through the pair role and reading
|
|
11
|
+
* attributes from the primary half, so a mixed file round-trips in its original edge order. The
|
|
12
|
+
* weight is written for edges whose weight was explicit (the role-weight column's validity).
|
|
13
|
+
*/
|
|
14
|
+
import { GraphFormatError } from "@graphty/graph-format";
|
|
15
|
+
import { pairFolding } from "../../common/direction.js";
|
|
16
|
+
import { isPajekLabel, quotePajekLabel } from "../../common/escape.js";
|
|
17
|
+
import { capabilities, checkCapabilities, LOSS, sanitizeIds } from "../../common/export.js";
|
|
18
|
+
import { formatDecimal, formatF32, formatF64, formatInteger } from "../../common/format.js";
|
|
19
|
+
import { canonicalId } from "../../common/ids.js";
|
|
20
|
+
import { resolveExportOptions } from "../../common/options.js";
|
|
21
|
+
import { explicitWeights } from "../../common/weights.js";
|
|
22
|
+
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
23
|
+
import { formatIntervals, isParameterKey, LABEL_COLUMN, ORIGINAL_ID_KEY, RELATION_COLUMN, SHAPE_COLUMN, SHAPES, } from "./syntax.js";
|
|
24
|
+
/**
|
|
25
|
+
* The LossNote codes of the Pajek exporter's check(): the Pajek-specific ones and, aliased, the
|
|
26
|
+
* shared ones it records itself (`LOSS` holds the rest of the generic pre-flight's codes). A key
|
|
27
|
+
* is the code without its severity and format prefixes.
|
|
28
|
+
*/
|
|
29
|
+
export const PAJEK_LOSS = Object.freeze({
|
|
30
|
+
/** A label or text value holds a double quote or a line break; export() will throw E_UNSUPPORTED. */
|
|
31
|
+
TEXT: "E_PAJEK_TEXT",
|
|
32
|
+
/** A column whose name cannot be a parameter key (whitespace, a quote, numeric, a shape keyword); skipped. */
|
|
33
|
+
KEY_DROPPED: "W_PAJEK_KEY_DROPPED",
|
|
34
|
+
/** A label role column that is not text; its values are written as text and re-import as string. */
|
|
35
|
+
LABEL_AS_TEXT: "W_PAJEK_LABEL_AS_TEXT",
|
|
36
|
+
/** A non-finite f64 value; written as Infinity / NaN text, which re-imports as string. */
|
|
37
|
+
NONFINITE_AS_TEXT: "W_PAJEK_NONFINITE_AS_TEXT",
|
|
38
|
+
/** A mutual pair; written as one undirected edge, the mark lost. */
|
|
39
|
+
MUTUAL_AS_UNDIRECTED: LOSS.MUTUAL_AS_UNDIRECTED,
|
|
40
|
+
/** A start / end / timestamp role column; Pajek intervals are written from the spells role only. */
|
|
41
|
+
TEMPORAL_DROPPED: LOSS.TEMPORAL,
|
|
42
|
+
/** A position column with a stride other than 2 or 3. */
|
|
43
|
+
POSITION_STRIDE: "W_PAJEK_POSITION_STRIDE",
|
|
44
|
+
/** meta.extra.pajek.firstMode is not a count within 0..N; the two-mode header is not written. */
|
|
45
|
+
FIRST_MODE_DROPPED: "W_PAJEK_FIRST_MODE_DROPPED",
|
|
46
|
+
/** A role column Pajek has no slot for (kind, ...) written as a plain parameter; the role is lost. */
|
|
47
|
+
ROLE_DROPPED: LOSS.ROLE,
|
|
48
|
+
/** A `shape` column with a value outside the shape keywords is written as a parameter (a string on re-import). */
|
|
49
|
+
SHAPE_AS_PARAMETER: "W_PAJEK_SHAPE_AS_PARAMETER",
|
|
50
|
+
/** A vertex line with coordinates, a shape or parameters needs a label: the id text is written and reads back as a label. */
|
|
51
|
+
LABEL_GAINED: "W_PAJEK_LABEL_GAINED",
|
|
52
|
+
/** A role-less node column named `label` reads back with the label role (its values become the vertex labels). */
|
|
53
|
+
ROLE_ASSUMED: LOSS.ROLE_ASSUMED,
|
|
54
|
+
/** Under sanitizeIds "mangle": an original id whose text reads back as the other type under ids "canonical". */
|
|
55
|
+
ID_TEXT_TYPE: LOSS.ID_TEXT_TYPE,
|
|
56
|
+
});
|
|
57
|
+
/** The roles Pajek has a slot for beyond the structural, position and temporal ones. */
|
|
58
|
+
const SLOT_ROLES = new Set(["label"]);
|
|
59
|
+
/** The names the importer gives the slot columns, for the name-change notes. */
|
|
60
|
+
const ROLE_NAMES = Object.freeze({ label: "label" });
|
|
61
|
+
/** The roles the exporter handles structurally rather than as parameters. */
|
|
62
|
+
const STRUCTURAL_ROLES = new Set([
|
|
63
|
+
"directed",
|
|
64
|
+
"pair",
|
|
65
|
+
"mutual",
|
|
66
|
+
"weight",
|
|
67
|
+
"timeText",
|
|
68
|
+
"originalId",
|
|
69
|
+
]);
|
|
70
|
+
/** The roles the generic checker reports for a format without viz, hierarchy or open-interval support. */
|
|
71
|
+
const CHECKED_ROLES = new Set([
|
|
72
|
+
"color",
|
|
73
|
+
"size",
|
|
74
|
+
"shape",
|
|
75
|
+
"thickness",
|
|
76
|
+
"parent",
|
|
77
|
+
"parents",
|
|
78
|
+
"open",
|
|
79
|
+
]);
|
|
80
|
+
/** Roles the generic checker lets through under temporal "spells" that Pajek cannot write. */
|
|
81
|
+
const DROPPED_TEMPORAL_ROLES = new Set(["start", "end", "timestamp", "timestamps"]);
|
|
82
|
+
const CAPABILITIES = capabilities({
|
|
83
|
+
mixedDirection: true,
|
|
84
|
+
multiEdges: true,
|
|
85
|
+
selfLoops: true,
|
|
86
|
+
edgeIds: "none",
|
|
87
|
+
idCharset: "dense-1-based",
|
|
88
|
+
dtypes: ["f64", "i32", "bool", "string"],
|
|
89
|
+
temporal: "spells",
|
|
90
|
+
positions: true,
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Decide the slot of every column of a table.
|
|
94
|
+
* @param table - the node or edge table
|
|
95
|
+
* @param domain - node or edge
|
|
96
|
+
* @param notes - the note list check() fills; null when writing
|
|
97
|
+
* @returns the plan, in declaration order
|
|
98
|
+
*/
|
|
99
|
+
function plan(table, domain, notes) {
|
|
100
|
+
const planned = [];
|
|
101
|
+
const rows = (column) => column.length - column.nullCount;
|
|
102
|
+
for (const column of table) {
|
|
103
|
+
const { meta } = column;
|
|
104
|
+
const { role, name, dtype } = meta;
|
|
105
|
+
if (role === "label" && domain === "node") {
|
|
106
|
+
if (dtype !== "string" && dtype !== "dict" && notes !== null) {
|
|
107
|
+
notes.push(note(PAJEK_LOSS.LABEL_AS_TEXT, `node column "${name}" (label) is ${dtype}; labels are text and re-import as string`, name, rows(column)));
|
|
108
|
+
}
|
|
109
|
+
planned.push({ column, slot: "label" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (role === "position") {
|
|
113
|
+
if (domain === "node" && (meta.components === 2 || meta.components === 3)) {
|
|
114
|
+
planned.push({ column, slot: "position" });
|
|
115
|
+
}
|
|
116
|
+
else if (notes !== null && domain === "node") {
|
|
117
|
+
notes.push(note(PAJEK_LOSS.POSITION_STRIDE, `node column "${name}" (position) has ${meta.components} components; Pajek coordinates are x y [z]`, name, rows(column)));
|
|
118
|
+
}
|
|
119
|
+
else if (notes !== null) {
|
|
120
|
+
notes.push(note(LOSS.POSITIONS, `edge column "${name}" (position) cannot be written`, name, rows(column)));
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (role === "spells") {
|
|
125
|
+
if (dtype === "list" &&
|
|
126
|
+
(meta.itemDtype === "f64" || meta.itemDtype === "i32" || meta.itemDtype === "f32")) {
|
|
127
|
+
planned.push({ column, slot: "spells" });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (notes !== null) {
|
|
131
|
+
notes.push(note(LOSS.SPELLS, `${domain} column "${name}" (spells) is not a list of number pairs; it cannot be written`, name, rows(column)));
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (role !== null && DROPPED_TEMPORAL_ROLES.has(role)) {
|
|
136
|
+
if (notes !== null) {
|
|
137
|
+
notes.push(note(PAJEK_LOSS.TEMPORAL_DROPPED, `${domain} column "${name}" (${role}) cannot be written; Pajek intervals come from the spells role`, name, rows(column)));
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (role !== null && (STRUCTURAL_ROLES.has(role) || CHECKED_ROLES.has(role))) {
|
|
142
|
+
// structural columns are folded into the topology; viz and hierarchy roles are reported by the generic checker
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (domain === "edge" && role === "id") {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (dtype === "list" || dtype === "json") {
|
|
149
|
+
// reported by the generic checker (lists: false, json: false)
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (domain === "node" && name === SHAPE_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
153
|
+
if (allShapeKeywords(column)) {
|
|
154
|
+
planned.push({ column, slot: "shape" });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// a value outside the keywords: the whole column is a `shape "<text>"` parameter, which the
|
|
158
|
+
// importer reads back as one string column (a change for a dict column only)
|
|
159
|
+
if (notes !== null && dtype === "dict") {
|
|
160
|
+
notes.push(note(PAJEK_LOSS.SHAPE_AS_PARAMETER, `node column "${name}" holds values outside the Pajek shape keywords; it is written as a parameter and reads back as string`, name, rows(column)));
|
|
161
|
+
}
|
|
162
|
+
planned.push({ column, slot: "param" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (domain === "edge" && name === RELATION_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
166
|
+
planned.push({ column, slot: "relation" });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!isParameterKey(name)) {
|
|
170
|
+
if (notes !== null) {
|
|
171
|
+
notes.push(note(PAJEK_LOSS.KEY_DROPPED, `${domain} column "${name}" cannot be a Pajek parameter key; it is not written`, name, rows(column)));
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
planned.push({ column, slot: "param" });
|
|
176
|
+
}
|
|
177
|
+
return planned;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The notes about vertex labels: Pajek's grammar puts the label second, so a vertex line that
|
|
181
|
+
* carries coordinates, a shape, parameters or an interval needs one, and a node without a label
|
|
182
|
+
* value gets its id text written there (it reads back as a label); a role-less column named
|
|
183
|
+
* `label` reads back with the label role.
|
|
184
|
+
* @param snapshot - the snapshot
|
|
185
|
+
* @param nodePlan - the node column plan
|
|
186
|
+
* @param notes - where to record
|
|
187
|
+
*/
|
|
188
|
+
function labelNotes(snapshot, nodePlan, notes) {
|
|
189
|
+
let labels = null;
|
|
190
|
+
let carried = 0;
|
|
191
|
+
for (const { column, slot } of nodePlan) {
|
|
192
|
+
if (slot === "label") {
|
|
193
|
+
labels = column;
|
|
194
|
+
}
|
|
195
|
+
else if (slot !== "skip" && slot !== "relation") {
|
|
196
|
+
carried++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const plain = [...snapshot.nodes].find((c) => c.meta.role === null && c.meta.name === LABEL_COLUMN);
|
|
200
|
+
if (plain !== undefined) {
|
|
201
|
+
notes.push(note(PAJEK_LOSS.ROLE_ASSUMED, `node column "${LABEL_COLUMN}" has no role; written as a parameter, it reads back as the vertex label (role label)`, LABEL_COLUMN, plain.length - plain.nullCount));
|
|
202
|
+
}
|
|
203
|
+
if (carried === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let gained = 0;
|
|
207
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
208
|
+
if (labels === null || !labels.isSet(i)) {
|
|
209
|
+
gained++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (gained > 0) {
|
|
213
|
+
notes.push(note(PAJEK_LOSS.LABEL_GAINED, `${gained} vertex line(s) carry coordinates, a shape or parameters and need a label; the id text is written there and reads back as a label`, labels === null ? LABEL_COLUMN : labels.meta.name, gained));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Whether every set value of a shape column is a Pajek shape keyword.
|
|
218
|
+
* @param column - the shape column (string or dict)
|
|
219
|
+
* @returns true when the shape slot can hold the column
|
|
220
|
+
*/
|
|
221
|
+
function allShapeKeywords(column) {
|
|
222
|
+
for (let r = 0; r < column.length; r++) {
|
|
223
|
+
if (column.isSet(r) && !SHAPES.has(column.value(r))) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Build a frozen LossNote.
|
|
231
|
+
* @param code - the code
|
|
232
|
+
* @param message - the message
|
|
233
|
+
* @param column - the column, or null
|
|
234
|
+
* @param count - the count, or null
|
|
235
|
+
* @returns the note
|
|
236
|
+
*/
|
|
237
|
+
function note(code, message, column = null, count = null) {
|
|
238
|
+
return Object.freeze({ code, message, column, count });
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The text of one numeric cell: integers as digits, f64 with a decimal point guaranteed so the
|
|
242
|
+
* importer's inference keeps the dtype, f32 as the shortest fround-round-trip decimal.
|
|
243
|
+
* @param value - the value
|
|
244
|
+
* @param dtype - the column dtype
|
|
245
|
+
* @returns the text
|
|
246
|
+
*/
|
|
247
|
+
function numberText(value, dtype) {
|
|
248
|
+
switch (dtype) {
|
|
249
|
+
case "i32":
|
|
250
|
+
case "u32":
|
|
251
|
+
case "u8":
|
|
252
|
+
return formatInteger(value);
|
|
253
|
+
case "f32":
|
|
254
|
+
return formatF32(value);
|
|
255
|
+
case "f64":
|
|
256
|
+
return formatDecimal(value, "f64");
|
|
257
|
+
default: {
|
|
258
|
+
const name = dtype;
|
|
259
|
+
throw new Error(`unknown numeric dtype ${name}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The text of a set cell as a parameter value or label, before quoting.
|
|
265
|
+
* @param column - the column
|
|
266
|
+
* @param row - the row
|
|
267
|
+
* @returns the text
|
|
268
|
+
*/
|
|
269
|
+
function cellText(column, row) {
|
|
270
|
+
switch (column.dtype) {
|
|
271
|
+
case "f32":
|
|
272
|
+
case "f64":
|
|
273
|
+
case "i32":
|
|
274
|
+
case "u32":
|
|
275
|
+
case "u8": {
|
|
276
|
+
const value = column.value(row);
|
|
277
|
+
if (typeof value === "number") {
|
|
278
|
+
return numberText(value, column.dtype);
|
|
279
|
+
}
|
|
280
|
+
const parts = [];
|
|
281
|
+
const vector = value;
|
|
282
|
+
for (let k = 0; k < vector.length; k++) {
|
|
283
|
+
parts.push(numberText(vector[k], column.dtype));
|
|
284
|
+
}
|
|
285
|
+
return parts.join(" ");
|
|
286
|
+
}
|
|
287
|
+
case "bool":
|
|
288
|
+
return column.value(row) === true ? "true" : "false";
|
|
289
|
+
case "dict":
|
|
290
|
+
case "string":
|
|
291
|
+
return column.value(row);
|
|
292
|
+
case "list":
|
|
293
|
+
case "json":
|
|
294
|
+
return JSON.stringify(column.value(row));
|
|
295
|
+
default: {
|
|
296
|
+
const name = column.dtype;
|
|
297
|
+
throw new Error(`unknown dtype ${name}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Count the cells of a plan that cannot be written (text with a quote or line break, non-finite
|
|
303
|
+
* f64) and add the notes.
|
|
304
|
+
* @param planned - the plan
|
|
305
|
+
* @param domain - node or edge
|
|
306
|
+
* @param notes - the note list
|
|
307
|
+
*/
|
|
308
|
+
function checkCells(planned, domain, notes) {
|
|
309
|
+
for (const { column, slot } of planned) {
|
|
310
|
+
if (slot === "position" || slot === "spells") {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const { name } = column.meta;
|
|
314
|
+
let badText = 0;
|
|
315
|
+
let nonFinite = 0;
|
|
316
|
+
for (let r = 0; r < column.length; r++) {
|
|
317
|
+
if (!column.isSet(r)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (column.dtype === "f64" || column.dtype === "f32") {
|
|
321
|
+
const value = column.value(r);
|
|
322
|
+
if (typeof value === "number") {
|
|
323
|
+
if (!Number.isFinite(value)) {
|
|
324
|
+
nonFinite++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (value !== undefined && !Array.from(value).every((v) => Number.isFinite(v))) {
|
|
328
|
+
nonFinite++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (column.dtype === "string" || column.dtype === "dict") {
|
|
332
|
+
if (!isPajekLabel(column.value(r))) {
|
|
333
|
+
badText++;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (badText > 0) {
|
|
338
|
+
notes.push(note(PAJEK_LOSS.TEXT, `${badText} value(s) of ${domain} column "${name}" hold a double quote or a line break; Pajek cannot write them`, name, badText));
|
|
339
|
+
}
|
|
340
|
+
if (nonFinite > 0) {
|
|
341
|
+
notes.push(note(PAJEK_LOSS.NONFINITE_AS_TEXT, `${nonFinite} non-finite value(s) of ${domain} column "${name}" are written as text`, name, nonFinite));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The first-mode count to write in `*Vertices N N1`, from meta.extra.pajek.firstMode.
|
|
347
|
+
* @param snapshot - the snapshot
|
|
348
|
+
* @returns the count, or null when absent or invalid
|
|
349
|
+
*/
|
|
350
|
+
function firstModeOf(snapshot) {
|
|
351
|
+
const { pajek } = snapshot.meta.extra;
|
|
352
|
+
if (typeof pajek !== "object" || pajek === null) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const value = pajek.firstMode;
|
|
356
|
+
if (value === undefined) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= snapshot.nodeCount) {
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
return NaN;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The label of a vertex: the label column's value when set, the id's text when the id is not
|
|
366
|
+
* the vertex number, otherwise null (no label needed). A label is forced when something follows
|
|
367
|
+
* it on the line, because Pajek's grammar puts the label second.
|
|
368
|
+
* @param labels - the label column, or null
|
|
369
|
+
* @param ids - the sanitised ids
|
|
370
|
+
* @param i - the node index
|
|
371
|
+
* @param needed - whether the line has more after the label
|
|
372
|
+
* @returns the label text, or null
|
|
373
|
+
*/
|
|
374
|
+
function labelOf(labels, ids, i, needed) {
|
|
375
|
+
if (labels !== null && labels.isSet(i)) {
|
|
376
|
+
return cellText(labels, i);
|
|
377
|
+
}
|
|
378
|
+
if (needed || ids.isChanged(i)) {
|
|
379
|
+
return idText(ids.originalAt(i));
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The text of an id.
|
|
385
|
+
* @param id - the id
|
|
386
|
+
* @returns String(id)
|
|
387
|
+
*/
|
|
388
|
+
function idText(id) {
|
|
389
|
+
return typeof id === "number" ? formatF64(id) : id;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The spells text of a row, or null when unset or empty.
|
|
393
|
+
* @param column - the spells list column
|
|
394
|
+
* @param row - the row
|
|
395
|
+
* @returns the interval token or null
|
|
396
|
+
*/
|
|
397
|
+
function spellsText(column, row) {
|
|
398
|
+
if (column.dtype !== "list" || !column.isSet(row)) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const items = column.sliceOf(row);
|
|
402
|
+
if (items.length === 0) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const pairs = [];
|
|
406
|
+
for (const item of items) {
|
|
407
|
+
const pair = item;
|
|
408
|
+
pairs.push([pair[0], pair[1]]);
|
|
409
|
+
}
|
|
410
|
+
return formatIntervals(pairs);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* The coordinates text of a vertex, or null when unset.
|
|
414
|
+
* @param column - the position column (2 or 3 components)
|
|
415
|
+
* @param i - the node index
|
|
416
|
+
* @param writeZ - whether z is written for a 3-component column
|
|
417
|
+
* @returns "x y" or "x y z", or null
|
|
418
|
+
*/
|
|
419
|
+
function coordinatesText(column, i, writeZ) {
|
|
420
|
+
if (!column.isSet(i)) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const dtype = column.dtype;
|
|
424
|
+
const vector = column.value(i);
|
|
425
|
+
const x = numberText(vector[0], dtype);
|
|
426
|
+
const y = numberText(vector[1], dtype);
|
|
427
|
+
if (column.meta.components === 3 && writeZ) {
|
|
428
|
+
return `${x} ${y} ${numberText(vector[2], dtype)}`;
|
|
429
|
+
}
|
|
430
|
+
return `${x} ${y}`;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Whether a 3-component position column needs its z written: the importer recorded sourceDims 3,
|
|
434
|
+
* or any set row has a non-zero z (never drop data because of a 2-D hint).
|
|
435
|
+
* @param column - the position column
|
|
436
|
+
* @returns true when z is written
|
|
437
|
+
*/
|
|
438
|
+
function needsZ(column) {
|
|
439
|
+
if (column.meta.components !== 3) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
if (column.meta.extra.sourceDims !== 2) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
for (let i = 0; i < column.length; i++) {
|
|
446
|
+
if (column.isSet(i)) {
|
|
447
|
+
const vector = column.value(i);
|
|
448
|
+
if (vector[2] !== 0) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The text parts of a Pajek document, one per line.
|
|
457
|
+
* @param snapshot - the snapshot
|
|
458
|
+
* @param options - the resolved format options
|
|
459
|
+
* @param options.networkHeader - whether the `*Network` line is written
|
|
460
|
+
* @param options.mangle - whether the original ids are written as `graphty_originalId` parameters
|
|
461
|
+
* @yields lines with their terminator
|
|
462
|
+
* @returns nothing
|
|
463
|
+
*/
|
|
464
|
+
function* writeParts(snapshot, options) {
|
|
465
|
+
const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, options.mangle ? "mangle" : "error");
|
|
466
|
+
const nodePlan = withoutReservedKey(plan(snapshot.nodes, "node", null), options.mangle);
|
|
467
|
+
const edgePlan = plan(snapshot.edges, "edge", null);
|
|
468
|
+
// fail before writing anything when a text cannot be written (the check() E_ note)
|
|
469
|
+
const preflight = [];
|
|
470
|
+
checkCells(nodePlan, "node", preflight);
|
|
471
|
+
checkCells(edgePlan, "edge", preflight);
|
|
472
|
+
checkIdTexts(snapshot.nodeCount, ids, options.mangle, preflight);
|
|
473
|
+
for (const n of preflight) {
|
|
474
|
+
if (n.code === PAJEK_LOSS.TEXT) {
|
|
475
|
+
throw new GraphFormatError("E_UNSUPPORTED", n.message, {
|
|
476
|
+
reason: "pajek label",
|
|
477
|
+
column: n.column,
|
|
478
|
+
count: n.count,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (options.networkHeader && snapshot.meta.name !== null) {
|
|
483
|
+
yield `*Network ${snapshot.meta.name}\n`;
|
|
484
|
+
}
|
|
485
|
+
const firstMode = firstModeOf(snapshot);
|
|
486
|
+
yield firstMode !== null && !Number.isNaN(firstMode)
|
|
487
|
+
? `*Vertices ${snapshot.nodeCount} ${firstMode}\n`
|
|
488
|
+
: `*Vertices ${snapshot.nodeCount}\n`;
|
|
489
|
+
yield* writeVertices(snapshot, ids, nodePlan, options.mangle);
|
|
490
|
+
yield* writeLines(snapshot, edgePlan);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The node plan without a user column named like the exporter's originalId key, which is
|
|
494
|
+
* reserved under "mangle" (check() reports it as not written).
|
|
495
|
+
* @param nodePlan - the node column plan
|
|
496
|
+
* @param mangle - whether the original ids are written as parameters
|
|
497
|
+
* @returns the plan to write
|
|
498
|
+
*/
|
|
499
|
+
function withoutReservedKey(nodePlan, mangle) {
|
|
500
|
+
if (!mangle) {
|
|
501
|
+
return nodePlan;
|
|
502
|
+
}
|
|
503
|
+
return nodePlan.filter((p) => !(p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY));
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The notes about the id texts a vertex line carries: an id written as a label or, under
|
|
507
|
+
* "mangle", as the `graphty_originalId` parameter must be a Pajek label (E_PAJEK_TEXT otherwise),
|
|
508
|
+
* and under "mangle" an original id whose text reads back as the other type under ids
|
|
509
|
+
* "canonical" is reported (the importer coerces the parameter text like every id cell).
|
|
510
|
+
* @param nodeCount - the node count
|
|
511
|
+
* @param ids - the sanitised ids
|
|
512
|
+
* @param mangle - whether the original ids are written as parameters
|
|
513
|
+
* @param notes - where to record
|
|
514
|
+
*/
|
|
515
|
+
function checkIdTexts(nodeCount, ids, mangle, notes) {
|
|
516
|
+
let badText = 0;
|
|
517
|
+
let typeChanged = 0;
|
|
518
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
519
|
+
if (!ids.isChanged(i)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const original = ids.originalAt(i);
|
|
523
|
+
const text = idText(original);
|
|
524
|
+
if (!isPajekLabel(text)) {
|
|
525
|
+
badText++;
|
|
526
|
+
}
|
|
527
|
+
else if (mangle && typeof canonicalId(text) !== typeof original) {
|
|
528
|
+
typeChanged++;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (badText > 0) {
|
|
532
|
+
notes.push(note(PAJEK_LOSS.TEXT, `${badText} node id(s) hold a double quote or a line break; Pajek cannot write them as labels or parameters`, null, badText));
|
|
533
|
+
}
|
|
534
|
+
if (typeChanged > 0) {
|
|
535
|
+
notes.push(note(PAJEK_LOSS.ID_TEXT_TYPE, `${typeChanged} original id(s) read back from ${ORIGINAL_ID_KEY} as the other type under ids: "canonical" (a string "1" becomes 1, a number 1.5 becomes "1.5")`, null, typeChanged));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The vertex lines: number, label, coordinates, shape, parameters and interval.
|
|
540
|
+
* @param snapshot - the snapshot
|
|
541
|
+
* @param ids - the sanitised ids
|
|
542
|
+
* @param nodePlan - the node column plan
|
|
543
|
+
* @param mangle - whether a renumbered vertex carries its original id as a parameter
|
|
544
|
+
* @yields one line per vertex
|
|
545
|
+
* @returns nothing
|
|
546
|
+
*/
|
|
547
|
+
function* writeVertices(snapshot, ids, nodePlan, mangle) {
|
|
548
|
+
let labels = null;
|
|
549
|
+
let position = null;
|
|
550
|
+
let shape = null;
|
|
551
|
+
let nodeSpells = null;
|
|
552
|
+
const nodeParams = [];
|
|
553
|
+
for (const { column, slot } of nodePlan) {
|
|
554
|
+
switch (slot) {
|
|
555
|
+
case "label":
|
|
556
|
+
labels = column;
|
|
557
|
+
break;
|
|
558
|
+
case "position":
|
|
559
|
+
position = column;
|
|
560
|
+
break;
|
|
561
|
+
case "shape":
|
|
562
|
+
shape = column;
|
|
563
|
+
break;
|
|
564
|
+
case "spells":
|
|
565
|
+
nodeSpells = column;
|
|
566
|
+
break;
|
|
567
|
+
case "param":
|
|
568
|
+
nodeParams.push(column);
|
|
569
|
+
break;
|
|
570
|
+
case "relation":
|
|
571
|
+
case "skip":
|
|
572
|
+
break;
|
|
573
|
+
default: {
|
|
574
|
+
const name = slot;
|
|
575
|
+
throw new Error(`unknown slot ${name}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const writeZ = position !== null && needsZ(position);
|
|
580
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
581
|
+
const parts = [];
|
|
582
|
+
if (position !== null) {
|
|
583
|
+
const coordinates = coordinatesText(position, i, writeZ);
|
|
584
|
+
if (coordinates !== null) {
|
|
585
|
+
parts.push(coordinates);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (shape !== null && shape.isSet(i)) {
|
|
589
|
+
const keyword = cellText(shape, i);
|
|
590
|
+
if (SHAPES.has(keyword)) {
|
|
591
|
+
parts.push(keyword);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
parts.push(`${SHAPE_COLUMN} ${quotePajekLabel(keyword)}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const column of nodeParams) {
|
|
598
|
+
if (column.isSet(i)) {
|
|
599
|
+
parts.push(`${column.meta.name} ${quotePajekLabel(cellText(column, i))}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (mangle && ids.isChanged(i)) {
|
|
603
|
+
parts.push(`${ORIGINAL_ID_KEY} ${quotePajekLabel(idText(ids.originalAt(i)))}`);
|
|
604
|
+
}
|
|
605
|
+
if (nodeSpells !== null) {
|
|
606
|
+
const spells = spellsText(nodeSpells, i);
|
|
607
|
+
if (spells !== null) {
|
|
608
|
+
parts.push(spells);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const label = labelOf(labels, ids, i, parts.length > 0);
|
|
612
|
+
let line = String(i + 1);
|
|
613
|
+
if (label !== null) {
|
|
614
|
+
line += ` ${quotePajekLabel(label)}`;
|
|
615
|
+
}
|
|
616
|
+
if (parts.length > 0) {
|
|
617
|
+
line += ` ${parts.join(" ")}`;
|
|
618
|
+
}
|
|
619
|
+
yield `${line}\n`;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The line sections: runs of `*Arcs` / `*Edges` (with a relation number and name when the edge
|
|
624
|
+
* has one), one line per written edge with its weight, parameters and interval.
|
|
625
|
+
* @param snapshot - the snapshot
|
|
626
|
+
* @param edgePlan - the edge column plan
|
|
627
|
+
* @yields one line per section header and edge
|
|
628
|
+
* @returns nothing
|
|
629
|
+
*/
|
|
630
|
+
function* writeLines(snapshot, edgePlan) {
|
|
631
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
632
|
+
const weights = explicitWeights(snapshot);
|
|
633
|
+
const { src, dst } = snapshot.edgeList();
|
|
634
|
+
let relation = null;
|
|
635
|
+
let edgeSpells = null;
|
|
636
|
+
const edgeParams = [];
|
|
637
|
+
for (const { column, slot } of edgePlan) {
|
|
638
|
+
switch (slot) {
|
|
639
|
+
case "relation":
|
|
640
|
+
relation = column;
|
|
641
|
+
break;
|
|
642
|
+
case "spells":
|
|
643
|
+
edgeSpells = column;
|
|
644
|
+
break;
|
|
645
|
+
case "param":
|
|
646
|
+
edgeParams.push(column);
|
|
647
|
+
break;
|
|
648
|
+
case "label":
|
|
649
|
+
case "position":
|
|
650
|
+
case "shape":
|
|
651
|
+
case "skip":
|
|
652
|
+
break;
|
|
653
|
+
default: {
|
|
654
|
+
const name = slot;
|
|
655
|
+
throw new Error(`unknown slot ${name}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const relationNumbers = new Map();
|
|
660
|
+
let currentKind = null;
|
|
661
|
+
let currentRelation = null;
|
|
662
|
+
let sectionOpen = false;
|
|
663
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
664
|
+
if (folding.folded(e)) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
// a mutual pair (design section 3.6): both directions, written once as an undirected edge
|
|
668
|
+
const directed = snapshot.directed && folding.sourceDirected(e) && !folding.isMutual(e);
|
|
669
|
+
const kind = directed ? "arcs" : "edges";
|
|
670
|
+
const relationName = relation !== null && relation.isSet(e) ? cellText(relation, e) : null;
|
|
671
|
+
if (!sectionOpen || kind !== currentKind || relationName !== currentRelation) {
|
|
672
|
+
currentKind = kind;
|
|
673
|
+
currentRelation = relationName;
|
|
674
|
+
sectionOpen = true;
|
|
675
|
+
const keyword = kind === "arcs" ? "*Arcs" : "*Edges";
|
|
676
|
+
if (relationName === null) {
|
|
677
|
+
yield `${keyword}\n`;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
let number = relationNumbers.get(relationName);
|
|
681
|
+
if (number === undefined) {
|
|
682
|
+
number = relationNumbers.size + 1;
|
|
683
|
+
relationNumbers.set(relationName, number);
|
|
684
|
+
}
|
|
685
|
+
yield `${keyword} :${number} ${quotePajekLabel(relationName)}\n`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
let line = `${src[e] + 1} ${dst[e] + 1}`;
|
|
689
|
+
const weight = weights.text(e);
|
|
690
|
+
if (weight !== null) {
|
|
691
|
+
line += ` ${weight}`;
|
|
692
|
+
}
|
|
693
|
+
for (const column of edgeParams) {
|
|
694
|
+
if (column.isSet(e)) {
|
|
695
|
+
line += ` ${column.meta.name} ${quotePajekLabel(cellText(column, e))}`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (edgeSpells !== null) {
|
|
699
|
+
const spells = spellsText(edgeSpells, e);
|
|
700
|
+
if (spells !== null) {
|
|
701
|
+
line += ` ${spells}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
yield `${line}\n`;
|
|
705
|
+
}
|
|
706
|
+
if (!sectionOpen) {
|
|
707
|
+
yield snapshot.directed ? "*Arcs\n" : "*Edges\n";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Resolve the Pajek export options.
|
|
712
|
+
* @param options - the caller's options
|
|
713
|
+
* @returns the resolved format options; the common options are checked by resolveExportOptions
|
|
714
|
+
*/
|
|
715
|
+
function resolvePajekOptions(options) {
|
|
716
|
+
const common = resolveExportOptions(options);
|
|
717
|
+
const value = options?.networkHeader;
|
|
718
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
719
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option networkHeader: not a boolean", {
|
|
720
|
+
option: "networkHeader",
|
|
721
|
+
found: typeof value,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return { networkHeader: value ?? false, mangle: common.sanitizeIds === "mangle" };
|
|
725
|
+
}
|
|
726
|
+
/** The Pajek NET exporter. */
|
|
727
|
+
export const pajekExporter = Object.freeze({
|
|
728
|
+
format: "pajek",
|
|
729
|
+
capabilities: CAPABILITIES,
|
|
730
|
+
/**
|
|
731
|
+
* Pre-flight: the generic capability gaps plus the Pajek-specific ones.
|
|
732
|
+
* @param snapshot - the snapshot
|
|
733
|
+
* @param options - export options
|
|
734
|
+
* @returns the loss notes, empty when the export is exact
|
|
735
|
+
*/
|
|
736
|
+
check(snapshot, options) {
|
|
737
|
+
const resolved = resolveExportOptions(options);
|
|
738
|
+
const { mangle } = resolvePajekOptions(options);
|
|
739
|
+
const notes = [];
|
|
740
|
+
const planned = plan(snapshot.nodes, "node", notes);
|
|
741
|
+
const nodePlan = withoutReservedKey(planned, mangle);
|
|
742
|
+
const edgePlan = plan(snapshot.edges, "edge", notes);
|
|
743
|
+
const ids = sanitizeIds(snapshot, CAPABILITIES.idCharset, mangle ? "mangle" : "error");
|
|
744
|
+
checkIdTexts(snapshot.nodeCount, ids, mangle, notes);
|
|
745
|
+
if (nodePlan.length !== planned.length) {
|
|
746
|
+
const reserved = planned.find((p) => p.slot === "param" && p.column.meta.name === ORIGINAL_ID_KEY);
|
|
747
|
+
if (reserved !== undefined) {
|
|
748
|
+
notes.push(note(PAJEK_LOSS.KEY_DROPPED, `node column "${ORIGINAL_ID_KEY}" is the parameter the exporter writes the original ids under (sanitizeIds "mangle"); it is not written`, ORIGINAL_ID_KEY, reserved.column.length - reserved.column.nullCount));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// columns this exporter handles itself although the generic table says otherwise: the spells
|
|
752
|
+
// list (written as an interval token, or reported above with the spells code), the shape
|
|
753
|
+
// keyword and the relation name (dict columns written as keywords)
|
|
754
|
+
const owned = new Set();
|
|
755
|
+
for (const { column, slot } of [...nodePlan, ...edgePlan]) {
|
|
756
|
+
if (slot === "shape" || slot === "relation") {
|
|
757
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
758
|
+
}
|
|
759
|
+
else if (slot === "param" && column.meta.domain === "node" && column.meta.name === SHAPE_COLUMN) {
|
|
760
|
+
// the shape-as-parameter note already says the column reads back as string
|
|
761
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
for (const table of [snapshot.nodes, snapshot.edges]) {
|
|
765
|
+
for (const column of table) {
|
|
766
|
+
if (column.meta.role === "spells") {
|
|
767
|
+
owned.add(`${column.meta.domain}:${column.meta.name}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const n of checkCapabilities(snapshot, CAPABILITIES, resolved, {
|
|
772
|
+
roles: SLOT_ROLES,
|
|
773
|
+
roleNames: ROLE_NAMES,
|
|
774
|
+
})) {
|
|
775
|
+
if (n.column !== null && (n.code === LOSS.LIST || n.code === LOSS.DTYPE)) {
|
|
776
|
+
const domain = n.message.startsWith("node column") ? "node" : "edge";
|
|
777
|
+
if (owned.has(`${domain}:${n.column}`)) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (n.code === LOSS.COLUMN_NAME_CHANGED && n.message.startsWith("edge column")) {
|
|
782
|
+
// the label slot is a vertex slot; an edge label is a parameter under its own name
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
notes.push(n);
|
|
786
|
+
}
|
|
787
|
+
labelNotes(snapshot, nodePlan, notes);
|
|
788
|
+
for (const column of snapshot.edges) {
|
|
789
|
+
if (column.meta.role === "label") {
|
|
790
|
+
notes.push(note(LOSS.ROLE, `edge column "${column.meta.name}" (label) is written as a plain parameter; Pajek labels vertices only and the role is lost`, column.meta.name, column.length - column.nullCount));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
checkCells(nodePlan, "node", notes);
|
|
794
|
+
checkCells(edgePlan, "edge", notes);
|
|
795
|
+
const folding = pairFolding(snapshot, { foldMutual: true });
|
|
796
|
+
if (folding.mutualCount > 0) {
|
|
797
|
+
notes.push(note(PAJEK_LOSS.MUTUAL_AS_UNDIRECTED, `${folding.mutualCount} mutual pair(s) are written as undirected edges; the mutual mark is lost`, snapshot.edges.byRole("mutual")?.meta.name ?? null, folding.mutualCount));
|
|
798
|
+
}
|
|
799
|
+
if (Number.isNaN(firstModeOf(snapshot))) {
|
|
800
|
+
notes.push(note(PAJEK_LOSS.FIRST_MODE_DROPPED, "meta.extra.pajek.firstMode is not a count within 0..nodeCount; the two-mode header is not written"));
|
|
801
|
+
}
|
|
802
|
+
return notes;
|
|
803
|
+
},
|
|
804
|
+
/**
|
|
805
|
+
* Write the snapshot as UTF-8 chunks.
|
|
806
|
+
* @param snapshot - the snapshot
|
|
807
|
+
* @param options - export options
|
|
808
|
+
* @returns the chunks
|
|
809
|
+
*/
|
|
810
|
+
export(snapshot, options) {
|
|
811
|
+
const resolved = resolvePajekOptions(options);
|
|
812
|
+
return encodeChunks(writeParts(snapshot, resolved));
|
|
813
|
+
},
|
|
814
|
+
/**
|
|
815
|
+
* Write the snapshot as one string.
|
|
816
|
+
* @param snapshot - the snapshot
|
|
817
|
+
* @param options - export options
|
|
818
|
+
* @returns the document
|
|
819
|
+
*/
|
|
820
|
+
exportToString(snapshot, options) {
|
|
821
|
+
const resolved = resolvePajekOptions(options);
|
|
822
|
+
return joinText(writeParts(snapshot, resolved));
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
//# sourceMappingURL=exporter.js.map
|