@particle-academy/fancy-sheets 0.1.2 → 0.3.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 +100 -10
- package/dist/index.cjs +785 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +785 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -123,12 +123,41 @@ function lexFormula(input) {
|
|
|
123
123
|
if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
|
|
124
124
|
const pos = i;
|
|
125
125
|
i++;
|
|
126
|
-
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_"))
|
|
127
|
-
|
|
126
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_" || input[i] === " ")) {
|
|
127
|
+
if (input[i] === " ") {
|
|
128
|
+
let lookAhead = i + 1;
|
|
129
|
+
while (lookAhead < len && input[lookAhead] === " ") lookAhead++;
|
|
130
|
+
if (lookAhead < len && (input[lookAhead] >= "A" && input[lookAhead] <= "Z" || input[lookAhead] >= "a" && input[lookAhead] <= "z" || input[lookAhead] >= "0" && input[lookAhead] <= "9" || input[lookAhead] === "!")) {
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
let word = input.slice(pos, i).trimEnd();
|
|
139
|
+
i = pos + word.length;
|
|
128
140
|
if (word.toUpperCase() === "TRUE" || word.toUpperCase() === "FALSE") {
|
|
129
141
|
tokens.push({ type: "boolean", value: word.toUpperCase(), position: pos });
|
|
130
142
|
continue;
|
|
131
143
|
}
|
|
144
|
+
if (i < len && input[i] === "!") {
|
|
145
|
+
const sheetName = word;
|
|
146
|
+
i++;
|
|
147
|
+
const refStart = i;
|
|
148
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
|
|
149
|
+
const ref1 = input.slice(refStart, i);
|
|
150
|
+
if (i < len && input[i] === ":") {
|
|
151
|
+
i++;
|
|
152
|
+
const ref2Start = i;
|
|
153
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
|
|
154
|
+
const ref2 = input.slice(ref2Start, i);
|
|
155
|
+
tokens.push({ type: "sheetRangeRef", value: sheetName + "!" + ref1 + ":" + ref2, position: pos });
|
|
156
|
+
} else {
|
|
157
|
+
tokens.push({ type: "sheetCellRef", value: sheetName + "!" + ref1.toUpperCase(), position: pos });
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
132
161
|
if (i < len && input[i] === ":") {
|
|
133
162
|
const colonPos = i;
|
|
134
163
|
i++;
|
|
@@ -272,6 +301,19 @@ function parseFormula(tokens) {
|
|
|
272
301
|
const parts = t.value.split(":");
|
|
273
302
|
return { type: "rangeRef", start: parts[0], end: parts[1] };
|
|
274
303
|
}
|
|
304
|
+
if (t.type === "sheetCellRef") {
|
|
305
|
+
advance();
|
|
306
|
+
const bangIdx = t.value.indexOf("!");
|
|
307
|
+
return { type: "sheetCellRef", sheet: t.value.slice(0, bangIdx), address: t.value.slice(bangIdx + 1).toUpperCase() };
|
|
308
|
+
}
|
|
309
|
+
if (t.type === "sheetRangeRef") {
|
|
310
|
+
advance();
|
|
311
|
+
const bangIdx = t.value.indexOf("!");
|
|
312
|
+
const sheetName = t.value.slice(0, bangIdx);
|
|
313
|
+
const rangePart = t.value.slice(bangIdx + 1);
|
|
314
|
+
const parts = rangePart.split(":");
|
|
315
|
+
return { type: "sheetRangeRef", sheet: sheetName, start: parts[0].toUpperCase(), end: parts[1].toUpperCase() };
|
|
316
|
+
}
|
|
275
317
|
if (t.type === "function") {
|
|
276
318
|
const name = advance().value;
|
|
277
319
|
expect("paren", "(");
|
|
@@ -356,6 +398,105 @@ registerFunction("ABS", (args) => {
|
|
|
356
398
|
if (isNaN(num)) return "#VALUE!";
|
|
357
399
|
return Math.abs(num);
|
|
358
400
|
});
|
|
401
|
+
function toNum(args) {
|
|
402
|
+
const val = args.flat()[0];
|
|
403
|
+
const n = typeof val === "number" ? val : Number(val);
|
|
404
|
+
return isNaN(n) ? NaN : n;
|
|
405
|
+
}
|
|
406
|
+
registerFunction("SQRT", (args) => {
|
|
407
|
+
const n = toNum(args);
|
|
408
|
+
return isNaN(n) ? "#VALUE!" : n < 0 ? "#NUM!" : Math.sqrt(n);
|
|
409
|
+
});
|
|
410
|
+
registerFunction("POWER", (args) => {
|
|
411
|
+
const flat = args.flat();
|
|
412
|
+
const base = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
413
|
+
const exp = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
414
|
+
if (isNaN(base) || isNaN(exp)) return "#VALUE!";
|
|
415
|
+
return Math.pow(base, exp);
|
|
416
|
+
});
|
|
417
|
+
registerFunction("MOD", (args) => {
|
|
418
|
+
const flat = args.flat();
|
|
419
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
420
|
+
const div = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
421
|
+
if (isNaN(num) || isNaN(div) || div === 0) return "#VALUE!";
|
|
422
|
+
return num % div;
|
|
423
|
+
});
|
|
424
|
+
registerFunction("INT", (args) => {
|
|
425
|
+
const n = toNum(args);
|
|
426
|
+
return isNaN(n) ? "#VALUE!" : Math.floor(n);
|
|
427
|
+
});
|
|
428
|
+
registerFunction("TRUNC", (args) => {
|
|
429
|
+
const flat = args.flat();
|
|
430
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
431
|
+
const decimals = flat[1] != null ? typeof flat[1] === "number" ? flat[1] : Number(flat[1]) : 0;
|
|
432
|
+
if (isNaN(num)) return "#VALUE!";
|
|
433
|
+
const factor = Math.pow(10, decimals);
|
|
434
|
+
return Math.trunc(num * factor) / factor;
|
|
435
|
+
});
|
|
436
|
+
registerFunction("FLOOR", (args) => {
|
|
437
|
+
const flat = args.flat();
|
|
438
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
439
|
+
const sig = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
440
|
+
if (isNaN(num) || isNaN(sig) || sig === 0) return "#VALUE!";
|
|
441
|
+
return Math.floor(num / sig) * sig;
|
|
442
|
+
});
|
|
443
|
+
registerFunction("CEILING", (args) => {
|
|
444
|
+
const flat = args.flat();
|
|
445
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
446
|
+
const sig = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
447
|
+
if (isNaN(num) || isNaN(sig) || sig === 0) return "#VALUE!";
|
|
448
|
+
return Math.ceil(num / sig) * sig;
|
|
449
|
+
});
|
|
450
|
+
registerFunction("SIGN", (args) => {
|
|
451
|
+
const n = toNum(args);
|
|
452
|
+
return isNaN(n) ? "#VALUE!" : Math.sign(n);
|
|
453
|
+
});
|
|
454
|
+
registerFunction("PRODUCT", (args) => {
|
|
455
|
+
const nums = toNumbers(args);
|
|
456
|
+
if (nums.length === 0) return 0;
|
|
457
|
+
return nums.reduce((a, b) => a * b, 1);
|
|
458
|
+
});
|
|
459
|
+
registerFunction("PI", () => Math.PI);
|
|
460
|
+
registerFunction("EXP", (args) => {
|
|
461
|
+
const n = toNum(args);
|
|
462
|
+
return isNaN(n) ? "#VALUE!" : Math.exp(n);
|
|
463
|
+
});
|
|
464
|
+
registerFunction("LN", (args) => {
|
|
465
|
+
const n = toNum(args);
|
|
466
|
+
return isNaN(n) || n <= 0 ? "#NUM!" : Math.log(n);
|
|
467
|
+
});
|
|
468
|
+
registerFunction("LOG", (args) => {
|
|
469
|
+
const flat = args.flat();
|
|
470
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
471
|
+
const base = flat[1] != null ? typeof flat[1] === "number" ? flat[1] : Number(flat[1]) : 10;
|
|
472
|
+
if (isNaN(num) || num <= 0 || isNaN(base) || base <= 0 || base === 1) return "#NUM!";
|
|
473
|
+
return Math.log(num) / Math.log(base);
|
|
474
|
+
});
|
|
475
|
+
registerFunction("LOG10", (args) => {
|
|
476
|
+
const n = toNum(args);
|
|
477
|
+
return isNaN(n) || n <= 0 ? "#NUM!" : Math.log10(n);
|
|
478
|
+
});
|
|
479
|
+
registerFunction("RAND", () => Math.random());
|
|
480
|
+
registerFunction("RANDBETWEEN", (args) => {
|
|
481
|
+
const flat = args.flat();
|
|
482
|
+
const low = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
483
|
+
const high = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
484
|
+
if (isNaN(low) || isNaN(high)) return "#VALUE!";
|
|
485
|
+
return Math.floor(Math.random() * (high - low + 1)) + low;
|
|
486
|
+
});
|
|
487
|
+
registerFunction("MEDIAN", (args) => {
|
|
488
|
+
const nums = toNumbers(args).sort((a, b) => a - b);
|
|
489
|
+
if (nums.length === 0) return 0;
|
|
490
|
+
const mid = Math.floor(nums.length / 2);
|
|
491
|
+
return nums.length % 2 === 0 ? (nums[mid - 1] + nums[mid]) / 2 : nums[mid];
|
|
492
|
+
});
|
|
493
|
+
registerFunction("FACT", (args) => {
|
|
494
|
+
const n = toNum(args);
|
|
495
|
+
if (isNaN(n) || n < 0 || n !== Math.floor(n)) return "#VALUE!";
|
|
496
|
+
let result = 1;
|
|
497
|
+
for (let i = 2; i <= n; i++) result *= i;
|
|
498
|
+
return result;
|
|
499
|
+
});
|
|
359
500
|
|
|
360
501
|
// src/engine/formula/functions/text.ts
|
|
361
502
|
registerFunction("UPPER", (args) => {
|
|
@@ -377,6 +518,108 @@ registerFunction("TRIM", (args) => {
|
|
|
377
518
|
registerFunction("CONCAT", (args) => {
|
|
378
519
|
return args.flat().map((v) => v != null ? String(v) : "").join("");
|
|
379
520
|
});
|
|
521
|
+
registerFunction("LEFT", (args) => {
|
|
522
|
+
const flat = args.flat();
|
|
523
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
524
|
+
const chars = flat[1] != null ? Number(flat[1]) : 1;
|
|
525
|
+
return text.slice(0, chars);
|
|
526
|
+
});
|
|
527
|
+
registerFunction("RIGHT", (args) => {
|
|
528
|
+
const flat = args.flat();
|
|
529
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
530
|
+
const chars = flat[1] != null ? Number(flat[1]) : 1;
|
|
531
|
+
return text.slice(-chars);
|
|
532
|
+
});
|
|
533
|
+
registerFunction("MID", (args) => {
|
|
534
|
+
const flat = args.flat();
|
|
535
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
536
|
+
const start = Number(flat[1]) - 1;
|
|
537
|
+
const chars = Number(flat[2]);
|
|
538
|
+
if (isNaN(start) || isNaN(chars)) return "#VALUE!";
|
|
539
|
+
return text.slice(start, start + chars);
|
|
540
|
+
});
|
|
541
|
+
registerFunction("FIND", (args) => {
|
|
542
|
+
const flat = args.flat();
|
|
543
|
+
const search = flat[0] != null ? String(flat[0]) : "";
|
|
544
|
+
const text = flat[1] != null ? String(flat[1]) : "";
|
|
545
|
+
const startPos = flat[2] != null ? Number(flat[2]) - 1 : 0;
|
|
546
|
+
const idx = text.indexOf(search, startPos);
|
|
547
|
+
return idx === -1 ? "#VALUE!" : idx + 1;
|
|
548
|
+
});
|
|
549
|
+
registerFunction("SEARCH", (args) => {
|
|
550
|
+
const flat = args.flat();
|
|
551
|
+
const search = flat[0] != null ? String(flat[0]).toLowerCase() : "";
|
|
552
|
+
const text = flat[1] != null ? String(flat[1]).toLowerCase() : "";
|
|
553
|
+
const startPos = flat[2] != null ? Number(flat[2]) - 1 : 0;
|
|
554
|
+
const idx = text.indexOf(search, startPos);
|
|
555
|
+
return idx === -1 ? "#VALUE!" : idx + 1;
|
|
556
|
+
});
|
|
557
|
+
registerFunction("SUBSTITUTE", (args) => {
|
|
558
|
+
const flat = args.flat();
|
|
559
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
560
|
+
const oldText = flat[1] != null ? String(flat[1]) : "";
|
|
561
|
+
const newText = flat[2] != null ? String(flat[2]) : "";
|
|
562
|
+
const nth = flat[3] != null ? Number(flat[3]) : 0;
|
|
563
|
+
if (nth === 0) return text.split(oldText).join(newText);
|
|
564
|
+
let count = 0;
|
|
565
|
+
return text.replace(new RegExp(oldText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), (match) => {
|
|
566
|
+
count++;
|
|
567
|
+
return count === nth ? newText : match;
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
registerFunction("REPLACE", (args) => {
|
|
571
|
+
const flat = args.flat();
|
|
572
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
573
|
+
const start = Number(flat[1]) - 1;
|
|
574
|
+
const chars = Number(flat[2]);
|
|
575
|
+
const newText = flat[3] != null ? String(flat[3]) : "";
|
|
576
|
+
if (isNaN(start) || isNaN(chars)) return "#VALUE!";
|
|
577
|
+
return text.slice(0, start) + newText + text.slice(start + chars);
|
|
578
|
+
});
|
|
579
|
+
registerFunction("REPT", (args) => {
|
|
580
|
+
const flat = args.flat();
|
|
581
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
582
|
+
const times = Number(flat[1]);
|
|
583
|
+
if (isNaN(times) || times < 0) return "#VALUE!";
|
|
584
|
+
return text.repeat(times);
|
|
585
|
+
});
|
|
586
|
+
registerFunction("EXACT", (args) => {
|
|
587
|
+
const flat = args.flat();
|
|
588
|
+
return String(flat[0] ?? "") === String(flat[1] ?? "");
|
|
589
|
+
});
|
|
590
|
+
registerFunction("PROPER", (args) => {
|
|
591
|
+
const val = args.flat()[0];
|
|
592
|
+
const text = val != null ? String(val) : "";
|
|
593
|
+
return text.replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
|
|
594
|
+
});
|
|
595
|
+
registerFunction("VALUE", (args) => {
|
|
596
|
+
const val = args.flat()[0];
|
|
597
|
+
const num = Number(val);
|
|
598
|
+
return isNaN(num) ? "#VALUE!" : num;
|
|
599
|
+
});
|
|
600
|
+
registerFunction("TEXT", (args) => {
|
|
601
|
+
const flat = args.flat();
|
|
602
|
+
const val = flat[0];
|
|
603
|
+
const fmt = flat[1] != null ? String(flat[1]) : "";
|
|
604
|
+
if (val == null) return "";
|
|
605
|
+
const num = Number(val);
|
|
606
|
+
if (isNaN(num)) return String(val);
|
|
607
|
+
if (fmt.includes("%")) return (num * 100).toFixed(fmt.split(".")[1]?.length ?? 0) + "%";
|
|
608
|
+
if (fmt.includes(".")) {
|
|
609
|
+
const decimals = fmt.split(".")[1]?.replace(/[^0#]/g, "").length ?? 0;
|
|
610
|
+
return num.toFixed(decimals);
|
|
611
|
+
}
|
|
612
|
+
return String(num);
|
|
613
|
+
});
|
|
614
|
+
registerFunction("CHAR", (args) => {
|
|
615
|
+
const code = Number(args.flat()[0]);
|
|
616
|
+
if (isNaN(code)) return "#VALUE!";
|
|
617
|
+
return String.fromCharCode(code);
|
|
618
|
+
});
|
|
619
|
+
registerFunction("CODE", (args) => {
|
|
620
|
+
const text = String(args.flat()[0] ?? "");
|
|
621
|
+
return text.length > 0 ? text.charCodeAt(0) : "#VALUE!";
|
|
622
|
+
});
|
|
380
623
|
|
|
381
624
|
// src/engine/formula/functions/logic.ts
|
|
382
625
|
function toBool(v) {
|
|
@@ -401,9 +644,410 @@ registerFunction("OR", (args) => {
|
|
|
401
644
|
registerFunction("NOT", (args) => {
|
|
402
645
|
return !toBool(args.flat()[0]);
|
|
403
646
|
});
|
|
647
|
+
registerFunction("IFERROR", (args) => {
|
|
648
|
+
const flat = args.flat();
|
|
649
|
+
const val = flat[0];
|
|
650
|
+
if (typeof val === "string" && val.startsWith("#")) return flat[1] ?? "";
|
|
651
|
+
return val;
|
|
652
|
+
});
|
|
653
|
+
registerFunction("IFBLANK", (args) => {
|
|
654
|
+
const flat = args.flat();
|
|
655
|
+
const val = flat[0];
|
|
656
|
+
if (val === null || val === void 0 || val === "") return flat[1] ?? "";
|
|
657
|
+
return val;
|
|
658
|
+
});
|
|
659
|
+
registerFunction("SWITCH", (args) => {
|
|
660
|
+
const flat = args.flat();
|
|
661
|
+
const expr = flat[0];
|
|
662
|
+
for (let i = 1; i < flat.length - 1; i += 2) {
|
|
663
|
+
if (flat[i] === expr) return flat[i + 1];
|
|
664
|
+
}
|
|
665
|
+
return flat.length % 2 === 0 ? flat[flat.length - 1] : "#N/A";
|
|
666
|
+
});
|
|
667
|
+
registerFunction("CHOOSE", (args) => {
|
|
668
|
+
const flat = args.flat();
|
|
669
|
+
const index = Number(flat[0]);
|
|
670
|
+
if (isNaN(index) || index < 1 || index >= flat.length) return "#VALUE!";
|
|
671
|
+
return flat[index];
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// src/engine/formula/functions/conditional.ts
|
|
675
|
+
function matchesCriteria(value, criteria) {
|
|
676
|
+
if (criteria === null || criteria === void 0) return false;
|
|
677
|
+
const critStr = String(criteria);
|
|
678
|
+
if (critStr.startsWith("<>")) {
|
|
679
|
+
const target = critStr.slice(2);
|
|
680
|
+
const num2 = Number(target);
|
|
681
|
+
if (!isNaN(num2) && typeof value === "number") return value !== num2;
|
|
682
|
+
return String(value) !== target;
|
|
683
|
+
}
|
|
684
|
+
if (critStr.startsWith(">=")) {
|
|
685
|
+
const num2 = Number(critStr.slice(2));
|
|
686
|
+
return typeof value === "number" && value >= num2;
|
|
687
|
+
}
|
|
688
|
+
if (critStr.startsWith("<=")) {
|
|
689
|
+
const num2 = Number(critStr.slice(2));
|
|
690
|
+
return typeof value === "number" && value <= num2;
|
|
691
|
+
}
|
|
692
|
+
if (critStr.startsWith(">")) {
|
|
693
|
+
const num2 = Number(critStr.slice(1));
|
|
694
|
+
return typeof value === "number" && value > num2;
|
|
695
|
+
}
|
|
696
|
+
if (critStr.startsWith("<")) {
|
|
697
|
+
const num2 = Number(critStr.slice(1));
|
|
698
|
+
return typeof value === "number" && value < num2;
|
|
699
|
+
}
|
|
700
|
+
if (critStr.startsWith("=")) {
|
|
701
|
+
const target = critStr.slice(1);
|
|
702
|
+
const num2 = Number(target);
|
|
703
|
+
if (!isNaN(num2) && typeof value === "number") return value === num2;
|
|
704
|
+
return String(value).toLowerCase() === target.toLowerCase();
|
|
705
|
+
}
|
|
706
|
+
if (critStr.includes("*") || critStr.includes("?")) {
|
|
707
|
+
const pattern = critStr.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
708
|
+
return new RegExp("^" + pattern + "$", "i").test(String(value));
|
|
709
|
+
}
|
|
710
|
+
if (typeof criteria === "number") return value === criteria;
|
|
711
|
+
const num = Number(critStr);
|
|
712
|
+
if (!isNaN(num) && typeof value === "number") return value === num;
|
|
713
|
+
return String(value).toLowerCase() === critStr.toLowerCase();
|
|
714
|
+
}
|
|
715
|
+
registerFunction("SUMIF", (args) => {
|
|
716
|
+
const range = args[0] ?? [];
|
|
717
|
+
const criteria = (args[1] ?? [])[0];
|
|
718
|
+
const sumRange = args[2] ?? range;
|
|
719
|
+
let total = 0;
|
|
720
|
+
for (let i = 0; i < range.length; i++) {
|
|
721
|
+
if (matchesCriteria(range[i], criteria)) {
|
|
722
|
+
const val = Number(sumRange[i] ?? 0);
|
|
723
|
+
if (!isNaN(val)) total += val;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return total;
|
|
727
|
+
});
|
|
728
|
+
registerFunction("SUMIFS", (args) => {
|
|
729
|
+
const sumRange = args[0] ?? [];
|
|
730
|
+
let total = 0;
|
|
731
|
+
for (let i = 0; i < sumRange.length; i++) {
|
|
732
|
+
let allMatch = true;
|
|
733
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
734
|
+
const criteriaRange = args[p] ?? [];
|
|
735
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
736
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
737
|
+
allMatch = false;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (allMatch) {
|
|
742
|
+
const val = Number(sumRange[i] ?? 0);
|
|
743
|
+
if (!isNaN(val)) total += val;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return total;
|
|
747
|
+
});
|
|
748
|
+
registerFunction("COUNTIF", (args) => {
|
|
749
|
+
const range = args[0] ?? [];
|
|
750
|
+
const criteria = (args[1] ?? [])[0];
|
|
751
|
+
let count = 0;
|
|
752
|
+
for (const val of range) {
|
|
753
|
+
if (matchesCriteria(val, criteria)) count++;
|
|
754
|
+
}
|
|
755
|
+
return count;
|
|
756
|
+
});
|
|
757
|
+
registerFunction("COUNTIFS", (args) => {
|
|
758
|
+
if (args.length < 2) return 0;
|
|
759
|
+
const len = (args[0] ?? []).length;
|
|
760
|
+
let count = 0;
|
|
761
|
+
for (let i = 0; i < len; i++) {
|
|
762
|
+
let allMatch = true;
|
|
763
|
+
for (let p = 0; p < args.length - 1; p += 2) {
|
|
764
|
+
const range = args[p] ?? [];
|
|
765
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
766
|
+
if (!matchesCriteria(range[i], criteria)) {
|
|
767
|
+
allMatch = false;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (allMatch) count++;
|
|
772
|
+
}
|
|
773
|
+
return count;
|
|
774
|
+
});
|
|
775
|
+
registerFunction("AVERAGEIF", (args) => {
|
|
776
|
+
const range = args[0] ?? [];
|
|
777
|
+
const criteria = (args[1] ?? [])[0];
|
|
778
|
+
const avgRange = args[2] ?? range;
|
|
779
|
+
let total = 0;
|
|
780
|
+
let count = 0;
|
|
781
|
+
for (let i = 0; i < range.length; i++) {
|
|
782
|
+
if (matchesCriteria(range[i], criteria)) {
|
|
783
|
+
const val = Number(avgRange[i] ?? 0);
|
|
784
|
+
if (!isNaN(val)) {
|
|
785
|
+
total += val;
|
|
786
|
+
count++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return count === 0 ? "#DIV/0!" : total / count;
|
|
791
|
+
});
|
|
792
|
+
registerFunction("AVERAGEIFS", (args) => {
|
|
793
|
+
const avgRange = args[0] ?? [];
|
|
794
|
+
let total = 0;
|
|
795
|
+
let count = 0;
|
|
796
|
+
for (let i = 0; i < avgRange.length; i++) {
|
|
797
|
+
let allMatch = true;
|
|
798
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
799
|
+
const criteriaRange = args[p] ?? [];
|
|
800
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
801
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
802
|
+
allMatch = false;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (allMatch) {
|
|
807
|
+
const val = Number(avgRange[i] ?? 0);
|
|
808
|
+
if (!isNaN(val)) {
|
|
809
|
+
total += val;
|
|
810
|
+
count++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return count === 0 ? "#DIV/0!" : total / count;
|
|
815
|
+
});
|
|
816
|
+
registerFunction("MINIFS", (args) => {
|
|
817
|
+
const minRange = args[0] ?? [];
|
|
818
|
+
let result = Infinity;
|
|
819
|
+
for (let i = 0; i < minRange.length; i++) {
|
|
820
|
+
let allMatch = true;
|
|
821
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
822
|
+
const criteriaRange = args[p] ?? [];
|
|
823
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
824
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
825
|
+
allMatch = false;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (allMatch) {
|
|
830
|
+
const val = Number(minRange[i]);
|
|
831
|
+
if (!isNaN(val) && val < result) result = val;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return result === Infinity ? 0 : result;
|
|
835
|
+
});
|
|
836
|
+
registerFunction("MAXIFS", (args) => {
|
|
837
|
+
const maxRange = args[0] ?? [];
|
|
838
|
+
let result = -Infinity;
|
|
839
|
+
for (let i = 0; i < maxRange.length; i++) {
|
|
840
|
+
let allMatch = true;
|
|
841
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
842
|
+
const criteriaRange = args[p] ?? [];
|
|
843
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
844
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
845
|
+
allMatch = false;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (allMatch) {
|
|
850
|
+
const val = Number(maxRange[i]);
|
|
851
|
+
if (!isNaN(val) && val > result) result = val;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return result === -Infinity ? 0 : result;
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// src/engine/formula/functions/lookup.ts
|
|
858
|
+
registerFunction("VLOOKUP", (args) => {
|
|
859
|
+
const flat = args.flat();
|
|
860
|
+
const key = flat[0];
|
|
861
|
+
const range = args[1] ?? [];
|
|
862
|
+
const colIdx = Number(flat[2] ?? flat[args[1]?.length ?? 0]);
|
|
863
|
+
if (key === null || isNaN(colIdx)) return "#VALUE!";
|
|
864
|
+
for (let i = 0; i < range.length; i++) {
|
|
865
|
+
if (range[i] === key || typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
866
|
+
return range[i] ?? "#N/A";
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return "#N/A";
|
|
870
|
+
});
|
|
871
|
+
registerFunction("HLOOKUP", (args) => {
|
|
872
|
+
const flat = args.flat();
|
|
873
|
+
const key = flat[0];
|
|
874
|
+
const range = args[1] ?? [];
|
|
875
|
+
for (let i = 0; i < range.length; i++) {
|
|
876
|
+
if (range[i] === key || typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
877
|
+
return range[i] ?? "#N/A";
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return "#N/A";
|
|
881
|
+
});
|
|
882
|
+
registerFunction("INDEX", (args) => {
|
|
883
|
+
const range = args[0] ?? [];
|
|
884
|
+
const flat = args.flat();
|
|
885
|
+
const row = Number(flat[range.length] ?? flat[1]);
|
|
886
|
+
if (isNaN(row) || row < 1 || row > range.length) return "#REF!";
|
|
887
|
+
return range[row - 1] ?? null;
|
|
888
|
+
});
|
|
889
|
+
registerFunction("MATCH", (args) => {
|
|
890
|
+
const flat = args.flat();
|
|
891
|
+
const key = flat[0];
|
|
892
|
+
const range = args[1] ?? [];
|
|
893
|
+
for (let i = 0; i < range.length; i++) {
|
|
894
|
+
if (range[i] === key || typeof range[i] === "number" && typeof key === "number" && range[i] === key) {
|
|
895
|
+
return i + 1;
|
|
896
|
+
}
|
|
897
|
+
if (typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
898
|
+
return i + 1;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return "#N/A";
|
|
902
|
+
});
|
|
903
|
+
registerFunction("ROWS", (args) => {
|
|
904
|
+
return (args[0] ?? []).length;
|
|
905
|
+
});
|
|
906
|
+
registerFunction("COLUMNS", (args) => {
|
|
907
|
+
return 1;
|
|
908
|
+
});
|
|
909
|
+
registerFunction("ROW", (args) => {
|
|
910
|
+
const val = args.flat()[0];
|
|
911
|
+
if (typeof val === "number") return val;
|
|
912
|
+
return "#VALUE!";
|
|
913
|
+
});
|
|
914
|
+
registerFunction("COLUMN", (args) => {
|
|
915
|
+
const val = args.flat()[0];
|
|
916
|
+
if (typeof val === "number") return val;
|
|
917
|
+
return "#VALUE!";
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// src/engine/formula/functions/datetime.ts
|
|
921
|
+
var EXCEL_EPOCH = new Date(1899, 11, 30).getTime();
|
|
922
|
+
function dateToSerial(d) {
|
|
923
|
+
const ms = d.getTime() - EXCEL_EPOCH;
|
|
924
|
+
return Math.floor(ms / 864e5);
|
|
925
|
+
}
|
|
926
|
+
function serialToDate(serial) {
|
|
927
|
+
return new Date(EXCEL_EPOCH + serial * 864e5);
|
|
928
|
+
}
|
|
929
|
+
function toSerial(val) {
|
|
930
|
+
if (typeof val === "number") return val;
|
|
931
|
+
if (typeof val === "string") {
|
|
932
|
+
const d = new Date(val);
|
|
933
|
+
if (!isNaN(d.getTime())) return dateToSerial(d);
|
|
934
|
+
}
|
|
935
|
+
return NaN;
|
|
936
|
+
}
|
|
937
|
+
registerFunction("TODAY", () => {
|
|
938
|
+
return dateToSerial(/* @__PURE__ */ new Date());
|
|
939
|
+
});
|
|
940
|
+
registerFunction("NOW", () => {
|
|
941
|
+
const now = /* @__PURE__ */ new Date();
|
|
942
|
+
const serial = dateToSerial(now);
|
|
943
|
+
const fraction = (now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()) / 86400;
|
|
944
|
+
return serial + fraction;
|
|
945
|
+
});
|
|
946
|
+
registerFunction("DATE", (args) => {
|
|
947
|
+
const flat = args.flat();
|
|
948
|
+
const year = Number(flat[0]);
|
|
949
|
+
const month = Number(flat[1]) - 1;
|
|
950
|
+
const day = Number(flat[2]);
|
|
951
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return "#VALUE!";
|
|
952
|
+
return dateToSerial(new Date(year, month, day));
|
|
953
|
+
});
|
|
954
|
+
registerFunction("YEAR", (args) => {
|
|
955
|
+
const serial = toSerial(args.flat()[0]);
|
|
956
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
957
|
+
return serialToDate(serial).getFullYear();
|
|
958
|
+
});
|
|
959
|
+
registerFunction("MONTH", (args) => {
|
|
960
|
+
const serial = toSerial(args.flat()[0]);
|
|
961
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
962
|
+
return serialToDate(serial).getMonth() + 1;
|
|
963
|
+
});
|
|
964
|
+
registerFunction("DAY", (args) => {
|
|
965
|
+
const serial = toSerial(args.flat()[0]);
|
|
966
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
967
|
+
return serialToDate(serial).getDate();
|
|
968
|
+
});
|
|
969
|
+
registerFunction("HOUR", (args) => {
|
|
970
|
+
const val = Number(args.flat()[0]);
|
|
971
|
+
if (isNaN(val)) return "#VALUE!";
|
|
972
|
+
const fraction = val % 1;
|
|
973
|
+
return Math.floor(fraction * 24);
|
|
974
|
+
});
|
|
975
|
+
registerFunction("MINUTE", (args) => {
|
|
976
|
+
const val = Number(args.flat()[0]);
|
|
977
|
+
if (isNaN(val)) return "#VALUE!";
|
|
978
|
+
const fraction = val % 1;
|
|
979
|
+
return Math.floor(fraction * 24 * 60 % 60);
|
|
980
|
+
});
|
|
981
|
+
registerFunction("SECOND", (args) => {
|
|
982
|
+
const val = Number(args.flat()[0]);
|
|
983
|
+
if (isNaN(val)) return "#VALUE!";
|
|
984
|
+
const fraction = val % 1;
|
|
985
|
+
return Math.floor(fraction * 24 * 3600 % 60);
|
|
986
|
+
});
|
|
987
|
+
registerFunction("WEEKDAY", (args) => {
|
|
988
|
+
const flat = args.flat();
|
|
989
|
+
const serial = toSerial(flat[0]);
|
|
990
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
991
|
+
const day = serialToDate(serial).getDay();
|
|
992
|
+
const type = Number(flat[1] ?? 1);
|
|
993
|
+
if (type === 1) return day + 1;
|
|
994
|
+
if (type === 2) return day === 0 ? 7 : day;
|
|
995
|
+
return day;
|
|
996
|
+
});
|
|
997
|
+
registerFunction("DATEDIF", (args) => {
|
|
998
|
+
const flat = args.flat();
|
|
999
|
+
const startSerial = toSerial(flat[0]);
|
|
1000
|
+
const endSerial = toSerial(flat[1]);
|
|
1001
|
+
const unit = String(flat[2] ?? "D").toUpperCase();
|
|
1002
|
+
if (isNaN(startSerial) || isNaN(endSerial)) return "#VALUE!";
|
|
1003
|
+
const startDate = serialToDate(startSerial);
|
|
1004
|
+
const endDate = serialToDate(endSerial);
|
|
1005
|
+
if (unit === "D") return Math.floor(endSerial - startSerial);
|
|
1006
|
+
if (unit === "M") {
|
|
1007
|
+
return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
|
|
1008
|
+
}
|
|
1009
|
+
if (unit === "Y") return endDate.getFullYear() - startDate.getFullYear();
|
|
1010
|
+
return "#VALUE!";
|
|
1011
|
+
});
|
|
1012
|
+
registerFunction("EDATE", (args) => {
|
|
1013
|
+
const flat = args.flat();
|
|
1014
|
+
const serial = toSerial(flat[0]);
|
|
1015
|
+
const months = Number(flat[1]);
|
|
1016
|
+
if (isNaN(serial) || isNaN(months)) return "#VALUE!";
|
|
1017
|
+
const d = serialToDate(serial);
|
|
1018
|
+
d.setMonth(d.getMonth() + months);
|
|
1019
|
+
return dateToSerial(d);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// src/engine/formula/functions/info.ts
|
|
1023
|
+
registerFunction("ISBLANK", (args) => {
|
|
1024
|
+
const val = args.flat()[0];
|
|
1025
|
+
return val === null || val === void 0 || val === "";
|
|
1026
|
+
});
|
|
1027
|
+
registerFunction("ISNUMBER", (args) => {
|
|
1028
|
+
return typeof args.flat()[0] === "number";
|
|
1029
|
+
});
|
|
1030
|
+
registerFunction("ISTEXT", (args) => {
|
|
1031
|
+
const val = args.flat()[0];
|
|
1032
|
+
return typeof val === "string" && !val.startsWith("#");
|
|
1033
|
+
});
|
|
1034
|
+
registerFunction("ISERROR", (args) => {
|
|
1035
|
+
const val = args.flat()[0];
|
|
1036
|
+
return typeof val === "string" && val.startsWith("#");
|
|
1037
|
+
});
|
|
1038
|
+
registerFunction("ISLOGICAL", (args) => {
|
|
1039
|
+
return typeof args.flat()[0] === "boolean";
|
|
1040
|
+
});
|
|
1041
|
+
registerFunction("TYPE", (args) => {
|
|
1042
|
+
const val = args.flat()[0];
|
|
1043
|
+
if (typeof val === "number") return 1;
|
|
1044
|
+
if (typeof val === "string") return val.startsWith("#") ? 16 : 2;
|
|
1045
|
+
if (typeof val === "boolean") return 4;
|
|
1046
|
+
return 1;
|
|
1047
|
+
});
|
|
404
1048
|
|
|
405
1049
|
// src/engine/formula/evaluator.ts
|
|
406
|
-
function evaluateAST(node, getCellValue, getRangeValues) {
|
|
1050
|
+
function evaluateAST(node, getCellValue, getRangeValues, ctx) {
|
|
407
1051
|
switch (node.type) {
|
|
408
1052
|
case "number":
|
|
409
1053
|
return node.value;
|
|
@@ -413,9 +1057,19 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
413
1057
|
return node.value;
|
|
414
1058
|
case "cellRef":
|
|
415
1059
|
return getCellValue(node.address);
|
|
416
|
-
case "rangeRef":
|
|
1060
|
+
case "rangeRef": {
|
|
417
1061
|
const vals = getRangeValues(node.start, node.end);
|
|
418
1062
|
return vals[0] ?? null;
|
|
1063
|
+
}
|
|
1064
|
+
case "sheetCellRef": {
|
|
1065
|
+
if (!ctx?.getSheetCellValue) return "#REF!";
|
|
1066
|
+
return ctx.getSheetCellValue(node.sheet, node.address);
|
|
1067
|
+
}
|
|
1068
|
+
case "sheetRangeRef": {
|
|
1069
|
+
if (!ctx?.getSheetRangeValues) return "#REF!";
|
|
1070
|
+
const vals = ctx.getSheetRangeValues(node.sheet, node.start, node.end);
|
|
1071
|
+
return vals[0] ?? null;
|
|
1072
|
+
}
|
|
419
1073
|
case "functionCall": {
|
|
420
1074
|
const entry = getFunction(node.name);
|
|
421
1075
|
if (!entry) return `#NAME?`;
|
|
@@ -423,7 +1077,11 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
423
1077
|
if (arg.type === "rangeRef") {
|
|
424
1078
|
return getRangeValues(arg.start, arg.end);
|
|
425
1079
|
}
|
|
426
|
-
|
|
1080
|
+
if (arg.type === "sheetRangeRef") {
|
|
1081
|
+
if (!ctx?.getSheetRangeValues) return ["#REF!"];
|
|
1082
|
+
return ctx.getSheetRangeValues(arg.sheet, arg.start, arg.end);
|
|
1083
|
+
}
|
|
1084
|
+
const val = evaluateAST(arg, getCellValue, getRangeValues, ctx);
|
|
427
1085
|
return [val];
|
|
428
1086
|
});
|
|
429
1087
|
try {
|
|
@@ -433,8 +1091,8 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
433
1091
|
}
|
|
434
1092
|
}
|
|
435
1093
|
case "binaryOp": {
|
|
436
|
-
const left = evaluateAST(node.left, getCellValue, getRangeValues);
|
|
437
|
-
const right = evaluateAST(node.right, getCellValue, getRangeValues);
|
|
1094
|
+
const left = evaluateAST(node.left, getCellValue, getRangeValues, ctx);
|
|
1095
|
+
const right = evaluateAST(node.right, getCellValue, getRangeValues, ctx);
|
|
438
1096
|
const lNum = typeof left === "number" ? left : Number(left);
|
|
439
1097
|
const rNum = typeof right === "number" ? right : Number(right);
|
|
440
1098
|
switch (node.operator) {
|
|
@@ -467,7 +1125,7 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
467
1125
|
}
|
|
468
1126
|
}
|
|
469
1127
|
case "unaryOp": {
|
|
470
|
-
const operand = evaluateAST(node.operand, getCellValue, getRangeValues);
|
|
1128
|
+
const operand = evaluateAST(node.operand, getCellValue, getRangeValues, ctx);
|
|
471
1129
|
const num = typeof operand === "number" ? operand : Number(operand);
|
|
472
1130
|
if (isNaN(num)) return "#VALUE!";
|
|
473
1131
|
return node.operator === "-" ? -num : num;
|
|
@@ -581,7 +1239,7 @@ function getRecalculationOrder(graph) {
|
|
|
581
1239
|
function recalculateWorkbook(workbook) {
|
|
582
1240
|
return {
|
|
583
1241
|
...workbook,
|
|
584
|
-
sheets: workbook.sheets.map(recalculateSheet)
|
|
1242
|
+
sheets: workbook.sheets.map((s) => recalculateSheet(s, workbook.sheets))
|
|
585
1243
|
};
|
|
586
1244
|
}
|
|
587
1245
|
function createInitialState(data) {
|
|
@@ -611,7 +1269,7 @@ function pushUndo(state) {
|
|
|
611
1269
|
if (stack.length > 50) stack.shift();
|
|
612
1270
|
return { undoStack: stack, redoStack: [] };
|
|
613
1271
|
}
|
|
614
|
-
function recalculateSheet(sheet) {
|
|
1272
|
+
function recalculateSheet(sheet, allSheets) {
|
|
615
1273
|
const graph = buildDependencyGraph(sheet.cells);
|
|
616
1274
|
if (graph.size === 0) return sheet;
|
|
617
1275
|
const circular = detectCircularRefs(graph);
|
|
@@ -627,6 +1285,28 @@ function recalculateSheet(sheet) {
|
|
|
627
1285
|
const addresses = expandRange(startAddr, endAddr);
|
|
628
1286
|
return addresses.map(getCellValue);
|
|
629
1287
|
};
|
|
1288
|
+
const getSheetCellValue = (sheetName, addr) => {
|
|
1289
|
+
if (!allSheets) return "#REF!";
|
|
1290
|
+
const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
|
|
1291
|
+
if (!target) return "#REF!";
|
|
1292
|
+
const c = target.cells[addr];
|
|
1293
|
+
if (!c) return null;
|
|
1294
|
+
if (c.formula && c.computedValue !== void 0) return c.computedValue;
|
|
1295
|
+
return c.value;
|
|
1296
|
+
};
|
|
1297
|
+
const getSheetRangeValues = (sheetName, startAddr, endAddr) => {
|
|
1298
|
+
if (!allSheets) return [];
|
|
1299
|
+
const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
|
|
1300
|
+
if (!target) return [];
|
|
1301
|
+
const addresses = expandRange(startAddr, endAddr);
|
|
1302
|
+
return addresses.map((a) => {
|
|
1303
|
+
const c = target.cells[a];
|
|
1304
|
+
if (!c) return null;
|
|
1305
|
+
if (c.formula && c.computedValue !== void 0) return c.computedValue;
|
|
1306
|
+
return c.value;
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
const ctx = { getSheetCellValue, getSheetRangeValues };
|
|
630
1310
|
for (const addr of order) {
|
|
631
1311
|
const cell = cells[addr];
|
|
632
1312
|
if (!cell?.formula) continue;
|
|
@@ -637,7 +1317,7 @@ function recalculateSheet(sheet) {
|
|
|
637
1317
|
try {
|
|
638
1318
|
const tokens = lexFormula(cell.formula);
|
|
639
1319
|
const ast = parseFormula(tokens);
|
|
640
|
-
const result = evaluateAST(ast, getCellValue, getRangeValues);
|
|
1320
|
+
const result = evaluateAST(ast, getCellValue, getRangeValues, ctx);
|
|
641
1321
|
cells[addr] = { ...cell, computedValue: result };
|
|
642
1322
|
} catch {
|
|
643
1323
|
cells[addr] = { ...cell, computedValue: "#ERROR!" };
|
|
@@ -662,7 +1342,7 @@ function reducer(state, action) {
|
|
|
662
1342
|
if (existing?.format) cellData.format = existing.format;
|
|
663
1343
|
const workbook = updateActiveSheet(state, (s) => {
|
|
664
1344
|
const updated = { ...s, cells: { ...s.cells, [action.address]: cellData } };
|
|
665
|
-
return recalculateSheet(updated);
|
|
1345
|
+
return recalculateSheet(updated, state.workbook.sheets);
|
|
666
1346
|
});
|
|
667
1347
|
return { ...state, workbook, ...history };
|
|
668
1348
|
}
|
|
@@ -818,6 +1498,20 @@ function reducer(state, action) {
|
|
|
818
1498
|
selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
|
|
819
1499
|
editingCell: null
|
|
820
1500
|
};
|
|
1501
|
+
case "SET_FROZEN_ROWS": {
|
|
1502
|
+
const workbook = updateActiveSheet(state, (s) => ({
|
|
1503
|
+
...s,
|
|
1504
|
+
frozenRows: Math.max(0, action.count)
|
|
1505
|
+
}));
|
|
1506
|
+
return { ...state, workbook };
|
|
1507
|
+
}
|
|
1508
|
+
case "SET_FROZEN_COLS": {
|
|
1509
|
+
const workbook = updateActiveSheet(state, (s) => ({
|
|
1510
|
+
...s,
|
|
1511
|
+
frozenCols: Math.max(0, action.count)
|
|
1512
|
+
}));
|
|
1513
|
+
return { ...state, workbook };
|
|
1514
|
+
}
|
|
821
1515
|
case "UNDO": {
|
|
822
1516
|
if (state.undoStack.length === 0) return state;
|
|
823
1517
|
const prev = state.undoStack[state.undoStack.length - 1];
|
|
@@ -862,6 +1556,8 @@ function useSpreadsheetStore(initialData) {
|
|
|
862
1556
|
addSheet: () => dispatch({ type: "ADD_SHEET" }),
|
|
863
1557
|
renameSheet: (sheetId, name) => dispatch({ type: "RENAME_SHEET", sheetId, name }),
|
|
864
1558
|
deleteSheet: (sheetId) => dispatch({ type: "DELETE_SHEET", sheetId }),
|
|
1559
|
+
setFrozenRows: (count) => dispatch({ type: "SET_FROZEN_ROWS", count }),
|
|
1560
|
+
setFrozenCols: (count) => dispatch({ type: "SET_FROZEN_COLS", count }),
|
|
865
1561
|
setActiveSheet: (sheetId) => dispatch({ type: "SET_ACTIVE_SHEET", sheetId }),
|
|
866
1562
|
undo: () => dispatch({ type: "UNDO" }),
|
|
867
1563
|
redo: () => dispatch({ type: "REDO" }),
|
|
@@ -950,11 +1646,49 @@ function RowHeader({ rowIndex }) {
|
|
|
950
1646
|
);
|
|
951
1647
|
}
|
|
952
1648
|
RowHeader.displayName = "RowHeader";
|
|
1649
|
+
var EXCEL_EPOCH2 = new Date(1899, 11, 30).getTime();
|
|
1650
|
+
function serialToDateStr(serial) {
|
|
1651
|
+
const d = new Date(EXCEL_EPOCH2 + Math.floor(serial) * 864e5);
|
|
1652
|
+
const y = d.getFullYear();
|
|
1653
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1654
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1655
|
+
return `${y}-${m}-${day}`;
|
|
1656
|
+
}
|
|
1657
|
+
function serialToDateTimeStr(serial) {
|
|
1658
|
+
const date = serialToDateStr(serial);
|
|
1659
|
+
const fraction = serial % 1;
|
|
1660
|
+
const totalSeconds = Math.round(fraction * 86400);
|
|
1661
|
+
const h = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
|
|
1662
|
+
const min = String(Math.floor(totalSeconds % 3600 / 60)).padStart(2, "0");
|
|
1663
|
+
const s = String(totalSeconds % 60).padStart(2, "0");
|
|
1664
|
+
return `${date} ${h}:${min}:${s}`;
|
|
1665
|
+
}
|
|
1666
|
+
function isDateFormula(formula) {
|
|
1667
|
+
if (!formula) return false;
|
|
1668
|
+
const f = formula.toUpperCase();
|
|
1669
|
+
return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
|
|
1670
|
+
}
|
|
1671
|
+
function formatCellValue(val, cell) {
|
|
1672
|
+
if (val === null || val === void 0) return "";
|
|
1673
|
+
const fmt = cell?.format?.displayFormat;
|
|
1674
|
+
if (typeof val === "number") {
|
|
1675
|
+
if (fmt === "date") return serialToDateStr(val);
|
|
1676
|
+
if (fmt === "datetime") return serialToDateTimeStr(val);
|
|
1677
|
+
if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
|
|
1678
|
+
if (fmt === "currency") return "$" + val.toFixed(2);
|
|
1679
|
+
if (fmt === "auto" || !fmt) {
|
|
1680
|
+
if (cell?.formula && isDateFormula(cell.formula)) {
|
|
1681
|
+
return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
1686
|
+
return String(val);
|
|
1687
|
+
}
|
|
953
1688
|
function getCellDisplayValue2(cell) {
|
|
954
1689
|
if (!cell) return "";
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
return String(cell.value);
|
|
1690
|
+
const val = cell.formula && cell.computedValue !== void 0 ? cell.computedValue : cell.value;
|
|
1691
|
+
return formatCellValue(val, cell);
|
|
958
1692
|
}
|
|
959
1693
|
var Cell = react.memo(function Cell2({ address, row, col }) {
|
|
960
1694
|
const {
|
|
@@ -1250,13 +1984,42 @@ function SpreadsheetGrid({ className }) {
|
|
|
1250
1984
|
children: [
|
|
1251
1985
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
|
|
1252
1986
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
1253
|
-
Array.from({ length: rowCount }, (_, rowIdx) =>
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1987
|
+
Array.from({ length: rowCount }, (_, rowIdx) => {
|
|
1988
|
+
const isFrozenRow = rowIdx < activeSheet.frozenRows;
|
|
1989
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1990
|
+
"div",
|
|
1991
|
+
{
|
|
1992
|
+
className: "flex",
|
|
1993
|
+
style: isFrozenRow ? {
|
|
1994
|
+
position: "sticky",
|
|
1995
|
+
top: rowHeight + rowIdx * rowHeight,
|
|
1996
|
+
zIndex: 8,
|
|
1997
|
+
backgroundColor: "inherit"
|
|
1998
|
+
} : void 0,
|
|
1999
|
+
children: [
|
|
2000
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
|
|
2001
|
+
Array.from({ length: columnCount }, (_2, colIdx) => {
|
|
2002
|
+
const addr = toAddress(rowIdx, colIdx);
|
|
2003
|
+
const isFrozenCol = colIdx < activeSheet.frozenCols;
|
|
2004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2005
|
+
"div",
|
|
2006
|
+
{
|
|
2007
|
+
style: isFrozenCol ? {
|
|
2008
|
+
position: "sticky",
|
|
2009
|
+
left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
|
|
2010
|
+
zIndex: isFrozenRow ? 9 : 6,
|
|
2011
|
+
backgroundColor: "inherit"
|
|
2012
|
+
} : void 0,
|
|
2013
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
|
|
2014
|
+
},
|
|
2015
|
+
addr
|
|
2016
|
+
);
|
|
2017
|
+
})
|
|
2018
|
+
]
|
|
2019
|
+
},
|
|
2020
|
+
rowIdx
|
|
2021
|
+
);
|
|
2022
|
+
}),
|
|
1260
2023
|
/* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
|
|
1261
2024
|
editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1262
2025
|
"div",
|