@graphty/graph-io 0.3.40 → 0.3.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
- package/dist/chunks/children-CA4coBi0.js.map +1 -0
- package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
- package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
- package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
- package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
- package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
- package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
- package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
- package/dist/chunks/text-DHOJXnED.js.map +1 -0
- package/dist/csv.js +323 -241
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +195 -97
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +27 -27
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +50 -27
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +86 -42
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +358 -184
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/lists.js +30 -15
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +288 -146
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +129 -80
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +78 -36
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +39 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +112 -66
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +65 -37
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +34 -24
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +17 -13
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +54 -20
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +7 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +79 -40
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +38 -12
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/children.ts +54 -27
- package/src/common/attributes.ts +118 -49
- package/src/common/input.ts +439 -197
- package/src/common/lists.ts +29 -15
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +377 -175
- package/src/formats/csv/importer.ts +155 -84
- package/src/formats/csv/records.ts +90 -35
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/dot/tokenizer.ts +118 -68
- package/src/formats/gexf/exporter.ts +67 -36
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +44 -24
- package/src/formats/graphml/importer.ts +18 -11
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/dialect.ts +61 -22
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +88 -40
- package/src/formats/xgmml/exporter.ts +40 -12
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
- package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
- package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
- package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
- package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
- package/dist/chunks/importer-DcKB97kW.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
|
@@ -177,129 +177,192 @@ interface PlannedColumn {
|
|
|
177
177
|
*/
|
|
178
178
|
function plan(table: Iterable<Column>, domain: "node" | "edge", notes: LossNote[] | null): PlannedColumn[] {
|
|
179
179
|
const planned: PlannedColumn[] = [];
|
|
180
|
-
const rows = (column: Column): number => column.length - column.nullCount;
|
|
181
180
|
for (const column of table) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (dtype !== "string" && dtype !== "dict" && notes !== null) {
|
|
186
|
-
notes.push(
|
|
187
|
-
note(
|
|
188
|
-
PAJEK_LOSS.LABEL_AS_TEXT,
|
|
189
|
-
`node column "${name}" (label) is ${dtype}; labels are text and re-import as string`,
|
|
190
|
-
name,
|
|
191
|
-
rows(column),
|
|
192
|
-
),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
planned.push({ column, slot: "label" });
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (role === "position") {
|
|
199
|
-
if (domain === "node" && (meta.components === 2 || meta.components === 3)) {
|
|
200
|
-
planned.push({ column, slot: "position" });
|
|
201
|
-
} else if (notes !== null && domain === "node") {
|
|
202
|
-
notes.push(
|
|
203
|
-
note(
|
|
204
|
-
PAJEK_LOSS.POSITION_STRIDE,
|
|
205
|
-
`node column "${name}" (position) has ${meta.components} components; Pajek coordinates are x y [z]`,
|
|
206
|
-
name,
|
|
207
|
-
rows(column),
|
|
208
|
-
),
|
|
209
|
-
);
|
|
210
|
-
} else if (notes !== null) {
|
|
211
|
-
notes.push(
|
|
212
|
-
note(LOSS.POSITIONS, `edge column "${name}" (position) cannot be written`, name, rows(column)),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (role === "spells") {
|
|
218
|
-
if (
|
|
219
|
-
dtype === "list" &&
|
|
220
|
-
(meta.itemDtype === "f64" || meta.itemDtype === "i32" || meta.itemDtype === "f32")
|
|
221
|
-
) {
|
|
222
|
-
planned.push({ column, slot: "spells" });
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (notes !== null) {
|
|
226
|
-
notes.push(
|
|
227
|
-
note(
|
|
228
|
-
LOSS.SPELLS,
|
|
229
|
-
`${domain} column "${name}" (spells) is not a list of number pairs; it cannot be written`,
|
|
230
|
-
name,
|
|
231
|
-
rows(column),
|
|
232
|
-
),
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
continue;
|
|
181
|
+
const slot = columnSlot(column, domain, notes);
|
|
182
|
+
if (slot !== null) {
|
|
183
|
+
planned.push({ column, slot });
|
|
236
184
|
}
|
|
237
|
-
if (role !== null && DROPPED_TEMPORAL_ROLES.has(role)) {
|
|
238
|
-
if (notes !== null) {
|
|
239
|
-
notes.push(
|
|
240
|
-
note(
|
|
241
|
-
PAJEK_LOSS.TEMPORAL_DROPPED,
|
|
242
|
-
`${domain} column "${name}" (${role}) cannot be written; Pajek intervals come from the spells role`,
|
|
243
|
-
name,
|
|
244
|
-
rows(column),
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (role !== null && (STRUCTURAL_ROLES.has(role) || CHECKED_ROLES.has(role))) {
|
|
251
|
-
// structural columns are folded into the topology; viz and hierarchy roles are reported by the generic checker
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (domain === "edge" && role === "id") {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (dtype === "list" || dtype === "json") {
|
|
258
|
-
// reported by the generic checker (lists: false, json: false)
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (domain === "node" && name === SHAPE_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
262
|
-
if (allShapeKeywords(column)) {
|
|
263
|
-
planned.push({ column, slot: "shape" });
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
// a value outside the keywords: the whole column is a `shape "<text>"` parameter, which the
|
|
267
|
-
// importer reads back as one string column (a change for a dict column only)
|
|
268
|
-
if (notes !== null && dtype === "dict") {
|
|
269
|
-
notes.push(
|
|
270
|
-
note(
|
|
271
|
-
PAJEK_LOSS.SHAPE_AS_PARAMETER,
|
|
272
|
-
`node column "${name}" holds values outside the Pajek shape keywords; it is written as a parameter and reads back as string`,
|
|
273
|
-
name,
|
|
274
|
-
rows(column),
|
|
275
|
-
),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
planned.push({ column, slot: "param" });
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (domain === "edge" && name === RELATION_COLUMN && (dtype === "dict" || dtype === "string")) {
|
|
282
|
-
planned.push({ column, slot: "relation" });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (!isParameterKey(name)) {
|
|
286
|
-
if (notes !== null) {
|
|
287
|
-
notes.push(
|
|
288
|
-
note(
|
|
289
|
-
PAJEK_LOSS.KEY_DROPPED,
|
|
290
|
-
`${domain} column "${name}" cannot be a Pajek parameter key; it is not written`,
|
|
291
|
-
name,
|
|
292
|
-
rows(column),
|
|
293
|
-
),
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
planned.push({ column, slot: "param" });
|
|
299
185
|
}
|
|
300
186
|
return planned;
|
|
301
187
|
}
|
|
302
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Record a note about a whole column, counting its set values; nothing when writing.
|
|
191
|
+
* @param notes - the note list check() fills; null when writing
|
|
192
|
+
* @param code - the code
|
|
193
|
+
* @param message - the message
|
|
194
|
+
* @param column - the column the note is about
|
|
195
|
+
*/
|
|
196
|
+
function columnNote(notes: LossNote[] | null, code: string, message: string, column: Column): void {
|
|
197
|
+
notes?.push(note(code, message, column.meta.name, column.length - column.nullCount));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The slot of one column, recording a note for a column that is not written as it is.
|
|
202
|
+
* @param column - the column
|
|
203
|
+
* @param domain - node or edge
|
|
204
|
+
* @param notes - the note list check() fills; null when writing
|
|
205
|
+
* @returns the slot, or null when the column is not written
|
|
206
|
+
*/
|
|
207
|
+
function columnSlot(column: Column, domain: "node" | "edge", notes: LossNote[] | null): Slot | null {
|
|
208
|
+
const { role, name } = column.meta;
|
|
209
|
+
if (role === "label" && domain === "node") {
|
|
210
|
+
return labelSlot(column, notes);
|
|
211
|
+
}
|
|
212
|
+
if (role === "position") {
|
|
213
|
+
return positionSlot(column, domain, notes);
|
|
214
|
+
}
|
|
215
|
+
if (role === "spells") {
|
|
216
|
+
return spellsSlot(column, domain, notes);
|
|
217
|
+
}
|
|
218
|
+
if (role !== null && DROPPED_TEMPORAL_ROLES.has(role)) {
|
|
219
|
+
columnNote(
|
|
220
|
+
notes,
|
|
221
|
+
PAJEK_LOSS.TEMPORAL_DROPPED,
|
|
222
|
+
`${domain} column "${name}" (${role}) cannot be written; Pajek intervals come from the spells role`,
|
|
223
|
+
column,
|
|
224
|
+
);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (writtenElsewhere(column, domain)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return namedSlot(column, domain, notes);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The node label column: the label slot, noting a label that is not text.
|
|
235
|
+
* @param column - the label role column
|
|
236
|
+
* @param notes - the note list check() fills; null when writing
|
|
237
|
+
* @returns the label slot
|
|
238
|
+
*/
|
|
239
|
+
function labelSlot(column: Column, notes: LossNote[] | null): Slot {
|
|
240
|
+
const { name, dtype } = column.meta;
|
|
241
|
+
if (dtype !== "string" && dtype !== "dict") {
|
|
242
|
+
columnNote(
|
|
243
|
+
notes,
|
|
244
|
+
PAJEK_LOSS.LABEL_AS_TEXT,
|
|
245
|
+
`node column "${name}" (label) is ${dtype}; labels are text and re-import as string`,
|
|
246
|
+
column,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return "label";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A position role column: the coordinates of a node with 2 or 3 components, else not written.
|
|
254
|
+
* @param column - the position role column
|
|
255
|
+
* @param domain - node or edge
|
|
256
|
+
* @param notes - the note list check() fills; null when writing
|
|
257
|
+
* @returns the position slot, or null
|
|
258
|
+
*/
|
|
259
|
+
function positionSlot(column: Column, domain: "node" | "edge", notes: LossNote[] | null): Slot | null {
|
|
260
|
+
const { name, components } = column.meta;
|
|
261
|
+
if (domain === "edge") {
|
|
262
|
+
columnNote(notes, LOSS.POSITIONS, `edge column "${name}" (position) cannot be written`, column);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (components === 2 || components === 3) {
|
|
266
|
+
return "position";
|
|
267
|
+
}
|
|
268
|
+
columnNote(
|
|
269
|
+
notes,
|
|
270
|
+
PAJEK_LOSS.POSITION_STRIDE,
|
|
271
|
+
`node column "${name}" (position) has ${components} components; Pajek coordinates are x y [z]`,
|
|
272
|
+
column,
|
|
273
|
+
);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A spells role column: the interval slot for a list of numbers, else not written.
|
|
279
|
+
* @param column - the spells role column
|
|
280
|
+
* @param domain - node or edge
|
|
281
|
+
* @param notes - the note list check() fills; null when writing
|
|
282
|
+
* @returns the spells slot, or null
|
|
283
|
+
*/
|
|
284
|
+
function spellsSlot(column: Column, domain: "node" | "edge", notes: LossNote[] | null): Slot | null {
|
|
285
|
+
const { name, dtype, itemDtype } = column.meta;
|
|
286
|
+
if (dtype === "list" && (itemDtype === "f64" || itemDtype === "i32" || itemDtype === "f32")) {
|
|
287
|
+
return "spells";
|
|
288
|
+
}
|
|
289
|
+
columnNote(
|
|
290
|
+
notes,
|
|
291
|
+
LOSS.SPELLS,
|
|
292
|
+
`${domain} column "${name}" (spells) is not a list of number pairs; it cannot be written`,
|
|
293
|
+
column,
|
|
294
|
+
);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether a column is folded into the topology, reported by the generic checker, or an edge id,
|
|
300
|
+
* so it is never a parameter.
|
|
301
|
+
* @param column - the column
|
|
302
|
+
* @param domain - node or edge
|
|
303
|
+
* @returns true when the column has no slot of its own
|
|
304
|
+
*/
|
|
305
|
+
function writtenElsewhere(column: Column, domain: "node" | "edge"): boolean {
|
|
306
|
+
const { role, dtype } = column.meta;
|
|
307
|
+
// structural columns are folded into the topology; viz and hierarchy roles, lists and json are
|
|
308
|
+
// reported by the generic checker (lists: false, json: false)
|
|
309
|
+
if (role !== null && (STRUCTURAL_ROLES.has(role) || CHECKED_ROLES.has(role))) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return (domain === "edge" && role === "id") || dtype === "list" || dtype === "json";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The slot of a column written by its name: the shape keyword, the relation name, or a parameter.
|
|
317
|
+
* @param column - the column
|
|
318
|
+
* @param domain - node or edge
|
|
319
|
+
* @param notes - the note list check() fills; null when writing
|
|
320
|
+
* @returns the slot, or null when the name cannot be a parameter key
|
|
321
|
+
*/
|
|
322
|
+
function namedSlot(column: Column, domain: "node" | "edge", notes: LossNote[] | null): Slot | null {
|
|
323
|
+
const { name, dtype } = column.meta;
|
|
324
|
+
const text = dtype === "dict" || dtype === "string";
|
|
325
|
+
if (text && domain === "node" && name === SHAPE_COLUMN) {
|
|
326
|
+
return shapeSlot(column, notes);
|
|
327
|
+
}
|
|
328
|
+
if (text && domain === "edge" && name === RELATION_COLUMN) {
|
|
329
|
+
return "relation";
|
|
330
|
+
}
|
|
331
|
+
if (!isParameterKey(name)) {
|
|
332
|
+
columnNote(
|
|
333
|
+
notes,
|
|
334
|
+
PAJEK_LOSS.KEY_DROPPED,
|
|
335
|
+
`${domain} column "${name}" cannot be a Pajek parameter key; it is not written`,
|
|
336
|
+
column,
|
|
337
|
+
);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return "param";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A text node column named `shape`: the shape slot when every value is a keyword, else the whole
|
|
345
|
+
* column is a `shape "<text>"` parameter, which the importer reads back as one string column (a
|
|
346
|
+
* change for a dict column only).
|
|
347
|
+
* @param column - the shape column (string or dict)
|
|
348
|
+
* @param notes - the note list check() fills; null when writing
|
|
349
|
+
* @returns the shape or param slot
|
|
350
|
+
*/
|
|
351
|
+
function shapeSlot(column: Column, notes: LossNote[] | null): Slot {
|
|
352
|
+
if (allShapeKeywords(column)) {
|
|
353
|
+
return "shape";
|
|
354
|
+
}
|
|
355
|
+
if (column.meta.dtype === "dict") {
|
|
356
|
+
columnNote(
|
|
357
|
+
notes,
|
|
358
|
+
PAJEK_LOSS.SHAPE_AS_PARAMETER,
|
|
359
|
+
`node column "${column.meta.name}" holds values outside the Pajek shape keywords; it is written as a parameter and reads back as string`,
|
|
360
|
+
column,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return "param";
|
|
364
|
+
}
|
|
365
|
+
|
|
303
366
|
/**
|
|
304
367
|
* The notes about vertex labels: Pajek's grammar puts the label second, so a vertex line that
|
|
305
368
|
* carries coordinates, a shape, parameters or an interval needs one, and a node without a label
|
|
@@ -686,21 +686,7 @@ class PajekParser {
|
|
|
686
686
|
this.section = "none";
|
|
687
687
|
return;
|
|
688
688
|
case "vertices":
|
|
689
|
-
|
|
690
|
-
// the `*Vertices N` line of a project object (*Partition, *Vector, a skipped
|
|
691
|
-
// section), wherever it stands: never the network's own
|
|
692
|
-
this.section = previous;
|
|
693
|
-
if (previous === "object") {
|
|
694
|
-
this.objects[this.objects.length - 1].count = h.count;
|
|
695
|
-
}
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
if (this.vertexCount >= 0) {
|
|
699
|
-
// a second `*Vertices` without a `*Network` header starts the next network
|
|
700
|
-
this.ended = true;
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
this.verticesHeader(h, line);
|
|
689
|
+
this.verticesSectionLine(h, line, previous, afterObjectHeader);
|
|
704
690
|
return;
|
|
705
691
|
case "arcs":
|
|
706
692
|
case "edges":
|
|
@@ -739,6 +725,32 @@ class PajekParser {
|
|
|
739
725
|
}
|
|
740
726
|
}
|
|
741
727
|
|
|
728
|
+
/**
|
|
729
|
+
* A `*Vertices` line: a project object's own count, the start of the next network, or the
|
|
730
|
+
* network's vertices header.
|
|
731
|
+
* @param h - the header
|
|
732
|
+
* @param line - the line number
|
|
733
|
+
* @param previous - the section before this header
|
|
734
|
+
* @param afterObjectHeader - the previous line was a `*Partition`, `*Vector` or skipped section header
|
|
735
|
+
*/
|
|
736
|
+
private verticesSectionLine(h: SectionHeader, line: number, previous: Section, afterObjectHeader: boolean): void {
|
|
737
|
+
if (afterObjectHeader) {
|
|
738
|
+
// the `*Vertices N` line of a project object (*Partition, *Vector, a skipped
|
|
739
|
+
// section), wherever it stands: never the network's own
|
|
740
|
+
this.section = previous;
|
|
741
|
+
if (previous === "object") {
|
|
742
|
+
this.objects[this.objects.length - 1].count = h.count;
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (this.vertexCount >= 0) {
|
|
747
|
+
// a second `*Vertices` without a `*Network` header starts the next network
|
|
748
|
+
this.ended = true;
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
this.verticesHeader(h, line);
|
|
752
|
+
}
|
|
753
|
+
|
|
742
754
|
/**
|
|
743
755
|
* `*Vertices N [N1]`.
|
|
744
756
|
* @param h - the header
|
|
@@ -350,57 +350,88 @@ export function parseSectionHeader(line: string): SectionHeader | null {
|
|
|
350
350
|
}
|
|
351
351
|
const keyword = tokens[0];
|
|
352
352
|
const kind = SECTION_KINDS.get(keyword.toLowerCase()) ?? "unsupported";
|
|
353
|
-
let
|
|
354
|
-
let secondCount: number | null = null;
|
|
355
|
-
let relation: number | null = null;
|
|
356
|
-
let name: string | null = null;
|
|
357
|
-
const extra: string[] = [];
|
|
358
|
-
let i = 1;
|
|
353
|
+
let args: HeaderArgs;
|
|
359
354
|
switch (kind) {
|
|
360
355
|
case "vertices":
|
|
361
|
-
|
|
362
|
-
count = Number(tokens[i]);
|
|
363
|
-
i++;
|
|
364
|
-
if (i < tokens.length && INTEGER_TEXT.test(tokens[i])) {
|
|
365
|
-
secondCount = Number(tokens[i]);
|
|
366
|
-
i++;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
356
|
+
args = verticesArgs(tokens);
|
|
369
357
|
break;
|
|
370
358
|
case "arcs":
|
|
371
359
|
case "edges":
|
|
372
360
|
case "arcslist":
|
|
373
361
|
case "edgeslist":
|
|
374
362
|
case "matrix":
|
|
375
|
-
|
|
376
|
-
relation = Number(tokens[i].slice(1));
|
|
377
|
-
i++;
|
|
378
|
-
if (i < tokens.length) {
|
|
379
|
-
name = tokens[i];
|
|
380
|
-
i++;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
363
|
+
args = relationArgs(tokens);
|
|
383
364
|
break;
|
|
384
365
|
case "network":
|
|
385
366
|
case "partition":
|
|
386
367
|
case "vector":
|
|
387
|
-
|
|
388
|
-
name = tokens.slice(i).join(" ");
|
|
389
|
-
i = tokens.length;
|
|
390
|
-
}
|
|
368
|
+
args = nameArgs(tokens);
|
|
391
369
|
break;
|
|
392
370
|
case "unsupported":
|
|
393
|
-
|
|
371
|
+
args = { ...NO_ARGS, next: tokens.length };
|
|
394
372
|
break;
|
|
395
373
|
default: {
|
|
396
374
|
const unknown: string = kind;
|
|
397
375
|
throw new Error(`unknown section kind ${unknown}`);
|
|
398
376
|
}
|
|
399
377
|
}
|
|
400
|
-
|
|
401
|
-
|
|
378
|
+
const { count, secondCount, relation, name, next } = args;
|
|
379
|
+
return { kind, keyword, count, secondCount, relation, name, extra: tokens.slice(next) };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The arguments a section header's keyword takes, and the index of the first token after them. */
|
|
383
|
+
interface HeaderArgs {
|
|
384
|
+
readonly count: number | null;
|
|
385
|
+
readonly secondCount: number | null;
|
|
386
|
+
readonly relation: number | null;
|
|
387
|
+
readonly name: string | null;
|
|
388
|
+
readonly next: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const NO_ARGS: HeaderArgs = { count: null, secondCount: null, relation: null, name: null, next: 1 };
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* `*Vertices N [N1]`: the vertex count and the optional first-mode count.
|
|
395
|
+
* @param tokens - the header tokens, the keyword first
|
|
396
|
+
* @returns the arguments
|
|
397
|
+
*/
|
|
398
|
+
function verticesArgs(tokens: readonly string[]): HeaderArgs {
|
|
399
|
+
if (tokens.length < 2 || !INTEGER_TEXT.test(tokens[1])) {
|
|
400
|
+
return NO_ARGS;
|
|
401
|
+
}
|
|
402
|
+
const count = Number(tokens[1]);
|
|
403
|
+
if (tokens.length < 3 || !INTEGER_TEXT.test(tokens[2])) {
|
|
404
|
+
return { ...NO_ARGS, count, next: 2 };
|
|
402
405
|
}
|
|
403
|
-
return {
|
|
406
|
+
return { ...NO_ARGS, count, secondCount: Number(tokens[2]), next: 3 };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* `*Arcs [:k ["name"]]` and the other line sections: the relation number and its name.
|
|
411
|
+
* @param tokens - the header tokens, the keyword first
|
|
412
|
+
* @returns the arguments
|
|
413
|
+
*/
|
|
414
|
+
function relationArgs(tokens: readonly string[]): HeaderArgs {
|
|
415
|
+
if (tokens.length < 2 || !tokens[1].startsWith(":") || !INTEGER_TEXT.test(tokens[1].slice(1))) {
|
|
416
|
+
return NO_ARGS;
|
|
417
|
+
}
|
|
418
|
+
const relation = Number(tokens[1].slice(1));
|
|
419
|
+
if (tokens.length < 3) {
|
|
420
|
+
return { ...NO_ARGS, relation, next: 2 };
|
|
421
|
+
}
|
|
422
|
+
return { ...NO_ARGS, relation, name: tokens[2], next: 3 };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* `*Network name`, `*Partition name`, `*Vector name`: every token after the keyword is the name.
|
|
427
|
+
* @param tokens - the header tokens, the keyword first
|
|
428
|
+
* @returns the arguments
|
|
429
|
+
*/
|
|
430
|
+
function nameArgs(tokens: readonly string[]): HeaderArgs {
|
|
431
|
+
if (tokens.length < 2) {
|
|
432
|
+
return NO_ARGS;
|
|
433
|
+
}
|
|
434
|
+
return { ...NO_ARGS, name: tokens.slice(1).join(" "), next: tokens.length };
|
|
404
435
|
}
|
|
405
436
|
|
|
406
437
|
/**
|
|
@@ -447,38 +478,48 @@ export function parseIntervals(token: string): [number, number][] {
|
|
|
447
478
|
}
|
|
448
479
|
const spells: [number, number][] = [];
|
|
449
480
|
for (const part of body.split(",")) {
|
|
450
|
-
|
|
451
|
-
if (match === null) {
|
|
452
|
-
throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
|
|
453
|
-
}
|
|
454
|
-
const [, startText, endText] = match;
|
|
455
|
-
if (startText === "*" && endText === undefined) {
|
|
456
|
-
throw new Error(`malformed time interval ${token}: "*" alone is not a time point`);
|
|
457
|
-
}
|
|
458
|
-
const start = startText === "*" ? -Infinity : Number(startText);
|
|
459
|
-
let end: number;
|
|
460
|
-
if (endText === undefined) {
|
|
461
|
-
end = start;
|
|
462
|
-
} else if (endText === "*") {
|
|
463
|
-
end = Infinity;
|
|
464
|
-
} else {
|
|
465
|
-
end = Number(endText);
|
|
466
|
-
}
|
|
467
|
-
// only `*` means an open end: a numeral beyond the f64 range is not a time
|
|
468
|
-
if (
|
|
469
|
-
(startText !== "*" && !Number.isFinite(start)) ||
|
|
470
|
-
(endText !== undefined && endText !== "*" && !Number.isFinite(end))
|
|
471
|
-
) {
|
|
472
|
-
throw new Error(`malformed time interval ${token}: "${part}" is beyond the number range`);
|
|
473
|
-
}
|
|
474
|
-
if (start > end) {
|
|
475
|
-
throw new Error(`malformed time interval ${token}: ${startText} is after ${endText ?? ""}`);
|
|
476
|
-
}
|
|
477
|
-
spells.push([start, end]);
|
|
481
|
+
spells.push(parseSpell(token, part));
|
|
478
482
|
}
|
|
479
483
|
return spells;
|
|
480
484
|
}
|
|
481
485
|
|
|
486
|
+
/**
|
|
487
|
+
* One part of a Pajek time interval token: `a-b`, `a-*`, `*-b` or a single time point `a`.
|
|
488
|
+
* @param token - the whole token, for the error message
|
|
489
|
+
* @param part - the part between commas
|
|
490
|
+
* @returns the spell as a [start, end] pair
|
|
491
|
+
*/
|
|
492
|
+
function parseSpell(token: string, part: string): [number, number] {
|
|
493
|
+
const match = TIME_POINT.exec(part.replaceAll(/\s+/g, ""));
|
|
494
|
+
if (match === null) {
|
|
495
|
+
throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
|
|
496
|
+
}
|
|
497
|
+
const [, startText, endText] = match;
|
|
498
|
+
if (startText === "*" && endText === undefined) {
|
|
499
|
+
throw new Error(`malformed time interval ${token}: "*" alone is not a time point`);
|
|
500
|
+
}
|
|
501
|
+
const start = startText === "*" ? -Infinity : Number(startText);
|
|
502
|
+
let end: number;
|
|
503
|
+
if (endText === undefined) {
|
|
504
|
+
end = start;
|
|
505
|
+
} else if (endText === "*") {
|
|
506
|
+
end = Infinity;
|
|
507
|
+
} else {
|
|
508
|
+
end = Number(endText);
|
|
509
|
+
}
|
|
510
|
+
// only `*` means an open end: a numeral beyond the f64 range is not a time
|
|
511
|
+
if (
|
|
512
|
+
(startText !== "*" && !Number.isFinite(start)) ||
|
|
513
|
+
(endText !== undefined && endText !== "*" && !Number.isFinite(end))
|
|
514
|
+
) {
|
|
515
|
+
throw new Error(`malformed time interval ${token}: "${part}" is beyond the number range`);
|
|
516
|
+
}
|
|
517
|
+
if (start > end) {
|
|
518
|
+
throw new Error(`malformed time interval ${token}: ${startText} is after ${endText ?? ""}`);
|
|
519
|
+
}
|
|
520
|
+
return [start, end];
|
|
521
|
+
}
|
|
522
|
+
|
|
482
523
|
// any length, so a reference beyond U+10FFFF is seen (and kept as written with a warning)
|
|
483
524
|
const CHARACTER_REFERENCE = /&#(?:[xX]([\da-fA-F]+)|(\d+));/g;
|
|
484
525
|
|