Skip to content

Commit ea842c3

Browse files
Copilotmrjf
andauthored
fix: resolve all TypeScript strict-mode errors across source and test files
Source fixes: - src/core/reindex.ts: add undefined guards for noUncheckedIndexedAccess - src/core/timestamp.ts: guard regex match group against undefined - src/io/json_normalize.ts: use Dtype.float64/int64/bool/object statics, fix Series/DataFrame constructors, convert Record to Map - src/stats/combine_first.ts: replace hasColumn() with has() - src/stats/crosstab.ts: fix bucket array init and margin arithmetic types - src/stats/factorize.ts: use null instead of undefined for Series name - src/stats/isin.ts: use string|symbol index type for Symbol.iterator check - src/stats/memory_usage.ts: use df.items() instead of for-of on DataFrame - src/stats/clip_with_bounds.ts: add DataFrameBoundArg type for DataFrame bounds - src/groupby/groupby.ts: implement aggNamed() method on DataFrameGroupBy Test fixes: - Cast toArray() results to (number|null)[] for null-containing assertions - Replace Index .length with .size - Add non-null assertions for indexed access - Use DataFrame.fromColumns or proper Map constructor - Fix type annotations for property-based tests - Use identity comparison pattern for toBe with union types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
1 parent 7441465 commit ea842c3

23 files changed

Lines changed: 139 additions & 65 deletions

