From e1d91aa74ccda49b94219c4d1617e3f4c35032f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20=C5=81asocha?= Date: Thu, 27 Feb 2020 00:40:19 +0100 Subject: [PATCH] Formula helpers and named expressions (part 2) (#192) * Add test for 255-long named expression * Add more validations for named expressions * Export ExportedCellChange object instead of class Because in a moment I will introduce second class, representing changed named expression. * It no longer exports only values * Don't leak internal detail of workbook named expressions, -1 sheet * Add tests for named expression changes * Added test for matrix formula without braces * Add tests for errors in named expressions with something other than formula * lint-fix * Add ability to calculate fire-and-forget formulas * Refactor common part * lint-fix * Add documentation * Fix tests * Make it more clear what is the reason we initialize -1 sheet * Add possibility to calculateFormula in context of some sheet * Rename spec * Change the test other way around * Support any raw cell content as an expression * Refactoring: Rename NamedExpressions#storeFormulaInCell -> #storeExpressionInCell * Refactoring: Rename #changeNamedExpressionFormula -> #changeNamedExpressionExpression * Support other types of input when changing named expressions * Refactoring: change naming external formula to temporary formula Since formula is not stored, temporary sounds better * Refactoring: Make HyperFormula know less not directly know that named expression is stored in some specific cell. * lint-fix --- src/CellValue.ts | 57 ++++++++- src/ContentChanges.ts | 15 +-- src/Evaluator.ts | 3 + src/HyperFormula.ts | 108 ++++++++++------- src/NamedExpressions.ts | 76 +++++++++--- src/SingleThreadEvaluator.ts | 4 + src/index.ts | 6 +- test/CellValueExporter.spec.ts | 59 +++++----- test/cruds/adding-columns.spec.ts | 4 +- test/cruds/adding-row.spec.ts | 4 +- test/cruds/change-cell-content.spec.ts | 37 ++---- test/cruds/copy-paste.spec.ts | 10 +- test/cruds/move-columns.spec.ts | 6 +- test/cruds/move-rows.spec.ts | 6 +- test/cruds/removing-columns.spec.ts | 5 +- test/cruds/removing-rows.spec.ts | 5 +- test/cruds/removing-sheet.spec.ts | 6 +- test/cruds/replace-sheet-content.spec.ts | 15 +-- test/external-formulas.spec.ts | 41 ------- test/interpreter/matrix-plugin.spec.ts | 13 +++ test/named-expressions.spec.ts | 141 ++++++++++++++++++++--- test/temporary-formulas.spec.ts | 98 ++++++++++++++++ 22 files changed, 504 insertions(+), 215 deletions(-) delete mode 100644 test/external-formulas.spec.ts create mode 100644 test/temporary-formulas.spec.ts diff --git a/src/CellValue.ts b/src/CellValue.ts index bfdaeb6b68..8e459fb0a7 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -1,8 +1,44 @@ -import {CellError, ErrorType, InternalCellValue, NoErrorCellValue} from './Cell' +import {CellError, ErrorType, InternalCellValue, NoErrorCellValue, simpleCellAddress, SimpleCellAddress} from './Cell' +import {CellValueChange} from './ContentChanges' import {Config} from './Config' +import {NamedExpressions} from './NamedExpressions' export type CellValue = NoErrorCellValue | DetailedCellError +export type ExportedChange = ExportedCellChange | ExportedNamedExpressionChange + +export class ExportedCellChange { + constructor( + public readonly address: SimpleCellAddress, + public readonly newValue: CellValue, + ) { + } + + public get col() { + return this.address.col + } + + public get row() { + return this.address.row + } + + public get sheet() { + return this.address.sheet + } + + public get value() { + return this.newValue + } +} + +export class ExportedNamedExpressionChange { + constructor( + public readonly name: string, + public readonly newValue: CellValue, + ) { + } +} + export class DetailedCellError { constructor( public readonly error: CellError, @@ -19,13 +55,28 @@ export class DetailedCellError { } } -export class CellValueExporter { +export class Exporter { constructor( private readonly config: Config, + private readonly namedExpressions: NamedExpressions, ) { } - public export(value: InternalCellValue): CellValue { + public exportChange(change: CellValueChange): ExportedChange { + if (change.sheet === NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS) { + return new ExportedNamedExpressionChange( + this.namedExpressions.fetchNameForNamedExpressionRow(change.row), + this.exportValue(change.value), + ) + } else { + return new ExportedCellChange( + simpleCellAddress(change.sheet, change.col, change.row), + this.exportValue(change.value), + ) + } + } + + public exportValue(value: InternalCellValue): CellValue { if (this.config.smartRounding && typeof value == 'number' && !Number.isInteger(value)) { return this.cellValueRounding(value) } else if (value instanceof CellError) { diff --git a/src/ContentChanges.ts b/src/ContentChanges.ts index 4a364e3d69..0fc02bf0c2 100644 --- a/src/ContentChanges.ts +++ b/src/ContentChanges.ts @@ -1,5 +1,4 @@ import {InternalCellValue, SimpleCellAddress} from './Cell' -import {CellValueExporter} from './CellValue' import {Matrix} from './Matrix' export interface CellValueChange { @@ -9,6 +8,9 @@ export interface CellValueChange { value: InternalCellValue, } +export interface IChangeExporter { + exportChange: (arg: CellValueChange) => T, +} export type ChangeList = CellValueChange[] export class ContentChanges { @@ -38,15 +40,10 @@ export class ContentChanges { this.changes.push(...change) } - public exportChanges(exporter: CellValueExporter): ChangeList { - const ret: ChangeList = [] + public exportChanges(exporter: IChangeExporter): T[] { + const ret: T[] = [] this.changes.forEach((e, i) => { - ret[i] = { - sheet: this.changes[i].sheet, - col: this.changes[i].col, - row: this.changes[i].row, - value: exporter.export(this.changes[i].value), - } + ret[i] = exporter.exportChange(this.changes[i]) }) return ret } diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 41e7f421b9..9e40047e4d 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -1,8 +1,11 @@ import {ContentChanges} from './ContentChanges' +import {InternalCellValue, SimpleCellAddress} from './Cell' +import {Ast} from './parser' import {Vertex} from './DependencyGraph' export interface Evaluator { run(): void, partialRun(vertices: Vertex[]): ContentChanges, + runAndForget(ast: Ast, address: SimpleCellAddress): InternalCellValue, destroy(): void, } diff --git a/src/HyperFormula.ts b/src/HyperFormula.ts index 054237bde6..4ae30cdc2b 100644 --- a/src/HyperFormula.ts +++ b/src/HyperFormula.ts @@ -10,10 +10,9 @@ import { SimpleCellAddress, } from './Cell' import {CellContent, CellContentParser, isMatrix, RawCellContent} from './CellContentParser' -import {CellValue, CellValueExporter} from './CellValue' +import {CellValue, ExportedChange, Exporter} from './CellValue' import {IColumnSearchStrategy} from './ColumnSearch/ColumnSearchStrategy' import {Config} from './Config' -import {ChangeList} from './ContentChanges' import {CrudOperations, normalizeAddedIndexes, normalizeRemovedIndexes} from './CrudOperations' import { AddressMapping, @@ -34,7 +33,7 @@ import {Sheet, Sheets} from './GraphBuilder' import {IBatchExecutor} from './IBatchExecutor' import {LazilyTransformingAstService} from './LazilyTransformingAstService' import {NamedExpressions} from './NamedExpressions' -import {AstNodeType, ParserWithCaching, simpleCellAddressFromString, simpleCellAddressToString, Unparser} from './parser' +import {AstNodeType, ParserWithCaching, simpleCellAddressFromString, simpleCellAddressToString, Unparser, Ast} from './parser' import {Statistics, StatType} from './statistics/Statistics' export type Index = [number, number] @@ -97,7 +96,7 @@ export class HyperFormula { } private readonly crudOperations: CrudOperations - private readonly cellValueExporter: CellValueExporter + private readonly exporter: Exporter private readonly namedExpressions: NamedExpressions constructor( @@ -119,9 +118,8 @@ export class HyperFormula { public readonly lazilyTransformingAstService: LazilyTransformingAstService, ) { this.crudOperations = new CrudOperations(config, stats, dependencyGraph, columnSearch, parser, cellContentParser, lazilyTransformingAstService) - this.cellValueExporter = new CellValueExporter(config) - this.namedExpressions = new NamedExpressions(this.cellContentParser, this.dependencyGraph, this.parser) - this.addressMapping.addSheet(-1, new SparseStrategy(0, 0)) + this.namedExpressions = new NamedExpressions(this.addressMapping, this.cellContentParser, this.dependencyGraph, this.parser, this.crudOperations) + this.exporter = new Exporter(config, this.namedExpressions) } /** @@ -131,7 +129,7 @@ export class HyperFormula { * @param address - cell coordinates */ public getCellValue(address: SimpleCellAddress): CellValue { - return this.cellValueExporter.export(this.dependencyGraph.getCellValue(address)) + return this.exporter.exportValue(this.dependencyGraph.getCellValue(address)) } /** @@ -168,7 +166,7 @@ export class HyperFormula { for (let j = 0; j < sheetWidth; j++) { const address = simpleCellAddress(sheet, j, i) - arr[i][j] = this.cellValueExporter.export(this.dependencyGraph.getCellValue(address)) + arr[i][j] = this.exporter.exportValue(this.dependencyGraph.getCellValue(address)) } } @@ -238,7 +236,7 @@ export class HyperFormula { * @param topLeftCornerAddress - top left corner of block of cells * @param cellContents - array with content */ - public setCellContents(topLeftCornerAddress: SimpleCellAddress, cellContents: RawCellContent[][] | RawCellContent): ChangeList { + public setCellContents(topLeftCornerAddress: SimpleCellAddress, cellContents: RawCellContent[][] | RawCellContent): ExportedChange[] { if (!(cellContents instanceof Array)) { this.crudOperations.setCellContent(topLeftCornerAddress, cellContents) return this.recomputeIfDependencyGraphNeedsIt() @@ -292,7 +290,7 @@ export class HyperFormula { * @param sheet - sheet id in which rows will be added * @param indexes - non-contiguous indexes with format [row, amount], where row is a row number above which the rows will be added */ - public addRows(sheet: number, ...indexes: Index[]): ChangeList { + public addRows(sheet: number, ...indexes: Index[]): ExportedChange[] { this.crudOperations.addRows(sheet, ...indexes) return this.recomputeIfDependencyGraphNeedsIt() } @@ -322,7 +320,7 @@ export class HyperFormula { * @param sheet - sheet id from which rows will be removed * @param indexes - non-contiguous indexes with format [row, amount] * */ - public removeRows(sheet: number, ...indexes: Index[]): ChangeList { + public removeRows(sheet: number, ...indexes: Index[]): ExportedChange[] { this.crudOperations.removeRows(sheet, ...indexes) return this.recomputeIfDependencyGraphNeedsIt() } @@ -352,7 +350,7 @@ export class HyperFormula { * @param sheet - sheet id in which columns will be added * @param indexes - non-contiguous indexes with format [column, amount], where column is a column number from which new columns will be added * */ - public addColumns(sheet: number, ...indexes: Index[]): ChangeList { + public addColumns(sheet: number, ...indexes: Index[]): ExportedChange[] { this.crudOperations.addColumns(sheet, ...indexes) return this.recomputeIfDependencyGraphNeedsIt() } @@ -382,7 +380,7 @@ export class HyperFormula { * @param sheet - sheet id from which columns will be removed * @param indexes - non-contiguous indexes with format [column, amount] * */ - public removeColumns(sheet: number, ...indexes: Index[]): ChangeList { + public removeColumns(sheet: number, ...indexes: Index[]): ExportedChange[] { this.crudOperations.removeColumns(sheet, ...indexes) return this.recomputeIfDependencyGraphNeedsIt() } @@ -414,7 +412,7 @@ export class HyperFormula { * @param height - height of the cell block being moved * @param destinationLeftCorner - upper left address of the target cell block */ - public moveCells(sourceLeftCorner: SimpleCellAddress, width: number, height: number, destinationLeftCorner: SimpleCellAddress): ChangeList { + public moveCells(sourceLeftCorner: SimpleCellAddress, width: number, height: number, destinationLeftCorner: SimpleCellAddress): ExportedChange[] { this.crudOperations.moveCells(sourceLeftCorner, width, height, destinationLeftCorner) return this.recomputeIfDependencyGraphNeedsIt() } @@ -446,7 +444,7 @@ export class HyperFormula { * @param numberOfRows - number of rows to move * @param targetRow - row number before which rows will be moved */ - public moveRows(sheet: number, startRow: number, numberOfRows: number, targetRow: number): ChangeList { + public moveRows(sheet: number, startRow: number, numberOfRows: number, targetRow: number): ExportedChange[] { this.crudOperations.moveRows(sheet, startRow, numberOfRows, targetRow) return this.recomputeIfDependencyGraphNeedsIt() } @@ -478,7 +476,7 @@ export class HyperFormula { * @param numberOfColumns - number of columns to move * @param targetColumn - column number before which columns will be moved */ - public moveColumns(sheet: number, startColumn: number, numberOfColumns: number, targetColumn: number): ChangeList { + public moveColumns(sheet: number, startColumn: number, numberOfColumns: number, targetColumn: number): ExportedChange[] { this.crudOperations.moveColumns(sheet, startColumn, numberOfColumns, targetColumn) return this.recomputeIfDependencyGraphNeedsIt() } @@ -518,7 +516,7 @@ export class HyperFormula { * * @param targetLeftCorner - upper left address of the target cell block * */ - public paste(targetLeftCorner: SimpleCellAddress): ChangeList { + public paste(targetLeftCorner: SimpleCellAddress): ExportedChange[] { this.crudOperations.paste(targetLeftCorner) return this.recomputeIfDependencyGraphNeedsIt() } @@ -538,7 +536,7 @@ export class HyperFormula { public getValuesInRange(range: AbsoluteCellRange): InternalCellValue[][] { return this.dependencyGraph.getValuesInRange(range).map( (subarray: InternalCellValue[]) => subarray.map( - (arg) => this.cellValueExporter.export(arg), + (arg) => this.exporter.exportValue(arg), ), ) } @@ -588,7 +586,7 @@ export class HyperFormula { * * @param name - sheet name */ - public removeSheet(name: string): ChangeList { + public removeSheet(name: string): ExportedChange[] { this.crudOperations.removeSheet(name) return this.recomputeIfDependencyGraphNeedsIt() } @@ -614,7 +612,7 @@ export class HyperFormula { * * @param name - sheet name * */ - public clearSheet(name: string): ChangeList { + public clearSheet(name: string): ExportedChange[] { this.crudOperations.ensureSheetExists(name) this.crudOperations.clearSheet(name) return this.recomputeIfDependencyGraphNeedsIt() @@ -642,7 +640,7 @@ export class HyperFormula { * @param sheetName - sheet name * @param values - array of new values * */ - public setSheetContent(sheetName: string, values: RawCellContent[][]): ChangeList { + public setSheetContent(sheetName: string, values: RawCellContent[][]): ExportedChange[] { this.crudOperations.ensureSheetExists(sheetName) const sheetId = this.getSheetId(sheetName)! @@ -811,7 +809,7 @@ export class HyperFormula { * * @param batchOperations */ - public batch(batchOperations: (e: IBatchExecutor) => void): ChangeList { + public batch(batchOperations: (e: IBatchExecutor) => void): ExportedChange[] { try { batchOperations(this.crudOperations) } catch (e) { @@ -824,16 +822,15 @@ export class HyperFormula { /** * Add named expression * - * @param batchOperations */ - public addNamedExpression(expressionName: string, formulaString: string): ChangeList { + public addNamedExpression(expressionName: string, expression: RawCellContent): ExportedChange[] { if (!this.namedExpressions.isNameValid(expressionName)) { throw new NamedExpressionNameIsInvalid(expressionName) } if (!this.namedExpressions.isNameAvailable(expressionName)) { throw new NamedExpressionNameIsAlreadyTaken(expressionName) } - this.namedExpressions.addNamedExpression(expressionName, formulaString) + this.namedExpressions.addNamedExpression(expressionName, expression) return this.recomputeIfDependencyGraphNeedsIt() } @@ -845,11 +842,11 @@ export class HyperFormula { * @returns CellValue | null */ public getNamedExpressionValue(expressionName: string): CellValue | null { - const internalNamedExpressionAddress = this.namedExpressions.getInternalNamedExpressionAddress(expressionName) - if (internalNamedExpressionAddress === null) { + const namedExpressionValue = this.namedExpressions.getNamedExpressionValue(expressionName) + if (namedExpressionValue === null) { return null } else { - return this.cellValueExporter.export(this.dependencyGraph.getCellValue(internalNamedExpressionAddress)) + return this.exporter.exportValue(namedExpressionValue) } } @@ -859,11 +856,11 @@ export class HyperFormula { * @param expressionName - an expression name * @param newFormulaString - a new formula */ - public changeNamedExpressionFormula(expressionName: string, newFormulaString: string): ChangeList { + public changeNamedExpressionExpression(expressionName: string, newExpression: RawCellContent): ExportedChange[] { if (!this.namedExpressions.doesNamedExpressionExist(expressionName)) { throw new NamedExpressionDoesNotExist(expressionName) } - this.namedExpressions.changeNamedExpressionFormula(expressionName, newFormulaString) + this.namedExpressions.changeNamedExpressionExpression(expressionName, newExpression) return this.recomputeIfDependencyGraphNeedsIt() } @@ -872,7 +869,7 @@ export class HyperFormula { * * @param expressionName - an expression name */ - public removeNamedExpression(expressionName: string): ChangeList { + public removeNamedExpression(expressionName: string): ExportedChange[] { this.namedExpressions.removeNamedExpression(expressionName) return this.recomputeIfDependencyGraphNeedsIt() } @@ -894,13 +891,30 @@ export class HyperFormula { * @returns normalized formula */ public normalizeFormula(formulaString: string): string { - const parsedCellContent = this.cellContentParser.parse(formulaString) - if (!(parsedCellContent instanceof CellContent.Formula)) { + const [ast, address] = this.extractTemporaryFormula(formulaString) + if (!ast) { + throw new Error('This is not a formula') + } + return this.unparser.unparse(ast, address) + } + + /** + * Calculates fire-and-forget formula + * + * @param formulaString - a formula, ex. "=SUM(Sheet1!A1:A100)" + * @param sheetName - a name of the sheet in context of which we evaluate formula + * + * @returns value of the formula + */ + public calculateFormula(formulaString: string, sheetName: string): CellValue { + this.crudOperations.ensureSheetExists(sheetName) + const sheetId = this.sheetMapping.fetch(sheetName) + const [ast, address] = this.extractTemporaryFormula(formulaString, sheetId) + if (!ast) { throw new Error('This is not a formula') } - const exampleExternalFormulaAddress = { sheet: -1, col: 0, row: 0 } - const {ast} = this.parser.parse(parsedCellContent.formula, exampleExternalFormulaAddress) - return this.unparser.unparse(ast, exampleExternalFormulaAddress) + const internalCellValue = this.evaluator.runAndForget(ast, address) + return this.exporter.exportValue(internalCellValue) } /** @@ -911,18 +925,26 @@ export class HyperFormula { * @returns whether formula can be executed outside of regular worksheet */ public validateFormula(formulaString: string): boolean { - const parsedCellContent = this.cellContentParser.parse(formulaString) - if (!(parsedCellContent instanceof CellContent.Formula)) { + const [ast, address] = this.extractTemporaryFormula(formulaString) + if (!ast) { return false } - const exampleExternalFormulaAddress = { sheet: -1, col: 0, row: 0 } - const {ast} = this.parser.parse(parsedCellContent.formula, exampleExternalFormulaAddress) if (ast.type === AstNodeType.ERROR && !ast.error) { return false } return true } + private extractTemporaryFormula(formulaString: string, sheetId: number = 1): [Ast | false, SimpleCellAddress] { + const parsedCellContent = this.cellContentParser.parse(formulaString) + const exampleTemporaryFormulaAddress = { sheet: sheetId, col: 0, row: 0 } + if (!(parsedCellContent instanceof CellContent.Formula)) { + return [false, exampleTemporaryFormulaAddress] + } + const {ast} = this.parser.parse(parsedCellContent.formula, exampleTemporaryFormulaAddress) + return [ast, exampleTemporaryFormulaAddress] + } + /** * Destroys instance of HyperFormula * */ @@ -939,7 +961,7 @@ export class HyperFormula { /** * Runs recomputation starting from recently changed vertices. */ - private recomputeIfDependencyGraphNeedsIt(): ChangeList { + private recomputeIfDependencyGraphNeedsIt(): ExportedChange[] { const changes = this.crudOperations.getAndClearContentChanges() const verticesToRecomputeFrom = Array.from(this.dependencyGraph.verticesToRecompute()) this.dependencyGraph.clearRecentlyChangedVertices() @@ -948,6 +970,6 @@ export class HyperFormula { changes.addAll(this.evaluator.partialRun(verticesToRecomputeFrom)) } - return changes.exportChanges(this.cellValueExporter) + return changes.exportChanges(this.exporter) } } diff --git a/src/NamedExpressions.ts b/src/NamedExpressions.ts index 23d99eeb08..983dd1d499 100644 --- a/src/NamedExpressions.ts +++ b/src/NamedExpressions.ts @@ -1,8 +1,9 @@ import {absolutizeDependencies} from './absolutizeDependencies' -import {SimpleCellAddress, simpleCellAddress} from './Cell' -import {CellContent, CellContentParser} from './CellContentParser' -import {DependencyGraph} from './DependencyGraph' -import { ParserWithCaching} from './parser' +import {SimpleCellAddress, simpleCellAddress, InternalCellValue} from './Cell' +import {CellContent, CellContentParser, RawCellContent} from './CellContentParser' +import {DependencyGraph, AddressMapping, SparseStrategy} from './DependencyGraph' +import {ParserWithCaching} from './parser' +import {CrudOperations} from './CrudOperations' class NamedExpression { constructor( @@ -14,6 +15,7 @@ class NamedExpression { class NamedExpressionsStore { private readonly mapping = new Map() + private readonly rowMapping = new Map() public has(expressionName: string): boolean { return this.mapping.has(this.normalizeExpressionName(expressionName)) @@ -25,14 +27,24 @@ class NamedExpressionsStore { public add(namedExpression: NamedExpression): void { this.mapping.set(this.normalizeExpressionName(namedExpression.name), namedExpression) + this.rowMapping.set(namedExpression.row, namedExpression) } public get(expressionName: string): NamedExpression | undefined { return this.mapping.get(this.normalizeExpressionName(expressionName)) } + public getByRow(row: number): NamedExpression | undefined { + return this.rowMapping.get(row) + } + public remove(expressionName: string): void { - this.mapping.delete(this.normalizeExpressionName(expressionName)) + const normalizedExpressionName = this.normalizeExpressionName(expressionName) + const namedExpression = this.mapping.get(normalizedExpressionName) + if (namedExpression) { + this.mapping.delete(normalizedExpressionName) + this.rowMapping.delete(namedExpression.row) + } } public getAllNamedExpressions(): NamedExpression[] { @@ -50,10 +62,13 @@ export class NamedExpressions { private workbookStore = new NamedExpressionsStore() constructor( + addressMapping: AddressMapping, private readonly cellContentParser: CellContentParser, private readonly dependencyGraph: DependencyGraph, private readonly parser: ParserWithCaching, + private readonly crudOperations: CrudOperations, ) { + addressMapping.addSheet(-1, new SparseStrategy(0, 0)) } public doesNamedExpressionExist(expressionName: string): boolean { @@ -64,11 +79,22 @@ export class NamedExpressions { return this.workbookStore.isNameAvailable(expressionName) } + public fetchNameForNamedExpressionRow(row: number): string { + const namedExpression = this.workbookStore.getByRow(row) + if (!namedExpression) { + throw new Error('Requested Named Expression does not exist') + } + return namedExpression.name + } + public isNameValid(expressionName: string): boolean { - return !expressionName.match(/^\d/) + if (/^[A-Za-z]+[0-9]+$/.test(expressionName)) { + return false + } + return /^[A-Za-z\u00C0-\u02AF_][A-Za-z0-9\u00C0-\u02AF\._]*$/.test(expressionName) } - public addNamedExpression(expressionName: string, formulaString: string): void { + public addNamedExpression(expressionName: string, expression: RawCellContent): void { if (!this.isNameValid(expressionName)) { throw new Error('Name of Named Expression is invalid') } @@ -76,12 +102,12 @@ export class NamedExpressions { throw new Error('Name of Named Expression already taken') } const namedExpression = new NamedExpression(expressionName, this.nextNamedExpressionRow) - this.storeFormulaInCell(namedExpression, formulaString) + this.storeExpressionInCell(namedExpression, expression) this.nextNamedExpressionRow++ this.workbookStore.add(namedExpression) } - public getInternalNamedExpressionAddress(expressionName: string): SimpleCellAddress | null { + private getInternalNamedExpressionAddress(expressionName: string): SimpleCellAddress | null { const namedExpression = this.workbookStore.get(expressionName) if (namedExpression === undefined) { return null @@ -99,29 +125,43 @@ export class NamedExpressions { this.workbookStore.remove(expressionName) } - public changeNamedExpressionFormula(expressionName: string, newFormulaString: string): void { + public changeNamedExpressionExpression(expressionName: string, newExpression: RawCellContent): void { const namedExpression = this.workbookStore.get(expressionName) if (!namedExpression) { throw new Error('Requested Named Expression does not exist') } - this.storeFormulaInCell(namedExpression, newFormulaString) + this.storeExpressionInCell(namedExpression, newExpression) } public getAllNamedExpressionsNames(): string[] { return this.workbookStore.getAllNamedExpressions().map((ne) => ne.name) } + public getNamedExpressionValue(expressionName: string): InternalCellValue | null { + const internalNamedExpressionAddress = this.getInternalNamedExpressionAddress(expressionName) + if (internalNamedExpressionAddress === null) { + return null + } else { + return this.dependencyGraph.getCellValue(internalNamedExpressionAddress) + } + } + private buildAddress(namedExpressionRow: number) { return simpleCellAddress(NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS, 0, namedExpressionRow) } - private storeFormulaInCell(namedExpression: NamedExpression, formula: string) { - const parsedCellContent = this.cellContentParser.parse(formula) - if (!(parsedCellContent instanceof CellContent.Formula)) { - throw new Error('This is not a formula') - } + private storeExpressionInCell(namedExpression: NamedExpression, expression: RawCellContent) { + const parsedCellContent = this.cellContentParser.parse(expression) const address = this.buildAddress(namedExpression.row) - const {ast, hash, hasVolatileFunction, hasStructuralChangeFunction, dependencies} = this.parser.parse(parsedCellContent.formula, address) - this.dependencyGraph.setFormulaToCell(address, ast, absolutizeDependencies(dependencies, address), hasVolatileFunction, hasStructuralChangeFunction) + if (parsedCellContent instanceof CellContent.MatrixFormula) { + throw new Error('Matrix formulas are not supported') + } else if (parsedCellContent instanceof CellContent.Formula) { + const {ast, hash, hasVolatileFunction, hasStructuralChangeFunction, dependencies} = this.parser.parse(parsedCellContent.formula, address) + this.dependencyGraph.setFormulaToCell(address, ast, absolutizeDependencies(dependencies, address), hasVolatileFunction, hasStructuralChangeFunction) + } else if (parsedCellContent instanceof CellContent.Empty) { + this.crudOperations.setCellEmpty(address) + } else { + this.crudOperations.setValueToCell(parsedCellContent.value, address) + } } } diff --git a/src/SingleThreadEvaluator.ts b/src/SingleThreadEvaluator.ts index 1e111c9e1f..cc2a321a44 100644 --- a/src/SingleThreadEvaluator.ts +++ b/src/SingleThreadEvaluator.ts @@ -94,6 +94,10 @@ export class SingleThreadEvaluator implements Evaluator { this.interpreter.destroy() } + public runAndForget(ast: Ast, address: SimpleCellAddress) { + return this.evaluateAstToScalarValue(ast, address) + } + /** * Recalculates formulas in the topological sort order */ diff --git a/src/index.ts b/src/index.ts index 6e4c3c5e04..13decbb6dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { CellError, EmptyValue } from './Cell' -import { CellValue, DetailedCellError } from './CellValue' +import { CellValue, DetailedCellError, ExportedCellChange, ExportedNamedExpressionChange } from './CellValue' import {Config} from './Config' import { InvalidAddressError, @@ -19,6 +19,8 @@ class HyperFormulaNS extends HyperFormula { public static EmptyValue = EmptyValue public static DetailedCellError = DetailedCellError public static LazilyTransformingAstService = LazilyTransformingAstService + public static ExportedCellChange = ExportedCellChange + public static ExportedNamedExpressionChange = ExportedNamedExpressionChange } export default HyperFormulaNS @@ -36,4 +38,6 @@ export { CellError, DetailedCellError, LazilyTransformingAstService, + ExportedCellChange, + ExportedNamedExpressionChange, } diff --git a/test/CellValueExporter.spec.ts b/test/CellValueExporter.spec.ts index 53d324aed2..bb5e4a69f3 100644 --- a/test/CellValueExporter.spec.ts +++ b/test/CellValueExporter.spec.ts @@ -1,56 +1,59 @@ import {CellError, Config, DetailedCellError, EmptyValue} from '../src' import {ErrorType} from '../src/Cell' -import {CellValueExporter} from '../src/CellValue' +import {Exporter} from '../src/CellValue' import {enGB, plPL} from '../src/i18n' import {detailedError} from './testUtils' +import {NamedExpressions} from '../src/NamedExpressions' + +const namedExpressionsMock = {} as NamedExpressions describe( 'rounding', () => { it( 'no rounding', () =>{ - const config = new Config({ smartRounding : false}) - const cellValueExporter = new CellValueExporter(config) - expect(cellValueExporter.export(1.000000000000001)).toBe(1.000000000000001) - expect(cellValueExporter.export(-1.000000000000001)).toBe(-1.000000000000001) - expect(cellValueExporter.export(0.000000000000001)).toBe(0.000000000000001) - expect(cellValueExporter.export(-0.000000000000001)).toBe(-0.000000000000001) - expect(cellValueExporter.export(true)).toBe(true) - expect(cellValueExporter.export(false)).toBe(false) - expect(cellValueExporter.export(1)).toBe(1) - expect(cellValueExporter.export(EmptyValue)).toBe(EmptyValue) - expect(cellValueExporter.export('abcd')).toBe('abcd') + const config = new Config({ smartRounding : false }) + const cellValueExporter = new Exporter(config, namedExpressionsMock) + expect(cellValueExporter.exportValue(1.000000000000001)).toBe(1.000000000000001) + expect(cellValueExporter.exportValue(-1.000000000000001)).toBe(-1.000000000000001) + expect(cellValueExporter.exportValue(0.000000000000001)).toBe(0.000000000000001) + expect(cellValueExporter.exportValue(-0.000000000000001)).toBe(-0.000000000000001) + expect(cellValueExporter.exportValue(true)).toBe(true) + expect(cellValueExporter.exportValue(false)).toBe(false) + expect(cellValueExporter.exportValue(1)).toBe(1) + expect(cellValueExporter.exportValue(EmptyValue)).toBe(EmptyValue) + expect(cellValueExporter.exportValue('abcd')).toBe('abcd') }) it( 'with rounding', () =>{ const config = new Config() - const cellValueExporter = new CellValueExporter(config) - expect(cellValueExporter.export(1.0000000000001)).toBe(1.0000000000001) - expect(cellValueExporter.export(-1.0000000000001)).toBe(-1.0000000000001) - expect(cellValueExporter.export(1.000000000000001)).toBe(1) - expect(cellValueExporter.export(-1.000000000000001)).toBe(-1) - expect(cellValueExporter.export(0.0000000000001)).toBe(0.0000000000001) - expect(cellValueExporter.export(-0.0000000000001)).toBe(-0.0000000000001) - expect(cellValueExporter.export(true)).toBe(true) - expect(cellValueExporter.export(false)).toBe(false) - expect(cellValueExporter.export(1)).toBe(1) - expect(cellValueExporter.export(EmptyValue)).toBe(EmptyValue) - expect(cellValueExporter.export('abcd')).toBe('abcd') + const cellValueExporter = new Exporter(config, namedExpressionsMock) + expect(cellValueExporter.exportValue(1.0000000000001)).toBe(1.0000000000001) + expect(cellValueExporter.exportValue(-1.0000000000001)).toBe(-1.0000000000001) + expect(cellValueExporter.exportValue(1.000000000000001)).toBe(1) + expect(cellValueExporter.exportValue(-1.000000000000001)).toBe(-1) + expect(cellValueExporter.exportValue(0.0000000000001)).toBe(0.0000000000001) + expect(cellValueExporter.exportValue(-0.0000000000001)).toBe(-0.0000000000001) + expect(cellValueExporter.exportValue(true)).toBe(true) + expect(cellValueExporter.exportValue(false)).toBe(false) + expect(cellValueExporter.exportValue(1)).toBe(1) + expect(cellValueExporter.exportValue(EmptyValue)).toBe(EmptyValue) + expect(cellValueExporter.exportValue('abcd')).toBe('abcd') }) }) describe('detailed error', () => { it('should return detailed errors', () => { const config = new Config({ language: enGB }) - const cellValueExporter = new CellValueExporter(config) + const cellValueExporter = new Exporter(config, namedExpressionsMock) - const error = cellValueExporter.export(new CellError(ErrorType.VALUE)) as DetailedCellError + const error = cellValueExporter.exportValue(new CellError(ErrorType.VALUE)) as DetailedCellError expect(error).toEqual(detailedError(ErrorType.VALUE)) expect(error.value).toEqual('#VALUE!') }) it('should return detailed errors with translation', () => { const config = new Config({ language: plPL }) - const cellValueExporter = new CellValueExporter(config) + const cellValueExporter = new Exporter(config, namedExpressionsMock) - const error = cellValueExporter.export(new CellError(ErrorType.VALUE)) as DetailedCellError + const error = cellValueExporter.exportValue(new CellError(ErrorType.VALUE)) as DetailedCellError expect(error).toEqual(detailedError(ErrorType.VALUE, config)) expect(error.value).toEqual('#ARG!') }) diff --git a/test/cruds/adding-columns.spec.ts b/test/cruds/adding-columns.spec.ts index adfcdab9b8..cef08a9f5e 100644 --- a/test/cruds/adding-columns.spec.ts +++ b/test/cruds/adding-columns.spec.ts @@ -1,4 +1,4 @@ -import {Config, EmptyValue, HyperFormula} from '../../src' +import {Config, EmptyValue, HyperFormula, ExportedCellChange} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import { simpleCellAddress} from '../../src/Cell' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' @@ -183,7 +183,7 @@ describe('Adding column - reevaluation', () => { const changes = engine.addColumns(0, [1, 1]) expect(changes.length).toBe(1) - expect(changes).toContainEqual({ sheet: 0, col: 3, row: 0, value: 3 }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 3, 0), 3)) }) }) diff --git a/test/cruds/adding-row.spec.ts b/test/cruds/adding-row.spec.ts index dcadf11b93..63132d49f0 100644 --- a/test/cruds/adding-row.spec.ts +++ b/test/cruds/adding-row.spec.ts @@ -1,4 +1,4 @@ -import {Config, EmptyValue, HyperFormula} from '../../src' +import {Config, EmptyValue, HyperFormula, ExportedCellChange} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {simpleCellAddress} from '../../src/Cell' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' @@ -193,7 +193,7 @@ describe('Adding row - reevaluation', () => { const changes = engine.addRows(0, [1, 1]) expect(changes.length).toBe(1) - expect(changes).toContainEqual({ sheet: 0, col: 1, row: 2, value: 1 }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 1, 2), 1)) }) }) diff --git a/test/cruds/change-cell-content.spec.ts b/test/cruds/change-cell-content.spec.ts index 02c6b22f87..e87274e941 100644 --- a/test/cruds/change-cell-content.spec.ts +++ b/test/cruds/change-cell-content.spec.ts @@ -1,4 +1,4 @@ -import {Config, EmptyValue, HyperFormula, InvalidAddressError, NoSheetWithIdError} from '../../src' +import {Config, EmptyValue, HyperFormula, InvalidAddressError, NoSheetWithIdError, ExportedCellChange} from '../../src' import {ErrorType, simpleCellAddress} from '../../src/Cell' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' import {EmptyCellVertex, MatrixVertex} from '../../src/DependencyGraph' @@ -430,12 +430,7 @@ describe('changing cell content', () => { const changes = engine.setCellContents(adr('A1'), '2') expect(changes.length).toBe(1) - expect(changes).toContainEqual({ - sheet: 0, - col: 0, - row: 0, - value: 2, - }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 0, 0), 2)) }) it('returns dependent formula value change', () => { @@ -448,18 +443,8 @@ describe('changing cell content', () => { const changes = engine.setCellContents(adr('A1'), '2') expect(changes.length).toBe(2) - expect(changes).toContainEqual({ - sheet: 0, - col: 0, - row: 0, - value: 2, - }) - expect(changes).toContainEqual({ - sheet: 0, - col: 1, - row: 0, - value: 2, - }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 0, 0), 2)) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 1, 0), 2)) }) it('returns dependent matrix value changes', () => { @@ -473,7 +458,7 @@ describe('changing cell content', () => { const changes = engine.setCellContents(adr('A1'), '2') expect(changes.length).toBe(5) - expect(changes.map((change) => change.value)).toEqual(expect.arrayContaining([2, 10, 12, 18, 22])) + expect(changes.map((change) => change.newValue)).toEqual(expect.arrayContaining([2, 10, 12, 18, 22])) }) it('returns change of numeric matrix', () => { @@ -486,13 +471,7 @@ describe('changing cell content', () => { const changes = engine.setCellContents(adr('A1'), '7') expect(changes.length).toBe(1) - expect(changes).toContainEqual({ - sheet: 0, - row: 0, - col: 0, - value: 7, - }) - + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 0, 0), 7 )) }) }) @@ -567,7 +546,7 @@ describe('change multiple cells contents', () => { const changes = engine.setCellContents(adr('A1'), [['7', '8'], ['9', '10']]) expect(changes.length).toEqual(4) - expect(changes.map((change) => change.value)).toEqual(expect.arrayContaining([7, 8, 9, 10])) + expect(changes.map((change) => change.newValue)).toEqual(expect.arrayContaining([7, 8, 9, 10])) }) it('returns changes of mutliple values dependent formulas', () => { @@ -582,7 +561,7 @@ describe('change multiple cells contents', () => { const changes = engine.setCellContents(adr('A1'), [['7', '8'], ['9', '10']]) expect(changes.length).toEqual(6) - expect(changes.map((change) => change.value)).toEqual(expect.arrayContaining([7, 8, 9, 10, 15, 18])) + expect(changes.map((change) => change.newValue)).toEqual(expect.arrayContaining([7, 8, 9, 10, 15, 18])) }) }) diff --git a/test/cruds/copy-paste.spec.ts b/test/cruds/copy-paste.spec.ts index 35a4dfc9a6..e31b887487 100644 --- a/test/cruds/copy-paste.spec.ts +++ b/test/cruds/copy-paste.spec.ts @@ -1,5 +1,5 @@ -import {Config, EmptyValue, HyperFormula} from '../../src' -import {ErrorType} from '../../src/Cell' +import {Config, EmptyValue, HyperFormula, ExportedCellChange} from '../../src' +import {ErrorType, simpleCellAddress} from '../../src/Cell' import {CellAddress} from '../../src/parser' import {CellReferenceType} from '../../src/parser/CellAddress' import '../testConfig' @@ -36,7 +36,7 @@ describe('Copy - paste integration', () => { engine.copy(adr('A1'), 1, 1) const changes = engine.paste(adr('A2')) - expectArrayWithSameContent([{ sheet:0, col: 0, row: 1, value: EmptyValue}], changes) + expectArrayWithSameContent([new ExportedCellChange(simpleCellAddress(0, 0, 1), EmptyValue)], changes) }) it('should work for single number', () => { @@ -161,8 +161,8 @@ describe('Copy - paste integration', () => { const changes = engine.paste(adr('A2')) expectArrayWithSameContent([ - { sheet:0, col: 0, row: 1, value: 1}, - { sheet:0, col: 1, row: 1, value: 1}, + new ExportedCellChange(simpleCellAddress(0, 0, 1), 1), + new ExportedCellChange(simpleCellAddress(0, 1, 1), 1), ], changes) }) diff --git a/test/cruds/move-columns.spec.ts b/test/cruds/move-columns.spec.ts index 3e27cad1cc..c04613bccf 100644 --- a/test/cruds/move-columns.spec.ts +++ b/test/cruds/move-columns.spec.ts @@ -1,5 +1,5 @@ -import {EmptyValue, HyperFormula} from '../../src' -import {ErrorType} from '../../src/Cell' +import {EmptyValue, HyperFormula, ExportedCellChange} from '../../src' +import {ErrorType, simpleCellAddress} from '../../src/Cell' import {InvalidArgumentsError} from '../../src' import {CellAddress} from '../../src/parser' import {adr, detailedError, extractRange, extractReference} from '../testUtils' @@ -184,7 +184,7 @@ describe('Move columns', () => { const changes = engine.moveColumns(0, 1, 1, 3) expect(changes.length).toEqual(1) - expect(changes).toContainEqual({ sheet: 0, col: 2, row: 1, value: 1 }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress( 0, 2, 1), 1 )) }) it('should return #CYCLE when moving formula onto referred range', () => { diff --git a/test/cruds/move-rows.spec.ts b/test/cruds/move-rows.spec.ts index ceb18ee268..fb6b4d0191 100644 --- a/test/cruds/move-rows.spec.ts +++ b/test/cruds/move-rows.spec.ts @@ -1,5 +1,5 @@ -import {EmptyValue, HyperFormula} from '../../src' -import {ErrorType} from '../../src/Cell' +import {EmptyValue, HyperFormula, ExportedCellChange} from '../../src' +import {ErrorType, simpleCellAddress} from '../../src/Cell' import {InvalidArgumentsError} from '../../src' import {CellAddress} from '../../src/parser' import {adr, detailedError, extractRange, extractReference} from '../testUtils' @@ -203,7 +203,7 @@ describe('Move rows', () => { const changes = engine.moveRows(0, 1, 1, 3) expect(changes.length).toEqual(1) - expect(changes).toContainEqual({ sheet: 0, col: 1, row: 2, value: 1 }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress( 0, 1, 2), 1 )) }) it('should return #CYCLE when moving formula onto referred range', () => { diff --git a/test/cruds/removing-columns.spec.ts b/test/cruds/removing-columns.spec.ts index 2dc9477208..d714801ec5 100644 --- a/test/cruds/removing-columns.spec.ts +++ b/test/cruds/removing-columns.spec.ts @@ -1,8 +1,9 @@ -import {Config, HyperFormula} from '../../src' +import {Config, HyperFormula, ExportedCellChange} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' import {MatrixVertex, RangeVertex} from '../../src/DependencyGraph' import {CellAddress} from '../../src/parser' +import {simpleCellAddress} from '../../src/Cell' import '../testConfig' import { adr, @@ -492,7 +493,7 @@ describe('Removing columns - reevaluation', () => { const changes = engine.removeColumns(0, [0, 1]) expect(changes.length).toBe(1) - expect(changes).toContainEqual({ sheet: 0, row: 0, col: 1, value: 2}) + expect(changes).toEqual([new ExportedCellChange(simpleCellAddress(0, 1, 0), 2)]) }) }) diff --git a/test/cruds/removing-rows.spec.ts b/test/cruds/removing-rows.spec.ts index 583cc386f4..b8853d180b 100644 --- a/test/cruds/removing-rows.spec.ts +++ b/test/cruds/removing-rows.spec.ts @@ -1,4 +1,5 @@ -import {Config, HyperFormula} from '../../src' +import {Config, HyperFormula, ExportedCellChange} from '../../src' +import {simpleCellAddress} from '../../src/Cell' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' import {MatrixVertex} from '../../src/DependencyGraph' @@ -824,7 +825,7 @@ describe('Removing rows - sheet dimensions', () => { const changes = engine.removeRows(0, [0, 1]) expect(changes.length).toBe(1) - expect(changes).toContainEqual({ sheet: 0, row: 1, col: 0, value: 2}) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 0, 1), 2)) }) }) diff --git a/test/cruds/removing-sheet.spec.ts b/test/cruds/removing-sheet.spec.ts index 56e59f6396..9f9c8f1752 100644 --- a/test/cruds/removing-sheet.spec.ts +++ b/test/cruds/removing-sheet.spec.ts @@ -1,6 +1,6 @@ -import {Config, HyperFormula} from '../../src' +import {Config, HyperFormula, ExportedCellChange} from '../../src' import {AbsoluteCellRange} from '../../src/AbsoluteCellRange' -import {ErrorType} from '../../src/Cell' +import {ErrorType, simpleCellAddress} from '../../src/Cell' import {ColumnIndex} from '../../src/ColumnSearch/ColumnIndex' import {MatrixVertex} from '../../src/DependencyGraph' import {NoSheetWithNameError} from '../../src' @@ -221,7 +221,7 @@ describe('remove sheet - adjust formula dependencies', () => { const changes = engine.removeSheet('Sheet2') expect(changes.length).toBe(1) - expect(changes).toContainEqual({ sheet: 0, row: 0, col: 0, value: detailedError(ErrorType.REF) }) + expect(changes).toContainEqual(new ExportedCellChange(simpleCellAddress(0, 0, 0), detailedError(ErrorType.REF))) }) }) diff --git a/test/cruds/replace-sheet-content.spec.ts b/test/cruds/replace-sheet-content.spec.ts index cb32129cea..2ca9117a8c 100644 --- a/test/cruds/replace-sheet-content.spec.ts +++ b/test/cruds/replace-sheet-content.spec.ts @@ -1,4 +1,5 @@ -import {Config, EmptyValue, HyperFormula} from '../../src' +import {Config, EmptyValue, HyperFormula, ExportedCellChange} from '../../src' +import {simpleCellAddress} from '../../src/Cell' import {adr, expectArrayWithSameContent} from '../testUtils' describe('Replace sheet content - checking if its possible', () => { @@ -52,8 +53,8 @@ describe('Replace sheet content', () => { const changes = engine.setSheetContent('Sheet1', [['3', '4']]) expectArrayWithSameContent(changes, [ - { sheet: 0, col: 0, row: 0, value: 3 }, - { sheet: 0, col: 1, row: 0, value: 4 }, + new ExportedCellChange(simpleCellAddress(0, 0, 0), 3), + new ExportedCellChange(simpleCellAddress(0, 1, 0), 4), ]) }) @@ -69,10 +70,10 @@ describe('Replace sheet content', () => { expect(changes.length).toEqual(4) expectArrayWithSameContent(changes, [ - { sheet: 0, col: 0, row: 0, value: 3 }, - { sheet: 0, col: 1, row: 0, value: 4 }, - { sheet: 0, col: 0, row: 1, value: EmptyValue }, - { sheet: 0, col: 1, row: 1, value: EmptyValue }, + new ExportedCellChange(simpleCellAddress(0, 0, 0), 3), + new ExportedCellChange(simpleCellAddress(0, 1, 0), 4), + new ExportedCellChange(simpleCellAddress(0, 0, 1), EmptyValue), + new ExportedCellChange(simpleCellAddress(0, 1, 1), EmptyValue), ]) }) diff --git a/test/external-formulas.spec.ts b/test/external-formulas.spec.ts deleted file mode 100644 index e6ad8a0f98..0000000000 --- a/test/external-formulas.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {HyperFormula} from '../src' -import './testConfig' - -describe('External formulas - normalization', () => { - it('works', () => { - const engine = HyperFormula.buildFromArray([]) - - const normalizedFormula = engine.normalizeFormula('=SHEET1!A1+10') - - expect(normalizedFormula).toEqual('=Sheet1!A1+10') - }) -}) - -describe('External formulas - validation', () => { - it('ok for formulas', () => { - const engine = HyperFormula.buildFromArray([]) - - const formula = '=Sheet1!A1+10' - - expect(engine.validateFormula(formula)).toBe(true) - }) - - it('fail for simple values', () => { - const engine = HyperFormula.buildFromArray([]) - - expect(engine.validateFormula('42')).toBe(false) - expect(engine.validateFormula('some text')).toBe(false) - }) - - it('fail when not a formula', () => { - const engine = HyperFormula.buildFromArray([]) - - expect(engine.validateFormula('=SOME SYNTAX ERROR')).toBe(false) - }) - - it('ok when literal error', () => { - const engine = HyperFormula.buildFromArray([]) - - expect(engine.validateFormula('=#N/A')).toBe(true) - }) -}) diff --git a/test/interpreter/matrix-plugin.spec.ts b/test/interpreter/matrix-plugin.spec.ts index ffd073caec..62810f1252 100644 --- a/test/interpreter/matrix-plugin.spec.ts +++ b/test/interpreter/matrix-plugin.spec.ts @@ -218,4 +218,17 @@ describe('Function TRANSPOSE', () => { expect(engine.getCellValue(adr('A1'))).toEqual(detailedError(ErrorType.VALUE)) }) + + it('transpose without braces', () => { + const engine = HyperFormula.buildFromArray([ + ['1', '2'], + ['3', '4'], + ['5', '6'], + ['=TRANSPOSE(A1:B3)'], + ], configWithMatrixPlugin) + + expect(engine.getCellValue(adr('A4'))).toEqual(detailedError(ErrorType.VALUE)) + expect(engine.getCellValue(adr('A5'))).toEqual(EmptyValue) + expect(engine.getCellValue(adr('B4'))).toEqual(EmptyValue) + }) }) diff --git a/test/named-expressions.spec.ts b/test/named-expressions.spec.ts index 82150623d4..80cdaeb231 100644 --- a/test/named-expressions.spec.ts +++ b/test/named-expressions.spec.ts @@ -1,6 +1,7 @@ -import {HyperFormula} from '../src' +import {HyperFormula, ExportedNamedExpressionChange, EmptyValue} from '../src' import './testConfig' -import {adr} from './testUtils' +import {adr, detailedError} from './testUtils' +import {ErrorType} from '../src/Cell' describe('Named expressions', () => { it('basic usage', () => { @@ -13,14 +14,70 @@ describe('Named expressions', () => { expect(engine.getNamedExpressionValue('myName')).toEqual(52) }) + it('using string expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', 'foobarbaz') + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', 'foobarbaz')]) + expect(engine.getNamedExpressionValue('myName')).toEqual('foobarbaz') + }) + + it('using number expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', '42') + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', 42)]) + expect(engine.getNamedExpressionValue('myName')).toEqual(42) + }) + + it('using empty expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', null) + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', EmptyValue)]) + expect(engine.getNamedExpressionValue('myName')).toEqual(EmptyValue) + }) + + it('using native number as expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', 42) + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', 42)]) + expect(engine.getNamedExpressionValue('myName')).toEqual(42) + }) + + it('using native boolean as expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', true) + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', true)]) + expect(engine.getNamedExpressionValue('myName')).toEqual(true) + }) + + it('using error expression', () => { + const engine = HyperFormula.buildEmpty() + + const changes = engine.addNamedExpression('myName', '#VALUE!') + + expect(changes).toEqual([new ExportedNamedExpressionChange('myName', detailedError(ErrorType.VALUE))]) + expect(engine.getNamedExpressionValue('myName')).toEqual(detailedError(ErrorType.VALUE)) + }) + it('is recomputed', () => { const engine = HyperFormula.buildFromArray([ ['42'], ]) engine.addNamedExpression('myName', '=Sheet1!A1+10') - engine.setCellContents(adr('A1'), '20') + const changes = engine.setCellContents(adr('A1'), '20') + expect(changes.length).toBe(2) + expect(changes).toContainEqual(new ExportedNamedExpressionChange('myName', 30)) expect(engine.getNamedExpressionValue('myName')).toEqual(30) }) @@ -29,11 +86,11 @@ describe('Named expressions', () => { ['42'], ]) - engine.addNamedExpression('myName1', '=Sheet1!A1+10') - engine.addNamedExpression('myName2', '=Sheet1!A1+11') + engine.addNamedExpression('myName.1', '=Sheet1!A1+10') + engine.addNamedExpression('myName.2', '=Sheet1!A1+11') - expect(engine.getNamedExpressionValue('myName1')).toEqual(52) - expect(engine.getNamedExpressionValue('myName2')).toEqual(53) + expect(engine.getNamedExpressionValue('myName.1')).toEqual(52) + expect(engine.getNamedExpressionValue('myName.2')).toEqual(53) }) it('adding the same named expression twice is forbidden', () => { @@ -53,6 +110,14 @@ describe('Named expressions', () => { }).toThrowError("Name of Named Expression '1definitelyIncorrectName' is invalid") }) + it('when adding named expression, matrix formulas are not accepted', () => { + const engine = HyperFormula.buildEmpty() + + expect(() => { + engine.addNamedExpression('myName', '{=TRANSPOSE(A1:B2)}') + }).toThrowError(/Matrix formulas are not supported/) + }) + it('retrieving non-existing named expression', () => { const engine = HyperFormula.buildEmpty() @@ -77,29 +142,50 @@ describe('Named expressions', () => { ]) engine.addNamedExpression('myName', '=Sheet1!A1+10') - engine.changeNamedExpressionFormula('myName', '=Sheet1!A1+11') + engine.changeNamedExpressionExpression('myName', '=Sheet1!A1+11') expect(engine.getNamedExpressionValue('myName')).toEqual(53) }) + it('is possible to change named expression formula to other expression', () => { + const engine = HyperFormula.buildFromArray([ + ['42'], + ]) + engine.addNamedExpression('myName', '=Sheet1!A1+10') + + engine.changeNamedExpressionExpression('myName', 58) + + expect(engine.getNamedExpressionValue('myName')).toEqual(58) + }) + + it('when changing named expression, only formulas are accepted', () => { + const engine = HyperFormula.buildEmpty() + + engine.addNamedExpression('myName', '=42') + + expect(() => { + engine.changeNamedExpressionExpression('myName', '{=TRANSPOSE(A1:B2)}') + }).toThrowError(/not supported/) + }) + it('changing not existing named expression', () => { const engine = HyperFormula.buildEmpty() expect(() => { - engine.changeNamedExpressionFormula('myName', '=42') + engine.changeNamedExpressionExpression('myName', '=42') }).toThrowError("Named Expression 'myName' does not exist") }) it('listing named expressions', () => { const engine = HyperFormula.buildEmpty() - engine.addNamedExpression('myName1', '=42') - engine.addNamedExpression('myName2', '=42') + engine.addNamedExpression('myName.1', '=42') + engine.addNamedExpression('myName.2', '=42') const namedExpressions = engine.listNamedExpressions() expect(namedExpressions).toEqual([ - 'myName1', - 'myName2', + 'myName.1', + 'myName.2', ]) }) @@ -110,10 +196,37 @@ describe('Named expressions', () => { expect(engine.getNamedExpressionValue('MYname')).toEqual(42) expect(() => { - engine.changeNamedExpressionFormula('MYname', '=43') + engine.changeNamedExpressionExpression('MYname', '=43') }).not.toThrow() expect(() => { engine.removeNamedExpression('MYname') }).not.toThrow() }) + + it('allow even 255 character named expressions', () => { + const engine = HyperFormula.buildEmpty() + + const longExpressionName = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + expect(longExpressionName.length).toBe(255) + expect(() => { + engine.addNamedExpression(longExpressionName, '=42') + }).not.toThrow() + }) + + it('validates characters which are allowed in name', () => { + const engine = HyperFormula.buildEmpty() + + expect(() => engine.addNamedExpression('1CantStartWithNumber', '=42')).toThrowError(/Name .* is invalid/) + expect(() => engine.addNamedExpression('Spaces Are Not Allowed', '=42')).toThrowError(/Name .* is invalid/) + expect(() => engine.addNamedExpression('.CantStartWithDot', '=42')).toThrowError(/Name .* is invalid/) + expect(() => engine.addNamedExpression('_CanStartWithUnderscore', '=42')).not.toThrowError() + expect(() => engine.addNamedExpression('dots.are.fine', '=42')).not.toThrowError() + expect(() => engine.addNamedExpression('underscores_are_fine', '=42')).not.toThrowError() + expect(() => engine.addNamedExpression('ś.zażółć.gęślą.jaźń.unicode.is.fine', '=42')).not.toThrowError() + expect(() => engine.addNamedExpression('If.It.Only.Has.Something.Like.Reference.Not.In.Beginning.Then.Its.Ok.A100', '=42')).not.toThrowError() + expect(() => engine.addNamedExpression('A100', '=42')).toThrowError(/Name .* is invalid/) + expect(() => engine.addNamedExpression('$A$50', '=42')).toThrowError(/Name .* is invalid/) + expect(() => engine.addNamedExpression('SheetName!$A$50', '=42')).toThrowError(/Name .* is invalid/) + }) }) diff --git a/test/temporary-formulas.spec.ts b/test/temporary-formulas.spec.ts new file mode 100644 index 0000000000..b4b0ded00e --- /dev/null +++ b/test/temporary-formulas.spec.ts @@ -0,0 +1,98 @@ +import {HyperFormula} from '../src' +import {ErrorType} from '../src/Cell' +import './testConfig' +import {detailedError} from './testUtils' + +describe('Temporary formulas - normalization', () => { + it('works', () => { + const engine = HyperFormula.buildFromArray([]) + + const normalizedFormula = engine.normalizeFormula('=SHEET1!A1+10') + + expect(normalizedFormula).toEqual('=Sheet1!A1+10') + }) +}) + +describe('Temporary formulas - validation', () => { + it('ok for formulas', () => { + const engine = HyperFormula.buildFromArray([]) + + const formula = '=Sheet1!A1+10' + + expect(engine.validateFormula(formula)).toBe(true) + }) + + it('fail for simple values', () => { + const engine = HyperFormula.buildFromArray([]) + + expect(engine.validateFormula('42')).toBe(false) + expect(engine.validateFormula('some text')).toBe(false) + }) + + it('fail when not a formula', () => { + const engine = HyperFormula.buildFromArray([]) + + expect(engine.validateFormula('=SOME SYNTAX ERROR')).toBe(false) + }) + + it('ok when literal error', () => { + const engine = HyperFormula.buildFromArray([]) + + expect(engine.validateFormula('=#N/A')).toBe(true) + }) +}) + +describe('Temporary formulas - calculation', () => { + it('basic usage', () => { + const engine = HyperFormula.buildFromArray([ + ['42'], + ]) + + const result = engine.calculateFormula('=Sheet1!A1+10', 'Sheet1') + + expect(result).toEqual(52) + }) + + it('formulas are executed in context of given sheet', () => { + const engine = HyperFormula.buildFromSheets({ + Sheet1: [['42']], + Sheet2: [['58']], + }) + + expect(engine.calculateFormula('=A1+10', 'Sheet1')).toEqual(52) + expect(engine.calculateFormula('=A1+10', 'Sheet2')).toEqual(68) + }) + + it('when sheet name does not exist', () => { + const engine = HyperFormula.buildFromArray([ + ['42'], + ]) + + expect(() => { + engine.calculateFormula('=Sheet1!A1+10', 'NotExistingSheet') + }).toThrowError(/no sheet with name/) + }) + + it('non-scalars doesnt work', () => { + const engine = HyperFormula.buildFromArray([ + ['1', '2'], + ['1', '2'], + ]) + + const result = engine.calculateFormula('=TRANSPOSE(A1:B2)', 'Sheet1') + + expect(result).toEqual(detailedError(ErrorType.VALUE)) + }) + + it('passing something which is not a formula doesnt work', () => { + const engine = HyperFormula.buildFromArray([]) + + expect(() => { + engine.calculateFormula('{=TRANSPOSE(A1:B2)}', 'Sheet1') + }).toThrowError(/not a formula/) + + expect(() => { + engine.calculateFormula('42', 'Sheet1') + }).toThrowError(/not a formula/) + }) +})