@particle-academy/fancy-sheets 0.1.2 → 0.2.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 +602 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +602 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -356,6 +356,105 @@ registerFunction("ABS", (args) => {
|
|
|
356
356
|
if (isNaN(num)) return "#VALUE!";
|
|
357
357
|
return Math.abs(num);
|
|
358
358
|
});
|
|
359
|
+
function toNum(args) {
|
|
360
|
+
const val = args.flat()[0];
|
|
361
|
+
const n = typeof val === "number" ? val : Number(val);
|
|
362
|
+
return isNaN(n) ? NaN : n;
|
|
363
|
+
}
|
|
364
|
+
registerFunction("SQRT", (args) => {
|
|
365
|
+
const n = toNum(args);
|
|
366
|
+
return isNaN(n) ? "#VALUE!" : n < 0 ? "#NUM!" : Math.sqrt(n);
|
|
367
|
+
});
|
|
368
|
+
registerFunction("POWER", (args) => {
|
|
369
|
+
const flat = args.flat();
|
|
370
|
+
const base = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
371
|
+
const exp = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
372
|
+
if (isNaN(base) || isNaN(exp)) return "#VALUE!";
|
|
373
|
+
return Math.pow(base, exp);
|
|
374
|
+
});
|
|
375
|
+
registerFunction("MOD", (args) => {
|
|
376
|
+
const flat = args.flat();
|
|
377
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
378
|
+
const div = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
379
|
+
if (isNaN(num) || isNaN(div) || div === 0) return "#VALUE!";
|
|
380
|
+
return num % div;
|
|
381
|
+
});
|
|
382
|
+
registerFunction("INT", (args) => {
|
|
383
|
+
const n = toNum(args);
|
|
384
|
+
return isNaN(n) ? "#VALUE!" : Math.floor(n);
|
|
385
|
+
});
|
|
386
|
+
registerFunction("TRUNC", (args) => {
|
|
387
|
+
const flat = args.flat();
|
|
388
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
389
|
+
const decimals = flat[1] != null ? typeof flat[1] === "number" ? flat[1] : Number(flat[1]) : 0;
|
|
390
|
+
if (isNaN(num)) return "#VALUE!";
|
|
391
|
+
const factor = Math.pow(10, decimals);
|
|
392
|
+
return Math.trunc(num * factor) / factor;
|
|
393
|
+
});
|
|
394
|
+
registerFunction("FLOOR", (args) => {
|
|
395
|
+
const flat = args.flat();
|
|
396
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
397
|
+
const sig = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
398
|
+
if (isNaN(num) || isNaN(sig) || sig === 0) return "#VALUE!";
|
|
399
|
+
return Math.floor(num / sig) * sig;
|
|
400
|
+
});
|
|
401
|
+
registerFunction("CEILING", (args) => {
|
|
402
|
+
const flat = args.flat();
|
|
403
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
404
|
+
const sig = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
405
|
+
if (isNaN(num) || isNaN(sig) || sig === 0) return "#VALUE!";
|
|
406
|
+
return Math.ceil(num / sig) * sig;
|
|
407
|
+
});
|
|
408
|
+
registerFunction("SIGN", (args) => {
|
|
409
|
+
const n = toNum(args);
|
|
410
|
+
return isNaN(n) ? "#VALUE!" : Math.sign(n);
|
|
411
|
+
});
|
|
412
|
+
registerFunction("PRODUCT", (args) => {
|
|
413
|
+
const nums = toNumbers(args);
|
|
414
|
+
if (nums.length === 0) return 0;
|
|
415
|
+
return nums.reduce((a, b) => a * b, 1);
|
|
416
|
+
});
|
|
417
|
+
registerFunction("PI", () => Math.PI);
|
|
418
|
+
registerFunction("EXP", (args) => {
|
|
419
|
+
const n = toNum(args);
|
|
420
|
+
return isNaN(n) ? "#VALUE!" : Math.exp(n);
|
|
421
|
+
});
|
|
422
|
+
registerFunction("LN", (args) => {
|
|
423
|
+
const n = toNum(args);
|
|
424
|
+
return isNaN(n) || n <= 0 ? "#NUM!" : Math.log(n);
|
|
425
|
+
});
|
|
426
|
+
registerFunction("LOG", (args) => {
|
|
427
|
+
const flat = args.flat();
|
|
428
|
+
const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
429
|
+
const base = flat[1] != null ? typeof flat[1] === "number" ? flat[1] : Number(flat[1]) : 10;
|
|
430
|
+
if (isNaN(num) || num <= 0 || isNaN(base) || base <= 0 || base === 1) return "#NUM!";
|
|
431
|
+
return Math.log(num) / Math.log(base);
|
|
432
|
+
});
|
|
433
|
+
registerFunction("LOG10", (args) => {
|
|
434
|
+
const n = toNum(args);
|
|
435
|
+
return isNaN(n) || n <= 0 ? "#NUM!" : Math.log10(n);
|
|
436
|
+
});
|
|
437
|
+
registerFunction("RAND", () => Math.random());
|
|
438
|
+
registerFunction("RANDBETWEEN", (args) => {
|
|
439
|
+
const flat = args.flat();
|
|
440
|
+
const low = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
|
|
441
|
+
const high = typeof flat[1] === "number" ? flat[1] : Number(flat[1]);
|
|
442
|
+
if (isNaN(low) || isNaN(high)) return "#VALUE!";
|
|
443
|
+
return Math.floor(Math.random() * (high - low + 1)) + low;
|
|
444
|
+
});
|
|
445
|
+
registerFunction("MEDIAN", (args) => {
|
|
446
|
+
const nums = toNumbers(args).sort((a, b) => a - b);
|
|
447
|
+
if (nums.length === 0) return 0;
|
|
448
|
+
const mid = Math.floor(nums.length / 2);
|
|
449
|
+
return nums.length % 2 === 0 ? (nums[mid - 1] + nums[mid]) / 2 : nums[mid];
|
|
450
|
+
});
|
|
451
|
+
registerFunction("FACT", (args) => {
|
|
452
|
+
const n = toNum(args);
|
|
453
|
+
if (isNaN(n) || n < 0 || n !== Math.floor(n)) return "#VALUE!";
|
|
454
|
+
let result = 1;
|
|
455
|
+
for (let i = 2; i <= n; i++) result *= i;
|
|
456
|
+
return result;
|
|
457
|
+
});
|
|
359
458
|
|
|
360
459
|
// src/engine/formula/functions/text.ts
|
|
361
460
|
registerFunction("UPPER", (args) => {
|
|
@@ -377,6 +476,108 @@ registerFunction("TRIM", (args) => {
|
|
|
377
476
|
registerFunction("CONCAT", (args) => {
|
|
378
477
|
return args.flat().map((v) => v != null ? String(v) : "").join("");
|
|
379
478
|
});
|
|
479
|
+
registerFunction("LEFT", (args) => {
|
|
480
|
+
const flat = args.flat();
|
|
481
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
482
|
+
const chars = flat[1] != null ? Number(flat[1]) : 1;
|
|
483
|
+
return text.slice(0, chars);
|
|
484
|
+
});
|
|
485
|
+
registerFunction("RIGHT", (args) => {
|
|
486
|
+
const flat = args.flat();
|
|
487
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
488
|
+
const chars = flat[1] != null ? Number(flat[1]) : 1;
|
|
489
|
+
return text.slice(-chars);
|
|
490
|
+
});
|
|
491
|
+
registerFunction("MID", (args) => {
|
|
492
|
+
const flat = args.flat();
|
|
493
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
494
|
+
const start = Number(flat[1]) - 1;
|
|
495
|
+
const chars = Number(flat[2]);
|
|
496
|
+
if (isNaN(start) || isNaN(chars)) return "#VALUE!";
|
|
497
|
+
return text.slice(start, start + chars);
|
|
498
|
+
});
|
|
499
|
+
registerFunction("FIND", (args) => {
|
|
500
|
+
const flat = args.flat();
|
|
501
|
+
const search = flat[0] != null ? String(flat[0]) : "";
|
|
502
|
+
const text = flat[1] != null ? String(flat[1]) : "";
|
|
503
|
+
const startPos = flat[2] != null ? Number(flat[2]) - 1 : 0;
|
|
504
|
+
const idx = text.indexOf(search, startPos);
|
|
505
|
+
return idx === -1 ? "#VALUE!" : idx + 1;
|
|
506
|
+
});
|
|
507
|
+
registerFunction("SEARCH", (args) => {
|
|
508
|
+
const flat = args.flat();
|
|
509
|
+
const search = flat[0] != null ? String(flat[0]).toLowerCase() : "";
|
|
510
|
+
const text = flat[1] != null ? String(flat[1]).toLowerCase() : "";
|
|
511
|
+
const startPos = flat[2] != null ? Number(flat[2]) - 1 : 0;
|
|
512
|
+
const idx = text.indexOf(search, startPos);
|
|
513
|
+
return idx === -1 ? "#VALUE!" : idx + 1;
|
|
514
|
+
});
|
|
515
|
+
registerFunction("SUBSTITUTE", (args) => {
|
|
516
|
+
const flat = args.flat();
|
|
517
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
518
|
+
const oldText = flat[1] != null ? String(flat[1]) : "";
|
|
519
|
+
const newText = flat[2] != null ? String(flat[2]) : "";
|
|
520
|
+
const nth = flat[3] != null ? Number(flat[3]) : 0;
|
|
521
|
+
if (nth === 0) return text.split(oldText).join(newText);
|
|
522
|
+
let count = 0;
|
|
523
|
+
return text.replace(new RegExp(oldText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), (match) => {
|
|
524
|
+
count++;
|
|
525
|
+
return count === nth ? newText : match;
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
registerFunction("REPLACE", (args) => {
|
|
529
|
+
const flat = args.flat();
|
|
530
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
531
|
+
const start = Number(flat[1]) - 1;
|
|
532
|
+
const chars = Number(flat[2]);
|
|
533
|
+
const newText = flat[3] != null ? String(flat[3]) : "";
|
|
534
|
+
if (isNaN(start) || isNaN(chars)) return "#VALUE!";
|
|
535
|
+
return text.slice(0, start) + newText + text.slice(start + chars);
|
|
536
|
+
});
|
|
537
|
+
registerFunction("REPT", (args) => {
|
|
538
|
+
const flat = args.flat();
|
|
539
|
+
const text = flat[0] != null ? String(flat[0]) : "";
|
|
540
|
+
const times = Number(flat[1]);
|
|
541
|
+
if (isNaN(times) || times < 0) return "#VALUE!";
|
|
542
|
+
return text.repeat(times);
|
|
543
|
+
});
|
|
544
|
+
registerFunction("EXACT", (args) => {
|
|
545
|
+
const flat = args.flat();
|
|
546
|
+
return String(flat[0] ?? "") === String(flat[1] ?? "");
|
|
547
|
+
});
|
|
548
|
+
registerFunction("PROPER", (args) => {
|
|
549
|
+
const val = args.flat()[0];
|
|
550
|
+
const text = val != null ? String(val) : "";
|
|
551
|
+
return text.replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
|
|
552
|
+
});
|
|
553
|
+
registerFunction("VALUE", (args) => {
|
|
554
|
+
const val = args.flat()[0];
|
|
555
|
+
const num = Number(val);
|
|
556
|
+
return isNaN(num) ? "#VALUE!" : num;
|
|
557
|
+
});
|
|
558
|
+
registerFunction("TEXT", (args) => {
|
|
559
|
+
const flat = args.flat();
|
|
560
|
+
const val = flat[0];
|
|
561
|
+
const fmt = flat[1] != null ? String(flat[1]) : "";
|
|
562
|
+
if (val == null) return "";
|
|
563
|
+
const num = Number(val);
|
|
564
|
+
if (isNaN(num)) return String(val);
|
|
565
|
+
if (fmt.includes("%")) return (num * 100).toFixed(fmt.split(".")[1]?.length ?? 0) + "%";
|
|
566
|
+
if (fmt.includes(".")) {
|
|
567
|
+
const decimals = fmt.split(".")[1]?.replace(/[^0#]/g, "").length ?? 0;
|
|
568
|
+
return num.toFixed(decimals);
|
|
569
|
+
}
|
|
570
|
+
return String(num);
|
|
571
|
+
});
|
|
572
|
+
registerFunction("CHAR", (args) => {
|
|
573
|
+
const code = Number(args.flat()[0]);
|
|
574
|
+
if (isNaN(code)) return "#VALUE!";
|
|
575
|
+
return String.fromCharCode(code);
|
|
576
|
+
});
|
|
577
|
+
registerFunction("CODE", (args) => {
|
|
578
|
+
const text = String(args.flat()[0] ?? "");
|
|
579
|
+
return text.length > 0 ? text.charCodeAt(0) : "#VALUE!";
|
|
580
|
+
});
|
|
380
581
|
|
|
381
582
|
// src/engine/formula/functions/logic.ts
|
|
382
583
|
function toBool(v) {
|
|
@@ -401,6 +602,407 @@ registerFunction("OR", (args) => {
|
|
|
401
602
|
registerFunction("NOT", (args) => {
|
|
402
603
|
return !toBool(args.flat()[0]);
|
|
403
604
|
});
|
|
605
|
+
registerFunction("IFERROR", (args) => {
|
|
606
|
+
const flat = args.flat();
|
|
607
|
+
const val = flat[0];
|
|
608
|
+
if (typeof val === "string" && val.startsWith("#")) return flat[1] ?? "";
|
|
609
|
+
return val;
|
|
610
|
+
});
|
|
611
|
+
registerFunction("IFBLANK", (args) => {
|
|
612
|
+
const flat = args.flat();
|
|
613
|
+
const val = flat[0];
|
|
614
|
+
if (val === null || val === void 0 || val === "") return flat[1] ?? "";
|
|
615
|
+
return val;
|
|
616
|
+
});
|
|
617
|
+
registerFunction("SWITCH", (args) => {
|
|
618
|
+
const flat = args.flat();
|
|
619
|
+
const expr = flat[0];
|
|
620
|
+
for (let i = 1; i < flat.length - 1; i += 2) {
|
|
621
|
+
if (flat[i] === expr) return flat[i + 1];
|
|
622
|
+
}
|
|
623
|
+
return flat.length % 2 === 0 ? flat[flat.length - 1] : "#N/A";
|
|
624
|
+
});
|
|
625
|
+
registerFunction("CHOOSE", (args) => {
|
|
626
|
+
const flat = args.flat();
|
|
627
|
+
const index = Number(flat[0]);
|
|
628
|
+
if (isNaN(index) || index < 1 || index >= flat.length) return "#VALUE!";
|
|
629
|
+
return flat[index];
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/engine/formula/functions/conditional.ts
|
|
633
|
+
function matchesCriteria(value, criteria) {
|
|
634
|
+
if (criteria === null || criteria === void 0) return false;
|
|
635
|
+
const critStr = String(criteria);
|
|
636
|
+
if (critStr.startsWith("<>")) {
|
|
637
|
+
const target = critStr.slice(2);
|
|
638
|
+
const num2 = Number(target);
|
|
639
|
+
if (!isNaN(num2) && typeof value === "number") return value !== num2;
|
|
640
|
+
return String(value) !== target;
|
|
641
|
+
}
|
|
642
|
+
if (critStr.startsWith(">=")) {
|
|
643
|
+
const num2 = Number(critStr.slice(2));
|
|
644
|
+
return typeof value === "number" && value >= num2;
|
|
645
|
+
}
|
|
646
|
+
if (critStr.startsWith("<=")) {
|
|
647
|
+
const num2 = Number(critStr.slice(2));
|
|
648
|
+
return typeof value === "number" && value <= num2;
|
|
649
|
+
}
|
|
650
|
+
if (critStr.startsWith(">")) {
|
|
651
|
+
const num2 = Number(critStr.slice(1));
|
|
652
|
+
return typeof value === "number" && value > num2;
|
|
653
|
+
}
|
|
654
|
+
if (critStr.startsWith("<")) {
|
|
655
|
+
const num2 = Number(critStr.slice(1));
|
|
656
|
+
return typeof value === "number" && value < num2;
|
|
657
|
+
}
|
|
658
|
+
if (critStr.startsWith("=")) {
|
|
659
|
+
const target = critStr.slice(1);
|
|
660
|
+
const num2 = Number(target);
|
|
661
|
+
if (!isNaN(num2) && typeof value === "number") return value === num2;
|
|
662
|
+
return String(value).toLowerCase() === target.toLowerCase();
|
|
663
|
+
}
|
|
664
|
+
if (critStr.includes("*") || critStr.includes("?")) {
|
|
665
|
+
const pattern = critStr.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
666
|
+
return new RegExp("^" + pattern + "$", "i").test(String(value));
|
|
667
|
+
}
|
|
668
|
+
if (typeof criteria === "number") return value === criteria;
|
|
669
|
+
const num = Number(critStr);
|
|
670
|
+
if (!isNaN(num) && typeof value === "number") return value === num;
|
|
671
|
+
return String(value).toLowerCase() === critStr.toLowerCase();
|
|
672
|
+
}
|
|
673
|
+
registerFunction("SUMIF", (args) => {
|
|
674
|
+
const range = args[0] ?? [];
|
|
675
|
+
const criteria = (args[1] ?? [])[0];
|
|
676
|
+
const sumRange = args[2] ?? range;
|
|
677
|
+
let total = 0;
|
|
678
|
+
for (let i = 0; i < range.length; i++) {
|
|
679
|
+
if (matchesCriteria(range[i], criteria)) {
|
|
680
|
+
const val = Number(sumRange[i] ?? 0);
|
|
681
|
+
if (!isNaN(val)) total += val;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return total;
|
|
685
|
+
});
|
|
686
|
+
registerFunction("SUMIFS", (args) => {
|
|
687
|
+
const sumRange = args[0] ?? [];
|
|
688
|
+
let total = 0;
|
|
689
|
+
for (let i = 0; i < sumRange.length; i++) {
|
|
690
|
+
let allMatch = true;
|
|
691
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
692
|
+
const criteriaRange = args[p] ?? [];
|
|
693
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
694
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
695
|
+
allMatch = false;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (allMatch) {
|
|
700
|
+
const val = Number(sumRange[i] ?? 0);
|
|
701
|
+
if (!isNaN(val)) total += val;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return total;
|
|
705
|
+
});
|
|
706
|
+
registerFunction("COUNTIF", (args) => {
|
|
707
|
+
const range = args[0] ?? [];
|
|
708
|
+
const criteria = (args[1] ?? [])[0];
|
|
709
|
+
let count = 0;
|
|
710
|
+
for (const val of range) {
|
|
711
|
+
if (matchesCriteria(val, criteria)) count++;
|
|
712
|
+
}
|
|
713
|
+
return count;
|
|
714
|
+
});
|
|
715
|
+
registerFunction("COUNTIFS", (args) => {
|
|
716
|
+
if (args.length < 2) return 0;
|
|
717
|
+
const len = (args[0] ?? []).length;
|
|
718
|
+
let count = 0;
|
|
719
|
+
for (let i = 0; i < len; i++) {
|
|
720
|
+
let allMatch = true;
|
|
721
|
+
for (let p = 0; p < args.length - 1; p += 2) {
|
|
722
|
+
const range = args[p] ?? [];
|
|
723
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
724
|
+
if (!matchesCriteria(range[i], criteria)) {
|
|
725
|
+
allMatch = false;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (allMatch) count++;
|
|
730
|
+
}
|
|
731
|
+
return count;
|
|
732
|
+
});
|
|
733
|
+
registerFunction("AVERAGEIF", (args) => {
|
|
734
|
+
const range = args[0] ?? [];
|
|
735
|
+
const criteria = (args[1] ?? [])[0];
|
|
736
|
+
const avgRange = args[2] ?? range;
|
|
737
|
+
let total = 0;
|
|
738
|
+
let count = 0;
|
|
739
|
+
for (let i = 0; i < range.length; i++) {
|
|
740
|
+
if (matchesCriteria(range[i], criteria)) {
|
|
741
|
+
const val = Number(avgRange[i] ?? 0);
|
|
742
|
+
if (!isNaN(val)) {
|
|
743
|
+
total += val;
|
|
744
|
+
count++;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return count === 0 ? "#DIV/0!" : total / count;
|
|
749
|
+
});
|
|
750
|
+
registerFunction("AVERAGEIFS", (args) => {
|
|
751
|
+
const avgRange = args[0] ?? [];
|
|
752
|
+
let total = 0;
|
|
753
|
+
let count = 0;
|
|
754
|
+
for (let i = 0; i < avgRange.length; i++) {
|
|
755
|
+
let allMatch = true;
|
|
756
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
757
|
+
const criteriaRange = args[p] ?? [];
|
|
758
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
759
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
760
|
+
allMatch = false;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (allMatch) {
|
|
765
|
+
const val = Number(avgRange[i] ?? 0);
|
|
766
|
+
if (!isNaN(val)) {
|
|
767
|
+
total += val;
|
|
768
|
+
count++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return count === 0 ? "#DIV/0!" : total / count;
|
|
773
|
+
});
|
|
774
|
+
registerFunction("MINIFS", (args) => {
|
|
775
|
+
const minRange = args[0] ?? [];
|
|
776
|
+
let result = Infinity;
|
|
777
|
+
for (let i = 0; i < minRange.length; i++) {
|
|
778
|
+
let allMatch = true;
|
|
779
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
780
|
+
const criteriaRange = args[p] ?? [];
|
|
781
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
782
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
783
|
+
allMatch = false;
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (allMatch) {
|
|
788
|
+
const val = Number(minRange[i]);
|
|
789
|
+
if (!isNaN(val) && val < result) result = val;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return result === Infinity ? 0 : result;
|
|
793
|
+
});
|
|
794
|
+
registerFunction("MAXIFS", (args) => {
|
|
795
|
+
const maxRange = args[0] ?? [];
|
|
796
|
+
let result = -Infinity;
|
|
797
|
+
for (let i = 0; i < maxRange.length; i++) {
|
|
798
|
+
let allMatch = true;
|
|
799
|
+
for (let p = 1; p < args.length - 1; p += 2) {
|
|
800
|
+
const criteriaRange = args[p] ?? [];
|
|
801
|
+
const criteria = (args[p + 1] ?? [])[0];
|
|
802
|
+
if (!matchesCriteria(criteriaRange[i], criteria)) {
|
|
803
|
+
allMatch = false;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (allMatch) {
|
|
808
|
+
const val = Number(maxRange[i]);
|
|
809
|
+
if (!isNaN(val) && val > result) result = val;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return result === -Infinity ? 0 : result;
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// src/engine/formula/functions/lookup.ts
|
|
816
|
+
registerFunction("VLOOKUP", (args) => {
|
|
817
|
+
const flat = args.flat();
|
|
818
|
+
const key = flat[0];
|
|
819
|
+
const range = args[1] ?? [];
|
|
820
|
+
const colIdx = Number(flat[2] ?? flat[args[1]?.length ?? 0]);
|
|
821
|
+
if (key === null || isNaN(colIdx)) return "#VALUE!";
|
|
822
|
+
for (let i = 0; i < range.length; i++) {
|
|
823
|
+
if (range[i] === key || typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
824
|
+
return range[i] ?? "#N/A";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return "#N/A";
|
|
828
|
+
});
|
|
829
|
+
registerFunction("HLOOKUP", (args) => {
|
|
830
|
+
const flat = args.flat();
|
|
831
|
+
const key = flat[0];
|
|
832
|
+
const range = args[1] ?? [];
|
|
833
|
+
for (let i = 0; i < range.length; i++) {
|
|
834
|
+
if (range[i] === key || typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
835
|
+
return range[i] ?? "#N/A";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return "#N/A";
|
|
839
|
+
});
|
|
840
|
+
registerFunction("INDEX", (args) => {
|
|
841
|
+
const range = args[0] ?? [];
|
|
842
|
+
const flat = args.flat();
|
|
843
|
+
const row = Number(flat[range.length] ?? flat[1]);
|
|
844
|
+
if (isNaN(row) || row < 1 || row > range.length) return "#REF!";
|
|
845
|
+
return range[row - 1] ?? null;
|
|
846
|
+
});
|
|
847
|
+
registerFunction("MATCH", (args) => {
|
|
848
|
+
const flat = args.flat();
|
|
849
|
+
const key = flat[0];
|
|
850
|
+
const range = args[1] ?? [];
|
|
851
|
+
for (let i = 0; i < range.length; i++) {
|
|
852
|
+
if (range[i] === key || typeof range[i] === "number" && typeof key === "number" && range[i] === key) {
|
|
853
|
+
return i + 1;
|
|
854
|
+
}
|
|
855
|
+
if (typeof range[i] === "string" && typeof key === "string" && range[i].toLowerCase() === key.toLowerCase()) {
|
|
856
|
+
return i + 1;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return "#N/A";
|
|
860
|
+
});
|
|
861
|
+
registerFunction("ROWS", (args) => {
|
|
862
|
+
return (args[0] ?? []).length;
|
|
863
|
+
});
|
|
864
|
+
registerFunction("COLUMNS", (args) => {
|
|
865
|
+
return 1;
|
|
866
|
+
});
|
|
867
|
+
registerFunction("ROW", (args) => {
|
|
868
|
+
const val = args.flat()[0];
|
|
869
|
+
if (typeof val === "number") return val;
|
|
870
|
+
return "#VALUE!";
|
|
871
|
+
});
|
|
872
|
+
registerFunction("COLUMN", (args) => {
|
|
873
|
+
const val = args.flat()[0];
|
|
874
|
+
if (typeof val === "number") return val;
|
|
875
|
+
return "#VALUE!";
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// src/engine/formula/functions/datetime.ts
|
|
879
|
+
var EXCEL_EPOCH = new Date(1899, 11, 30).getTime();
|
|
880
|
+
function dateToSerial(d) {
|
|
881
|
+
const ms = d.getTime() - EXCEL_EPOCH;
|
|
882
|
+
return Math.floor(ms / 864e5);
|
|
883
|
+
}
|
|
884
|
+
function serialToDate(serial) {
|
|
885
|
+
return new Date(EXCEL_EPOCH + serial * 864e5);
|
|
886
|
+
}
|
|
887
|
+
function toSerial(val) {
|
|
888
|
+
if (typeof val === "number") return val;
|
|
889
|
+
if (typeof val === "string") {
|
|
890
|
+
const d = new Date(val);
|
|
891
|
+
if (!isNaN(d.getTime())) return dateToSerial(d);
|
|
892
|
+
}
|
|
893
|
+
return NaN;
|
|
894
|
+
}
|
|
895
|
+
registerFunction("TODAY", () => {
|
|
896
|
+
return dateToSerial(/* @__PURE__ */ new Date());
|
|
897
|
+
});
|
|
898
|
+
registerFunction("NOW", () => {
|
|
899
|
+
const now = /* @__PURE__ */ new Date();
|
|
900
|
+
const serial = dateToSerial(now);
|
|
901
|
+
const fraction = (now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()) / 86400;
|
|
902
|
+
return serial + fraction;
|
|
903
|
+
});
|
|
904
|
+
registerFunction("DATE", (args) => {
|
|
905
|
+
const flat = args.flat();
|
|
906
|
+
const year = Number(flat[0]);
|
|
907
|
+
const month = Number(flat[1]) - 1;
|
|
908
|
+
const day = Number(flat[2]);
|
|
909
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return "#VALUE!";
|
|
910
|
+
return dateToSerial(new Date(year, month, day));
|
|
911
|
+
});
|
|
912
|
+
registerFunction("YEAR", (args) => {
|
|
913
|
+
const serial = toSerial(args.flat()[0]);
|
|
914
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
915
|
+
return serialToDate(serial).getFullYear();
|
|
916
|
+
});
|
|
917
|
+
registerFunction("MONTH", (args) => {
|
|
918
|
+
const serial = toSerial(args.flat()[0]);
|
|
919
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
920
|
+
return serialToDate(serial).getMonth() + 1;
|
|
921
|
+
});
|
|
922
|
+
registerFunction("DAY", (args) => {
|
|
923
|
+
const serial = toSerial(args.flat()[0]);
|
|
924
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
925
|
+
return serialToDate(serial).getDate();
|
|
926
|
+
});
|
|
927
|
+
registerFunction("HOUR", (args) => {
|
|
928
|
+
const val = Number(args.flat()[0]);
|
|
929
|
+
if (isNaN(val)) return "#VALUE!";
|
|
930
|
+
const fraction = val % 1;
|
|
931
|
+
return Math.floor(fraction * 24);
|
|
932
|
+
});
|
|
933
|
+
registerFunction("MINUTE", (args) => {
|
|
934
|
+
const val = Number(args.flat()[0]);
|
|
935
|
+
if (isNaN(val)) return "#VALUE!";
|
|
936
|
+
const fraction = val % 1;
|
|
937
|
+
return Math.floor(fraction * 24 * 60 % 60);
|
|
938
|
+
});
|
|
939
|
+
registerFunction("SECOND", (args) => {
|
|
940
|
+
const val = Number(args.flat()[0]);
|
|
941
|
+
if (isNaN(val)) return "#VALUE!";
|
|
942
|
+
const fraction = val % 1;
|
|
943
|
+
return Math.floor(fraction * 24 * 3600 % 60);
|
|
944
|
+
});
|
|
945
|
+
registerFunction("WEEKDAY", (args) => {
|
|
946
|
+
const flat = args.flat();
|
|
947
|
+
const serial = toSerial(flat[0]);
|
|
948
|
+
if (isNaN(serial)) return "#VALUE!";
|
|
949
|
+
const day = serialToDate(serial).getDay();
|
|
950
|
+
const type = Number(flat[1] ?? 1);
|
|
951
|
+
if (type === 1) return day + 1;
|
|
952
|
+
if (type === 2) return day === 0 ? 7 : day;
|
|
953
|
+
return day;
|
|
954
|
+
});
|
|
955
|
+
registerFunction("DATEDIF", (args) => {
|
|
956
|
+
const flat = args.flat();
|
|
957
|
+
const startSerial = toSerial(flat[0]);
|
|
958
|
+
const endSerial = toSerial(flat[1]);
|
|
959
|
+
const unit = String(flat[2] ?? "D").toUpperCase();
|
|
960
|
+
if (isNaN(startSerial) || isNaN(endSerial)) return "#VALUE!";
|
|
961
|
+
const startDate = serialToDate(startSerial);
|
|
962
|
+
const endDate = serialToDate(endSerial);
|
|
963
|
+
if (unit === "D") return Math.floor(endSerial - startSerial);
|
|
964
|
+
if (unit === "M") {
|
|
965
|
+
return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
|
|
966
|
+
}
|
|
967
|
+
if (unit === "Y") return endDate.getFullYear() - startDate.getFullYear();
|
|
968
|
+
return "#VALUE!";
|
|
969
|
+
});
|
|
970
|
+
registerFunction("EDATE", (args) => {
|
|
971
|
+
const flat = args.flat();
|
|
972
|
+
const serial = toSerial(flat[0]);
|
|
973
|
+
const months = Number(flat[1]);
|
|
974
|
+
if (isNaN(serial) || isNaN(months)) return "#VALUE!";
|
|
975
|
+
const d = serialToDate(serial);
|
|
976
|
+
d.setMonth(d.getMonth() + months);
|
|
977
|
+
return dateToSerial(d);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// src/engine/formula/functions/info.ts
|
|
981
|
+
registerFunction("ISBLANK", (args) => {
|
|
982
|
+
const val = args.flat()[0];
|
|
983
|
+
return val === null || val === void 0 || val === "";
|
|
984
|
+
});
|
|
985
|
+
registerFunction("ISNUMBER", (args) => {
|
|
986
|
+
return typeof args.flat()[0] === "number";
|
|
987
|
+
});
|
|
988
|
+
registerFunction("ISTEXT", (args) => {
|
|
989
|
+
const val = args.flat()[0];
|
|
990
|
+
return typeof val === "string" && !val.startsWith("#");
|
|
991
|
+
});
|
|
992
|
+
registerFunction("ISERROR", (args) => {
|
|
993
|
+
const val = args.flat()[0];
|
|
994
|
+
return typeof val === "string" && val.startsWith("#");
|
|
995
|
+
});
|
|
996
|
+
registerFunction("ISLOGICAL", (args) => {
|
|
997
|
+
return typeof args.flat()[0] === "boolean";
|
|
998
|
+
});
|
|
999
|
+
registerFunction("TYPE", (args) => {
|
|
1000
|
+
const val = args.flat()[0];
|
|
1001
|
+
if (typeof val === "number") return 1;
|
|
1002
|
+
if (typeof val === "string") return val.startsWith("#") ? 16 : 2;
|
|
1003
|
+
if (typeof val === "boolean") return 4;
|
|
1004
|
+
return 1;
|
|
1005
|
+
});
|
|
404
1006
|
|
|
405
1007
|
// src/engine/formula/evaluator.ts
|
|
406
1008
|
function evaluateAST(node, getCellValue, getRangeValues) {
|