@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.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] === "_")) i++;
127
- const word = input.slice(pos, 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] === "_" || 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
- const val = evaluateAST(arg, getCellValue, getRangeValues);
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
- if (cell.formula && cell.computedValue !== void 0) return String(cell.computedValue ?? "");
956
- if (cell.value === null) return "";
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) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
1254
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
1255
- Array.from({ length: columnCount }, (_2, colIdx) => {
1256
- const addr = toAddress(rowIdx, colIdx);
1257
- return /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx }, addr);
1258
- })
1259
- ] }, rowIdx)),
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",