Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ yarn add @gui-chat-plugin/spreadsheet gui-chat-protocol

> `gui-chat-protocol` is a peer dependency — install it alongside the plugin; the host application provides the runtime and this plugin only declares the compatible range.

> The package runs on an ES2023 runtime: Node 22 or later, Chrome 110, Safari 16, Firefox 115 or later. It uses ES2023's array methods (`toSorted`, `toReversed`) as they are, with no polyfill.


## Usage

Expand Down
2 changes: 1 addition & 1 deletion src/core/functionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { functionRegistry } from "../engine/functions/index";
/** Each category's function names, sorted; the engine's internal functions are left out. */
export const registeredFunctionsByCategory = (): Record<string, string[]> =>
Object.fromEntries(
[...functionRegistry.getFunctionsByCategory()].map(([category, definitions]) => [category, definitions.map((definition) => definition.name).sort()]),
[...functionRegistry.getFunctionsByCategory()].map(([category, definitions]) => [category, definitions.map((definition) => definition.name).toSorted()]),
);

/** Whether `name` is one of the engine's own functions, in any case. A workbook's own functions are not. */
Expand Down
14 changes: 7 additions & 7 deletions src/engine/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// Where a cell sits in the grid of the sheet it belongs to, carrying the row so a formula's result is
// written back without re-indexing.
interface CellPosition {
cells: any[];

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, windows-latest)

Unexpected any. Specify a different type

Check warning on line 30 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, windows-latest)

Unexpected any. Specify a different type
row: number;
col: number;
}
Expand Down Expand Up @@ -81,7 +81,7 @@
};
}

export function normalizeData(data: any): SpreadsheetCell[][] {

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, windows-latest)

Unexpected any. Specify a different type

Check warning on line 84 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, windows-latest)

Unexpected any. Specify a different type
// Handle null/undefined
if (!data) {
return [];
Expand All @@ -99,7 +99,7 @@

// If data is already a 2D array, return as-is
if (Array.isArray(data[0])) {
return data as SpreadsheetCell[][];

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, ubuntu-latest)

Do not use any type assertions

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, macos-latest)

Do not use any type assertions

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, ubuntu-latest)

Do not use any type assertions

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, macos-latest)

Do not use any type assertions

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, windows-latest)

Do not use any type assertions

Check warning on line 102 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, windows-latest)

Do not use any type assertions
}

// If data is a flat array of cell objects, convert to 2D by pairing cells
Expand Down Expand Up @@ -238,7 +238,7 @@
const formulas: FormulaInfo[] = [];

// Create a copy of the data with calculated values
const calculated: any[][] = data.map((row) => [...row]);

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, windows-latest)

Unexpected any. Specify a different type

Check warning on line 241 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, windows-latest)

Unexpected any. Specify a different type

// Track cells being calculated to detect circular references
const calculating = new Set<string>();
Expand All @@ -254,8 +254,8 @@

// How far a sheet reaches, for bounding `A:A` and `1:1`; null for a sheet that does not exist.
const extentOf = (name: string | null, reach?: ExtentReach): SheetExtent | null => {
const sheet = name === null || name === sheetName ? self : (opened.get(name) ?? openOther(name));
return sheet ? sheet.extent(reach) : null;
const named = name === null || name === sheetName ? self : (opened.get(name) ?? openOther(name));
return named ? named.extent(reach) : null;
};

