@alaarab/ogrid-js 2.1.15 → 2.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/esm/index.js +4158 -129
- package/dist/styles/ogrid.css +38 -0
- package/dist/types/OGrid.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/renderer/TableRenderer.d.ts +2 -0
- package/dist/types/state/ClipboardState.d.ts +10 -0
- package/dist/types/state/FillHandleState.d.ts +3 -1
- package/dist/types/state/FormulaEngineState.d.ts +78 -0
- package/dist/types/state/GridState.d.ts +15 -0
- package/dist/types/state/VirtualScrollState.d.ts +25 -1
- package/dist/types/types/gridTypes.d.ts +30 -3
- package/package.json +2 -2
package/dist/esm/index.js
CHANGED
|
@@ -36,14 +36,23 @@ function escapeCsvValue(value) {
|
|
|
36
36
|
function buildCsvHeader(columns) {
|
|
37
37
|
return columns.map((c) => escapeCsvValue(c.name)).join(",");
|
|
38
38
|
}
|
|
39
|
-
function buildCsvRows(items, columns, getValue) {
|
|
39
|
+
function buildCsvRows(items, columns, getValue, formulaOptions) {
|
|
40
40
|
return items.map(
|
|
41
|
-
(item) => columns.map((
|
|
41
|
+
(item, rowIdx) => columns.map((col) => {
|
|
42
|
+
if (formulaOptions?.exportMode === "formulas" && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.columnIdToIndex) {
|
|
43
|
+
const colIdx = formulaOptions.columnIdToIndex.get(col.columnId);
|
|
44
|
+
if (colIdx !== void 0 && formulaOptions.hasFormula(colIdx, rowIdx)) {
|
|
45
|
+
const formula = formulaOptions.getFormula(colIdx, rowIdx);
|
|
46
|
+
if (formula) return escapeCsvValue(formula);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return escapeCsvValue(getValue(item, col.columnId));
|
|
50
|
+
}).join(",")
|
|
42
51
|
);
|
|
43
52
|
}
|
|
44
|
-
function exportToCsv(items, columns, getValue, filename) {
|
|
53
|
+
function exportToCsv(items, columns, getValue, filename, formulaOptions) {
|
|
45
54
|
const header = buildCsvHeader(columns);
|
|
46
|
-
const rows = buildCsvRows(items, columns, getValue);
|
|
55
|
+
const rows = buildCsvRows(items, columns, getValue, formulaOptions);
|
|
47
56
|
const csv = [header, ...rows].join("\n");
|
|
48
57
|
triggerCsvDownload(csv, filename ?? `export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`);
|
|
49
58
|
}
|
|
@@ -710,6 +719,66 @@ function calculateDropTarget(params) {
|
|
|
710
719
|
}
|
|
711
720
|
return { targetIndex, indicatorX };
|
|
712
721
|
}
|
|
722
|
+
function computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, overscan = 2) {
|
|
723
|
+
if (columnWidths.length === 0 || containerWidth <= 0) {
|
|
724
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
725
|
+
}
|
|
726
|
+
let cumWidth = 0;
|
|
727
|
+
let rawStart = columnWidths.length;
|
|
728
|
+
let rawEnd = -1;
|
|
729
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
730
|
+
const colStart = cumWidth;
|
|
731
|
+
cumWidth += columnWidths[i];
|
|
732
|
+
if (cumWidth > scrollLeft && rawStart === columnWidths.length) {
|
|
733
|
+
rawStart = i;
|
|
734
|
+
}
|
|
735
|
+
if (colStart < scrollLeft + containerWidth) {
|
|
736
|
+
rawEnd = i;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (rawStart > rawEnd) {
|
|
740
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
741
|
+
}
|
|
742
|
+
const startIndex = Math.max(0, rawStart - overscan);
|
|
743
|
+
const endIndex = Math.min(columnWidths.length - 1, rawEnd + overscan);
|
|
744
|
+
let leftOffset = 0;
|
|
745
|
+
for (let i = 0; i < startIndex; i++) {
|
|
746
|
+
leftOffset += columnWidths[i];
|
|
747
|
+
}
|
|
748
|
+
let rightOffset = 0;
|
|
749
|
+
for (let i = endIndex + 1; i < columnWidths.length; i++) {
|
|
750
|
+
rightOffset += columnWidths[i];
|
|
751
|
+
}
|
|
752
|
+
return { startIndex, endIndex, leftOffset, rightOffset };
|
|
753
|
+
}
|
|
754
|
+
function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColumns) {
|
|
755
|
+
const pinnedLeft = [];
|
|
756
|
+
const pinnedRight = [];
|
|
757
|
+
const unpinned = [];
|
|
758
|
+
for (const col of visibleCols) {
|
|
759
|
+
const pin = pinnedColumns?.[col.columnId];
|
|
760
|
+
if (pin === "left") pinnedLeft.push(col);
|
|
761
|
+
else if (pin === "right") pinnedRight.push(col);
|
|
762
|
+
else unpinned.push(col);
|
|
763
|
+
}
|
|
764
|
+
if (!columnRange || columnRange.endIndex < 0) {
|
|
765
|
+
return {
|
|
766
|
+
pinnedLeft,
|
|
767
|
+
virtualizedUnpinned: unpinned,
|
|
768
|
+
pinnedRight,
|
|
769
|
+
leftSpacerWidth: 0,
|
|
770
|
+
rightSpacerWidth: 0
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const virtualizedUnpinned = unpinned.slice(columnRange.startIndex, columnRange.endIndex + 1);
|
|
774
|
+
return {
|
|
775
|
+
pinnedLeft,
|
|
776
|
+
virtualizedUnpinned,
|
|
777
|
+
pinnedRight,
|
|
778
|
+
leftSpacerWidth: columnRange.leftOffset,
|
|
779
|
+
rightSpacerWidth: columnRange.rightOffset
|
|
780
|
+
};
|
|
781
|
+
}
|
|
713
782
|
function computeVisibleRange(scrollTop, rowHeight, containerHeight, totalRows, overscan = 5) {
|
|
714
783
|
if (totalRows <= 0 || rowHeight <= 0 || containerHeight <= 0) {
|
|
715
784
|
return { startIndex: 0, endIndex: 0, offsetTop: 0, offsetBottom: 0 };
|
|
@@ -737,6 +806,244 @@ function getScrollTopForRow(rowIndex, rowHeight, containerHeight, align = "start
|
|
|
737
806
|
return Math.max(0, rowTop - containerHeight + rowHeight);
|
|
738
807
|
}
|
|
739
808
|
}
|
|
809
|
+
function workerBody() {
|
|
810
|
+
const ctx = self;
|
|
811
|
+
ctx.onmessage = (e) => {
|
|
812
|
+
const msg = e.data;
|
|
813
|
+
if (msg.type !== "sort-filter") return;
|
|
814
|
+
const { requestId, values, filters, sort } = msg;
|
|
815
|
+
const rowCount = values.length;
|
|
816
|
+
let indices = [];
|
|
817
|
+
const filterEntries = Object.entries(filters);
|
|
818
|
+
if (filterEntries.length === 0) {
|
|
819
|
+
indices = new Array(rowCount);
|
|
820
|
+
for (let i = 0; i < rowCount; i++) indices[i] = i;
|
|
821
|
+
} else {
|
|
822
|
+
for (let r = 0; r < rowCount; r++) {
|
|
823
|
+
let pass = true;
|
|
824
|
+
for (let f = 0; f < filterEntries.length; f++) {
|
|
825
|
+
const colIdx = Number(filterEntries[f][0]);
|
|
826
|
+
const filter = filterEntries[f][1];
|
|
827
|
+
const cellVal = values[r][colIdx];
|
|
828
|
+
switch (filter.type) {
|
|
829
|
+
case "text": {
|
|
830
|
+
const trimmed = filter.value.trim().toLowerCase();
|
|
831
|
+
if (trimmed && !String(cellVal ?? "").toLowerCase().includes(trimmed)) {
|
|
832
|
+
pass = false;
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "multiSelect": {
|
|
837
|
+
if (filter.value.length > 0) {
|
|
838
|
+
const set = new Set(filter.value);
|
|
839
|
+
if (!set.has(String(cellVal ?? ""))) {
|
|
840
|
+
pass = false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
case "date": {
|
|
846
|
+
if (cellVal == null) {
|
|
847
|
+
pass = false;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
const ts = new Date(String(cellVal)).getTime();
|
|
851
|
+
if (isNaN(ts)) {
|
|
852
|
+
pass = false;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
if (filter.value.from) {
|
|
856
|
+
const fromTs = (/* @__PURE__ */ new Date(filter.value.from + "T00:00:00")).getTime();
|
|
857
|
+
if (ts < fromTs) {
|
|
858
|
+
pass = false;
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (filter.value.to) {
|
|
863
|
+
const toTs = (/* @__PURE__ */ new Date(filter.value.to + "T23:59:59.999")).getTime();
|
|
864
|
+
if (ts > toTs) {
|
|
865
|
+
pass = false;
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!pass) break;
|
|
873
|
+
}
|
|
874
|
+
if (pass) indices.push(r);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (sort) {
|
|
878
|
+
const { columnIndex, direction } = sort;
|
|
879
|
+
const dir = direction === "asc" ? 1 : -1;
|
|
880
|
+
indices.sort((a, b) => {
|
|
881
|
+
const av = values[a][columnIndex];
|
|
882
|
+
const bv = values[b][columnIndex];
|
|
883
|
+
if (av == null && bv == null) return 0;
|
|
884
|
+
if (av == null) return -1 * dir;
|
|
885
|
+
if (bv == null) return 1 * dir;
|
|
886
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
887
|
+
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
888
|
+
}
|
|
889
|
+
const sa = String(av).toLowerCase();
|
|
890
|
+
const sb = String(bv).toLowerCase();
|
|
891
|
+
return sa === sb ? 0 : sa > sb ? dir : -dir;
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const response = {
|
|
895
|
+
type: "sort-filter-result",
|
|
896
|
+
requestId,
|
|
897
|
+
indices
|
|
898
|
+
};
|
|
899
|
+
ctx.postMessage(response);
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
var workerInstance = null;
|
|
903
|
+
var requestCounter = 0;
|
|
904
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
905
|
+
function createSortFilterWorker() {
|
|
906
|
+
if (workerInstance) return workerInstance;
|
|
907
|
+
if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const fnStr = workerBody.toString();
|
|
912
|
+
const blob = new Blob(
|
|
913
|
+
[`(${fnStr})()`],
|
|
914
|
+
{ type: "application/javascript" }
|
|
915
|
+
);
|
|
916
|
+
const url = URL.createObjectURL(blob);
|
|
917
|
+
workerInstance = new Worker(url);
|
|
918
|
+
URL.revokeObjectURL(url);
|
|
919
|
+
workerInstance.onmessage = (e) => {
|
|
920
|
+
const { requestId, indices } = e.data;
|
|
921
|
+
const pending = pendingRequests.get(requestId);
|
|
922
|
+
if (pending) {
|
|
923
|
+
pendingRequests.delete(requestId);
|
|
924
|
+
pending.resolve(indices);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
workerInstance.onerror = (err) => {
|
|
928
|
+
for (const [id, pending] of pendingRequests) {
|
|
929
|
+
pending.reject(new Error(err.message || "Worker error"));
|
|
930
|
+
pendingRequests.delete(id);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
return workerInstance;
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function terminateSortFilterWorker() {
|
|
939
|
+
if (workerInstance) {
|
|
940
|
+
workerInstance.terminate();
|
|
941
|
+
workerInstance = null;
|
|
942
|
+
}
|
|
943
|
+
for (const [id, pending] of pendingRequests) {
|
|
944
|
+
pending.reject(new Error("Worker terminated"));
|
|
945
|
+
pendingRequests.delete(id);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function extractValueMatrix(data, columns) {
|
|
949
|
+
const matrix = new Array(data.length);
|
|
950
|
+
for (let r = 0; r < data.length; r++) {
|
|
951
|
+
const row = new Array(columns.length);
|
|
952
|
+
for (let c = 0; c < columns.length; c++) {
|
|
953
|
+
const val = getCellValue(data[r], columns[c]);
|
|
954
|
+
if (val == null) {
|
|
955
|
+
row[c] = null;
|
|
956
|
+
} else if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
|
|
957
|
+
row[c] = val;
|
|
958
|
+
} else {
|
|
959
|
+
row[c] = String(val);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
matrix[r] = row;
|
|
963
|
+
}
|
|
964
|
+
return matrix;
|
|
965
|
+
}
|
|
966
|
+
function processClientSideDataAsync(data, columns, filters, sortBy, sortDirection) {
|
|
967
|
+
if (sortBy) {
|
|
968
|
+
const sortCol = columns.find((c) => c.columnId === sortBy);
|
|
969
|
+
if (sortCol?.compare) {
|
|
970
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const worker = createSortFilterWorker();
|
|
974
|
+
if (!worker) {
|
|
975
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
976
|
+
}
|
|
977
|
+
const columnIndexMap = /* @__PURE__ */ new Map();
|
|
978
|
+
for (let i = 0; i < columns.length; i++) {
|
|
979
|
+
columnIndexMap.set(columns[i].columnId, i);
|
|
980
|
+
}
|
|
981
|
+
const values = extractValueMatrix(data, columns);
|
|
982
|
+
const columnMeta = columns.map((col, idx) => ({
|
|
983
|
+
type: col.type ?? "text",
|
|
984
|
+
index: idx
|
|
985
|
+
}));
|
|
986
|
+
const workerFilters = {};
|
|
987
|
+
for (const col of columns) {
|
|
988
|
+
const filterKey = getFilterField(col);
|
|
989
|
+
const val = filters[filterKey];
|
|
990
|
+
if (!val) continue;
|
|
991
|
+
const colIdx = columnIndexMap.get(col.columnId);
|
|
992
|
+
if (colIdx === void 0) continue;
|
|
993
|
+
switch (val.type) {
|
|
994
|
+
case "text":
|
|
995
|
+
workerFilters[colIdx] = { type: "text", value: val.value };
|
|
996
|
+
break;
|
|
997
|
+
case "multiSelect":
|
|
998
|
+
workerFilters[colIdx] = { type: "multiSelect", value: val.value };
|
|
999
|
+
break;
|
|
1000
|
+
case "date":
|
|
1001
|
+
workerFilters[colIdx] = { type: "date", value: { from: val.value.from, to: val.value.to } };
|
|
1002
|
+
break;
|
|
1003
|
+
// 'people' filter has a UserLike object — fall back to sync
|
|
1004
|
+
case "people":
|
|
1005
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
let sort;
|
|
1009
|
+
if (sortBy) {
|
|
1010
|
+
const sortColIdx = columnIndexMap.get(sortBy);
|
|
1011
|
+
if (sortColIdx !== void 0) {
|
|
1012
|
+
sort = { columnIndex: sortColIdx, direction: sortDirection ?? "asc" };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const requestId = ++requestCounter;
|
|
1016
|
+
return new Promise((resolve, reject) => {
|
|
1017
|
+
pendingRequests.set(requestId, {
|
|
1018
|
+
resolve: (indices) => {
|
|
1019
|
+
const result = new Array(indices.length);
|
|
1020
|
+
for (let i = 0; i < indices.length; i++) {
|
|
1021
|
+
result[i] = data[indices[i]];
|
|
1022
|
+
}
|
|
1023
|
+
resolve(result);
|
|
1024
|
+
},
|
|
1025
|
+
reject
|
|
1026
|
+
});
|
|
1027
|
+
const request = {
|
|
1028
|
+
type: "sort-filter",
|
|
1029
|
+
requestId,
|
|
1030
|
+
values,
|
|
1031
|
+
columnMeta,
|
|
1032
|
+
filters: workerFilters,
|
|
1033
|
+
sort
|
|
1034
|
+
};
|
|
1035
|
+
worker.postMessage(request);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
var FormulaError = class {
|
|
1039
|
+
constructor(type, message) {
|
|
1040
|
+
this.type = type;
|
|
1041
|
+
this.message = message;
|
|
1042
|
+
}
|
|
1043
|
+
toString() {
|
|
1044
|
+
return this.type;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
740
1047
|
function getHeaderFilterConfig(col, input) {
|
|
741
1048
|
const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
|
|
742
1049
|
const filterType = filterable?.type ?? "none";
|
|
@@ -863,7 +1170,8 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
863
1170
|
const canEditAny = canEditInline || canEditPopup;
|
|
864
1171
|
const isEditing = input.editingCell?.rowId === rowId && input.editingCell?.columnId === col.columnId;
|
|
865
1172
|
const isActive = !input.isDragging && input.activeCell?.rowIndex === rowIndex && input.activeCell?.columnIndex === globalColIndex;
|
|
866
|
-
const
|
|
1173
|
+
const isSingleCellRange = input.selectionRange != null && input.selectionRange.startRow === input.selectionRange.endRow && input.selectionRange.startCol === input.selectionRange.endCol;
|
|
1174
|
+
const isInRange = input.selectionRange != null && !isSingleCellRange && isInSelectionRange(input.selectionRange, rowIndex, colIdx);
|
|
867
1175
|
const isInCutRange = input.cutRange != null && isInSelectionRange(input.cutRange, rowIndex, colIdx);
|
|
868
1176
|
const isInCopyRange = input.copyRange != null && isInSelectionRange(input.copyRange, rowIndex, colIdx);
|
|
869
1177
|
const isSelectionEndCell = !input.isDragging && input.copyRange == null && input.cutRange == null && input.selectionRange != null && rowIndex === input.selectionRange.endRow && colIdx === input.selectionRange.endCol;
|
|
@@ -905,6 +1213,9 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
905
1213
|
};
|
|
906
1214
|
}
|
|
907
1215
|
function resolveCellDisplayContent(col, item, displayValue) {
|
|
1216
|
+
if (displayValue instanceof FormulaError) {
|
|
1217
|
+
return displayValue.toString();
|
|
1218
|
+
}
|
|
908
1219
|
const c = col;
|
|
909
1220
|
if (c.renderCell && typeof c.renderCell === "function") {
|
|
910
1221
|
return c.renderCell(item);
|
|
@@ -920,10 +1231,14 @@ function resolveCellDisplayContent(col, item, displayValue) {
|
|
|
920
1231
|
}
|
|
921
1232
|
return String(displayValue);
|
|
922
1233
|
}
|
|
923
|
-
function resolveCellStyle(col, item) {
|
|
1234
|
+
function resolveCellStyle(col, item, displayValue) {
|
|
924
1235
|
const c = col;
|
|
925
|
-
|
|
926
|
-
|
|
1236
|
+
const isError = displayValue instanceof FormulaError;
|
|
1237
|
+
const base = c.cellStyle ? typeof c.cellStyle === "function" ? c.cellStyle(item) : c.cellStyle : void 0;
|
|
1238
|
+
if (isError) {
|
|
1239
|
+
return { ...base, color: "var(--ogrid-formula-error-color, #d32f2f)" };
|
|
1240
|
+
}
|
|
1241
|
+
return base;
|
|
927
1242
|
}
|
|
928
1243
|
function buildInlineEditorProps(item, col, descriptor, callbacks) {
|
|
929
1244
|
return {
|
|
@@ -1033,6 +1348,7 @@ var AUTOSIZE_MAX_PX = 520;
|
|
|
1033
1348
|
function measureHeaderWidth(th) {
|
|
1034
1349
|
const cs = getComputedStyle(th);
|
|
1035
1350
|
const thPadding = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
|
|
1351
|
+
const thBorders = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
|
|
1036
1352
|
let resizeHandleWidth = 0;
|
|
1037
1353
|
for (let i = 0; i < th.children.length; i++) {
|
|
1038
1354
|
const child = th.children[i];
|
|
@@ -1055,12 +1371,14 @@ function measureHeaderWidth(th) {
|
|
|
1055
1371
|
overflow: child.style.overflow,
|
|
1056
1372
|
flexShrink: child.style.flexShrink,
|
|
1057
1373
|
width: child.style.width,
|
|
1058
|
-
minWidth: child.style.minWidth
|
|
1374
|
+
minWidth: child.style.minWidth,
|
|
1375
|
+
maxWidth: child.style.maxWidth
|
|
1059
1376
|
});
|
|
1060
1377
|
child.style.overflow = "visible";
|
|
1061
1378
|
child.style.flexShrink = "0";
|
|
1062
1379
|
child.style.width = "max-content";
|
|
1063
1380
|
child.style.minWidth = "max-content";
|
|
1381
|
+
child.style.maxWidth = "none";
|
|
1064
1382
|
}
|
|
1065
1383
|
expandDescendants(child);
|
|
1066
1384
|
}
|
|
@@ -1078,8 +1396,9 @@ function measureHeaderWidth(th) {
|
|
|
1078
1396
|
m.el.style.flexShrink = m.flexShrink;
|
|
1079
1397
|
m.el.style.width = m.width;
|
|
1080
1398
|
m.el.style.minWidth = m.minWidth;
|
|
1399
|
+
m.el.style.maxWidth = m.maxWidth;
|
|
1081
1400
|
}
|
|
1082
|
-
return expandedWidth + resizeHandleWidth + thPadding;
|
|
1401
|
+
return expandedWidth + resizeHandleWidth + thPadding + thBorders;
|
|
1083
1402
|
}
|
|
1084
1403
|
function measureColumnContentWidth(columnId, minWidth, container) {
|
|
1085
1404
|
const minW = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
@@ -1290,7 +1609,7 @@ function formatCellValueForTsv(raw, formatted) {
|
|
|
1290
1609
|
return "[Object]";
|
|
1291
1610
|
}
|
|
1292
1611
|
}
|
|
1293
|
-
function formatSelectionAsTsv(items, visibleCols, range) {
|
|
1612
|
+
function formatSelectionAsTsv(items, visibleCols, range, formulaOptions) {
|
|
1294
1613
|
const norm = normalizeSelectionRange(range);
|
|
1295
1614
|
const rows = [];
|
|
1296
1615
|
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
@@ -1299,6 +1618,16 @@ function formatSelectionAsTsv(items, visibleCols, range) {
|
|
|
1299
1618
|
if (r >= items.length || c >= visibleCols.length) break;
|
|
1300
1619
|
const item = items[r];
|
|
1301
1620
|
const col = visibleCols[c];
|
|
1621
|
+
if (formulaOptions?.hasFormula && formulaOptions?.getFormula) {
|
|
1622
|
+
const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
|
|
1623
|
+
if (flatColIndex >= 0 && formulaOptions.hasFormula(flatColIndex, r)) {
|
|
1624
|
+
const formulaStr = formulaOptions.getFormula(flatColIndex, r);
|
|
1625
|
+
if (formulaStr) {
|
|
1626
|
+
cells.push(formulaStr);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1302
1631
|
const raw = getCellValue(item, col);
|
|
1303
1632
|
const clipboard = col.clipboardFormatter ? col.clipboardFormatter(raw, item) : null;
|
|
1304
1633
|
const formatted = clipboard ?? (col.valueFormatter ? col.valueFormatter(raw, item) : raw);
|
|
@@ -1313,7 +1642,7 @@ function parseTsvClipboard(text) {
|
|
|
1313
1642
|
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
1314
1643
|
return lines.map((line) => line.split(" "));
|
|
1315
1644
|
}
|
|
1316
|
-
function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols) {
|
|
1645
|
+
function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions) {
|
|
1317
1646
|
const events = [];
|
|
1318
1647
|
for (let r = 0; r < parsedRows.length; r++) {
|
|
1319
1648
|
const cells = parsedRows[r];
|
|
@@ -1324,9 +1653,16 @@ function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols)
|
|
|
1324
1653
|
const item = items[targetRow];
|
|
1325
1654
|
const col = visibleCols[targetCol];
|
|
1326
1655
|
if (!isColumnEditable(col, item)) continue;
|
|
1327
|
-
const
|
|
1656
|
+
const cellText = cells[c] ?? "";
|
|
1657
|
+
if (cellText.startsWith("=") && formulaOptions?.setFormula) {
|
|
1658
|
+
const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
|
|
1659
|
+
if (flatColIndex >= 0) {
|
|
1660
|
+
formulaOptions.setFormula(flatColIndex, targetRow, cellText);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1328
1664
|
const oldValue = getCellValue(item, col);
|
|
1329
|
-
const result = parseValue(
|
|
1665
|
+
const result = parseValue(cellText, oldValue, item, col);
|
|
1330
1666
|
if (!result.valid) continue;
|
|
1331
1667
|
events.push({
|
|
1332
1668
|
item,
|
|
@@ -1361,12 +1697,111 @@ function applyCutClear(cutRange, items, visibleCols) {
|
|
|
1361
1697
|
}
|
|
1362
1698
|
return events;
|
|
1363
1699
|
}
|
|
1364
|
-
function
|
|
1700
|
+
function indexToColumnLetter(index) {
|
|
1701
|
+
let result = "";
|
|
1702
|
+
let n = index;
|
|
1703
|
+
while (n >= 0) {
|
|
1704
|
+
result = String.fromCharCode(n % 26 + 65) + result;
|
|
1705
|
+
n = Math.floor(n / 26) - 1;
|
|
1706
|
+
}
|
|
1707
|
+
return result;
|
|
1708
|
+
}
|
|
1709
|
+
function formatCellReference(colIndex, rowNumber) {
|
|
1710
|
+
return `${indexToColumnLetter(colIndex)}${rowNumber}`;
|
|
1711
|
+
}
|
|
1712
|
+
function columnLetterToIndex(letters) {
|
|
1713
|
+
let result = 0;
|
|
1714
|
+
const upper = letters.toUpperCase();
|
|
1715
|
+
for (let i = 0; i < upper.length; i++) {
|
|
1716
|
+
result = result * 26 + (upper.charCodeAt(i) - 64);
|
|
1717
|
+
}
|
|
1718
|
+
return result - 1;
|
|
1719
|
+
}
|
|
1720
|
+
var CELL_REF_RE = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
|
|
1721
|
+
var ADJUST_REF_RE = /(?:'[^']*'!|[A-Za-z_]\w*!)?(\$?)([A-Z]+)(\$?)(\d+)/g;
|
|
1722
|
+
function parseCellRef(ref) {
|
|
1723
|
+
const m = ref.match(CELL_REF_RE);
|
|
1724
|
+
if (!m) return null;
|
|
1725
|
+
const absCol = m[1] === "$";
|
|
1726
|
+
const colLetters = m[2];
|
|
1727
|
+
const absRow = m[3] === "$";
|
|
1728
|
+
const rowNum = parseInt(m[4], 10);
|
|
1729
|
+
if (rowNum < 1) return null;
|
|
1730
|
+
return {
|
|
1731
|
+
col: columnLetterToIndex(colLetters),
|
|
1732
|
+
row: rowNum - 1,
|
|
1733
|
+
// 0-based internally
|
|
1734
|
+
absCol,
|
|
1735
|
+
absRow
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function parseRange(rangeStr) {
|
|
1739
|
+
const parts = rangeStr.split(":");
|
|
1740
|
+
if (parts.length !== 2) return null;
|
|
1741
|
+
const start = parseCellRef(parts[0]);
|
|
1742
|
+
const end = parseCellRef(parts[1]);
|
|
1743
|
+
if (!start || !end) return null;
|
|
1744
|
+
return { start, end };
|
|
1745
|
+
}
|
|
1746
|
+
function formatAddress(addr) {
|
|
1747
|
+
const colStr = (addr.absCol ? "$" : "") + indexToColumnLetter(addr.col);
|
|
1748
|
+
const rowStr = (addr.absRow ? "$" : "") + (addr.row + 1);
|
|
1749
|
+
const cellStr = colStr + rowStr;
|
|
1750
|
+
if (addr.sheet) {
|
|
1751
|
+
const sheetStr = addr.sheet.includes(" ") ? `'${addr.sheet}'` : addr.sheet;
|
|
1752
|
+
return `${sheetStr}!${cellStr}`;
|
|
1753
|
+
}
|
|
1754
|
+
return cellStr;
|
|
1755
|
+
}
|
|
1756
|
+
function adjustFormulaReferences(formula, colDelta, rowDelta) {
|
|
1757
|
+
ADJUST_REF_RE.lastIndex = 0;
|
|
1758
|
+
return formula.replace(ADJUST_REF_RE, (match, colAbs, colLetters, rowAbs, rowDigits) => {
|
|
1759
|
+
const cellRefStart = match.indexOf(colAbs + colLetters);
|
|
1760
|
+
const sheetPrefix = cellRefStart > 0 ? match.substring(0, cellRefStart) : "";
|
|
1761
|
+
let newCol = colLetters;
|
|
1762
|
+
let newRow = rowDigits;
|
|
1763
|
+
if (colAbs !== "$") {
|
|
1764
|
+
const colIdx = columnLetterToIndex(colLetters) + colDelta;
|
|
1765
|
+
if (colIdx < 0) return "#REF!";
|
|
1766
|
+
newCol = indexToColumnLetter(colIdx);
|
|
1767
|
+
}
|
|
1768
|
+
if (rowAbs !== "$") {
|
|
1769
|
+
const rowNum = parseInt(rowDigits, 10) + rowDelta;
|
|
1770
|
+
if (rowNum < 1) return "#REF!";
|
|
1771
|
+
newRow = String(rowNum);
|
|
1772
|
+
}
|
|
1773
|
+
return `${sheetPrefix}${colAbs}${newCol}${rowAbs}${newRow}`;
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function toCellKey(col, row, sheet) {
|
|
1777
|
+
if (sheet) return `${sheet}:${col},${row}`;
|
|
1778
|
+
return `${col},${row}`;
|
|
1779
|
+
}
|
|
1780
|
+
function fromCellKey(key) {
|
|
1781
|
+
const colonIdx = key.indexOf(":");
|
|
1782
|
+
if (colonIdx >= 0 && isNaN(parseInt(key.substring(0, colonIdx), 10))) {
|
|
1783
|
+
const sheet = key.substring(0, colonIdx);
|
|
1784
|
+
const rest = key.substring(colonIdx + 1);
|
|
1785
|
+
const commaIdx = rest.indexOf(",");
|
|
1786
|
+
return {
|
|
1787
|
+
col: parseInt(rest.substring(0, commaIdx), 10),
|
|
1788
|
+
row: parseInt(rest.substring(commaIdx + 1), 10),
|
|
1789
|
+
sheet
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
const i = key.indexOf(",");
|
|
1793
|
+
return {
|
|
1794
|
+
col: parseInt(key.substring(0, i), 10),
|
|
1795
|
+
row: parseInt(key.substring(i + 1), 10)
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function applyFillValues(range, sourceRow, sourceCol, items, visibleCols, formulaOptions) {
|
|
1365
1799
|
const events = [];
|
|
1366
1800
|
const startItem = items[range.startRow];
|
|
1367
1801
|
const startColDef = visibleCols[range.startCol];
|
|
1368
1802
|
if (!startItem || !startColDef) return events;
|
|
1369
1803
|
const startValue = getCellValue(startItem, startColDef);
|
|
1804
|
+
const srcFlatColIndex = formulaOptions ? formulaOptions.flatColumns.findIndex((c) => c.columnId === startColDef.columnId) : -1;
|
|
1370
1805
|
for (let row = range.startRow; row <= range.endRow; row++) {
|
|
1371
1806
|
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
1372
1807
|
if (row === sourceRow && col === sourceCol) continue;
|
|
@@ -1374,6 +1809,19 @@ function applyFillValues(range, sourceRow, sourceCol, items, visibleCols) {
|
|
|
1374
1809
|
const item = items[row];
|
|
1375
1810
|
const colDef = visibleCols[col];
|
|
1376
1811
|
if (!isColumnEditable(colDef, item)) continue;
|
|
1812
|
+
if (formulaOptions && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.setFormula && srcFlatColIndex >= 0 && formulaOptions.hasFormula(srcFlatColIndex, sourceRow)) {
|
|
1813
|
+
const srcFormula = formulaOptions.getFormula(srcFlatColIndex, sourceRow);
|
|
1814
|
+
if (srcFormula) {
|
|
1815
|
+
const rowDelta = row - sourceRow;
|
|
1816
|
+
const colDelta = col - sourceCol;
|
|
1817
|
+
const adjusted = adjustFormulaReferences(srcFormula, colDelta, rowDelta);
|
|
1818
|
+
const targetFlatColIdx = formulaOptions.flatColumns.findIndex((c) => c.columnId === colDef.columnId);
|
|
1819
|
+
if (targetFlatColIdx >= 0) {
|
|
1820
|
+
formulaOptions.setFormula(targetFlatColIdx, row, adjusted);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1377
1825
|
const oldValue = getCellValue(item, colDef);
|
|
1378
1826
|
const result = parseValue(startValue, oldValue, item, colDef);
|
|
1379
1827
|
if (!result.valid) continue;
|
|
@@ -1476,118 +1924,3311 @@ var UndoRedoStack = class {
|
|
|
1476
1924
|
return lastBatch;
|
|
1477
1925
|
}
|
|
1478
1926
|
/**
|
|
1479
|
-
* Pop the most recent redo entry.
|
|
1480
|
-
* Returns the batch of events (in original order) to be re-applied by the caller,
|
|
1481
|
-
* or null if there is nothing to redo.
|
|
1927
|
+
* Pop the most recent redo entry.
|
|
1928
|
+
* Returns the batch of events (in original order) to be re-applied by the caller,
|
|
1929
|
+
* or null if there is nothing to redo.
|
|
1930
|
+
*/
|
|
1931
|
+
redo() {
|
|
1932
|
+
const nextBatch = this.redoStack.pop();
|
|
1933
|
+
if (!nextBatch) return null;
|
|
1934
|
+
this.history.push(nextBatch);
|
|
1935
|
+
return nextBatch;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Clear all history and redo state.
|
|
1939
|
+
* Does not affect any open batch — call endBatch() first if needed.
|
|
1940
|
+
*/
|
|
1941
|
+
clear() {
|
|
1942
|
+
this.history = [];
|
|
1943
|
+
this.redoStack = [];
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
function validateColumns(columns) {
|
|
1947
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
1948
|
+
console.warn("[OGrid] columns prop is empty or not an array");
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
1952
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1953
|
+
for (const col of columns) {
|
|
1954
|
+
if (!col.columnId) {
|
|
1955
|
+
console.warn("[OGrid] Column missing columnId:", col);
|
|
1956
|
+
}
|
|
1957
|
+
if (ids.has(col.columnId)) {
|
|
1958
|
+
console.warn(`[OGrid] Duplicate columnId: "${col.columnId}"`);
|
|
1959
|
+
}
|
|
1960
|
+
ids.add(col.columnId);
|
|
1961
|
+
if (isDev && col.editable === true && col.cellEditor == null) {
|
|
1962
|
+
console.warn(
|
|
1963
|
+
`[OGrid] Column "${col.columnId}" has editable=true but no cellEditor defined. Cells will not open an editor on double-click. Set cellEditor to 'text', 'select', 'checkbox', 'date', or a custom component.`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function validateVirtualScrollConfig(config) {
|
|
1969
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
1970
|
+
if (config.enabled !== true) return;
|
|
1971
|
+
if (!config.rowHeight || config.rowHeight <= 0) {
|
|
1972
|
+
console.warn(
|
|
1973
|
+
"[OGrid] virtualScroll.enabled is true but rowHeight is missing or <= 0. Set a positive rowHeight (e.g. virtualScroll: { enabled: true, rowHeight: 36 }) for correct virtual scrolling behavior."
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
function validateRowIds(items, getRowId) {
|
|
1978
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
1979
|
+
if (!getRowId) return;
|
|
1980
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1981
|
+
const limit = Math.min(items.length, 100);
|
|
1982
|
+
for (let i = 0; i < limit; i++) {
|
|
1983
|
+
const id = getRowId(items[i]);
|
|
1984
|
+
if (id == null) {
|
|
1985
|
+
console.warn(`[OGrid] getRowId returned null/undefined for row ${i}`);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (ids.has(id)) {
|
|
1989
|
+
console.warn(
|
|
1990
|
+
`[OGrid] Duplicate row ID "${id}" at index ${i}. getRowId must return unique values.`
|
|
1991
|
+
);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
ids.add(id);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1998
|
+
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
1999
|
+
var SIDEBAR_TRANSITION_MS = 300;
|
|
2000
|
+
var Z_INDEX = {
|
|
2001
|
+
/** Column resize drag handle */
|
|
2002
|
+
RESIZE_HANDLE: 1,
|
|
2003
|
+
/** Active/editing cell outline */
|
|
2004
|
+
ACTIVE_CELL: 2,
|
|
2005
|
+
/** Fill handle dot */
|
|
2006
|
+
FILL_HANDLE: 3,
|
|
2007
|
+
/** Selection range overlay (marching ants) */
|
|
2008
|
+
SELECTION_OVERLAY: 4,
|
|
2009
|
+
/** Row number column */
|
|
2010
|
+
ROW_NUMBER: 5,
|
|
2011
|
+
/** Clipboard overlay (copy/cut animation) */
|
|
2012
|
+
CLIPBOARD_OVERLAY: 5,
|
|
2013
|
+
/** Sticky pinned body cells */
|
|
2014
|
+
PINNED: 6,
|
|
2015
|
+
/** Selection checkbox column in body */
|
|
2016
|
+
SELECTION_CELL: 7,
|
|
2017
|
+
/** Sticky thead row */
|
|
2018
|
+
THEAD: 8,
|
|
2019
|
+
/** Pinned header cells (sticky both axes) */
|
|
2020
|
+
PINNED_HEADER: 10,
|
|
2021
|
+
/** Focused header cell */
|
|
2022
|
+
HEADER_FOCUS: 11,
|
|
2023
|
+
/** Checkbox column in sticky header (sticky both axes) */
|
|
2024
|
+
SELECTION_HEADER_PINNED: 12,
|
|
2025
|
+
/** Loading overlay within table */
|
|
2026
|
+
LOADING: 2,
|
|
2027
|
+
/** Column reorder drop indicator */
|
|
2028
|
+
DROP_INDICATOR: 100,
|
|
2029
|
+
/** Dropdown menus (column chooser, pagination size select) */
|
|
2030
|
+
DROPDOWN: 1e3,
|
|
2031
|
+
/** Filter popovers */
|
|
2032
|
+
FILTER_POPOVER: 1e3,
|
|
2033
|
+
/** Modal dialogs */
|
|
2034
|
+
MODAL: 2e3,
|
|
2035
|
+
/** Fullscreen grid container */
|
|
2036
|
+
FULLSCREEN: 9999,
|
|
2037
|
+
/** Context menus (right-click grid menu) */
|
|
2038
|
+
CONTEXT_MENU: 1e4
|
|
2039
|
+
};
|
|
2040
|
+
var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
|
|
2041
|
+
var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
|
|
2042
|
+
var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
|
|
2043
|
+
var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
|
|
2044
|
+
var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
|
|
2045
|
+
var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
|
|
2046
|
+
var NA_ERROR = new FormulaError("#N/A", "No match found");
|
|
2047
|
+
function isFormulaError(value) {
|
|
2048
|
+
return value instanceof FormulaError;
|
|
2049
|
+
}
|
|
2050
|
+
var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
|
|
2051
|
+
var SINGLE_CHAR_OPERATORS = {
|
|
2052
|
+
"+": "PLUS",
|
|
2053
|
+
"-": "MINUS",
|
|
2054
|
+
"*": "MULTIPLY",
|
|
2055
|
+
"/": "DIVIDE",
|
|
2056
|
+
"^": "POWER",
|
|
2057
|
+
"%": "PERCENT",
|
|
2058
|
+
"&": "AMPERSAND",
|
|
2059
|
+
"=": "EQ"
|
|
2060
|
+
};
|
|
2061
|
+
var DELIMITERS = {
|
|
2062
|
+
"(": "LPAREN",
|
|
2063
|
+
")": "RPAREN",
|
|
2064
|
+
",": "COMMA",
|
|
2065
|
+
":": "COLON"
|
|
2066
|
+
};
|
|
2067
|
+
function tokenize(input) {
|
|
2068
|
+
const tokens = [];
|
|
2069
|
+
let pos = 0;
|
|
2070
|
+
while (pos < input.length) {
|
|
2071
|
+
const ch = input[pos];
|
|
2072
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
2073
|
+
pos++;
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
if (ch >= "0" && ch <= "9" || ch === "." && pos + 1 < input.length && input[pos + 1] >= "0" && input[pos + 1] <= "9") {
|
|
2077
|
+
const start = pos;
|
|
2078
|
+
while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
|
|
2079
|
+
pos++;
|
|
2080
|
+
}
|
|
2081
|
+
if (pos < input.length && input[pos] === ".") {
|
|
2082
|
+
pos++;
|
|
2083
|
+
while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
|
|
2084
|
+
pos++;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
tokens.push({ type: "NUMBER", value: input.slice(start, pos), position: start });
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (ch === "'") {
|
|
2091
|
+
const start = pos;
|
|
2092
|
+
pos++;
|
|
2093
|
+
const nameStart = pos;
|
|
2094
|
+
while (pos < input.length && input[pos] !== "'") {
|
|
2095
|
+
pos++;
|
|
2096
|
+
}
|
|
2097
|
+
const sheetName = input.slice(nameStart, pos);
|
|
2098
|
+
if (pos < input.length && input[pos] === "'") {
|
|
2099
|
+
pos++;
|
|
2100
|
+
if (pos < input.length && input[pos] === "!") {
|
|
2101
|
+
pos++;
|
|
2102
|
+
tokens.push({ type: "SHEET_REF", value: sheetName, position: start });
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
throw new FormulaError("#ERROR!", `Invalid sheet reference at position ${start}`);
|
|
2107
|
+
}
|
|
2108
|
+
if (ch === '"') {
|
|
2109
|
+
const start = pos;
|
|
2110
|
+
pos++;
|
|
2111
|
+
const scanStart = pos;
|
|
2112
|
+
let hasEscapes = false;
|
|
2113
|
+
while (pos < input.length) {
|
|
2114
|
+
if (input[pos] === '"') {
|
|
2115
|
+
if (pos + 1 < input.length && input[pos + 1] === '"') {
|
|
2116
|
+
hasEscapes = true;
|
|
2117
|
+
pos += 2;
|
|
2118
|
+
} else {
|
|
2119
|
+
break;
|
|
2120
|
+
}
|
|
2121
|
+
} else {
|
|
2122
|
+
pos++;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
let value;
|
|
2126
|
+
if (!hasEscapes) {
|
|
2127
|
+
value = input.slice(scanStart, pos);
|
|
2128
|
+
} else {
|
|
2129
|
+
value = input.slice(scanStart, pos).replace(/""/g, '"');
|
|
2130
|
+
}
|
|
2131
|
+
if (pos < input.length) pos++;
|
|
2132
|
+
tokens.push({ type: "STRING", value, position: start });
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
if (ch === ">" && pos + 1 < input.length && input[pos + 1] === "=") {
|
|
2136
|
+
tokens.push({ type: "GTE", value: ">=", position: pos });
|
|
2137
|
+
pos += 2;
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
if (ch === "<" && pos + 1 < input.length && input[pos + 1] === "=") {
|
|
2141
|
+
tokens.push({ type: "LTE", value: "<=", position: pos });
|
|
2142
|
+
pos += 2;
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
if (ch === "<" && pos + 1 < input.length && input[pos + 1] === ">") {
|
|
2146
|
+
tokens.push({ type: "NEQ", value: "<>", position: pos });
|
|
2147
|
+
pos += 2;
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
if (ch === ">" || ch === "<") {
|
|
2151
|
+
const type = ch === ">" ? "GT" : "LT";
|
|
2152
|
+
tokens.push({ type, value: ch, position: pos });
|
|
2153
|
+
pos++;
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (SINGLE_CHAR_OPERATORS[ch]) {
|
|
2157
|
+
tokens.push({ type: SINGLE_CHAR_OPERATORS[ch], value: ch, position: pos });
|
|
2158
|
+
pos++;
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (DELIMITERS[ch]) {
|
|
2162
|
+
tokens.push({ type: DELIMITERS[ch], value: ch, position: pos });
|
|
2163
|
+
pos++;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
if (ch === "$" || ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z") {
|
|
2167
|
+
const start = pos;
|
|
2168
|
+
while (pos < input.length && (input[pos] >= "A" && input[pos] <= "Z" || input[pos] >= "a" && input[pos] <= "z" || input[pos] >= "0" && input[pos] <= "9" || input[pos] === "$" || input[pos] === "_")) {
|
|
2169
|
+
pos++;
|
|
2170
|
+
}
|
|
2171
|
+
const word = input.slice(start, pos);
|
|
2172
|
+
if (pos < input.length && input[pos] === "!") {
|
|
2173
|
+
pos++;
|
|
2174
|
+
tokens.push({ type: "SHEET_REF", value: word, position: start });
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (pos < input.length && input[pos] === "(") {
|
|
2178
|
+
tokens.push({ type: "FUNCTION", value: word, position: start });
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const upper = word.toUpperCase();
|
|
2182
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
2183
|
+
tokens.push({ type: "BOOLEAN", value: upper, position: start });
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
if (CELL_REF_PATTERN.test(word)) {
|
|
2187
|
+
tokens.push({ type: "CELL_REF", value: word, position: start });
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
tokens.push({ type: "IDENTIFIER", value: word, position: start });
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
throw new FormulaError("#ERROR!", `Unexpected character: ${ch}`);
|
|
2194
|
+
}
|
|
2195
|
+
tokens.push({ type: "EOF", value: "", position: pos });
|
|
2196
|
+
return tokens;
|
|
2197
|
+
}
|
|
2198
|
+
function parse(tokens, namedRanges) {
|
|
2199
|
+
let pos = 0;
|
|
2200
|
+
function peek() {
|
|
2201
|
+
return tokens[pos];
|
|
2202
|
+
}
|
|
2203
|
+
function advance() {
|
|
2204
|
+
const token = tokens[pos];
|
|
2205
|
+
pos++;
|
|
2206
|
+
return token;
|
|
2207
|
+
}
|
|
2208
|
+
function expect(type) {
|
|
2209
|
+
const token = peek();
|
|
2210
|
+
if (token && token.type === type) {
|
|
2211
|
+
return advance();
|
|
2212
|
+
}
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
function errorNode(message) {
|
|
2216
|
+
return { kind: "error", error: new FormulaError("#ERROR!", message) };
|
|
2217
|
+
}
|
|
2218
|
+
function expression() {
|
|
2219
|
+
return comparison();
|
|
2220
|
+
}
|
|
2221
|
+
function comparison() {
|
|
2222
|
+
let left = concat();
|
|
2223
|
+
while (peek()) {
|
|
2224
|
+
const t = peek();
|
|
2225
|
+
let op = null;
|
|
2226
|
+
if (t.type === "GT") op = ">";
|
|
2227
|
+
else if (t.type === "LT") op = "<";
|
|
2228
|
+
else if (t.type === "GTE") op = ">=";
|
|
2229
|
+
else if (t.type === "LTE") op = "<=";
|
|
2230
|
+
else if (t.type === "EQ") op = "=";
|
|
2231
|
+
else if (t.type === "NEQ") op = "<>";
|
|
2232
|
+
else break;
|
|
2233
|
+
advance();
|
|
2234
|
+
const right = concat();
|
|
2235
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2236
|
+
}
|
|
2237
|
+
return left;
|
|
2238
|
+
}
|
|
2239
|
+
function concat() {
|
|
2240
|
+
let left = addition();
|
|
2241
|
+
while (peek() && peek().type === "AMPERSAND") {
|
|
2242
|
+
advance();
|
|
2243
|
+
const right = addition();
|
|
2244
|
+
left = { kind: "binaryOp", op: "&", left, right };
|
|
2245
|
+
}
|
|
2246
|
+
return left;
|
|
2247
|
+
}
|
|
2248
|
+
function addition() {
|
|
2249
|
+
let left = multiplication();
|
|
2250
|
+
while (peek()) {
|
|
2251
|
+
const t = peek();
|
|
2252
|
+
let op = null;
|
|
2253
|
+
if (t.type === "PLUS") op = "+";
|
|
2254
|
+
else if (t.type === "MINUS") op = "-";
|
|
2255
|
+
else break;
|
|
2256
|
+
advance();
|
|
2257
|
+
const right = multiplication();
|
|
2258
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2259
|
+
}
|
|
2260
|
+
return left;
|
|
2261
|
+
}
|
|
2262
|
+
function multiplication() {
|
|
2263
|
+
let left = power();
|
|
2264
|
+
while (peek()) {
|
|
2265
|
+
const t = peek();
|
|
2266
|
+
let op = null;
|
|
2267
|
+
if (t.type === "MULTIPLY") op = "*";
|
|
2268
|
+
else if (t.type === "DIVIDE") op = "/";
|
|
2269
|
+
else break;
|
|
2270
|
+
advance();
|
|
2271
|
+
const right = power();
|
|
2272
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2273
|
+
}
|
|
2274
|
+
return left;
|
|
2275
|
+
}
|
|
2276
|
+
function power() {
|
|
2277
|
+
let left = unary();
|
|
2278
|
+
while (peek() && peek().type === "POWER") {
|
|
2279
|
+
advance();
|
|
2280
|
+
const right = unary();
|
|
2281
|
+
left = { kind: "binaryOp", op: "^", left, right };
|
|
2282
|
+
}
|
|
2283
|
+
return left;
|
|
2284
|
+
}
|
|
2285
|
+
function unary() {
|
|
2286
|
+
const t = peek();
|
|
2287
|
+
if (t && (t.type === "MINUS" || t.type === "PLUS")) {
|
|
2288
|
+
const op = t.type === "MINUS" ? "-" : "+";
|
|
2289
|
+
advance();
|
|
2290
|
+
const operand = unary();
|
|
2291
|
+
return { kind: "unaryOp", op, operand };
|
|
2292
|
+
}
|
|
2293
|
+
return postfix();
|
|
2294
|
+
}
|
|
2295
|
+
function postfix() {
|
|
2296
|
+
let node = primary();
|
|
2297
|
+
if (peek() && peek().type === "PERCENT") {
|
|
2298
|
+
advance();
|
|
2299
|
+
node = { kind: "binaryOp", op: "%", left: node, right: { kind: "number", value: 100 } };
|
|
2300
|
+
}
|
|
2301
|
+
return node;
|
|
2302
|
+
}
|
|
2303
|
+
function primary() {
|
|
2304
|
+
const t = peek();
|
|
2305
|
+
if (!t || t.type === "EOF") {
|
|
2306
|
+
return errorNode("Unexpected end of expression");
|
|
2307
|
+
}
|
|
2308
|
+
if (t.type === "NUMBER") {
|
|
2309
|
+
advance();
|
|
2310
|
+
return { kind: "number", value: parseFloat(t.value) };
|
|
2311
|
+
}
|
|
2312
|
+
if (t.type === "STRING") {
|
|
2313
|
+
advance();
|
|
2314
|
+
return { kind: "string", value: t.value };
|
|
2315
|
+
}
|
|
2316
|
+
if (t.type === "BOOLEAN") {
|
|
2317
|
+
advance();
|
|
2318
|
+
return { kind: "boolean", value: t.value.toUpperCase() === "TRUE" };
|
|
2319
|
+
}
|
|
2320
|
+
if (t.type === "CELL_REF") {
|
|
2321
|
+
return cellRefOrRange();
|
|
2322
|
+
}
|
|
2323
|
+
if (t.type === "FUNCTION") {
|
|
2324
|
+
return functionCall();
|
|
2325
|
+
}
|
|
2326
|
+
if (t.type === "IDENTIFIER") {
|
|
2327
|
+
return namedRangeRef();
|
|
2328
|
+
}
|
|
2329
|
+
if (t.type === "SHEET_REF") {
|
|
2330
|
+
return sheetRef();
|
|
2331
|
+
}
|
|
2332
|
+
if (t.type === "LPAREN") {
|
|
2333
|
+
advance();
|
|
2334
|
+
const node = expression();
|
|
2335
|
+
if (!expect("RPAREN")) {
|
|
2336
|
+
return errorNode("Expected closing parenthesis");
|
|
2337
|
+
}
|
|
2338
|
+
return node;
|
|
2339
|
+
}
|
|
2340
|
+
advance();
|
|
2341
|
+
return errorNode(`Unexpected token: ${t.value}`);
|
|
2342
|
+
}
|
|
2343
|
+
function cellRefOrRange() {
|
|
2344
|
+
const refToken = advance();
|
|
2345
|
+
const address = parseCellRef(refToken.value);
|
|
2346
|
+
if (!address) {
|
|
2347
|
+
return errorNode(`Invalid cell reference: ${refToken.value}`);
|
|
2348
|
+
}
|
|
2349
|
+
if (peek() && peek().type === "COLON") {
|
|
2350
|
+
advance();
|
|
2351
|
+
const endToken = expect("CELL_REF");
|
|
2352
|
+
if (!endToken) {
|
|
2353
|
+
return errorNode('Expected cell reference after ":"');
|
|
2354
|
+
}
|
|
2355
|
+
const endAddress = parseCellRef(endToken.value);
|
|
2356
|
+
if (!endAddress) {
|
|
2357
|
+
return errorNode(`Invalid cell reference: ${endToken.value}`);
|
|
2358
|
+
}
|
|
2359
|
+
return {
|
|
2360
|
+
kind: "range",
|
|
2361
|
+
start: address,
|
|
2362
|
+
end: endAddress,
|
|
2363
|
+
raw: `${refToken.value}:${endToken.value}`
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
kind: "cellRef",
|
|
2368
|
+
address,
|
|
2369
|
+
raw: refToken.value
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function functionCall() {
|
|
2373
|
+
const nameToken = advance();
|
|
2374
|
+
const name = nameToken.value.toUpperCase();
|
|
2375
|
+
if (!expect("LPAREN")) {
|
|
2376
|
+
return errorNode(`Expected "(" after function name "${name}"`);
|
|
2377
|
+
}
|
|
2378
|
+
const args = [];
|
|
2379
|
+
if (peek() && peek().type !== "RPAREN" && peek().type !== "EOF") {
|
|
2380
|
+
args.push(expression());
|
|
2381
|
+
while (peek() && peek().type === "COMMA") {
|
|
2382
|
+
advance();
|
|
2383
|
+
args.push(expression());
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (!expect("RPAREN")) {
|
|
2387
|
+
return errorNode(`Expected ")" after function arguments for "${name}"`);
|
|
2388
|
+
}
|
|
2389
|
+
return { kind: "functionCall", name, args };
|
|
2390
|
+
}
|
|
2391
|
+
function namedRangeRef() {
|
|
2392
|
+
const nameToken = advance();
|
|
2393
|
+
const name = nameToken.value.toUpperCase();
|
|
2394
|
+
const ref = namedRanges?.get(name);
|
|
2395
|
+
if (!ref) {
|
|
2396
|
+
return { kind: "error", error: new FormulaError("#NAME?", `Unknown name: ${nameToken.value}`) };
|
|
2397
|
+
}
|
|
2398
|
+
if (ref.includes(":")) {
|
|
2399
|
+
const rangeRef = parseRange(ref);
|
|
2400
|
+
if (rangeRef) {
|
|
2401
|
+
return { kind: "range", start: rangeRef.start, end: rangeRef.end, raw: ref };
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
const cellRef = parseCellRef(ref);
|
|
2405
|
+
if (cellRef) {
|
|
2406
|
+
return { kind: "cellRef", address: cellRef, raw: ref };
|
|
2407
|
+
}
|
|
2408
|
+
return { kind: "error", error: new FormulaError("#REF!", `Invalid named range reference: ${ref}`) };
|
|
2409
|
+
}
|
|
2410
|
+
function sheetRef() {
|
|
2411
|
+
const sheetToken = advance();
|
|
2412
|
+
const sheetName = sheetToken.value;
|
|
2413
|
+
const cellToken = expect("CELL_REF");
|
|
2414
|
+
if (!cellToken) {
|
|
2415
|
+
return errorNode(`Expected cell reference after sheet "${sheetName}!"`);
|
|
2416
|
+
}
|
|
2417
|
+
const address = parseCellRef(cellToken.value);
|
|
2418
|
+
if (!address) {
|
|
2419
|
+
return errorNode(`Invalid cell reference: ${cellToken.value}`);
|
|
2420
|
+
}
|
|
2421
|
+
address.sheet = sheetName;
|
|
2422
|
+
if (peek() && peek().type === "COLON") {
|
|
2423
|
+
advance();
|
|
2424
|
+
const endToken = expect("CELL_REF");
|
|
2425
|
+
if (!endToken) {
|
|
2426
|
+
return errorNode('Expected cell reference after ":"');
|
|
2427
|
+
}
|
|
2428
|
+
const endAddress = parseCellRef(endToken.value);
|
|
2429
|
+
if (!endAddress) {
|
|
2430
|
+
return errorNode(`Invalid cell reference: ${endToken.value}`);
|
|
2431
|
+
}
|
|
2432
|
+
endAddress.sheet = sheetName;
|
|
2433
|
+
return {
|
|
2434
|
+
kind: "range",
|
|
2435
|
+
start: address,
|
|
2436
|
+
end: endAddress,
|
|
2437
|
+
raw: `${sheetName}!${cellToken.value}:${endToken.value}`
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
kind: "cellRef",
|
|
2442
|
+
address,
|
|
2443
|
+
raw: `${sheetName}!${cellToken.value}`
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
const result = expression();
|
|
2447
|
+
if (peek() && peek().type !== "EOF") {
|
|
2448
|
+
return errorNode(`Unexpected token after expression: ${peek().value}`);
|
|
2449
|
+
}
|
|
2450
|
+
return result;
|
|
2451
|
+
}
|
|
2452
|
+
function toNumber(val) {
|
|
2453
|
+
if (val instanceof FormulaError) return val;
|
|
2454
|
+
if (val === null || val === void 0 || val === "") return 0;
|
|
2455
|
+
if (typeof val === "boolean") return val ? 1 : 0;
|
|
2456
|
+
if (typeof val === "number") return val;
|
|
2457
|
+
if (val instanceof Date) return val.getTime();
|
|
2458
|
+
const n = Number(val);
|
|
2459
|
+
if (isNaN(n)) return new FormulaError("#VALUE!", `Cannot convert "${val}" to number`);
|
|
2460
|
+
return n;
|
|
2461
|
+
}
|
|
2462
|
+
function toString(val) {
|
|
2463
|
+
if (val === null || val === void 0) return "";
|
|
2464
|
+
if (val instanceof FormulaError) return val.toString();
|
|
2465
|
+
if (val instanceof Date) return val.toLocaleDateString();
|
|
2466
|
+
return String(val);
|
|
2467
|
+
}
|
|
2468
|
+
function toBoolean(val) {
|
|
2469
|
+
if (typeof val === "boolean") return val;
|
|
2470
|
+
if (typeof val === "number") return val !== 0;
|
|
2471
|
+
if (typeof val === "string") {
|
|
2472
|
+
if (val.toUpperCase() === "TRUE") return true;
|
|
2473
|
+
if (val.toUpperCase() === "FALSE") return false;
|
|
2474
|
+
return val.length > 0;
|
|
2475
|
+
}
|
|
2476
|
+
return val !== null && val !== void 0;
|
|
2477
|
+
}
|
|
2478
|
+
function flattenArgs(args, context, evaluator) {
|
|
2479
|
+
const result = [];
|
|
2480
|
+
for (const arg of args) {
|
|
2481
|
+
if (arg.kind === "range") {
|
|
2482
|
+
const values = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
2483
|
+
for (const row of values) {
|
|
2484
|
+
for (const val of row) {
|
|
2485
|
+
result.push(val);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
} else {
|
|
2489
|
+
result.push(evaluator.evaluate(arg, context));
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return result;
|
|
2493
|
+
}
|
|
2494
|
+
var FormulaEvaluator = class {
|
|
2495
|
+
constructor(builtInFunctions) {
|
|
2496
|
+
this.functions = new Map(builtInFunctions);
|
|
2497
|
+
}
|
|
2498
|
+
registerFunction(name, fn) {
|
|
2499
|
+
this.functions.set(name.toUpperCase(), fn);
|
|
2500
|
+
}
|
|
2501
|
+
evaluate(node, context) {
|
|
2502
|
+
switch (node.kind) {
|
|
2503
|
+
case "number":
|
|
2504
|
+
return node.value;
|
|
2505
|
+
case "string":
|
|
2506
|
+
return node.value;
|
|
2507
|
+
case "boolean":
|
|
2508
|
+
return node.value;
|
|
2509
|
+
case "error":
|
|
2510
|
+
return node.error;
|
|
2511
|
+
case "cellRef": {
|
|
2512
|
+
const val = context.getCellValue(node.address);
|
|
2513
|
+
return val;
|
|
2514
|
+
}
|
|
2515
|
+
case "range":
|
|
2516
|
+
return context.getCellValue(node.start);
|
|
2517
|
+
case "functionCall":
|
|
2518
|
+
return this.evaluateFunction(node.name, node.args, context);
|
|
2519
|
+
case "binaryOp":
|
|
2520
|
+
return this.evaluateBinaryOp(node.op, node.left, node.right, context);
|
|
2521
|
+
case "unaryOp":
|
|
2522
|
+
return this.evaluateUnaryOp(node.op, node.operand, context);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
evaluateFunction(name, args, context) {
|
|
2526
|
+
const fn = this.functions.get(name);
|
|
2527
|
+
if (!fn) {
|
|
2528
|
+
return new FormulaError("#NAME?", `Unknown function: ${name}`);
|
|
2529
|
+
}
|
|
2530
|
+
if (args.length < fn.minArgs) {
|
|
2531
|
+
return new FormulaError("#ERROR!", `${name} requires at least ${fn.minArgs} argument(s)`);
|
|
2532
|
+
}
|
|
2533
|
+
if (fn.maxArgs >= 0 && args.length > fn.maxArgs) {
|
|
2534
|
+
return new FormulaError("#ERROR!", `${name} accepts at most ${fn.maxArgs} argument(s)`);
|
|
2535
|
+
}
|
|
2536
|
+
return fn.evaluate(args, context, this);
|
|
2537
|
+
}
|
|
2538
|
+
evaluateBinaryOp(op, left, right, context) {
|
|
2539
|
+
if (op === "&") {
|
|
2540
|
+
const l = this.evaluate(left, context);
|
|
2541
|
+
if (l instanceof FormulaError) return l;
|
|
2542
|
+
const r = this.evaluate(right, context);
|
|
2543
|
+
if (r instanceof FormulaError) return r;
|
|
2544
|
+
return toString(l) + toString(r);
|
|
2545
|
+
}
|
|
2546
|
+
if (op === ">" || op === "<" || op === ">=" || op === "<=" || op === "=" || op === "<>") {
|
|
2547
|
+
const l = this.evaluate(left, context);
|
|
2548
|
+
if (l instanceof FormulaError) return l;
|
|
2549
|
+
const r = this.evaluate(right, context);
|
|
2550
|
+
if (r instanceof FormulaError) return r;
|
|
2551
|
+
return this.compare(op, l, r);
|
|
2552
|
+
}
|
|
2553
|
+
const lVal = this.evaluate(left, context);
|
|
2554
|
+
if (lVal instanceof FormulaError) return lVal;
|
|
2555
|
+
const rVal = this.evaluate(right, context);
|
|
2556
|
+
if (rVal instanceof FormulaError) return rVal;
|
|
2557
|
+
const lNum = toNumber(lVal);
|
|
2558
|
+
if (lNum instanceof FormulaError) return lNum;
|
|
2559
|
+
const rNum = toNumber(rVal);
|
|
2560
|
+
if (rNum instanceof FormulaError) return rNum;
|
|
2561
|
+
switch (op) {
|
|
2562
|
+
case "+":
|
|
2563
|
+
return lNum + rNum;
|
|
2564
|
+
case "-":
|
|
2565
|
+
return lNum - rNum;
|
|
2566
|
+
case "*":
|
|
2567
|
+
return lNum * rNum;
|
|
2568
|
+
case "/":
|
|
2569
|
+
if (rNum === 0) return new FormulaError("#DIV/0!");
|
|
2570
|
+
return lNum / rNum;
|
|
2571
|
+
case "^":
|
|
2572
|
+
return Math.pow(lNum, rNum);
|
|
2573
|
+
case "%":
|
|
2574
|
+
return lNum * rNum / 100;
|
|
2575
|
+
default:
|
|
2576
|
+
return new FormulaError("#ERROR!", `Unknown operator: ${op}`);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
evaluateUnaryOp(op, operand, context) {
|
|
2580
|
+
const val = this.evaluate(operand, context);
|
|
2581
|
+
if (val instanceof FormulaError) return val;
|
|
2582
|
+
const num = toNumber(val);
|
|
2583
|
+
if (num instanceof FormulaError) return num;
|
|
2584
|
+
return op === "-" ? -num : num;
|
|
2585
|
+
}
|
|
2586
|
+
compare(op, left, right) {
|
|
2587
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
2588
|
+
return this.numCompare(op, left, right);
|
|
2589
|
+
}
|
|
2590
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
2591
|
+
return this.strCompare(op, left, right);
|
|
2592
|
+
}
|
|
2593
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
2594
|
+
return this.numCompare(op, left ? 1 : 0, right ? 1 : 0);
|
|
2595
|
+
}
|
|
2596
|
+
const lNum = toNumber(left);
|
|
2597
|
+
const rNum = toNumber(right);
|
|
2598
|
+
if (typeof lNum === "number" && typeof rNum === "number") {
|
|
2599
|
+
return this.numCompare(op, lNum, rNum);
|
|
2600
|
+
}
|
|
2601
|
+
return this.strCompare(op, toString(left), toString(right));
|
|
2602
|
+
}
|
|
2603
|
+
numCompare(op, a, b) {
|
|
2604
|
+
switch (op) {
|
|
2605
|
+
case ">":
|
|
2606
|
+
return a > b;
|
|
2607
|
+
case "<":
|
|
2608
|
+
return a < b;
|
|
2609
|
+
case ">=":
|
|
2610
|
+
return a >= b;
|
|
2611
|
+
case "<=":
|
|
2612
|
+
return a <= b;
|
|
2613
|
+
case "=":
|
|
2614
|
+
return a === b;
|
|
2615
|
+
case "<>":
|
|
2616
|
+
return a !== b;
|
|
2617
|
+
default:
|
|
2618
|
+
return false;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
strCompare(op, a, b) {
|
|
2622
|
+
const al = a.toLowerCase();
|
|
2623
|
+
const bl = b.toLowerCase();
|
|
2624
|
+
switch (op) {
|
|
2625
|
+
case ">":
|
|
2626
|
+
return al > bl;
|
|
2627
|
+
case "<":
|
|
2628
|
+
return al < bl;
|
|
2629
|
+
case ">=":
|
|
2630
|
+
return al >= bl;
|
|
2631
|
+
case "<=":
|
|
2632
|
+
return al <= bl;
|
|
2633
|
+
case "=":
|
|
2634
|
+
return al === bl;
|
|
2635
|
+
case "<>":
|
|
2636
|
+
return al !== bl;
|
|
2637
|
+
default:
|
|
2638
|
+
return false;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
|
|
2643
|
+
var DependencyGraph = class {
|
|
2644
|
+
constructor() {
|
|
2645
|
+
this.dependencies = /* @__PURE__ */ new Map();
|
|
2646
|
+
this.dependents = /* @__PURE__ */ new Map();
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Set the dependencies for a cell, replacing any previous ones.
|
|
2650
|
+
* Updates both the forward (dependencies) and reverse (dependents) maps.
|
|
2651
|
+
*/
|
|
2652
|
+
setDependencies(cell, deps) {
|
|
2653
|
+
this.removeDependenciesInternal(cell);
|
|
2654
|
+
this.dependencies.set(cell, deps);
|
|
2655
|
+
for (const dep of deps) {
|
|
2656
|
+
let depSet = this.dependents.get(dep);
|
|
2657
|
+
if (!depSet) {
|
|
2658
|
+
depSet = /* @__PURE__ */ new Set();
|
|
2659
|
+
this.dependents.set(dep, depSet);
|
|
2660
|
+
}
|
|
2661
|
+
depSet.add(cell);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Remove all dependency information for a cell from both maps.
|
|
2666
|
+
*/
|
|
2667
|
+
removeDependencies(cell) {
|
|
2668
|
+
this.removeDependenciesInternal(cell);
|
|
2669
|
+
this.dependencies.delete(cell);
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Get all cells that directly or transitively depend on `changedCell`,
|
|
2673
|
+
* returned in topological order (a cell appears AFTER all cells it depends on).
|
|
2674
|
+
*
|
|
2675
|
+
* Uses Kahn's algorithm. If a cycle is detected, cycle participants are
|
|
2676
|
+
* appended at the end (the engine will assign #CIRC! to them).
|
|
2677
|
+
*/
|
|
2678
|
+
getRecalcOrder(changedCell) {
|
|
2679
|
+
return this.topologicalSort(/* @__PURE__ */ new Set([changedCell]));
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Same as getRecalcOrder but for multiple changed cells.
|
|
2683
|
+
* Union of all transitive dependents, topologically sorted.
|
|
2684
|
+
*/
|
|
2685
|
+
getRecalcOrderBatch(changedCells) {
|
|
2686
|
+
return this.topologicalSort(new Set(changedCells));
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Check if adding dependencies from `cell` to `deps` would create a cycle.
|
|
2690
|
+
* DFS from each dep: if we can reach `cell`, it would create a cycle.
|
|
2691
|
+
*/
|
|
2692
|
+
wouldCreateCycle(cell, deps) {
|
|
2693
|
+
if (deps.has(cell)) {
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2697
|
+
for (const dep of deps) {
|
|
2698
|
+
if (this.canReach(dep, cell, visited)) {
|
|
2699
|
+
return true;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Return direct dependents of a cell (cells whose formulas reference this cell).
|
|
2706
|
+
* Returns an empty set if none.
|
|
2707
|
+
*/
|
|
2708
|
+
getDependents(cell) {
|
|
2709
|
+
return this.dependents.get(cell) ?? EMPTY_SET;
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Return direct dependencies of a cell (cells referenced in this cell's formula).
|
|
2713
|
+
* Returns an empty set if none.
|
|
2714
|
+
*/
|
|
2715
|
+
getDependencies(cell) {
|
|
2716
|
+
return this.dependencies.get(cell) ?? EMPTY_SET;
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Clear both maps entirely.
|
|
2720
|
+
*/
|
|
2721
|
+
clear() {
|
|
2722
|
+
this.dependencies.clear();
|
|
2723
|
+
this.dependents.clear();
|
|
2724
|
+
}
|
|
2725
|
+
// ---------------------------------------------------------------------------
|
|
2726
|
+
// Private helpers
|
|
2727
|
+
// ---------------------------------------------------------------------------
|
|
2728
|
+
/**
|
|
2729
|
+
* Remove `cell` from the forward dependencies map and clean up reverse
|
|
2730
|
+
* references in the dependents map.
|
|
2731
|
+
*/
|
|
2732
|
+
removeDependenciesInternal(cell) {
|
|
2733
|
+
const oldDeps = this.dependencies.get(cell);
|
|
2734
|
+
if (!oldDeps) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
for (const oldDep of oldDeps) {
|
|
2738
|
+
const depSet = this.dependents.get(oldDep);
|
|
2739
|
+
if (depSet) {
|
|
2740
|
+
depSet.delete(cell);
|
|
2741
|
+
if (depSet.size === 0) {
|
|
2742
|
+
this.dependents.delete(oldDep);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
this.dependencies.delete(cell);
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Iterative DFS: check if `target` is reachable from `start` by following
|
|
2750
|
+
* the dependency chain. Iterative to avoid stack overflow for deep chains.
|
|
2751
|
+
*/
|
|
2752
|
+
canReach(start, target, visited) {
|
|
2753
|
+
if (start === target) return true;
|
|
2754
|
+
if (visited.has(start)) return false;
|
|
2755
|
+
const stack = [start];
|
|
2756
|
+
visited.add(start);
|
|
2757
|
+
while (stack.length > 0) {
|
|
2758
|
+
const current = stack.pop();
|
|
2759
|
+
const deps = this.dependencies.get(current);
|
|
2760
|
+
if (!deps) continue;
|
|
2761
|
+
for (const dep of deps) {
|
|
2762
|
+
if (dep === target) return true;
|
|
2763
|
+
if (!visited.has(dep)) {
|
|
2764
|
+
visited.add(dep);
|
|
2765
|
+
stack.push(dep);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
return false;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Topological sort using Kahn's algorithm.
|
|
2773
|
+
*
|
|
2774
|
+
* 1. Collect all cells transitively dependent on the changed cell(s).
|
|
2775
|
+
* 2. Build in-degree map for these cells (count how many of their
|
|
2776
|
+
* dependencies are in the affected set).
|
|
2777
|
+
* 3. Start with cells whose in-degree is 0 (only depend on unaffected
|
|
2778
|
+
* cells or the changed cells themselves).
|
|
2779
|
+
* 4. Process queue: for each cell, reduce in-degree of its dependents,
|
|
2780
|
+
* add to queue when in-degree reaches 0.
|
|
2781
|
+
* 5. If any cells remain unprocessed, they're in a cycle — append them
|
|
2782
|
+
* at the end (engine marks as #CIRC!).
|
|
2783
|
+
*/
|
|
2784
|
+
topologicalSort(changedCells) {
|
|
2785
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2786
|
+
const bfsQueue = [];
|
|
2787
|
+
for (const changed of changedCells) {
|
|
2788
|
+
const directDependents = this.dependents.get(changed);
|
|
2789
|
+
if (directDependents) {
|
|
2790
|
+
for (const dep of directDependents) {
|
|
2791
|
+
if (!affected.has(dep)) {
|
|
2792
|
+
affected.add(dep);
|
|
2793
|
+
bfsQueue.push(dep);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
let head = 0;
|
|
2799
|
+
while (head < bfsQueue.length) {
|
|
2800
|
+
const current = bfsQueue[head++];
|
|
2801
|
+
const currentDependents = this.dependents.get(current);
|
|
2802
|
+
if (currentDependents) {
|
|
2803
|
+
for (const dep of currentDependents) {
|
|
2804
|
+
if (!affected.has(dep)) {
|
|
2805
|
+
affected.add(dep);
|
|
2806
|
+
bfsQueue.push(dep);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
if (affected.size === 0) {
|
|
2812
|
+
return [];
|
|
2813
|
+
}
|
|
2814
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (const cell of affected) {
|
|
2816
|
+
let degree = 0;
|
|
2817
|
+
const deps = this.dependencies.get(cell);
|
|
2818
|
+
if (deps) {
|
|
2819
|
+
for (const dep of deps) {
|
|
2820
|
+
if (affected.has(dep)) {
|
|
2821
|
+
degree++;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
inDegree.set(cell, degree);
|
|
2826
|
+
}
|
|
2827
|
+
const queue = [];
|
|
2828
|
+
for (const [cell, degree] of inDegree) {
|
|
2829
|
+
if (degree === 0) {
|
|
2830
|
+
queue.push(cell);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
const result = [];
|
|
2834
|
+
let queueHead = 0;
|
|
2835
|
+
while (queueHead < queue.length) {
|
|
2836
|
+
const cell = queue[queueHead++];
|
|
2837
|
+
result.push(cell);
|
|
2838
|
+
const cellDependents = this.dependents.get(cell);
|
|
2839
|
+
if (cellDependents) {
|
|
2840
|
+
for (const dependent of cellDependents) {
|
|
2841
|
+
if (affected.has(dependent)) {
|
|
2842
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
2843
|
+
inDegree.set(dependent, newDegree);
|
|
2844
|
+
if (newDegree === 0) {
|
|
2845
|
+
queue.push(dependent);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (result.length < affected.size) {
|
|
2852
|
+
const resultSet = new Set(result);
|
|
2853
|
+
for (const cell of affected) {
|
|
2854
|
+
if (!resultSet.has(cell)) {
|
|
2855
|
+
result.push(cell);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
return result;
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
function registerMathFunctions(registry) {
|
|
2863
|
+
registry.set("SUM", {
|
|
2864
|
+
minArgs: 1,
|
|
2865
|
+
maxArgs: -1,
|
|
2866
|
+
evaluate(args, context, evaluator) {
|
|
2867
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2868
|
+
let sum = 0;
|
|
2869
|
+
for (const val of values) {
|
|
2870
|
+
if (val instanceof FormulaError) return val;
|
|
2871
|
+
if (typeof val === "number") {
|
|
2872
|
+
sum += val;
|
|
2873
|
+
} else if (typeof val === "boolean") {
|
|
2874
|
+
sum += val ? 1 : 0;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return sum;
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
registry.set("AVERAGE", {
|
|
2881
|
+
minArgs: 1,
|
|
2882
|
+
maxArgs: -1,
|
|
2883
|
+
evaluate(args, context, evaluator) {
|
|
2884
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2885
|
+
let sum = 0;
|
|
2886
|
+
let count = 0;
|
|
2887
|
+
for (const val of values) {
|
|
2888
|
+
if (val instanceof FormulaError) return val;
|
|
2889
|
+
if (typeof val === "number") {
|
|
2890
|
+
sum += val;
|
|
2891
|
+
count++;
|
|
2892
|
+
} else if (typeof val === "boolean") {
|
|
2893
|
+
sum += val ? 1 : 0;
|
|
2894
|
+
count++;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No numeric values for AVERAGE");
|
|
2898
|
+
return sum / count;
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
registry.set("MIN", {
|
|
2902
|
+
minArgs: 1,
|
|
2903
|
+
maxArgs: -1,
|
|
2904
|
+
evaluate(args, context, evaluator) {
|
|
2905
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2906
|
+
let min = Infinity;
|
|
2907
|
+
for (const val of values) {
|
|
2908
|
+
if (val instanceof FormulaError) return val;
|
|
2909
|
+
if (typeof val === "number") {
|
|
2910
|
+
if (val < min) min = val;
|
|
2911
|
+
} else if (typeof val === "boolean") {
|
|
2912
|
+
const n = val ? 1 : 0;
|
|
2913
|
+
if (n < min) min = n;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return min === Infinity ? 0 : min;
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
registry.set("MAX", {
|
|
2920
|
+
minArgs: 1,
|
|
2921
|
+
maxArgs: -1,
|
|
2922
|
+
evaluate(args, context, evaluator) {
|
|
2923
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2924
|
+
let max = -Infinity;
|
|
2925
|
+
for (const val of values) {
|
|
2926
|
+
if (val instanceof FormulaError) return val;
|
|
2927
|
+
if (typeof val === "number") {
|
|
2928
|
+
if (val > max) max = val;
|
|
2929
|
+
} else if (typeof val === "boolean") {
|
|
2930
|
+
const n = val ? 1 : 0;
|
|
2931
|
+
if (n > max) max = n;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return max === -Infinity ? 0 : max;
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
registry.set("COUNT", {
|
|
2938
|
+
minArgs: 1,
|
|
2939
|
+
maxArgs: -1,
|
|
2940
|
+
evaluate(args, context, evaluator) {
|
|
2941
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2942
|
+
let count = 0;
|
|
2943
|
+
for (const val of values) {
|
|
2944
|
+
if (val instanceof FormulaError) return val;
|
|
2945
|
+
if (typeof val === "number") {
|
|
2946
|
+
count++;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return count;
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
registry.set("COUNTA", {
|
|
2953
|
+
minArgs: 1,
|
|
2954
|
+
maxArgs: -1,
|
|
2955
|
+
evaluate(args, context, evaluator) {
|
|
2956
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2957
|
+
let count = 0;
|
|
2958
|
+
for (const val of values) {
|
|
2959
|
+
if (val instanceof FormulaError) return val;
|
|
2960
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
2961
|
+
count++;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return count;
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
registry.set("ROUND", {
|
|
2968
|
+
minArgs: 2,
|
|
2969
|
+
maxArgs: 2,
|
|
2970
|
+
evaluate(args, context, evaluator) {
|
|
2971
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2972
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2973
|
+
const num = toNumber(rawNum);
|
|
2974
|
+
if (num instanceof FormulaError) return num;
|
|
2975
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
2976
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
2977
|
+
const digits = toNumber(rawDigits);
|
|
2978
|
+
if (digits instanceof FormulaError) return digits;
|
|
2979
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
2980
|
+
return Math.round(num * factor) / factor;
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
registry.set("ABS", {
|
|
2984
|
+
minArgs: 1,
|
|
2985
|
+
maxArgs: 1,
|
|
2986
|
+
evaluate(args, context, evaluator) {
|
|
2987
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
2988
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
2989
|
+
const num = toNumber(rawVal);
|
|
2990
|
+
if (num instanceof FormulaError) return num;
|
|
2991
|
+
return Math.abs(num);
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
registry.set("CEILING", {
|
|
2995
|
+
minArgs: 2,
|
|
2996
|
+
maxArgs: 2,
|
|
2997
|
+
evaluate(args, context, evaluator) {
|
|
2998
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2999
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3000
|
+
const num = toNumber(rawNum);
|
|
3001
|
+
if (num instanceof FormulaError) return num;
|
|
3002
|
+
const rawSig = evaluator.evaluate(args[1], context);
|
|
3003
|
+
if (rawSig instanceof FormulaError) return rawSig;
|
|
3004
|
+
const significance = toNumber(rawSig);
|
|
3005
|
+
if (significance instanceof FormulaError) return significance;
|
|
3006
|
+
if (significance === 0) return 0;
|
|
3007
|
+
return Math.ceil(num / significance) * significance;
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
registry.set("FLOOR", {
|
|
3011
|
+
minArgs: 2,
|
|
3012
|
+
maxArgs: 2,
|
|
3013
|
+
evaluate(args, context, evaluator) {
|
|
3014
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3015
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3016
|
+
const num = toNumber(rawNum);
|
|
3017
|
+
if (num instanceof FormulaError) return num;
|
|
3018
|
+
const rawSig = evaluator.evaluate(args[1], context);
|
|
3019
|
+
if (rawSig instanceof FormulaError) return rawSig;
|
|
3020
|
+
const significance = toNumber(rawSig);
|
|
3021
|
+
if (significance instanceof FormulaError) return significance;
|
|
3022
|
+
if (significance === 0) return 0;
|
|
3023
|
+
return Math.floor(num / significance) * significance;
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
registry.set("MOD", {
|
|
3027
|
+
minArgs: 2,
|
|
3028
|
+
maxArgs: 2,
|
|
3029
|
+
evaluate(args, context, evaluator) {
|
|
3030
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3031
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3032
|
+
const num = toNumber(rawNum);
|
|
3033
|
+
if (num instanceof FormulaError) return num;
|
|
3034
|
+
const rawDiv = evaluator.evaluate(args[1], context);
|
|
3035
|
+
if (rawDiv instanceof FormulaError) return rawDiv;
|
|
3036
|
+
const divisor = toNumber(rawDiv);
|
|
3037
|
+
if (divisor instanceof FormulaError) return divisor;
|
|
3038
|
+
if (divisor === 0) return new FormulaError("#DIV/0!", "Division by zero in MOD");
|
|
3039
|
+
return num % divisor;
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
registry.set("POWER", {
|
|
3043
|
+
minArgs: 2,
|
|
3044
|
+
maxArgs: 2,
|
|
3045
|
+
evaluate(args, context, evaluator) {
|
|
3046
|
+
const rawBase = evaluator.evaluate(args[0], context);
|
|
3047
|
+
if (rawBase instanceof FormulaError) return rawBase;
|
|
3048
|
+
const base = toNumber(rawBase);
|
|
3049
|
+
if (base instanceof FormulaError) return base;
|
|
3050
|
+
const rawExp = evaluator.evaluate(args[1], context);
|
|
3051
|
+
if (rawExp instanceof FormulaError) return rawExp;
|
|
3052
|
+
const exponent = toNumber(rawExp);
|
|
3053
|
+
if (exponent instanceof FormulaError) return exponent;
|
|
3054
|
+
return Math.pow(base, exponent);
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
registry.set("SQRT", {
|
|
3058
|
+
minArgs: 1,
|
|
3059
|
+
maxArgs: 1,
|
|
3060
|
+
evaluate(args, context, evaluator) {
|
|
3061
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3062
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3063
|
+
const num = toNumber(rawVal);
|
|
3064
|
+
if (num instanceof FormulaError) return num;
|
|
3065
|
+
if (num < 0) return new FormulaError("#VALUE!", "Cannot take square root of negative number");
|
|
3066
|
+
return Math.sqrt(num);
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
registry.set("ROUNDUP", {
|
|
3070
|
+
minArgs: 2,
|
|
3071
|
+
maxArgs: 2,
|
|
3072
|
+
evaluate(args, context, evaluator) {
|
|
3073
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3074
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3075
|
+
const num = toNumber(rawNum);
|
|
3076
|
+
if (num instanceof FormulaError) return num;
|
|
3077
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
3078
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
3079
|
+
const digits = toNumber(rawDigits);
|
|
3080
|
+
if (digits instanceof FormulaError) return digits;
|
|
3081
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
3082
|
+
return num >= 0 ? Math.ceil(num * factor) / factor : Math.floor(num * factor) / factor;
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
registry.set("ROUNDDOWN", {
|
|
3086
|
+
minArgs: 2,
|
|
3087
|
+
maxArgs: 2,
|
|
3088
|
+
evaluate(args, context, evaluator) {
|
|
3089
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3090
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3091
|
+
const num = toNumber(rawNum);
|
|
3092
|
+
if (num instanceof FormulaError) return num;
|
|
3093
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
3094
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
3095
|
+
const digits = toNumber(rawDigits);
|
|
3096
|
+
if (digits instanceof FormulaError) return digits;
|
|
3097
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
3098
|
+
return Math.trunc(num * factor) / factor;
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
registry.set("INT", {
|
|
3102
|
+
minArgs: 1,
|
|
3103
|
+
maxArgs: 1,
|
|
3104
|
+
evaluate(args, context, evaluator) {
|
|
3105
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3106
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3107
|
+
const num = toNumber(rawVal);
|
|
3108
|
+
if (num instanceof FormulaError) return num;
|
|
3109
|
+
return Math.floor(num);
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
registry.set("TRUNC", {
|
|
3113
|
+
minArgs: 1,
|
|
3114
|
+
maxArgs: 2,
|
|
3115
|
+
evaluate(args, context, evaluator) {
|
|
3116
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3117
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3118
|
+
const num = toNumber(rawVal);
|
|
3119
|
+
if (num instanceof FormulaError) return num;
|
|
3120
|
+
let digits = 0;
|
|
3121
|
+
if (args.length >= 2) {
|
|
3122
|
+
const rawD = evaluator.evaluate(args[1], context);
|
|
3123
|
+
if (rawD instanceof FormulaError) return rawD;
|
|
3124
|
+
const d = toNumber(rawD);
|
|
3125
|
+
if (d instanceof FormulaError) return d;
|
|
3126
|
+
digits = Math.trunc(d);
|
|
3127
|
+
}
|
|
3128
|
+
const factor = Math.pow(10, digits);
|
|
3129
|
+
return Math.trunc(num * factor) / factor;
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
registry.set("PRODUCT", {
|
|
3133
|
+
minArgs: 1,
|
|
3134
|
+
maxArgs: -1,
|
|
3135
|
+
evaluate(args, context, evaluator) {
|
|
3136
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3137
|
+
let product = 1;
|
|
3138
|
+
let hasNumber = false;
|
|
3139
|
+
for (const val of values) {
|
|
3140
|
+
if (val instanceof FormulaError) return val;
|
|
3141
|
+
if (typeof val === "number") {
|
|
3142
|
+
product *= val;
|
|
3143
|
+
hasNumber = true;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
return hasNumber ? product : 0;
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
registry.set("SUMPRODUCT", {
|
|
3150
|
+
minArgs: 1,
|
|
3151
|
+
maxArgs: -1,
|
|
3152
|
+
evaluate(args, context, evaluator) {
|
|
3153
|
+
const arrays = [];
|
|
3154
|
+
for (const arg of args) {
|
|
3155
|
+
if (arg.kind !== "range") {
|
|
3156
|
+
return new FormulaError("#VALUE!", "SUMPRODUCT arguments must be ranges");
|
|
3157
|
+
}
|
|
3158
|
+
arrays.push(context.getRangeValues({ start: arg.start, end: arg.end }));
|
|
3159
|
+
}
|
|
3160
|
+
if (arrays.length === 0) return 0;
|
|
3161
|
+
const rows = arrays[0].length;
|
|
3162
|
+
const cols = rows > 0 ? arrays[0][0].length : 0;
|
|
3163
|
+
for (let a = 1; a < arrays.length; a++) {
|
|
3164
|
+
if (arrays[a].length !== rows || rows > 0 && arrays[a][0].length !== cols) {
|
|
3165
|
+
return new FormulaError("#VALUE!", "SUMPRODUCT arrays must have same dimensions");
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
let sum = 0;
|
|
3169
|
+
for (let r = 0; r < rows; r++) {
|
|
3170
|
+
for (let c = 0; c < cols; c++) {
|
|
3171
|
+
let product = 1;
|
|
3172
|
+
for (let a = 0; a < arrays.length; a++) {
|
|
3173
|
+
const v = toNumber(arrays[a][r][c]);
|
|
3174
|
+
if (v instanceof FormulaError) {
|
|
3175
|
+
product = 0;
|
|
3176
|
+
break;
|
|
3177
|
+
}
|
|
3178
|
+
product *= v;
|
|
3179
|
+
}
|
|
3180
|
+
sum += product;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return sum;
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
registry.set("MEDIAN", {
|
|
3187
|
+
minArgs: 1,
|
|
3188
|
+
maxArgs: -1,
|
|
3189
|
+
evaluate(args, context, evaluator) {
|
|
3190
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3191
|
+
const nums = [];
|
|
3192
|
+
for (const val of values) {
|
|
3193
|
+
if (val instanceof FormulaError) return val;
|
|
3194
|
+
if (typeof val === "number") nums.push(val);
|
|
3195
|
+
}
|
|
3196
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "No numeric values for MEDIAN");
|
|
3197
|
+
nums.sort((a, b) => a - b);
|
|
3198
|
+
const mid = Math.floor(nums.length / 2);
|
|
3199
|
+
return nums.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
registry.set("LARGE", {
|
|
3203
|
+
minArgs: 2,
|
|
3204
|
+
maxArgs: 2,
|
|
3205
|
+
evaluate(args, context, evaluator) {
|
|
3206
|
+
if (args[0].kind !== "range") {
|
|
3207
|
+
return new FormulaError("#VALUE!", "LARGE first argument must be a range");
|
|
3208
|
+
}
|
|
3209
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3210
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3211
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3212
|
+
const k = toNumber(rawK);
|
|
3213
|
+
if (k instanceof FormulaError) return k;
|
|
3214
|
+
const nums = [];
|
|
3215
|
+
for (const row of rangeData) {
|
|
3216
|
+
for (const cell of row) {
|
|
3217
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
const ki = Math.trunc(k);
|
|
3221
|
+
if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "LARGE k out of range");
|
|
3222
|
+
nums.sort((a, b) => b - a);
|
|
3223
|
+
return nums[ki - 1];
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
registry.set("SMALL", {
|
|
3227
|
+
minArgs: 2,
|
|
3228
|
+
maxArgs: 2,
|
|
3229
|
+
evaluate(args, context, evaluator) {
|
|
3230
|
+
if (args[0].kind !== "range") {
|
|
3231
|
+
return new FormulaError("#VALUE!", "SMALL first argument must be a range");
|
|
3232
|
+
}
|
|
3233
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3234
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3235
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3236
|
+
const k = toNumber(rawK);
|
|
3237
|
+
if (k instanceof FormulaError) return k;
|
|
3238
|
+
const nums = [];
|
|
3239
|
+
for (const row of rangeData) {
|
|
3240
|
+
for (const cell of row) {
|
|
3241
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
const ki = Math.trunc(k);
|
|
3245
|
+
if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "SMALL k out of range");
|
|
3246
|
+
nums.sort((a, b) => a - b);
|
|
3247
|
+
return nums[ki - 1];
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
registry.set("RANK", {
|
|
3251
|
+
minArgs: 2,
|
|
3252
|
+
maxArgs: 3,
|
|
3253
|
+
evaluate(args, context, evaluator) {
|
|
3254
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3255
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3256
|
+
const num = toNumber(rawNum);
|
|
3257
|
+
if (num instanceof FormulaError) return num;
|
|
3258
|
+
if (args[1].kind !== "range") {
|
|
3259
|
+
return new FormulaError("#VALUE!", "RANK second argument must be a range");
|
|
3260
|
+
}
|
|
3261
|
+
const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3262
|
+
let order = 0;
|
|
3263
|
+
if (args.length >= 3) {
|
|
3264
|
+
const rawO = evaluator.evaluate(args[2], context);
|
|
3265
|
+
if (rawO instanceof FormulaError) return rawO;
|
|
3266
|
+
const o = toNumber(rawO);
|
|
3267
|
+
if (o instanceof FormulaError) return o;
|
|
3268
|
+
order = o;
|
|
3269
|
+
}
|
|
3270
|
+
const nums = [];
|
|
3271
|
+
for (const row of rangeData) {
|
|
3272
|
+
for (const cell of row) {
|
|
3273
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
if (!nums.includes(num)) return new FormulaError("#N/A", "RANK value not found in range");
|
|
3277
|
+
let rank = 1;
|
|
3278
|
+
for (const n of nums) {
|
|
3279
|
+
if (order === 0 ? n > num : n < num) rank++;
|
|
3280
|
+
}
|
|
3281
|
+
return rank;
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
registry.set("SIGN", {
|
|
3285
|
+
minArgs: 1,
|
|
3286
|
+
maxArgs: 1,
|
|
3287
|
+
evaluate(args, context, evaluator) {
|
|
3288
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3289
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3290
|
+
const num = toNumber(rawVal);
|
|
3291
|
+
if (num instanceof FormulaError) return num;
|
|
3292
|
+
return num > 0 ? 1 : num < 0 ? -1 : 0;
|
|
3293
|
+
}
|
|
3294
|
+
});
|
|
3295
|
+
registry.set("LOG", {
|
|
3296
|
+
minArgs: 1,
|
|
3297
|
+
maxArgs: 2,
|
|
3298
|
+
evaluate(args, context, evaluator) {
|
|
3299
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3300
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3301
|
+
const num = toNumber(rawVal);
|
|
3302
|
+
if (num instanceof FormulaError) return num;
|
|
3303
|
+
if (num <= 0) return new FormulaError("#NUM!", "LOG requires a positive number");
|
|
3304
|
+
let base = 10;
|
|
3305
|
+
if (args.length >= 2) {
|
|
3306
|
+
const rawB = evaluator.evaluate(args[1], context);
|
|
3307
|
+
if (rawB instanceof FormulaError) return rawB;
|
|
3308
|
+
const b = toNumber(rawB);
|
|
3309
|
+
if (b instanceof FormulaError) return b;
|
|
3310
|
+
if (b <= 0 || b === 1) return new FormulaError("#NUM!", "LOG base must be positive and not 1");
|
|
3311
|
+
base = b;
|
|
3312
|
+
}
|
|
3313
|
+
return Math.log(num) / Math.log(base);
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
3316
|
+
registry.set("LN", {
|
|
3317
|
+
minArgs: 1,
|
|
3318
|
+
maxArgs: 1,
|
|
3319
|
+
evaluate(args, context, evaluator) {
|
|
3320
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3321
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3322
|
+
const num = toNumber(rawVal);
|
|
3323
|
+
if (num instanceof FormulaError) return num;
|
|
3324
|
+
if (num <= 0) return new FormulaError("#NUM!", "LN requires a positive number");
|
|
3325
|
+
return Math.log(num);
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3328
|
+
registry.set("EXP", {
|
|
3329
|
+
minArgs: 1,
|
|
3330
|
+
maxArgs: 1,
|
|
3331
|
+
evaluate(args, context, evaluator) {
|
|
3332
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3333
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3334
|
+
const num = toNumber(rawVal);
|
|
3335
|
+
if (num instanceof FormulaError) return num;
|
|
3336
|
+
return Math.exp(num);
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
registry.set("PI", {
|
|
3340
|
+
minArgs: 0,
|
|
3341
|
+
maxArgs: 0,
|
|
3342
|
+
evaluate() {
|
|
3343
|
+
return Math.PI;
|
|
3344
|
+
}
|
|
3345
|
+
});
|
|
3346
|
+
registry.set("RAND", {
|
|
3347
|
+
minArgs: 0,
|
|
3348
|
+
maxArgs: 0,
|
|
3349
|
+
evaluate() {
|
|
3350
|
+
return Math.random();
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
registry.set("RANDBETWEEN", {
|
|
3354
|
+
minArgs: 2,
|
|
3355
|
+
maxArgs: 2,
|
|
3356
|
+
evaluate(args, context, evaluator) {
|
|
3357
|
+
const rawLow = evaluator.evaluate(args[0], context);
|
|
3358
|
+
if (rawLow instanceof FormulaError) return rawLow;
|
|
3359
|
+
const low = toNumber(rawLow);
|
|
3360
|
+
if (low instanceof FormulaError) return low;
|
|
3361
|
+
const rawHigh = evaluator.evaluate(args[1], context);
|
|
3362
|
+
if (rawHigh instanceof FormulaError) return rawHigh;
|
|
3363
|
+
const high = toNumber(rawHigh);
|
|
3364
|
+
if (high instanceof FormulaError) return high;
|
|
3365
|
+
const lo = Math.ceil(low);
|
|
3366
|
+
const hi = Math.floor(high);
|
|
3367
|
+
if (lo > hi) return new FormulaError("#NUM!", "RANDBETWEEN bottom must be <= top");
|
|
3368
|
+
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
function flattenArgs2(args, context, evaluator) {
|
|
3373
|
+
const result = [];
|
|
3374
|
+
for (const arg of args) {
|
|
3375
|
+
if (arg.kind === "range") {
|
|
3376
|
+
const values = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
3377
|
+
for (const row of values) {
|
|
3378
|
+
for (const cell of row) {
|
|
3379
|
+
result.push(cell);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
} else {
|
|
3383
|
+
result.push(evaluator.evaluate(arg, context));
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return result;
|
|
3387
|
+
}
|
|
3388
|
+
function registerLogicalFunctions(registry) {
|
|
3389
|
+
registry.set("IF", {
|
|
3390
|
+
minArgs: 2,
|
|
3391
|
+
maxArgs: 3,
|
|
3392
|
+
evaluate(args, context, evaluator) {
|
|
3393
|
+
const condition = evaluator.evaluate(args[0], context);
|
|
3394
|
+
if (condition instanceof FormulaError) return condition;
|
|
3395
|
+
if (condition) {
|
|
3396
|
+
return evaluator.evaluate(args[1], context);
|
|
3397
|
+
} else {
|
|
3398
|
+
if (args.length >= 3) {
|
|
3399
|
+
return evaluator.evaluate(args[2], context);
|
|
3400
|
+
}
|
|
3401
|
+
return false;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
registry.set("AND", {
|
|
3406
|
+
minArgs: 1,
|
|
3407
|
+
maxArgs: -1,
|
|
3408
|
+
evaluate(args, context, evaluator) {
|
|
3409
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3410
|
+
for (const val of values) {
|
|
3411
|
+
if (val instanceof FormulaError) return val;
|
|
3412
|
+
if (!val) return false;
|
|
3413
|
+
}
|
|
3414
|
+
return true;
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
registry.set("OR", {
|
|
3418
|
+
minArgs: 1,
|
|
3419
|
+
maxArgs: -1,
|
|
3420
|
+
evaluate(args, context, evaluator) {
|
|
3421
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3422
|
+
for (const val of values) {
|
|
3423
|
+
if (val instanceof FormulaError) return val;
|
|
3424
|
+
if (val) return true;
|
|
3425
|
+
}
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3428
|
+
});
|
|
3429
|
+
registry.set("NOT", {
|
|
3430
|
+
minArgs: 1,
|
|
3431
|
+
maxArgs: 1,
|
|
3432
|
+
evaluate(args, context, evaluator) {
|
|
3433
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3434
|
+
if (val instanceof FormulaError) return val;
|
|
3435
|
+
return !val;
|
|
3436
|
+
}
|
|
3437
|
+
});
|
|
3438
|
+
registry.set("IFERROR", {
|
|
3439
|
+
minArgs: 2,
|
|
3440
|
+
maxArgs: 2,
|
|
3441
|
+
evaluate(args, context, evaluator) {
|
|
3442
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3443
|
+
if (val instanceof FormulaError) {
|
|
3444
|
+
return evaluator.evaluate(args[1], context);
|
|
3445
|
+
}
|
|
3446
|
+
return val;
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
registry.set("IFNA", {
|
|
3450
|
+
minArgs: 2,
|
|
3451
|
+
maxArgs: 2,
|
|
3452
|
+
evaluate(args, context, evaluator) {
|
|
3453
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3454
|
+
if (val instanceof FormulaError && val.type === "#N/A") {
|
|
3455
|
+
return evaluator.evaluate(args[1], context);
|
|
3456
|
+
}
|
|
3457
|
+
return val;
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
registry.set("IFS", {
|
|
3461
|
+
minArgs: 2,
|
|
3462
|
+
maxArgs: -1,
|
|
3463
|
+
evaluate(args, context, evaluator) {
|
|
3464
|
+
if (args.length % 2 !== 0) {
|
|
3465
|
+
return new FormulaError("#VALUE!", "IFS requires pairs of condition, value");
|
|
3466
|
+
}
|
|
3467
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
3468
|
+
const condition = evaluator.evaluate(args[i], context);
|
|
3469
|
+
if (condition instanceof FormulaError) return condition;
|
|
3470
|
+
if (condition) {
|
|
3471
|
+
return evaluator.evaluate(args[i + 1], context);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return new FormulaError("#N/A", "IFS no condition was TRUE");
|
|
3475
|
+
}
|
|
3476
|
+
});
|
|
3477
|
+
registry.set("SWITCH", {
|
|
3478
|
+
minArgs: 3,
|
|
3479
|
+
maxArgs: -1,
|
|
3480
|
+
evaluate(args, context, evaluator) {
|
|
3481
|
+
const expr = evaluator.evaluate(args[0], context);
|
|
3482
|
+
if (expr instanceof FormulaError) return expr;
|
|
3483
|
+
const hasDefault = (args.length - 1) % 2 !== 0;
|
|
3484
|
+
const pairCount = hasDefault ? (args.length - 2) / 2 : (args.length - 1) / 2;
|
|
3485
|
+
for (let i = 0; i < pairCount; i++) {
|
|
3486
|
+
const caseVal = evaluator.evaluate(args[1 + i * 2], context);
|
|
3487
|
+
if (caseVal instanceof FormulaError) return caseVal;
|
|
3488
|
+
if (expr === caseVal) {
|
|
3489
|
+
return evaluator.evaluate(args[2 + i * 2], context);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (hasDefault) {
|
|
3493
|
+
return evaluator.evaluate(args[args.length - 1], context);
|
|
3494
|
+
}
|
|
3495
|
+
return new FormulaError("#N/A", "SWITCH no match found");
|
|
3496
|
+
}
|
|
3497
|
+
});
|
|
3498
|
+
registry.set("CHOOSE", {
|
|
3499
|
+
minArgs: 2,
|
|
3500
|
+
maxArgs: -1,
|
|
3501
|
+
evaluate(args, context, evaluator) {
|
|
3502
|
+
const rawIdx = evaluator.evaluate(args[0], context);
|
|
3503
|
+
if (rawIdx instanceof FormulaError) return rawIdx;
|
|
3504
|
+
if (typeof rawIdx !== "number") return new FormulaError("#VALUE!", "CHOOSE index must be a number");
|
|
3505
|
+
const idx = Math.trunc(rawIdx);
|
|
3506
|
+
if (idx < 1 || idx >= args.length) {
|
|
3507
|
+
return new FormulaError("#VALUE!", "CHOOSE index out of range");
|
|
3508
|
+
}
|
|
3509
|
+
return evaluator.evaluate(args[idx], context);
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
registry.set("XOR", {
|
|
3513
|
+
minArgs: 1,
|
|
3514
|
+
maxArgs: -1,
|
|
3515
|
+
evaluate(args, context, evaluator) {
|
|
3516
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3517
|
+
let trueCount = 0;
|
|
3518
|
+
for (const val of values) {
|
|
3519
|
+
if (val instanceof FormulaError) return val;
|
|
3520
|
+
if (val) trueCount++;
|
|
3521
|
+
}
|
|
3522
|
+
return trueCount % 2 === 1;
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
function registerLookupFunctions(registry) {
|
|
3527
|
+
registry.set("VLOOKUP", {
|
|
3528
|
+
minArgs: 3,
|
|
3529
|
+
maxArgs: 4,
|
|
3530
|
+
evaluate(args, context, evaluator) {
|
|
3531
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3532
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3533
|
+
if (args[1].kind !== "range") {
|
|
3534
|
+
return new FormulaError("#VALUE!", "VLOOKUP table_array must be a range");
|
|
3535
|
+
}
|
|
3536
|
+
const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3537
|
+
const rawColIndex = evaluator.evaluate(args[2], context);
|
|
3538
|
+
if (rawColIndex instanceof FormulaError) return rawColIndex;
|
|
3539
|
+
const colIndex = toNumber(rawColIndex);
|
|
3540
|
+
if (colIndex instanceof FormulaError) return colIndex;
|
|
3541
|
+
if (colIndex < 1) return new FormulaError("#VALUE!", "VLOOKUP col_index must be >= 1");
|
|
3542
|
+
if (tableData.length > 0 && colIndex > tableData[0].length) {
|
|
3543
|
+
return new FormulaError("#REF!", "VLOOKUP col_index exceeds table columns");
|
|
3544
|
+
}
|
|
3545
|
+
let rangeLookup = true;
|
|
3546
|
+
if (args.length >= 4) {
|
|
3547
|
+
const rawRL = evaluator.evaluate(args[3], context);
|
|
3548
|
+
if (rawRL instanceof FormulaError) return rawRL;
|
|
3549
|
+
rangeLookup = !!rawRL;
|
|
3550
|
+
}
|
|
3551
|
+
const col = Math.trunc(colIndex) - 1;
|
|
3552
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3553
|
+
if (rangeLookup) {
|
|
3554
|
+
let bestRow = -1;
|
|
3555
|
+
for (let r = 0; r < tableData.length; r++) {
|
|
3556
|
+
const cellVal = tableData[r][0];
|
|
3557
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3558
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3559
|
+
if (cellVal <= lookupValue) bestRow = r;
|
|
3560
|
+
else break;
|
|
3561
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3562
|
+
if (cellVal.toLowerCase() <= lookupLower) bestRow = r;
|
|
3563
|
+
else break;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
if (bestRow === -1) return new FormulaError("#N/A", "VLOOKUP no match found");
|
|
3567
|
+
return tableData[bestRow][col] ?? null;
|
|
3568
|
+
} else {
|
|
3569
|
+
for (let r = 0; r < tableData.length; r++) {
|
|
3570
|
+
const cellVal = tableData[r][0];
|
|
3571
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3572
|
+
if (cellVal.toLowerCase() === lookupLower) {
|
|
3573
|
+
return tableData[r][col] ?? null;
|
|
3574
|
+
}
|
|
3575
|
+
} else if (cellVal === lookupValue) {
|
|
3576
|
+
return tableData[r][col] ?? null;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return new FormulaError("#N/A", "VLOOKUP no exact match found");
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
});
|
|
3583
|
+
registry.set("INDEX", {
|
|
3584
|
+
minArgs: 2,
|
|
3585
|
+
maxArgs: 3,
|
|
3586
|
+
evaluate(args, context, evaluator) {
|
|
3587
|
+
if (args[0].kind !== "range") {
|
|
3588
|
+
return new FormulaError("#VALUE!", "INDEX first argument must be a range");
|
|
3589
|
+
}
|
|
3590
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3591
|
+
const rawRow = evaluator.evaluate(args[1], context);
|
|
3592
|
+
if (rawRow instanceof FormulaError) return rawRow;
|
|
3593
|
+
const rowNum = toNumber(rawRow);
|
|
3594
|
+
if (rowNum instanceof FormulaError) return rowNum;
|
|
3595
|
+
let colNum = 1;
|
|
3596
|
+
if (args.length >= 3) {
|
|
3597
|
+
const rawCol = evaluator.evaluate(args[2], context);
|
|
3598
|
+
if (rawCol instanceof FormulaError) return rawCol;
|
|
3599
|
+
const c2 = toNumber(rawCol);
|
|
3600
|
+
if (c2 instanceof FormulaError) return c2;
|
|
3601
|
+
colNum = c2;
|
|
3602
|
+
}
|
|
3603
|
+
const r = Math.trunc(rowNum) - 1;
|
|
3604
|
+
const c = Math.trunc(colNum) - 1;
|
|
3605
|
+
if (r < 0 || r >= rangeData.length) {
|
|
3606
|
+
return new FormulaError("#REF!", "INDEX row out of bounds");
|
|
3607
|
+
}
|
|
3608
|
+
if (c < 0 || rangeData.length > 0 && c >= rangeData[0].length) {
|
|
3609
|
+
return new FormulaError("#REF!", "INDEX column out of bounds");
|
|
3610
|
+
}
|
|
3611
|
+
return rangeData[r][c] ?? null;
|
|
3612
|
+
}
|
|
3613
|
+
});
|
|
3614
|
+
registry.set("HLOOKUP", {
|
|
3615
|
+
minArgs: 3,
|
|
3616
|
+
maxArgs: 4,
|
|
3617
|
+
evaluate(args, context, evaluator) {
|
|
3618
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3619
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3620
|
+
if (args[1].kind !== "range") {
|
|
3621
|
+
return new FormulaError("#VALUE!", "HLOOKUP table_array must be a range");
|
|
3622
|
+
}
|
|
3623
|
+
const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3624
|
+
const rawRowIndex = evaluator.evaluate(args[2], context);
|
|
3625
|
+
if (rawRowIndex instanceof FormulaError) return rawRowIndex;
|
|
3626
|
+
const rowIndex = toNumber(rawRowIndex);
|
|
3627
|
+
if (rowIndex instanceof FormulaError) return rowIndex;
|
|
3628
|
+
if (rowIndex < 1) return new FormulaError("#VALUE!", "HLOOKUP row_index must be >= 1");
|
|
3629
|
+
if (rowIndex > tableData.length) {
|
|
3630
|
+
return new FormulaError("#REF!", "HLOOKUP row_index exceeds table rows");
|
|
3631
|
+
}
|
|
3632
|
+
let rangeLookup = true;
|
|
3633
|
+
if (args.length >= 4) {
|
|
3634
|
+
const rawRL = evaluator.evaluate(args[3], context);
|
|
3635
|
+
if (rawRL instanceof FormulaError) return rawRL;
|
|
3636
|
+
rangeLookup = !!rawRL;
|
|
3637
|
+
}
|
|
3638
|
+
const row = Math.trunc(rowIndex) - 1;
|
|
3639
|
+
const firstRow = tableData[0] || [];
|
|
3640
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3641
|
+
if (rangeLookup) {
|
|
3642
|
+
let bestCol = -1;
|
|
3643
|
+
for (let c = 0; c < firstRow.length; c++) {
|
|
3644
|
+
const cellVal = firstRow[c];
|
|
3645
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3646
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3647
|
+
if (cellVal <= lookupValue) bestCol = c;
|
|
3648
|
+
else break;
|
|
3649
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3650
|
+
if (cellVal.toLowerCase() <= lookupLower) bestCol = c;
|
|
3651
|
+
else break;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
if (bestCol === -1) return new FormulaError("#N/A", "HLOOKUP no match found");
|
|
3655
|
+
return tableData[row][bestCol] ?? null;
|
|
3656
|
+
} else {
|
|
3657
|
+
for (let c = 0; c < firstRow.length; c++) {
|
|
3658
|
+
const cellVal = firstRow[c];
|
|
3659
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3660
|
+
if (cellVal.toLowerCase() === lookupLower) return tableData[row][c] ?? null;
|
|
3661
|
+
} else if (cellVal === lookupValue) {
|
|
3662
|
+
return tableData[row][c] ?? null;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return new FormulaError("#N/A", "HLOOKUP no exact match found");
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
});
|
|
3669
|
+
registry.set("XLOOKUP", {
|
|
3670
|
+
minArgs: 3,
|
|
3671
|
+
maxArgs: 6,
|
|
3672
|
+
evaluate(args, context, evaluator) {
|
|
3673
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3674
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3675
|
+
if (args[1].kind !== "range") {
|
|
3676
|
+
return new FormulaError("#VALUE!", "XLOOKUP lookup_array must be a range");
|
|
3677
|
+
}
|
|
3678
|
+
const lookupArray = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3679
|
+
if (args[2].kind !== "range") {
|
|
3680
|
+
return new FormulaError("#VALUE!", "XLOOKUP return_array must be a range");
|
|
3681
|
+
}
|
|
3682
|
+
const returnArray = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
3683
|
+
let ifNotFound = new FormulaError("#N/A", "XLOOKUP no match found");
|
|
3684
|
+
if (args.length >= 4) {
|
|
3685
|
+
ifNotFound = evaluator.evaluate(args[3], context);
|
|
3686
|
+
}
|
|
3687
|
+
let matchMode = 0;
|
|
3688
|
+
if (args.length >= 5) {
|
|
3689
|
+
const rawMM = evaluator.evaluate(args[4], context);
|
|
3690
|
+
if (rawMM instanceof FormulaError) return rawMM;
|
|
3691
|
+
const mm = toNumber(rawMM);
|
|
3692
|
+
if (mm instanceof FormulaError) return mm;
|
|
3693
|
+
matchMode = Math.trunc(mm);
|
|
3694
|
+
}
|
|
3695
|
+
let searchMode = 1;
|
|
3696
|
+
if (args.length >= 6) {
|
|
3697
|
+
const rawSM = evaluator.evaluate(args[5], context);
|
|
3698
|
+
if (rawSM instanceof FormulaError) return rawSM;
|
|
3699
|
+
const sm = toNumber(rawSM);
|
|
3700
|
+
if (sm instanceof FormulaError) return sm;
|
|
3701
|
+
searchMode = Math.trunc(sm);
|
|
3702
|
+
}
|
|
3703
|
+
const isRow = lookupArray.length === 1;
|
|
3704
|
+
const len = isRow ? lookupArray[0].length : lookupArray.length;
|
|
3705
|
+
const getVal = isRow ? (i) => lookupArray[0][i] : (i) => lookupArray[i][0];
|
|
3706
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3707
|
+
const eq = (a, b) => {
|
|
3708
|
+
if (lookupLower !== null && typeof a === "string") return a.toLowerCase() === lookupLower;
|
|
3709
|
+
return a === b;
|
|
3710
|
+
};
|
|
3711
|
+
let foundIdx = -1;
|
|
3712
|
+
if (matchMode === 0) {
|
|
3713
|
+
const start = searchMode >= 0 ? 0 : len - 1;
|
|
3714
|
+
const end = searchMode >= 0 ? len : -1;
|
|
3715
|
+
const step = searchMode >= 0 ? 1 : -1;
|
|
3716
|
+
for (let i = start; i !== end; i += step) {
|
|
3717
|
+
if (eq(getVal(i), lookupValue)) {
|
|
3718
|
+
foundIdx = i;
|
|
3719
|
+
break;
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
} else if (matchMode === -1) {
|
|
3723
|
+
let best = -1;
|
|
3724
|
+
for (let i = 0; i < len; i++) {
|
|
3725
|
+
const v = getVal(i);
|
|
3726
|
+
if (eq(v, lookupValue)) {
|
|
3727
|
+
foundIdx = i;
|
|
3728
|
+
break;
|
|
3729
|
+
}
|
|
3730
|
+
if (typeof lookupValue === "number" && typeof v === "number" && v < lookupValue) {
|
|
3731
|
+
if (best === -1 || v > getVal(best)) best = i;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
if (foundIdx === -1) foundIdx = best;
|
|
3735
|
+
} else if (matchMode === 1) {
|
|
3736
|
+
let best = -1;
|
|
3737
|
+
for (let i = 0; i < len; i++) {
|
|
3738
|
+
const v = getVal(i);
|
|
3739
|
+
if (eq(v, lookupValue)) {
|
|
3740
|
+
foundIdx = i;
|
|
3741
|
+
break;
|
|
3742
|
+
}
|
|
3743
|
+
if (typeof lookupValue === "number" && typeof v === "number" && v > lookupValue) {
|
|
3744
|
+
if (best === -1 || v < getVal(best)) best = i;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
if (foundIdx === -1) foundIdx = best;
|
|
3748
|
+
}
|
|
3749
|
+
if (foundIdx === -1) return ifNotFound;
|
|
3750
|
+
const isReturnRow = returnArray.length === 1;
|
|
3751
|
+
if (isReturnRow) return returnArray[0][foundIdx] ?? null;
|
|
3752
|
+
return returnArray[foundIdx]?.[0] ?? null;
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
registry.set("MATCH", {
|
|
3756
|
+
minArgs: 2,
|
|
3757
|
+
maxArgs: 3,
|
|
3758
|
+
evaluate(args, context, evaluator) {
|
|
3759
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3760
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3761
|
+
if (args[1].kind !== "range") {
|
|
3762
|
+
return new FormulaError("#VALUE!", "MATCH lookup_array must be a range");
|
|
3763
|
+
}
|
|
3764
|
+
const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3765
|
+
const isSingleRow = rangeData.length === 1;
|
|
3766
|
+
const values = isSingleRow ? rangeData[0] : rangeData;
|
|
3767
|
+
const getValue = isSingleRow ? (i) => values[i] : (i) => values[i][0];
|
|
3768
|
+
const valuesLength = isSingleRow ? values.length : values.length;
|
|
3769
|
+
let matchType = 1;
|
|
3770
|
+
if (args.length >= 3) {
|
|
3771
|
+
const rawMT = evaluator.evaluate(args[2], context);
|
|
3772
|
+
if (rawMT instanceof FormulaError) return rawMT;
|
|
3773
|
+
const mt = toNumber(rawMT);
|
|
3774
|
+
if (mt instanceof FormulaError) return mt;
|
|
3775
|
+
matchType = mt;
|
|
3776
|
+
}
|
|
3777
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3778
|
+
if (matchType === 0) {
|
|
3779
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3780
|
+
const cellVal = getValue(i);
|
|
3781
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3782
|
+
if (cellVal.toLowerCase() === lookupLower) return i + 1;
|
|
3783
|
+
} else if (cellVal === lookupValue) {
|
|
3784
|
+
return i + 1;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
return new FormulaError("#N/A", "MATCH no exact match found");
|
|
3788
|
+
} else if (matchType === 1) {
|
|
3789
|
+
let bestIndex = -1;
|
|
3790
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3791
|
+
const cellVal = getValue(i);
|
|
3792
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3793
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3794
|
+
if (cellVal <= lookupValue) bestIndex = i;
|
|
3795
|
+
else break;
|
|
3796
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3797
|
+
if (cellVal.toLowerCase() <= lookupLower) bestIndex = i;
|
|
3798
|
+
else break;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
|
|
3802
|
+
return bestIndex + 1;
|
|
3803
|
+
} else {
|
|
3804
|
+
let bestIndex = -1;
|
|
3805
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3806
|
+
const cellVal = getValue(i);
|
|
3807
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3808
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3809
|
+
if (cellVal >= lookupValue) bestIndex = i;
|
|
3810
|
+
else break;
|
|
3811
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3812
|
+
if (cellVal.toLowerCase() >= lookupLower) bestIndex = i;
|
|
3813
|
+
else break;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
|
|
3817
|
+
return bestIndex + 1;
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
function registerTextFunctions(registry) {
|
|
3823
|
+
registry.set("CONCATENATE", {
|
|
3824
|
+
minArgs: 1,
|
|
3825
|
+
maxArgs: -1,
|
|
3826
|
+
evaluate(args, context, evaluator) {
|
|
3827
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3828
|
+
const parts = [];
|
|
3829
|
+
for (const val of values) {
|
|
3830
|
+
if (val instanceof FormulaError) return val;
|
|
3831
|
+
parts.push(toString(val));
|
|
3832
|
+
}
|
|
3833
|
+
return parts.join("");
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
registry.set("CONCAT", {
|
|
3837
|
+
minArgs: 1,
|
|
3838
|
+
maxArgs: -1,
|
|
3839
|
+
evaluate(args, context, evaluator) {
|
|
3840
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3841
|
+
const parts = [];
|
|
3842
|
+
for (const val of values) {
|
|
3843
|
+
if (val instanceof FormulaError) return val;
|
|
3844
|
+
parts.push(toString(val));
|
|
3845
|
+
}
|
|
3846
|
+
return parts.join("");
|
|
3847
|
+
}
|
|
3848
|
+
});
|
|
3849
|
+
registry.set("UPPER", {
|
|
3850
|
+
minArgs: 1,
|
|
3851
|
+
maxArgs: 1,
|
|
3852
|
+
evaluate(args, context, evaluator) {
|
|
3853
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3854
|
+
if (val instanceof FormulaError) return val;
|
|
3855
|
+
return toString(val).toUpperCase();
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
registry.set("LOWER", {
|
|
3859
|
+
minArgs: 1,
|
|
3860
|
+
maxArgs: 1,
|
|
3861
|
+
evaluate(args, context, evaluator) {
|
|
3862
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3863
|
+
if (val instanceof FormulaError) return val;
|
|
3864
|
+
return toString(val).toLowerCase();
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
registry.set("TRIM", {
|
|
3868
|
+
minArgs: 1,
|
|
3869
|
+
maxArgs: 1,
|
|
3870
|
+
evaluate(args, context, evaluator) {
|
|
3871
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3872
|
+
if (val instanceof FormulaError) return val;
|
|
3873
|
+
return toString(val).trim();
|
|
3874
|
+
}
|
|
3875
|
+
});
|
|
3876
|
+
registry.set("LEFT", {
|
|
3877
|
+
minArgs: 1,
|
|
3878
|
+
maxArgs: 2,
|
|
3879
|
+
evaluate(args, context, evaluator) {
|
|
3880
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3881
|
+
if (val instanceof FormulaError) return val;
|
|
3882
|
+
const text = toString(val);
|
|
3883
|
+
let numChars = 1;
|
|
3884
|
+
if (args.length >= 2) {
|
|
3885
|
+
const rawNum = evaluator.evaluate(args[1], context);
|
|
3886
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3887
|
+
const n = toNumber(rawNum);
|
|
3888
|
+
if (n instanceof FormulaError) return n;
|
|
3889
|
+
numChars = Math.trunc(n);
|
|
3890
|
+
}
|
|
3891
|
+
if (numChars < 0) return new FormulaError("#VALUE!", "LEFT num_chars must be >= 0");
|
|
3892
|
+
return text.substring(0, numChars);
|
|
3893
|
+
}
|
|
3894
|
+
});
|
|
3895
|
+
registry.set("RIGHT", {
|
|
3896
|
+
minArgs: 1,
|
|
3897
|
+
maxArgs: 2,
|
|
3898
|
+
evaluate(args, context, evaluator) {
|
|
3899
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3900
|
+
if (val instanceof FormulaError) return val;
|
|
3901
|
+
const text = toString(val);
|
|
3902
|
+
let numChars = 1;
|
|
3903
|
+
if (args.length >= 2) {
|
|
3904
|
+
const rawNum = evaluator.evaluate(args[1], context);
|
|
3905
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3906
|
+
const n = toNumber(rawNum);
|
|
3907
|
+
if (n instanceof FormulaError) return n;
|
|
3908
|
+
numChars = Math.trunc(n);
|
|
3909
|
+
}
|
|
3910
|
+
if (numChars < 0) return new FormulaError("#VALUE!", "RIGHT num_chars must be >= 0");
|
|
3911
|
+
return text.substring(Math.max(0, text.length - numChars));
|
|
3912
|
+
}
|
|
3913
|
+
});
|
|
3914
|
+
registry.set("MID", {
|
|
3915
|
+
minArgs: 3,
|
|
3916
|
+
maxArgs: 3,
|
|
3917
|
+
evaluate(args, context, evaluator) {
|
|
3918
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3919
|
+
if (val instanceof FormulaError) return val;
|
|
3920
|
+
const text = toString(val);
|
|
3921
|
+
const rawStart = evaluator.evaluate(args[1], context);
|
|
3922
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
3923
|
+
const startPos = toNumber(rawStart);
|
|
3924
|
+
if (startPos instanceof FormulaError) return startPos;
|
|
3925
|
+
const rawNum = evaluator.evaluate(args[2], context);
|
|
3926
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3927
|
+
const numChars = toNumber(rawNum);
|
|
3928
|
+
if (numChars instanceof FormulaError) return numChars;
|
|
3929
|
+
const start = Math.trunc(startPos);
|
|
3930
|
+
const count = Math.trunc(numChars);
|
|
3931
|
+
if (start < 1) return new FormulaError("#VALUE!", "MID start_num must be >= 1");
|
|
3932
|
+
if (count < 0) return new FormulaError("#VALUE!", "MID num_chars must be >= 0");
|
|
3933
|
+
return text.substring(start - 1, start - 1 + count);
|
|
3934
|
+
}
|
|
3935
|
+
});
|
|
3936
|
+
registry.set("LEN", {
|
|
3937
|
+
minArgs: 1,
|
|
3938
|
+
maxArgs: 1,
|
|
3939
|
+
evaluate(args, context, evaluator) {
|
|
3940
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3941
|
+
if (val instanceof FormulaError) return val;
|
|
3942
|
+
return toString(val).length;
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
registry.set("SUBSTITUTE", {
|
|
3946
|
+
minArgs: 3,
|
|
3947
|
+
maxArgs: 4,
|
|
3948
|
+
evaluate(args, context, evaluator) {
|
|
3949
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3950
|
+
if (val instanceof FormulaError) return val;
|
|
3951
|
+
const text = toString(val);
|
|
3952
|
+
const rawOld = evaluator.evaluate(args[1], context);
|
|
3953
|
+
if (rawOld instanceof FormulaError) return rawOld;
|
|
3954
|
+
const oldText = toString(rawOld);
|
|
3955
|
+
const rawNew = evaluator.evaluate(args[2], context);
|
|
3956
|
+
if (rawNew instanceof FormulaError) return rawNew;
|
|
3957
|
+
const newText = toString(rawNew);
|
|
3958
|
+
if (args.length >= 4) {
|
|
3959
|
+
const rawInstance = evaluator.evaluate(args[3], context);
|
|
3960
|
+
if (rawInstance instanceof FormulaError) return rawInstance;
|
|
3961
|
+
const instanceNum = toNumber(rawInstance);
|
|
3962
|
+
if (instanceNum instanceof FormulaError) return instanceNum;
|
|
3963
|
+
const n = Math.trunc(instanceNum);
|
|
3964
|
+
if (n < 1) return new FormulaError("#VALUE!", "SUBSTITUTE instance_num must be >= 1");
|
|
3965
|
+
let count = 0;
|
|
3966
|
+
let result = "";
|
|
3967
|
+
let searchFrom = 0;
|
|
3968
|
+
while (searchFrom <= text.length) {
|
|
3969
|
+
const idx = text.indexOf(oldText, searchFrom);
|
|
3970
|
+
if (idx === -1) {
|
|
3971
|
+
result += text.substring(searchFrom);
|
|
3972
|
+
break;
|
|
3973
|
+
}
|
|
3974
|
+
count++;
|
|
3975
|
+
if (count === n) {
|
|
3976
|
+
result += text.substring(searchFrom, idx) + newText;
|
|
3977
|
+
result += text.substring(idx + oldText.length);
|
|
3978
|
+
break;
|
|
3979
|
+
} else {
|
|
3980
|
+
result += text.substring(searchFrom, idx + oldText.length);
|
|
3981
|
+
searchFrom = idx + oldText.length;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
return result;
|
|
3985
|
+
} else {
|
|
3986
|
+
if (oldText === "") return text;
|
|
3987
|
+
return text.split(oldText).join(newText);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
registry.set("FIND", {
|
|
3992
|
+
minArgs: 2,
|
|
3993
|
+
maxArgs: 3,
|
|
3994
|
+
evaluate(args, context, evaluator) {
|
|
3995
|
+
const rawFind = evaluator.evaluate(args[0], context);
|
|
3996
|
+
if (rawFind instanceof FormulaError) return rawFind;
|
|
3997
|
+
const findText = toString(rawFind);
|
|
3998
|
+
const rawWithin = evaluator.evaluate(args[1], context);
|
|
3999
|
+
if (rawWithin instanceof FormulaError) return rawWithin;
|
|
4000
|
+
const withinText = toString(rawWithin);
|
|
4001
|
+
let startNum = 1;
|
|
4002
|
+
if (args.length >= 3) {
|
|
4003
|
+
const rawStart = evaluator.evaluate(args[2], context);
|
|
4004
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4005
|
+
const s = toNumber(rawStart);
|
|
4006
|
+
if (s instanceof FormulaError) return s;
|
|
4007
|
+
startNum = Math.trunc(s);
|
|
4008
|
+
}
|
|
4009
|
+
if (startNum < 1) return new FormulaError("#VALUE!", "FIND start_num must be >= 1");
|
|
4010
|
+
const idx = withinText.indexOf(findText, startNum - 1);
|
|
4011
|
+
if (idx === -1) return new FormulaError("#VALUE!", "FIND text not found");
|
|
4012
|
+
return idx + 1;
|
|
4013
|
+
}
|
|
4014
|
+
});
|
|
4015
|
+
registry.set("SEARCH", {
|
|
4016
|
+
minArgs: 2,
|
|
4017
|
+
maxArgs: 3,
|
|
4018
|
+
evaluate(args, context, evaluator) {
|
|
4019
|
+
const rawFind = evaluator.evaluate(args[0], context);
|
|
4020
|
+
if (rawFind instanceof FormulaError) return rawFind;
|
|
4021
|
+
const findText = toString(rawFind).toLowerCase();
|
|
4022
|
+
const rawWithin = evaluator.evaluate(args[1], context);
|
|
4023
|
+
if (rawWithin instanceof FormulaError) return rawWithin;
|
|
4024
|
+
const withinText = toString(rawWithin).toLowerCase();
|
|
4025
|
+
let startNum = 1;
|
|
4026
|
+
if (args.length >= 3) {
|
|
4027
|
+
const rawStart = evaluator.evaluate(args[2], context);
|
|
4028
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4029
|
+
const s = toNumber(rawStart);
|
|
4030
|
+
if (s instanceof FormulaError) return s;
|
|
4031
|
+
startNum = Math.trunc(s);
|
|
4032
|
+
}
|
|
4033
|
+
if (startNum < 1) return new FormulaError("#VALUE!", "SEARCH start_num must be >= 1");
|
|
4034
|
+
const idx = withinText.indexOf(findText, startNum - 1);
|
|
4035
|
+
if (idx === -1) return new FormulaError("#VALUE!", "SEARCH text not found");
|
|
4036
|
+
return idx + 1;
|
|
4037
|
+
}
|
|
4038
|
+
});
|
|
4039
|
+
registry.set("REPLACE", {
|
|
4040
|
+
minArgs: 4,
|
|
4041
|
+
maxArgs: 4,
|
|
4042
|
+
evaluate(args, context, evaluator) {
|
|
4043
|
+
const rawText = evaluator.evaluate(args[0], context);
|
|
4044
|
+
if (rawText instanceof FormulaError) return rawText;
|
|
4045
|
+
const text = toString(rawText);
|
|
4046
|
+
const rawStart = evaluator.evaluate(args[1], context);
|
|
4047
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4048
|
+
const startPos = toNumber(rawStart);
|
|
4049
|
+
if (startPos instanceof FormulaError) return startPos;
|
|
4050
|
+
const rawNum = evaluator.evaluate(args[2], context);
|
|
4051
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
4052
|
+
const numChars = toNumber(rawNum);
|
|
4053
|
+
if (numChars instanceof FormulaError) return numChars;
|
|
4054
|
+
const rawNew = evaluator.evaluate(args[3], context);
|
|
4055
|
+
if (rawNew instanceof FormulaError) return rawNew;
|
|
4056
|
+
const newText = toString(rawNew);
|
|
4057
|
+
const start = Math.trunc(startPos) - 1;
|
|
4058
|
+
const count = Math.trunc(numChars);
|
|
4059
|
+
return text.substring(0, start) + newText + text.substring(start + count);
|
|
4060
|
+
}
|
|
4061
|
+
});
|
|
4062
|
+
registry.set("REPT", {
|
|
4063
|
+
minArgs: 2,
|
|
4064
|
+
maxArgs: 2,
|
|
4065
|
+
evaluate(args, context, evaluator) {
|
|
4066
|
+
const rawText = evaluator.evaluate(args[0], context);
|
|
4067
|
+
if (rawText instanceof FormulaError) return rawText;
|
|
4068
|
+
const text = toString(rawText);
|
|
4069
|
+
const rawTimes = evaluator.evaluate(args[1], context);
|
|
4070
|
+
if (rawTimes instanceof FormulaError) return rawTimes;
|
|
4071
|
+
const times = toNumber(rawTimes);
|
|
4072
|
+
if (times instanceof FormulaError) return times;
|
|
4073
|
+
const n = Math.trunc(times);
|
|
4074
|
+
if (n < 0) return new FormulaError("#VALUE!", "REPT number must be >= 0");
|
|
4075
|
+
return text.repeat(n);
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
4078
|
+
registry.set("EXACT", {
|
|
4079
|
+
minArgs: 2,
|
|
4080
|
+
maxArgs: 2,
|
|
4081
|
+
evaluate(args, context, evaluator) {
|
|
4082
|
+
const rawA = evaluator.evaluate(args[0], context);
|
|
4083
|
+
if (rawA instanceof FormulaError) return rawA;
|
|
4084
|
+
const rawB = evaluator.evaluate(args[1], context);
|
|
4085
|
+
if (rawB instanceof FormulaError) return rawB;
|
|
4086
|
+
return toString(rawA) === toString(rawB);
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
registry.set("PROPER", {
|
|
4090
|
+
minArgs: 1,
|
|
4091
|
+
maxArgs: 1,
|
|
4092
|
+
evaluate(args, context, evaluator) {
|
|
4093
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4094
|
+
if (val instanceof FormulaError) return val;
|
|
4095
|
+
return toString(val).toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4096
|
+
}
|
|
4097
|
+
});
|
|
4098
|
+
registry.set("CLEAN", {
|
|
4099
|
+
minArgs: 1,
|
|
4100
|
+
maxArgs: 1,
|
|
4101
|
+
evaluate(args, context, evaluator) {
|
|
4102
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4103
|
+
if (val instanceof FormulaError) return val;
|
|
4104
|
+
return toString(val).replace(/[\x00-\x1F]/g, "");
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
registry.set("CHAR", {
|
|
4108
|
+
minArgs: 1,
|
|
4109
|
+
maxArgs: 1,
|
|
4110
|
+
evaluate(args, context, evaluator) {
|
|
4111
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
4112
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
4113
|
+
const num = toNumber(rawVal);
|
|
4114
|
+
if (num instanceof FormulaError) return num;
|
|
4115
|
+
const n = Math.trunc(num);
|
|
4116
|
+
if (n < 1 || n > 65535) return new FormulaError("#VALUE!", "CHAR number must be 1-65535");
|
|
4117
|
+
return String.fromCharCode(n);
|
|
4118
|
+
}
|
|
4119
|
+
});
|
|
4120
|
+
registry.set("CODE", {
|
|
4121
|
+
minArgs: 1,
|
|
4122
|
+
maxArgs: 1,
|
|
4123
|
+
evaluate(args, context, evaluator) {
|
|
4124
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4125
|
+
if (val instanceof FormulaError) return val;
|
|
4126
|
+
const text = toString(val);
|
|
4127
|
+
if (text.length === 0) return new FormulaError("#VALUE!", "CODE requires non-empty text");
|
|
4128
|
+
return text.charCodeAt(0);
|
|
4129
|
+
}
|
|
4130
|
+
});
|
|
4131
|
+
registry.set("TEXT", {
|
|
4132
|
+
minArgs: 2,
|
|
4133
|
+
maxArgs: 2,
|
|
4134
|
+
evaluate(args, context, evaluator) {
|
|
4135
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
4136
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
4137
|
+
const rawFmt = evaluator.evaluate(args[1], context);
|
|
4138
|
+
if (rawFmt instanceof FormulaError) return rawFmt;
|
|
4139
|
+
const fmt = toString(rawFmt);
|
|
4140
|
+
const num = toNumber(rawVal);
|
|
4141
|
+
if (num instanceof FormulaError) return toString(rawVal);
|
|
4142
|
+
if (fmt.includes("%")) {
|
|
4143
|
+
const decimals2 = (fmt.match(/0/g) || []).length - 1;
|
|
4144
|
+
return (num * 100).toFixed(Math.max(0, decimals2)) + "%";
|
|
4145
|
+
}
|
|
4146
|
+
const decimalMatch = fmt.match(/\.(0+)/);
|
|
4147
|
+
const decimals = decimalMatch ? decimalMatch[1].length : 0;
|
|
4148
|
+
const useCommas = fmt.includes(",");
|
|
4149
|
+
const result = num.toFixed(decimals);
|
|
4150
|
+
if (useCommas) {
|
|
4151
|
+
const [intPart, decPart] = result.split(".");
|
|
4152
|
+
const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4153
|
+
return decPart ? withCommas + "." + decPart : withCommas;
|
|
4154
|
+
}
|
|
4155
|
+
return result;
|
|
4156
|
+
}
|
|
4157
|
+
});
|
|
4158
|
+
registry.set("VALUE", {
|
|
4159
|
+
minArgs: 1,
|
|
4160
|
+
maxArgs: 1,
|
|
4161
|
+
evaluate(args, context, evaluator) {
|
|
4162
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4163
|
+
if (val instanceof FormulaError) return val;
|
|
4164
|
+
if (typeof val === "number") return val;
|
|
4165
|
+
const raw = toString(val).trim();
|
|
4166
|
+
const isPercent = raw.endsWith("%");
|
|
4167
|
+
const text = raw.replace(/[,$%\s]/g, "");
|
|
4168
|
+
const n = Number(text);
|
|
4169
|
+
if (isNaN(n)) return new FormulaError("#VALUE!", "VALUE cannot convert text to number");
|
|
4170
|
+
return isPercent ? n / 100 : n;
|
|
4171
|
+
}
|
|
4172
|
+
});
|
|
4173
|
+
registry.set("TEXTJOIN", {
|
|
4174
|
+
minArgs: 3,
|
|
4175
|
+
maxArgs: -1,
|
|
4176
|
+
evaluate(args, context, evaluator) {
|
|
4177
|
+
const rawDelim = evaluator.evaluate(args[0], context);
|
|
4178
|
+
if (rawDelim instanceof FormulaError) return rawDelim;
|
|
4179
|
+
const delimiter = toString(rawDelim);
|
|
4180
|
+
const rawIgnore = evaluator.evaluate(args[1], context);
|
|
4181
|
+
if (rawIgnore instanceof FormulaError) return rawIgnore;
|
|
4182
|
+
const ignoreEmpty = !!rawIgnore;
|
|
4183
|
+
const parts = [];
|
|
4184
|
+
for (let i = 2; i < args.length; i++) {
|
|
4185
|
+
const arg = args[i];
|
|
4186
|
+
if (arg.kind === "range") {
|
|
4187
|
+
const rangeData = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
4188
|
+
for (const row of rangeData) {
|
|
4189
|
+
for (const cell of row) {
|
|
4190
|
+
if (cell instanceof FormulaError) return cell;
|
|
4191
|
+
const s = toString(cell);
|
|
4192
|
+
if (!ignoreEmpty || s !== "") parts.push(s);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
} else {
|
|
4196
|
+
const val = evaluator.evaluate(args[i], context);
|
|
4197
|
+
if (val instanceof FormulaError) return val;
|
|
4198
|
+
const s = toString(val);
|
|
4199
|
+
if (!ignoreEmpty || s !== "") parts.push(s);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
return parts.join(delimiter);
|
|
4203
|
+
}
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
function toDate(val) {
|
|
4207
|
+
if (val instanceof FormulaError) return val;
|
|
4208
|
+
if (val instanceof Date) {
|
|
4209
|
+
if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
|
|
4210
|
+
return val;
|
|
4211
|
+
}
|
|
4212
|
+
if (typeof val === "string") {
|
|
4213
|
+
const d = new Date(val);
|
|
4214
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
|
|
4215
|
+
return d;
|
|
4216
|
+
}
|
|
4217
|
+
if (typeof val === "number") {
|
|
4218
|
+
const d = new Date(val);
|
|
4219
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
|
|
4220
|
+
return d;
|
|
4221
|
+
}
|
|
4222
|
+
return new FormulaError("#VALUE!", "Cannot convert value to date");
|
|
4223
|
+
}
|
|
4224
|
+
function registerDateFunctions(registry) {
|
|
4225
|
+
registry.set("TODAY", {
|
|
4226
|
+
minArgs: 0,
|
|
4227
|
+
maxArgs: 0,
|
|
4228
|
+
evaluate(_args, context) {
|
|
4229
|
+
const now = context.now();
|
|
4230
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
4233
|
+
registry.set("NOW", {
|
|
4234
|
+
minArgs: 0,
|
|
4235
|
+
maxArgs: 0,
|
|
4236
|
+
evaluate(_args, context) {
|
|
4237
|
+
return context.now();
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
registry.set("YEAR", {
|
|
4241
|
+
minArgs: 1,
|
|
4242
|
+
maxArgs: 1,
|
|
4243
|
+
evaluate(args, context, evaluator) {
|
|
4244
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4245
|
+
if (val instanceof FormulaError) return val;
|
|
4246
|
+
const date = toDate(val);
|
|
4247
|
+
if (date instanceof FormulaError) return date;
|
|
4248
|
+
return date.getFullYear();
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
registry.set("MONTH", {
|
|
4252
|
+
minArgs: 1,
|
|
4253
|
+
maxArgs: 1,
|
|
4254
|
+
evaluate(args, context, evaluator) {
|
|
4255
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4256
|
+
if (val instanceof FormulaError) return val;
|
|
4257
|
+
const date = toDate(val);
|
|
4258
|
+
if (date instanceof FormulaError) return date;
|
|
4259
|
+
return date.getMonth() + 1;
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
registry.set("DAY", {
|
|
4263
|
+
minArgs: 1,
|
|
4264
|
+
maxArgs: 1,
|
|
4265
|
+
evaluate(args, context, evaluator) {
|
|
4266
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4267
|
+
if (val instanceof FormulaError) return val;
|
|
4268
|
+
const date = toDate(val);
|
|
4269
|
+
if (date instanceof FormulaError) return date;
|
|
4270
|
+
return date.getDate();
|
|
4271
|
+
}
|
|
4272
|
+
});
|
|
4273
|
+
registry.set("DATE", {
|
|
4274
|
+
minArgs: 3,
|
|
4275
|
+
maxArgs: 3,
|
|
4276
|
+
evaluate(args, context, evaluator) {
|
|
4277
|
+
const rawY = evaluator.evaluate(args[0], context);
|
|
4278
|
+
if (rawY instanceof FormulaError) return rawY;
|
|
4279
|
+
const y = toNumber(rawY);
|
|
4280
|
+
if (y instanceof FormulaError) return y;
|
|
4281
|
+
const rawM = evaluator.evaluate(args[1], context);
|
|
4282
|
+
if (rawM instanceof FormulaError) return rawM;
|
|
4283
|
+
const m = toNumber(rawM);
|
|
4284
|
+
if (m instanceof FormulaError) return m;
|
|
4285
|
+
const rawD = evaluator.evaluate(args[2], context);
|
|
4286
|
+
if (rawD instanceof FormulaError) return rawD;
|
|
4287
|
+
const d = toNumber(rawD);
|
|
4288
|
+
if (d instanceof FormulaError) return d;
|
|
4289
|
+
return new Date(Math.trunc(y), Math.trunc(m) - 1, Math.trunc(d));
|
|
4290
|
+
}
|
|
4291
|
+
});
|
|
4292
|
+
registry.set("DATEDIF", {
|
|
4293
|
+
minArgs: 3,
|
|
4294
|
+
maxArgs: 3,
|
|
4295
|
+
evaluate(args, context, evaluator) {
|
|
4296
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
4297
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4298
|
+
const startDate = toDate(rawStart);
|
|
4299
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
4300
|
+
const rawEnd = evaluator.evaluate(args[1], context);
|
|
4301
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
4302
|
+
const endDate = toDate(rawEnd);
|
|
4303
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
4304
|
+
const rawUnit = evaluator.evaluate(args[2], context);
|
|
4305
|
+
if (rawUnit instanceof FormulaError) return rawUnit;
|
|
4306
|
+
const unit = String(rawUnit).toUpperCase();
|
|
4307
|
+
if (startDate > endDate) return new FormulaError("#NUM!", "DATEDIF start date must be <= end date");
|
|
4308
|
+
switch (unit) {
|
|
4309
|
+
case "Y": {
|
|
4310
|
+
let years = endDate.getFullYear() - startDate.getFullYear();
|
|
4311
|
+
if (endDate.getMonth() < startDate.getMonth() || endDate.getMonth() === startDate.getMonth() && endDate.getDate() < startDate.getDate()) {
|
|
4312
|
+
years--;
|
|
4313
|
+
}
|
|
4314
|
+
return years;
|
|
4315
|
+
}
|
|
4316
|
+
case "M": {
|
|
4317
|
+
let months = (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth();
|
|
4318
|
+
if (endDate.getDate() < startDate.getDate()) months--;
|
|
4319
|
+
return months;
|
|
4320
|
+
}
|
|
4321
|
+
case "D":
|
|
4322
|
+
return Math.floor((endDate.getTime() - startDate.getTime()) / 864e5);
|
|
4323
|
+
default:
|
|
4324
|
+
return new FormulaError("#VALUE!", "DATEDIF unit must be Y, M, or D");
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
registry.set("EDATE", {
|
|
4329
|
+
minArgs: 2,
|
|
4330
|
+
maxArgs: 2,
|
|
4331
|
+
evaluate(args, context, evaluator) {
|
|
4332
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4333
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4334
|
+
const date = toDate(rawDate);
|
|
4335
|
+
if (date instanceof FormulaError) return date;
|
|
4336
|
+
const rawMonths = evaluator.evaluate(args[1], context);
|
|
4337
|
+
if (rawMonths instanceof FormulaError) return rawMonths;
|
|
4338
|
+
const months = toNumber(rawMonths);
|
|
4339
|
+
if (months instanceof FormulaError) return months;
|
|
4340
|
+
const result = new Date(date);
|
|
4341
|
+
result.setMonth(result.getMonth() + Math.trunc(months));
|
|
4342
|
+
return result;
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
registry.set("EOMONTH", {
|
|
4346
|
+
minArgs: 2,
|
|
4347
|
+
maxArgs: 2,
|
|
4348
|
+
evaluate(args, context, evaluator) {
|
|
4349
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4350
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4351
|
+
const date = toDate(rawDate);
|
|
4352
|
+
if (date instanceof FormulaError) return date;
|
|
4353
|
+
const rawMonths = evaluator.evaluate(args[1], context);
|
|
4354
|
+
if (rawMonths instanceof FormulaError) return rawMonths;
|
|
4355
|
+
const months = toNumber(rawMonths);
|
|
4356
|
+
if (months instanceof FormulaError) return months;
|
|
4357
|
+
const result = new Date(date.getFullYear(), date.getMonth() + Math.trunc(months) + 1, 0);
|
|
4358
|
+
return result;
|
|
4359
|
+
}
|
|
4360
|
+
});
|
|
4361
|
+
registry.set("WEEKDAY", {
|
|
4362
|
+
minArgs: 1,
|
|
4363
|
+
maxArgs: 2,
|
|
4364
|
+
evaluate(args, context, evaluator) {
|
|
4365
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4366
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4367
|
+
const date = toDate(rawDate);
|
|
4368
|
+
if (date instanceof FormulaError) return date;
|
|
4369
|
+
let returnType = 1;
|
|
4370
|
+
if (args.length >= 2) {
|
|
4371
|
+
const rawRT = evaluator.evaluate(args[1], context);
|
|
4372
|
+
if (rawRT instanceof FormulaError) return rawRT;
|
|
4373
|
+
const rt = toNumber(rawRT);
|
|
4374
|
+
if (rt instanceof FormulaError) return rt;
|
|
4375
|
+
returnType = Math.trunc(rt);
|
|
4376
|
+
}
|
|
4377
|
+
const day = date.getDay();
|
|
4378
|
+
switch (returnType) {
|
|
4379
|
+
case 1:
|
|
4380
|
+
return day + 1;
|
|
4381
|
+
// 1=Sun, 7=Sat
|
|
4382
|
+
case 2:
|
|
4383
|
+
return day === 0 ? 7 : day;
|
|
4384
|
+
// 1=Mon, 7=Sun
|
|
4385
|
+
case 3:
|
|
4386
|
+
return day === 0 ? 6 : day - 1;
|
|
4387
|
+
// 0=Mon, 6=Sun
|
|
4388
|
+
default:
|
|
4389
|
+
return new FormulaError("#VALUE!", "WEEKDAY return_type must be 1, 2, or 3");
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
});
|
|
4393
|
+
registry.set("HOUR", {
|
|
4394
|
+
minArgs: 1,
|
|
4395
|
+
maxArgs: 1,
|
|
4396
|
+
evaluate(args, context, evaluator) {
|
|
4397
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4398
|
+
if (val instanceof FormulaError) return val;
|
|
4399
|
+
const date = toDate(val);
|
|
4400
|
+
if (date instanceof FormulaError) return date;
|
|
4401
|
+
return date.getHours();
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
registry.set("MINUTE", {
|
|
4405
|
+
minArgs: 1,
|
|
4406
|
+
maxArgs: 1,
|
|
4407
|
+
evaluate(args, context, evaluator) {
|
|
4408
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4409
|
+
if (val instanceof FormulaError) return val;
|
|
4410
|
+
const date = toDate(val);
|
|
4411
|
+
if (date instanceof FormulaError) return date;
|
|
4412
|
+
return date.getMinutes();
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
4415
|
+
registry.set("SECOND", {
|
|
4416
|
+
minArgs: 1,
|
|
4417
|
+
maxArgs: 1,
|
|
4418
|
+
evaluate(args, context, evaluator) {
|
|
4419
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4420
|
+
if (val instanceof FormulaError) return val;
|
|
4421
|
+
const date = toDate(val);
|
|
4422
|
+
if (date instanceof FormulaError) return date;
|
|
4423
|
+
return date.getSeconds();
|
|
4424
|
+
}
|
|
4425
|
+
});
|
|
4426
|
+
registry.set("NETWORKDAYS", {
|
|
4427
|
+
minArgs: 2,
|
|
4428
|
+
maxArgs: 2,
|
|
4429
|
+
evaluate(args, context, evaluator) {
|
|
4430
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
4431
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4432
|
+
const startDate = toDate(rawStart);
|
|
4433
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
4434
|
+
const rawEnd = evaluator.evaluate(args[1], context);
|
|
4435
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
4436
|
+
const endDate = toDate(rawEnd);
|
|
4437
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
4438
|
+
const start = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
4439
|
+
const end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
|
4440
|
+
const sign = end >= start ? 1 : -1;
|
|
4441
|
+
const [from, to] = sign === 1 ? [start, end] : [end, start];
|
|
4442
|
+
let count = 0;
|
|
4443
|
+
const current = new Date(from);
|
|
4444
|
+
while (current <= to) {
|
|
4445
|
+
const day = current.getDay();
|
|
4446
|
+
if (day !== 0 && day !== 6) count++;
|
|
4447
|
+
current.setDate(current.getDate() + 1);
|
|
4448
|
+
}
|
|
4449
|
+
return count * sign;
|
|
4450
|
+
}
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
function makeCriteria(op, value) {
|
|
4454
|
+
return { op, value, valueLower: typeof value === "string" ? value.toLowerCase() : null };
|
|
4455
|
+
}
|
|
4456
|
+
function parseCriteria(criteria) {
|
|
4457
|
+
if (typeof criteria === "number") {
|
|
4458
|
+
return makeCriteria("=", criteria);
|
|
4459
|
+
}
|
|
4460
|
+
if (typeof criteria !== "string") {
|
|
4461
|
+
return makeCriteria("=", criteria);
|
|
4462
|
+
}
|
|
4463
|
+
const str = criteria.trim();
|
|
4464
|
+
if (str.startsWith(">=")) {
|
|
4465
|
+
return makeCriteria(">=", parseNumericOrString(str.substring(2).trim()));
|
|
4466
|
+
}
|
|
4467
|
+
if (str.startsWith("<=")) {
|
|
4468
|
+
return makeCriteria("<=", parseNumericOrString(str.substring(2).trim()));
|
|
4469
|
+
}
|
|
4470
|
+
if (str.startsWith("<>")) {
|
|
4471
|
+
return makeCriteria("<>", parseNumericOrString(str.substring(2).trim()));
|
|
4472
|
+
}
|
|
4473
|
+
if (str.startsWith(">")) {
|
|
4474
|
+
return makeCriteria(">", parseNumericOrString(str.substring(1).trim()));
|
|
4475
|
+
}
|
|
4476
|
+
if (str.startsWith("<")) {
|
|
4477
|
+
return makeCriteria("<", parseNumericOrString(str.substring(1).trim()));
|
|
4478
|
+
}
|
|
4479
|
+
if (str.startsWith("=")) {
|
|
4480
|
+
return makeCriteria("=", parseNumericOrString(str.substring(1).trim()));
|
|
4481
|
+
}
|
|
4482
|
+
return makeCriteria("=", parseNumericOrString(str));
|
|
4483
|
+
}
|
|
4484
|
+
function parseNumericOrString(s) {
|
|
4485
|
+
const n = Number(s);
|
|
4486
|
+
if (!isNaN(n) && s !== "") return n;
|
|
4487
|
+
return s;
|
|
4488
|
+
}
|
|
4489
|
+
function matchesCriteria(cellValue, criteria) {
|
|
4490
|
+
const { op, value } = criteria;
|
|
4491
|
+
let comparableCell = cellValue;
|
|
4492
|
+
if (typeof value === "number" && typeof cellValue !== "number") {
|
|
4493
|
+
const n = toNumber(cellValue);
|
|
4494
|
+
if (n instanceof FormulaError) return false;
|
|
4495
|
+
comparableCell = n;
|
|
4496
|
+
}
|
|
4497
|
+
if (typeof comparableCell === "string" && criteria.valueLower !== null) {
|
|
4498
|
+
const a = comparableCell.toLowerCase();
|
|
4499
|
+
const b = criteria.valueLower;
|
|
4500
|
+
switch (op) {
|
|
4501
|
+
case "=":
|
|
4502
|
+
return a === b;
|
|
4503
|
+
case "<>":
|
|
4504
|
+
return a !== b;
|
|
4505
|
+
case ">":
|
|
4506
|
+
return a > b;
|
|
4507
|
+
case "<":
|
|
4508
|
+
return a < b;
|
|
4509
|
+
case ">=":
|
|
4510
|
+
return a >= b;
|
|
4511
|
+
case "<=":
|
|
4512
|
+
return a <= b;
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
if (typeof comparableCell === "number" && typeof value === "number") {
|
|
4516
|
+
switch (op) {
|
|
4517
|
+
case "=":
|
|
4518
|
+
return comparableCell === value;
|
|
4519
|
+
case "<>":
|
|
4520
|
+
return comparableCell !== value;
|
|
4521
|
+
case ">":
|
|
4522
|
+
return comparableCell > value;
|
|
4523
|
+
case "<":
|
|
4524
|
+
return comparableCell < value;
|
|
4525
|
+
case ">=":
|
|
4526
|
+
return comparableCell >= value;
|
|
4527
|
+
case "<=":
|
|
4528
|
+
return comparableCell <= value;
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
if (op === "=") return comparableCell === value;
|
|
4532
|
+
if (op === "<>") return comparableCell !== value;
|
|
4533
|
+
return false;
|
|
4534
|
+
}
|
|
4535
|
+
function registerStatsFunctions(registry) {
|
|
4536
|
+
registry.set("SUMIF", {
|
|
4537
|
+
minArgs: 2,
|
|
4538
|
+
maxArgs: 3,
|
|
4539
|
+
evaluate(args, context, evaluator) {
|
|
4540
|
+
if (args[0].kind !== "range") {
|
|
4541
|
+
return new FormulaError("#VALUE!", "SUMIF range must be a cell range");
|
|
4542
|
+
}
|
|
4543
|
+
const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4544
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4545
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4546
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4547
|
+
let sumRange;
|
|
4548
|
+
if (args.length >= 3) {
|
|
4549
|
+
if (args[2].kind !== "range") {
|
|
4550
|
+
return new FormulaError("#VALUE!", "SUMIF sum_range must be a cell range");
|
|
4551
|
+
}
|
|
4552
|
+
sumRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
4553
|
+
} else {
|
|
4554
|
+
sumRange = criteriaRange;
|
|
4555
|
+
}
|
|
4556
|
+
let sum = 0;
|
|
4557
|
+
for (let r = 0; r < criteriaRange.length; r++) {
|
|
4558
|
+
for (let c = 0; c < criteriaRange[r].length; c++) {
|
|
4559
|
+
if (matchesCriteria(criteriaRange[r][c], criteria)) {
|
|
4560
|
+
const sumVal = sumRange[r] && sumRange[r][c] !== void 0 ? sumRange[r][c] : null;
|
|
4561
|
+
const n = toNumber(sumVal);
|
|
4562
|
+
if (typeof n === "number") {
|
|
4563
|
+
sum += n;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
return sum;
|
|
4569
|
+
}
|
|
4570
|
+
});
|
|
4571
|
+
registry.set("COUNTIF", {
|
|
4572
|
+
minArgs: 2,
|
|
4573
|
+
maxArgs: 2,
|
|
4574
|
+
evaluate(args, context, evaluator) {
|
|
4575
|
+
if (args[0].kind !== "range") {
|
|
4576
|
+
return new FormulaError("#VALUE!", "COUNTIF range must be a cell range");
|
|
4577
|
+
}
|
|
4578
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4579
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4580
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4581
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4582
|
+
let count = 0;
|
|
4583
|
+
for (let r = 0; r < rangeData.length; r++) {
|
|
4584
|
+
for (let c = 0; c < rangeData[r].length; c++) {
|
|
4585
|
+
if (matchesCriteria(rangeData[r][c], criteria)) {
|
|
4586
|
+
count++;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
return count;
|
|
4591
|
+
}
|
|
4592
|
+
});
|
|
4593
|
+
registry.set("AVERAGEIF", {
|
|
4594
|
+
minArgs: 2,
|
|
4595
|
+
maxArgs: 3,
|
|
4596
|
+
evaluate(args, context, evaluator) {
|
|
4597
|
+
if (args[0].kind !== "range") {
|
|
4598
|
+
return new FormulaError("#VALUE!", "AVERAGEIF range must be a cell range");
|
|
4599
|
+
}
|
|
4600
|
+
const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4601
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4602
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4603
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4604
|
+
let avgRange;
|
|
4605
|
+
if (args.length >= 3) {
|
|
4606
|
+
if (args[2].kind !== "range") {
|
|
4607
|
+
return new FormulaError("#VALUE!", "AVERAGEIF avg_range must be a cell range");
|
|
4608
|
+
}
|
|
4609
|
+
avgRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
4610
|
+
} else {
|
|
4611
|
+
avgRange = criteriaRange;
|
|
4612
|
+
}
|
|
4613
|
+
let sum = 0;
|
|
4614
|
+
let count = 0;
|
|
4615
|
+
for (let r = 0; r < criteriaRange.length; r++) {
|
|
4616
|
+
for (let c = 0; c < criteriaRange[r].length; c++) {
|
|
4617
|
+
if (matchesCriteria(criteriaRange[r][c], criteria)) {
|
|
4618
|
+
const avgVal = avgRange[r] && avgRange[r][c] !== void 0 ? avgRange[r][c] : null;
|
|
4619
|
+
const n = toNumber(avgVal);
|
|
4620
|
+
if (typeof n === "number") {
|
|
4621
|
+
sum += n;
|
|
4622
|
+
count++;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIF");
|
|
4628
|
+
return sum / count;
|
|
4629
|
+
}
|
|
4630
|
+
});
|
|
4631
|
+
registry.set("SUMIFS", {
|
|
4632
|
+
minArgs: 3,
|
|
4633
|
+
maxArgs: -1,
|
|
4634
|
+
evaluate(args, context, evaluator) {
|
|
4635
|
+
if ((args.length - 1) % 2 !== 0) {
|
|
4636
|
+
return new FormulaError("#VALUE!", "SUMIFS requires sum_range + pairs of criteria_range, criteria");
|
|
4637
|
+
}
|
|
4638
|
+
if (args[0].kind !== "range") {
|
|
4639
|
+
return new FormulaError("#VALUE!", "SUMIFS sum_range must be a cell range");
|
|
4640
|
+
}
|
|
4641
|
+
const sumRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4642
|
+
const pairs = [];
|
|
4643
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4644
|
+
const rangeArg = args[i];
|
|
4645
|
+
if (rangeArg.kind !== "range") {
|
|
4646
|
+
return new FormulaError("#VALUE!", "SUMIFS criteria_range must be a cell range");
|
|
4647
|
+
}
|
|
4648
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4649
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4650
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4651
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4652
|
+
}
|
|
4653
|
+
let sum = 0;
|
|
4654
|
+
for (let r = 0; r < sumRange.length; r++) {
|
|
4655
|
+
for (let c = 0; c < sumRange[r].length; c++) {
|
|
4656
|
+
let allMatch = true;
|
|
4657
|
+
for (const pair of pairs) {
|
|
4658
|
+
const cellVal = pair.range[r]?.[c];
|
|
4659
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4660
|
+
allMatch = false;
|
|
4661
|
+
break;
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
if (allMatch) {
|
|
4665
|
+
const n = toNumber(sumRange[r][c]);
|
|
4666
|
+
if (typeof n === "number") sum += n;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
return sum;
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
registry.set("COUNTIFS", {
|
|
4674
|
+
minArgs: 2,
|
|
4675
|
+
maxArgs: -1,
|
|
4676
|
+
evaluate(args, context, evaluator) {
|
|
4677
|
+
if (args.length % 2 !== 0) {
|
|
4678
|
+
return new FormulaError("#VALUE!", "COUNTIFS requires pairs of criteria_range, criteria");
|
|
4679
|
+
}
|
|
4680
|
+
const pairs = [];
|
|
4681
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
4682
|
+
const rangeArg = args[i];
|
|
4683
|
+
if (rangeArg.kind !== "range") {
|
|
4684
|
+
return new FormulaError("#VALUE!", "COUNTIFS criteria_range must be a cell range");
|
|
4685
|
+
}
|
|
4686
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4687
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4688
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4689
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4690
|
+
}
|
|
4691
|
+
const firstRange = pairs[0].range;
|
|
4692
|
+
let count = 0;
|
|
4693
|
+
for (let r = 0; r < firstRange.length; r++) {
|
|
4694
|
+
for (let c = 0; c < firstRange[r].length; c++) {
|
|
4695
|
+
let allMatch = true;
|
|
4696
|
+
for (const pair of pairs) {
|
|
4697
|
+
const cellVal = pair.range[r]?.[c];
|
|
4698
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4699
|
+
allMatch = false;
|
|
4700
|
+
break;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
if (allMatch) count++;
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
return count;
|
|
4707
|
+
}
|
|
4708
|
+
});
|
|
4709
|
+
registry.set("AVERAGEIFS", {
|
|
4710
|
+
minArgs: 3,
|
|
4711
|
+
maxArgs: -1,
|
|
4712
|
+
evaluate(args, context, evaluator) {
|
|
4713
|
+
if ((args.length - 1) % 2 !== 0) {
|
|
4714
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS requires avg_range + pairs of criteria_range, criteria");
|
|
4715
|
+
}
|
|
4716
|
+
if (args[0].kind !== "range") {
|
|
4717
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS avg_range must be a cell range");
|
|
4718
|
+
}
|
|
4719
|
+
const avgRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4720
|
+
const pairs = [];
|
|
4721
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4722
|
+
const rangeArg = args[i];
|
|
4723
|
+
if (rangeArg.kind !== "range") {
|
|
4724
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS criteria_range must be a cell range");
|
|
4725
|
+
}
|
|
4726
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4727
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4728
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4729
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4730
|
+
}
|
|
4731
|
+
let sum = 0;
|
|
4732
|
+
let count = 0;
|
|
4733
|
+
for (let r = 0; r < avgRange.length; r++) {
|
|
4734
|
+
for (let c = 0; c < avgRange[r].length; c++) {
|
|
4735
|
+
let allMatch = true;
|
|
4736
|
+
for (const pair of pairs) {
|
|
4737
|
+
const cellVal = pair.range[r]?.[c];
|
|
4738
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4739
|
+
allMatch = false;
|
|
4740
|
+
break;
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
if (allMatch) {
|
|
4744
|
+
const n = toNumber(avgRange[r][c]);
|
|
4745
|
+
if (typeof n === "number") {
|
|
4746
|
+
sum += n;
|
|
4747
|
+
count++;
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIFS");
|
|
4753
|
+
return sum / count;
|
|
4754
|
+
}
|
|
4755
|
+
});
|
|
4756
|
+
}
|
|
4757
|
+
function registerInfoFunctions(registry) {
|
|
4758
|
+
registry.set("ISBLANK", {
|
|
4759
|
+
minArgs: 1,
|
|
4760
|
+
maxArgs: 1,
|
|
4761
|
+
evaluate(args, context, evaluator) {
|
|
4762
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4763
|
+
return val === null || val === void 0 || val === "";
|
|
4764
|
+
}
|
|
4765
|
+
});
|
|
4766
|
+
registry.set("ISNUMBER", {
|
|
4767
|
+
minArgs: 1,
|
|
4768
|
+
maxArgs: 1,
|
|
4769
|
+
evaluate(args, context, evaluator) {
|
|
4770
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4771
|
+
return typeof val === "number" && !isNaN(val);
|
|
4772
|
+
}
|
|
4773
|
+
});
|
|
4774
|
+
registry.set("ISTEXT", {
|
|
4775
|
+
minArgs: 1,
|
|
4776
|
+
maxArgs: 1,
|
|
4777
|
+
evaluate(args, context, evaluator) {
|
|
4778
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4779
|
+
return typeof val === "string";
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
registry.set("ISERROR", {
|
|
4783
|
+
minArgs: 1,
|
|
4784
|
+
maxArgs: 1,
|
|
4785
|
+
evaluate(args, context, evaluator) {
|
|
4786
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4787
|
+
return val instanceof FormulaError;
|
|
4788
|
+
}
|
|
4789
|
+
});
|
|
4790
|
+
registry.set("ISNA", {
|
|
4791
|
+
minArgs: 1,
|
|
4792
|
+
maxArgs: 1,
|
|
4793
|
+
evaluate(args, context, evaluator) {
|
|
4794
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4795
|
+
return val instanceof FormulaError && val.type === "#N/A";
|
|
4796
|
+
}
|
|
4797
|
+
});
|
|
4798
|
+
registry.set("TYPE", {
|
|
4799
|
+
minArgs: 1,
|
|
4800
|
+
maxArgs: 1,
|
|
4801
|
+
evaluate(args, context, evaluator) {
|
|
4802
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4803
|
+
if (val instanceof FormulaError) return 16;
|
|
4804
|
+
if (typeof val === "number") return 1;
|
|
4805
|
+
if (typeof val === "string") return 2;
|
|
4806
|
+
if (typeof val === "boolean") return 4;
|
|
4807
|
+
if (val === null || val === void 0) return 1;
|
|
4808
|
+
return 1;
|
|
4809
|
+
}
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
function createBuiltInFunctions() {
|
|
4813
|
+
const registry = /* @__PURE__ */ new Map();
|
|
4814
|
+
registerMathFunctions(registry);
|
|
4815
|
+
registerLogicalFunctions(registry);
|
|
4816
|
+
registerLookupFunctions(registry);
|
|
4817
|
+
registerTextFunctions(registry);
|
|
4818
|
+
registerDateFunctions(registry);
|
|
4819
|
+
registerStatsFunctions(registry);
|
|
4820
|
+
registerInfoFunctions(registry);
|
|
4821
|
+
return registry;
|
|
4822
|
+
}
|
|
4823
|
+
function extractDependencies(node) {
|
|
4824
|
+
const deps = /* @__PURE__ */ new Set();
|
|
4825
|
+
function walk(n) {
|
|
4826
|
+
switch (n.kind) {
|
|
4827
|
+
case "cellRef":
|
|
4828
|
+
deps.add(toCellKey(n.address.col, n.address.row, n.address.sheet));
|
|
4829
|
+
break;
|
|
4830
|
+
case "range": {
|
|
4831
|
+
const sheet = n.start.sheet;
|
|
4832
|
+
const minRow = Math.min(n.start.row, n.end.row);
|
|
4833
|
+
const maxRow = Math.max(n.start.row, n.end.row);
|
|
4834
|
+
const minCol = Math.min(n.start.col, n.end.col);
|
|
4835
|
+
const maxCol = Math.max(n.start.col, n.end.col);
|
|
4836
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
4837
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
4838
|
+
deps.add(toCellKey(c, r, sheet));
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
break;
|
|
4842
|
+
}
|
|
4843
|
+
case "functionCall":
|
|
4844
|
+
for (const arg of n.args) walk(arg);
|
|
4845
|
+
break;
|
|
4846
|
+
case "binaryOp":
|
|
4847
|
+
walk(n.left);
|
|
4848
|
+
walk(n.right);
|
|
4849
|
+
break;
|
|
4850
|
+
case "unaryOp":
|
|
4851
|
+
walk(n.operand);
|
|
4852
|
+
break;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
walk(node);
|
|
4856
|
+
return deps;
|
|
4857
|
+
}
|
|
4858
|
+
var FormulaEngine = class {
|
|
4859
|
+
constructor(config) {
|
|
4860
|
+
this.formulas = /* @__PURE__ */ new Map();
|
|
4861
|
+
this.parsedFormulas = /* @__PURE__ */ new Map();
|
|
4862
|
+
this.values = /* @__PURE__ */ new Map();
|
|
4863
|
+
this.depGraph = new DependencyGraph();
|
|
4864
|
+
this.namedRanges = /* @__PURE__ */ new Map();
|
|
4865
|
+
this.sheetAccessors = /* @__PURE__ */ new Map();
|
|
4866
|
+
const builtIns = createBuiltInFunctions();
|
|
4867
|
+
if (config?.customFunctions) {
|
|
4868
|
+
for (const [name, fn] of Object.entries(config.customFunctions)) {
|
|
4869
|
+
builtIns.set(name.toUpperCase(), fn);
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
if (config?.namedRanges) {
|
|
4873
|
+
for (const [name, ref] of Object.entries(config.namedRanges)) {
|
|
4874
|
+
this.namedRanges.set(name.toUpperCase(), ref);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
this.evaluator = new FormulaEvaluator(builtIns);
|
|
4878
|
+
this.maxChainLength = config?.maxChainLength ?? 1e3;
|
|
4879
|
+
}
|
|
4880
|
+
/**
|
|
4881
|
+
* Set or clear a formula for a cell.
|
|
4882
|
+
*/
|
|
4883
|
+
setFormula(col, row, formula, accessor) {
|
|
4884
|
+
const key = toCellKey(col, row);
|
|
4885
|
+
if (formula === null || formula === "") {
|
|
4886
|
+
const oldValue2 = this.values.get(key);
|
|
4887
|
+
this.formulas.delete(key);
|
|
4888
|
+
this.parsedFormulas.delete(key);
|
|
4889
|
+
this.values.delete(key);
|
|
4890
|
+
this.depGraph.removeDependencies(key);
|
|
4891
|
+
return {
|
|
4892
|
+
updatedCells: oldValue2 !== void 0 ? [{ cellKey: key, col, row, oldValue: oldValue2, newValue: void 0 }] : []
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
const expression = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
4896
|
+
let ast;
|
|
4897
|
+
try {
|
|
4898
|
+
const tokens = tokenize(expression);
|
|
4899
|
+
ast = parse(tokens, this.namedRanges);
|
|
4900
|
+
} catch (err) {
|
|
4901
|
+
const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
|
|
4902
|
+
ast = { kind: "error", error };
|
|
4903
|
+
}
|
|
4904
|
+
const deps = extractDependencies(ast);
|
|
4905
|
+
if (deps.has(key) || this.depGraph.wouldCreateCycle(key, deps)) {
|
|
4906
|
+
const oldValue2 = this.values.get(key);
|
|
4907
|
+
const circError = new FormulaError("#CIRC!", "Circular reference detected");
|
|
4908
|
+
this.formulas.set(key, formula);
|
|
4909
|
+
this.parsedFormulas.set(key, ast);
|
|
4910
|
+
this.values.set(key, circError);
|
|
4911
|
+
this.depGraph.setDependencies(key, deps);
|
|
4912
|
+
return {
|
|
4913
|
+
updatedCells: [{ cellKey: key, col, row, oldValue: oldValue2, newValue: circError }]
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
this.depGraph.setDependencies(key, deps);
|
|
4917
|
+
this.formulas.set(key, formula);
|
|
4918
|
+
this.parsedFormulas.set(key, ast);
|
|
4919
|
+
const oldValue = this.values.get(key);
|
|
4920
|
+
const context = this.createContext(accessor);
|
|
4921
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
4922
|
+
this.values.set(key, newValue);
|
|
4923
|
+
const updatedCells = [
|
|
4924
|
+
{ cellKey: key, col, row, oldValue, newValue }
|
|
4925
|
+
];
|
|
4926
|
+
const recalcOrder = this.depGraph.getRecalcOrder(key);
|
|
4927
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4928
|
+
return { updatedCells };
|
|
4929
|
+
}
|
|
4930
|
+
/**
|
|
4931
|
+
* Notify the engine that a non-formula cell's value changed.
|
|
4932
|
+
*/
|
|
4933
|
+
onCellChanged(col, row, accessor) {
|
|
4934
|
+
const key = toCellKey(col, row);
|
|
4935
|
+
const recalcOrder = this.depGraph.getRecalcOrder(key);
|
|
4936
|
+
if (recalcOrder.length === 0) return { updatedCells: [] };
|
|
4937
|
+
const updatedCells = [];
|
|
4938
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4939
|
+
return { updatedCells };
|
|
4940
|
+
}
|
|
4941
|
+
/**
|
|
4942
|
+
* Batch notify: multiple cells changed.
|
|
4943
|
+
*/
|
|
4944
|
+
onCellsChanged(cells, accessor) {
|
|
4945
|
+
const keys = cells.map((c) => toCellKey(c.col, c.row));
|
|
4946
|
+
const recalcOrder = this.depGraph.getRecalcOrderBatch(keys);
|
|
4947
|
+
if (recalcOrder.length === 0) return { updatedCells: [] };
|
|
4948
|
+
const updatedCells = [];
|
|
4949
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4950
|
+
return { updatedCells };
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Get the current computed value for a cell.
|
|
4954
|
+
*/
|
|
4955
|
+
getValue(col, row) {
|
|
4956
|
+
return this.values.get(toCellKey(col, row));
|
|
4957
|
+
}
|
|
4958
|
+
/**
|
|
4959
|
+
* Get the formula string for a cell.
|
|
4960
|
+
*/
|
|
4961
|
+
getFormula(col, row) {
|
|
4962
|
+
return this.formulas.get(toCellKey(col, row));
|
|
4963
|
+
}
|
|
4964
|
+
/**
|
|
4965
|
+
* Check if a cell has a formula.
|
|
4966
|
+
*/
|
|
4967
|
+
hasFormula(col, row) {
|
|
4968
|
+
return this.formulas.has(toCellKey(col, row));
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Register a custom function at runtime.
|
|
4972
|
+
*/
|
|
4973
|
+
registerFunction(name, fn) {
|
|
4974
|
+
this.evaluator.registerFunction(name, fn);
|
|
4975
|
+
}
|
|
4976
|
+
/**
|
|
4977
|
+
* Full recalculation of all formulas.
|
|
4978
|
+
*/
|
|
4979
|
+
recalcAll(accessor) {
|
|
4980
|
+
const updatedCells = [];
|
|
4981
|
+
const context = this.createContext(accessor);
|
|
4982
|
+
if (this.formulas.size === 0) return { updatedCells };
|
|
4983
|
+
const allFormulaKeys = [];
|
|
4984
|
+
for (const key of this.formulas.keys()) allFormulaKeys.push(key);
|
|
4985
|
+
const recalcOrder = this.depGraph.getRecalcOrderBatch(allFormulaKeys);
|
|
4986
|
+
const ordered = new Set(recalcOrder);
|
|
4987
|
+
for (const key of allFormulaKeys) {
|
|
4988
|
+
if (!ordered.has(key)) {
|
|
4989
|
+
const { col, row } = fromCellKey(key);
|
|
4990
|
+
const ast = this.parsedFormulas.get(key);
|
|
4991
|
+
if (!ast) continue;
|
|
4992
|
+
const oldValue = this.values.get(key);
|
|
4993
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
4994
|
+
this.values.set(key, newValue);
|
|
4995
|
+
updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4999
|
+
return { updatedCells };
|
|
5000
|
+
}
|
|
5001
|
+
/**
|
|
5002
|
+
* Clear all formulas and cached values.
|
|
5003
|
+
*/
|
|
5004
|
+
clear() {
|
|
5005
|
+
this.formulas.clear();
|
|
5006
|
+
this.parsedFormulas.clear();
|
|
5007
|
+
this.values.clear();
|
|
5008
|
+
this.depGraph.clear();
|
|
5009
|
+
}
|
|
5010
|
+
/**
|
|
5011
|
+
* Get all formula entries for serialization.
|
|
5012
|
+
*/
|
|
5013
|
+
getAllFormulas() {
|
|
5014
|
+
const result = [];
|
|
5015
|
+
for (const [key, formula] of this.formulas) {
|
|
5016
|
+
const { col, row } = fromCellKey(key);
|
|
5017
|
+
result.push({ col, row, formula });
|
|
5018
|
+
}
|
|
5019
|
+
return result;
|
|
5020
|
+
}
|
|
5021
|
+
/**
|
|
5022
|
+
* Bulk-load formulas. Recalculates everything.
|
|
5023
|
+
*/
|
|
5024
|
+
loadFormulas(formulas, accessor) {
|
|
5025
|
+
this.clear();
|
|
5026
|
+
for (const { col, row, formula } of formulas) {
|
|
5027
|
+
const key = toCellKey(col, row);
|
|
5028
|
+
const expression = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
5029
|
+
let ast;
|
|
5030
|
+
try {
|
|
5031
|
+
const tokens = tokenize(expression);
|
|
5032
|
+
ast = parse(tokens, this.namedRanges);
|
|
5033
|
+
} catch (err) {
|
|
5034
|
+
const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
|
|
5035
|
+
ast = { kind: "error", error };
|
|
5036
|
+
}
|
|
5037
|
+
this.formulas.set(key, formula);
|
|
5038
|
+
this.parsedFormulas.set(key, ast);
|
|
5039
|
+
const deps = extractDependencies(ast);
|
|
5040
|
+
this.depGraph.setDependencies(key, deps);
|
|
5041
|
+
}
|
|
5042
|
+
return this.recalcAll(accessor);
|
|
5043
|
+
}
|
|
5044
|
+
// --- Named Ranges ---
|
|
5045
|
+
/**
|
|
5046
|
+
* Define a named range (e.g. "Revenue" → "A1:A10").
|
|
5047
|
+
*/
|
|
5048
|
+
defineNamedRange(name, ref) {
|
|
5049
|
+
this.namedRanges.set(name.toUpperCase(), ref);
|
|
5050
|
+
}
|
|
5051
|
+
/**
|
|
5052
|
+
* Remove a named range by name.
|
|
5053
|
+
*/
|
|
5054
|
+
removeNamedRange(name) {
|
|
5055
|
+
this.namedRanges.delete(name.toUpperCase());
|
|
5056
|
+
}
|
|
5057
|
+
/**
|
|
5058
|
+
* Get all named ranges as a Map (name → ref).
|
|
1482
5059
|
*/
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (!nextBatch) return null;
|
|
1486
|
-
this.history.push(nextBatch);
|
|
1487
|
-
return nextBatch;
|
|
5060
|
+
getNamedRanges() {
|
|
5061
|
+
return this.namedRanges;
|
|
1488
5062
|
}
|
|
5063
|
+
// --- Sheet Accessors ---
|
|
1489
5064
|
/**
|
|
1490
|
-
*
|
|
1491
|
-
* Does not affect any open batch — call endBatch() first if needed.
|
|
5065
|
+
* Register a data accessor for a named sheet (for cross-sheet references).
|
|
1492
5066
|
*/
|
|
1493
|
-
|
|
1494
|
-
this.
|
|
1495
|
-
this.redoStack = [];
|
|
5067
|
+
registerSheet(name, accessor) {
|
|
5068
|
+
this.sheetAccessors.set(name, accessor);
|
|
1496
5069
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
5070
|
+
/**
|
|
5071
|
+
* Unregister a sheet accessor.
|
|
5072
|
+
*/
|
|
5073
|
+
unregisterSheet(name) {
|
|
5074
|
+
this.sheetAccessors.delete(name);
|
|
1502
5075
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
5076
|
+
// --- Formula Auditing ---
|
|
5077
|
+
/**
|
|
5078
|
+
* Get all cells that a cell depends on (deep, transitive precedents).
|
|
5079
|
+
*/
|
|
5080
|
+
getPrecedents(col, row) {
|
|
5081
|
+
const key = toCellKey(col, row);
|
|
5082
|
+
const result = [];
|
|
5083
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5084
|
+
const queue = [];
|
|
5085
|
+
const directDeps = this.depGraph.getDependencies(key);
|
|
5086
|
+
for (const dep of directDeps) {
|
|
5087
|
+
if (!visited.has(dep)) {
|
|
5088
|
+
visited.add(dep);
|
|
5089
|
+
queue.push(dep);
|
|
5090
|
+
}
|
|
1508
5091
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
5092
|
+
let head = 0;
|
|
5093
|
+
while (head < queue.length) {
|
|
5094
|
+
const current = queue[head++];
|
|
5095
|
+
const parsed = fromCellKey(current);
|
|
5096
|
+
result.push({
|
|
5097
|
+
cellKey: current,
|
|
5098
|
+
col: parsed.col,
|
|
5099
|
+
row: parsed.row,
|
|
5100
|
+
formula: this.formulas.get(current),
|
|
5101
|
+
value: this.values.has(current) ? this.values.get(current) : void 0
|
|
5102
|
+
});
|
|
5103
|
+
const deps = this.depGraph.getDependencies(current);
|
|
5104
|
+
for (const dep of deps) {
|
|
5105
|
+
if (!visited.has(dep)) {
|
|
5106
|
+
visited.add(dep);
|
|
5107
|
+
queue.push(dep);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
1511
5110
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
5111
|
+
return result;
|
|
5112
|
+
}
|
|
5113
|
+
/**
|
|
5114
|
+
* Get all cells that depend on this cell (deep, transitive dependents).
|
|
5115
|
+
*/
|
|
5116
|
+
getDependents(col, row) {
|
|
5117
|
+
const key = toCellKey(col, row);
|
|
5118
|
+
const result = [];
|
|
5119
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5120
|
+
const queue = [];
|
|
5121
|
+
const directDeps = this.depGraph.getDependents(key);
|
|
5122
|
+
for (const dep of directDeps) {
|
|
5123
|
+
if (!visited.has(dep)) {
|
|
5124
|
+
visited.add(dep);
|
|
5125
|
+
queue.push(dep);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
let head = 0;
|
|
5129
|
+
while (head < queue.length) {
|
|
5130
|
+
const current = queue[head++];
|
|
5131
|
+
const parsed = fromCellKey(current);
|
|
5132
|
+
result.push({
|
|
5133
|
+
cellKey: current,
|
|
5134
|
+
col: parsed.col,
|
|
5135
|
+
row: parsed.row,
|
|
5136
|
+
formula: this.formulas.get(current),
|
|
5137
|
+
value: this.values.has(current) ? this.values.get(current) : void 0
|
|
5138
|
+
});
|
|
5139
|
+
const deps = this.depGraph.getDependents(current);
|
|
5140
|
+
for (const dep of deps) {
|
|
5141
|
+
if (!visited.has(dep)) {
|
|
5142
|
+
visited.add(dep);
|
|
5143
|
+
queue.push(dep);
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
1517
5146
|
}
|
|
5147
|
+
return result;
|
|
1518
5148
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
5149
|
+
/**
|
|
5150
|
+
* Get a full audit trail for a cell: target + precedents + dependents.
|
|
5151
|
+
*/
|
|
5152
|
+
getAuditTrail(col, row) {
|
|
5153
|
+
const key = toCellKey(col, row);
|
|
5154
|
+
const target = {
|
|
5155
|
+
cellKey: key,
|
|
5156
|
+
col,
|
|
5157
|
+
row,
|
|
5158
|
+
formula: this.formulas.get(key),
|
|
5159
|
+
value: this.values.has(key) ? this.values.get(key) : void 0
|
|
5160
|
+
};
|
|
5161
|
+
return {
|
|
5162
|
+
target,
|
|
5163
|
+
precedents: this.getPrecedents(col, row),
|
|
5164
|
+
dependents: this.getDependents(col, row)
|
|
5165
|
+
};
|
|
1527
5166
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
)
|
|
1544
|
-
|
|
5167
|
+
// --- Private methods ---
|
|
5168
|
+
createContext(accessor) {
|
|
5169
|
+
const contextNow = /* @__PURE__ */ new Date();
|
|
5170
|
+
return {
|
|
5171
|
+
getCellValue: (addr) => {
|
|
5172
|
+
const key = toCellKey(addr.col, addr.row, addr.sheet);
|
|
5173
|
+
const cached = this.values.get(key);
|
|
5174
|
+
if (cached !== void 0) return cached;
|
|
5175
|
+
if (addr.sheet) {
|
|
5176
|
+
const sheetAccessor = this.sheetAccessors.get(addr.sheet);
|
|
5177
|
+
if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
|
|
5178
|
+
return sheetAccessor.getCellValue(addr.col, addr.row);
|
|
5179
|
+
}
|
|
5180
|
+
return accessor.getCellValue(addr.col, addr.row);
|
|
5181
|
+
},
|
|
5182
|
+
getRangeValues: (range) => {
|
|
5183
|
+
const result = [];
|
|
5184
|
+
const sheet = range.start.sheet;
|
|
5185
|
+
const rangeAccessor = sheet ? this.sheetAccessors.get(sheet) : accessor;
|
|
5186
|
+
if (sheet && !rangeAccessor) {
|
|
5187
|
+
return [[new FormulaError("#REF!", `Unknown sheet: ${sheet}`)]];
|
|
5188
|
+
}
|
|
5189
|
+
const minRow = Math.min(range.start.row, range.end.row);
|
|
5190
|
+
const maxRow = Math.max(range.start.row, range.end.row);
|
|
5191
|
+
const minCol = Math.min(range.start.col, range.end.col);
|
|
5192
|
+
const maxCol = Math.max(range.start.col, range.end.col);
|
|
5193
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
5194
|
+
const row = [];
|
|
5195
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
5196
|
+
const key = toCellKey(c, r, sheet);
|
|
5197
|
+
const cached = this.values.get(key);
|
|
5198
|
+
if (cached !== void 0) {
|
|
5199
|
+
row.push(cached);
|
|
5200
|
+
} else {
|
|
5201
|
+
row.push(rangeAccessor.getCellValue(c, r));
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
result.push(row);
|
|
5205
|
+
}
|
|
5206
|
+
return result;
|
|
5207
|
+
},
|
|
5208
|
+
now: () => contextNow
|
|
5209
|
+
};
|
|
5210
|
+
}
|
|
5211
|
+
recalcCells(order, accessor, updatedCells) {
|
|
5212
|
+
const context = this.createContext(accessor);
|
|
5213
|
+
let count = 0;
|
|
5214
|
+
for (const key of order) {
|
|
5215
|
+
if (count++ > this.maxChainLength) {
|
|
5216
|
+
const { col: col2, row: row2 } = fromCellKey(key);
|
|
5217
|
+
const oldValue2 = this.values.get(key);
|
|
5218
|
+
const circError = new FormulaError("#CIRC!", "Dependency chain too long");
|
|
5219
|
+
this.values.set(key, circError);
|
|
5220
|
+
updatedCells.push({ cellKey: key, col: col2, row: row2, oldValue: oldValue2, newValue: circError });
|
|
5221
|
+
continue;
|
|
5222
|
+
}
|
|
5223
|
+
const ast = this.parsedFormulas.get(key);
|
|
5224
|
+
if (!ast) continue;
|
|
5225
|
+
const { col, row } = fromCellKey(key);
|
|
5226
|
+
const oldValue = this.values.get(key);
|
|
5227
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
5228
|
+
this.values.set(key, newValue);
|
|
5229
|
+
updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
|
|
1545
5230
|
}
|
|
1546
|
-
ids.add(id);
|
|
1547
5231
|
}
|
|
1548
|
-
}
|
|
1549
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1550
|
-
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
1551
|
-
var SIDEBAR_TRANSITION_MS = 300;
|
|
1552
|
-
var Z_INDEX = {
|
|
1553
|
-
/** Column resize drag handle */
|
|
1554
|
-
RESIZE_HANDLE: 1,
|
|
1555
|
-
/** Active/editing cell outline */
|
|
1556
|
-
ACTIVE_CELL: 2,
|
|
1557
|
-
/** Fill handle dot */
|
|
1558
|
-
FILL_HANDLE: 3,
|
|
1559
|
-
/** Selection range overlay (marching ants) */
|
|
1560
|
-
SELECTION_OVERLAY: 4,
|
|
1561
|
-
/** Row number column */
|
|
1562
|
-
ROW_NUMBER: 5,
|
|
1563
|
-
/** Clipboard overlay (copy/cut animation) */
|
|
1564
|
-
CLIPBOARD_OVERLAY: 5,
|
|
1565
|
-
/** Sticky pinned body cells */
|
|
1566
|
-
PINNED: 6,
|
|
1567
|
-
/** Selection checkbox column in body */
|
|
1568
|
-
SELECTION_CELL: 7,
|
|
1569
|
-
/** Sticky thead row */
|
|
1570
|
-
THEAD: 8,
|
|
1571
|
-
/** Pinned header cells (sticky both axes) */
|
|
1572
|
-
PINNED_HEADER: 10,
|
|
1573
|
-
/** Focused header cell */
|
|
1574
|
-
HEADER_FOCUS: 11,
|
|
1575
|
-
/** Checkbox column in sticky header (sticky both axes) */
|
|
1576
|
-
SELECTION_HEADER_PINNED: 12,
|
|
1577
|
-
/** Loading overlay within table */
|
|
1578
|
-
LOADING: 2,
|
|
1579
|
-
/** Column reorder drop indicator */
|
|
1580
|
-
DROP_INDICATOR: 100,
|
|
1581
|
-
/** Dropdown menus (column chooser, pagination size select) */
|
|
1582
|
-
DROPDOWN: 1e3,
|
|
1583
|
-
/** Filter popovers */
|
|
1584
|
-
FILTER_POPOVER: 1e3,
|
|
1585
|
-
/** Modal dialogs */
|
|
1586
|
-
MODAL: 2e3,
|
|
1587
|
-
/** Fullscreen grid container */
|
|
1588
|
-
FULLSCREEN: 9999,
|
|
1589
|
-
/** Context menus (right-click grid menu) */
|
|
1590
|
-
CONTEXT_MENU: 1e4
|
|
1591
5232
|
};
|
|
1592
5233
|
|
|
1593
5234
|
// src/utils/getCellCoordinates.ts
|
|
@@ -1643,6 +5284,8 @@ var GridState = class {
|
|
|
1643
5284
|
// Guards against stale fetch responses
|
|
1644
5285
|
this._abortController = null;
|
|
1645
5286
|
this._firstDataRendered = false;
|
|
5287
|
+
// Formula engine (optional — wired by OGrid when formulas option is enabled)
|
|
5288
|
+
this._formulaEngine = null;
|
|
1646
5289
|
// Filter options for client-side data (used by sidebar filters panel & header filter popovers)
|
|
1647
5290
|
this._filterOptions = {};
|
|
1648
5291
|
// Column display order (array of columnIds)
|
|
@@ -1667,6 +5310,7 @@ var GridState = class {
|
|
|
1667
5310
|
this._ariaLabel = options.ariaLabel;
|
|
1668
5311
|
this._stickyHeader = options.stickyHeader ?? true;
|
|
1669
5312
|
this._fullScreen = options.fullScreen ?? false;
|
|
5313
|
+
this._workerSort = options.workerSort ?? false;
|
|
1670
5314
|
if (!this._dataSource) {
|
|
1671
5315
|
this._filterOptions = deriveFilterOptionsFromData(
|
|
1672
5316
|
this._data,
|
|
@@ -1770,6 +5414,31 @@ var GridState = class {
|
|
|
1770
5414
|
const items = filtered.slice(startIdx, endIdx);
|
|
1771
5415
|
return { items, totalCount };
|
|
1772
5416
|
}
|
|
5417
|
+
/** Whether worker sort should be used for the current data set. */
|
|
5418
|
+
get useWorkerSort() {
|
|
5419
|
+
return this._workerSort === true || this._workerSort === "auto" && this._data.length > 5e3;
|
|
5420
|
+
}
|
|
5421
|
+
/**
|
|
5422
|
+
* Async version of getProcessedItems that offloads sort/filter to a Web Worker.
|
|
5423
|
+
* Falls back to sync when worker sort is not active.
|
|
5424
|
+
*/
|
|
5425
|
+
async getProcessedItemsAsync() {
|
|
5426
|
+
if (this.isServerSide || !this.useWorkerSort) {
|
|
5427
|
+
return this.getProcessedItems();
|
|
5428
|
+
}
|
|
5429
|
+
const filtered = await processClientSideDataAsync(
|
|
5430
|
+
this._data,
|
|
5431
|
+
this._columns,
|
|
5432
|
+
this._filters,
|
|
5433
|
+
this._sort?.field,
|
|
5434
|
+
this._sort?.direction
|
|
5435
|
+
);
|
|
5436
|
+
const totalCount = filtered.length;
|
|
5437
|
+
const startIdx = (this._page - 1) * this._pageSize;
|
|
5438
|
+
const endIdx = startIdx + this._pageSize;
|
|
5439
|
+
const items = filtered.slice(startIdx, endIdx);
|
|
5440
|
+
return { items, totalCount };
|
|
5441
|
+
}
|
|
1773
5442
|
// --- Server-side fetch ---
|
|
1774
5443
|
fetchServerData() {
|
|
1775
5444
|
if (!this._dataSource) return;
|
|
@@ -1951,19 +5620,29 @@ var GridState = class {
|
|
|
1951
5620
|
},
|
|
1952
5621
|
getColumnOrder: () => [...this._columnOrder],
|
|
1953
5622
|
setColumnOrder: (order) => this.setColumnOrder(order),
|
|
1954
|
-
exportToCsv: (filename) => {
|
|
5623
|
+
exportToCsv: (filename, options) => {
|
|
1955
5624
|
const { items } = this.getProcessedItems();
|
|
1956
5625
|
const cols = this.visibleColumnDefs.map((c) => ({ columnId: c.columnId, name: c.name }));
|
|
5626
|
+
const formulaOptions = this._formulaEngine?.isEnabled() ? {
|
|
5627
|
+
getFormula: this._formulaEngine.getFormula.bind(this._formulaEngine),
|
|
5628
|
+
hasFormula: this._formulaEngine.hasFormula.bind(this._formulaEngine),
|
|
5629
|
+
columnIdToIndex: new Map(this.visibleColumnDefs.map((c, i) => [c.columnId, i])),
|
|
5630
|
+
exportMode: options?.exportMode ?? "values"
|
|
5631
|
+
} : void 0;
|
|
1957
5632
|
exportToCsv(items, cols, (item, colId) => {
|
|
1958
5633
|
const col = this._columns.find((c) => c.columnId === colId);
|
|
1959
5634
|
if (!col) return "";
|
|
1960
5635
|
const val = getCellValue(item, col);
|
|
1961
5636
|
if (col.valueFormatter) return col.valueFormatter(val, item);
|
|
1962
5637
|
return val != null ? String(val) : "";
|
|
1963
|
-
}, filename);
|
|
5638
|
+
}, filename, formulaOptions);
|
|
1964
5639
|
}
|
|
1965
5640
|
};
|
|
1966
5641
|
}
|
|
5642
|
+
/** Wire in the formula engine so exportToCsv can use it. */
|
|
5643
|
+
setFormulaEngine(engine) {
|
|
5644
|
+
this._formulaEngine = engine;
|
|
5645
|
+
}
|
|
1967
5646
|
destroy() {
|
|
1968
5647
|
if (this._abortController) {
|
|
1969
5648
|
this._abortController.abort();
|
|
@@ -2159,6 +5838,9 @@ var TableRenderer = class {
|
|
|
2159
5838
|
this.table = document.createElement("table");
|
|
2160
5839
|
this.table.className = "ogrid-table";
|
|
2161
5840
|
this.table.setAttribute("role", "grid");
|
|
5841
|
+
if (this.virtualScrollState) {
|
|
5842
|
+
this.table.setAttribute("data-virtual-scroll", "");
|
|
5843
|
+
}
|
|
2162
5844
|
this.thead = document.createElement("thead");
|
|
2163
5845
|
if (this.state.stickyHeader) {
|
|
2164
5846
|
this.thead.classList.add("ogrid-sticky-header");
|
|
@@ -2194,6 +5876,7 @@ var TableRenderer = class {
|
|
|
2194
5876
|
parts.push(`allSel:${is?.allSelected ?? ""}`);
|
|
2195
5877
|
parts.push(`someSel:${is?.someSelected ?? ""}`);
|
|
2196
5878
|
parts.push(`rn:${is?.showRowNumbers ?? ""}`);
|
|
5879
|
+
parts.push(`cl:${is?.showColumnLetters ?? ""}`);
|
|
2197
5880
|
for (const [colId, config] of this.filterConfigs) {
|
|
2198
5881
|
const hasActive = this.headerFilterState?.hasActiveFilter(config);
|
|
2199
5882
|
if (hasActive) parts.push(`flt:${colId}`);
|
|
@@ -2395,6 +6078,30 @@ var TableRenderer = class {
|
|
|
2395
6078
|
this.thead.innerHTML = "";
|
|
2396
6079
|
const visibleCols = this.state.visibleColumnDefs;
|
|
2397
6080
|
const hasCheckbox = this.hasCheckboxColumn();
|
|
6081
|
+
const hasRowNumbers = this.hasRowNumbersColumn();
|
|
6082
|
+
if (this.interactionState?.showColumnLetters) {
|
|
6083
|
+
const letterTr = document.createElement("tr");
|
|
6084
|
+
letterTr.className = "ogrid-column-letter-row";
|
|
6085
|
+
if (hasCheckbox) {
|
|
6086
|
+
const th = document.createElement("th");
|
|
6087
|
+
th.className = "ogrid-column-letter-cell";
|
|
6088
|
+
th.style.width = `${CHECKBOX_COLUMN_WIDTH}px`;
|
|
6089
|
+
letterTr.appendChild(th);
|
|
6090
|
+
}
|
|
6091
|
+
if (hasRowNumbers) {
|
|
6092
|
+
const th = document.createElement("th");
|
|
6093
|
+
th.className = "ogrid-column-letter-cell";
|
|
6094
|
+
th.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
|
|
6095
|
+
letterTr.appendChild(th);
|
|
6096
|
+
}
|
|
6097
|
+
for (let colIdx = 0; colIdx < visibleCols.length; colIdx++) {
|
|
6098
|
+
const th = document.createElement("th");
|
|
6099
|
+
th.className = "ogrid-column-letter-cell";
|
|
6100
|
+
th.textContent = indexToColumnLetter(colIdx);
|
|
6101
|
+
letterTr.appendChild(th);
|
|
6102
|
+
}
|
|
6103
|
+
this.thead.appendChild(letterTr);
|
|
6104
|
+
}
|
|
2398
6105
|
const headerRows = buildHeaderRows(this.state.allColumns, this.state.visibleColumns);
|
|
2399
6106
|
if (headerRows.length > 1) {
|
|
2400
6107
|
for (const row of headerRows) {
|
|
@@ -2571,6 +6278,23 @@ var TableRenderer = class {
|
|
|
2571
6278
|
this.tbody.appendChild(topSpacer);
|
|
2572
6279
|
}
|
|
2573
6280
|
}
|
|
6281
|
+
const colVirtActive = vs?.columnVirtualizationEnabled === true && vs.columnRange != null;
|
|
6282
|
+
let renderCols = visibleCols;
|
|
6283
|
+
let colGlobalIndexMap = null;
|
|
6284
|
+
let colLeftSpacerWidth = 0;
|
|
6285
|
+
let colRightSpacerWidth = 0;
|
|
6286
|
+
if (colVirtActive && vs) {
|
|
6287
|
+
const partition = partitionColumnsForVirtualization(
|
|
6288
|
+
visibleCols,
|
|
6289
|
+
vs.columnRange,
|
|
6290
|
+
this.interactionState?.pinnedColumns
|
|
6291
|
+
);
|
|
6292
|
+
const combined = [...partition.pinnedLeft, ...partition.virtualizedUnpinned, ...partition.pinnedRight];
|
|
6293
|
+
colGlobalIndexMap = combined.map((c) => visibleCols.indexOf(c));
|
|
6294
|
+
renderCols = combined;
|
|
6295
|
+
colLeftSpacerWidth = partition.leftSpacerWidth;
|
|
6296
|
+
colRightSpacerWidth = partition.rightSpacerWidth;
|
|
6297
|
+
}
|
|
2574
6298
|
for (let rowIndex = startIndex; rowIndex <= endIndex; rowIndex++) {
|
|
2575
6299
|
const item = items[rowIndex];
|
|
2576
6300
|
if (!item) continue;
|
|
@@ -2610,9 +6334,18 @@ var TableRenderer = class {
|
|
|
2610
6334
|
td.textContent = String(rowNumberOffset + rowIndex + 1);
|
|
2611
6335
|
tr.appendChild(td);
|
|
2612
6336
|
}
|
|
2613
|
-
|
|
2614
|
-
const
|
|
2615
|
-
|
|
6337
|
+
if (colLeftSpacerWidth > 0) {
|
|
6338
|
+
const spacerTd = document.createElement("td");
|
|
6339
|
+
spacerTd.style.width = `${colLeftSpacerWidth}px`;
|
|
6340
|
+
spacerTd.style.minWidth = `${colLeftSpacerWidth}px`;
|
|
6341
|
+
spacerTd.style.padding = "0";
|
|
6342
|
+
spacerTd.style.border = "none";
|
|
6343
|
+
spacerTd.setAttribute("aria-hidden", "true");
|
|
6344
|
+
tr.appendChild(spacerTd);
|
|
6345
|
+
}
|
|
6346
|
+
for (let colIndex = 0; colIndex < renderCols.length; colIndex++) {
|
|
6347
|
+
const col = renderCols[colIndex];
|
|
6348
|
+
const globalColIndex = (colGlobalIndexMap ? colGlobalIndexMap[colIndex] : colIndex) + colOffset;
|
|
2616
6349
|
const td = document.createElement("td");
|
|
2617
6350
|
td.className = "ogrid-cell";
|
|
2618
6351
|
td.setAttribute("data-column-id", col.columnId);
|
|
@@ -2680,6 +6413,15 @@ var TableRenderer = class {
|
|
|
2680
6413
|
}
|
|
2681
6414
|
tr.appendChild(td);
|
|
2682
6415
|
}
|
|
6416
|
+
if (colRightSpacerWidth > 0) {
|
|
6417
|
+
const spacerTd = document.createElement("td");
|
|
6418
|
+
spacerTd.style.width = `${colRightSpacerWidth}px`;
|
|
6419
|
+
spacerTd.style.minWidth = `${colRightSpacerWidth}px`;
|
|
6420
|
+
spacerTd.style.padding = "0";
|
|
6421
|
+
spacerTd.style.border = "none";
|
|
6422
|
+
spacerTd.setAttribute("aria-hidden", "true");
|
|
6423
|
+
tr.appendChild(spacerTd);
|
|
6424
|
+
}
|
|
2683
6425
|
this.tbody.appendChild(tr);
|
|
2684
6426
|
}
|
|
2685
6427
|
if (isVirtual && vs) {
|
|
@@ -4026,6 +7768,14 @@ var VirtualScrollState = class {
|
|
|
4026
7768
|
this._ro = null;
|
|
4027
7769
|
this._resizeRafId = 0;
|
|
4028
7770
|
this._cachedRange = { startIndex: 0, endIndex: -1, offsetTop: 0, offsetBottom: 0 };
|
|
7771
|
+
// Column virtualization
|
|
7772
|
+
this._scrollLeft = 0;
|
|
7773
|
+
this._scrollLeftRafId = 0;
|
|
7774
|
+
this._containerWidth = 0;
|
|
7775
|
+
this._columnWidths = [];
|
|
7776
|
+
this._cachedColumnRange = null;
|
|
7777
|
+
this._roWidth = null;
|
|
7778
|
+
this._resizeWidthRafId = 0;
|
|
4029
7779
|
this._config = config ?? { enabled: false };
|
|
4030
7780
|
validateVirtualScrollConfig(this._config);
|
|
4031
7781
|
}
|
|
@@ -4054,6 +7804,71 @@ var VirtualScrollState = class {
|
|
|
4054
7804
|
get totalHeight() {
|
|
4055
7805
|
return computeTotalHeight(this._totalRows, this._config.rowHeight ?? DEFAULT_ROW_HEIGHT);
|
|
4056
7806
|
}
|
|
7807
|
+
/** Whether column virtualization is active. */
|
|
7808
|
+
get columnVirtualizationEnabled() {
|
|
7809
|
+
return this._config.columns === true;
|
|
7810
|
+
}
|
|
7811
|
+
/** Get the current visible column range (null when column virtualization is disabled). */
|
|
7812
|
+
get columnRange() {
|
|
7813
|
+
return this._cachedColumnRange;
|
|
7814
|
+
}
|
|
7815
|
+
/** Set the unpinned column widths for horizontal virtualization. */
|
|
7816
|
+
setColumnWidths(widths) {
|
|
7817
|
+
this._columnWidths = widths;
|
|
7818
|
+
this.recomputeColumnRange();
|
|
7819
|
+
}
|
|
7820
|
+
/** Handle horizontal scroll events. RAF-throttled. */
|
|
7821
|
+
handleHorizontalScroll(scrollLeft) {
|
|
7822
|
+
if (!this.columnVirtualizationEnabled) return;
|
|
7823
|
+
if (this._scrollLeftRafId) cancelAnimationFrame(this._scrollLeftRafId);
|
|
7824
|
+
this._scrollLeftRafId = requestAnimationFrame(() => {
|
|
7825
|
+
this._scrollLeftRafId = 0;
|
|
7826
|
+
this._scrollLeft = scrollLeft;
|
|
7827
|
+
this.recomputeColumnRange();
|
|
7828
|
+
});
|
|
7829
|
+
}
|
|
7830
|
+
/** Observe a container element for width changes (column virtualization). */
|
|
7831
|
+
observeContainerWidth(el) {
|
|
7832
|
+
this.disconnectWidthObserver();
|
|
7833
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
7834
|
+
this._roWidth = new ResizeObserver((entries) => {
|
|
7835
|
+
if (entries.length === 0) return;
|
|
7836
|
+
this._containerWidth = entries[0].contentRect.width;
|
|
7837
|
+
if (this._resizeWidthRafId) cancelAnimationFrame(this._resizeWidthRafId);
|
|
7838
|
+
this._resizeWidthRafId = requestAnimationFrame(() => {
|
|
7839
|
+
this._resizeWidthRafId = 0;
|
|
7840
|
+
this.recomputeColumnRange();
|
|
7841
|
+
});
|
|
7842
|
+
});
|
|
7843
|
+
this._roWidth.observe(el);
|
|
7844
|
+
}
|
|
7845
|
+
this._containerWidth = el.clientWidth;
|
|
7846
|
+
}
|
|
7847
|
+
disconnectWidthObserver() {
|
|
7848
|
+
if (this._roWidth) {
|
|
7849
|
+
this._roWidth.disconnect();
|
|
7850
|
+
this._roWidth = null;
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
/** Recompute visible column range and emit if changed. */
|
|
7854
|
+
recomputeColumnRange() {
|
|
7855
|
+
if (!this.columnVirtualizationEnabled || this._columnWidths.length === 0 || this._containerWidth <= 0) {
|
|
7856
|
+
if (this._cachedColumnRange !== null) {
|
|
7857
|
+
this._cachedColumnRange = null;
|
|
7858
|
+
this.emitter.emit("columnRangeChanged", { columnRange: null });
|
|
7859
|
+
}
|
|
7860
|
+
return;
|
|
7861
|
+
}
|
|
7862
|
+
const overscan = this._config.columnOverscan ?? 2;
|
|
7863
|
+
const newRange = computeVisibleColumnRange(this._scrollLeft, this._columnWidths, this._containerWidth, overscan);
|
|
7864
|
+
const prev = this._cachedColumnRange;
|
|
7865
|
+
if (!prev || prev.startIndex !== newRange.startIndex || prev.endIndex !== newRange.endIndex) {
|
|
7866
|
+
this._cachedColumnRange = newRange;
|
|
7867
|
+
this.emitter.emit("columnRangeChanged", { columnRange: newRange });
|
|
7868
|
+
} else {
|
|
7869
|
+
this._cachedColumnRange = newRange;
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
4057
7872
|
/** Handle scroll events from the table container. RAF-throttled. */
|
|
4058
7873
|
handleScroll(scrollTop) {
|
|
4059
7874
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
@@ -4130,6 +7945,10 @@ var VirtualScrollState = class {
|
|
|
4130
7945
|
this.emitter.on("rangeChanged", handler);
|
|
4131
7946
|
return () => this.emitter.off("rangeChanged", handler);
|
|
4132
7947
|
}
|
|
7948
|
+
onColumnRangeChanged(handler) {
|
|
7949
|
+
this.emitter.on("columnRangeChanged", handler);
|
|
7950
|
+
return () => this.emitter.off("columnRangeChanged", handler);
|
|
7951
|
+
}
|
|
4133
7952
|
onConfigChanged(handler) {
|
|
4134
7953
|
this.emitter.on("configChanged", handler);
|
|
4135
7954
|
return () => this.emitter.off("configChanged", handler);
|
|
@@ -4137,7 +7956,10 @@ var VirtualScrollState = class {
|
|
|
4137
7956
|
destroy() {
|
|
4138
7957
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
4139
7958
|
if (this._resizeRafId) cancelAnimationFrame(this._resizeRafId);
|
|
7959
|
+
if (this._scrollLeftRafId) cancelAnimationFrame(this._scrollLeftRafId);
|
|
7960
|
+
if (this._resizeWidthRafId) cancelAnimationFrame(this._resizeWidthRafId);
|
|
4140
7961
|
this.disconnectObserver();
|
|
7962
|
+
this.disconnectWidthObserver();
|
|
4141
7963
|
this.emitter.removeAllListeners();
|
|
4142
7964
|
}
|
|
4143
7965
|
};
|
|
@@ -4261,7 +8083,7 @@ var KeyboardNavState = class {
|
|
|
4261
8083
|
const maxColIndex = visibleCols.length - 1 + colOffset;
|
|
4262
8084
|
if (items.length === 0) return;
|
|
4263
8085
|
if (activeCell === null) {
|
|
4264
|
-
if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End"].includes(e.key)) {
|
|
8086
|
+
if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End", "PageDown", "PageUp"].includes(e.key)) {
|
|
4265
8087
|
this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
4266
8088
|
e.preventDefault();
|
|
4267
8089
|
}
|
|
@@ -4362,6 +8184,43 @@ var KeyboardNavState = class {
|
|
|
4362
8184
|
});
|
|
4363
8185
|
break;
|
|
4364
8186
|
}
|
|
8187
|
+
case "PageDown":
|
|
8188
|
+
case "PageUp": {
|
|
8189
|
+
e.preventDefault();
|
|
8190
|
+
const wrapper = this.wrapperRef;
|
|
8191
|
+
let pageSize = 10;
|
|
8192
|
+
let rowHeight = 36;
|
|
8193
|
+
if (wrapper) {
|
|
8194
|
+
const firstRow = wrapper.querySelector("tbody tr");
|
|
8195
|
+
if (firstRow && firstRow.offsetHeight > 0) {
|
|
8196
|
+
rowHeight = firstRow.offsetHeight;
|
|
8197
|
+
pageSize = Math.max(1, Math.floor(wrapper.clientHeight / rowHeight));
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
const pgDirection = e.key === "PageDown" ? 1 : -1;
|
|
8201
|
+
const newRowPage = Math.max(0, Math.min(rowIndex + pgDirection * pageSize, maxRowIndex));
|
|
8202
|
+
const pgShift = e.shiftKey;
|
|
8203
|
+
if (pgShift) {
|
|
8204
|
+
this.setSelectionRange({
|
|
8205
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
8206
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
8207
|
+
endRow: newRowPage,
|
|
8208
|
+
endCol: selectionRange?.endCol ?? dataColIndex
|
|
8209
|
+
});
|
|
8210
|
+
} else {
|
|
8211
|
+
this.setSelectionRange({
|
|
8212
|
+
startRow: newRowPage,
|
|
8213
|
+
startCol: dataColIndex,
|
|
8214
|
+
endRow: newRowPage,
|
|
8215
|
+
endCol: dataColIndex
|
|
8216
|
+
});
|
|
8217
|
+
}
|
|
8218
|
+
this.setActiveCell({ rowIndex: newRowPage, columnIndex });
|
|
8219
|
+
if (wrapper) {
|
|
8220
|
+
wrapper.scrollTop = getScrollTopForRow(newRowPage, rowHeight, wrapper.clientHeight, "center");
|
|
8221
|
+
}
|
|
8222
|
+
break;
|
|
8223
|
+
}
|
|
4365
8224
|
case "Enter":
|
|
4366
8225
|
case "F2": {
|
|
4367
8226
|
e.preventDefault();
|
|
@@ -4492,8 +8351,9 @@ var ClipboardState = class {
|
|
|
4492
8351
|
const range = this.getEffectiveRange();
|
|
4493
8352
|
if (range == null) return;
|
|
4494
8353
|
const norm = normalizeSelectionRange(range);
|
|
4495
|
-
const { items, visibleCols } = this.params;
|
|
4496
|
-
const
|
|
8354
|
+
const { items, visibleCols, formulas, flatColumns, getFormula, hasFormula, colOffset } = this.params;
|
|
8355
|
+
const formulaOptions = formulas && flatColumns ? { colOffset, flatColumns, getFormula, hasFormula } : void 0;
|
|
8356
|
+
const tsv = formatSelectionAsTsv(items, visibleCols, norm, formulaOptions);
|
|
4497
8357
|
this.internalClipboard = tsv;
|
|
4498
8358
|
this._copyRange = norm;
|
|
4499
8359
|
this._cutRange = null;
|
|
@@ -4536,9 +8396,10 @@ var ClipboardState = class {
|
|
|
4536
8396
|
const norm = this.getEffectiveRange();
|
|
4537
8397
|
const anchorRow = norm ? norm.startRow : 0;
|
|
4538
8398
|
const anchorCol = norm ? norm.startCol : 0;
|
|
4539
|
-
const { items, visibleCols } = this.params;
|
|
8399
|
+
const { items, visibleCols, formulas, flatColumns, setFormula, colOffset } = this.params;
|
|
8400
|
+
const formulaOptions = formulas && flatColumns ? { colOffset, flatColumns, setFormula } : void 0;
|
|
4540
8401
|
const parsedRows = parseTsvClipboard(text);
|
|
4541
|
-
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
|
|
8402
|
+
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions);
|
|
4542
8403
|
for (const evt of pasteEvents) onCellValueChanged(evt);
|
|
4543
8404
|
if (this._cutRange) {
|
|
4544
8405
|
const cutEvents = applyCutClear(this._cutRange, items, visibleCols);
|
|
@@ -4792,9 +8653,9 @@ var FillHandleState = class {
|
|
|
4792
8653
|
this.emitter.emit("fillRangeChange", { fillRange: null });
|
|
4793
8654
|
}
|
|
4794
8655
|
applyFillValuesFromCore(norm, start) {
|
|
4795
|
-
const { items, visibleCols, onCellValueChanged, beginBatch, endBatch } = this.params;
|
|
8656
|
+
const { items, visibleCols, onCellValueChanged, beginBatch, endBatch, formulaOptions } = this.params;
|
|
4796
8657
|
if (!onCellValueChanged) return;
|
|
4797
|
-
const fillEvents = applyFillValues(norm, start.startRow, start.startCol, items, visibleCols);
|
|
8658
|
+
const fillEvents = applyFillValues(norm, start.startRow, start.startCol, items, visibleCols, formulaOptions);
|
|
4798
8659
|
if (fillEvents.length > 0) {
|
|
4799
8660
|
beginBatch?.();
|
|
4800
8661
|
for (const evt of fillEvents) onCellValueChanged(evt);
|
|
@@ -5260,6 +9121,8 @@ var InlineCellEditor = class {
|
|
|
5260
9121
|
e.stopPropagation();
|
|
5261
9122
|
this.onCancel?.();
|
|
5262
9123
|
this.closeEditor();
|
|
9124
|
+
} else if ((e.ctrlKey || e.metaKey) && ["c", "x", "v", "a", "z", "y"].includes(e.key)) {
|
|
9125
|
+
e.stopPropagation();
|
|
5263
9126
|
}
|
|
5264
9127
|
});
|
|
5265
9128
|
input.addEventListener("blur", () => {
|
|
@@ -5268,7 +9131,16 @@ var InlineCellEditor = class {
|
|
|
5268
9131
|
}
|
|
5269
9132
|
this.closeEditor();
|
|
5270
9133
|
});
|
|
5271
|
-
|
|
9134
|
+
if (type === "date") {
|
|
9135
|
+
setTimeout(() => {
|
|
9136
|
+
try {
|
|
9137
|
+
input.showPicker();
|
|
9138
|
+
} catch {
|
|
9139
|
+
}
|
|
9140
|
+
}, 0);
|
|
9141
|
+
} else {
|
|
9142
|
+
setTimeout(() => input.select(), 0);
|
|
9143
|
+
}
|
|
5272
9144
|
return input;
|
|
5273
9145
|
}
|
|
5274
9146
|
createTextEditor(value) {
|
|
@@ -5346,6 +9218,7 @@ var InlineCellEditor = class {
|
|
|
5346
9218
|
dropdown.style.border = "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))";
|
|
5347
9219
|
dropdown.style.zIndex = "1001";
|
|
5348
9220
|
dropdown.style.boxShadow = "0 4px 16px rgba(0,0,0,0.2)";
|
|
9221
|
+
dropdown.style.textAlign = "left";
|
|
5349
9222
|
wrapper.appendChild(dropdown);
|
|
5350
9223
|
let highlightedIndex = Math.max(values.findIndex((v) => String(v) === String(value)), 0);
|
|
5351
9224
|
const buildOptions = () => {
|
|
@@ -5463,6 +9336,7 @@ var InlineCellEditor = class {
|
|
|
5463
9336
|
dropdown.style.backgroundColor = "var(--ogrid-bg, #fff)";
|
|
5464
9337
|
dropdown.style.border = "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))";
|
|
5465
9338
|
dropdown.style.zIndex = "1001";
|
|
9339
|
+
dropdown.style.textAlign = "left";
|
|
5466
9340
|
wrapper.appendChild(dropdown);
|
|
5467
9341
|
const values = column.cellEditorParams?.values ?? [];
|
|
5468
9342
|
const formatValue = column.cellEditorParams?.formatValue ?? ((v) => String(v));
|
|
@@ -5510,6 +9384,8 @@ var InlineCellEditor = class {
|
|
|
5510
9384
|
e.stopPropagation();
|
|
5511
9385
|
this.onCancel?.();
|
|
5512
9386
|
this.closeEditor();
|
|
9387
|
+
} else if ((e.ctrlKey || e.metaKey) && ["c", "x", "v", "a", "z", "y"].includes(e.key)) {
|
|
9388
|
+
e.stopPropagation();
|
|
5513
9389
|
}
|
|
5514
9390
|
});
|
|
5515
9391
|
input.addEventListener("blur", (e) => {
|
|
@@ -5919,7 +9795,11 @@ var OGridRendering = class {
|
|
|
5919
9795
|
allSelected: rowSelectionState?.isAllSelected(items),
|
|
5920
9796
|
someSelected: rowSelectionState?.isSomeSelected(items),
|
|
5921
9797
|
// Row numbers
|
|
5922
|
-
showRowNumbers: options.showRowNumbers,
|
|
9798
|
+
showRowNumbers: options.showRowNumbers || options.cellReferences,
|
|
9799
|
+
// Column letters
|
|
9800
|
+
showColumnLetters: !!options.cellReferences,
|
|
9801
|
+
// Name box
|
|
9802
|
+
showNameBox: !!options.cellReferences,
|
|
5923
9803
|
// Column pinning
|
|
5924
9804
|
pinnedColumns: pinningState?.pinnedColumns,
|
|
5925
9805
|
leftOffsets,
|
|
@@ -6258,6 +10138,7 @@ var OGrid = class {
|
|
|
6258
10138
|
this.unsubscribes = [];
|
|
6259
10139
|
this.isFullScreen = false;
|
|
6260
10140
|
this.fullscreenBtn = null;
|
|
10141
|
+
this.nameBoxEl = null;
|
|
6261
10142
|
this.options = options;
|
|
6262
10143
|
this.state = new GridState(options);
|
|
6263
10144
|
this.api = this.state.getApi();
|
|
@@ -6269,6 +10150,13 @@ var OGrid = class {
|
|
|
6269
10150
|
this.toolbarEl.className = "ogrid-toolbar";
|
|
6270
10151
|
const toolbarSpacer = document.createElement("div");
|
|
6271
10152
|
this.toolbarEl.appendChild(toolbarSpacer);
|
|
10153
|
+
if (options.cellReferences) {
|
|
10154
|
+
this.nameBoxEl = document.createElement("div");
|
|
10155
|
+
this.nameBoxEl.className = "ogrid-name-box";
|
|
10156
|
+
this.nameBoxEl.style.cssText = "display:inline-flex;align-items:center;padding:0 8px;font-family:'Consolas','Courier New',monospace;font-size:12px;border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));border-radius:3px;height:24px;margin-right:8px;background:var(--ogrid-bg, #fff);min-width:40px;color:var(--ogrid-fg-secondary, rgba(0,0,0,0.6));";
|
|
10157
|
+
this.nameBoxEl.textContent = "\u2014";
|
|
10158
|
+
toolbarSpacer.appendChild(this.nameBoxEl);
|
|
10159
|
+
}
|
|
6272
10160
|
if (options.fullScreen) {
|
|
6273
10161
|
const toolbarRight = document.createElement("div");
|
|
6274
10162
|
toolbarRight.style.display = "flex";
|
|
@@ -6405,6 +10293,24 @@ var OGrid = class {
|
|
|
6405
10293
|
this.cellEditor = result.cellEditor;
|
|
6406
10294
|
this.contextMenu = result.contextMenu;
|
|
6407
10295
|
this.unsubscribes.push(...result.unsubscribes);
|
|
10296
|
+
if (this.nameBoxEl && this.selectionState) {
|
|
10297
|
+
const nameBox = this.nameBoxEl;
|
|
10298
|
+
const sel = this.selectionState;
|
|
10299
|
+
let colOffset = 0;
|
|
10300
|
+
if (this.rowSelectionState) colOffset++;
|
|
10301
|
+
if (options.showRowNumbers || options.cellReferences) colOffset++;
|
|
10302
|
+
this.unsubscribes.push(
|
|
10303
|
+
sel.onSelectionChange(({ activeCell }) => {
|
|
10304
|
+
if (activeCell) {
|
|
10305
|
+
const dataColIndex = activeCell.columnIndex - colOffset;
|
|
10306
|
+
const rowNumber = (this.state.page - 1) * this.state.pageSize + activeCell.rowIndex + 1;
|
|
10307
|
+
nameBox.textContent = formatCellReference(dataColIndex, rowNumber);
|
|
10308
|
+
} else {
|
|
10309
|
+
nameBox.textContent = "\u2014";
|
|
10310
|
+
}
|
|
10311
|
+
})
|
|
10312
|
+
);
|
|
10313
|
+
}
|
|
6408
10314
|
}
|
|
6409
10315
|
this.unsubscribes.push(
|
|
6410
10316
|
this.state.onStateChange(() => {
|
|
@@ -6427,16 +10333,25 @@ var OGrid = class {
|
|
|
6427
10333
|
this.renderer.setVirtualScrollState(this.virtualScrollState);
|
|
6428
10334
|
const handleScroll = () => {
|
|
6429
10335
|
this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
|
|
10336
|
+
this.virtualScrollState?.handleHorizontalScroll(this.tableContainer.scrollLeft);
|
|
6430
10337
|
};
|
|
6431
10338
|
this.tableContainer.addEventListener("scroll", handleScroll, { passive: true });
|
|
6432
10339
|
this.unsubscribes.push(() => {
|
|
6433
10340
|
this.tableContainer.removeEventListener("scroll", handleScroll);
|
|
6434
10341
|
});
|
|
10342
|
+
if (options.virtualScroll?.columns) {
|
|
10343
|
+
this.virtualScrollState.observeContainerWidth(this.tableContainer);
|
|
10344
|
+
}
|
|
6435
10345
|
this.unsubscribes.push(
|
|
6436
10346
|
this.virtualScrollState.onRangeChanged(() => {
|
|
6437
10347
|
this.renderingHelper.updateRendererInteractionState();
|
|
6438
10348
|
})
|
|
6439
10349
|
);
|
|
10350
|
+
this.unsubscribes.push(
|
|
10351
|
+
this.virtualScrollState.onColumnRangeChanged(() => {
|
|
10352
|
+
this.renderingHelper.updateRendererInteractionState();
|
|
10353
|
+
})
|
|
10354
|
+
);
|
|
6440
10355
|
this.api.scrollToRow = (index, opts) => {
|
|
6441
10356
|
this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
|
|
6442
10357
|
};
|
|
@@ -6679,4 +10594,118 @@ var OGrid = class {
|
|
|
6679
10594
|
}
|
|
6680
10595
|
};
|
|
6681
10596
|
|
|
6682
|
-
|
|
10597
|
+
// src/state/FormulaEngineState.ts
|
|
10598
|
+
var FormulaEngineState = class {
|
|
10599
|
+
constructor(options) {
|
|
10600
|
+
this.emitter = new EventEmitter();
|
|
10601
|
+
this.engine = null;
|
|
10602
|
+
this.options = options;
|
|
10603
|
+
if (options.formulas) {
|
|
10604
|
+
this.engine = new FormulaEngine({
|
|
10605
|
+
customFunctions: options.formulaFunctions,
|
|
10606
|
+
namedRanges: options.namedRanges
|
|
10607
|
+
});
|
|
10608
|
+
if (options.sheets) {
|
|
10609
|
+
for (const [name, accessor] of Object.entries(options.sheets)) {
|
|
10610
|
+
this.engine.registerSheet(name, accessor);
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
/**
|
|
10616
|
+
* Initialize with an accessor — loads `initialFormulas` if provided.
|
|
10617
|
+
* Must be called after the grid data is available so the accessor is valid.
|
|
10618
|
+
*/
|
|
10619
|
+
initialize(accessor) {
|
|
10620
|
+
if (!this.engine || !this.options.initialFormulas?.length) return;
|
|
10621
|
+
const result = this.engine.loadFormulas(this.options.initialFormulas, accessor);
|
|
10622
|
+
if (result.updatedCells.length > 0) {
|
|
10623
|
+
this.emitRecalc(result);
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
/**
|
|
10627
|
+
* Set or clear a formula for a cell. Triggers recalculation of dependents
|
|
10628
|
+
* and emits `formulaRecalc`.
|
|
10629
|
+
*/
|
|
10630
|
+
setFormula(col, row, formula, accessor) {
|
|
10631
|
+
if (!this.engine) return void 0;
|
|
10632
|
+
const result = this.engine.setFormula(col, row, formula, accessor);
|
|
10633
|
+
if (result.updatedCells.length > 0) {
|
|
10634
|
+
this.emitRecalc(result);
|
|
10635
|
+
}
|
|
10636
|
+
return result;
|
|
10637
|
+
}
|
|
10638
|
+
/**
|
|
10639
|
+
* Notify the engine that a non-formula cell's value changed.
|
|
10640
|
+
* Triggers recalculation of any formulas that depend on the changed cell.
|
|
10641
|
+
*/
|
|
10642
|
+
onCellChanged(col, row, accessor) {
|
|
10643
|
+
if (!this.engine) return void 0;
|
|
10644
|
+
const result = this.engine.onCellChanged(col, row, accessor);
|
|
10645
|
+
if (result.updatedCells.length > 0) {
|
|
10646
|
+
this.emitRecalc(result);
|
|
10647
|
+
}
|
|
10648
|
+
return result;
|
|
10649
|
+
}
|
|
10650
|
+
/** Get the computed value for a formula cell (or undefined if no formula). */
|
|
10651
|
+
getValue(col, row) {
|
|
10652
|
+
return this.engine?.getValue(col, row);
|
|
10653
|
+
}
|
|
10654
|
+
/** Check if a cell has a formula. */
|
|
10655
|
+
hasFormula(col, row) {
|
|
10656
|
+
return this.engine?.hasFormula(col, row) ?? false;
|
|
10657
|
+
}
|
|
10658
|
+
/** Get the formula string for a cell (or undefined if no formula). */
|
|
10659
|
+
getFormula(col, row) {
|
|
10660
|
+
return this.engine?.getFormula(col, row);
|
|
10661
|
+
}
|
|
10662
|
+
/** Whether the formula engine is active. */
|
|
10663
|
+
isEnabled() {
|
|
10664
|
+
return this.engine !== null;
|
|
10665
|
+
}
|
|
10666
|
+
/** Define a named range. */
|
|
10667
|
+
defineNamedRange(name, ref) {
|
|
10668
|
+
this.engine?.defineNamedRange(name, ref);
|
|
10669
|
+
}
|
|
10670
|
+
/** Remove a named range. */
|
|
10671
|
+
removeNamedRange(name) {
|
|
10672
|
+
this.engine?.removeNamedRange(name);
|
|
10673
|
+
}
|
|
10674
|
+
/** Register a sheet accessor for cross-sheet references. */
|
|
10675
|
+
registerSheet(name, accessor) {
|
|
10676
|
+
this.engine?.registerSheet(name, accessor);
|
|
10677
|
+
}
|
|
10678
|
+
/** Unregister a sheet accessor. */
|
|
10679
|
+
unregisterSheet(name) {
|
|
10680
|
+
this.engine?.unregisterSheet(name);
|
|
10681
|
+
}
|
|
10682
|
+
/** Get all cells that a cell depends on (deep, transitive). */
|
|
10683
|
+
getPrecedents(col, row) {
|
|
10684
|
+
return this.engine?.getPrecedents(col, row) ?? [];
|
|
10685
|
+
}
|
|
10686
|
+
/** Get all cells that depend on a cell (deep, transitive). */
|
|
10687
|
+
getDependents(col, row) {
|
|
10688
|
+
return this.engine?.getDependents(col, row) ?? [];
|
|
10689
|
+
}
|
|
10690
|
+
/** Get full audit trail for a cell. */
|
|
10691
|
+
getAuditTrail(col, row) {
|
|
10692
|
+
return this.engine?.getAuditTrail(col, row) ?? null;
|
|
10693
|
+
}
|
|
10694
|
+
/** Subscribe to the `formulaRecalc` event. Returns an unsubscribe function. */
|
|
10695
|
+
onFormulaRecalc(handler) {
|
|
10696
|
+
this.emitter.on("formulaRecalc", handler);
|
|
10697
|
+
return () => this.emitter.off("formulaRecalc", handler);
|
|
10698
|
+
}
|
|
10699
|
+
/** Clean up all listeners. */
|
|
10700
|
+
destroy() {
|
|
10701
|
+
this.engine = null;
|
|
10702
|
+
this.emitter.removeAllListeners();
|
|
10703
|
+
}
|
|
10704
|
+
// --- Private ---
|
|
10705
|
+
emitRecalc(result) {
|
|
10706
|
+
this.options.onFormulaRecalc?.(result);
|
|
10707
|
+
this.emitter.emit("formulaRecalc", result);
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10710
|
+
|
|
10711
|
+
export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, CIRC_ERROR, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, DIV_ZERO_ERROR, DependencyGraph, EventEmitter, FillHandleState, FormulaEngine, FormulaEngineState, FormulaError, FormulaEvaluator, GENERAL_ERROR, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NAME_ERROR, NA_ERROR, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, REF_ERROR, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VALUE_ERROR, VirtualScrollState, Z_INDEX, adjustFormulaReferences, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, columnLetterToIndex, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleColumnRange, computeVisibleRange, createBuiltInFunctions, createSortFilterWorker, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, extractValueMatrix, findCtrlArrowTarget, flattenArgs, flattenColumns, formatAddress, formatCellReference, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, toString as formulaToString, fromCellKey, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, indexToColumnLetter, injectGlobalStyles, isColumnEditable, isFilterConfig, isFormulaError, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parse, parseCellRef, parseRange, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, processClientSideDataAsync, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, terminateSortFilterWorker, toBoolean, toCellKey, toNumber, toUserLike, tokenize, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };
|