@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.
Files changed (111) hide show
  1. package/README.md +32 -0
  2. package/dist/canvas/cell-renderer.d.ts +45 -0
  3. package/dist/canvas/cell-renderer.d.ts.map +1 -0
  4. package/dist/canvas/grid-renderer.d.ts +29 -0
  5. package/dist/canvas/grid-renderer.d.ts.map +1 -0
  6. package/dist/canvas/header-renderer.d.ts +58 -0
  7. package/dist/canvas/header-renderer.d.ts.map +1 -0
  8. package/dist/canvas/hit-testing.d.ts +81 -0
  9. package/dist/canvas/hit-testing.d.ts.map +1 -0
  10. package/dist/canvas/index.d.ts +9 -0
  11. package/dist/canvas/index.d.ts.map +1 -0
  12. package/dist/canvas/renderer.d.ts +140 -0
  13. package/dist/canvas/renderer.d.ts.map +1 -0
  14. package/dist/canvas/selection-renderer.d.ts +55 -0
  15. package/dist/canvas/selection-renderer.d.ts.map +1 -0
  16. package/dist/canvas/text-renderer.d.ts +49 -0
  17. package/dist/canvas/text-renderer.d.ts.map +1 -0
  18. package/dist/canvas/types.d.ts +200 -0
  19. package/dist/canvas/types.d.ts.map +1 -0
  20. package/dist/collaboration/firebase-provider.d.ts +13 -0
  21. package/dist/collaboration/firebase-provider.d.ts.map +1 -0
  22. package/dist/collaboration/index.d.ts +3 -0
  23. package/dist/collaboration/index.d.ts.map +1 -0
  24. package/dist/collaboration/types.d.ts +34 -0
  25. package/dist/collaboration/types.d.ts.map +1 -0
  26. package/dist/event-emitter.d.ts +13 -0
  27. package/dist/event-emitter.d.ts.map +1 -0
  28. package/dist/export/csv.d.ts +5 -0
  29. package/dist/export/csv.d.ts.map +1 -0
  30. package/dist/export/index.d.ts +2 -0
  31. package/dist/export/index.d.ts.map +1 -0
  32. package/dist/features/filter.d.ts +58 -0
  33. package/dist/features/filter.d.ts.map +1 -0
  34. package/dist/features/freeze.d.ts +86 -0
  35. package/dist/features/freeze.d.ts.map +1 -0
  36. package/dist/features/index.d.ts +4 -0
  37. package/dist/features/index.d.ts.map +1 -0
  38. package/dist/features/sort.d.ts +15 -0
  39. package/dist/features/sort.d.ts.map +1 -0
  40. package/dist/format-pool.d.ts +17 -0
  41. package/dist/format-pool.d.ts.map +1 -0
  42. package/dist/formula-graph.d.ts +12 -0
  43. package/dist/formula-graph.d.ts.map +1 -0
  44. package/dist/formula-parser/cell-reference.d.ts +7 -0
  45. package/dist/formula-parser/cell-reference.d.ts.map +1 -0
  46. package/dist/formula-parser/formula-adjust.d.ts +13 -0
  47. package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
  48. package/dist/formula-parser/formula-ranges.d.ts +22 -0
  49. package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
  50. package/dist/formula-parser/index.d.ts +6 -0
  51. package/dist/formula-parser/index.d.ts.map +1 -0
  52. package/dist/formula-parser/parser.d.ts +18 -0
  53. package/dist/formula-parser/parser.d.ts.map +1 -0
  54. package/dist/formula-parser/types.d.ts +33 -0
  55. package/dist/formula-parser/types.d.ts.map +1 -0
  56. package/dist/index.d.ts +15 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.esm.js +5823 -0
  59. package/dist/index.esm.js.map +1 -0
  60. package/dist/index.js +5885 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/sheet.d.ts +119 -0
  63. package/dist/sheet.d.ts.map +1 -0
  64. package/dist/style-pool.d.ts +17 -0
  65. package/dist/style-pool.d.ts.map +1 -0
  66. package/dist/types.d.ts +260 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/utils/cell-key.d.ts +7 -0
  69. package/dist/utils/cell-key.d.ts.map +1 -0
  70. package/dist/utils/format-utils.d.ts +75 -0
  71. package/dist/utils/format-utils.d.ts.map +1 -0
  72. package/dist/utils/range.d.ts +13 -0
  73. package/dist/utils/range.d.ts.map +1 -0
  74. package/dist/workbook.d.ts +155 -0
  75. package/dist/workbook.d.ts.map +1 -0
  76. package/package.json +46 -0
  77. package/src/canvas/cell-renderer.ts +181 -0
  78. package/src/canvas/grid-renderer.ts +238 -0
  79. package/src/canvas/header-renderer.ts +402 -0
  80. package/src/canvas/hit-testing.ts +537 -0
  81. package/src/canvas/index.ts +16 -0
  82. package/src/canvas/renderer.ts +1056 -0
  83. package/src/canvas/selection-renderer.ts +604 -0
  84. package/src/canvas/text-renderer.ts +321 -0
  85. package/src/canvas/types.ts +289 -0
  86. package/src/collaboration/firebase-provider.ts +48 -0
  87. package/src/collaboration/index.ts +5 -0
  88. package/src/collaboration/types.ts +38 -0
  89. package/src/event-emitter.ts +73 -0
  90. package/src/export/csv.ts +101 -0
  91. package/src/export/index.ts +4 -0
  92. package/src/features/filter.ts +231 -0
  93. package/src/features/freeze.ts +271 -0
  94. package/src/features/index.ts +5 -0
  95. package/src/features/sort.ts +282 -0
  96. package/src/format-pool.ts +61 -0
  97. package/src/formula-graph.ts +84 -0
  98. package/src/formula-parser/cell-reference.ts +99 -0
  99. package/src/formula-parser/formula-adjust.ts +129 -0
  100. package/src/formula-parser/formula-ranges.ts +159 -0
  101. package/src/formula-parser/index.ts +8 -0
  102. package/src/formula-parser/parser.ts +438 -0
  103. package/src/formula-parser/types.ts +39 -0
  104. package/src/index.ts +25 -0
  105. package/src/sheet.ts +502 -0
  106. package/src/style-pool.ts +62 -0
  107. package/src/types.ts +291 -0
  108. package/src/utils/cell-key.ts +19 -0
  109. package/src/utils/format-utils.ts +515 -0
  110. package/src/utils/range.ts +53 -0
  111. 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
+