@pagent-libs/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- package/src/workbook.ts +1031 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { SortOrder, Cell, Sheet } from '../types';
|
|
2
|
+
import { getCellKey, parseCellKey } from '../utils/cell-key';
|
|
3
|
+
|
|
4
|
+
export class SortManager {
|
|
5
|
+
static sortRows(sheet: Sheet, sortOrder: SortOrder[], dataRange?: { startRow: number; endRow: number }): void {
|
|
6
|
+
if (sortOrder.length === 0) return;
|
|
7
|
+
|
|
8
|
+
// Determine data range to sort
|
|
9
|
+
const startRow = dataRange?.startRow ?? this.detectDataStartRow(sheet);
|
|
10
|
+
const endRow = dataRange?.endRow ?? this.detectDataEndRow(sheet);
|
|
11
|
+
|
|
12
|
+
if (startRow >= endRow) return;
|
|
13
|
+
|
|
14
|
+
// Extract rows to sort
|
|
15
|
+
const rowsToSort: Array<{ rowIndex: number; values: unknown[] }> = [];
|
|
16
|
+
|
|
17
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
18
|
+
const rowValues: unknown[] = [];
|
|
19
|
+
for (let col = 0; col < sheet.colCount; col++) {
|
|
20
|
+
const cell = sheet.cells.get(getCellKey(row, col));
|
|
21
|
+
rowValues.push(cell?.value ?? null);
|
|
22
|
+
}
|
|
23
|
+
rowsToSort.push({ rowIndex: row, values: rowValues });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sort rows based on sort order
|
|
27
|
+
rowsToSort.sort((a, b) => {
|
|
28
|
+
for (const sort of sortOrder) {
|
|
29
|
+
const aValue = a.values[sort.column];
|
|
30
|
+
const bValue = b.values[sort.column];
|
|
31
|
+
|
|
32
|
+
let comparison = 0;
|
|
33
|
+
|
|
34
|
+
// Handle different value types
|
|
35
|
+
if (aValue === null && bValue === null) {
|
|
36
|
+
comparison = 0;
|
|
37
|
+
} else if (aValue === null) {
|
|
38
|
+
comparison = sort.direction === 'asc' ? -1 : 1;
|
|
39
|
+
} else if (bValue === null) {
|
|
40
|
+
comparison = sort.direction === 'asc' ? 1 : -1;
|
|
41
|
+
} else {
|
|
42
|
+
// Both have values, compare based on type
|
|
43
|
+
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
44
|
+
comparison = aValue - bValue;
|
|
45
|
+
} else if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
46
|
+
comparison = aValue.localeCompare(bValue);
|
|
47
|
+
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
|
48
|
+
comparison = aValue === bValue ? 0 : (aValue ? 1 : -1);
|
|
49
|
+
} else {
|
|
50
|
+
// Mixed types: convert to strings for comparison
|
|
51
|
+
const aStr = String(aValue);
|
|
52
|
+
const bStr = String(bValue);
|
|
53
|
+
comparison = aStr.localeCompare(bStr);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Apply sort direction
|
|
58
|
+
if (sort.direction === 'desc') {
|
|
59
|
+
comparison = -comparison;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (comparison !== 0) {
|
|
63
|
+
return comparison;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return 0; // All sort criteria equal
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Create mapping from old row to new row
|
|
71
|
+
const rowMapping = new Map<number, number>();
|
|
72
|
+
rowsToSort.forEach((row, newIndex) => {
|
|
73
|
+
rowMapping.set(row.rowIndex, startRow + newIndex);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Move cells to new positions and update formulas
|
|
77
|
+
const cellsToMove: Array<{ oldKey: string; newKey: string; cell: Cell }> = [];
|
|
78
|
+
const formulaUpdates: Array<{ oldKey: string; newKey: string; cell: Cell }> = [];
|
|
79
|
+
|
|
80
|
+
// Collect all cells that need to be moved
|
|
81
|
+
for (const [key, cell] of sheet.cells) {
|
|
82
|
+
const { row } = parseCellKey(key);
|
|
83
|
+
if (row >= startRow && row <= endRow) {
|
|
84
|
+
const newRow = rowMapping.get(row);
|
|
85
|
+
if (newRow !== undefined) {
|
|
86
|
+
const newKey = getCellKey(newRow, parseCellKey(key).col);
|
|
87
|
+
cellsToMove.push({ oldKey: key, newKey, cell });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Clear old cells
|
|
93
|
+
for (const { oldKey } of cellsToMove) {
|
|
94
|
+
sheet.cells.delete(oldKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Move cells to new positions
|
|
98
|
+
for (const { newKey, cell } of cellsToMove) {
|
|
99
|
+
if (cell.formula) {
|
|
100
|
+
// Formula needs to be updated based on row movement
|
|
101
|
+
const adjustedFormula = this.adjustFormulaForRowSort(cell.formula, rowMapping, startRow, endRow);
|
|
102
|
+
const updatedCell = { ...cell, formula: adjustedFormula };
|
|
103
|
+
sheet.cells.set(newKey, updatedCell);
|
|
104
|
+
formulaUpdates.push({ oldKey: '', newKey, cell: updatedCell });
|
|
105
|
+
} else {
|
|
106
|
+
sheet.cells.set(newKey, cell);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static adjustFormulaForRowSort(
|
|
112
|
+
formula: string,
|
|
113
|
+
rowMapping: Map<number, number>,
|
|
114
|
+
sortStartRow: number,
|
|
115
|
+
sortEndRow: number
|
|
116
|
+
): string {
|
|
117
|
+
if (!formula.startsWith('=')) {
|
|
118
|
+
return formula;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Remove leading = for processing
|
|
123
|
+
const expression = formula.slice(1);
|
|
124
|
+
|
|
125
|
+
// Adjust cell references in the formula
|
|
126
|
+
// Match patterns like A1, $A1, A$1, $A$1, and ranges like A1:B10, $A$1:$B$10
|
|
127
|
+
const adjustedExpression = expression.replace(
|
|
128
|
+
/(\$?[A-Z]+\$?\d+)(?::(\$?[A-Z]+\$?\d+))?/g,
|
|
129
|
+
(match, startRef, endRef) => {
|
|
130
|
+
// Parse the start reference
|
|
131
|
+
const startCellRef = this.parseCellReference(startRef);
|
|
132
|
+
if (!startCellRef) return match;
|
|
133
|
+
|
|
134
|
+
// Adjust the row if it's within the sorted range and not absolute
|
|
135
|
+
let adjustedStartRow = startCellRef.row;
|
|
136
|
+
if (!startCellRef.rowAbsolute && startCellRef.row >= sortStartRow && startCellRef.row <= sortEndRow) {
|
|
137
|
+
const newRow = rowMapping.get(startCellRef.row);
|
|
138
|
+
if (newRow !== undefined) {
|
|
139
|
+
adjustedStartRow = newRow;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Reconstruct the start reference
|
|
144
|
+
const startResult = this.reconstructCellReference(startCellRef, adjustedStartRow);
|
|
145
|
+
|
|
146
|
+
// If it's a range, adjust the end reference too
|
|
147
|
+
if (endRef) {
|
|
148
|
+
const endCellRef = this.parseCellReference(endRef);
|
|
149
|
+
if (!endCellRef) return match;
|
|
150
|
+
|
|
151
|
+
// Adjust the row if it's within the sorted range and not absolute
|
|
152
|
+
let adjustedEndRow = endCellRef.row;
|
|
153
|
+
if (!endCellRef.rowAbsolute && endCellRef.row >= sortStartRow && endCellRef.row <= sortEndRow) {
|
|
154
|
+
const newRow = rowMapping.get(endCellRef.row);
|
|
155
|
+
if (newRow !== undefined) {
|
|
156
|
+
adjustedEndRow = newRow;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reconstruct the end reference
|
|
161
|
+
const endResult = this.reconstructCellReference(endCellRef, adjustedEndRow);
|
|
162
|
+
return `${startResult}:${endResult}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return startResult;
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return '=' + adjustedExpression;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// If formula adjustment fails, return original formula
|
|
172
|
+
console.warn('Failed to adjust formula during sort:', formula, error);
|
|
173
|
+
return formula;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static parseCellReference(ref: string): { row: number; col: number; rowAbsolute: boolean; colAbsolute: boolean } | null {
|
|
178
|
+
const match = ref.match(/^(\$?)([A-Z]+)(\$?)(\d+)$/);
|
|
179
|
+
if (!match) return null;
|
|
180
|
+
|
|
181
|
+
const [, colDollar, colLetters, rowDollar, rowNumber] = match;
|
|
182
|
+
const colAbsolute = colDollar === '$';
|
|
183
|
+
const rowAbsolute = rowDollar === '$';
|
|
184
|
+
|
|
185
|
+
// Convert column letters to number (A=0, B=1, ..., Z=25, AA=26, etc.)
|
|
186
|
+
let col = 0;
|
|
187
|
+
for (let i = 0; i < colLetters.length; i++) {
|
|
188
|
+
col = col * 26 + (colLetters.charCodeAt(i) - 'A'.charCodeAt(0) + 1);
|
|
189
|
+
}
|
|
190
|
+
col -= 1; // Convert to 0-based
|
|
191
|
+
|
|
192
|
+
const row = parseInt(rowNumber, 10) - 1; // Convert to 0-based
|
|
193
|
+
|
|
194
|
+
return { row, col, rowAbsolute, colAbsolute };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private static reconstructCellReference(
|
|
198
|
+
cellRef: { col: number; colAbsolute: boolean; rowAbsolute: boolean },
|
|
199
|
+
adjustedRow: number
|
|
200
|
+
): string {
|
|
201
|
+
// Convert column number to letters (0=A, 1=B, ..., 25=Z, 26=AA, etc.)
|
|
202
|
+
let colLetters = '';
|
|
203
|
+
let col = cellRef.col + 1; // Convert to 1-based for calculation
|
|
204
|
+
while (col > 0) {
|
|
205
|
+
col -= 1;
|
|
206
|
+
colLetters = String.fromCharCode('A'.charCodeAt(0) + (col % 26)) + colLetters;
|
|
207
|
+
col = Math.floor(col / 26);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rowNumber = adjustedRow + 1; // Convert to 1-based
|
|
211
|
+
|
|
212
|
+
let result = '';
|
|
213
|
+
if (cellRef.colAbsolute) result += '$';
|
|
214
|
+
result += colLetters;
|
|
215
|
+
if (cellRef.rowAbsolute) result += '$';
|
|
216
|
+
result += rowNumber;
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static detectDataStartRow(sheet: Sheet): number {
|
|
222
|
+
// Start from row 0, find first non-empty row
|
|
223
|
+
for (let row = 0; row < sheet.rowCount; row++) {
|
|
224
|
+
for (let col = 0; col < sheet.colCount; col++) {
|
|
225
|
+
const cell = sheet.cells.get(getCellKey(row, col));
|
|
226
|
+
if (cell && (cell.value !== null || cell.formula)) {
|
|
227
|
+
return row;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private static detectDataEndRow(sheet: Sheet): number {
|
|
235
|
+
// Start from the bottom, find last non-empty row
|
|
236
|
+
for (let row = sheet.rowCount - 1; row >= 0; row--) {
|
|
237
|
+
for (let col = 0; col < sheet.colCount; col++) {
|
|
238
|
+
const cell = sheet.cells.get(getCellKey(row, col));
|
|
239
|
+
if (cell && (cell.value !== null || cell.formula)) {
|
|
240
|
+
return row;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return Math.max(0, sheet.rowCount - 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static getColumnSortDirection(column: number, currentSortOrder: SortOrder[]): 'asc' | 'desc' | null {
|
|
248
|
+
const sort = currentSortOrder.find(s => s.column === column);
|
|
249
|
+
return sort ? sort.direction : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static toggleColumnSort(
|
|
253
|
+
column: number,
|
|
254
|
+
currentSortOrder: SortOrder[],
|
|
255
|
+
multiColumn: boolean = false
|
|
256
|
+
): SortOrder[] {
|
|
257
|
+
const existingSortIndex = currentSortOrder.findIndex(s => s.column === column);
|
|
258
|
+
|
|
259
|
+
if (existingSortIndex >= 0) {
|
|
260
|
+
// Column is already in sort order
|
|
261
|
+
const existingSort = currentSortOrder[existingSortIndex];
|
|
262
|
+
if (existingSort.direction === 'asc') {
|
|
263
|
+
// asc -> desc
|
|
264
|
+
const newSortOrder = [...currentSortOrder];
|
|
265
|
+
newSortOrder[existingSortIndex] = { ...existingSort, direction: 'desc' };
|
|
266
|
+
return newSortOrder;
|
|
267
|
+
} else {
|
|
268
|
+
// desc -> remove from sort (no sort)
|
|
269
|
+
return currentSortOrder.filter(s => s.column !== column);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// Column not in sort order
|
|
273
|
+
if (multiColumn && currentSortOrder.length > 0) {
|
|
274
|
+
// Add as secondary sort (ascending)
|
|
275
|
+
return [...currentSortOrder, { column, direction: 'asc' }];
|
|
276
|
+
} else {
|
|
277
|
+
// Replace existing sort or start new sort (ascending)
|
|
278
|
+
return [{ column, direction: 'asc' }];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Format pool for shared format objects
|
|
2
|
+
|
|
3
|
+
import type { CellFormat } from './types';
|
|
4
|
+
|
|
5
|
+
export class FormatPool {
|
|
6
|
+
private formats: Map<string, CellFormat> = new Map();
|
|
7
|
+
private formatToId: Map<string, string> = new Map();
|
|
8
|
+
private nextId = 1;
|
|
9
|
+
|
|
10
|
+
getOrCreate(format: CellFormat): string {
|
|
11
|
+
const formatKey = this.getFormatKey(format);
|
|
12
|
+
const existingId = this.formatToId.get(formatKey);
|
|
13
|
+
if (existingId) {
|
|
14
|
+
return existingId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const id = `format_${this.nextId++}`;
|
|
18
|
+
this.formats.set(id, format);
|
|
19
|
+
this.formatToId.set(formatKey, id);
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(formatId: string): CellFormat | undefined {
|
|
24
|
+
return this.formats.get(formatId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getFormatKey(format: CellFormat): string {
|
|
28
|
+
// Create a deterministic key from format properties
|
|
29
|
+
const keys = Object.keys(format).sort();
|
|
30
|
+
return keys.map((key) => `${key}:${format[key as keyof CellFormat]}`).join('|');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clear(): void {
|
|
34
|
+
this.formats.clear();
|
|
35
|
+
this.formatToId.clear();
|
|
36
|
+
this.nextId = 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
size(): number {
|
|
40
|
+
return this.formats.size;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getAllFormats(): Map<string, CellFormat> {
|
|
44
|
+
return new Map(this.formats);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setFormatToId(formatToId: Map<string, string>): void {
|
|
48
|
+
this.formatToId = formatToId;
|
|
49
|
+
}
|
|
50
|
+
setFormats(formats: Map<string, CellFormat>): void {
|
|
51
|
+
this.formats = formats;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getNextId(): number {
|
|
55
|
+
return this.nextId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setNextId(nextId: number): void {
|
|
59
|
+
this.nextId = nextId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Formula dependency graph for incremental recalculation
|
|
2
|
+
|
|
3
|
+
import type { FormulaGraph, FormulaNode, CellValue } from './types';
|
|
4
|
+
|
|
5
|
+
export class FormulaGraphImpl implements FormulaGraph {
|
|
6
|
+
nodes: Map<string, FormulaNode> = new Map();
|
|
7
|
+
|
|
8
|
+
addFormula(cellKey: string, formula: string, dependencies: Set<string>): void {
|
|
9
|
+
const node: FormulaNode = {
|
|
10
|
+
cellKey,
|
|
11
|
+
formula,
|
|
12
|
+
dependencies: new Set(dependencies),
|
|
13
|
+
dependents: new Set(),
|
|
14
|
+
isDirty: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Update dependents of dependencies
|
|
18
|
+
for (const depKey of dependencies) {
|
|
19
|
+
const depNode = this.nodes.get(depKey);
|
|
20
|
+
if (depNode) {
|
|
21
|
+
depNode.dependents.add(cellKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.nodes.set(cellKey, node);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
removeFormula(cellKey: string): void {
|
|
29
|
+
const node = this.nodes.get(cellKey);
|
|
30
|
+
if (!node) return;
|
|
31
|
+
|
|
32
|
+
// Remove from dependents of dependencies
|
|
33
|
+
for (const depKey of node.dependencies) {
|
|
34
|
+
const depNode = this.nodes.get(depKey);
|
|
35
|
+
if (depNode) {
|
|
36
|
+
depNode.dependents.delete(cellKey);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.nodes.delete(cellKey);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getDependents(cellKey: string): Set<string> {
|
|
44
|
+
const node = this.nodes.get(cellKey);
|
|
45
|
+
return node ? new Set(node.dependents) : new Set();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getDependencies(cellKey: string): Set<string> {
|
|
49
|
+
const node = this.nodes.get(cellKey);
|
|
50
|
+
return node ? new Set(node.dependencies) : new Set();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
invalidate(cellKey: string): void {
|
|
54
|
+
const node = this.nodes.get(cellKey);
|
|
55
|
+
if (!node) return;
|
|
56
|
+
|
|
57
|
+
node.isDirty = true;
|
|
58
|
+
node.cachedValue = undefined;
|
|
59
|
+
|
|
60
|
+
// Invalidate all dependents
|
|
61
|
+
for (const dependentKey of node.dependents) {
|
|
62
|
+
this.invalidate(dependentKey);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getDirtyCells(): Set<string> {
|
|
67
|
+
const dirty = new Set<string>();
|
|
68
|
+
for (const [key, node] of this.nodes) {
|
|
69
|
+
if (node.isDirty) {
|
|
70
|
+
dirty.add(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return dirty;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
markClean(cellKey: string, value: CellValue): void {
|
|
77
|
+
const node = this.nodes.get(cellKey);
|
|
78
|
+
if (node) {
|
|
79
|
+
node.isDirty = false;
|
|
80
|
+
node.cachedValue = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Cell reference parsing utilities
|
|
2
|
+
|
|
3
|
+
import type { CellReference, RangeReference } from './types';
|
|
4
|
+
|
|
5
|
+
const COLUMN_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
6
|
+
|
|
7
|
+
export function parseCellReference(ref: string): CellReference | null {
|
|
8
|
+
// Match patterns like A1, $A1, A$1, $A$1, sheetname!A1, 'sheet name'!A1
|
|
9
|
+
let sheetName: string | undefined;
|
|
10
|
+
let cellPart = ref;
|
|
11
|
+
|
|
12
|
+
// Check for cross-sheet reference (sheetname!A1 or 'sheet name'!A1)
|
|
13
|
+
const sheetMatch = ref.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
|
|
14
|
+
if (sheetMatch) {
|
|
15
|
+
sheetName = sheetMatch[1] || sheetMatch[2]; // Use quoted name if present, otherwise unquoted
|
|
16
|
+
cellPart = sheetMatch[3];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Match patterns like A1, $A1, A$1, $A$1
|
|
20
|
+
const match = cellPart.match(/^(\$?)([A-Z]+)(\$?)(\d+)$/);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
|
|
23
|
+
const [, colAbs, colStr, rowAbs, rowStr] = match;
|
|
24
|
+
const col = columnLabelToIndex(colStr);
|
|
25
|
+
const row = parseInt(rowStr, 10) - 1; // Convert to 0-based
|
|
26
|
+
|
|
27
|
+
if (col === -1 || isNaN(row) || row < 0) return null;
|
|
28
|
+
|
|
29
|
+
// $ prefix means absolute, no $ means relative
|
|
30
|
+
return {
|
|
31
|
+
row,
|
|
32
|
+
col,
|
|
33
|
+
rowAbsolute: rowAbs === '$', // Row is absolute if $ is present before row number
|
|
34
|
+
colAbsolute: colAbs === '$', // Column is absolute if $ is present before column letter
|
|
35
|
+
sheetName,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseRangeReference(ref: string): RangeReference | null {
|
|
40
|
+
// Match patterns like A1:B10, $A$1:$B$10, sheetname!A1:B10, 'sheet name'!A1:B10
|
|
41
|
+
let sheetName: string | undefined;
|
|
42
|
+
let rangePart = ref;
|
|
43
|
+
|
|
44
|
+
// Check for cross-sheet reference (sheetname!A1:B10 or 'sheet name'!A1:B10)
|
|
45
|
+
const sheetMatch = ref.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
|
|
46
|
+
if (sheetMatch) {
|
|
47
|
+
sheetName = sheetMatch[1] || sheetMatch[2]; // Use quoted name if present, otherwise unquoted
|
|
48
|
+
rangePart = sheetMatch[3];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Split by colon, but be careful not to split if colon is inside quotes
|
|
52
|
+
const colonIndex = rangePart.indexOf(':');
|
|
53
|
+
if (colonIndex === -1) return null;
|
|
54
|
+
|
|
55
|
+
const startPart = rangePart.substring(0, colonIndex);
|
|
56
|
+
const endPart = rangePart.substring(colonIndex + 1);
|
|
57
|
+
|
|
58
|
+
const start = parseCellReference(sheetName ? `${sheetName}!${startPart}` : startPart);
|
|
59
|
+
const end = parseCellReference(sheetName ? `${sheetName}!${endPart}` : endPart);
|
|
60
|
+
|
|
61
|
+
if (!start || !end) return null;
|
|
62
|
+
|
|
63
|
+
// Ensure both start and end have the same sheet name
|
|
64
|
+
if (sheetName) {
|
|
65
|
+
start.sheetName = sheetName;
|
|
66
|
+
end.sheetName = sheetName;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { start, end };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function columnLabelToIndex(label: string): number {
|
|
73
|
+
let index = 0;
|
|
74
|
+
for (let i = 0; i < label.length; i++) {
|
|
75
|
+
const char = label[i].toUpperCase();
|
|
76
|
+
const charIndex = COLUMN_LETTERS.indexOf(char);
|
|
77
|
+
if (charIndex === -1) return -1;
|
|
78
|
+
index = index * 26 + (charIndex + 1);
|
|
79
|
+
}
|
|
80
|
+
return index - 1; // Convert to 0-based
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function columnIndexToLabel(index: number): string {
|
|
84
|
+
let label = '';
|
|
85
|
+
index += 1; // Convert to 1-based
|
|
86
|
+
while (index > 0) {
|
|
87
|
+
index -= 1;
|
|
88
|
+
label = COLUMN_LETTERS[index % 26] + label;
|
|
89
|
+
index = Math.floor(index / 26);
|
|
90
|
+
}
|
|
91
|
+
return label;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function cellReferenceToKey(ref: CellReference, currentRow: number, currentCol: number): string {
|
|
95
|
+
const row = ref.rowAbsolute ? ref.row : currentRow + ref.row;
|
|
96
|
+
const col = ref.colAbsolute ? ref.col : currentCol + ref.col;
|
|
97
|
+
return `${row}:${col}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Utility functions for adjusting formulas when copying/filling cells
|
|
2
|
+
|
|
3
|
+
import { parseCellReference, columnIndexToLabel } from './cell-reference';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adjusts a formula when copying from source cell to target cell.
|
|
7
|
+
* Increments relative references (without $) and keeps absolute references (with $) unchanged.
|
|
8
|
+
*
|
|
9
|
+
* @param formula The original formula string
|
|
10
|
+
* @param sourceRow Source cell row (0-based)
|
|
11
|
+
* @param sourceCol Source cell column (0-based)
|
|
12
|
+
* @param targetRow Target cell row (0-based)
|
|
13
|
+
* @param targetCol Target cell column (0-based)
|
|
14
|
+
* @returns Adjusted formula string
|
|
15
|
+
*/
|
|
16
|
+
export function adjustFormula(
|
|
17
|
+
formula: string,
|
|
18
|
+
sourceRow: number,
|
|
19
|
+
sourceCol: number,
|
|
20
|
+
targetRow: number,
|
|
21
|
+
targetCol: number
|
|
22
|
+
): string {
|
|
23
|
+
if (!formula.startsWith('=')) {
|
|
24
|
+
// Not a formula, return as-is
|
|
25
|
+
return formula;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rowOffset = targetRow - sourceRow;
|
|
29
|
+
const colOffset = targetCol - sourceCol;
|
|
30
|
+
|
|
31
|
+
if (rowOffset === 0 && colOffset === 0) {
|
|
32
|
+
// Same cell, no adjustment needed
|
|
33
|
+
return formula;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Remove leading = for processing
|
|
37
|
+
const expression = formula.slice(1);
|
|
38
|
+
|
|
39
|
+
// Adjust cell references in the formula
|
|
40
|
+
// Match patterns like A1, $A1, A$1, $A$1, and ranges like A1:B10, $A$1:$B$10
|
|
41
|
+
const adjustedExpression = expression.replace(
|
|
42
|
+
/(\$?[A-Z]+\$?\d+)(?::(\$?[A-Z]+\$?\d+))?/g,
|
|
43
|
+
(match, startRef, endRef) => {
|
|
44
|
+
const startCellRef = parseCellReference(startRef);
|
|
45
|
+
if (!startCellRef) return match;
|
|
46
|
+
|
|
47
|
+
// For relative references, parseCellReference returns the absolute position (e.g., A1 = row 0, col 0)
|
|
48
|
+
// We need to calculate the offset from the source cell, then apply it to the target cell
|
|
49
|
+
let newStartRow: number;
|
|
50
|
+
let newStartCol: number;
|
|
51
|
+
|
|
52
|
+
if (startCellRef.rowAbsolute) {
|
|
53
|
+
// Absolute row: keep the same row
|
|
54
|
+
newStartRow = startCellRef.row;
|
|
55
|
+
} else {
|
|
56
|
+
// Relative row: calculate offset from source and apply to target
|
|
57
|
+
// The referenced cell's absolute row is startCellRef.row
|
|
58
|
+
// The offset from source is: startCellRef.row - sourceRow
|
|
59
|
+
// Apply same offset to target: targetRow + (startCellRef.row - sourceRow)
|
|
60
|
+
newStartRow = targetRow + (startCellRef.row - sourceRow);
|
|
61
|
+
if (newStartRow < 0) newStartRow = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (startCellRef.colAbsolute) {
|
|
65
|
+
// Absolute column: keep the same column
|
|
66
|
+
newStartCol = startCellRef.col;
|
|
67
|
+
} else {
|
|
68
|
+
// Relative column: calculate offset from source and apply to target
|
|
69
|
+
// The referenced cell's absolute col is startCellRef.col
|
|
70
|
+
// The offset from source is: startCellRef.col - sourceCol
|
|
71
|
+
// Apply same offset to target: targetCol + (startCellRef.col - sourceCol)
|
|
72
|
+
newStartCol = targetCol + (startCellRef.col - sourceCol);
|
|
73
|
+
if (newStartCol < 0) newStartCol = 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const startColLabel = columnIndexToLabel(newStartCol);
|
|
77
|
+
const startRowLabel = (newStartRow + 1).toString();
|
|
78
|
+
|
|
79
|
+
let startResult = '';
|
|
80
|
+
if (startCellRef.colAbsolute) startResult += '$';
|
|
81
|
+
startResult += startColLabel;
|
|
82
|
+
if (startCellRef.rowAbsolute) startResult += '$';
|
|
83
|
+
startResult += startRowLabel;
|
|
84
|
+
|
|
85
|
+
// If it's a range, adjust the end cell too
|
|
86
|
+
if (endRef) {
|
|
87
|
+
const endCellRef = parseCellReference(endRef);
|
|
88
|
+
if (!endCellRef) return match;
|
|
89
|
+
|
|
90
|
+
let newEndRow: number;
|
|
91
|
+
let newEndCol: number;
|
|
92
|
+
|
|
93
|
+
if (endCellRef.rowAbsolute) {
|
|
94
|
+
// Absolute row: keep the same row
|
|
95
|
+
newEndRow = endCellRef.row;
|
|
96
|
+
} else {
|
|
97
|
+
// Relative row: calculate offset from source and apply to target
|
|
98
|
+
newEndRow = targetRow + (endCellRef.row - sourceRow);
|
|
99
|
+
if (newEndRow < 0) newEndRow = 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (endCellRef.colAbsolute) {
|
|
103
|
+
// Absolute column: keep the same column
|
|
104
|
+
newEndCol = endCellRef.col;
|
|
105
|
+
} else {
|
|
106
|
+
// Relative column: calculate offset from source and apply to target
|
|
107
|
+
newEndCol = targetCol + (endCellRef.col - sourceCol);
|
|
108
|
+
if (newEndCol < 0) newEndCol = 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const endColLabel = columnIndexToLabel(newEndCol);
|
|
112
|
+
const endRowLabel = (newEndRow + 1).toString();
|
|
113
|
+
|
|
114
|
+
let endResult = '';
|
|
115
|
+
if (endCellRef.colAbsolute) endResult += '$';
|
|
116
|
+
endResult += endColLabel;
|
|
117
|
+
if (endCellRef.rowAbsolute) endResult += '$';
|
|
118
|
+
endResult += endRowLabel;
|
|
119
|
+
|
|
120
|
+
return `${startResult}:${endResult}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return startResult;
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return '=' + adjustedExpression;
|
|
128
|
+
}
|
|
129
|
+
|