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