import { describe, it, expect } from 'vitest'; import { createArrayResult, columnVector, rowVector, computeSpillRange, spillCells, isInSpillRange, spillRangesOverlap, arrayUnique, arraySort, arrayFilter, arrayTranspose, arraySequence, arrayFlatten, getArrayValue, } from '../src/sheets/array-formulas.js'; describe('createArrayResult', () => { it('creates from 2D array', () => { const r = createArrayResult([[1, 2], [3, 4]]); expect(r.rows).toBe(2); expect(r.cols).toBe(2); expect(r.values).toEqual([[1, 2], [3, 4]]); }); it('normalizes ragged arrays', () => { const r = createArrayResult([[1, 2, 3], [4]]); expect(r.cols).toBe(3); expect(r.values[1]).toEqual([4, null, null]); }); it('handles empty array', () => { const r = createArrayResult([]); expect(r.rows).toBe(0); expect(r.cols).toBe(0); }); }); describe('columnVector', () => { it('creates column from flat array', () => { const r = columnVector([1, 2, 3]); expect(r.rows).toBe(3); expect(r.cols).toBe(1); expect(r.values).toEqual([[1], [2], [3]]); }); }); describe('rowVector', () => { it('creates row from flat array', () => { const r = rowVector([1, 2, 3]); expect(r.rows).toBe(1); expect(r.cols).toBe(3); expect(r.values).toEqual([[1, 2, 3]]); }); }); describe('computeSpillRange', () => { it('computes range from origin and result', () => { const result = createArrayResult([[1, 2], [3, 4], [5, 6]]); const range = computeSpillRange(2, 3, result); expect(range).toEqual({ originRow: 2, originCol: 3, rows: 3, cols: 2 }); }); }); describe('spillCells', () => { it('lists all cells in range', () => { const range = { originRow: 1, originCol: 1, rows: 2, cols: 3 }; const cells = spillCells(range); expect(cells).toHaveLength(6); expect(cells[0]).toEqual({ row: 1, col: 1 }); expect(cells[5]).toEqual({ row: 2, col: 3 }); }); }); describe('isInSpillRange', () => { const range = { originRow: 2, originCol: 3, rows: 3, cols: 2 }; it('returns true for cell inside range', () => { expect(isInSpillRange(3, 4, range)).toBe(true); }); it('returns true for origin cell', () => { expect(isInSpillRange(2, 3, range)).toBe(true); }); it('returns false for cell outside range', () => { expect(isInSpillRange(1, 3, range)).toBe(false); expect(isInSpillRange(5, 3, range)).toBe(false); expect(isInSpillRange(2, 5, range)).toBe(false); }); }); describe('spillRangesOverlap', () => { it('detects overlapping ranges', () => { const a = { originRow: 0, originCol: 0, rows: 3, cols: 3 }; const b = { originRow: 2, originCol: 2, rows: 2, cols: 2 }; expect(spillRangesOverlap(a, b)).toBe(true); }); it('detects non-overlapping ranges', () => { const a = { originRow: 0, originCol: 0, rows: 2, cols: 2 }; const b = { originRow: 3, originCol: 3, rows: 2, cols: 2 }; expect(spillRangesOverlap(a, b)).toBe(false); }); it('detects adjacent (non-overlapping)', () => { const a = { originRow: 0, originCol: 0, rows: 2, cols: 2 }; const b = { originRow: 0, originCol: 2, rows: 2, cols: 2 }; expect(spillRangesOverlap(a, b)).toBe(false); }); }); describe('arrayUnique', () => { it('removes duplicate rows', () => { const r = createArrayResult([[1, 'a'], [2, 'b'], [1, 'a'], [3, 'c']]); const u = arrayUnique(r); expect(u.rows).toBe(3); expect(u.values).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]); }); }); describe('arraySort', () => { it('sorts by first column ascending', () => { const r = createArrayResult([[3, 'c'], [1, 'a'], [2, 'b']]); const s = arraySort(r, 0, true); expect(s.values).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]); }); it('sorts descending', () => { const r = createArrayResult([[1], [3], [2]]); const s = arraySort(r, 0, false); expect(s.values).toEqual([[3], [2], [1]]); }); it('sorts strings', () => { const r = createArrayResult([['banana'], ['apple'], ['cherry']]); const s = arraySort(r, 0, true); expect(s.values[0][0]).toBe('apple'); }); }); describe('arrayFilter', () => { it('filters rows by predicate', () => { const r = createArrayResult([[1, 'a'], [2, 'b'], [3, 'c']]); const f = arrayFilter(r, 0, v => (v as number) > 1); expect(f.rows).toBe(2); expect(f.values).toEqual([[2, 'b'], [3, 'c']]); }); it('returns empty when nothing matches', () => { const r = createArrayResult([[1], [2]]); const f = arrayFilter(r, 0, v => (v as number) > 10); expect(f.rows).toBe(0); }); }); describe('arrayTranspose', () => { it('swaps rows and columns', () => { const r = createArrayResult([[1, 2, 3], [4, 5, 6]]); const t = arrayTranspose(r); expect(t.rows).toBe(3); expect(t.cols).toBe(2); expect(t.values).toEqual([[1, 4], [2, 5], [3, 6]]); }); it('handles empty array', () => { const t = arrayTranspose(createArrayResult([])); expect(t.rows).toBe(0); }); }); describe('arraySequence', () => { it('generates sequence', () => { const r = arraySequence(3, 1, 1, 1); expect(r.values).toEqual([[1], [2], [3]]); }); it('generates 2D sequence', () => { const r = arraySequence(2, 3, 10, 5); expect(r.values).toEqual([[10, 15, 20], [25, 30, 35]]); }); }); describe('arrayFlatten', () => { it('flattens 2D to column', () => { const r = createArrayResult([[1, 2], [3, 4]]); const f = arrayFlatten(r); expect(f.rows).toBe(4); expect(f.cols).toBe(1); expect(f.values).toEqual([[1], [2], [3], [4]]); }); }); describe('getArrayValue', () => { it('returns value at position', () => { const r = createArrayResult([[1, 2], [3, 4]]); expect(getArrayValue(r, 0, 0)).toBe(1); expect(getArrayValue(r, 1, 1)).toBe(4); }); it('returns null for out-of-bounds', () => { const r = createArrayResult([[1]]); expect(getArrayValue(r, 5, 0)).toBeNull(); expect(getArrayValue(r, 0, 5)).toBeNull(); expect(getArrayValue(r, -1, 0)).toBeNull(); }); });