Skip to content

Commit a3ef886

Browse files
committed
Make sure renameSheet handles dependency graph correctly
1 parent e48dd82 commit a3ef886

File tree

9 files changed

+101
-119
lines changed

9 files changed

+101
-119
lines changed

src/DependencyGraph/AddressMapping/AddressMapping.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,7 @@ export class AddressMapping {
212212
throw Error('Sheet not initialized.')
213213
}
214214

215-
if (source.sheet !== destination.sheet) {
216-
throw Error('Cannot move cells between sheets.')
217-
}
218-
219-
if (sheetMapping.has(destination)) {
215+
if (this.has(destination)) {
220216
throw new Error('Cannot move cell. Destination already occupied.')
221217
}
222218

src/DependencyGraph/DependencyGraph.ts

Lines changed: 70 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,19 @@ export class DependencyGraph {
310310
})
311311
}
312312

313+
/**
314+
* Marks all cell vertices in the sheet as dirty.
315+
*/
313316
private markAllCellsAsDirtyInSheet(sheetId: number): void {
314317
const sheetCells = this.addressMapping.sheetEntries(sheetId)
315318
for (const [_, vertex] of sheetCells) {
316319
this.graph.markNodeAsDirty(vertex)
317320
}
318321
}
319322

323+
/**
324+
* Marks all range verticesin the sheet as dirty.
325+
*/
320326
private markAllRangesAsDirtyInSheet(sheetId: number): void {
321327
const sheetRanges = this.rangeMapping.rangesInSheet(sheetId)
322328

@@ -344,37 +350,45 @@ export class DependencyGraph {
344350
}
345351

346352
/**
347-
* TODO
353+
* TODO: implement
354+
*
355+
* sheetToKeep:
356+
* - existing,
357+
* - contains cell vertices,
358+
* - contains range vertices,
359+
* - contains array formula vertices,
360+
* - has dependencies and depentents
361+
*
362+
* sheetToDelete:
363+
* - placeholder,
364+
* - contains only empty cell vertices,
365+
* - contains range vertices,
366+
* - has dependents in other sheets,
367+
* - has dependencies only in same sheet (ranges)
348368
*/
349-
public mergeSheets(sheetToKeep: number, sheetToDelete: number): void { // TODO: refactor
350-
const cellsFromSheetToDelete = this.addressMapping.sheetEntries(sheetToDelete)
351-
const rangesFromSheetToDelete = Array.from(this.rangeMapping.rangesInSheet(sheetToDelete))
352-
353-
for (const [addressToDelete, vertexToDelete] of cellsFromSheetToDelete) {
354-
const addressToKeep = simpleCellAddress(sheetToKeep, addressToDelete.col, addressToDelete.row)
355-
const { vertex: vertexToKeep } = this.fetchCellOrCreateEmpty(addressToKeep)
356-
this.mergeVertices(vertexToKeep, vertexToDelete)
357-
this.graph.markNodeAsDirty(vertexToKeep)
358-
}
369+
public mergeSheets(sheetToKeep: number, placeholderSheetToDelete: number): void {
370+
const rangeVertices = Array.from(this.rangeMapping.rangesInSheet(placeholderSheetToDelete))
359371

360-
for (const vertexToDelete of rangesFromSheetToDelete) {
361-
if (!(vertexToDelete instanceof RangeVertex)) {
372+
for (const vertexToDelete of rangeVertices) {
373+
if (!this.graph.hasNode(vertexToDelete)) {
362374
continue
363375
}
364376

365-
const start = vertexToDelete.getStart()
366-
const end = vertexToDelete.getEnd()
377+
const start = vertexToDelete.start
378+
const end = vertexToDelete.end
367379

368-
if (start.sheet !== sheetToDelete && end.sheet !== sheetToDelete) {
380+
if (start.sheet !== placeholderSheetToDelete && end.sheet !== placeholderSheetToDelete) {
369381
continue
370382
}
371383

372384
const targetStart = simpleCellAddress(sheetToKeep, start.col, start.row)
373385
const targetEnd = simpleCellAddress(sheetToKeep, end.col, end.row)
374386
const vertexToKeep = this.rangeMapping.getRangeVertex(targetStart, targetEnd)
375387

376-
if (vertexToKeep !== undefined) {
377-
this.mergeVertices(vertexToKeep, vertexToDelete)
388+
if (vertexToKeep) {
389+
this.rerouteDependents(vertexToDelete, vertexToKeep)
390+
this.removeVertexAndRerouteDependencies(vertexToDelete, vertexToKeep)
391+
this.rangeMapping.removeVertexIfExists(vertexToDelete)
378392
this.graph.markNodeAsDirty(vertexToKeep)
379393
} else {
380394
this.rangeMapping.removeVertexIfExists(vertexToDelete)
@@ -384,10 +398,24 @@ export class DependencyGraph {
384398
}
385399
}
386400

387-
this.addressMapping.removeSheet(sheetToDelete)
401+
const cellVertices = Array.from(this.addressMapping.sheetEntries(placeholderSheetToDelete))
388402

389-
// TODO: adjust arrayMapping
403+
for (const [addressToDelete, vertexToDelete] of cellVertices) {
404+
const addressToKeep = simpleCellAddress(sheetToKeep, addressToDelete.col, addressToDelete.row)
405+
const vertexToKeep = this.getCell(addressToKeep)
390406

407+
if (vertexToKeep) {
408+
this.rerouteDependents(vertexToDelete, vertexToKeep)
409+
this.removeVertexAndCleanupDependencies(vertexToDelete)
410+
this.addressMapping.removeCell(addressToDelete)
411+
this.graph.markNodeAsDirty(vertexToKeep)
412+
} else {
413+
this.addressMapping.moveCell(addressToDelete, addressToKeep)
414+
this.graph.markNodeAsDirty(vertexToDelete)
415+
}
416+
}
417+
418+
this.addressMapping.removeSheet(placeholderSheetToDelete)
391419
this.addStructuralNodesToChangeSet()
392420
}
393421

@@ -572,7 +600,7 @@ export class DependencyGraph {
572600
}
573601

574602
/**
575-
* Sets an array formula vertex to empty.
603+
* Sets an array empty..
576604
* - removes all corresponding entries from address mapping
577605
* - reroutes the edges
578606
* - removes vertex from graph and cleans up its dependencies
@@ -959,12 +987,14 @@ export class DependencyGraph {
959987
const allDeps: [(SimpleCellAddress | AbsoluteCellRange), Vertex][] = []
960988
const {smallerRangeVertex, restRange} = this.rangeMapping.findSmallerRange((vertex as RangeVertex).range) //checking whether this range was splitted by bruteForce or not
961989
let range
990+
962991
if (smallerRangeVertex !== undefined && this.graph.adjacentNodes(smallerRangeVertex).has(vertex)) {
963992
range = restRange
964993
allDeps.push([new AbsoluteCellRange(smallerRangeVertex.start, smallerRangeVertex.end), smallerRangeVertex])
965994
} else { //did we ever need to use full range
966995
range = (vertex as RangeVertex).range
967996
}
997+
968998
for (const address of range.addresses(this)) {
969999
const cell = this.addressMapping.getCell(address)
9701000
if (cell !== undefined) {
@@ -1106,7 +1136,8 @@ export class DependencyGraph {
11061136
verticesWithChangedSize
11071137
} = this.rangeMapping.truncateRanges(span, coordinate)
11081138
for (const [existingVertex, mergedVertex] of verticesToMerge) {
1109-
this.mergeVertices(existingVertex, mergedVertex)
1139+
this.rerouteDependents(mergedVertex, existingVertex)
1140+
this.removeVertexAndCleanupDependencies(mergedVertex)
11101141
}
11111142
for (const rangeVertex of verticesToRemove) {
11121143
this.removeVertexAndCleanupDependencies(rangeVertex)
@@ -1211,65 +1242,31 @@ export class DependencyGraph {
12111242
}
12121243

12131244
/**
1214-
* - If vertex has dependents in other sheets, changes it to EmptyCellVertex and returns false
1215-
* - Otherwise, removes it from the graph, cleans up its dependencies and returns true
1216-
*
1217-
* @returns True if the vertex was removed, false if it was changed to EmptyCellVertex.
1245+
* Reroutes dependent vertices of source to target. Also removes the edge target -> source if it exists.
12181246
*/
1219-
private removeVertexIfNoDependentsInOtherSheets(inputVertex: Vertex): boolean {
1220-
const dependents = this.getAdjacentNodesAddresses(inputVertex)
1247+
private rerouteDependents(source: Vertex, target: Vertex) {
1248+
const dependents = this.graph.adjacentNodes(source)
1249+
this.graph.removeEdgeIfExists(target, source)
12211250

1222-
const hasDependentInExistingSheet = dependents.some(addr => {
1223-
if (isSimpleCellAddress(addr)) {
1224-
return !this.isPlaceholder(addr.sheet)
1225-
}
1226-
1227-
if (isSimpleCellRange(addr)) {
1228-
return this.sheetMapping.hasSheetWithId(addr.start.sheet, { includePlaceholders: false }) || this.sheetMapping.hasSheetWithId(addr.end.sheet, { includePlaceholders: false })
1251+
dependents.forEach((adjacentNode) => {
1252+
if (this.graph.hasNode(adjacentNode)) {
1253+
this.graph.addEdge(target, adjacentNode)
12291254
}
1230-
1231-
return false
12321255
})
12331256

1234-
let dependencies: Set<[SimpleCellAddress | SimpleCellRange, Vertex]>
1235-
1236-
if (hasDependentInExistingSheet) {
1237-
dependencies = new Set(this.graph.removeDependencies(inputVertex))
1238-
} else {
1239-
dependencies = new Set(this.graph.removeNode(inputVertex))
1240-
}
1241-
1242-
while (dependencies.size > 0) {
1243-
const dependency = dependencies.values().next().value
1244-
dependencies.delete(dependency)
1245-
const [address, vertex] = dependency
1246-
if (this.graph.hasNode(vertex) && this.graph.adjacentNodesCount(vertex) === 0) {
1247-
if (vertex instanceof RangeVertex || vertex instanceof EmptyCellVertex) {
1248-
this.graph.removeNode(vertex).forEach((candidate) => dependencies.add(candidate))
1249-
}
1250-
if (vertex instanceof RangeVertex) {
1251-
this.rangeMapping.removeVertexIfExists(vertex)
1252-
} else if (vertex instanceof EmptyCellVertex) {
1253-
this.addressMapping.removeCell(address)
1254-
}
1255-
}
1256-
}
1257-
1258-
if (inputVertex instanceof RangeVertex) {
1259-
this.rangeMapping.removeVertexIfExists(inputVertex)
1260-
}
1261-
1262-
return !hasDependentInExistingSheet
12631257
}
12641258

1265-
private mergeVertices(vertexToKeep: Vertex, vertexToDelete: Vertex) {
1266-
const adjNodesStored = this.graph.adjacentNodes(vertexToDelete)
1259+
/**
1260+
* Removes a vertex from graph and reroutes its dependencies to other vertex. Also removes the edge vertexToKeep -> vertexToDelete if it exists.
1261+
*/
1262+
private removeVertexAndRerouteDependencies(vertexToDelete: Vertex, vertexToKeep: Vertex) {
1263+
const dependencies = this.graph.removeNode(vertexToDelete)
12671264

1268-
this.removeVertexAndCleanupDependencies(vertexToDelete)
12691265
this.graph.removeEdgeIfExists(vertexToKeep, vertexToDelete)
1270-
adjNodesStored.forEach((adjacentNode) => {
1271-
if (this.graph.hasNode(adjacentNode)) {
1272-
this.graph.addEdge(vertexToKeep, adjacentNode)
1266+
1267+
dependencies.forEach(([_, dependency]) => {
1268+
if (this.graph.hasNode(dependency)) {
1269+
this.graph.addEdge(dependency, vertexToKeep)
12731270
}
12741271
})
12751272
}
@@ -1280,6 +1277,7 @@ export class DependencyGraph {
12801277
*/
12811278
private removeVertexAndCleanupDependencies(inputVertex: Vertex) {
12821279
const dependencies = new Set(this.graph.removeNode(inputVertex))
1280+
12831281
while (dependencies.size > 0) {
12841282
const dependency = dependencies.values().next().value
12851283
dependencies.delete(dependency)

src/DependencyGraph/RangeMapping.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,25 @@ export class RangeMapping {
5050
* Adds or updates vertex in the mapping
5151
*/
5252
public addOrUpdateVertex(vertex: RangeVertex): void {
53-
let sheetMap = this.rangeMapping.get(vertex.getStart().sheet)
53+
let sheetMap = this.rangeMapping.get(vertex.sheet)
5454
if (sheetMap === undefined) {
5555
sheetMap = new Map()
56-
this.rangeMapping.set(vertex.getStart().sheet, sheetMap)
56+
this.rangeMapping.set(vertex.sheet, sheetMap)
5757
}
58-
const key = RangeMapping.calculateRangeKey(vertex.getStart(), vertex.getEnd())
58+
const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end)
5959
sheetMap.set(key, vertex)
6060
}
6161

6262
/**
6363
* Removes vertex from the mapping if it exists
6464
*/
6565
public removeVertexIfExists(vertex: RangeVertex): void {
66-
const sheet = vertex.getStart().sheet
66+
const sheet = vertex.sheet
6767
const sheetMap = this.rangeMapping.get(sheet)
6868
if (sheetMap === undefined) {
6969
return
7070
}
71-
const key = RangeMapping.calculateRangeKey(vertex.getStart(), vertex.getEnd())
71+
const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end)
7272
sheetMap.delete(key)
7373
if (sheetMap.size === 0) {
7474
this.rangeMapping.delete(sheet)

src/DependencyGraph/RangeVertex.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* Copyright (c) 2025 Handsoncode. All rights reserved.
44
*/
55

6+
import { SimpleCellAddress } from '..'
67
import {AbsoluteCellRange} from '../AbsoluteCellRange'
7-
import {SimpleCellAddress} from '../Cell'
88
import {CriterionLambda} from '../interpreter/Criterion'
99

1010
/**
@@ -30,15 +30,15 @@ export class RangeVertex {
3030
this.bruteForce = false
3131
}
3232

33-
public get start() {
33+
public get start(): SimpleCellAddress {
3434
return this.range.start
3535
}
3636

37-
public get end() {
37+
public get end(): SimpleCellAddress {
3838
return this.range.end
3939
}
4040

41-
public get sheet() {
41+
public get sheet(): number {
4242
return this.range.start.sheet
4343
}
4444

@@ -105,18 +105,4 @@ export class RangeVertex {
105105
this.dependentCacheRanges.forEach(range => range.criterionFunctionCache.clear())
106106
this.dependentCacheRanges.clear()
107107
}
108-
109-
/**
110-
* Returns start of the range (it's top-left corner)
111-
*/
112-
public getStart(): SimpleCellAddress {
113-
return this.start
114-
}
115-
116-
/**
117-
* Returns end of the range (it's bottom-right corner)
118-
*/
119-
public getEnd(): SimpleCellAddress {
120-
return this.end
121-
}
122108
}

src/DependencyGraph/SheetMapping.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,12 @@ export class SheetMapping {
209209
* Renames sheet.
210210
* If called with sheetId of a placeholder sheet, throws {NoSheetWithIdError}.
211211
* If newDisplayName is conflicting with an existing sheet, throws {SheetNameAlreadyTakenError}.
212-
* If newDisplayName is conflicting with a placeholder sheet name, throws {SheetNameAlreadyTakenError}.
212+
* If newDisplayName is conflicting with a placeholder sheet name, deletes the placeholder sheet and returns its id as mergedWithPlaceholderSheet.
213213
*
214214
* @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists.
215215
* @throws {NoSheetWithIdError} if the sheet with the given ID does not exist.
216216
*/
217-
public renameSheet(sheetId: number, newDisplayName: string): { previousDisplayName: Maybe<string>, mergedSheetWith?: number } {
217+
public renameSheet(sheetId: number, newDisplayName: string): { previousDisplayName: Maybe<string>, mergedWithPlaceholderSheet?: number } {
218218
const sheet = this._getSheetOrThrowError(sheetId, {})
219219

220220
const currentDisplayName = sheet.displayName
@@ -223,15 +223,15 @@ export class SheetMapping {
223223
}
224224

225225
const sheetWithConflictingName = this._getSheetByName(newDisplayName, { includePlaceholders: true })
226-
let mergedSheetWith: number | undefined = undefined
226+
let mergedWithPlaceholderSheet: number | undefined = undefined
227227

228228
if (sheetWithConflictingName !== undefined && sheetWithConflictingName.id !== sheet.id) {
229229
if (!sheetWithConflictingName.isPlaceholder) {
230230
throw new SheetNameAlreadyTakenError(newDisplayName)
231231
} else {
232232
this.mappingFromCanonicalNameToId.delete(sheetWithConflictingName.canonicalName)
233233
this.allSheets.delete(sheetWithConflictingName.id)
234-
mergedSheetWith = sheetWithConflictingName.id
234+
mergedWithPlaceholderSheet = sheetWithConflictingName.id
235235
}
236236
}
237237

@@ -240,7 +240,7 @@ export class SheetMapping {
240240

241241
sheet.displayName = newDisplayName
242242
this.storeSheetInMappings(sheet)
243-
return { previousDisplayName: currentDisplayName, mergedSheetWith }
243+
return { previousDisplayName: currentDisplayName, mergedWithPlaceholderSheet }
244244
}
245245

246246
/**

src/Operations.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,12 @@ export class Operations {
258258
* Renames a sheet in the workbook.
259259
*/
260260
public renameSheet(sheetId: number, newName: string): Maybe<string> {
261-
const { previousDisplayName, mergedSheetWith } = this.sheetMapping.renameSheet(sheetId, newName)
261+
const { previousDisplayName, mergedWithPlaceholderSheet } = this.sheetMapping.renameSheet(sheetId, newName)
262262

263-
if (mergedSheetWith !== undefined) {
264-
this.dependencyGraph.mergeSheets(sheetId, mergedSheetWith)
263+
if (mergedWithPlaceholderSheet !== undefined) {
264+
this.dependencyGraph.mergeSheets(sheetId, mergedWithPlaceholderSheet)
265265
this.stats.measure(StatType.TRANSFORM_ASTS, () => {
266-
const transformation = new RenameSheetTransformer(sheetId, mergedSheetWith)
266+
const transformation = new RenameSheetTransformer(sheetId, mergedWithPlaceholderSheet)
267267
transformation.performEagerTransformations(this.dependencyGraph, this.parser)
268268
this.lazilyTransformingAstService.addTransformation(transformation)
269269
})

0 commit comments

Comments
 (0)