UNPKG

hyperformula

Version:

HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas

268 lines 10.2 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { CellError, movedSimpleCellAddress } from "../Cell.mjs"; import { AddRowsTransformer } from "../dependencyTransformers/AddRowsTransformer.mjs"; import { RemoveRowsTransformer } from "../dependencyTransformers/RemoveRowsTransformer.mjs"; import { forceNormalizeString } from "../interpreter/ArithmeticHelper.mjs"; import { EmptyValue, getRawValue } from "../interpreter/InterpreterValue.mjs"; import { SimpleRangeValue } from "../SimpleRangeValue.mjs"; import { StatType } from "../statistics/index.mjs"; import { ColumnBinarySearch } from "./ColumnBinarySearch.mjs"; export class ColumnIndex { constructor(dependencyGraph, config, stats) { this.dependencyGraph = dependencyGraph; this.config = config; this.stats = stats; this.index = new Map(); this.transformingService = this.dependencyGraph.lazilyTransformingAstService; this.binarySearchStrategy = new ColumnBinarySearch(dependencyGraph); } add(value, address) { if (value === EmptyValue || value instanceof CellError) { return; } else if (value instanceof SimpleRangeValue) { for (const [arrayValue, cellAddress] of value.entriesFromTopLeftCorner(address)) { this.addSingleCellValue(getRawValue(arrayValue), cellAddress); } } else { this.addSingleCellValue(value, address); } } remove(value, address) { if (value === undefined) { return; } if (value instanceof SimpleRangeValue) { for (const [arrayValue, cellAddress] of value.entriesFromTopLeftCorner(address)) { this.removeSingleValue(getRawValue(arrayValue), cellAddress); } } else { this.removeSingleValue(value, address); } } change(oldValue, newValue, address) { if (oldValue === newValue) { return; } this.remove(oldValue, address); this.add(newValue, address); } applyChanges(contentChanges) { for (const change of contentChanges) { if (change.oldValue !== undefined) { this.change(getRawValue(change.oldValue), getRawValue(change.value), change.address); } } } moveValues(sourceRange, toRight, toBottom, toSheet) { for (const [value, address] of sourceRange) { const targetAddress = movedSimpleCellAddress(address, toSheet, toRight, toBottom); this.remove(value, address); this.add(value, targetAddress); } } removeValues(range) { for (const [value, address] of range) { this.remove(value, address); } } find(searchKey, rangeValue, { ordering, ifNoMatch, returnOccurrence }) { if (returnOccurrence == null) { returnOccurrence = ordering === 'none' ? 'first' : 'last'; } const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurrence); return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, ifNoMatch, returnOccurrence }); } findUsingColumnIndex(key, rangeValue, returnOccurrence) { const range = rangeValue.range; if (range === undefined) { return undefined; } this.ensureRecentData(range.sheet, range.start.col, key); const columnMap = this.getColumnMap(range.sheet, range.start.col); if (!columnMap) { return -1; } const normalizedKey = typeof key === 'string' ? forceNormalizeString(key) : key; const valueIndexForTheKey = columnMap.get(normalizedKey); if (!valueIndexForTheKey || !valueIndexForTheKey.index || valueIndexForTheKey.index.length === 0) { return undefined; } const rowNumber = ColumnIndex.findRowBelongingToRange(valueIndexForTheKey, range, returnOccurrence); return rowNumber !== undefined ? rowNumber - range.start.row : undefined; } static findRowBelongingToRange(valueIndex, range, returnOccurrence) { const start = range.start.row; const end = range.end.row; const positionInIndex = returnOccurrence === 'first' ? findInOrderedArray(start, valueIndex.index, 'upperBound') : findInOrderedArray(end, valueIndex.index, 'lowerBound'); if (positionInIndex === -1) { return undefined; } const rowNumber = valueIndex.index[positionInIndex]; const isRowNumberBelongingToRange = rowNumber >= start && rowNumber <= end; return isRowNumberBelongingToRange ? rowNumber : undefined; } advancedFind(keyMatcher, range, options = { returnOccurrence: 'first' }) { return this.binarySearchStrategy.advancedFind(keyMatcher, range, options); } addColumns(columnsSpan) { const sheetIndex = this.index.get(columnsSpan.sheet); if (!sheetIndex) { return; } sheetIndex.splice(columnsSpan.columnStart, 0, ...Array(columnsSpan.numberOfColumns)); } removeColumns(columnsSpan) { const sheetIndex = this.index.get(columnsSpan.sheet); if (!sheetIndex) { return; } sheetIndex.splice(columnsSpan.columnStart, columnsSpan.numberOfColumns); } removeSheet(sheetId) { this.index.delete(sheetId); } getColumnMap(sheet, col) { if (!this.index.has(sheet)) { this.index.set(sheet, []); } const sheetMap = this.index.get(sheet); // eslint-disable-line @typescript-eslint/no-non-null-assertion let columnMap = sheetMap[col]; if (!columnMap) { columnMap = new Map(); sheetMap[col] = columnMap; } return columnMap; } getValueIndex(sheet, col, value) { const columnMap = this.getColumnMap(sheet, col); let index = this.getColumnMap(sheet, col).get(value); if (!index) { index = { version: this.transformingService.version(), index: [] }; columnMap.set(value, index); } return index; } ensureRecentData(sheet, col, value) { const valueIndex = this.getValueIndex(sheet, col, value); const actualVersion = this.transformingService.version(); if (valueIndex.version === actualVersion) { return; } const relevantTransformations = this.transformingService.getTransformationsFrom(valueIndex.version, transformation => { return transformation.sheet === sheet && (transformation instanceof AddRowsTransformer || transformation instanceof RemoveRowsTransformer); }); for (const transformation of relevantTransformations) { if (transformation instanceof AddRowsTransformer) { this.addRows(col, transformation.rowsSpan, value); } else if (transformation instanceof RemoveRowsTransformer) { this.removeRows(col, transformation.rowsSpan, value); } } valueIndex.version = actualVersion; } addSingleCellValue(value, address) { this.stats.measure(StatType.BUILD_COLUMN_INDEX, () => { this.ensureRecentData(address.sheet, address.col, value); if (typeof value === 'string') { value = forceNormalizeString(value); } const valueIndex = this.getValueIndex(address.sheet, address.col, value); ColumnIndex.addValue(valueIndex, address.row); }); } removeSingleValue(value, address) { this.stats.measure(StatType.BUILD_COLUMN_INDEX, () => { this.ensureRecentData(address.sheet, address.col, value); const columnMap = this.getColumnMap(address.sheet, address.col); if (typeof value === 'string') { value = forceNormalizeString(value); } const valueIndex = columnMap.get(value); if (!valueIndex) { return; } const positionInIndex = findInOrderedArray(address.row, valueIndex.index); if (positionInIndex > -1) { valueIndex.index.splice(positionInIndex, 1); } if (valueIndex.index.length === 0) { columnMap.delete(value); } if (columnMap.size === 0) { delete this.index.get(address.sheet)[address.col]; // eslint-disable-line @typescript-eslint/no-non-null-assertion } }); } addRows(col, rowsSpan, value) { const valueIndex = this.getValueIndex(rowsSpan.sheet, col, value); ColumnIndex.shiftRows(valueIndex, rowsSpan.rowStart, rowsSpan.numberOfRows); } removeRows(col, rowsSpan, value) { const valueIndex = this.getValueIndex(rowsSpan.sheet, col, value); ColumnIndex.removeRowsFromValues(valueIndex, rowsSpan); ColumnIndex.shiftRows(valueIndex, rowsSpan.rowEnd + 1, -rowsSpan.numberOfRows); } static addValue(valueIndex, rowNumber) { const rowIndex = findInOrderedArray(rowNumber, valueIndex.index, 'lowerBound'); const isRowNumberAlreadyInIndex = valueIndex.index[rowIndex] === rowNumber; if (!isRowNumberAlreadyInIndex) { valueIndex.index.splice(rowIndex + 1, 0, rowNumber); } } static removeRowsFromValues(valueIndex, rowsSpan) { const start = findInOrderedArray(rowsSpan.rowStart, valueIndex.index, 'upperBound'); const end = findInOrderedArray(rowsSpan.rowEnd, valueIndex.index, 'lowerBound'); const isFoundSpanValid = start > -1 && end > -1 && start <= end && valueIndex.index[start] <= rowsSpan.rowEnd; if (isFoundSpanValid) { valueIndex.index.splice(start, end - start + 1); } } static shiftRows(valueIndex, afterRow, numberOfRows) { const positionInIndex = findInOrderedArray(afterRow, valueIndex.index, 'upperBound'); if (positionInIndex === -1) { return; } for (let i = positionInIndex; i < valueIndex.index.length; ++i) { valueIndex.index[i] += numberOfRows; } } } /* * Returns: * - index of the key, if the key exists in the array, * - index of the lower/upper bound (depending on handlingMisses parameter) otherwise. * Assumption: The array is ordered ascending and contains no repetitions. */ export function findInOrderedArray(key, values, handlingMisses = 'upperBound') { let start = 0; let end = values.length - 1; while (start <= end) { const center = Math.floor((start + end) / 2); if (key > values[center]) { start = center + 1; } else if (key < values[center]) { end = center - 1; } else { return center; } } const foundIndex = handlingMisses === 'lowerBound' ? end : start; const isIndexInRange = foundIndex >= 0 && foundIndex <= values.length; return isIndexInRange ? foundIndex : -1; }