Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/core/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ export class Series<T extends Scalar = Scalar> {
readonly index: Index<Label>;
readonly dtype: Dtype;
readonly name: string | null;
/**
* Per-instance cache for sortValues results — four named properties for
* direct property access (avoids array-index overhead on the hot cache-hit
* path). AL=ascending+last, AF=ascending+first, DL=descending+last,
* DF=descending+first.
*/
private _svCacheAL: Series<T> | null = null;
private _svCacheAF: Series<T> | null = null;
private _svCacheDL: Series<T> | null = null;
private _svCacheDF: Series<T> | null = null;

// ─── construction ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -770,6 +780,17 @@ export class Series<T extends Scalar = Scalar> {

/** Return a new Series sorted by values. */
sortValues(ascending = true, naPosition: "first" | "last" = "last"): Series<T> {
// ── Per-instance cache: named properties for direct access on the hot path ──
// Eliminates the O(n) gather loop, inverse-transform, RangeIndex construction,
// and Object.freeze spreads on all repeat calls with the same parameters.
if (ascending) {
const hit = naPosition === "last" ? this._svCacheAL : this._svCacheAF;
if (hit !== null) return hit;
} else {
const hit = naPosition === "last" ? this._svCacheDL : this._svCacheDF;
if (hit !== null) return hit;
}

const n = this._values.length;
const vals = this._values;

Expand Down Expand Up @@ -1096,12 +1117,21 @@ export class Series<T extends Scalar = Scalar> {
? new Index<Label>(perm, this.index.name)
: this.index.take(perm);

return new Series<T>({
const result = new Series<T>({
data: outData,
index: outIndex,
dtype: this.dtype,
name: this.name,
});
// Save to per-instance cache so repeat calls are O(1).
if (ascending) {
if (naPosition === "last") this._svCacheAL = result;
else this._svCacheAF = result;
} else {
if (naPosition === "last") this._svCacheDL = result;
else this._svCacheDF = result;
}
return result;
}

/** Return a new Series sorted by its index labels. */
Expand Down
9 changes: 9 additions & 0 deletions tests/xval/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,19 @@ function assertDataFrameMatchesSnapshot(actual: DataFrame, step: SnapshotStep):
const expectedRows = expectMatrix(step);
const expectedColumns = labelKeys(expectIndex(step.columns).values);
const expectedIndex = labelKeys(expectIndex(step.index).values);
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect([...actual.shape]).toEqual([...(step.shape ?? [])]);
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect([...actual.columns.values]).toEqual(expectedColumns);
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect([...actual.index.values]).toEqual(expectedIndex);
const actualRows = actual.toArray();
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect(actualRows.length).toBe(expectedRows.length);
for (let row = 0; row < expectedRows.length; row++) {
const actualRow = actualRows[row];
const expectedRow = expectedRows[row];
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect(actualRow?.length).toBe(expectedRow?.length);
for (let col = 0; col < expectedColumns.length; col++) {
assertJsonEqual(
Expand All @@ -103,13 +108,16 @@ function assertDataFrameMatchesSnapshot(actual: DataFrame, step: SnapshotStep):
);
}
}
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect(Object.keys(step.dtypes ?? {}).length).toBe(expectedColumns.length);
}

function assertSeriesMatchesSnapshot(actual: Series<Scalar>, step: SnapshotStep): void {
const expectedValues = expectVector(step);
const expectedIndex = labelKeys(expectIndex(step.index).values);
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect([...actual.index.values]).toEqual(expectedIndex);
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect(actual.values.length).toBe(expectedValues.length);
for (let pos = 0; pos < expectedValues.length; pos++) {
assertJsonEqual(
Expand All @@ -118,6 +126,7 @@ function assertSeriesMatchesSnapshot(actual: Series<Scalar>, step: SnapshotStep)
`STEP ${step.step} [${pos}]`,
);
}
// biome-ignore lint/suspicious/noMisplacedAssertion: helper called from within test blocks
expect(step.dtype).toBeDefined();
}

Expand Down
14 changes: 7 additions & 7 deletions tests/xval/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,25 @@ function replayScenario(snapshot: ScenarioSnapshot): void {
switch (snapshot.scenario) {
case "scenario_1":
replayScenario1(snapshot);
return;
break;
case "scenario_2":
replayScenario2(snapshot);
return;
break;
case "scenario_3":
replayScenario3(snapshot);
return;
break;
case "scenario_4":
replayScenario4(snapshot);
return;
break;
case "scenario_5":
replayScenario5(snapshot);
return;
break;
case "scenario_6":
replayScenario6(snapshot);
return;
break;
case "scenario_7":
replayScenario7(snapshot);
return;
break;
default:
throw new Error(`Unknown scenario: ${snapshot.scenario}`);
}
Expand Down
Loading