const formulaAt = (row: number, col: number): string | null => {
Expand Down Expand Up @@ -364,7 +364,7 @@
const isCalculatedFormula = ({ row, col }: CellPosition): boolean => evaluated.has(`${row},${col}`) && isFormulaSource(data[row]?.[col]);

// Helper to extract raw value from cell with recursive formula evaluation
const getRawValue = (cell: any, position?: CellPosition): CellValue => {

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, macos-latest)

Unexpected any. Specify a different type

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (24.x, windows-latest)

Unexpected any. Specify a different type

Check warning on line 367 in src/engine/calculator.ts

View workflow job for this annotation

GitHub Actions / lint_test (26.x, windows-latest)

Unexpected any. Specify a different type
// A formula already calculated holds its answer, which reads as it was answered: its "" stays
// "" and its text "1,200" stays text, as they read before it was calculated.
if (position && isCalculatedFormula(position)) return cell;
Expand Down Expand Up @@ -425,14 +425,14 @@
const locateCell = (ref: string): { cell: unknown; position: CellPosition; read: CellReader } | null => {
const resolved = resolveSheetData(ref);
if (!resolved) throw invalidRefError(ref); // Sheet not found → #REF!
const { sheetData, ref: cellRef, read, sheet } = resolved;
const { sheetData, ref: cellRef, read, sheet: owner } = resolved;

// `$` symbols and the A1 shape are parsed by the shared single-cell reader.
const coord = parseSingleCellRef(cellRef);
if (!coord) return null;

const { row, col } = coord;
sheet.ensureSpillsReaching(row, col);
owner.ensureSpillsReaching(row, col);
const gridRow = row >= 0 ? sheetData[row] : undefined;
if (!gridRow || col < 0 || col >= gridRow.length) return null;

Expand Down Expand Up @@ -472,11 +472,11 @@
if (answersTable(range)) return calledRangeValues(range, rangeOptions);
const resolved = resolveSheetData(range);
if (!resolved) return []; // Sheet not found → empty range
const { sheetData, ref: rangeRef, read, sheet } = resolved;
const { sheetData, ref: rangeRef, read, sheet: owner } = resolved;

const coords = expandRangeOrCell(rangeRef);
if (!coords) return [];
ensureSpillsCovering(sheet, coords);
ensureSpillsCovering(owner, coords);

const sourceRows = sourceSheetOf(range);
const values: CellValue[] = [];
Expand Down Expand Up @@ -586,7 +586,7 @@
const info: FormulaInfo = {
cell: { row: position.row, col: position.col },
formula: value,
dependencies: [], // TODO: Extract dependencies from formula
dependencies: [], // not tracked: recalculation follows references as it reads them
result: 0, // Will be updated below
};
formulas.push(info);
Expand Down
2 changes: 1 addition & 1 deletion src/engine/cellArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { CellArray, CellValue, FormulaValue } from "./types";
* a function building one from numbers it is given (SEQUENCE, MUNIT) refuses a size past it. */
export const MAX_TABLE_ROWS = 1_048_576;
export const MAX_TABLE_COLUMNS = 16_384;
export const MAX_TABLE_CELLS = 1_048_576;
const MAX_TABLE_CELLS = 1_048_576;

export const fitsInSheet = (rows: number, columns: number): boolean =>
rows <= MAX_TABLE_ROWS && columns <= MAX_TABLE_COLUMNS && rows * columns <= MAX_TABLE_CELLS;
Expand Down
4 changes: 2 additions & 2 deletions src/engine/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function findCellRefs(expr: string): CellRefSpan[] {
* `=A1+A10` had its `A10` broken into `<A1's value>0` and produced a plausible
* wrong number (#2357). */
export function substituteCellRefs(expr: string, cellRefs: CellRefSpan[], render: (ref: string, start: number) => string): string {
return [...cellRefs].reverse().reduce((text, { ref, start }) => text.slice(0, start) + render(ref, start) + text.slice(start + ref.length), expr);
return cellRefs.toReversed().reduce((text, { ref, start }) => text.slice(0, start) + render(ref, start) + text.slice(start + ref.length), expr);
}

/**
Expand Down Expand Up @@ -318,7 +318,7 @@ function blankComparisonSides(expr: string, context: EvaluatorContext): boolean[
}

/** The reference an operand is, perhaps in brackets, or null when it is anything more. */
export function loneReference(operand: string): string | null {
function loneReference(operand: string): string | null {
const inner = stripOuterParens(operand).trim();
const [only, ...rest] = findCellRefs(inner);
return only && rest.length === 0 && only.start === 0 && only.ref.length === inner.length ? only.ref : null;
Expand Down
6 changes: 3 additions & 3 deletions src/engine/functions/analysis-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function rSquared(pairs: NumericPairs): number | SpreadsheetError {
*/
export function percentileInclusive(values: number[], k: number): number | SpreadsheetError {
if (values.length === 0 || k < 0 || k > PERCENT_MAX) return NUM_ERROR;
const sorted = [...values].sort((a, b) => a - b);
const sorted = values.toSorted((a, b) => a - b);
const position = k * (sorted.length - 1);
const lower = sorted[Math.floor(position)] ?? 0;
const upper = sorted[Math.ceil(position)] ?? lower;
Expand All @@ -100,14 +100,14 @@ export function quartileInclusive(values: number[], quarter: number): number | S
export function kthLargest(values: number[], k: number): number | SpreadsheetError {
const position = Math.ceil(k);
if (position < 1 || position > values.length) return NUM_ERROR;
return [...values].sort((a, b) => b - a)[position - 1] ?? NUM_ERROR;
return values.toSorted((a, b) => b - a)[position - 1] ?? NUM_ERROR;
}

/** The k-th smallest value (SMALL), under the same rules as `kthLargest`. */
export function kthSmallest(values: number[], k: number): number | SpreadsheetError {
const position = Math.ceil(k);
if (position < 1 || position > values.length) return NUM_ERROR;
return [...values].sort((a, b) => a - b)[position - 1] ?? NUM_ERROR;
return values.toSorted((a, b) => a - b)[position - 1] ?? NUM_ERROR;
}

/** RANK: 1 + how many values beat `target`, so equal values share the top rank.
Expand Down
6 changes: 3 additions & 3 deletions src/engine/functions/array-functions-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function sortedByKeys(rows: CellArray, keys: SortKey[]): CellArray {
const positions = rows.map((_, index) => index);
const byKeys = (left: number, right: number): number =>
keys.reduce((decided, { values, order }) => decided || compareForSort(values[left] ?? "", values[right] ?? "", order), 0);
return [...positions].sort(byKeys).map((index) => rows[index] ?? []);
return positions.toSorted(byKeys).map((index) => rows[index] ?? []);
}

/** SORT: by the `index`-th column (1-based), or the `index`-th row when `byColumn`. */
Expand Down Expand Up @@ -97,7 +97,7 @@ export function sequence(rows: number, columns: number, start: number, step: num
const [height, width] = [Math.trunc(rows), Math.trunc(columns)];
if (height < 0 || width < 0 || !fitsInSheet(height, width)) return VALUE_ERROR;
if (height === 0 || width === 0) return CALC_ERROR;
return Array.from({ length: height }, (_, row) => Array.from({ length: width }, (_, col) => start + (row * width + col) * step));
return Array.from({ length: height }, (_, row) => Array.from({ length: width }, (_cell, col) => start + (row * width + col) * step));
}

/** The 0-based positions CHOOSEROWS / CHOOSECOLS pick: 1 is the first, -1 the last; 0 or past the
Expand All @@ -121,7 +121,7 @@ export function chooseColumns(table: CellArray, picks: number[]): TableResult {
export function stackHorizontally(tables: CellArray[]): CellArray {
const height = tables.reduce((tallest, table) => Math.max(tallest, table.length), 0);
return Array.from({ length: height }, (_, row) =>
tables.flatMap((table) => Array.from({ length: widthOf(table) }, (_, col) => table[row]?.[col] ?? NA_ERROR)),
tables.flatMap((table) => Array.from({ length: widthOf(table) }, (_cell, col) => table[row]?.[col] ?? NA_ERROR)),
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/engine/functions/descriptive-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MIN_SKEWNESS_VALUES = 3;
const PROBABILITY_SUM_TOLERANCE = 1e-9;

// A Float64Array sorts by value; a plain array's default sort compares text.
const ascending = (values: readonly number[]): number[] => Array.from(Float64Array.from(values).sort());
const ascending = (values: readonly number[]): number[] => Array.from(Float64Array.from(values).toSorted());

/** TRIMMEAN: the mean once ⌊n·percent / 2⌋ values are dropped from each end, as Excel rounds the
* dropped count down to an even number. A percent outside 0 ≤ p < 1, or no values: #NUM!. */
Expand Down
2 changes: 1 addition & 1 deletion src/engine/functions/distribution-families-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const logChoose = (n: number, k: number): number => logGamma(n + 1) - logGamma(k

/** The x in [low, high] where the increasing `cdf` reaches `p`, the bracket widened upward until
* it holds the answer, then bisected until it is as narrow as a double can tell. */
export function invertIncreasing(cdf: (x: number) => number, p: number, low: number, high: number): number {
function invertIncreasing(cdf: (x: number) => number, p: number, low: number, high: number): number {
const bracket = { low, high };
while (cdf(bracket.high) < p && bracket.high < Number.MAX_VALUE / 2) bracket.high *= 2;
for (let step = 0; step < MAX_BISECTIONS; step += 1) {
Expand Down
9 changes: 5 additions & 4 deletions src/engine/functions/forecast-ets-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const sum = (values: readonly number[]): number => values.reduce((total, value)
const mean = (values: readonly number[]): number => sum(values) / values.length;

function median(values: readonly number[]): number {
const sorted = [...values].sort((left, right) => left - right);
const sorted = values.toSorted((left, right) => left - right);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 1 ? (sorted[middle] ?? 0) : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
}
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface RegularSeries {

/** Each distinct time with the values at it, in time order. */
function groupedByTime(points: readonly TimedValue[]): { time: number; values: number[] }[] {
const sorted = [...points].sort((left, right) => left.time - right.time);
const sorted = points.toSorted((left, right) => left.time - right.time);
return sorted.reduce<{ time: number; values: number[] }[]>((groups, point) => {
const last = groups.at(-1);
const group = last && last.time === point.time ? last : { time: point.time, values: [] };
Expand Down Expand Up @@ -217,12 +217,13 @@ const squaredErrorOf = (values: readonly number[], period: number, smoothing: Sm
* one-step errors, least first (a tie in grid order). */
function scoredGrid(values: readonly number[], period: number): Scored[] {
const candidates = grid(1).flatMap((alpha) => grid(1).flatMap((beta) => (period === 0 ? [0] : grid(1 - alpha)).map((gamma) => ({ alpha, beta, gamma }))));
return candidates.map((smoothing) => ({ smoothing, error: squaredErrorOf(values, period, smoothing) })).sort((left, right) => left.error - right.error);
return candidates.map((smoothing) => ({ smoothing, error: squaredErrorOf(values, period, smoothing) })).toSorted((left, right) => left.error - right.error);
}

const clip = (value: number, limit: number): number => Math.min(Math.max(value, 0), limit);

/** The smoothings one `step` from `at` along each parameter, kept within 0 ≤ α, β ≤ 1 and 0 ≤ γ ≤ 1 − α. */
function neighbours(at: Smoothing, step: number, period: number): Smoothing[] {
const clip = (value: number, limit: number): number => Math.min(Math.max(value, 0), limit);
const moved = [-step, step].flatMap((delta) => {
const alpha = clip(at.alpha + delta, 1);
const alongAlpha = { ...at, alpha, gamma: Math.min(at.gamma, 1 - alpha) };
Expand Down
18 changes: 8 additions & 10 deletions src/engine/functions/group-by-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function sortedRows(rows: CellArray, sortOrder: readonly number[]): CellA
});
return decided.find((difference) => difference !== 0) ?? 0;
};
return positions.sort(compare).map((index) => rows[index] ?? []);
return positions.toSorted(compare).map((index) => rows[index] ?? []);
}

export interface GroupByOptions {
Expand Down Expand Up @@ -115,7 +115,7 @@ function withSubtotals(leaves: Leaf[], prefix: number, layout: SubtotalLayout):
block.flatMap((leaf) => leaf.dataRows),
layout.aggregate,
);
return layout.atTop ? [subtotal, ...inner] : [...inner, subtotal];
return layout.atTop ? [subtotal].concat(inner) : inner.concat([subtotal]);
});
}

Expand Down Expand Up @@ -151,14 +151,12 @@ export function pivotTable(
const columns: (Group | null)[] = withTotal(columnGroups, options.columnTotals);
const rows: (Group | null)[] = withTotal(rowGroups, options.rowTotals);
const blankRowKey = Array.from({ length: rowWidth }, (): CellValue => "");
const headerRows = Array.from({ length: columnDepth }, (_, level) => [
...blankRowKey,
...columns.map((group) => (group === null ? (level === 0 ? TOTAL : "") : (group.key[level] ?? ""))),
]);
const bodyRows = rows.map((rowGroup) => [
...(rowGroup === null ? [TOTAL, ...blankRowKey.slice(1)] : rowGroup.key),
...columns.map((columnGroup) => cell(both(rowGroup, columnGroup))),
]);
const headerRows = Array.from({ length: columnDepth }, (_, level) =>
blankRowKey.concat(columns.map((group) => (group === null ? (level === 0 ? TOTAL : "") : (group.key[level] ?? "")))),
);
const bodyRows = rows.map((rowGroup) =>
(rowGroup === null ? [TOTAL, ...blankRowKey.slice(1)] : rowGroup.key).concat(columns.map((columnGroup) => cell(both(rowGroup, columnGroup)))),
);
return [...headerRows, ...bodyRows];
}

Expand Down
3 changes: 2 additions & 1 deletion src/engine/functions/group-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ function tableOfHeight(args: string[], context: FunctionContext, index: number,
const split = (table: CellArray, hasHeaders: boolean): { data: CellArray; header: CellValue[] } =>
hasHeaders ? { data: table.slice(1), header: table[0] ?? [] } : { data: table, header: [] };

const generated = (prefix: string, count: number): CellValue[] => Array.from({ length: count }, (_, index) => `${prefix} ${index + 1}`);

// GROUPBY(row_fields, values, function, [field_headers], [total_depth], [sort_order], [filter_array], [field_relationship])
const groupByHandler: FunctionHandler = (args, context) => {
const keysTable = tableArg(args, context, 0);
Expand All @@ -76,7 +78,6 @@ const groupByHandler: FunctionHandler = (args, context) => {
if (typeof totalDepth !== "number" || isSpreadsheetErrorValue(sortOrder) || isSpreadsheetErrorValue(included)) {
return [totalDepth, sortOrder, included].find(isSpreadsheetErrorValue) ?? VALUE_ERROR;
}
const generated = (prefix: string, count: number): CellValue[] => Array.from({ length: count }, (_, index) => `${prefix} ${index + 1}`);
const headers =
mode === 3 ? [...keys.header, ...values.header] : mode === 2 ? [...generated("Row Field", keyWidth), ...generated("Value", width - keyWidth)] : null;
return groupByTable(keys.data, values.data, included, aggregate, { headers, totalDepth: Math.trunc(totalDepth), sortOrder });
Expand Down
12 changes: 6 additions & 6 deletions src/engine/functions/hypothesis-tests-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ interface Ranked {
tieSizes: number[];
}

export function midRanks(values: readonly number[]): Ranked {
const order = values.map((value, index) => ({ value, index })).sort((left, right) => left.value - right.value);
function midRanks(values: readonly number[]): Ranked {
const order = values.map((value, index) => ({ value, index })).toSorted((left, right) => left.value - right.value);
const starts = order.flatMap((entry, position) => (position === 0 || entry.value !== order[position - 1]?.value ? [position] : []));
const ranks = Array.from({ length: values.length }, () => 0);
const tieSizes = starts.map((start, group) => {
Expand Down Expand Up @@ -192,8 +192,8 @@ const KOLMOGOROV_SMALL_LAMBDA = 1;
/** The largest gap between the two empirical distribution functions, in units of 1 / (m·n) so
* that it is an integer: max |i·n − j·m| after each distinct value. */
function largestGap(first: number[], second: number[]): number {
const [left, right] = [Float64Array.from(first).sort(), Float64Array.from(second).sort()];
const pooled = Float64Array.from([...left, ...right]).sort();
const [left, right] = [Float64Array.from(first).toSorted(), Float64Array.from(second).toSorted()];
const pooled = Float64Array.from([...left, ...right]).toSorted();
const counter = { i: 0, j: 0, gap: 0 };
pooled.forEach((value, position) => {
if (pooled[position + 1] === value) return;
Expand Down Expand Up @@ -245,7 +245,7 @@ export function kolmogorovSmirnov(first: number[], second: number[]): Result {
const gap = largestGap(first, second);
if (gap === 0) return 1;
if (m * n > KS_EXACT_MAX_PRODUCT) return kolmogorovUpper((gap / (m * n)) * Math.sqrt((m * n) / (m + n)));
const pooled = Float64Array.from([...first, ...second]).sort();
const pooled = Float64Array.from([...first, ...second]).toSorted();
const observed = Array.from({ length: m + n + 1 }, (_, count) => count > 0 && pooled[count - 1] !== pooled[count]);
return clampProbability(ksExactOutside(m, n, gap, observed));
}
Expand Down Expand Up @@ -328,7 +328,7 @@ function shapiroProbability(oneMinusW: number, n: number): number {
export function shapiroWilk(values: number[]): Result {
const n = values.length;
if (n < SW_MIN_VALUES) return DIV_ZERO_ERROR;
const sorted = Float64Array.from(values).sort();
const sorted = Float64Array.from(values).toSorted();
if ((sorted[n - 1] ?? 0) - (sorted[0] ?? 0) === 0) return DIV_ZERO_ERROR;
const center = sorted[Math.floor(n / 2)] ?? 0;
const centered = sorted.map((value) => value - center);
Expand Down
10 changes: 5 additions & 5 deletions src/engine/functions/let-lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ const letHandler: FunctionHandler = (args, context) => {
const pairs = Array.from({ length: (args.length - 1) / 2 }, (_, index) => ({ name: args[2 * index]?.trim() ?? "", written: args[2 * index + 1] ?? "" }));
if (!pairs.every(({ name }) => isValidName(name))) return NAME_ERROR;
return withHoldings((holdings) => {
const bindings = pairs.reduce<Binding[]>(
(bound, { name, written }) => [...bound, bindingFor(name, substituteNames(written, bound), context, holdings)],
[],
);
const bindings = pairs.reduce<Binding[]>((bound, { name, written }) => {
bound.push(bindingFor(name, substituteNames(written, bound), context, holdings));
return bound;
}, []);
return valueOf(substituteNames(args.at(-1) ?? "", bindings), context);
});
};
Expand Down Expand Up @@ -168,7 +168,7 @@ const mapHandler: FunctionHandler = (args, context) => {
const sources = args.slice(0, -1).map((arg) => elementSource(arg, context, holdings));
const shape = combinedShape(sources.map((source) => ({ rows: source.rows, columns: source.columns })));
return Array.from({ length: shape.rows }, (_, row) =>
Array.from({ length: shape.columns }, (_, col) => {
Array.from({ length: shape.columns }, (_cell, col) => {
const positions = sources.map((source) => ({ source, sourceRow: alignedIndex(source.rows, row), sourceCol: alignedIndex(source.columns, col) }));
if (positions.some(({ sourceRow, sourceCol }) => sourceRow < 0 || sourceCol < 0)) return NA_ERROR;
const texts = positions.map(({ source, sourceRow, sourceCol }) => source.textAt(sourceRow, sourceCol));
Expand Down
Loading
Loading