jsonstat-io 0.1.0
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 +201 -0
- package/README.md +200 -0
- package/dist/arrow/index.cjs +60 -0
- package/dist/arrow/index.cjs.map +1 -0
- package/dist/arrow/index.d.cts +125 -0
- package/dist/arrow/index.d.ts +125 -0
- package/dist/arrow/index.js +3 -0
- package/dist/arrow/index.js.map +1 -0
- package/dist/arrowToCube-BdRWZ5oG.d.cts +29 -0
- package/dist/arrowToCube-Mjr79ZHM.d.ts +29 -0
- package/dist/chunk-2HY7NRKX.js +259 -0
- package/dist/chunk-2HY7NRKX.js.map +1 -0
- package/dist/chunk-62NIY7ML.js +191 -0
- package/dist/chunk-62NIY7ML.js.map +1 -0
- package/dist/chunk-AXUZF6O5.cjs +352 -0
- package/dist/chunk-AXUZF6O5.cjs.map +1 -0
- package/dist/chunk-EFRDFQOT.js +335 -0
- package/dist/chunk-EFRDFQOT.js.map +1 -0
- package/dist/chunk-GDKDYLZ4.cjs +263 -0
- package/dist/chunk-GDKDYLZ4.cjs.map +1 -0
- package/dist/chunk-IDTQZG4B.js +432 -0
- package/dist/chunk-IDTQZG4B.js.map +1 -0
- package/dist/chunk-KFIKGBIJ.cjs +200 -0
- package/dist/chunk-KFIKGBIJ.cjs.map +1 -0
- package/dist/chunk-L65CRXTV.js +64 -0
- package/dist/chunk-L65CRXTV.js.map +1 -0
- package/dist/chunk-PHLCWTHB.cjs +446 -0
- package/dist/chunk-PHLCWTHB.cjs.map +1 -0
- package/dist/chunk-VQCS4LVD.cjs +166 -0
- package/dist/chunk-VQCS4LVD.cjs.map +1 -0
- package/dist/chunk-VYS7DPB3.js +162 -0
- package/dist/chunk-VYS7DPB3.js.map +1 -0
- package/dist/chunk-ZEDTWRDN.cjs +70 -0
- package/dist/chunk-ZEDTWRDN.cjs.map +1 -0
- package/dist/cli/index.cjs +337 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +90 -0
- package/dist/cli/index.d.ts +90 -0
- package/dist/cli/index.js +333 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cubeBuilder-BEGA4C5Y.js +4 -0
- package/dist/cubeBuilder-BEGA4C5Y.js.map +1 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs +25 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs.map +1 -0
- package/dist/cubeReader-7QVUF3TI.js +4 -0
- package/dist/cubeReader-7QVUF3TI.js.map +1 -0
- package/dist/cubeReader-UN2BHQWK.cjs +21 -0
- package/dist/cubeReader-UN2BHQWK.cjs.map +1 -0
- package/dist/index.cjs +172 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +445 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ir-BArfNxo-.d.cts +143 -0
- package/dist/ir-Jae2ymov.d.ts +143 -0
- package/dist/jsonstat-BX4mBLci.d.cts +179 -0
- package/dist/jsonstat-BX4mBLci.d.ts +179 -0
- package/dist/sources/csv.cjs +40 -0
- package/dist/sources/csv.cjs.map +1 -0
- package/dist/sources/csv.d.cts +112 -0
- package/dist/sources/csv.d.ts +112 -0
- package/dist/sources/csv.js +3 -0
- package/dist/sources/csv.js.map +1 -0
- package/dist/sources/csvw.cjs +236 -0
- package/dist/sources/csvw.cjs.map +1 -0
- package/dist/sources/csvw.d.cts +143 -0
- package/dist/sources/csvw.d.ts +143 -0
- package/dist/sources/csvw.js +230 -0
- package/dist/sources/csvw.js.map +1 -0
- package/dist/sources/duckdb.cjs +121 -0
- package/dist/sources/duckdb.cjs.map +1 -0
- package/dist/sources/duckdb.d.cts +111 -0
- package/dist/sources/duckdb.d.ts +111 -0
- package/dist/sources/duckdb.js +115 -0
- package/dist/sources/duckdb.js.map +1 -0
- package/dist/sources/parquet.cjs +117 -0
- package/dist/sources/parquet.cjs.map +1 -0
- package/dist/sources/parquet.d.cts +83 -0
- package/dist/sources/parquet.d.ts +83 -0
- package/dist/sources/parquet.js +111 -0
- package/dist/sources/parquet.js.map +1 -0
- package/dist/sources/polars.cjs +94 -0
- package/dist/sources/polars.cjs.map +1 -0
- package/dist/sources/polars.d.cts +91 -0
- package/dist/sources/polars.d.ts +91 -0
- package/dist/sources/polars.js +87 -0
- package/dist/sources/polars.js.map +1 -0
- package/docs/api.md +322 -0
- package/docs/architecture.md +140 -0
- package/docs/cli.md +159 -0
- package/docs/formats/arrow.md +111 -0
- package/docs/formats/csv.md +115 -0
- package/docs/formats/csvw.md +152 -0
- package/docs/formats/duckdb.md +151 -0
- package/docs/formats/parquet.md +132 -0
- package/docs/formats/polars.md +111 -0
- package/docs/mapping.md +136 -0
- package/package.json +119 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkZEDTWRDN_cjs = require('./chunk-ZEDTWRDN.cjs');
|
|
4
|
+
|
|
5
|
+
// src/core/cubeBuilder.ts
|
|
6
|
+
var CubeBuilderError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CubeBuilderError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function resolveDimension(id, column) {
|
|
13
|
+
let categories;
|
|
14
|
+
if (column.categoryOrder && column.categoryOrder.length > 0) {
|
|
15
|
+
const known = new Set(column.categoryOrder);
|
|
16
|
+
for (const v of column.values) {
|
|
17
|
+
if (!known.has(v)) {
|
|
18
|
+
throw new CubeBuilderError(
|
|
19
|
+
`Dimension "${id}": value "${v}" not present in categoryOrder`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
categories = column.categoryOrder.slice();
|
|
24
|
+
} else {
|
|
25
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26
|
+
categories = [];
|
|
27
|
+
for (const v of column.values) {
|
|
28
|
+
if (v == null) {
|
|
29
|
+
throw new CubeBuilderError(
|
|
30
|
+
`Dimension "${id}": contains a null category value; dimensions cannot be null`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!seen.has(v)) {
|
|
34
|
+
seen.add(v);
|
|
35
|
+
categories.push(v);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const position = /* @__PURE__ */ new Map();
|
|
40
|
+
categories.forEach((c, i) => position.set(c, i));
|
|
41
|
+
return { id, column, categories, position };
|
|
42
|
+
}
|
|
43
|
+
function buildDimensionBlock(resolved) {
|
|
44
|
+
const dimension = {};
|
|
45
|
+
const size = [];
|
|
46
|
+
for (const r of resolved) {
|
|
47
|
+
const cat = {};
|
|
48
|
+
cat.index = r.categories.slice();
|
|
49
|
+
if (r.column.categoryLabels) cat.label = { ...r.column.categoryLabels };
|
|
50
|
+
if (r.column.categoryUnits) cat.unit = { ...r.column.categoryUnits };
|
|
51
|
+
if (r.column.categoryCoordinates)
|
|
52
|
+
cat.coordinates = { ...r.column.categoryCoordinates };
|
|
53
|
+
if (r.column.categoryChild) cat.child = { ...r.column.categoryChild };
|
|
54
|
+
const dim = { category: cat };
|
|
55
|
+
if (r.column.label) dim.label = r.column.label;
|
|
56
|
+
if (r.column.href) dim.href = r.column.href;
|
|
57
|
+
dimension[r.id] = dim;
|
|
58
|
+
size.push(r.categories.length);
|
|
59
|
+
}
|
|
60
|
+
return { dimension, size };
|
|
61
|
+
}
|
|
62
|
+
function scatterValues(obs, resolved, size) {
|
|
63
|
+
const total = chunkZEDTWRDN_cjs.totalCells(size);
|
|
64
|
+
const dense = new Array(total).fill(null);
|
|
65
|
+
const seen = new Uint8Array(total);
|
|
66
|
+
let duplicates = 0;
|
|
67
|
+
const n = obs.measure.values.length;
|
|
68
|
+
const k = resolved.length;
|
|
69
|
+
for (let row = 0; row < n; row++) {
|
|
70
|
+
const indices = new Array(k);
|
|
71
|
+
for (let d = 0; d < k; d++) {
|
|
72
|
+
const r = resolved[d];
|
|
73
|
+
const v = r.column.values[row];
|
|
74
|
+
const p = r.position.get(v);
|
|
75
|
+
if (p === void 0) {
|
|
76
|
+
throw new CubeBuilderError(
|
|
77
|
+
`Dimension "${r.id}": value "${v}" has no position (row ${row})`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
indices[d] = p;
|
|
81
|
+
}
|
|
82
|
+
const pos = chunkZEDTWRDN_cjs.flatPosition(indices, size);
|
|
83
|
+
if (seen[pos] === 1) duplicates++;
|
|
84
|
+
seen[pos] = 1;
|
|
85
|
+
dense[pos] = obs.measure.values[row];
|
|
86
|
+
}
|
|
87
|
+
return { dense, duplicates };
|
|
88
|
+
}
|
|
89
|
+
function chooseValueForm(dense, requested, threshold) {
|
|
90
|
+
const total = dense.length;
|
|
91
|
+
const nullCount = dense.reduce((acc, v) => acc + (v === null ? 1 : 0), 0);
|
|
92
|
+
const nullRatio = total === 0 ? 0 : nullCount / total;
|
|
93
|
+
let form;
|
|
94
|
+
if (requested === "dense") form = "dense";
|
|
95
|
+
else if (requested === "sparse") form = "sparse";
|
|
96
|
+
else form = nullRatio > threshold ? "sparse" : "dense";
|
|
97
|
+
if (form === "dense") {
|
|
98
|
+
return { value: dense, form, nullRatio };
|
|
99
|
+
}
|
|
100
|
+
const obj = {};
|
|
101
|
+
for (let i = 0; i < total; i++) {
|
|
102
|
+
const v = dense[i];
|
|
103
|
+
if (v !== null) obj[String(i)] = v;
|
|
104
|
+
}
|
|
105
|
+
return { value: obj, form, nullRatio };
|
|
106
|
+
}
|
|
107
|
+
function emitStatus(perCell, requested) {
|
|
108
|
+
if (requested === "none") return void 0;
|
|
109
|
+
const total = perCell.length;
|
|
110
|
+
if (total === 0) return void 0;
|
|
111
|
+
const first = perCell[0];
|
|
112
|
+
let uniform = true;
|
|
113
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const s of perCell) {
|
|
115
|
+
distinct.add(s);
|
|
116
|
+
if (s !== first) uniform = false;
|
|
117
|
+
}
|
|
118
|
+
if (requested === "string") return first;
|
|
119
|
+
if (requested === "array") return perCell.slice();
|
|
120
|
+
if (requested === "object") {
|
|
121
|
+
return sparseStatus(perCell);
|
|
122
|
+
}
|
|
123
|
+
if (uniform && first !== "") return first;
|
|
124
|
+
const counts = /* @__PURE__ */ new Map();
|
|
125
|
+
for (const s of perCell) counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
126
|
+
let dominant = "";
|
|
127
|
+
let dominantCount = -1;
|
|
128
|
+
for (const [s, c] of counts) {
|
|
129
|
+
if (c > dominantCount) {
|
|
130
|
+
dominant = s;
|
|
131
|
+
dominantCount = c;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const nonDominant = total - dominantCount;
|
|
135
|
+
if (dominant !== "" && nonDominant / total < 0.4) {
|
|
136
|
+
return sparseStatus(perCell, dominant);
|
|
137
|
+
}
|
|
138
|
+
return perCell.slice();
|
|
139
|
+
}
|
|
140
|
+
function sparseStatus(perCell, defaultStatus) {
|
|
141
|
+
const obj = {};
|
|
142
|
+
for (let i = 0; i < perCell.length; i++) {
|
|
143
|
+
const s = perCell[i];
|
|
144
|
+
if (s === "" || s === defaultStatus) continue;
|
|
145
|
+
obj[String(i)] = s;
|
|
146
|
+
}
|
|
147
|
+
return obj;
|
|
148
|
+
}
|
|
149
|
+
function validate(obs, resolved) {
|
|
150
|
+
const n = obs.measure.values.length;
|
|
151
|
+
for (const r of resolved) {
|
|
152
|
+
if (r.column.values.length !== n) {
|
|
153
|
+
throw new CubeBuilderError(
|
|
154
|
+
`Dimension "${r.id}": ${r.column.values.length} values \u2260 ${n} measure values`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (obs.status && obs.status.values.length !== n) {
|
|
159
|
+
throw new CubeBuilderError(
|
|
160
|
+
`Status column: ${obs.status.values.length} values \u2260 ${n} measure values`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const modelIds = new Set(obs.model.dimensionIds);
|
|
164
|
+
const colIds = new Set(Object.keys(obs.dimensions));
|
|
165
|
+
for (const id of modelIds) {
|
|
166
|
+
if (!colIds.has(id)) {
|
|
167
|
+
throw new CubeBuilderError(`model.dimensionIds lists "${id}" but no column exists`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const id of colIds) {
|
|
171
|
+
if (!modelIds.has(id)) {
|
|
172
|
+
throw new CubeBuilderError(`Column "${id}" is not listed in model.dimensionIds`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function buildDataset(obs, options = {}) {
|
|
177
|
+
const model = obs.model;
|
|
178
|
+
const dimensionIds = model.dimensionIds;
|
|
179
|
+
const resolved = dimensionIds.map((id) => {
|
|
180
|
+
const col = obs.dimensions[id];
|
|
181
|
+
if (!col) throw new CubeBuilderError(`Missing dimension column "${id}"`);
|
|
182
|
+
return resolveDimension(id, col);
|
|
183
|
+
});
|
|
184
|
+
validate(obs, resolved);
|
|
185
|
+
const { dimension, size } = buildDimensionBlock(resolved);
|
|
186
|
+
const { dense, duplicates } = scatterValues(obs, resolved, size);
|
|
187
|
+
const valueForm = options.valueForm ?? model.valueForm ?? "auto";
|
|
188
|
+
const threshold = options.sparseThreshold ?? model.sparseThreshold ?? 0.5;
|
|
189
|
+
const { value, form, nullRatio } = chooseValueForm(dense, valueForm, threshold);
|
|
190
|
+
let status;
|
|
191
|
+
if (obs.status) {
|
|
192
|
+
const statusForm = options.statusForm ?? model.statusForm ?? "auto";
|
|
193
|
+
if (statusForm !== "none") {
|
|
194
|
+
status = buildPerCellStatus(obs, resolved, size, statusForm);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const dataset = {
|
|
198
|
+
version: "2.0",
|
|
199
|
+
class: "dataset",
|
|
200
|
+
id: dimensionIds.slice(),
|
|
201
|
+
size,
|
|
202
|
+
dimension,
|
|
203
|
+
value
|
|
204
|
+
};
|
|
205
|
+
const role = filterRoles(model.roles);
|
|
206
|
+
if (role) dataset.role = role;
|
|
207
|
+
const meta = options.meta ?? model.meta;
|
|
208
|
+
if (meta) {
|
|
209
|
+
if (meta.label) dataset.label = meta.label;
|
|
210
|
+
if (meta.source) dataset.source = meta.source;
|
|
211
|
+
if (meta.updated) dataset.updated = meta.updated;
|
|
212
|
+
if (meta.extension) dataset.extension = meta.extension;
|
|
213
|
+
}
|
|
214
|
+
if (status) dataset.status = status;
|
|
215
|
+
return {
|
|
216
|
+
dataset,
|
|
217
|
+
diagnostics: {
|
|
218
|
+
valueForm: form,
|
|
219
|
+
nullRatio,
|
|
220
|
+
duplicates,
|
|
221
|
+
cellCount: dense.length
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function buildPerCellStatus(obs, resolved, size, statusForm) {
|
|
226
|
+
const total = chunkZEDTWRDN_cjs.totalCells(size);
|
|
227
|
+
const perCell = new Array(total).fill("");
|
|
228
|
+
const n = obs.measure.values.length;
|
|
229
|
+
const k = resolved.length;
|
|
230
|
+
const statusVals = obs.status?.values;
|
|
231
|
+
if (!statusVals) return void 0;
|
|
232
|
+
for (let row = 0; row < n; row++) {
|
|
233
|
+
const indices = new Array(k);
|
|
234
|
+
for (let d = 0; d < k; d++) {
|
|
235
|
+
indices[d] = resolved[d].position.get(resolved[d].column.values[row]);
|
|
236
|
+
}
|
|
237
|
+
const pos = chunkZEDTWRDN_cjs.flatPosition(indices, size);
|
|
238
|
+
perCell[pos] = statusVals[row] ?? "";
|
|
239
|
+
}
|
|
240
|
+
return emitStatus(perCell, statusForm);
|
|
241
|
+
}
|
|
242
|
+
function filterRoles(roles) {
|
|
243
|
+
if (!roles) return void 0;
|
|
244
|
+
const out = {};
|
|
245
|
+
let any = false;
|
|
246
|
+
for (const key of ["time", "geo", "metric"]) {
|
|
247
|
+
const arr = roles[key];
|
|
248
|
+
if (arr && arr.length > 0) {
|
|
249
|
+
out[key] = arr.slice();
|
|
250
|
+
any = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return any ? out : void 0;
|
|
254
|
+
}
|
|
255
|
+
function toDataset(obs, options) {
|
|
256
|
+
return buildDataset(obs, options).dataset;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
exports.CubeBuilderError = CubeBuilderError;
|
|
260
|
+
exports.buildDataset = buildDataset;
|
|
261
|
+
exports.toDataset = toDataset;
|
|
262
|
+
//# sourceMappingURL=chunk-GDKDYLZ4.cjs.map
|
|
263
|
+
//# sourceMappingURL=chunk-GDKDYLZ4.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/cubeBuilder.ts"],"names":["totalCells","flatPosition"],"mappings":"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","file":"chunk-GDKDYLZ4.cjs","sourcesContent":["/**\n * CubeBuilder — turns an [`Observations`](../model/ir.ts) IR into a JSON-stat\n * [`Dataset`](../model/jsonstat.ts).\n *\n * This is the central transform of the importer: a tidy long table → an\n * N-dimensional cube laid out in row-major order (\"what does not change, first\";\n * see wiki/format-specification.md).\n *\n * Responsibilities:\n * 1. Resolve dimension order and enumerate categories (first-seen order unless\n * `categoryOrder` pins it).\n * 2. Build the `dimension` block with `category.index`/`label`/`unit`/\n * `coordinates`/`child` and dimension `label`.\n * 3. Scatter the measure values into a row-major `value` array (dense) or a\n * sparse object, per [`CubeModel.valueForm`](../model/ir.ts) and the null\n * ratio threshold.\n * 4. Emit `status` in the most compact correct form (string/array/object).\n * 5. Attach `role`, and dataset-level `label`/`source`/`updated`/`extension`.\n *\n * Pure: no I/O, no side effects. All index math delegated to\n * [`strides`](./strides.ts).\n */\n\nimport type {\n DatasetMeta,\n DimensionColumn,\n Observations,\n} from \"../model/ir\";\nimport type {\n JsonStatCategory,\n JsonStatDataset,\n JsonStatDimension,\n JsonStatRole,\n JsonStatStatus,\n JsonStatValue,\n} from \"../model/jsonstat\";\nimport { enumerateCells, flatPosition, totalCells } from \"./strides\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** Thrown when the IR violates a structural invariant the builder relies on. */\nexport class CubeBuilderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CubeBuilderError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n /** Override the emitted value form. Defaults to `model.valueForm ?? \"auto\"`. */\n valueForm?: \"auto\" | \"dense\" | \"sparse\";\n /** Override the sparse threshold (null ratio). Defaults to 0.5. */\n sparseThreshold?: number;\n /** Override status emission. Defaults to `model.statusForm ?? \"auto\"`. */\n statusForm?: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\";\n /** Override dataset metadata. Defaults to `model.meta`. */\n meta?: DatasetMeta;\n}\n\n// ---------------------------------------------------------------------------\n// Category resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolved view of a dimension: the ordered category IDs and a map from\n * category ID → its position (0-based). The position is what gets multiplied\n * by the dimension's stride when scattering values.\n */\ninterface ResolvedDimension {\n id: string;\n column: DimensionColumn;\n categories: string[];\n position: Map<string, number>;\n}\n\nfunction resolveDimension(id: string, column: DimensionColumn): ResolvedDimension {\n let categories: string[];\n if (column.categoryOrder && column.categoryOrder.length > 0) {\n // Honor an explicit order, but verify every observed value is covered.\n const known = new Set(column.categoryOrder);\n for (const v of column.values) {\n if (!known.has(v)) {\n throw new CubeBuilderError(\n `Dimension \"${id}\": value \"${v}\" not present in categoryOrder`,\n );\n }\n }\n categories = column.categoryOrder.slice();\n } else {\n // First-seen order, deduplicated.\n const seen = new Set<string>();\n categories = [];\n for (const v of column.values) {\n if (v == null) {\n throw new CubeBuilderError(\n `Dimension \"${id}\": contains a null category value; dimensions cannot be null`,\n );\n }\n if (!seen.has(v)) {\n seen.add(v);\n categories.push(v);\n }\n }\n }\n const position = new Map<string, number>();\n categories.forEach((c, i) => position.set(c, i));\n return { id, column, categories, position };\n}\n\n// ---------------------------------------------------------------------------\n// Dimension block\n// ---------------------------------------------------------------------------\n\n/** Build the JSON-stat `dimension` object and the `size` array. */\nfunction buildDimensionBlock(\n resolved: ResolvedDimension[],\n): { dimension: Record<string, JsonStatDimension>; size: number[] } {\n const dimension: Record<string, JsonStatDimension> = {};\n const size: number[] = [];\n for (const r of resolved) {\n const cat: JsonStatCategory = {};\n // index: array form is the canonical, simplest representation.\n cat.index = r.categories.slice();\n if (r.column.categoryLabels) cat.label = { ...r.column.categoryLabels };\n if (r.column.categoryUnits) cat.unit = { ...r.column.categoryUnits };\n if (r.column.categoryCoordinates)\n cat.coordinates = { ...r.column.categoryCoordinates };\n if (r.column.categoryChild) cat.child = { ...r.column.categoryChild };\n\n const dim: JsonStatDimension = { category: cat };\n if (r.column.label) dim.label = r.column.label;\n if (r.column.href) dim.href = r.column.href;\n dimension[r.id] = dim;\n size.push(r.categories.length);\n }\n return { dimension, size };\n}\n\n// ---------------------------------------------------------------------------\n// Value scattering (long table → row-major cube)\n// ---------------------------------------------------------------------------\n\n/**\n * Scatter measure values into a dense array of length `total` (null-filled)\n * using row-major positions computed from the resolved dimensions.\n *\n * If two observations map to the same cell, the *last* one wins and a warning\n * is surfaced via the returned diagnostics (the cube model assumes one value\n * per cell; duplicates indicate an ill-formed source).\n */\nfunction scatterValues(\n obs: Observations,\n resolved: ResolvedDimension[],\n size: number[],\n): { dense: (number | null)[]; duplicates: number } {\n const total = totalCells(size);\n const dense: (number | null)[] = new Array(total).fill(null);\n const seen = new Uint8Array(total); // 0 = empty, 1 = filled\n let duplicates = 0;\n const n = obs.measure.values.length;\n const k = resolved.length;\n for (let row = 0; row < n; row++) {\n const indices = new Array<number>(k);\n for (let d = 0; d < k; d++) {\n const r = resolved[d];\n const v = r.column.values[row];\n const p = r.position.get(v);\n if (p === undefined) {\n // resolveDimension guarantees coverage when categoryOrder is used, but\n // guard regardless.\n throw new CubeBuilderError(\n `Dimension \"${r.id}\": value \"${v}\" has no position (row ${row})`,\n );\n }\n indices[d] = p;\n }\n const pos = flatPosition(indices, size);\n if (seen[pos] === 1) duplicates++;\n seen[pos] = 1;\n dense[pos] = obs.measure.values[row];\n }\n return { dense, duplicates };\n}\n\n// ---------------------------------------------------------------------------\n// Value form selection (dense vs sparse)\n// ---------------------------------------------------------------------------\n\n/**\n * Decide between dense (array) and sparse (object) `value` representation.\n *\n * Per wiki/sparse-cubes.md: sparse is preferred when many cells are null. The\n * threshold is the null ratio above which the object form is more compact.\n */\nfunction chooseValueForm(\n dense: (number | null)[],\n requested: \"auto\" | \"dense\" | \"sparse\",\n threshold: number,\n): { value: JsonStatValue; form: \"dense\" | \"sparse\"; nullRatio: number } {\n const total = dense.length;\n const nullCount = dense.reduce<number>((acc, v) => acc + (v === null ? 1 : 0), 0);\n const nullRatio = total === 0 ? 0 : nullCount / total;\n\n let form: \"dense\" | \"sparse\";\n if (requested === \"dense\") form = \"dense\";\n else if (requested === \"sparse\") form = \"sparse\";\n else form = nullRatio > threshold ? \"sparse\" : \"dense\";\n\n if (form === \"dense\") {\n return { value: dense, form, nullRatio };\n }\n // Sparse object: only non-null cells, keyed by stringified flat position.\n const obj: Record<string, number> = {};\n for (let i = 0; i < total; i++) {\n const v = dense[i];\n if (v !== null) obj[String(i)] = v;\n }\n return { value: obj, form, nullRatio };\n}\n\n// ---------------------------------------------------------------------------\n// Status emission\n// ---------------------------------------------------------------------------\n\n/**\n * Emit `status` in the most compact correct form.\n *\n * - If all statuses are identical and non-empty → string form.\n * - Else if few distinct non-default statuses → object form (sparse status).\n * - Else → array form (one entry per cell, aligned with dense `value`).\n *\n * The caller passes the *dense* per-cell status array (length = total cells).\n */\nfunction emitStatus(\n perCell: string[],\n requested: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\",\n): JsonStatStatus | undefined {\n if (requested === \"none\") return undefined;\n const total = perCell.length;\n if (total === 0) return undefined;\n\n // Collect distinct statuses and whether they're uniform.\n const first = perCell[0];\n let uniform = true;\n const distinct = new Set<string>();\n for (const s of perCell) {\n distinct.add(s);\n if (s !== first) uniform = false;\n }\n\n if (requested === \"string\") return first; // caller's explicit choice\n if (requested === \"array\") return perCell.slice();\n\n if (requested === \"object\") {\n return sparseStatus(perCell);\n }\n\n // auto:\n if (uniform && first !== \"\") return first;\n // If most cells share the dominant status, sparse object form is compact.\n const counts = new Map<string, number>();\n for (const s of perCell) counts.set(s, (counts.get(s) ?? 0) + 1);\n let dominant = \"\";\n let dominantCount = -1;\n for (const [s, c] of counts) {\n if (c > dominantCount) {\n dominant = s;\n dominantCount = c;\n }\n }\n // Use object form when fewer than ~40% of cells deviate from the dominant.\n const nonDominant = total - dominantCount;\n if (dominant !== \"\" && nonDominant / total < 0.4) {\n return sparseStatus(perCell, dominant);\n }\n return perCell.slice();\n}\n\n/** Build the sparse object status form. Omits `defaultStatus` entries. */\nfunction sparseStatus(\n perCell: string[],\n defaultStatus?: string,\n): Record<string, string> {\n const obj: Record<string, string> = {};\n for (let i = 0; i < perCell.length; i++) {\n const s = perCell[i];\n if (s === \"\" || s === defaultStatus) continue;\n obj[String(i)] = s;\n }\n return obj;\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/** Validate the structural invariants the builder relies on. */\nfunction validate(obs: Observations, resolved: ResolvedDimension[]): void {\n const n = obs.measure.values.length;\n for (const r of resolved) {\n if (r.column.values.length !== n) {\n throw new CubeBuilderError(\n `Dimension \"${r.id}\": ${r.column.values.length} values ≠ ${n} measure values`,\n );\n }\n }\n if (obs.status && obs.status.values.length !== n) {\n throw new CubeBuilderError(\n `Status column: ${obs.status.values.length} values ≠ ${n} measure values`,\n );\n }\n // Every model.dimensionIds entry must have a column, and vice versa.\n const modelIds = new Set(obs.model.dimensionIds);\n const colIds = new Set(Object.keys(obs.dimensions));\n for (const id of modelIds) {\n if (!colIds.has(id)) {\n throw new CubeBuilderError(`model.dimensionIds lists \"${id}\" but no column exists`);\n }\n }\n for (const id of colIds) {\n if (!modelIds.has(id)) {\n throw new CubeBuilderError(`Column \"${id}\" is not listed in model.dimensionIds`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Build result & public API\n// ---------------------------------------------------------------------------\n\nexport interface BuildResult {\n dataset: JsonStatDataset;\n /** Diagnostics from the build (non-fatal observations). */\n diagnostics: {\n valueForm: \"dense\" | \"sparse\";\n nullRatio: number;\n duplicates: number;\n cellCount: number;\n };\n}\n\n/**\n * Build a JSON-stat [`Dataset`](../model/jsonstat.ts) from an\n * [`Observations`](../model/ir.ts) IR.\n *\n * @throws [`CubeBuilderError`](#cubebuildererror) on invariant violations.\n */\nexport function buildDataset(obs: Observations, options: BuildOptions = {}): BuildResult {\n const model = obs.model;\n const dimensionIds = model.dimensionIds;\n\n // Resolve dimensions in the model-declared order (this defines stride order).\n const resolved = dimensionIds.map((id) => {\n const col = obs.dimensions[id];\n if (!col) throw new CubeBuilderError(`Missing dimension column \"${id}\"`);\n return resolveDimension(id, col);\n });\n\n validate(obs, resolved);\n\n const { dimension, size } = buildDimensionBlock(resolved);\n\n // Scatter measure values into the dense row-major array.\n const { dense, duplicates } = scatterValues(obs, resolved, size);\n\n // Choose value form.\n const valueForm = options.valueForm ?? model.valueForm ?? \"auto\";\n const threshold = options.sparseThreshold ?? model.sparseThreshold ?? 0.5;\n const { value, form, nullRatio } = chooseValueForm(dense, valueForm, threshold);\n\n // Status: if present, build a dense per-cell status array aligned with `value`.\n let status: JsonStatStatus | undefined;\n if (obs.status) {\n const statusForm = options.statusForm ?? model.statusForm ?? \"auto\";\n if (statusForm !== \"none\") {\n status = buildPerCellStatus(obs, resolved, size, statusForm);\n }\n }\n\n const dataset: JsonStatDataset = {\n version: \"2.0\",\n class: \"dataset\",\n id: dimensionIds.slice(),\n size,\n dimension,\n value,\n };\n\n // Roles (only attach defined role keys, and only if non-empty).\n const role = filterRoles(model.roles);\n if (role) dataset.role = role;\n\n // Dataset-level metadata.\n const meta = options.meta ?? model.meta;\n if (meta) {\n if (meta.label) dataset.label = meta.label;\n if (meta.source) dataset.source = meta.source;\n if (meta.updated) dataset.updated = meta.updated;\n if (meta.extension) dataset.extension = meta.extension;\n }\n\n if (status) dataset.status = status;\n\n return {\n dataset,\n diagnostics: {\n valueForm: form,\n nullRatio,\n duplicates,\n cellCount: dense.length,\n },\n };\n}\n\n/**\n * Build a per-cell status array by scattering the status column into row-major\n * order, then reduce to the requested form. Cells with no observation default\n * to \"\" (empty), which the emitter treats as \"no status\".\n */\nfunction buildPerCellStatus(\n obs: Observations,\n resolved: ResolvedDimension[],\n size: number[],\n statusForm: \"auto\" | \"array\" | \"string\" | \"object\" | \"none\",\n): JsonStatStatus | undefined {\n const total = totalCells(size);\n const perCell: string[] = new Array(total).fill(\"\");\n const n = obs.measure.values.length;\n const k = resolved.length;\n const statusVals = obs.status?.values;\n if (!statusVals) return undefined;\n for (let row = 0; row < n; row++) {\n const indices = new Array<number>(k);\n for (let d = 0; d < k; d++) {\n indices[d] = resolved[d].position.get(resolved[d].column.values[row])!;\n }\n const pos = flatPosition(indices, size);\n perCell[pos] = statusVals[row] ?? \"\";\n }\n return emitStatus(perCell, statusForm);\n}\n\n/** Return a role object with only non-empty arrays, or undefined if all empty. */\nfunction filterRoles(roles?: JsonStatRole): JsonStatRole | undefined {\n if (!roles) return undefined;\n const out: JsonStatRole = {};\n let any = false;\n for (const key of [\"time\", \"geo\", \"metric\"] as const) {\n const arr = roles[key];\n if (arr && arr.length > 0) {\n out[key] = arr.slice();\n any = true;\n }\n }\n return any ? out : undefined;\n}\n\n/**\n * Convenience: build and return *just* the dataset (no diagnostics).\n * Useful for the common programmatic case.\n */\nexport function toDataset(obs: Observations, options?: BuildOptions): JsonStatDataset {\n return buildDataset(obs, options).dataset;\n}\n\n// Re-export for consumers who want to enumerate cells themselves.\nexport { enumerateCells };\n"]}
|