diff --git a/src/core/series.ts b/src/core/series.ts index 3dd5ee16..24e6e593 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -712,11 +712,70 @@ export class Series { /** Return a new Series sorted by values. */ sortValues(ascending = true, naPosition: "first" | "last" = "last"): Series { - const pairs = this._values.map((v, i) => ({ v, i })); - pairs.sort((a, b) => compareScalars(a.v, b.v, ascending, naPosition)); + const n = this._values.length; + const vals = this._values; + + // Pre-partition NaN/null/undefined from finite values in one O(n) pass. + // This removes the NaN/null check from the comparator's hot path. + const finBuf = new Uint32Array(n); + const nanBuf = new Uint32Array(n); + let finCount = 0; + let nanCount = 0; + for (let i = 0; i < n; i++) { + const v = vals[i]; + if (v === null || v === undefined || (typeof v === "number" && Number.isNaN(v))) { + nanBuf[nanCount++] = i; + } else { + finBuf[finCount++] = i; + } + } + + // Sort the finite-index slice in-place with an indirect comparator. + // Dispatching to one of two monomorphic comparators avoids a per-call + // branch on `ascending` inside the sort's hot loop. + const finSlice = finBuf.subarray(0, finCount); + if (ascending) { + finSlice.sort((a, b) => { + const av = vals[a] as number | string | boolean; + const bv = vals[b] as number | string | boolean; + return av < bv ? -1 : av > bv ? 1 : 0; + }); + } else { + finSlice.sort((a, b) => { + const av = vals[a] as number | string | boolean; + const bv = vals[b] as number | string | boolean; + return av > bv ? -1 : av < bv ? 1 : 0; + }); + } + + // Gather results. Using for...of over TypedArray subviews yields number + // (not number | undefined), satisfying noUncheckedIndexedAccess. + const perm = new Array(n); + const outData = new Array(n); + let pos = 0; + if (naPosition === "first") { + for (const idx of nanBuf.subarray(0, nanCount)) { + perm[pos] = idx; + outData[pos++] = vals[idx] as T; + } + for (const idx of finSlice) { + perm[pos] = idx; + outData[pos++] = vals[idx] as T; + } + } else { + for (const idx of finSlice) { + perm[pos] = idx; + outData[pos++] = vals[idx] as T; + } + for (const idx of nanBuf.subarray(0, nanCount)) { + perm[pos] = idx; + outData[pos++] = vals[idx] as T; + } + } + return new Series({ - data: pairs.map(({ v }) => v), - index: this.index.take(pairs.map(({ i }) => i)), + data: outData, + index: this.index.take(perm), dtype: this.dtype, name: this.name, });