@web-ts-toolkit/json-frame 0.34.3 → 0.36.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/README.md CHANGED
@@ -1,3 +1,302 @@
1
1
  # `@web-ts-toolkit/json-frame`
2
2
 
3
- PLACEHOLDER
3
+ Normalize pandas `DataFrame.to_json()` payloads into one immutable, column-major `DataFrame` API for TypeScript.
4
+
5
+ `@web-ts-toolkit/json-frame` accepts a JSON string or parsed JSON value, ingests all six pandas DataFrame JSON orients, keeps row order and supported index labels intact, and exports back to every supported orient without adding runtime dependencies.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @web-ts-toolkit/json-frame
11
+ ```
12
+
13
+ Canonical import:
14
+
15
+ ```ts
16
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
17
+ ```
18
+
19
+ The package exports named values and types from the package root. There is no default export and no supported deep import path.
20
+
21
+ ## Quick Start
22
+
23
+ ```ts
24
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
25
+
26
+ type WeatherRow = {
27
+ city: string;
28
+ temp: number;
29
+ };
30
+
31
+ const frame = fromOrient<WeatherRow>('[{"city":"Paris","temp":21},{"city":"Rome","temp":30}]');
32
+ const hottest = frame.sort((left, right) => right.temp - left.temp).row(0);
33
+ const split = frame.toSplit();
34
+
35
+ void [hottest, split];
36
+ ```
37
+
38
+ `fromOrient(input, options?)` accepts:
39
+
40
+ - a JSON string produced by pandas `DataFrame.to_json()`
41
+ - a parsed JSON value with the same shape
42
+
43
+ The returned `DataFrame` is immutable. `filter`, `sort`, `select`, `rename`, and `resetIndex` all return new frames.
44
+
45
+ ## Supported Orients
46
+
47
+ All six pandas DataFrame JSON orients are supported.
48
+
49
+ ```ts
50
+ import { fromOrient, type TablePayload } from '@web-ts-toolkit/json-frame';
51
+
52
+ const recordsFrame = fromOrient([{ city: 'Paris', temp: 21 }], { orient: 'records' });
53
+
54
+ const indexFrame = fromOrient(
55
+ {
56
+ row_1: { city: 'Paris', temp: 21 },
57
+ },
58
+ { orient: 'index' },
59
+ );
60
+
61
+ const columnsFrame = fromOrient(
62
+ {
63
+ city: { row_1: 'Paris' },
64
+ temp: { row_1: 21 },
65
+ },
66
+ { orient: 'columns' },
67
+ );
68
+
69
+ const valuesFrame = fromOrient([['Paris', 21]], {
70
+ orient: 'values',
71
+ columns: ['city', 'temp'],
72
+ });
73
+
74
+ const splitFrame = fromOrient(
75
+ {
76
+ columns: ['city', 'temp'],
77
+ index: ['row_1'],
78
+ data: [['Paris', 21]],
79
+ },
80
+ { orient: 'split' },
81
+ );
82
+
83
+ const tablePayload = {
84
+ schema: {
85
+ fields: [
86
+ { name: 'row_id', type: 'string' },
87
+ { name: 'city', type: 'string' },
88
+ { name: 'temp', type: 'integer' },
89
+ ],
90
+ primaryKey: ['row_id'],
91
+ pandas_version: '3.0.3',
92
+ },
93
+ data: [{ row_id: 'row_1', city: 'Paris', temp: 21 }],
94
+ } satisfies TablePayload;
95
+
96
+ const tableFrame = fromOrient(tablePayload, { orient: 'table' });
97
+
98
+ void [recordsFrame, indexFrame, columnsFrame, valuesFrame, splitFrame, tableFrame];
99
+ ```
100
+
101
+ Orient notes:
102
+
103
+ - `records` and `values` do not carry source index labels. They receive a synthetic numeric index `0..n-1`.
104
+ - `index`, `columns`, `split`, and `table` preserve supported source index labels.
105
+ - `values` always requires `options.columns`, including empty input.
106
+ - `index` and `columns` are distinct supported payloads, but auto-detection cannot safely distinguish them from nested-object structure alone.
107
+
108
+ ## Auto Detection And Options
109
+
110
+ `options.orient` defaults to `auto`.
111
+
112
+ Auto-detection recognizes only structurally unambiguous payloads:
113
+
114
+ - `table`
115
+ - `split`
116
+ - non-empty `records`
117
+ - non-empty `values`
118
+
119
+ Pass an explicit orient for:
120
+
121
+ - any `index` payload
122
+ - any `columns` payload
123
+ - empty arrays
124
+ - empty objects
125
+ - empty `values` payloads
126
+
127
+ Other `fromOrient` options:
128
+
129
+ - `columns`: required for `orient: 'values'`
130
+ - `columnTypes`: explicit logical types for non-table inputs
131
+ - `packThreshold`: minimum length for internal numeric typed-array packing; `0` disables packing
132
+
133
+ ```ts
134
+ import { AmbiguousOrientError, JsonFrameOptionError, fromOrient } from '@web-ts-toolkit/json-frame';
135
+
136
+ let candidates: readonly string[] = [];
137
+ let option = '';
138
+
139
+ try {
140
+ fromOrient({ row_1: { city: 'Paris', temp: 21 } });
141
+ } catch (error) {
142
+ if (error instanceof AmbiguousOrientError) {
143
+ candidates = error.candidates;
144
+ }
145
+ }
146
+
147
+ try {
148
+ fromOrient([['Paris', 21]], { orient: 'values' });
149
+ } catch (error) {
150
+ if (error instanceof JsonFrameOptionError) {
151
+ option = error.option;
152
+ }
153
+ }
154
+
155
+ void [candidates, option];
156
+ ```
157
+
158
+ ## Transforms And Exporters
159
+
160
+ The `DataFrame` API is intentionally small and predictable.
161
+
162
+ Read access:
163
+
164
+ - `columns`
165
+ - `index`
166
+ - `columnInfo`
167
+ - `length`
168
+ - `row(position)`
169
+ - `rows()`
170
+
171
+ Transforms:
172
+
173
+ - `filter(predicate)`
174
+ - `sort(compare)`
175
+ - `select(...columns)`
176
+ - `rename(mapping)`
177
+ - `resetIndex()`
178
+
179
+ Exporters:
180
+
181
+ - `toRecords()`
182
+ - `toIndex()`
183
+ - `toColumns()`
184
+ - `toValues()`
185
+ - `toSplit()`
186
+ - `toTable(options?)`
187
+ - `toJSONString(orient, options?)`
188
+
189
+ ```ts
190
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
191
+
192
+ type WeatherRow = {
193
+ city: string;
194
+ temp: number | null;
195
+ coastal: boolean;
196
+ };
197
+
198
+ const frame = fromOrient<WeatherRow>(
199
+ [
200
+ { city: 'Paris', temp: 21.5, coastal: false },
201
+ { city: 'Tokyo', temp: 27.1, coastal: true },
202
+ { city: 'Oslo', temp: null, coastal: true },
203
+ ],
204
+ {
205
+ orient: 'records',
206
+ columnTypes: {
207
+ temp: 'float',
208
+ coastal: 'boolean',
209
+ },
210
+ packThreshold: 0,
211
+ },
212
+ );
213
+
214
+ const transformed = frame
215
+ .filter((row) => row.temp !== null)
216
+ .sort((left, right) => (right.temp ?? -1) - (left.temp ?? -1))
217
+ .rename({ temp: 'celsius' })
218
+ .select('city', 'celsius')
219
+ .resetIndex();
220
+
221
+ const table = transformed.toTable();
222
+ const json = transformed.toJSONString('records');
223
+
224
+ void [table, json];
225
+ ```
226
+
227
+ Transform behavior:
228
+
229
+ - row order is preserved unless you call `sort`
230
+ - `sort` is stable
231
+ - filtering and sorting preserve index labels
232
+ - `resetIndex()` is the only transform that replaces the current index with `0..n-1`
233
+ - `rename` ignores keys that are not current columns and rejects duplicate results
234
+ - exporters always return fresh JSON-compatible containers
235
+
236
+ ## Logical Types And Packing
237
+
238
+ `columnInfo` exposes logical type metadata for each column.
239
+
240
+ Supported logical types:
241
+
242
+ - `integer`
243
+ - `float`
244
+ - `string`
245
+ - `boolean`
246
+ - `datetime`
247
+ - `categorical`
248
+ - `mixed`
249
+ - `unknown`
250
+
251
+ Type rules:
252
+
253
+ - Table Schema metadata is authoritative when `orient: 'table'` is used.
254
+ - Other orients infer the narrowest logical type by scanning the full column.
255
+ - `null` marks a column as nullable without forcing an otherwise numeric column to become `mixed`.
256
+ - ISO-looking strings are preserved as strings unless table metadata or explicit `columnTypes` says otherwise.
257
+
258
+ Packing rules:
259
+
260
+ - typed-array packing is an internal optimization only
261
+ - packed and unpacked frames export the same JSON values
262
+ - packing never changes logical type metadata
263
+ - only non-null numeric columns at or above `packThreshold` are eligible
264
+
265
+ ## Round Trips And Index Provenance
266
+
267
+ Round trips are semantic rather than byte-for-byte.
268
+
269
+ - column order, row order, and cell JSON values are preserved
270
+ - label-bearing orients preserve supported source index labels
271
+ - `records` and `values` use a synthetic numeric index because the wire format does not carry labels
272
+ - `toRecords()` and `toValues()` never invent an `_index` column
273
+ - `toIndex()` and `toColumns()` stringify index labels as JSON object keys and throw if distinct labels would collide after stringification
274
+ - `toTable()` emits valid Table Schema and omits a synthetic index from table output
275
+
276
+ ## Errors
277
+
278
+ Structured runtime errors are exported from the package root.
279
+
280
+ - `JsonFrameParseError`: invalid JSON string input; original `SyntaxError` is available as `cause`
281
+ - `JsonFrameOptionError`: invalid options such as bad `packThreshold` or missing `columns` for `values`
282
+ - `JsonFrameValidationError`: payload shape or JSON-value validation failure
283
+ - `AmbiguousOrientError`: `auto` mode cannot distinguish between multiple valid orients
284
+ - `UnsupportedFeatureError`: supported contract deliberately excludes the requested pandas feature
285
+ - `ExportKeyCollisionError`: object-key exporters would collapse distinct index labels to the same JSON key
286
+
287
+ ## Unsupported Pandas Features In The Initial Release
288
+
289
+ - MultiIndex input or output
290
+ - duplicate column labels
291
+ - non-string column labels
292
+ - pandas Series JSON shapes
293
+ - JSON Lines, compression, file I/O, or streaming input
294
+ - arbitrary pandas extension dtype reconstruction beyond supported Table Schema metadata
295
+ - deep freezing of nested JSON object values
296
+
297
+ ## Runtime Requirements
298
+
299
+ - package source and published runtime are isomorphic
300
+ - no runtime dependencies
301
+ - no peer dependencies
302
+ - published entrypoints: CJS, ESM, `.d.ts`, and `.d.mts`
package/index.d.mts CHANGED
@@ -1,2 +1,196 @@
1
+ /** Scalar JSON values accepted by `fromOrient` payloads and exporters. */
2
+ type JsonPrimitive = string | number | boolean | null;
3
+ /** JSON arrays accepted by `fromOrient` payloads and returned by exporters. */
4
+ type JsonArray = readonly JsonValue[];
5
+ /** JSON objects accepted by `fromOrient` payloads and returned by exporters. */
6
+ interface JsonObject {
7
+ readonly [key: string]: JsonValue;
8
+ }
9
+ /** Any JSON-compatible value accepted by `fromOrient` payloads and exporters. */
10
+ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
11
+ /** pandas orient names supported by `fromOrient`. */
12
+ type ResolvedOrient = 'records' | 'index' | 'columns' | 'values' | 'split' | 'table';
13
+ /** Orient selection for `fromOrient`. `auto` performs structural detection when possible. */
14
+ type Orient = 'auto' | ResolvedOrient;
15
+ /** Public column labels are unique strings in the initial release. */
16
+ type ColumnLabel = string;
17
+ /** Supported index labels preserved by parsed frames and label-bearing exporters. */
18
+ type IndexLabel = string | number;
19
+ /** Internal index provenance exposed by later frame APIs. */
20
+ type IndexKind = 'source' | 'synthetic';
21
+ /** Parsed payload for `orient='records'`. */
22
+ type RecordsPayload = readonly JsonObject[];
23
+ /** Parsed payload for `orient='index'`. */
24
+ type IndexPayload = Readonly<Record<string, JsonObject>>;
25
+ /** Parsed payload for `orient='columns'`. */
26
+ type ColumnsPayload = Readonly<Record<string, JsonObject>>;
27
+ /** Parsed payload for `orient='values'`. */
28
+ type ValuesPayload = readonly JsonArray[];
29
+ /** Parsed payload for `orient='split'`. */
30
+ interface SplitPayload {
31
+ readonly columns: readonly ColumnLabel[];
32
+ readonly index: readonly IndexLabel[];
33
+ readonly data: readonly JsonArray[];
34
+ }
35
+ /** Known Frictionless/Table Schema field types emitted by pandas. */
36
+ type TableSchemaFieldType = 'any' | 'array' | 'boolean' | 'date' | 'datetime' | 'duration' | 'geojson' | 'integer' | 'number' | 'object' | 'string' | 'year' | 'yearmonth' | (string & {});
37
+ /** Table Schema constraint metadata preserved when present. */
38
+ interface TableSchemaConstraints {
39
+ /** Enumerated allowed scalar values, commonly used for pandas categoricals. */
40
+ readonly enum?: readonly JsonPrimitive[];
41
+ readonly [key: string]: JsonValue | undefined;
42
+ }
43
+ type JsonMetadata = Readonly<Record<string, JsonValue | undefined>>;
44
+ /** Table Schema field metadata preserved during table ingestion and export. */
45
+ type TableSchemaField = JsonMetadata & {
46
+ readonly name: string;
47
+ readonly type: TableSchemaFieldType;
48
+ readonly format?: string;
49
+ readonly constraints?: TableSchemaConstraints;
50
+ readonly ordered?: boolean;
51
+ readonly extDtype?: string;
52
+ readonly tz?: string;
53
+ readonly freq?: string;
54
+ };
55
+ /** Table Schema metadata preserved for `orient='table'`. */
56
+ type TableSchema = JsonMetadata & {
57
+ readonly fields: readonly TableSchemaField[];
58
+ readonly primaryKey?: readonly string[];
59
+ readonly pandas_version?: string;
60
+ };
61
+ /** Parsed payload for `orient='table'`. */
62
+ interface TablePayload {
63
+ readonly schema: TableSchema;
64
+ readonly data: readonly JsonObject[];
65
+ }
66
+ /** Logical column types inferred or preserved by the frame. */
67
+ type ColumnType = 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'categorical' | 'mixed' | 'unknown';
68
+ /** Public logical type metadata for a stored column. */
69
+ interface ColumnInfo {
70
+ /** Narrowest logical type inferred or preserved for the column. */
71
+ readonly type: ColumnType;
72
+ /** Whether any row in the column contains `null`. */
73
+ readonly nullable: boolean;
74
+ }
75
+ /**
76
+ * Options for `fromOrient`.
77
+ *
78
+ * `columns` is required only for explicit `values` input because that orient
79
+ * carries no column labels. `columnTypes` supplies explicit logical metadata
80
+ * for label-preserving orients that do not carry Table Schema field types.
81
+ */
82
+ interface FromOrientOptions {
83
+ /** Explicit orient, or `auto` to detect only structurally unambiguous shapes. */
84
+ readonly orient?: Orient;
85
+ /** Explicit column names used when parsing `orient='values'`. */
86
+ readonly columns?: readonly ColumnLabel[];
87
+ /** Explicit logical column types applied after parsing when no Table Schema is present. */
88
+ readonly columnTypes?: Readonly<Partial<Record<ColumnLabel, ColumnType>>>;
89
+ /** Packing threshold for typed-array storage. `0` disables packing. */
90
+ readonly packThreshold?: number;
91
+ }
92
+ /** Options for `toTable()` and `toJSONString('table', ...)`. */
93
+ interface ToTableOptions {
94
+ /** Override the emitted table index field name when the default would collide. */
95
+ readonly indexField?: string;
96
+ }
97
+ /** Additional options accepted by `toJSONString()`. */
98
+ type ToJSONStringOptions = ToTableOptions;
99
+ /**
100
+ * Immutable column-major view over pandas-compatible tabular JSON.
101
+ *
102
+ * Instances are created by `fromOrient()`. The generic row type improves row
103
+ * and callback ergonomics only; runtime validation still follows the JSON
104
+ * Frame contracts for the selected orient.
105
+ */
106
+ interface DataFrame<TRow extends Record<string, JsonValue> = Record<string, JsonValue>> {
107
+ readonly columns: readonly string[];
108
+ readonly index: readonly IndexLabel[];
109
+ readonly columnInfo: ReadonlyMap<string, ColumnInfo>;
110
+ readonly length: number;
111
+ toRecords(): RecordsPayload;
112
+ toIndex(): IndexPayload;
113
+ toColumns(): ColumnsPayload;
114
+ toValues(): ValuesPayload;
115
+ toSplit(): SplitPayload;
116
+ toTable(options?: ToTableOptions): TablePayload;
117
+ toJSONString(orient: ResolvedOrient, options?: ToJSONStringOptions): string;
118
+ row(position: number): Readonly<TRow>;
119
+ rows(): readonly Readonly<TRow>[];
120
+ filter(predicate: (row: Readonly<TRow>, index: IndexLabel, position: number) => boolean): DataFrame<TRow>;
121
+ sort(compare: (left: Readonly<TRow>, right: Readonly<TRow>, leftIndex: IndexLabel, rightIndex: IndexLabel, leftPosition: number, rightPosition: number) => number): DataFrame<TRow>;
122
+ select(...columns: readonly string[]): DataFrame<Record<string, JsonValue>>;
123
+ rename(mapping: Readonly<Record<string, string>>): DataFrame<Record<string, JsonValue>>;
124
+ resetIndex(): DataFrame<TRow>;
125
+ }
1
126
 
2
- export { }
127
+ /**
128
+ * Parses pandas-compatible JSON into an immutable `DataFrame`.
129
+ *
130
+ * `auto` detection recognizes only structurally unambiguous payloads. Pass an
131
+ * explicit `options.orient` for `index`, `columns`, empty arrays, empty
132
+ * objects, and all `values` payloads. The generic row type is for TypeScript
133
+ * ergonomics and is not runtime-validated beyond the documented JSON-orient
134
+ * contracts.
135
+ */
136
+ declare function fromOrient<TRow extends Record<string, JsonValue> = Record<string, JsonValue>>(input: string, options?: FromOrientOptions): DataFrame<TRow>;
137
+ declare function fromOrient<TRow extends Record<string, JsonValue>>(input: readonly TRow[], options?: FromOrientOptions & {
138
+ readonly orient?: 'auto' | 'records';
139
+ }): DataFrame<TRow>;
140
+ declare function fromOrient<TRow extends Record<string, JsonValue> = Record<string, JsonValue>>(input: JsonValue, options?: FromOrientOptions): DataFrame<TRow>;
141
+
142
+ type JsonFrameErrorContext = {
143
+ readonly orient?: Orient;
144
+ readonly path?: string;
145
+ readonly row?: number;
146
+ readonly column?: string;
147
+ readonly value?: unknown;
148
+ };
149
+ /** Base class for all structured `@web-ts-toolkit/json-frame` runtime failures. */
150
+ declare class JsonFrameError extends Error {
151
+ readonly orient?: Orient;
152
+ readonly path?: string;
153
+ readonly row?: number;
154
+ readonly column?: string;
155
+ readonly value?: unknown;
156
+ constructor(message: string, context?: JsonFrameErrorContext);
157
+ }
158
+ /** Raised when a JSON string cannot be parsed before orient validation begins. */
159
+ declare class JsonFrameParseError extends JsonFrameError {
160
+ readonly cause: SyntaxError;
161
+ constructor(message: string, cause: SyntaxError);
162
+ }
163
+ /** Raised when caller-supplied options are invalid before payload traversal begins. */
164
+ declare class JsonFrameOptionError extends JsonFrameError {
165
+ readonly option: string;
166
+ constructor(message: string, option: string, value: unknown);
167
+ }
168
+ /** Raised when an orient payload violates the documented JSON Frame contract. */
169
+ declare class JsonFrameValidationError extends JsonFrameError {
170
+ readonly orient?: ResolvedOrient;
171
+ constructor(message: string, context?: JsonFrameErrorContext & {
172
+ readonly orient?: ResolvedOrient;
173
+ });
174
+ }
175
+ /** Raised when `auto` detection cannot distinguish between multiple valid orients. */
176
+ declare class AmbiguousOrientError extends JsonFrameError {
177
+ readonly candidates: readonly ResolvedOrient[];
178
+ constructor(message: string, candidates: readonly ResolvedOrient[]);
179
+ }
180
+ /** Raised when a pandas shape or feature is explicitly outside the initial release scope. */
181
+ declare class UnsupportedFeatureError extends JsonFrameError {
182
+ readonly feature: string;
183
+ readonly orient?: ResolvedOrient;
184
+ constructor(message: string, feature: string, context?: JsonFrameErrorContext & {
185
+ readonly orient?: ResolvedOrient;
186
+ });
187
+ }
188
+ /** Raised when object-key exporters would collapse distinct index labels to the same JSON key. */
189
+ declare class ExportKeyCollisionError extends JsonFrameError {
190
+ readonly key: string;
191
+ readonly labels: readonly (string | number)[];
192
+ readonly orient: ResolvedOrient;
193
+ constructor(message: string, orient: ResolvedOrient, key: string, labels: readonly (string | number)[]);
194
+ }
195
+
196
+ export { AmbiguousOrientError, type ColumnInfo, type ColumnLabel, type ColumnType, type ColumnsPayload, type DataFrame, ExportKeyCollisionError, type FromOrientOptions, type IndexKind, type IndexLabel, type IndexPayload, type JsonArray, JsonFrameError, JsonFrameOptionError, JsonFrameParseError, JsonFrameValidationError, type JsonObject, type JsonPrimitive, type JsonValue, type Orient, type RecordsPayload, type ResolvedOrient, type SplitPayload, type TablePayload, type TableSchema, type TableSchemaConstraints, type TableSchemaField, type TableSchemaFieldType, type ToJSONStringOptions, type ToTableOptions, UnsupportedFeatureError, type ValuesPayload, fromOrient };