@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.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] === "_"))
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
954
|
-
|
|
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) =>
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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",
|