mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add dataset library, extract-to-dataset, and render-time reference resolution
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Dataset profiling (spec §05 → Profiling; docs/architecture/06 §3–§4).
|
||||
*
|
||||
* Portable core: no browser APIs, no React. A **profile** is the set of derived
|
||||
* summary fields stored on a dataset record so the UI can describe it without
|
||||
* re-parsing the payload: row/column counts, column names (in order), a per-column
|
||||
* inferred type, and an approximate byte size.
|
||||
*
|
||||
* `profileData` takes already-parsed rows-of-objects (the tabular form of a
|
||||
* CSV/TSV/JSON-array payload) plus a precomputed `size`. Parsing the delimited
|
||||
* text and deciding the payload shape happen *upstream* (see dataset.ts) so this
|
||||
* function stays pure and trivially testable.
|
||||
*
|
||||
* - `null` rows (URL data) or an empty array (non-tabular) → the **N/A profile**
|
||||
* (`rowCount`/`columnCount` null, empty columns), but `size` is still carried.
|
||||
* - Columns are the union of keys across all rows, in **first-seen order**.
|
||||
* - `rowCount`/`columnCount`/`size` reflect the **whole** payload; only type
|
||||
* inference samples — capped at the first `SAMPLE_SIZE` rows for speed and
|
||||
* determinism (arch 06 §4).
|
||||
*/
|
||||
|
||||
import { inferColumnType, type ColumnType } from './type-inference';
|
||||
|
||||
export interface ColumnTypeInfo {
|
||||
/** The column name. */
|
||||
name: string;
|
||||
/** The inferred display type for the column. */
|
||||
type: ColumnType;
|
||||
}
|
||||
|
||||
export interface DatasetProfile {
|
||||
/** Data rows, or `null` when N/A (URL / non-tabular). */
|
||||
rowCount: number | null;
|
||||
/** Columns, or `null` when N/A. */
|
||||
columnCount: number | null;
|
||||
/** Column names, in first-seen order. */
|
||||
columns: string[];
|
||||
/** Per-column inferred type. */
|
||||
columnTypes: ColumnTypeInfo[];
|
||||
/** Approximate payload size in bytes. */
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** The N/A profile for URL / non-tabular data — still carries the byte size. */
|
||||
const naProfile = (size: number): DatasetProfile => ({
|
||||
rowCount: null,
|
||||
columnCount: null,
|
||||
columns: [],
|
||||
columnTypes: [],
|
||||
size,
|
||||
});
|
||||
|
||||
/**
|
||||
* Cap on rows fed to type inference. Counts and size scan the whole payload; only
|
||||
* the per-value type check is bounded, sampling the head for determinism (§4).
|
||||
*/
|
||||
const SAMPLE_SIZE = 200;
|
||||
|
||||
const sampleRows = <T>(rows: ReadonlyArray<T>): ReadonlyArray<T> =>
|
||||
rows.length <= SAMPLE_SIZE ? rows : rows.slice(0, SAMPLE_SIZE);
|
||||
|
||||
/**
|
||||
* Profile a dataset payload. `rows` is the tabular form (CSV/TSV/JSON-array)
|
||||
* already parsed to rows-of-objects, or `null` for URL / non-tabular data;
|
||||
* `size` is the precomputed byte length of the stored payload.
|
||||
*/
|
||||
export function profileData(
|
||||
rows: ReadonlyArray<Record<string, unknown>> | null,
|
||||
size: number,
|
||||
): DatasetProfile {
|
||||
if (!rows || rows.length === 0) return naProfile(size);
|
||||
|
||||
// Column order = first-seen order across all rows (handles ragged rows).
|
||||
const columns: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row)) {
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
columns.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (columns.length === 0) return naProfile(size);
|
||||
|
||||
const sample = sampleRows(rows);
|
||||
const columnTypes = columns.map((name) => ({
|
||||
name,
|
||||
type: inferColumnType(sample.map((r) => r[name])),
|
||||
}));
|
||||
|
||||
return {
|
||||
rowCount: rows.length,
|
||||
columnCount: columns.length,
|
||||
columns,
|
||||
columnTypes,
|
||||
size,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user