@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,515 @@
1
+ // Format utilities for advanced cell formatting
2
+ import type { CellFormat, FormatType } from '../types';
3
+
4
+ /**
5
+ * Main number formatter that delegates to specific format handlers
6
+ */
7
+ export function formatNumber(value: number, format: CellFormat): string {
8
+ if (!format.type || format.type === 'text') {
9
+ return String(value);
10
+ }
11
+
12
+ switch (format.type) {
13
+ case 'number':
14
+ return formatPlainNumber(value, format);
15
+ case 'currency':
16
+ return formatCurrency(value, format);
17
+ case 'accounting':
18
+ return formatAccounting(value, format);
19
+ case 'percentage':
20
+ return formatPercentage(value, format);
21
+ case 'scientific':
22
+ return formatScientific(value, format);
23
+ case 'fraction':
24
+ return formatFraction(value, format);
25
+ case 'date':
26
+ return formatDate(value, format);
27
+ case 'time':
28
+ return formatTime(value, format);
29
+ case 'datetime':
30
+ return formatDateTime(value, format);
31
+ case 'duration':
32
+ return formatDuration(value, format);
33
+ case 'custom':
34
+ return format.pattern ? formatCustomPattern(value, format.pattern) : String(value);
35
+ default:
36
+ return String(value);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Format plain numbers with decimal places and thousands separator
42
+ */
43
+ export function formatPlainNumber(value: number, format: CellFormat): string {
44
+ const decimalPlaces = format.decimalPlaces ?? 2;
45
+ const useThousands = format.useThousandsSeparator ?? true;
46
+
47
+ let formatted = value.toFixed(decimalPlaces);
48
+
49
+ if (useThousands) {
50
+ // Add thousands separator
51
+ const parts = formatted.split('.');
52
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
53
+ formatted = parts.join('.');
54
+ }
55
+
56
+ // Remove trailing zeros after decimal point
57
+ formatted = formatted.replace(/\.?0+$/, '');
58
+
59
+ // Apply negative format
60
+ if (value < 0) {
61
+ switch (format.negativeFormat) {
62
+ case 'parentheses':
63
+ return `(${formatted.replace('-', '')})`;
64
+ case 'red':
65
+ return formatted; // Color would be handled by CSS/styling system
66
+ default:
67
+ return formatted; // 'minus' format
68
+ }
69
+ }
70
+
71
+ return formatted;
72
+ }
73
+
74
+ /**
75
+ * Format currency values
76
+ */
77
+ export function formatCurrency(value: number, format: CellFormat): string {
78
+ const currency = format.currencyCode || 'USD';
79
+ const decimalPlaces = format.decimalPlaces ?? 2;
80
+ const position = format.currencySymbolPosition || 'prefix';
81
+
82
+ try {
83
+ const formatter = new Intl.NumberFormat('en-US', {
84
+ style: 'currency',
85
+ currency,
86
+ minimumFractionDigits: decimalPlaces,
87
+ maximumFractionDigits: decimalPlaces,
88
+ });
89
+
90
+ const formatted = formatter.format(Math.abs(value));
91
+
92
+ // Apply negative format
93
+ if (value < 0) {
94
+ switch (format.negativeFormat) {
95
+ case 'parentheses':
96
+ return `(${formatted})`;
97
+ case 'red':
98
+ return formatted; // Color would be handled by CSS/styling system
99
+ default:
100
+ return `-${formatted}`; // 'minus' format
101
+ }
102
+ }
103
+
104
+ return formatted;
105
+ } catch {
106
+ // Fallback for unsupported currencies
107
+ const symbol = getCurrencySymbol(currency);
108
+ const numberPart = formatPlainNumber(value, format);
109
+ return position === 'prefix' ? `${symbol}${numberPart}` : `${numberPart}${symbol}`;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Format accounting style (currency symbol aligned left)
115
+ */
116
+ export function formatAccounting(value: number, format: CellFormat): string {
117
+ const currency = format.currencyCode || 'USD';
118
+
119
+ const symbol = getCurrencySymbol(currency);
120
+ const numberPart = formatPlainNumber(Math.abs(value), format);
121
+
122
+ if (value < 0) {
123
+ switch (format.negativeFormat) {
124
+ case 'parentheses':
125
+ return `(${symbol} ${numberPart})`;
126
+ case 'red':
127
+ return `${symbol} ${numberPart}`; // Color would be handled by CSS/styling system
128
+ default:
129
+ return `${symbol} (${numberPart})`; // 'minus' format
130
+ }
131
+ }
132
+
133
+ return `${symbol} ${numberPart}`;
134
+ }
135
+
136
+ /**
137
+ * Format percentage values
138
+ */
139
+ export function formatPercentage(value: number, format: CellFormat): string {
140
+ const decimalPlaces = format.decimalPlaces ?? 2;
141
+ const percentValue = value * 100; // Assume value is stored as decimal (0.5 = 50%)
142
+
143
+ let formatted = percentValue.toFixed(decimalPlaces);
144
+ formatted = formatted.replace(/\.?0+$/, ''); // Remove trailing zeros
145
+
146
+ return `${formatted}%`;
147
+ }
148
+
149
+ /**
150
+ * Format scientific notation
151
+ */
152
+ export function formatScientific(value: number, format: CellFormat): string {
153
+ const decimalPlaces = format.decimalPlaces ?? 2;
154
+ return value.toExponential(decimalPlaces);
155
+ }
156
+
157
+ /**
158
+ * Format fraction values
159
+ */
160
+ export function formatFraction(value: number, format: CellFormat): string {
161
+ const fractionType = format.fractionType || 'upToOne';
162
+
163
+ // Convert decimal to fraction based on type
164
+ switch (fractionType) {
165
+ case 'upToOne':
166
+ return formatToFraction(value, 9); // Allow single-digit denominators (1-9)
167
+ case 'upToTwo':
168
+ return formatToFraction(value, 99); // Allow two-digit denominators (1-99)
169
+ case 'upToThree':
170
+ return formatToFraction(value, 999); // Allow three-digit denominators (1-999)
171
+ case 'asHalves':
172
+ return formatToFraction(value, 2, true);
173
+ case 'asQuarters':
174
+ return formatToFraction(value, 4, true);
175
+ case 'asEighths':
176
+ return formatToFraction(value, 8, true);
177
+ case 'asSixteenths':
178
+ return formatToFraction(value, 16, true);
179
+ case 'asTenths':
180
+ return formatToFraction(value, 10, true);
181
+ case 'asHundredths':
182
+ return formatToFraction(value, 100, true);
183
+ default:
184
+ return formatToFraction(value, 9);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Helper to convert decimal to fraction
190
+ */
191
+ function formatToFraction(value: number, maxDenominator: number, exact: boolean = false): string {
192
+ const whole = Math.floor(value);
193
+ const fractional = value - whole;
194
+
195
+ if (fractional === 0) {
196
+ return whole.toString();
197
+ }
198
+
199
+ const fraction = exact ? toFraction(fractional, maxDenominator) : toSimpleFraction(fractional, maxDenominator);
200
+
201
+ if (fraction.denominator === 1) {
202
+ return `${whole + fraction.numerator}`;
203
+ }
204
+
205
+ return whole > 0 ? `${whole} ${fraction.numerator}/${fraction.denominator}` : `${fraction.numerator}/${fraction.denominator}`;
206
+ }
207
+
208
+ /**
209
+ * Convert decimal to exact fraction with given denominator
210
+ */
211
+ function toFraction(decimal: number, denominator: number): { numerator: number; denominator: number } {
212
+ const numerator = Math.round(decimal * denominator);
213
+ return { numerator, denominator };
214
+ }
215
+
216
+ /**
217
+ * Convert decimal to simple fraction
218
+ */
219
+ function toSimpleFraction(decimal: number, maxDenominator: number): { numerator: number; denominator: number } {
220
+ let bestNumerator = 1;
221
+ let bestDenominator = 1;
222
+ let bestError = Math.abs(decimal - 1);
223
+
224
+ for (let denominator = 1; denominator <= maxDenominator; denominator++) {
225
+ const numerator = Math.round(decimal * denominator);
226
+ const error = Math.abs(decimal - numerator / denominator);
227
+ if (error < bestError) {
228
+ bestNumerator = numerator;
229
+ bestDenominator = denominator;
230
+ bestError = error;
231
+ }
232
+ }
233
+
234
+ // Simplify fraction
235
+ const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
236
+ const divisor = gcd(bestNumerator, bestDenominator);
237
+ return {
238
+ numerator: bestNumerator / divisor,
239
+ denominator: bestDenominator / divisor
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Format date values from Excel date serial
245
+ */
246
+ export function formatDate(value: number, format: CellFormat): string {
247
+ const dateFormat = format.dateFormat || 'MM/DD/YYYY';
248
+ const jsDate = excelDateToJS(value);
249
+
250
+ return formatJSDate(jsDate, dateFormat);
251
+ }
252
+
253
+ /**
254
+ * Format time values
255
+ */
256
+ export function formatTime(value: number, format: CellFormat): string {
257
+ const timeFormat = format.timeFormat || 'HH:mm:ss';
258
+ const jsDate = excelDateToJS(value);
259
+
260
+ return formatJSTime(jsDate, timeFormat);
261
+ }
262
+
263
+ /**
264
+ * Format combined date and time
265
+ */
266
+ export function formatDateTime(value: number, format: CellFormat): string {
267
+ const dateFormat = format.dateFormat || 'MM/DD/YYYY';
268
+ const timeFormat = format.timeFormat || 'HH:mm:ss';
269
+ const jsDate = excelDateToJS(value);
270
+
271
+ const datePart = formatJSDate(jsDate, dateFormat);
272
+ const timePart = formatJSTime(jsDate, timeFormat);
273
+
274
+ return `${datePart} ${timePart}`;
275
+ }
276
+
277
+ /**
278
+ * Format duration values
279
+ */
280
+ export function formatDuration(value: number, format: CellFormat): string {
281
+ const durationType = format.durationFormat || 'hours';
282
+
283
+ switch (durationType) {
284
+ case 'hours':
285
+ return `${value}h`;
286
+ case 'minutes':
287
+ return `${value}m`;
288
+ case 'seconds':
289
+ return `${value}s`;
290
+ case 'milliseconds':
291
+ return `${value}ms`;
292
+ default:
293
+ return `${value}`;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Format custom pattern
299
+ */
300
+ export function formatCustomPattern(value: number, pattern: string): string {
301
+ // Basic implementation - handle common patterns
302
+ if (pattern.includes('#,##0.00')) {
303
+ return formatPlainNumber(value, { decimalPlaces: 2, useThousandsSeparator: true });
304
+ }
305
+ if (pattern.includes('0.0%')) {
306
+ return formatPercentage(value / 100, { decimalPlaces: 1 }); // Assume value is stored as decimal
307
+ }
308
+ if (pattern.includes('MM/DD/YYYY')) {
309
+ return formatDate(value, { dateFormat: 'MM/DD/YYYY' });
310
+ }
311
+
312
+ // Fallback to pattern as-is with value substituted
313
+ return pattern.replace(/0/g, value.toString());
314
+ }
315
+
316
+ /**
317
+ * Convert Excel date serial to JavaScript Date
318
+ */
319
+ export function excelDateToJS(serial: number): Date {
320
+ // Excel epoch is January 1, 1900
321
+ // But Excel incorrectly treats 1900 as a leap year, so we adjust
322
+ const utcDays = Math.floor(serial - 25569);
323
+ const utcValue = utcDays * 86400 * 1000;
324
+ return new Date(utcValue);
325
+ }
326
+
327
+ /**
328
+ * Convert JavaScript Date to Excel date serial
329
+ */
330
+ export function jsToExcelDate(date: Date): number {
331
+ // Excel epoch is January 1, 1900 (but Excel treats 1900 as leap year incorrectly)
332
+ // We need to account for this by using 25569 as the offset
333
+ const utcTime = date.getTime();
334
+ const utcDays = utcTime / (86400 * 1000);
335
+ return Math.floor(utcDays + 25569);
336
+ }
337
+
338
+ /**
339
+ * Parse common date string formats and return Excel date serial
340
+ * Supports formats like: YYYY/MM/DD, YYYY-MM-DD, DD-MM-YYYY, MM/DD/YYYY, etc.
341
+ */
342
+ export function parseDateString(dateStr: string): number | null {
343
+ const trimmed = dateStr.trim();
344
+
345
+ // Try various date formats
346
+ const patterns = [
347
+ // YYYY/MM/DD or YYYY-MM-DD or YYYY.MM.DD
348
+ /^(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})$/,
349
+ // DD-MM-YYYY or DD/MM/YYYY or DD.MM.YYYY
350
+ /^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/,
351
+ // MM/DD/YYYY (US format)
352
+ /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/,
353
+ ];
354
+
355
+ for (const pattern of patterns) {
356
+ const match = trimmed.match(pattern);
357
+ if (match) {
358
+ let year: number, month: number, day: number;
359
+
360
+ if (pattern === patterns[0]) {
361
+ // YYYY/MM/DD format
362
+ [, year, month, day] = match.map(Number);
363
+ } else if (pattern === patterns[1] || pattern === patterns[2]) {
364
+ // DD-MM-YYYY or MM/DD/YYYY format - need to determine which one
365
+ const first = Number(match[1]);
366
+ const second = Number(match[2]);
367
+ const third = Number(match[3]);
368
+
369
+ if (pattern === patterns[2]) {
370
+ // MM/DD/YYYY format
371
+ month = first;
372
+ day = second;
373
+ year = third;
374
+ } else {
375
+ // DD-MM-YYYY format (assuming European style)
376
+ day = first;
377
+ month = second;
378
+ year = third;
379
+ }
380
+ } else {
381
+ // DD-MM-YYYY format
382
+ [, day, month, year] = match.map(Number);
383
+ }
384
+
385
+ // Validate date
386
+ if (year >= 1900 && year <= 9999 &&
387
+ month >= 1 && month <= 12 &&
388
+ day >= 1 && day <= 31) {
389
+ try {
390
+ const date = new Date(year, month - 1, day);
391
+ // Verify the date is valid (handles invalid dates like Feb 30)
392
+ if (date.getFullYear() === year &&
393
+ date.getMonth() === month - 1 &&
394
+ date.getDate() === day) {
395
+ return jsToExcelDate(date);
396
+ }
397
+ } catch {
398
+ // Invalid date
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ return null;
405
+ }
406
+
407
+ /**
408
+ * Format JavaScript Date according to pattern
409
+ */
410
+ export function formatJSDate(date: Date, pattern: string): string {
411
+ const year = date.getFullYear();
412
+ const month = date.getMonth() + 1; // 0-based to 1-based
413
+ const day = date.getDate();
414
+
415
+ switch (pattern) {
416
+ case 'MM/DD/YYYY':
417
+ return `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year}`;
418
+ case 'DD-MM-YYYY':
419
+ return `${day.toString().padStart(2, '0')}-${month.toString().padStart(2, '0')}-${year}`;
420
+ case 'YYYY-MM-DD':
421
+ return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
422
+ case 'Month DD YYYY': {
423
+ const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
424
+ return `${monthNames[month - 1]} ${day} ${year}`;
425
+ }
426
+ default:
427
+ return date.toLocaleDateString();
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Format JavaScript Date time according to pattern
433
+ */
434
+ function formatJSTime(date: Date, pattern: string): string {
435
+ const hours = date.getHours();
436
+ const minutes = date.getMinutes();
437
+ const seconds = date.getSeconds();
438
+
439
+ switch (pattern) {
440
+ case 'HH:mm:ss':
441
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
442
+ case 'h:mm AM/PM': {
443
+ const hour12 = hours % 12 || 12;
444
+ const ampm = hours >= 12 ? 'PM' : 'AM';
445
+ return `${hour12}:${minutes.toString().padStart(2, '0')} ${ampm}`;
446
+ }
447
+ case 'HH:mm':
448
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
449
+ default:
450
+ return date.toLocaleTimeString();
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Get currency symbol for currency code
456
+ */
457
+ function getCurrencySymbol(currencyCode: string): string {
458
+ const symbols: Record<string, string> = {
459
+ 'USD': '$',
460
+ 'EUR': '€',
461
+ 'GBP': '£',
462
+ 'JPY': '¥',
463
+ 'CAD': 'C$',
464
+ 'AUD': 'A$',
465
+ 'CHF': 'CHF',
466
+ 'CNY': '¥',
467
+ 'INR': '₹',
468
+ 'KRW': '₩',
469
+ };
470
+
471
+ return symbols[currencyCode] || currencyCode;
472
+ }
473
+
474
+ /**
475
+ * Check if two formats are equivalent
476
+ */
477
+ export function areFormatsEqual(format1: CellFormat | undefined, format2: CellFormat | undefined): boolean {
478
+ if (!format1 && !format2) return true;
479
+ if (!format1 || !format2) return false;
480
+
481
+ return JSON.stringify(format1) === JSON.stringify(format2);
482
+ }
483
+
484
+ /**
485
+ * Get default format for a format type
486
+ */
487
+ export function getDefaultFormatForType(type: FormatType): CellFormat {
488
+ switch (type) {
489
+ case 'number':
490
+ return { type: 'number', decimalPlaces: 2, useThousandsSeparator: true };
491
+ case 'currency':
492
+ return { type: 'currency', currencyCode: 'USD', decimalPlaces: 2 };
493
+ case 'accounting':
494
+ return { type: 'accounting', currencyCode: 'USD', decimalPlaces: 2 };
495
+ case 'percentage':
496
+ return { type: 'percentage', decimalPlaces: 2 };
497
+ case 'scientific':
498
+ return { type: 'scientific', decimalPlaces: 2 };
499
+ case 'fraction':
500
+ return { type: 'fraction', fractionType: 'upToOne' };
501
+ case 'date':
502
+ return { type: 'date', dateFormat: 'MM/DD/YYYY' };
503
+ case 'time':
504
+ return { type: 'time', timeFormat: 'HH:mm:ss' };
505
+ case 'datetime':
506
+ return { type: 'datetime', dateFormat: 'MM/DD/YYYY', timeFormat: 'HH:mm:ss' };
507
+ case 'duration':
508
+ return { type: 'duration', durationFormat: 'hours' };
509
+ case 'custom':
510
+ return { type: 'custom', pattern: '#,##0.00' };
511
+ case 'text':
512
+ default:
513
+ return { type: 'text' };
514
+ }
515
+ }
@@ -0,0 +1,53 @@
1
+ // Range utility functions
2
+
3
+ import type { Range } from '../types';
4
+
5
+ export function normalizeRange(range: Range): Range {
6
+ return {
7
+ startRow: Math.min(range.startRow, range.endRow),
8
+ startCol: Math.min(range.startCol, range.endCol),
9
+ endRow: Math.max(range.startRow, range.endRow),
10
+ endCol: Math.max(range.startCol, range.endCol),
11
+ };
12
+ }
13
+
14
+ export function rangeContains(range: Range, row: number, col: number): boolean {
15
+ const normalized = normalizeRange(range);
16
+ return (
17
+ row >= normalized.startRow &&
18
+ row <= normalized.endRow &&
19
+ col >= normalized.startCol &&
20
+ col <= normalized.endCol
21
+ );
22
+ }
23
+
24
+ export function rangeIntersects(range1: Range, range2: Range): boolean {
25
+ const r1 = normalizeRange(range1);
26
+ const r2 = normalizeRange(range2);
27
+ return !(
28
+ r1.endRow < r2.startRow ||
29
+ r1.startRow > r2.endRow ||
30
+ r1.endCol < r2.startCol ||
31
+ r1.startCol > r2.endCol
32
+ );
33
+ }
34
+
35
+ export function getRangeCells(range: Range): Array<{ row: number; col: number }> {
36
+ const normalized = normalizeRange(range);
37
+ const cells: Array<{ row: number; col: number }> = [];
38
+ for (let row = normalized.startRow; row <= normalized.endRow; row++) {
39
+ for (let col = normalized.startCol; col <= normalized.endCol; col++) {
40
+ cells.push({ row, col });
41
+ }
42
+ }
43
+ return cells;
44
+ }
45
+
46
+ export function getRangeSize(range: Range): { rows: number; cols: number } {
47
+ const normalized = normalizeRange(range);
48
+ return {
49
+ rows: normalized.endRow - normalized.startRow + 1,
50
+ cols: normalized.endCol - normalized.startCol + 1,
51
+ };
52
+ }
53
+