Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scoped Named Expressions bugfix #647

Merged
merged 7 commits into from
Apr 26, 2021
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Added two new fired events, for suspending and resuming execution. (#637)
- Added listing in scopes to `listNamedExpressions` method. (#638)

### Fixed
- Fixed issues with scoped named expression. (#646, #641)

### Fixed
- Fixed an issue with losing formating info about DateTime numbers. (#626)
Expand Down
6 changes: 3 additions & 3 deletions src/CrudOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ export class CrudOperations {
const sheetId = this.sheetMapping.fetch(sheetName)
const originalName = this.sheetMapping.fetchDisplayName(sheetId)
const oldSheetContent = this.operations.getSheetClipboardCells(sheetId)
const version = this.operations.removeSheet(sheetName)
this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, version))
const {version, scopedNamedExpressions} = this.operations.removeSheet(sheetName)
this.undoRedo.saveOperation(new RemoveSheetUndoEntry(originalName, sheetId, oldSheetContent, scopedNamedExpressions, version))
}

public renameSheet(sheetId: number, newName: string): Maybe<string> {
Expand Down Expand Up @@ -658,7 +658,7 @@ export class CrudOperations {
}
}

public scopeId(sheetName: string | undefined): number | undefined {
public scopeId(sheetName?: string): Maybe<number> {
if (sheetName !== undefined) {
this.ensureSheetExists(sheetName)
return this.sheetMapping.fetch(sheetName)
Expand Down
20 changes: 15 additions & 5 deletions src/HyperFormula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3407,7 +3407,7 @@ export class HyperFormula implements TypedEmitter {
* Note that this method may trigger dependency graph recalculation.
*
* @param {string} expressionName - expression name, case insensitive.
* @param {string?} sheetScope - scope definition, `sheetName` for local scope or `undefined` for global scope
* @param {string?} scope - scope definition, `sheetName` for local scope or `undefined` for global scope
*
* @fires [[namedExpressionRemoved]] after the expression was removed
* @fires [[valuesUpdated]] if recalculation was triggered by this change
Expand Down Expand Up @@ -3443,27 +3443,37 @@ export class HyperFormula implements TypedEmitter {

/**
* Lists all named expressions.
* Returns an array of expression names as strings
* Returns an array of expression names defined in a scope, as strings.
*
* @param {string?} scope - scope definition, `sheetName` for local scope or `undefined` for global scope
*
* @throws [[NoSheetWithNameError]] when the given sheet name does not exists
*
* @example
* ```js
* const hfInstance = HyperFormula.buildFromArray([
* ['42'],
* ['50'],
* ['60'],
* ]);
*
* // add two named expressions
* // add two named expressions and one scoped
* hfInstance.addNamedExpression('prettyName', '=Sheet1!A1+100');
* hfInstance.addNamedExpression('prettyName2', '=Sheet1!A2+100');
* hfInstance.addNamedExpression('prettyName3', '=Sheet1!A3+100', 'Sheet1');
*
* // list the expressions, should return: ['prettyName', 'prettyName2'] for this example
* const listOfExpressions = hfInstance.listNamedExpressions();
*
* // list the expressions, should return: ['prettyName3'] for this example
* const listOfExpressions = hfInstance.listNamedExpressions('Sheet1');
* ```
*
* @category Named Expressions
*/
public listNamedExpressions(): string[] {
return this._namedExpressions.getAllNamedExpressionsNames()
public listNamedExpressions(scope?: string): string[] {
const scopeId = this._crudOperations.scopeId(scope)
return this._namedExpressions.getAllNamedExpressionsNamesInScope(scopeId)
}

/**
Expand Down
84 changes: 47 additions & 37 deletions src/NamedExpressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class NamedExpressions {
if (sheetId === undefined) {
return this.workbookStore.isNameAvailable(expressionName)
} else {
return this.worksheetStore(sheetId).isNameAvailable(expressionName)
return this.worksheetStore(sheetId)?.isNameAvailable(expressionName) ?? true
}
}

Expand All @@ -149,16 +149,16 @@ export class NamedExpressions {
if (sheetId === undefined) {
return this.workbookStore.getExisting(expressionName)
} else {
return this.worksheetStore(sheetId).get(expressionName)
return this.worksheetStore(sheetId)?.get(expressionName)
}
}

public nearestNamedExpression(expressionName: string, sheetId: number): Maybe<InternalNamedExpression> {
return this.worksheetStore(sheetId).get(expressionName) ?? this.workbookStore.getExisting(expressionName)
return this.worksheetStore(sheetId)?.get(expressionName) ?? this.workbookStore.getExisting(expressionName)
}

public isExpressionInScope(expressionName: string, sheetId: number): boolean {
return this.worksheetStore(sheetId).has(expressionName)
return this.worksheetStore(sheetId)?.has(expressionName) ?? false
}

public isNameValid(expressionName: string): boolean {
Expand All @@ -169,28 +169,29 @@ export class NamedExpressions {
}

public addNamedExpression(expressionName: string, sheetId?: number, options?: NamedExpressionOptions): InternalNamedExpression {
if (sheetId === undefined) {
let namedExpression = this.workbookStore.get(expressionName)
if (namedExpression) {
namedExpression.added = true
namedExpression.displayName = expressionName
namedExpression.options = options
} else {
namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), true, options)
this.workbookStore.add(namedExpression)
}
this.addressCache.set(namedExpression.address.row, namedExpression)
return namedExpression
const store = sheetId === undefined ? this.workbookStore : this.worksheetStoreOrCreate(sheetId)
let namedExpression = store.get(expressionName)
if (namedExpression) {
namedExpression.added = true
namedExpression.displayName = expressionName
namedExpression.options = options
} else {
const store = this.worksheetStore(sheetId)
const namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), true, options)
namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), true, options)
store.add(namedExpression)
this.addressCache.set(namedExpression.address.row, namedExpression)
return namedExpression
}
this.addressCache.set(namedExpression.address.row, namedExpression)
return namedExpression
}

private worksheetStore(sheetId: number): WorksheetStore {
public restoreNamedExpression(namedExpression: InternalNamedExpression, sheetId?: number): InternalNamedExpression {
const store = sheetId === undefined ? this.workbookStore : this.worksheetStoreOrCreate(sheetId)
namedExpression.added = true
store.add(namedExpression)
this.addressCache.set(namedExpression.address.row, namedExpression)
return namedExpression
}

private worksheetStoreOrCreate(sheetId: number): WorksheetStore {
let store = this.worksheetStores.get(sheetId)
if (!store) {
store = new WorksheetStore()
Expand All @@ -199,18 +200,12 @@ export class NamedExpressions {
return store
}

private worksheetStore(sheetId: number): Maybe<WorksheetStore> {
return this.worksheetStores.get(sheetId)
}

public namedExpressionOrPlaceholder(expressionName: string, sheetId: number): InternalNamedExpression {
let namedExpression = this.worksheetStore(sheetId).get(expressionName)
if (namedExpression) {
return namedExpression
} else {
namedExpression = this.workbookStore.get(expressionName)
if (namedExpression === undefined) {
namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), false)
this.workbookStore.add(namedExpression)
}
return namedExpression
}
return this.worksheetStoreOrCreate(sheetId).get(expressionName) ?? this.workbookNamedExpressionOrPlaceholder(expressionName)
}

public workbookNamedExpressionOrPlaceholder(expressionName: string): InternalNamedExpression {
Expand All @@ -229,20 +224,27 @@ export class NamedExpressions {
} else {
store = this.worksheetStore(sheetId)
}
const namedExpression = store.get(expressionName)
if (namedExpression === undefined || !namedExpression.added) {
const namedExpression = store?.get(expressionName)
if (store === undefined || namedExpression === undefined || !namedExpression.added) {
throw 'Named expression does not exist'
}
store.remove(expressionName)
if(store instanceof WorksheetStore && store.mapping.size === 0) {
this.worksheetStores.delete(sheetId!)
}
this.addressCache.delete(namedExpression.address.row)
}

public getAllNamedExpressionsNamesInScope(sheetId?: number): string[] {
return this.getAllNamedExpressions().filter(({scope}) => scope===sheetId).map((ne) => ne.expression.displayName)
}

public getAllNamedExpressionsNames(): string[] {
return this.workbookStore.getAllNamedExpressions().map((ne) => ne.displayName)
return this.getAllNamedExpressions().map((ne) => ne.expression.displayName)
}

public getAllNamedExpressions(): { expression: InternalNamedExpression, scope: Maybe<number> }[] {
const storedNamedExpressions: { expression: InternalNamedExpression, scope: Maybe<number> }[] = []
public getAllNamedExpressions(): { expression: InternalNamedExpression, scope?: number }[] {
const storedNamedExpressions: { expression: InternalNamedExpression, scope?: number }[] = []

this.workbookStore.getAllNamedExpressions().forEach(expr => {
storedNamedExpressions.push({
Expand All @@ -263,6 +265,14 @@ export class NamedExpressions {
return storedNamedExpressions
}

public getAllNamedExpressionsForScope(scope?: number): InternalNamedExpression[] {
if(scope === undefined) {
return this.workbookStore.getAllNamedExpressions()
} else {
return this.worksheetStores.get(scope)?.getAllNamedExpressions() ?? []
}
}

private nextAddress() {
return simpleCellAddress(NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS, 0, this.nextNamedExpressionRow++)
}
Expand Down
8 changes: 5 additions & 3 deletions src/Operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ export class Operations {

this.sheetMapping.removeSheet(sheetId)
this.columnSearch.removeSheet(sheetId)
return version!
const scopedNamedExpressions = this.namedExpressions.getAllNamedExpressionsForScope(sheetId).map(
(namedexpression) => this.removeNamedExpression(namedexpression.normalizeExpressionName(), sheetId)
)
return {version: version!, scopedNamedExpressions}
}

public clearSheet(sheetId: number) {
Expand Down Expand Up @@ -363,9 +366,8 @@ export class Operations {

public restoreNamedExpression(namedExpression: InternalNamedExpression, content: ClipboardCell, sheetId?: number) {
const expressionName = namedExpression.displayName
const options = namedExpression.options
this.restoreCell(namedExpression.address, content)
const restoredNamedExpression = this.namedExpressions.addNamedExpression(expressionName, sheetId, options)
const restoredNamedExpression = this.namedExpressions.restoreNamedExpression(namedExpression, sheetId)
this.adjustNamedExpressionEdges(restoredNamedExpression, expressionName, sheetId)
}

Expand Down
5 changes: 5 additions & 0 deletions src/UndoRedo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export class RemoveSheetUndoEntry extends BaseUndoEntry {
public readonly sheetName: string,
public readonly sheetId: number,
public readonly oldSheetContent: ClipboardCell[][],
public readonly scopedNamedExpressions: [InternalNamedExpression, ClipboardCell][],
public readonly version: number,
) {
super()
Expand Down Expand Up @@ -596,6 +597,10 @@ export class UndoRedo {
}
}

for(const [namedexpression, content] of operation.scopedNamedExpressions) {
this.operations.restoreNamedExpression(namedexpression, content, sheetId)
}

this.restoreOldDataFromVersion(operation.version - 1)
}

Expand Down
12 changes: 12 additions & 0 deletions test/named-expressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,18 @@ describe('Named expressions - store manipulation', () => {
])
})

it('listing scoped named expressions', () => {
const engine = HyperFormula.buildFromSheets({sheet1: [], sheet2: []})
engine.addNamedExpression('myName.1', '=42', 'sheet1')
engine.addNamedExpression('myName.2', '=42', 'sheet2')

const namedExpressions = engine.listNamedExpressions('sheet2')

expect(namedExpressions).toEqual([
'myName.2',
])
})

it('adding named expressions is case insensitive', () => {
const engine = HyperFormula.buildEmpty()

Expand Down
32 changes: 32 additions & 0 deletions test/scoped-namedExpressions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {HyperFormula} from '../src'
import {adr} from './testUtils'

describe('scoped named expressions', () => {
it('should be removed when sheet is removed', () => {
const engine = HyperFormula.buildFromSheets({'Sheet1': [[]], 'Sheet2': [[]]})
engine.addNamedExpression('TRUE', true, 'Sheet1')
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
expect(engine._namedExpressions.getAllNamedExpressionsNames()).toEqual(['TRUE'])
engine.removeSheet('Sheet1')
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
expect(engine._namedExpressions.getAllNamedExpressionsNames()).toEqual([])
})

it('removal should work with undo of sheet', () => {
const engine = HyperFormula.buildFromArray([['=TRUE']])
engine.addNamedExpression('TRUE', true, 'Sheet1')
engine.removeSheet('Sheet1')
engine.undo()
expect(engine.getCellValue(adr('A1'))).toEqual(true)
})

it('removal should work with undo of named expression', () => {
const engine = HyperFormula.buildFromArray([['=TRUE']])
engine.addNamedExpression('TRUE', true, 'Sheet1')
engine.removeNamedExpression('TRUE', 'Sheet1')
engine.undo()
expect(engine.getCellValue(adr('A1'))).toEqual(true)
})
})