src/core/reindex.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ function applyNearest(values: Scalar[], present: readonly boolean[]): Scalar[] {
198198
out[i] = rightVal[i];
199199
} else if (rd === -1) {
200200
out[i] = leftVal[i];
201-
} else if (rd <= ld) {
201+
} else if (rd !== undefined && ld !== undefined && rd <= ld) {
202202
// prefer right on tie
203203
out[i] = rightVal[i];
204204
} else {
@@ -268,7 +268,10 @@ export function reindexSeries<T extends Scalar>(
268268
const key = String(newLabels[i]);
269269
const positions = labelMap.get(key);
270270
if (positions !== undefined && positions.length > 0) {
271-
resultValues[i] = series.values[positions[0]] as Scalar;
271+
const pos0 = positions[0];
272+
if (pos0 !== undefined) {
273+
resultValues[i] = series.values[pos0] as Scalar;
274+
}
272275
present[i] = true;
273276
}
274277
}

src/core/timestamp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ function freqMs(freq: string): number {
331331
if (upper === "D") return MS_PER_DAY;
332332
// Try "Nunit" pattern (e.g. "2H", "15T").
333333
const m = /^(\d+)(.+)$/.exec(freq);
334-
if (m !== null) {
334+
if (m !== null && m[2] !== undefined) {
335335
return Number(m[1]) * freqMs(m[2]);
336336
}
337337
throw new Error(`Timestamp.floor/ceil/round: unsupported frequency "${freq}"`);

src/groupby/groupby.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { RangeIndex } from "../core/index.ts";
2121
import { DataFrame } from "../core/index.ts";
2222
import { Series } from "../core/index.ts";
2323
import type { Label, Scalar } from "../types.ts";
24+
import type { NamedAggSpec } from "./named_agg.ts";
2425

2526
// ─── types ────────────────────────────────────────────────────────────────────
2627

@@ -304,6 +305,61 @@ export class DataFrameGroupBy {
304305
return this._runAgg(colSpecs, asIndex);
305306
}
306307

308+
/**
309+
* Aggregate with named output columns via `NamedAgg` specs.
310+
*
311+
* Each key in `spec` becomes an output column name. The corresponding
312+
* `NamedAgg` tells which source column to read and which aggregation to apply.
313+
*
314+
* @example
315+
* ```ts
316+
* df.groupby("dept").aggNamed({
317+
* total: namedAgg("salary", "sum"),
318+
* avg: namedAgg("salary", "mean"),
319+
* });
320+
* ```
321+
*/
322+
aggNamed(spec: NamedAggSpec, asIndex = true): DataFrame {
323+
const groupKeys = this._groups.map((g) => g.key);
324+
const resultCols: Record<string, Scalar[]> = {};
325+
326+
if (!asIndex) {
327+
if (this._by.length === 1) {
328+
const byCol = this._by[0] as string;
329+
resultCols[byCol] = groupKeys.slice();
330+
} else {
331+
for (const by of this._by) {
332+
resultCols[by] = [];
333+
}
334+
for (const g of this._groups) {
335+
const parts = (g.key as string).split("__SEP__");
336+
this._by.forEach((by, idx) => {
337+
const byArr = resultCols[by];
338+
if (byArr !== undefined) {
339+
byArr.push(parts[idx] ?? null);
340+
}
341+
});
342+
}
343+
}
344+
}
345+
346+
for (const [outputCol, namedSpec] of Object.entries(spec)) {
347+
const srcVals = this._df.col(namedSpec.column).values as readonly Scalar[];
348+
const fn = resolveAgg(namedSpec.aggfunc);
349+
resultCols[outputCol] = this._groups.map((g) =>
350+
fn(g.positions.map((p) => srcVals[p] as Scalar)),
351+
);
352+
}
353+
354+
const rowIdx: Index<Label> = asIndex
355+
? new Index<Label>(groupKeys)
356+
: defaultIndex(groupKeys.length);
357+
358+
return DataFrame.fromColumns(resultCols as Record<string, readonly Scalar[]>, {
359+
index: rowIdx,
360+
});
361+
}
362+
307363
/** Shorthand for `agg("sum")` — numeric columns only, like pandas. */
308364
sum(): DataFrame {
309365
const cols = this._numericValueCols();

src/io/json_normalize.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export function jsonNormalize(
310310
let currentRecords: readonly JsonObject[] = records;
311311
for (let i = 0; i < pathList.length; i++) {
312312
const path = pathList[i];
313+
if (path === undefined) continue;
313314
const isFinal = i === pathList.length - 1;
314315
if (isFinal) {
315316
rows = normalizeWithPath(
@@ -343,7 +344,7 @@ export function jsonNormalize(
343344

344345
// ── Build column sets and DataFrame ───────────────────────────────────────
345346
if (rows.length === 0) {
346-
return new DataFrame({});
347+
return DataFrame.fromColumns({});
347348
}
348349

349350
// Union of all column names (preserve first-seen insertion order)
@@ -364,22 +365,20 @@ export function jsonNormalize(
364365
}
365366

366367
// Build Series columns and infer dtypes
367-
const seriesMap: Record<string, Series> = {};
368+
const colMap = new Map<string, Series<Scalar>>();
368369
for (const [col, vals] of Object.entries(colData)) {
369370
let dtype: Dtype;
370371
if (vals.every((v) => v === null || typeof v === "number")) {
371372
dtype = vals.some((v) => v !== null && !Number.isInteger(v))
372-
? new Dtype("float64")
373-
: new Dtype("int64");
373+
? Dtype.float64
374+
: Dtype.int64;
374375
} else if (vals.every((v) => v === null || typeof v === "boolean")) {
375-
dtype = new Dtype("bool");
376+
dtype = Dtype.bool;
376377
} else {
377-
dtype = new Dtype("object");
378+
dtype = Dtype.object;
378379
}
379-
seriesMap[col] = new Series(vals, { name: col, dtype });
380+
colMap.set(col, new Series({ data: vals, name: col, dtype }));
380381
}
381382

382-
return new DataFrame(seriesMap, {
383-
index: new RangeIndex(rows.length),
384-
});
383+
return new DataFrame(colMap, new RangeIndex(rows.length));
385384
}

src/stats/clip_with_bounds.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,15 @@ export interface SeriesClipBoundsOptions {
3939
readonly upper?: BoundArg;
4040
}
4141

42+
/** A bound that also accepts a DataFrame (for element-wise clipping). */
43+
export type DataFrameBoundArg = BoundArg | DataFrame;
44+
4245
/** Options for {@link clipDataFrameWithBounds}. */
43-
export interface DataFrameClipBoundsOptions extends SeriesClipBoundsOptions {
46+
export interface DataFrameClipBoundsOptions {
47+
/** Lower bound. May be any `BoundArg` or a `DataFrame` for element-wise clipping. */
48+
readonly lower?: DataFrameBoundArg;
49+
/** Upper bound. May be any `BoundArg` or a `DataFrame` for element-wise clipping. */
50+
readonly upper?: DataFrameBoundArg;
4451
/**
4552
* Axis along which a Series bound is broadcast:
4653
* - `0` / `"index"` (default): Series is indexed on **row labels** — each row

src/stats/combine_first.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ export function combineFirstDataFrame(self: DataFrame, other: DataFrame): DataFr
149149
const resultColMap = new Map<string, Series<Scalar>>();
150150

151151
for (const colName of unionCols) {
152-
const selfHasCol = self.hasColumn(colName);
153-
const otherHasCol = other.hasColumn(colName);
152+
const selfHasCol = self.has(colName);
153+
const otherHasCol = other.has(colName);
154154

155155
const data: Scalar[] = [];
156156

src/stats/crosstab.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ export function crosstab(
201201
// buckets[r][c] = collected numeric values for aggregation (when values+aggfunc provided).
202202
const buckets: Array<Array<number[] | undefined>> | null =
203203
values !== undefined
204-
? Array.from({ length: nRows }, () =>
205-
Array.from<number[] | undefined>({ length: nCols }, () => undefined),
204+
? Array.from({ length: nRows }, (): Array<number[] | undefined> =>
205+
new Array<number[] | undefined>(nCols).fill(undefined),
206206
)
207207
: null;
208208

@@ -293,12 +293,12 @@ export function crosstab(
293293
const name = labelStr(colUniques[ci] as Scalar);
294294
const col = colData[name];
295295
if (col !== undefined) {
296-
col.push(col.reduce((s, v) => s + (v as number), 0));
296+
col.push(col.reduce((s: number, v) => s + (typeof v === "number" ? v : 0), 0));
297297
}
298298
}
299299
// Add an "All" column with row totals (and grand total in the last cell).
300300
const allCol: Scalar[] = cells.map((row) => row.reduce((s, v) => s + v, 0));
301-
allCol.push(allCol.reduce((s, v) => s + (v as number), 0));
301+
allCol.push(allCol.reduce((s: number, v) => s + (typeof v === "number" ? v : 0), 0));
302302
colData[marginsName] = allCol;
303303
finalRowLabels = [...rowLabels, marginsName as Label];
304304
}

src/stats/factorize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export function seriesFactorize<T extends Scalar>(
193193
const codesSeries = new Series<number>({
194194
data: result.codes as number[],
195195
index: codesIndex,
196-
name: series.name ?? undefined,
196+
name: series.name ?? null,
197197
});
198198
const uniquesSeries = new Series<T>({
199199
data: result.uniques as T[],

src/stats/isin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function isIsinDict(obj: DataFrameIsinValues): obj is IsinDict {
9191
typeof obj === "object" &&
9292
!Array.isArray(obj) &&
9393
!(obj instanceof Set) &&
94-
typeof (obj as Record<string, unknown>)[Symbol.iterator] !== "function"
94+
typeof (obj as Record<string | symbol, unknown>)[Symbol.iterator] !== "function"
9595
);
9696
}
9797

src/stats/memory_usage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ export function dataFrameMemoryUsage(
176176
values.push(indexMemoryBytes(df.index, deep));
177177
}
178178

179-
for (const [colName, col] of df) {
179+
for (const [colName, col] of df.items()) {
180180
names.push(colName);
181181
let mem: number;
182182
if (deep) {
183-
mem = col.values.reduce((sum: number, v) => sum + deepScalarSize(v), 0);
183+
mem = col.values.reduce((sum: number, v: Scalar) => sum + deepScalarSize(v), 0);
184184
} else {
185185
mem = col.size * shallowElemSize(col.dtype.itemsize);
186186
}

0 commit comments

Comments
 (0)