@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,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
|
+
|