@alaarab/ogrid-react 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 +4193 -174
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/hooks/useClipboard.d.ts +10 -0
- package/dist/types/hooks/useDataGridEditing.d.ts +8 -0
- package/dist/types/hooks/useDataGridInteraction.d.ts +10 -0
- package/dist/types/hooks/useDataGridTableOrchestration.d.ts +7 -0
- package/dist/types/hooks/useFillHandle.d.ts +3 -0
- package/dist/types/hooks/useFormulaEngine.d.ts +52 -0
- package/dist/types/hooks/useOGridDataFetching.d.ts +2 -0
- package/dist/types/hooks/useVirtualScroll.d.ts +14 -4
- package/dist/types/index.d.ts +3 -3
- package/dist/types/types/dataGridTypes.d.ts +51 -0
- package/dist/types/utils/dataGridViewModel.d.ts +2 -1
- package/dist/types/utils/index.d.ts +2 -2
- package/package.json +2 -2
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as React5 from 'react';
|
|
2
2
|
import { useRef, useState, useCallback, useEffect, useMemo, useImperativeHandle, useLayoutEffect, forwardRef } from 'react';
|
|
3
3
|
import { createPortal, flushSync } from 'react-dom';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -41,14 +41,23 @@ function escapeCsvValue(value) {
|
|
|
41
41
|
function buildCsvHeader(columns) {
|
|
42
42
|
return columns.map((c) => escapeCsvValue(c.name)).join(",");
|
|
43
43
|
}
|
|
44
|
-
function buildCsvRows(items, columns, getValue) {
|
|
44
|
+
function buildCsvRows(items, columns, getValue, formulaOptions) {
|
|
45
45
|
return items.map(
|
|
46
|
-
(item) => columns.map((
|
|
46
|
+
(item, rowIdx) => columns.map((col) => {
|
|
47
|
+
if (formulaOptions?.exportMode === "formulas" && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.columnIdToIndex) {
|
|
48
|
+
const colIdx = formulaOptions.columnIdToIndex.get(col.columnId);
|
|
49
|
+
if (colIdx !== void 0 && formulaOptions.hasFormula(colIdx, rowIdx)) {
|
|
50
|
+
const formula = formulaOptions.getFormula(colIdx, rowIdx);
|
|
51
|
+
if (formula) return escapeCsvValue(formula);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return escapeCsvValue(getValue(item, col.columnId));
|
|
55
|
+
}).join(",")
|
|
47
56
|
);
|
|
48
57
|
}
|
|
49
|
-
function exportToCsv(items, columns, getValue, filename) {
|
|
58
|
+
function exportToCsv(items, columns, getValue, filename, formulaOptions) {
|
|
50
59
|
const header = buildCsvHeader(columns);
|
|
51
|
-
const rows = buildCsvRows(items, columns, getValue);
|
|
60
|
+
const rows = buildCsvRows(items, columns, getValue, formulaOptions);
|
|
52
61
|
const csv = [header, ...rows].join("\n");
|
|
53
62
|
triggerCsvDownload(csv, filename ?? `export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`);
|
|
54
63
|
}
|
|
@@ -715,6 +724,305 @@ function calculateDropTarget(params) {
|
|
|
715
724
|
}
|
|
716
725
|
return { targetIndex, indicatorX };
|
|
717
726
|
}
|
|
727
|
+
function computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, overscan = 2) {
|
|
728
|
+
if (columnWidths.length === 0 || containerWidth <= 0) {
|
|
729
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
730
|
+
}
|
|
731
|
+
let cumWidth = 0;
|
|
732
|
+
let rawStart = columnWidths.length;
|
|
733
|
+
let rawEnd = -1;
|
|
734
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
735
|
+
const colStart = cumWidth;
|
|
736
|
+
cumWidth += columnWidths[i];
|
|
737
|
+
if (cumWidth > scrollLeft && rawStart === columnWidths.length) {
|
|
738
|
+
rawStart = i;
|
|
739
|
+
}
|
|
740
|
+
if (colStart < scrollLeft + containerWidth) {
|
|
741
|
+
rawEnd = i;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (rawStart > rawEnd) {
|
|
745
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
746
|
+
}
|
|
747
|
+
const startIndex = Math.max(0, rawStart - overscan);
|
|
748
|
+
const endIndex = Math.min(columnWidths.length - 1, rawEnd + overscan);
|
|
749
|
+
let leftOffset = 0;
|
|
750
|
+
for (let i = 0; i < startIndex; i++) {
|
|
751
|
+
leftOffset += columnWidths[i];
|
|
752
|
+
}
|
|
753
|
+
let rightOffset = 0;
|
|
754
|
+
for (let i = endIndex + 1; i < columnWidths.length; i++) {
|
|
755
|
+
rightOffset += columnWidths[i];
|
|
756
|
+
}
|
|
757
|
+
return { startIndex, endIndex, leftOffset, rightOffset };
|
|
758
|
+
}
|
|
759
|
+
function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColumns) {
|
|
760
|
+
const pinnedLeft = [];
|
|
761
|
+
const pinnedRight = [];
|
|
762
|
+
const unpinned = [];
|
|
763
|
+
for (const col of visibleCols) {
|
|
764
|
+
const pin = pinnedColumns?.[col.columnId];
|
|
765
|
+
if (pin === "left") pinnedLeft.push(col);
|
|
766
|
+
else if (pin === "right") pinnedRight.push(col);
|
|
767
|
+
else unpinned.push(col);
|
|
768
|
+
}
|
|
769
|
+
if (!columnRange || columnRange.endIndex < 0) {
|
|
770
|
+
return {
|
|
771
|
+
pinnedLeft,
|
|
772
|
+
virtualizedUnpinned: unpinned,
|
|
773
|
+
pinnedRight,
|
|
774
|
+
leftSpacerWidth: 0,
|
|
775
|
+
rightSpacerWidth: 0
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const virtualizedUnpinned = unpinned.slice(columnRange.startIndex, columnRange.endIndex + 1);
|
|
779
|
+
return {
|
|
780
|
+
pinnedLeft,
|
|
781
|
+
virtualizedUnpinned,
|
|
782
|
+
pinnedRight,
|
|
783
|
+
leftSpacerWidth: columnRange.leftOffset,
|
|
784
|
+
rightSpacerWidth: columnRange.rightOffset
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function getScrollTopForRow(rowIndex, rowHeight, containerHeight, align = "start") {
|
|
788
|
+
const rowTop = rowIndex * rowHeight;
|
|
789
|
+
switch (align) {
|
|
790
|
+
case "start":
|
|
791
|
+
return rowTop;
|
|
792
|
+
case "center":
|
|
793
|
+
return Math.max(0, rowTop - (containerHeight - rowHeight) / 2);
|
|
794
|
+
case "end":
|
|
795
|
+
return Math.max(0, rowTop - containerHeight + rowHeight);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function workerBody() {
|
|
799
|
+
const ctx = self;
|
|
800
|
+
ctx.onmessage = (e) => {
|
|
801
|
+
const msg = e.data;
|
|
802
|
+
if (msg.type !== "sort-filter") return;
|
|
803
|
+
const { requestId, values, filters, sort } = msg;
|
|
804
|
+
const rowCount = values.length;
|
|
805
|
+
let indices = [];
|
|
806
|
+
const filterEntries = Object.entries(filters);
|
|
807
|
+
if (filterEntries.length === 0) {
|
|
808
|
+
indices = new Array(rowCount);
|
|
809
|
+
for (let i = 0; i < rowCount; i++) indices[i] = i;
|
|
810
|
+
} else {
|
|
811
|
+
for (let r = 0; r < rowCount; r++) {
|
|
812
|
+
let pass = true;
|
|
813
|
+
for (let f = 0; f < filterEntries.length; f++) {
|
|
814
|
+
const colIdx = Number(filterEntries[f][0]);
|
|
815
|
+
const filter = filterEntries[f][1];
|
|
816
|
+
const cellVal = values[r][colIdx];
|
|
817
|
+
switch (filter.type) {
|
|
818
|
+
case "text": {
|
|
819
|
+
const trimmed = filter.value.trim().toLowerCase();
|
|
820
|
+
if (trimmed && !String(cellVal ?? "").toLowerCase().includes(trimmed)) {
|
|
821
|
+
pass = false;
|
|
822
|
+
}
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case "multiSelect": {
|
|
826
|
+
if (filter.value.length > 0) {
|
|
827
|
+
const set = new Set(filter.value);
|
|
828
|
+
if (!set.has(String(cellVal ?? ""))) {
|
|
829
|
+
pass = false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
case "date": {
|
|
835
|
+
if (cellVal == null) {
|
|
836
|
+
pass = false;
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
const ts = new Date(String(cellVal)).getTime();
|
|
840
|
+
if (isNaN(ts)) {
|
|
841
|
+
pass = false;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
if (filter.value.from) {
|
|
845
|
+
const fromTs = (/* @__PURE__ */ new Date(filter.value.from + "T00:00:00")).getTime();
|
|
846
|
+
if (ts < fromTs) {
|
|
847
|
+
pass = false;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (filter.value.to) {
|
|
852
|
+
const toTs = (/* @__PURE__ */ new Date(filter.value.to + "T23:59:59.999")).getTime();
|
|
853
|
+
if (ts > toTs) {
|
|
854
|
+
pass = false;
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!pass) break;
|
|
862
|
+
}
|
|
863
|
+
if (pass) indices.push(r);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (sort) {
|
|
867
|
+
const { columnIndex, direction } = sort;
|
|
868
|
+
const dir = direction === "asc" ? 1 : -1;
|
|
869
|
+
indices.sort((a, b) => {
|
|
870
|
+
const av = values[a][columnIndex];
|
|
871
|
+
const bv = values[b][columnIndex];
|
|
872
|
+
if (av == null && bv == null) return 0;
|
|
873
|
+
if (av == null) return -1 * dir;
|
|
874
|
+
if (bv == null) return 1 * dir;
|
|
875
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
876
|
+
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
877
|
+
}
|
|
878
|
+
const sa = String(av).toLowerCase();
|
|
879
|
+
const sb = String(bv).toLowerCase();
|
|
880
|
+
return sa === sb ? 0 : sa > sb ? dir : -dir;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const response = {
|
|
884
|
+
type: "sort-filter-result",
|
|
885
|
+
requestId,
|
|
886
|
+
indices
|
|
887
|
+
};
|
|
888
|
+
ctx.postMessage(response);
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
var workerInstance = null;
|
|
892
|
+
var requestCounter = 0;
|
|
893
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
894
|
+
function createSortFilterWorker() {
|
|
895
|
+
if (workerInstance) return workerInstance;
|
|
896
|
+
if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const fnStr = workerBody.toString();
|
|
901
|
+
const blob = new Blob(
|
|
902
|
+
[`(${fnStr})()`],
|
|
903
|
+
{ type: "application/javascript" }
|
|
904
|
+
);
|
|
905
|
+
const url = URL.createObjectURL(blob);
|
|
906
|
+
workerInstance = new Worker(url);
|
|
907
|
+
URL.revokeObjectURL(url);
|
|
908
|
+
workerInstance.onmessage = (e) => {
|
|
909
|
+
const { requestId, indices } = e.data;
|
|
910
|
+
const pending = pendingRequests.get(requestId);
|
|
911
|
+
if (pending) {
|
|
912
|
+
pendingRequests.delete(requestId);
|
|
913
|
+
pending.resolve(indices);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
workerInstance.onerror = (err) => {
|
|
917
|
+
for (const [id, pending] of pendingRequests) {
|
|
918
|
+
pending.reject(new Error(err.message || "Worker error"));
|
|
919
|
+
pendingRequests.delete(id);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
return workerInstance;
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function extractValueMatrix(data, columns) {
|
|
928
|
+
const matrix = new Array(data.length);
|
|
929
|
+
for (let r = 0; r < data.length; r++) {
|
|
930
|
+
const row = new Array(columns.length);
|
|
931
|
+
for (let c = 0; c < columns.length; c++) {
|
|
932
|
+
const val = getCellValue(data[r], columns[c]);
|
|
933
|
+
if (val == null) {
|
|
934
|
+
row[c] = null;
|
|
935
|
+
} else if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
|
|
936
|
+
row[c] = val;
|
|
937
|
+
} else {
|
|
938
|
+
row[c] = String(val);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
matrix[r] = row;
|
|
942
|
+
}
|
|
943
|
+
return matrix;
|
|
944
|
+
}
|
|
945
|
+
function processClientSideDataAsync(data, columns, filters, sortBy, sortDirection) {
|
|
946
|
+
if (sortBy) {
|
|
947
|
+
const sortCol = columns.find((c) => c.columnId === sortBy);
|
|
948
|
+
if (sortCol?.compare) {
|
|
949
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const worker = createSortFilterWorker();
|
|
953
|
+
if (!worker) {
|
|
954
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
955
|
+
}
|
|
956
|
+
const columnIndexMap = /* @__PURE__ */ new Map();
|
|
957
|
+
for (let i = 0; i < columns.length; i++) {
|
|
958
|
+
columnIndexMap.set(columns[i].columnId, i);
|
|
959
|
+
}
|
|
960
|
+
const values = extractValueMatrix(data, columns);
|
|
961
|
+
const columnMeta = columns.map((col, idx) => ({
|
|
962
|
+
type: col.type ?? "text",
|
|
963
|
+
index: idx
|
|
964
|
+
}));
|
|
965
|
+
const workerFilters = {};
|
|
966
|
+
for (const col of columns) {
|
|
967
|
+
const filterKey = getFilterField(col);
|
|
968
|
+
const val = filters[filterKey];
|
|
969
|
+
if (!val) continue;
|
|
970
|
+
const colIdx = columnIndexMap.get(col.columnId);
|
|
971
|
+
if (colIdx === void 0) continue;
|
|
972
|
+
switch (val.type) {
|
|
973
|
+
case "text":
|
|
974
|
+
workerFilters[colIdx] = { type: "text", value: val.value };
|
|
975
|
+
break;
|
|
976
|
+
case "multiSelect":
|
|
977
|
+
workerFilters[colIdx] = { type: "multiSelect", value: val.value };
|
|
978
|
+
break;
|
|
979
|
+
case "date":
|
|
980
|
+
workerFilters[colIdx] = { type: "date", value: { from: val.value.from, to: val.value.to } };
|
|
981
|
+
break;
|
|
982
|
+
// 'people' filter has a UserLike object — fall back to sync
|
|
983
|
+
case "people":
|
|
984
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
let sort;
|
|
988
|
+
if (sortBy) {
|
|
989
|
+
const sortColIdx = columnIndexMap.get(sortBy);
|
|
990
|
+
if (sortColIdx !== void 0) {
|
|
991
|
+
sort = { columnIndex: sortColIdx, direction: sortDirection ?? "asc" };
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const requestId = ++requestCounter;
|
|
995
|
+
return new Promise((resolve, reject) => {
|
|
996
|
+
pendingRequests.set(requestId, {
|
|
997
|
+
resolve: (indices) => {
|
|
998
|
+
const result = new Array(indices.length);
|
|
999
|
+
for (let i = 0; i < indices.length; i++) {
|
|
1000
|
+
result[i] = data[indices[i]];
|
|
1001
|
+
}
|
|
1002
|
+
resolve(result);
|
|
1003
|
+
},
|
|
1004
|
+
reject
|
|
1005
|
+
});
|
|
1006
|
+
const request = {
|
|
1007
|
+
type: "sort-filter",
|
|
1008
|
+
requestId,
|
|
1009
|
+
values,
|
|
1010
|
+
columnMeta,
|
|
1011
|
+
filters: workerFilters,
|
|
1012
|
+
sort
|
|
1013
|
+
};
|
|
1014
|
+
worker.postMessage(request);
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
var FormulaError = class {
|
|
1018
|
+
constructor(type, message) {
|
|
1019
|
+
this.type = type;
|
|
1020
|
+
this.message = message;
|
|
1021
|
+
}
|
|
1022
|
+
toString() {
|
|
1023
|
+
return this.type;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
718
1026
|
function getHeaderFilterConfig(col, input) {
|
|
719
1027
|
const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
|
|
720
1028
|
const filterType = filterable?.type ?? "none";
|
|
@@ -841,7 +1149,8 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
841
1149
|
const canEditAny = canEditInline || canEditPopup;
|
|
842
1150
|
const isEditing = input.editingCell?.rowId === rowId && input.editingCell?.columnId === col.columnId;
|
|
843
1151
|
const isActive = !input.isDragging && input.activeCell?.rowIndex === rowIndex && input.activeCell?.columnIndex === globalColIndex;
|
|
844
|
-
const
|
|
1152
|
+
const isSingleCellRange = input.selectionRange != null && input.selectionRange.startRow === input.selectionRange.endRow && input.selectionRange.startCol === input.selectionRange.endCol;
|
|
1153
|
+
const isInRange = input.selectionRange != null && !isSingleCellRange && isInSelectionRange(input.selectionRange, rowIndex, colIdx);
|
|
845
1154
|
const isInCutRange = input.cutRange != null && isInSelectionRange(input.cutRange, rowIndex, colIdx);
|
|
846
1155
|
const isInCopyRange = input.copyRange != null && isInSelectionRange(input.copyRange, rowIndex, colIdx);
|
|
847
1156
|
const isSelectionEndCell = !input.isDragging && input.copyRange == null && input.cutRange == null && input.selectionRange != null && rowIndex === input.selectionRange.endRow && colIdx === input.selectionRange.endCol;
|
|
@@ -883,6 +1192,9 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
883
1192
|
};
|
|
884
1193
|
}
|
|
885
1194
|
function resolveCellDisplayContent(col, item, displayValue) {
|
|
1195
|
+
if (displayValue instanceof FormulaError) {
|
|
1196
|
+
return displayValue.toString();
|
|
1197
|
+
}
|
|
886
1198
|
const c = col;
|
|
887
1199
|
if (c.renderCell && typeof c.renderCell === "function") {
|
|
888
1200
|
return c.renderCell(item);
|
|
@@ -898,10 +1210,14 @@ function resolveCellDisplayContent(col, item, displayValue) {
|
|
|
898
1210
|
}
|
|
899
1211
|
return String(displayValue);
|
|
900
1212
|
}
|
|
901
|
-
function resolveCellStyle(col, item) {
|
|
1213
|
+
function resolveCellStyle(col, item, displayValue) {
|
|
902
1214
|
const c = col;
|
|
903
|
-
|
|
904
|
-
|
|
1215
|
+
const isError = displayValue instanceof FormulaError;
|
|
1216
|
+
const base = c.cellStyle ? typeof c.cellStyle === "function" ? c.cellStyle(item) : c.cellStyle : void 0;
|
|
1217
|
+
if (isError) {
|
|
1218
|
+
return { ...base, color: "var(--ogrid-formula-error-color, #d32f2f)" };
|
|
1219
|
+
}
|
|
1220
|
+
return base;
|
|
905
1221
|
}
|
|
906
1222
|
function buildInlineEditorProps(item, col, descriptor, callbacks) {
|
|
907
1223
|
return {
|
|
@@ -992,6 +1308,7 @@ var AUTOSIZE_MAX_PX = 520;
|
|
|
992
1308
|
function measureHeaderWidth(th) {
|
|
993
1309
|
const cs = getComputedStyle(th);
|
|
994
1310
|
const thPadding = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
|
|
1311
|
+
const thBorders = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
|
|
995
1312
|
let resizeHandleWidth = 0;
|
|
996
1313
|
for (let i = 0; i < th.children.length; i++) {
|
|
997
1314
|
const child = th.children[i];
|
|
@@ -1014,12 +1331,14 @@ function measureHeaderWidth(th) {
|
|
|
1014
1331
|
overflow: child.style.overflow,
|
|
1015
1332
|
flexShrink: child.style.flexShrink,
|
|
1016
1333
|
width: child.style.width,
|
|
1017
|
-
minWidth: child.style.minWidth
|
|
1334
|
+
minWidth: child.style.minWidth,
|
|
1335
|
+
maxWidth: child.style.maxWidth
|
|
1018
1336
|
});
|
|
1019
1337
|
child.style.overflow = "visible";
|
|
1020
1338
|
child.style.flexShrink = "0";
|
|
1021
1339
|
child.style.width = "max-content";
|
|
1022
1340
|
child.style.minWidth = "max-content";
|
|
1341
|
+
child.style.maxWidth = "none";
|
|
1023
1342
|
}
|
|
1024
1343
|
expandDescendants(child);
|
|
1025
1344
|
}
|
|
@@ -1037,8 +1356,9 @@ function measureHeaderWidth(th) {
|
|
|
1037
1356
|
m.el.style.flexShrink = m.flexShrink;
|
|
1038
1357
|
m.el.style.width = m.width;
|
|
1039
1358
|
m.el.style.minWidth = m.minWidth;
|
|
1359
|
+
m.el.style.maxWidth = m.maxWidth;
|
|
1040
1360
|
}
|
|
1041
|
-
return expandedWidth + resizeHandleWidth + thPadding;
|
|
1361
|
+
return expandedWidth + resizeHandleWidth + thPadding + thBorders;
|
|
1042
1362
|
}
|
|
1043
1363
|
function measureColumnContentWidth(columnId, minWidth, container) {
|
|
1044
1364
|
const minW = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
@@ -1249,7 +1569,7 @@ function formatCellValueForTsv(raw, formatted) {
|
|
|
1249
1569
|
return "[Object]";
|
|
1250
1570
|
}
|
|
1251
1571
|
}
|
|
1252
|
-
function formatSelectionAsTsv(items, visibleCols, range) {
|
|
1572
|
+
function formatSelectionAsTsv(items, visibleCols, range, formulaOptions) {
|
|
1253
1573
|
const norm = normalizeSelectionRange(range);
|
|
1254
1574
|
const rows = [];
|
|
1255
1575
|
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
@@ -1258,6 +1578,16 @@ function formatSelectionAsTsv(items, visibleCols, range) {
|
|
|
1258
1578
|
if (r >= items.length || c >= visibleCols.length) break;
|
|
1259
1579
|
const item = items[r];
|
|
1260
1580
|
const col = visibleCols[c];
|
|
1581
|
+
if (formulaOptions?.hasFormula && formulaOptions?.getFormula) {
|
|
1582
|
+
const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
|
|
1583
|
+
if (flatColIndex >= 0 && formulaOptions.hasFormula(flatColIndex, r)) {
|
|
1584
|
+
const formulaStr = formulaOptions.getFormula(flatColIndex, r);
|
|
1585
|
+
if (formulaStr) {
|
|
1586
|
+
cells.push(formulaStr);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1261
1591
|
const raw = getCellValue(item, col);
|
|
1262
1592
|
const clipboard = col.clipboardFormatter ? col.clipboardFormatter(raw, item) : null;
|
|
1263
1593
|
const formatted = clipboard ?? (col.valueFormatter ? col.valueFormatter(raw, item) : raw);
|
|
@@ -1272,7 +1602,7 @@ function parseTsvClipboard(text) {
|
|
|
1272
1602
|
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
1273
1603
|
return lines.map((line) => line.split(" "));
|
|
1274
1604
|
}
|
|
1275
|
-
function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols) {
|
|
1605
|
+
function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions) {
|
|
1276
1606
|
const events = [];
|
|
1277
1607
|
for (let r = 0; r < parsedRows.length; r++) {
|
|
1278
1608
|
const cells = parsedRows[r];
|
|
@@ -1283,9 +1613,16 @@ function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols)
|
|
|
1283
1613
|
const item = items[targetRow];
|
|
1284
1614
|
const col = visibleCols[targetCol];
|
|
1285
1615
|
if (!isColumnEditable(col, item)) continue;
|
|
1286
|
-
const
|
|
1616
|
+
const cellText = cells[c] ?? "";
|
|
1617
|
+
if (cellText.startsWith("=") && formulaOptions?.setFormula) {
|
|
1618
|
+
const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
|
|
1619
|
+
if (flatColIndex >= 0) {
|
|
1620
|
+
formulaOptions.setFormula(flatColIndex, targetRow, cellText);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1287
1624
|
const oldValue = getCellValue(item, col);
|
|
1288
|
-
const result = parseValue(
|
|
1625
|
+
const result = parseValue(cellText, oldValue, item, col);
|
|
1289
1626
|
if (!result.valid) continue;
|
|
1290
1627
|
events.push({
|
|
1291
1628
|
item,
|
|
@@ -1320,12 +1657,101 @@ function applyCutClear(cutRange, items, visibleCols) {
|
|
|
1320
1657
|
}
|
|
1321
1658
|
return events;
|
|
1322
1659
|
}
|
|
1323
|
-
function
|
|
1660
|
+
function indexToColumnLetter(index) {
|
|
1661
|
+
let result = "";
|
|
1662
|
+
let n = index;
|
|
1663
|
+
while (n >= 0) {
|
|
1664
|
+
result = String.fromCharCode(n % 26 + 65) + result;
|
|
1665
|
+
n = Math.floor(n / 26) - 1;
|
|
1666
|
+
}
|
|
1667
|
+
return result;
|
|
1668
|
+
}
|
|
1669
|
+
function formatCellReference(colIndex, rowNumber) {
|
|
1670
|
+
return `${indexToColumnLetter(colIndex)}${rowNumber}`;
|
|
1671
|
+
}
|
|
1672
|
+
function columnLetterToIndex(letters) {
|
|
1673
|
+
let result = 0;
|
|
1674
|
+
const upper = letters.toUpperCase();
|
|
1675
|
+
for (let i = 0; i < upper.length; i++) {
|
|
1676
|
+
result = result * 26 + (upper.charCodeAt(i) - 64);
|
|
1677
|
+
}
|
|
1678
|
+
return result - 1;
|
|
1679
|
+
}
|
|
1680
|
+
var CELL_REF_RE = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
|
|
1681
|
+
var ADJUST_REF_RE = /(?:'[^']*'!|[A-Za-z_]\w*!)?(\$?)([A-Z]+)(\$?)(\d+)/g;
|
|
1682
|
+
function parseCellRef(ref) {
|
|
1683
|
+
const m = ref.match(CELL_REF_RE);
|
|
1684
|
+
if (!m) return null;
|
|
1685
|
+
const absCol = m[1] === "$";
|
|
1686
|
+
const colLetters = m[2];
|
|
1687
|
+
const absRow = m[3] === "$";
|
|
1688
|
+
const rowNum = parseInt(m[4], 10);
|
|
1689
|
+
if (rowNum < 1) return null;
|
|
1690
|
+
return {
|
|
1691
|
+
col: columnLetterToIndex(colLetters),
|
|
1692
|
+
row: rowNum - 1,
|
|
1693
|
+
// 0-based internally
|
|
1694
|
+
absCol,
|
|
1695
|
+
absRow
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function parseRange(rangeStr) {
|
|
1699
|
+
const parts = rangeStr.split(":");
|
|
1700
|
+
if (parts.length !== 2) return null;
|
|
1701
|
+
const start = parseCellRef(parts[0]);
|
|
1702
|
+
const end = parseCellRef(parts[1]);
|
|
1703
|
+
if (!start || !end) return null;
|
|
1704
|
+
return { start, end };
|
|
1705
|
+
}
|
|
1706
|
+
function adjustFormulaReferences(formula, colDelta, rowDelta) {
|
|
1707
|
+
ADJUST_REF_RE.lastIndex = 0;
|
|
1708
|
+
return formula.replace(ADJUST_REF_RE, (match, colAbs, colLetters, rowAbs, rowDigits) => {
|
|
1709
|
+
const cellRefStart = match.indexOf(colAbs + colLetters);
|
|
1710
|
+
const sheetPrefix = cellRefStart > 0 ? match.substring(0, cellRefStart) : "";
|
|
1711
|
+
let newCol = colLetters;
|
|
1712
|
+
let newRow = rowDigits;
|
|
1713
|
+
if (colAbs !== "$") {
|
|
1714
|
+
const colIdx = columnLetterToIndex(colLetters) + colDelta;
|
|
1715
|
+
if (colIdx < 0) return "#REF!";
|
|
1716
|
+
newCol = indexToColumnLetter(colIdx);
|
|
1717
|
+
}
|
|
1718
|
+
if (rowAbs !== "$") {
|
|
1719
|
+
const rowNum = parseInt(rowDigits, 10) + rowDelta;
|
|
1720
|
+
if (rowNum < 1) return "#REF!";
|
|
1721
|
+
newRow = String(rowNum);
|
|
1722
|
+
}
|
|
1723
|
+
return `${sheetPrefix}${colAbs}${newCol}${rowAbs}${newRow}`;
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
function toCellKey(col, row, sheet) {
|
|
1727
|
+
if (sheet) return `${sheet}:${col},${row}`;
|
|
1728
|
+
return `${col},${row}`;
|
|
1729
|
+
}
|
|
1730
|
+
function fromCellKey(key) {
|
|
1731
|
+
const colonIdx = key.indexOf(":");
|
|
1732
|
+
if (colonIdx >= 0 && isNaN(parseInt(key.substring(0, colonIdx), 10))) {
|
|
1733
|
+
const sheet = key.substring(0, colonIdx);
|
|
1734
|
+
const rest = key.substring(colonIdx + 1);
|
|
1735
|
+
const commaIdx = rest.indexOf(",");
|
|
1736
|
+
return {
|
|
1737
|
+
col: parseInt(rest.substring(0, commaIdx), 10),
|
|
1738
|
+
row: parseInt(rest.substring(commaIdx + 1), 10),
|
|
1739
|
+
sheet
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
const i = key.indexOf(",");
|
|
1743
|
+
return {
|
|
1744
|
+
col: parseInt(key.substring(0, i), 10),
|
|
1745
|
+
row: parseInt(key.substring(i + 1), 10)
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function applyFillValues(range, sourceRow, sourceCol, items, visibleCols, formulaOptions) {
|
|
1324
1749
|
const events = [];
|
|
1325
1750
|
const startItem = items[range.startRow];
|
|
1326
1751
|
const startColDef = visibleCols[range.startCol];
|
|
1327
1752
|
if (!startItem || !startColDef) return events;
|
|
1328
1753
|
const startValue = getCellValue(startItem, startColDef);
|
|
1754
|
+
const srcFlatColIndex = formulaOptions ? formulaOptions.flatColumns.findIndex((c) => c.columnId === startColDef.columnId) : -1;
|
|
1329
1755
|
for (let row = range.startRow; row <= range.endRow; row++) {
|
|
1330
1756
|
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
1331
1757
|
if (row === sourceRow && col === sourceCol) continue;
|
|
@@ -1333,6 +1759,19 @@ function applyFillValues(range, sourceRow, sourceCol, items, visibleCols) {
|
|
|
1333
1759
|
const item = items[row];
|
|
1334
1760
|
const colDef = visibleCols[col];
|
|
1335
1761
|
if (!isColumnEditable(colDef, item)) continue;
|
|
1762
|
+
if (formulaOptions && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.setFormula && srcFlatColIndex >= 0 && formulaOptions.hasFormula(srcFlatColIndex, sourceRow)) {
|
|
1763
|
+
const srcFormula = formulaOptions.getFormula(srcFlatColIndex, sourceRow);
|
|
1764
|
+
if (srcFormula) {
|
|
1765
|
+
const rowDelta = row - sourceRow;
|
|
1766
|
+
const colDelta = col - sourceCol;
|
|
1767
|
+
const adjusted = adjustFormulaReferences(srcFormula, colDelta, rowDelta);
|
|
1768
|
+
const targetFlatColIdx = formulaOptions.flatColumns.findIndex((c) => c.columnId === colDef.columnId);
|
|
1769
|
+
if (targetFlatColIdx >= 0) {
|
|
1770
|
+
formulaOptions.setFormula(targetFlatColIdx, row, adjusted);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1336
1775
|
const oldValue = getCellValue(item, colDef);
|
|
1337
1776
|
const result = parseValue(startValue, oldValue, item, colDef);
|
|
1338
1777
|
if (!result.valid) continue;
|
|
@@ -1379,134 +1818,3307 @@ var UndoRedoStack = class {
|
|
|
1379
1818
|
* If a batch is open, accumulates into the batch instead.
|
|
1380
1819
|
* Clears the redo stack on any new entry.
|
|
1381
1820
|
*/
|
|
1382
|
-
push(events) {
|
|
1383
|
-
if (events.length === 0) return;
|
|
1384
|
-
if (this.batch !== null) {
|
|
1385
|
-
this.batch.push(...events);
|
|
1386
|
-
} else {
|
|
1387
|
-
this.history.push(events);
|
|
1388
|
-
if (this.history.length > this.maxDepth) {
|
|
1389
|
-
this.history.splice(0, this.history.length - this.maxDepth);
|
|
1821
|
+
push(events) {
|
|
1822
|
+
if (events.length === 0) return;
|
|
1823
|
+
if (this.batch !== null) {
|
|
1824
|
+
this.batch.push(...events);
|
|
1825
|
+
} else {
|
|
1826
|
+
this.history.push(events);
|
|
1827
|
+
if (this.history.length > this.maxDepth) {
|
|
1828
|
+
this.history.splice(0, this.history.length - this.maxDepth);
|
|
1829
|
+
}
|
|
1830
|
+
this.redoStack.length = 0;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Record a single event as a step (shorthand for push([event])).
|
|
1835
|
+
* If a batch is open, accumulates into the batch instead.
|
|
1836
|
+
*/
|
|
1837
|
+
record(event) {
|
|
1838
|
+
this.push([event]);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Start a batch — subsequent record/push calls accumulate into one undo step.
|
|
1842
|
+
* Has no effect if a batch is already open.
|
|
1843
|
+
*/
|
|
1844
|
+
beginBatch() {
|
|
1845
|
+
if (this.batch === null) {
|
|
1846
|
+
this.batch = [];
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* End a batch — commits all accumulated events as one undo step.
|
|
1851
|
+
* Has no effect if no batch is open or if the batch is empty.
|
|
1852
|
+
*/
|
|
1853
|
+
endBatch() {
|
|
1854
|
+
const b = this.batch;
|
|
1855
|
+
this.batch = null;
|
|
1856
|
+
if (!b || b.length === 0) return;
|
|
1857
|
+
this.history.push(b);
|
|
1858
|
+
if (this.history.length > this.maxDepth) {
|
|
1859
|
+
this.history.splice(0, this.history.length - this.maxDepth);
|
|
1860
|
+
}
|
|
1861
|
+
this.redoStack.length = 0;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Pop the most recent history entry for undo.
|
|
1865
|
+
* Returns the batch of events (in original order) to be reversed by the caller,
|
|
1866
|
+
* or null if there is nothing to undo.
|
|
1867
|
+
*
|
|
1868
|
+
* The caller is responsible for applying the events in reverse order.
|
|
1869
|
+
*/
|
|
1870
|
+
undo() {
|
|
1871
|
+
const lastBatch = this.history.pop();
|
|
1872
|
+
if (!lastBatch) return null;
|
|
1873
|
+
this.redoStack.push(lastBatch);
|
|
1874
|
+
return lastBatch;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Pop the most recent redo entry.
|
|
1878
|
+
* Returns the batch of events (in original order) to be re-applied by the caller,
|
|
1879
|
+
* or null if there is nothing to redo.
|
|
1880
|
+
*/
|
|
1881
|
+
redo() {
|
|
1882
|
+
const nextBatch = this.redoStack.pop();
|
|
1883
|
+
if (!nextBatch) return null;
|
|
1884
|
+
this.history.push(nextBatch);
|
|
1885
|
+
return nextBatch;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Clear all history and redo state.
|
|
1889
|
+
* Does not affect any open batch — call endBatch() first if needed.
|
|
1890
|
+
*/
|
|
1891
|
+
clear() {
|
|
1892
|
+
this.history = [];
|
|
1893
|
+
this.redoStack = [];
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
function validateColumns(columns) {
|
|
1897
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
1898
|
+
console.warn("[OGrid] columns prop is empty or not an array");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
1902
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1903
|
+
for (const col of columns) {
|
|
1904
|
+
if (!col.columnId) {
|
|
1905
|
+
console.warn("[OGrid] Column missing columnId:", col);
|
|
1906
|
+
}
|
|
1907
|
+
if (ids.has(col.columnId)) {
|
|
1908
|
+
console.warn(`[OGrid] Duplicate columnId: "${col.columnId}"`);
|
|
1909
|
+
}
|
|
1910
|
+
ids.add(col.columnId);
|
|
1911
|
+
if (isDev && col.editable === true && col.cellEditor == null) {
|
|
1912
|
+
console.warn(
|
|
1913
|
+
`[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.`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function validateVirtualScrollConfig(config) {
|
|
1919
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
1920
|
+
if (config.enabled !== true) return;
|
|
1921
|
+
if (!config.rowHeight || config.rowHeight <= 0) {
|
|
1922
|
+
console.warn(
|
|
1923
|
+
"[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."
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function validateRowIds(items, getRowId) {
|
|
1928
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
1929
|
+
if (!getRowId) return;
|
|
1930
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1931
|
+
const limit = Math.min(items.length, 100);
|
|
1932
|
+
for (let i = 0; i < limit; i++) {
|
|
1933
|
+
const id = getRowId(items[i]);
|
|
1934
|
+
if (id == null) {
|
|
1935
|
+
console.warn(`[OGrid] getRowId returned null/undefined for row ${i}`);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (ids.has(id)) {
|
|
1939
|
+
console.warn(
|
|
1940
|
+
`[OGrid] Duplicate row ID "${id}" at index ${i}. getRowId must return unique values.`
|
|
1941
|
+
);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
ids.add(id);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1948
|
+
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
1949
|
+
var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
|
|
1950
|
+
var SINGLE_CHAR_OPERATORS = {
|
|
1951
|
+
"+": "PLUS",
|
|
1952
|
+
"-": "MINUS",
|
|
1953
|
+
"*": "MULTIPLY",
|
|
1954
|
+
"/": "DIVIDE",
|
|
1955
|
+
"^": "POWER",
|
|
1956
|
+
"%": "PERCENT",
|
|
1957
|
+
"&": "AMPERSAND",
|
|
1958
|
+
"=": "EQ"
|
|
1959
|
+
};
|
|
1960
|
+
var DELIMITERS = {
|
|
1961
|
+
"(": "LPAREN",
|
|
1962
|
+
")": "RPAREN",
|
|
1963
|
+
",": "COMMA",
|
|
1964
|
+
":": "COLON"
|
|
1965
|
+
};
|
|
1966
|
+
function tokenize(input) {
|
|
1967
|
+
const tokens = [];
|
|
1968
|
+
let pos = 0;
|
|
1969
|
+
while (pos < input.length) {
|
|
1970
|
+
const ch = input[pos];
|
|
1971
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
1972
|
+
pos++;
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (ch >= "0" && ch <= "9" || ch === "." && pos + 1 < input.length && input[pos + 1] >= "0" && input[pos + 1] <= "9") {
|
|
1976
|
+
const start = pos;
|
|
1977
|
+
while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
|
|
1978
|
+
pos++;
|
|
1979
|
+
}
|
|
1980
|
+
if (pos < input.length && input[pos] === ".") {
|
|
1981
|
+
pos++;
|
|
1982
|
+
while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
|
|
1983
|
+
pos++;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
tokens.push({ type: "NUMBER", value: input.slice(start, pos), position: start });
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (ch === "'") {
|
|
1990
|
+
const start = pos;
|
|
1991
|
+
pos++;
|
|
1992
|
+
const nameStart = pos;
|
|
1993
|
+
while (pos < input.length && input[pos] !== "'") {
|
|
1994
|
+
pos++;
|
|
1995
|
+
}
|
|
1996
|
+
const sheetName = input.slice(nameStart, pos);
|
|
1997
|
+
if (pos < input.length && input[pos] === "'") {
|
|
1998
|
+
pos++;
|
|
1999
|
+
if (pos < input.length && input[pos] === "!") {
|
|
2000
|
+
pos++;
|
|
2001
|
+
tokens.push({ type: "SHEET_REF", value: sheetName, position: start });
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
throw new FormulaError("#ERROR!", `Invalid sheet reference at position ${start}`);
|
|
2006
|
+
}
|
|
2007
|
+
if (ch === '"') {
|
|
2008
|
+
const start = pos;
|
|
2009
|
+
pos++;
|
|
2010
|
+
const scanStart = pos;
|
|
2011
|
+
let hasEscapes = false;
|
|
2012
|
+
while (pos < input.length) {
|
|
2013
|
+
if (input[pos] === '"') {
|
|
2014
|
+
if (pos + 1 < input.length && input[pos + 1] === '"') {
|
|
2015
|
+
hasEscapes = true;
|
|
2016
|
+
pos += 2;
|
|
2017
|
+
} else {
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
pos++;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
let value;
|
|
2025
|
+
if (!hasEscapes) {
|
|
2026
|
+
value = input.slice(scanStart, pos);
|
|
2027
|
+
} else {
|
|
2028
|
+
value = input.slice(scanStart, pos).replace(/""/g, '"');
|
|
2029
|
+
}
|
|
2030
|
+
if (pos < input.length) pos++;
|
|
2031
|
+
tokens.push({ type: "STRING", value, position: start });
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
if (ch === ">" && pos + 1 < input.length && input[pos + 1] === "=") {
|
|
2035
|
+
tokens.push({ type: "GTE", value: ">=", position: pos });
|
|
2036
|
+
pos += 2;
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (ch === "<" && pos + 1 < input.length && input[pos + 1] === "=") {
|
|
2040
|
+
tokens.push({ type: "LTE", value: "<=", position: pos });
|
|
2041
|
+
pos += 2;
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
if (ch === "<" && pos + 1 < input.length && input[pos + 1] === ">") {
|
|
2045
|
+
tokens.push({ type: "NEQ", value: "<>", position: pos });
|
|
2046
|
+
pos += 2;
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
if (ch === ">" || ch === "<") {
|
|
2050
|
+
const type = ch === ">" ? "GT" : "LT";
|
|
2051
|
+
tokens.push({ type, value: ch, position: pos });
|
|
2052
|
+
pos++;
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
if (SINGLE_CHAR_OPERATORS[ch]) {
|
|
2056
|
+
tokens.push({ type: SINGLE_CHAR_OPERATORS[ch], value: ch, position: pos });
|
|
2057
|
+
pos++;
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
if (DELIMITERS[ch]) {
|
|
2061
|
+
tokens.push({ type: DELIMITERS[ch], value: ch, position: pos });
|
|
2062
|
+
pos++;
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (ch === "$" || ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z") {
|
|
2066
|
+
const start = pos;
|
|
2067
|
+
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] === "_")) {
|
|
2068
|
+
pos++;
|
|
2069
|
+
}
|
|
2070
|
+
const word = input.slice(start, pos);
|
|
2071
|
+
if (pos < input.length && input[pos] === "!") {
|
|
2072
|
+
pos++;
|
|
2073
|
+
tokens.push({ type: "SHEET_REF", value: word, position: start });
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
if (pos < input.length && input[pos] === "(") {
|
|
2077
|
+
tokens.push({ type: "FUNCTION", value: word, position: start });
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
const upper = word.toUpperCase();
|
|
2081
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
2082
|
+
tokens.push({ type: "BOOLEAN", value: upper, position: start });
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (CELL_REF_PATTERN.test(word)) {
|
|
2086
|
+
tokens.push({ type: "CELL_REF", value: word, position: start });
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
tokens.push({ type: "IDENTIFIER", value: word, position: start });
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
throw new FormulaError("#ERROR!", `Unexpected character: ${ch}`);
|
|
2093
|
+
}
|
|
2094
|
+
tokens.push({ type: "EOF", value: "", position: pos });
|
|
2095
|
+
return tokens;
|
|
2096
|
+
}
|
|
2097
|
+
function parse(tokens, namedRanges) {
|
|
2098
|
+
let pos = 0;
|
|
2099
|
+
function peek() {
|
|
2100
|
+
return tokens[pos];
|
|
2101
|
+
}
|
|
2102
|
+
function advance() {
|
|
2103
|
+
const token = tokens[pos];
|
|
2104
|
+
pos++;
|
|
2105
|
+
return token;
|
|
2106
|
+
}
|
|
2107
|
+
function expect(type) {
|
|
2108
|
+
const token = peek();
|
|
2109
|
+
if (token && token.type === type) {
|
|
2110
|
+
return advance();
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
function errorNode(message) {
|
|
2115
|
+
return { kind: "error", error: new FormulaError("#ERROR!", message) };
|
|
2116
|
+
}
|
|
2117
|
+
function expression() {
|
|
2118
|
+
return comparison();
|
|
2119
|
+
}
|
|
2120
|
+
function comparison() {
|
|
2121
|
+
let left = concat();
|
|
2122
|
+
while (peek()) {
|
|
2123
|
+
const t = peek();
|
|
2124
|
+
let op = null;
|
|
2125
|
+
if (t.type === "GT") op = ">";
|
|
2126
|
+
else if (t.type === "LT") op = "<";
|
|
2127
|
+
else if (t.type === "GTE") op = ">=";
|
|
2128
|
+
else if (t.type === "LTE") op = "<=";
|
|
2129
|
+
else if (t.type === "EQ") op = "=";
|
|
2130
|
+
else if (t.type === "NEQ") op = "<>";
|
|
2131
|
+
else break;
|
|
2132
|
+
advance();
|
|
2133
|
+
const right = concat();
|
|
2134
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2135
|
+
}
|
|
2136
|
+
return left;
|
|
2137
|
+
}
|
|
2138
|
+
function concat() {
|
|
2139
|
+
let left = addition();
|
|
2140
|
+
while (peek() && peek().type === "AMPERSAND") {
|
|
2141
|
+
advance();
|
|
2142
|
+
const right = addition();
|
|
2143
|
+
left = { kind: "binaryOp", op: "&", left, right };
|
|
2144
|
+
}
|
|
2145
|
+
return left;
|
|
2146
|
+
}
|
|
2147
|
+
function addition() {
|
|
2148
|
+
let left = multiplication();
|
|
2149
|
+
while (peek()) {
|
|
2150
|
+
const t = peek();
|
|
2151
|
+
let op = null;
|
|
2152
|
+
if (t.type === "PLUS") op = "+";
|
|
2153
|
+
else if (t.type === "MINUS") op = "-";
|
|
2154
|
+
else break;
|
|
2155
|
+
advance();
|
|
2156
|
+
const right = multiplication();
|
|
2157
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2158
|
+
}
|
|
2159
|
+
return left;
|
|
2160
|
+
}
|
|
2161
|
+
function multiplication() {
|
|
2162
|
+
let left = power();
|
|
2163
|
+
while (peek()) {
|
|
2164
|
+
const t = peek();
|
|
2165
|
+
let op = null;
|
|
2166
|
+
if (t.type === "MULTIPLY") op = "*";
|
|
2167
|
+
else if (t.type === "DIVIDE") op = "/";
|
|
2168
|
+
else break;
|
|
2169
|
+
advance();
|
|
2170
|
+
const right = power();
|
|
2171
|
+
left = { kind: "binaryOp", op, left, right };
|
|
2172
|
+
}
|
|
2173
|
+
return left;
|
|
2174
|
+
}
|
|
2175
|
+
function power() {
|
|
2176
|
+
let left = unary();
|
|
2177
|
+
while (peek() && peek().type === "POWER") {
|
|
2178
|
+
advance();
|
|
2179
|
+
const right = unary();
|
|
2180
|
+
left = { kind: "binaryOp", op: "^", left, right };
|
|
2181
|
+
}
|
|
2182
|
+
return left;
|
|
2183
|
+
}
|
|
2184
|
+
function unary() {
|
|
2185
|
+
const t = peek();
|
|
2186
|
+
if (t && (t.type === "MINUS" || t.type === "PLUS")) {
|
|
2187
|
+
const op = t.type === "MINUS" ? "-" : "+";
|
|
2188
|
+
advance();
|
|
2189
|
+
const operand = unary();
|
|
2190
|
+
return { kind: "unaryOp", op, operand };
|
|
2191
|
+
}
|
|
2192
|
+
return postfix();
|
|
2193
|
+
}
|
|
2194
|
+
function postfix() {
|
|
2195
|
+
let node = primary();
|
|
2196
|
+
if (peek() && peek().type === "PERCENT") {
|
|
2197
|
+
advance();
|
|
2198
|
+
node = { kind: "binaryOp", op: "%", left: node, right: { kind: "number", value: 100 } };
|
|
2199
|
+
}
|
|
2200
|
+
return node;
|
|
2201
|
+
}
|
|
2202
|
+
function primary() {
|
|
2203
|
+
const t = peek();
|
|
2204
|
+
if (!t || t.type === "EOF") {
|
|
2205
|
+
return errorNode("Unexpected end of expression");
|
|
2206
|
+
}
|
|
2207
|
+
if (t.type === "NUMBER") {
|
|
2208
|
+
advance();
|
|
2209
|
+
return { kind: "number", value: parseFloat(t.value) };
|
|
2210
|
+
}
|
|
2211
|
+
if (t.type === "STRING") {
|
|
2212
|
+
advance();
|
|
2213
|
+
return { kind: "string", value: t.value };
|
|
2214
|
+
}
|
|
2215
|
+
if (t.type === "BOOLEAN") {
|
|
2216
|
+
advance();
|
|
2217
|
+
return { kind: "boolean", value: t.value.toUpperCase() === "TRUE" };
|
|
2218
|
+
}
|
|
2219
|
+
if (t.type === "CELL_REF") {
|
|
2220
|
+
return cellRefOrRange();
|
|
2221
|
+
}
|
|
2222
|
+
if (t.type === "FUNCTION") {
|
|
2223
|
+
return functionCall();
|
|
2224
|
+
}
|
|
2225
|
+
if (t.type === "IDENTIFIER") {
|
|
2226
|
+
return namedRangeRef();
|
|
2227
|
+
}
|
|
2228
|
+
if (t.type === "SHEET_REF") {
|
|
2229
|
+
return sheetRef();
|
|
2230
|
+
}
|
|
2231
|
+
if (t.type === "LPAREN") {
|
|
2232
|
+
advance();
|
|
2233
|
+
const node = expression();
|
|
2234
|
+
if (!expect("RPAREN")) {
|
|
2235
|
+
return errorNode("Expected closing parenthesis");
|
|
2236
|
+
}
|
|
2237
|
+
return node;
|
|
2238
|
+
}
|
|
2239
|
+
advance();
|
|
2240
|
+
return errorNode(`Unexpected token: ${t.value}`);
|
|
2241
|
+
}
|
|
2242
|
+
function cellRefOrRange() {
|
|
2243
|
+
const refToken = advance();
|
|
2244
|
+
const address = parseCellRef(refToken.value);
|
|
2245
|
+
if (!address) {
|
|
2246
|
+
return errorNode(`Invalid cell reference: ${refToken.value}`);
|
|
2247
|
+
}
|
|
2248
|
+
if (peek() && peek().type === "COLON") {
|
|
2249
|
+
advance();
|
|
2250
|
+
const endToken = expect("CELL_REF");
|
|
2251
|
+
if (!endToken) {
|
|
2252
|
+
return errorNode('Expected cell reference after ":"');
|
|
2253
|
+
}
|
|
2254
|
+
const endAddress = parseCellRef(endToken.value);
|
|
2255
|
+
if (!endAddress) {
|
|
2256
|
+
return errorNode(`Invalid cell reference: ${endToken.value}`);
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
kind: "range",
|
|
2260
|
+
start: address,
|
|
2261
|
+
end: endAddress,
|
|
2262
|
+
raw: `${refToken.value}:${endToken.value}`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
kind: "cellRef",
|
|
2267
|
+
address,
|
|
2268
|
+
raw: refToken.value
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function functionCall() {
|
|
2272
|
+
const nameToken = advance();
|
|
2273
|
+
const name = nameToken.value.toUpperCase();
|
|
2274
|
+
if (!expect("LPAREN")) {
|
|
2275
|
+
return errorNode(`Expected "(" after function name "${name}"`);
|
|
2276
|
+
}
|
|
2277
|
+
const args = [];
|
|
2278
|
+
if (peek() && peek().type !== "RPAREN" && peek().type !== "EOF") {
|
|
2279
|
+
args.push(expression());
|
|
2280
|
+
while (peek() && peek().type === "COMMA") {
|
|
2281
|
+
advance();
|
|
2282
|
+
args.push(expression());
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (!expect("RPAREN")) {
|
|
2286
|
+
return errorNode(`Expected ")" after function arguments for "${name}"`);
|
|
2287
|
+
}
|
|
2288
|
+
return { kind: "functionCall", name, args };
|
|
2289
|
+
}
|
|
2290
|
+
function namedRangeRef() {
|
|
2291
|
+
const nameToken = advance();
|
|
2292
|
+
const name = nameToken.value.toUpperCase();
|
|
2293
|
+
const ref = namedRanges?.get(name);
|
|
2294
|
+
if (!ref) {
|
|
2295
|
+
return { kind: "error", error: new FormulaError("#NAME?", `Unknown name: ${nameToken.value}`) };
|
|
2296
|
+
}
|
|
2297
|
+
if (ref.includes(":")) {
|
|
2298
|
+
const rangeRef = parseRange(ref);
|
|
2299
|
+
if (rangeRef) {
|
|
2300
|
+
return { kind: "range", start: rangeRef.start, end: rangeRef.end, raw: ref };
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
const cellRef = parseCellRef(ref);
|
|
2304
|
+
if (cellRef) {
|
|
2305
|
+
return { kind: "cellRef", address: cellRef, raw: ref };
|
|
2306
|
+
}
|
|
2307
|
+
return { kind: "error", error: new FormulaError("#REF!", `Invalid named range reference: ${ref}`) };
|
|
2308
|
+
}
|
|
2309
|
+
function sheetRef() {
|
|
2310
|
+
const sheetToken = advance();
|
|
2311
|
+
const sheetName = sheetToken.value;
|
|
2312
|
+
const cellToken = expect("CELL_REF");
|
|
2313
|
+
if (!cellToken) {
|
|
2314
|
+
return errorNode(`Expected cell reference after sheet "${sheetName}!"`);
|
|
2315
|
+
}
|
|
2316
|
+
const address = parseCellRef(cellToken.value);
|
|
2317
|
+
if (!address) {
|
|
2318
|
+
return errorNode(`Invalid cell reference: ${cellToken.value}`);
|
|
2319
|
+
}
|
|
2320
|
+
address.sheet = sheetName;
|
|
2321
|
+
if (peek() && peek().type === "COLON") {
|
|
2322
|
+
advance();
|
|
2323
|
+
const endToken = expect("CELL_REF");
|
|
2324
|
+
if (!endToken) {
|
|
2325
|
+
return errorNode('Expected cell reference after ":"');
|
|
2326
|
+
}
|
|
2327
|
+
const endAddress = parseCellRef(endToken.value);
|
|
2328
|
+
if (!endAddress) {
|
|
2329
|
+
return errorNode(`Invalid cell reference: ${endToken.value}`);
|
|
2330
|
+
}
|
|
2331
|
+
endAddress.sheet = sheetName;
|
|
2332
|
+
return {
|
|
2333
|
+
kind: "range",
|
|
2334
|
+
start: address,
|
|
2335
|
+
end: endAddress,
|
|
2336
|
+
raw: `${sheetName}!${cellToken.value}:${endToken.value}`
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
kind: "cellRef",
|
|
2341
|
+
address,
|
|
2342
|
+
raw: `${sheetName}!${cellToken.value}`
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
const result = expression();
|
|
2346
|
+
if (peek() && peek().type !== "EOF") {
|
|
2347
|
+
return errorNode(`Unexpected token after expression: ${peek().value}`);
|
|
2348
|
+
}
|
|
2349
|
+
return result;
|
|
2350
|
+
}
|
|
2351
|
+
function toNumber(val) {
|
|
2352
|
+
if (val instanceof FormulaError) return val;
|
|
2353
|
+
if (val === null || val === void 0 || val === "") return 0;
|
|
2354
|
+
if (typeof val === "boolean") return val ? 1 : 0;
|
|
2355
|
+
if (typeof val === "number") return val;
|
|
2356
|
+
if (val instanceof Date) return val.getTime();
|
|
2357
|
+
const n = Number(val);
|
|
2358
|
+
if (isNaN(n)) return new FormulaError("#VALUE!", `Cannot convert "${val}" to number`);
|
|
2359
|
+
return n;
|
|
2360
|
+
}
|
|
2361
|
+
function toString(val) {
|
|
2362
|
+
if (val === null || val === void 0) return "";
|
|
2363
|
+
if (val instanceof FormulaError) return val.toString();
|
|
2364
|
+
if (val instanceof Date) return val.toLocaleDateString();
|
|
2365
|
+
return String(val);
|
|
2366
|
+
}
|
|
2367
|
+
function flattenArgs(args, context, evaluator) {
|
|
2368
|
+
const result = [];
|
|
2369
|
+
for (const arg of args) {
|
|
2370
|
+
if (arg.kind === "range") {
|
|
2371
|
+
const values = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
2372
|
+
for (const row of values) {
|
|
2373
|
+
for (const val of row) {
|
|
2374
|
+
result.push(val);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
} else {
|
|
2378
|
+
result.push(evaluator.evaluate(arg, context));
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return result;
|
|
2382
|
+
}
|
|
2383
|
+
var FormulaEvaluator = class {
|
|
2384
|
+
constructor(builtInFunctions) {
|
|
2385
|
+
this.functions = new Map(builtInFunctions);
|
|
2386
|
+
}
|
|
2387
|
+
registerFunction(name, fn) {
|
|
2388
|
+
this.functions.set(name.toUpperCase(), fn);
|
|
2389
|
+
}
|
|
2390
|
+
evaluate(node, context) {
|
|
2391
|
+
switch (node.kind) {
|
|
2392
|
+
case "number":
|
|
2393
|
+
return node.value;
|
|
2394
|
+
case "string":
|
|
2395
|
+
return node.value;
|
|
2396
|
+
case "boolean":
|
|
2397
|
+
return node.value;
|
|
2398
|
+
case "error":
|
|
2399
|
+
return node.error;
|
|
2400
|
+
case "cellRef": {
|
|
2401
|
+
const val = context.getCellValue(node.address);
|
|
2402
|
+
return val;
|
|
2403
|
+
}
|
|
2404
|
+
case "range":
|
|
2405
|
+
return context.getCellValue(node.start);
|
|
2406
|
+
case "functionCall":
|
|
2407
|
+
return this.evaluateFunction(node.name, node.args, context);
|
|
2408
|
+
case "binaryOp":
|
|
2409
|
+
return this.evaluateBinaryOp(node.op, node.left, node.right, context);
|
|
2410
|
+
case "unaryOp":
|
|
2411
|
+
return this.evaluateUnaryOp(node.op, node.operand, context);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
evaluateFunction(name, args, context) {
|
|
2415
|
+
const fn = this.functions.get(name);
|
|
2416
|
+
if (!fn) {
|
|
2417
|
+
return new FormulaError("#NAME?", `Unknown function: ${name}`);
|
|
2418
|
+
}
|
|
2419
|
+
if (args.length < fn.minArgs) {
|
|
2420
|
+
return new FormulaError("#ERROR!", `${name} requires at least ${fn.minArgs} argument(s)`);
|
|
2421
|
+
}
|
|
2422
|
+
if (fn.maxArgs >= 0 && args.length > fn.maxArgs) {
|
|
2423
|
+
return new FormulaError("#ERROR!", `${name} accepts at most ${fn.maxArgs} argument(s)`);
|
|
2424
|
+
}
|
|
2425
|
+
return fn.evaluate(args, context, this);
|
|
2426
|
+
}
|
|
2427
|
+
evaluateBinaryOp(op, left, right, context) {
|
|
2428
|
+
if (op === "&") {
|
|
2429
|
+
const l = this.evaluate(left, context);
|
|
2430
|
+
if (l instanceof FormulaError) return l;
|
|
2431
|
+
const r = this.evaluate(right, context);
|
|
2432
|
+
if (r instanceof FormulaError) return r;
|
|
2433
|
+
return toString(l) + toString(r);
|
|
2434
|
+
}
|
|
2435
|
+
if (op === ">" || op === "<" || op === ">=" || op === "<=" || op === "=" || op === "<>") {
|
|
2436
|
+
const l = this.evaluate(left, context);
|
|
2437
|
+
if (l instanceof FormulaError) return l;
|
|
2438
|
+
const r = this.evaluate(right, context);
|
|
2439
|
+
if (r instanceof FormulaError) return r;
|
|
2440
|
+
return this.compare(op, l, r);
|
|
2441
|
+
}
|
|
2442
|
+
const lVal = this.evaluate(left, context);
|
|
2443
|
+
if (lVal instanceof FormulaError) return lVal;
|
|
2444
|
+
const rVal = this.evaluate(right, context);
|
|
2445
|
+
if (rVal instanceof FormulaError) return rVal;
|
|
2446
|
+
const lNum = toNumber(lVal);
|
|
2447
|
+
if (lNum instanceof FormulaError) return lNum;
|
|
2448
|
+
const rNum = toNumber(rVal);
|
|
2449
|
+
if (rNum instanceof FormulaError) return rNum;
|
|
2450
|
+
switch (op) {
|
|
2451
|
+
case "+":
|
|
2452
|
+
return lNum + rNum;
|
|
2453
|
+
case "-":
|
|
2454
|
+
return lNum - rNum;
|
|
2455
|
+
case "*":
|
|
2456
|
+
return lNum * rNum;
|
|
2457
|
+
case "/":
|
|
2458
|
+
if (rNum === 0) return new FormulaError("#DIV/0!");
|
|
2459
|
+
return lNum / rNum;
|
|
2460
|
+
case "^":
|
|
2461
|
+
return Math.pow(lNum, rNum);
|
|
2462
|
+
case "%":
|
|
2463
|
+
return lNum * rNum / 100;
|
|
2464
|
+
default:
|
|
2465
|
+
return new FormulaError("#ERROR!", `Unknown operator: ${op}`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
evaluateUnaryOp(op, operand, context) {
|
|
2469
|
+
const val = this.evaluate(operand, context);
|
|
2470
|
+
if (val instanceof FormulaError) return val;
|
|
2471
|
+
const num = toNumber(val);
|
|
2472
|
+
if (num instanceof FormulaError) return num;
|
|
2473
|
+
return op === "-" ? -num : num;
|
|
2474
|
+
}
|
|
2475
|
+
compare(op, left, right) {
|
|
2476
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
2477
|
+
return this.numCompare(op, left, right);
|
|
2478
|
+
}
|
|
2479
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
2480
|
+
return this.strCompare(op, left, right);
|
|
2481
|
+
}
|
|
2482
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
2483
|
+
return this.numCompare(op, left ? 1 : 0, right ? 1 : 0);
|
|
2484
|
+
}
|
|
2485
|
+
const lNum = toNumber(left);
|
|
2486
|
+
const rNum = toNumber(right);
|
|
2487
|
+
if (typeof lNum === "number" && typeof rNum === "number") {
|
|
2488
|
+
return this.numCompare(op, lNum, rNum);
|
|
2489
|
+
}
|
|
2490
|
+
return this.strCompare(op, toString(left), toString(right));
|
|
2491
|
+
}
|
|
2492
|
+
numCompare(op, a, b) {
|
|
2493
|
+
switch (op) {
|
|
2494
|
+
case ">":
|
|
2495
|
+
return a > b;
|
|
2496
|
+
case "<":
|
|
2497
|
+
return a < b;
|
|
2498
|
+
case ">=":
|
|
2499
|
+
return a >= b;
|
|
2500
|
+
case "<=":
|
|
2501
|
+
return a <= b;
|
|
2502
|
+
case "=":
|
|
2503
|
+
return a === b;
|
|
2504
|
+
case "<>":
|
|
2505
|
+
return a !== b;
|
|
2506
|
+
default:
|
|
2507
|
+
return false;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
strCompare(op, a, b) {
|
|
2511
|
+
const al = a.toLowerCase();
|
|
2512
|
+
const bl = b.toLowerCase();
|
|
2513
|
+
switch (op) {
|
|
2514
|
+
case ">":
|
|
2515
|
+
return al > bl;
|
|
2516
|
+
case "<":
|
|
2517
|
+
return al < bl;
|
|
2518
|
+
case ">=":
|
|
2519
|
+
return al >= bl;
|
|
2520
|
+
case "<=":
|
|
2521
|
+
return al <= bl;
|
|
2522
|
+
case "=":
|
|
2523
|
+
return al === bl;
|
|
2524
|
+
case "<>":
|
|
2525
|
+
return al !== bl;
|
|
2526
|
+
default:
|
|
2527
|
+
return false;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
|
|
2532
|
+
var DependencyGraph = class {
|
|
2533
|
+
constructor() {
|
|
2534
|
+
this.dependencies = /* @__PURE__ */ new Map();
|
|
2535
|
+
this.dependents = /* @__PURE__ */ new Map();
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Set the dependencies for a cell, replacing any previous ones.
|
|
2539
|
+
* Updates both the forward (dependencies) and reverse (dependents) maps.
|
|
2540
|
+
*/
|
|
2541
|
+
setDependencies(cell, deps) {
|
|
2542
|
+
this.removeDependenciesInternal(cell);
|
|
2543
|
+
this.dependencies.set(cell, deps);
|
|
2544
|
+
for (const dep of deps) {
|
|
2545
|
+
let depSet = this.dependents.get(dep);
|
|
2546
|
+
if (!depSet) {
|
|
2547
|
+
depSet = /* @__PURE__ */ new Set();
|
|
2548
|
+
this.dependents.set(dep, depSet);
|
|
2549
|
+
}
|
|
2550
|
+
depSet.add(cell);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Remove all dependency information for a cell from both maps.
|
|
2555
|
+
*/
|
|
2556
|
+
removeDependencies(cell) {
|
|
2557
|
+
this.removeDependenciesInternal(cell);
|
|
2558
|
+
this.dependencies.delete(cell);
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Get all cells that directly or transitively depend on `changedCell`,
|
|
2562
|
+
* returned in topological order (a cell appears AFTER all cells it depends on).
|
|
2563
|
+
*
|
|
2564
|
+
* Uses Kahn's algorithm. If a cycle is detected, cycle participants are
|
|
2565
|
+
* appended at the end (the engine will assign #CIRC! to them).
|
|
2566
|
+
*/
|
|
2567
|
+
getRecalcOrder(changedCell) {
|
|
2568
|
+
return this.topologicalSort(/* @__PURE__ */ new Set([changedCell]));
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Same as getRecalcOrder but for multiple changed cells.
|
|
2572
|
+
* Union of all transitive dependents, topologically sorted.
|
|
2573
|
+
*/
|
|
2574
|
+
getRecalcOrderBatch(changedCells) {
|
|
2575
|
+
return this.topologicalSort(new Set(changedCells));
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Check if adding dependencies from `cell` to `deps` would create a cycle.
|
|
2579
|
+
* DFS from each dep: if we can reach `cell`, it would create a cycle.
|
|
2580
|
+
*/
|
|
2581
|
+
wouldCreateCycle(cell, deps) {
|
|
2582
|
+
if (deps.has(cell)) {
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2586
|
+
for (const dep of deps) {
|
|
2587
|
+
if (this.canReach(dep, cell, visited)) {
|
|
2588
|
+
return true;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return false;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Return direct dependents of a cell (cells whose formulas reference this cell).
|
|
2595
|
+
* Returns an empty set if none.
|
|
2596
|
+
*/
|
|
2597
|
+
getDependents(cell) {
|
|
2598
|
+
return this.dependents.get(cell) ?? EMPTY_SET;
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Return direct dependencies of a cell (cells referenced in this cell's formula).
|
|
2602
|
+
* Returns an empty set if none.
|
|
2603
|
+
*/
|
|
2604
|
+
getDependencies(cell) {
|
|
2605
|
+
return this.dependencies.get(cell) ?? EMPTY_SET;
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Clear both maps entirely.
|
|
2609
|
+
*/
|
|
2610
|
+
clear() {
|
|
2611
|
+
this.dependencies.clear();
|
|
2612
|
+
this.dependents.clear();
|
|
2613
|
+
}
|
|
2614
|
+
// ---------------------------------------------------------------------------
|
|
2615
|
+
// Private helpers
|
|
2616
|
+
// ---------------------------------------------------------------------------
|
|
2617
|
+
/**
|
|
2618
|
+
* Remove `cell` from the forward dependencies map and clean up reverse
|
|
2619
|
+
* references in the dependents map.
|
|
2620
|
+
*/
|
|
2621
|
+
removeDependenciesInternal(cell) {
|
|
2622
|
+
const oldDeps = this.dependencies.get(cell);
|
|
2623
|
+
if (!oldDeps) {
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
for (const oldDep of oldDeps) {
|
|
2627
|
+
const depSet = this.dependents.get(oldDep);
|
|
2628
|
+
if (depSet) {
|
|
2629
|
+
depSet.delete(cell);
|
|
2630
|
+
if (depSet.size === 0) {
|
|
2631
|
+
this.dependents.delete(oldDep);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
this.dependencies.delete(cell);
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Iterative DFS: check if `target` is reachable from `start` by following
|
|
2639
|
+
* the dependency chain. Iterative to avoid stack overflow for deep chains.
|
|
2640
|
+
*/
|
|
2641
|
+
canReach(start, target, visited) {
|
|
2642
|
+
if (start === target) return true;
|
|
2643
|
+
if (visited.has(start)) return false;
|
|
2644
|
+
const stack = [start];
|
|
2645
|
+
visited.add(start);
|
|
2646
|
+
while (stack.length > 0) {
|
|
2647
|
+
const current = stack.pop();
|
|
2648
|
+
const deps = this.dependencies.get(current);
|
|
2649
|
+
if (!deps) continue;
|
|
2650
|
+
for (const dep of deps) {
|
|
2651
|
+
if (dep === target) return true;
|
|
2652
|
+
if (!visited.has(dep)) {
|
|
2653
|
+
visited.add(dep);
|
|
2654
|
+
stack.push(dep);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Topological sort using Kahn's algorithm.
|
|
2662
|
+
*
|
|
2663
|
+
* 1. Collect all cells transitively dependent on the changed cell(s).
|
|
2664
|
+
* 2. Build in-degree map for these cells (count how many of their
|
|
2665
|
+
* dependencies are in the affected set).
|
|
2666
|
+
* 3. Start with cells whose in-degree is 0 (only depend on unaffected
|
|
2667
|
+
* cells or the changed cells themselves).
|
|
2668
|
+
* 4. Process queue: for each cell, reduce in-degree of its dependents,
|
|
2669
|
+
* add to queue when in-degree reaches 0.
|
|
2670
|
+
* 5. If any cells remain unprocessed, they're in a cycle — append them
|
|
2671
|
+
* at the end (engine marks as #CIRC!).
|
|
2672
|
+
*/
|
|
2673
|
+
topologicalSort(changedCells) {
|
|
2674
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2675
|
+
const bfsQueue = [];
|
|
2676
|
+
for (const changed of changedCells) {
|
|
2677
|
+
const directDependents = this.dependents.get(changed);
|
|
2678
|
+
if (directDependents) {
|
|
2679
|
+
for (const dep of directDependents) {
|
|
2680
|
+
if (!affected.has(dep)) {
|
|
2681
|
+
affected.add(dep);
|
|
2682
|
+
bfsQueue.push(dep);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
let head = 0;
|
|
2688
|
+
while (head < bfsQueue.length) {
|
|
2689
|
+
const current = bfsQueue[head++];
|
|
2690
|
+
const currentDependents = this.dependents.get(current);
|
|
2691
|
+
if (currentDependents) {
|
|
2692
|
+
for (const dep of currentDependents) {
|
|
2693
|
+
if (!affected.has(dep)) {
|
|
2694
|
+
affected.add(dep);
|
|
2695
|
+
bfsQueue.push(dep);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (affected.size === 0) {
|
|
2701
|
+
return [];
|
|
2702
|
+
}
|
|
2703
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2704
|
+
for (const cell of affected) {
|
|
2705
|
+
let degree = 0;
|
|
2706
|
+
const deps = this.dependencies.get(cell);
|
|
2707
|
+
if (deps) {
|
|
2708
|
+
for (const dep of deps) {
|
|
2709
|
+
if (affected.has(dep)) {
|
|
2710
|
+
degree++;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
inDegree.set(cell, degree);
|
|
2715
|
+
}
|
|
2716
|
+
const queue = [];
|
|
2717
|
+
for (const [cell, degree] of inDegree) {
|
|
2718
|
+
if (degree === 0) {
|
|
2719
|
+
queue.push(cell);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const result = [];
|
|
2723
|
+
let queueHead = 0;
|
|
2724
|
+
while (queueHead < queue.length) {
|
|
2725
|
+
const cell = queue[queueHead++];
|
|
2726
|
+
result.push(cell);
|
|
2727
|
+
const cellDependents = this.dependents.get(cell);
|
|
2728
|
+
if (cellDependents) {
|
|
2729
|
+
for (const dependent of cellDependents) {
|
|
2730
|
+
if (affected.has(dependent)) {
|
|
2731
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
2732
|
+
inDegree.set(dependent, newDegree);
|
|
2733
|
+
if (newDegree === 0) {
|
|
2734
|
+
queue.push(dependent);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (result.length < affected.size) {
|
|
2741
|
+
const resultSet = new Set(result);
|
|
2742
|
+
for (const cell of affected) {
|
|
2743
|
+
if (!resultSet.has(cell)) {
|
|
2744
|
+
result.push(cell);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return result;
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
function registerMathFunctions(registry) {
|
|
2752
|
+
registry.set("SUM", {
|
|
2753
|
+
minArgs: 1,
|
|
2754
|
+
maxArgs: -1,
|
|
2755
|
+
evaluate(args, context, evaluator) {
|
|
2756
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2757
|
+
let sum = 0;
|
|
2758
|
+
for (const val of values) {
|
|
2759
|
+
if (val instanceof FormulaError) return val;
|
|
2760
|
+
if (typeof val === "number") {
|
|
2761
|
+
sum += val;
|
|
2762
|
+
} else if (typeof val === "boolean") {
|
|
2763
|
+
sum += val ? 1 : 0;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
return sum;
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
registry.set("AVERAGE", {
|
|
2770
|
+
minArgs: 1,
|
|
2771
|
+
maxArgs: -1,
|
|
2772
|
+
evaluate(args, context, evaluator) {
|
|
2773
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2774
|
+
let sum = 0;
|
|
2775
|
+
let count = 0;
|
|
2776
|
+
for (const val of values) {
|
|
2777
|
+
if (val instanceof FormulaError) return val;
|
|
2778
|
+
if (typeof val === "number") {
|
|
2779
|
+
sum += val;
|
|
2780
|
+
count++;
|
|
2781
|
+
} else if (typeof val === "boolean") {
|
|
2782
|
+
sum += val ? 1 : 0;
|
|
2783
|
+
count++;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No numeric values for AVERAGE");
|
|
2787
|
+
return sum / count;
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
registry.set("MIN", {
|
|
2791
|
+
minArgs: 1,
|
|
2792
|
+
maxArgs: -1,
|
|
2793
|
+
evaluate(args, context, evaluator) {
|
|
2794
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2795
|
+
let min = Infinity;
|
|
2796
|
+
for (const val of values) {
|
|
2797
|
+
if (val instanceof FormulaError) return val;
|
|
2798
|
+
if (typeof val === "number") {
|
|
2799
|
+
if (val < min) min = val;
|
|
2800
|
+
} else if (typeof val === "boolean") {
|
|
2801
|
+
const n = val ? 1 : 0;
|
|
2802
|
+
if (n < min) min = n;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return min === Infinity ? 0 : min;
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
registry.set("MAX", {
|
|
2809
|
+
minArgs: 1,
|
|
2810
|
+
maxArgs: -1,
|
|
2811
|
+
evaluate(args, context, evaluator) {
|
|
2812
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2813
|
+
let max = -Infinity;
|
|
2814
|
+
for (const val of values) {
|
|
2815
|
+
if (val instanceof FormulaError) return val;
|
|
2816
|
+
if (typeof val === "number") {
|
|
2817
|
+
if (val > max) max = val;
|
|
2818
|
+
} else if (typeof val === "boolean") {
|
|
2819
|
+
const n = val ? 1 : 0;
|
|
2820
|
+
if (n > max) max = n;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return max === -Infinity ? 0 : max;
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
registry.set("COUNT", {
|
|
2827
|
+
minArgs: 1,
|
|
2828
|
+
maxArgs: -1,
|
|
2829
|
+
evaluate(args, context, evaluator) {
|
|
2830
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2831
|
+
let count = 0;
|
|
2832
|
+
for (const val of values) {
|
|
2833
|
+
if (val instanceof FormulaError) return val;
|
|
2834
|
+
if (typeof val === "number") {
|
|
2835
|
+
count++;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return count;
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
registry.set("COUNTA", {
|
|
2842
|
+
minArgs: 1,
|
|
2843
|
+
maxArgs: -1,
|
|
2844
|
+
evaluate(args, context, evaluator) {
|
|
2845
|
+
const values = flattenArgs(args, context, evaluator);
|
|
2846
|
+
let count = 0;
|
|
2847
|
+
for (const val of values) {
|
|
2848
|
+
if (val instanceof FormulaError) return val;
|
|
2849
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
2850
|
+
count++;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return count;
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
registry.set("ROUND", {
|
|
2857
|
+
minArgs: 2,
|
|
2858
|
+
maxArgs: 2,
|
|
2859
|
+
evaluate(args, context, evaluator) {
|
|
2860
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2861
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2862
|
+
const num = toNumber(rawNum);
|
|
2863
|
+
if (num instanceof FormulaError) return num;
|
|
2864
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
2865
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
2866
|
+
const digits = toNumber(rawDigits);
|
|
2867
|
+
if (digits instanceof FormulaError) return digits;
|
|
2868
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
2869
|
+
return Math.round(num * factor) / factor;
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
registry.set("ABS", {
|
|
2873
|
+
minArgs: 1,
|
|
2874
|
+
maxArgs: 1,
|
|
2875
|
+
evaluate(args, context, evaluator) {
|
|
2876
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
2877
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
2878
|
+
const num = toNumber(rawVal);
|
|
2879
|
+
if (num instanceof FormulaError) return num;
|
|
2880
|
+
return Math.abs(num);
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
registry.set("CEILING", {
|
|
2884
|
+
minArgs: 2,
|
|
2885
|
+
maxArgs: 2,
|
|
2886
|
+
evaluate(args, context, evaluator) {
|
|
2887
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2888
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2889
|
+
const num = toNumber(rawNum);
|
|
2890
|
+
if (num instanceof FormulaError) return num;
|
|
2891
|
+
const rawSig = evaluator.evaluate(args[1], context);
|
|
2892
|
+
if (rawSig instanceof FormulaError) return rawSig;
|
|
2893
|
+
const significance = toNumber(rawSig);
|
|
2894
|
+
if (significance instanceof FormulaError) return significance;
|
|
2895
|
+
if (significance === 0) return 0;
|
|
2896
|
+
return Math.ceil(num / significance) * significance;
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
registry.set("FLOOR", {
|
|
2900
|
+
minArgs: 2,
|
|
2901
|
+
maxArgs: 2,
|
|
2902
|
+
evaluate(args, context, evaluator) {
|
|
2903
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2904
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2905
|
+
const num = toNumber(rawNum);
|
|
2906
|
+
if (num instanceof FormulaError) return num;
|
|
2907
|
+
const rawSig = evaluator.evaluate(args[1], context);
|
|
2908
|
+
if (rawSig instanceof FormulaError) return rawSig;
|
|
2909
|
+
const significance = toNumber(rawSig);
|
|
2910
|
+
if (significance instanceof FormulaError) return significance;
|
|
2911
|
+
if (significance === 0) return 0;
|
|
2912
|
+
return Math.floor(num / significance) * significance;
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
registry.set("MOD", {
|
|
2916
|
+
minArgs: 2,
|
|
2917
|
+
maxArgs: 2,
|
|
2918
|
+
evaluate(args, context, evaluator) {
|
|
2919
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2920
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2921
|
+
const num = toNumber(rawNum);
|
|
2922
|
+
if (num instanceof FormulaError) return num;
|
|
2923
|
+
const rawDiv = evaluator.evaluate(args[1], context);
|
|
2924
|
+
if (rawDiv instanceof FormulaError) return rawDiv;
|
|
2925
|
+
const divisor = toNumber(rawDiv);
|
|
2926
|
+
if (divisor instanceof FormulaError) return divisor;
|
|
2927
|
+
if (divisor === 0) return new FormulaError("#DIV/0!", "Division by zero in MOD");
|
|
2928
|
+
return num % divisor;
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
registry.set("POWER", {
|
|
2932
|
+
minArgs: 2,
|
|
2933
|
+
maxArgs: 2,
|
|
2934
|
+
evaluate(args, context, evaluator) {
|
|
2935
|
+
const rawBase = evaluator.evaluate(args[0], context);
|
|
2936
|
+
if (rawBase instanceof FormulaError) return rawBase;
|
|
2937
|
+
const base = toNumber(rawBase);
|
|
2938
|
+
if (base instanceof FormulaError) return base;
|
|
2939
|
+
const rawExp = evaluator.evaluate(args[1], context);
|
|
2940
|
+
if (rawExp instanceof FormulaError) return rawExp;
|
|
2941
|
+
const exponent = toNumber(rawExp);
|
|
2942
|
+
if (exponent instanceof FormulaError) return exponent;
|
|
2943
|
+
return Math.pow(base, exponent);
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
registry.set("SQRT", {
|
|
2947
|
+
minArgs: 1,
|
|
2948
|
+
maxArgs: 1,
|
|
2949
|
+
evaluate(args, context, evaluator) {
|
|
2950
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
2951
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
2952
|
+
const num = toNumber(rawVal);
|
|
2953
|
+
if (num instanceof FormulaError) return num;
|
|
2954
|
+
if (num < 0) return new FormulaError("#VALUE!", "Cannot take square root of negative number");
|
|
2955
|
+
return Math.sqrt(num);
|
|
2956
|
+
}
|
|
2957
|
+
});
|
|
2958
|
+
registry.set("ROUNDUP", {
|
|
2959
|
+
minArgs: 2,
|
|
2960
|
+
maxArgs: 2,
|
|
2961
|
+
evaluate(args, context, evaluator) {
|
|
2962
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2963
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2964
|
+
const num = toNumber(rawNum);
|
|
2965
|
+
if (num instanceof FormulaError) return num;
|
|
2966
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
2967
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
2968
|
+
const digits = toNumber(rawDigits);
|
|
2969
|
+
if (digits instanceof FormulaError) return digits;
|
|
2970
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
2971
|
+
return num >= 0 ? Math.ceil(num * factor) / factor : Math.floor(num * factor) / factor;
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2974
|
+
registry.set("ROUNDDOWN", {
|
|
2975
|
+
minArgs: 2,
|
|
2976
|
+
maxArgs: 2,
|
|
2977
|
+
evaluate(args, context, evaluator) {
|
|
2978
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
2979
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
2980
|
+
const num = toNumber(rawNum);
|
|
2981
|
+
if (num instanceof FormulaError) return num;
|
|
2982
|
+
const rawDigits = evaluator.evaluate(args[1], context);
|
|
2983
|
+
if (rawDigits instanceof FormulaError) return rawDigits;
|
|
2984
|
+
const digits = toNumber(rawDigits);
|
|
2985
|
+
if (digits instanceof FormulaError) return digits;
|
|
2986
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
2987
|
+
return Math.trunc(num * factor) / factor;
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
registry.set("INT", {
|
|
2991
|
+
minArgs: 1,
|
|
2992
|
+
maxArgs: 1,
|
|
2993
|
+
evaluate(args, context, evaluator) {
|
|
2994
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
2995
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
2996
|
+
const num = toNumber(rawVal);
|
|
2997
|
+
if (num instanceof FormulaError) return num;
|
|
2998
|
+
return Math.floor(num);
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
registry.set("TRUNC", {
|
|
3002
|
+
minArgs: 1,
|
|
3003
|
+
maxArgs: 2,
|
|
3004
|
+
evaluate(args, context, evaluator) {
|
|
3005
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3006
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3007
|
+
const num = toNumber(rawVal);
|
|
3008
|
+
if (num instanceof FormulaError) return num;
|
|
3009
|
+
let digits = 0;
|
|
3010
|
+
if (args.length >= 2) {
|
|
3011
|
+
const rawD = evaluator.evaluate(args[1], context);
|
|
3012
|
+
if (rawD instanceof FormulaError) return rawD;
|
|
3013
|
+
const d = toNumber(rawD);
|
|
3014
|
+
if (d instanceof FormulaError) return d;
|
|
3015
|
+
digits = Math.trunc(d);
|
|
3016
|
+
}
|
|
3017
|
+
const factor = Math.pow(10, digits);
|
|
3018
|
+
return Math.trunc(num * factor) / factor;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
registry.set("PRODUCT", {
|
|
3022
|
+
minArgs: 1,
|
|
3023
|
+
maxArgs: -1,
|
|
3024
|
+
evaluate(args, context, evaluator) {
|
|
3025
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3026
|
+
let product = 1;
|
|
3027
|
+
let hasNumber = false;
|
|
3028
|
+
for (const val of values) {
|
|
3029
|
+
if (val instanceof FormulaError) return val;
|
|
3030
|
+
if (typeof val === "number") {
|
|
3031
|
+
product *= val;
|
|
3032
|
+
hasNumber = true;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return hasNumber ? product : 0;
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
3038
|
+
registry.set("SUMPRODUCT", {
|
|
3039
|
+
minArgs: 1,
|
|
3040
|
+
maxArgs: -1,
|
|
3041
|
+
evaluate(args, context, evaluator) {
|
|
3042
|
+
const arrays = [];
|
|
3043
|
+
for (const arg of args) {
|
|
3044
|
+
if (arg.kind !== "range") {
|
|
3045
|
+
return new FormulaError("#VALUE!", "SUMPRODUCT arguments must be ranges");
|
|
3046
|
+
}
|
|
3047
|
+
arrays.push(context.getRangeValues({ start: arg.start, end: arg.end }));
|
|
3048
|
+
}
|
|
3049
|
+
if (arrays.length === 0) return 0;
|
|
3050
|
+
const rows = arrays[0].length;
|
|
3051
|
+
const cols = rows > 0 ? arrays[0][0].length : 0;
|
|
3052
|
+
for (let a = 1; a < arrays.length; a++) {
|
|
3053
|
+
if (arrays[a].length !== rows || rows > 0 && arrays[a][0].length !== cols) {
|
|
3054
|
+
return new FormulaError("#VALUE!", "SUMPRODUCT arrays must have same dimensions");
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
let sum = 0;
|
|
3058
|
+
for (let r = 0; r < rows; r++) {
|
|
3059
|
+
for (let c = 0; c < cols; c++) {
|
|
3060
|
+
let product = 1;
|
|
3061
|
+
for (let a = 0; a < arrays.length; a++) {
|
|
3062
|
+
const v = toNumber(arrays[a][r][c]);
|
|
3063
|
+
if (v instanceof FormulaError) {
|
|
3064
|
+
product = 0;
|
|
3065
|
+
break;
|
|
3066
|
+
}
|
|
3067
|
+
product *= v;
|
|
3068
|
+
}
|
|
3069
|
+
sum += product;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return sum;
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
registry.set("MEDIAN", {
|
|
3076
|
+
minArgs: 1,
|
|
3077
|
+
maxArgs: -1,
|
|
3078
|
+
evaluate(args, context, evaluator) {
|
|
3079
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3080
|
+
const nums = [];
|
|
3081
|
+
for (const val of values) {
|
|
3082
|
+
if (val instanceof FormulaError) return val;
|
|
3083
|
+
if (typeof val === "number") nums.push(val);
|
|
3084
|
+
}
|
|
3085
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "No numeric values for MEDIAN");
|
|
3086
|
+
nums.sort((a, b) => a - b);
|
|
3087
|
+
const mid = Math.floor(nums.length / 2);
|
|
3088
|
+
return nums.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
registry.set("LARGE", {
|
|
3092
|
+
minArgs: 2,
|
|
3093
|
+
maxArgs: 2,
|
|
3094
|
+
evaluate(args, context, evaluator) {
|
|
3095
|
+
if (args[0].kind !== "range") {
|
|
3096
|
+
return new FormulaError("#VALUE!", "LARGE first argument must be a range");
|
|
3097
|
+
}
|
|
3098
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3099
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3100
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3101
|
+
const k = toNumber(rawK);
|
|
3102
|
+
if (k instanceof FormulaError) return k;
|
|
3103
|
+
const nums = [];
|
|
3104
|
+
for (const row of rangeData) {
|
|
3105
|
+
for (const cell of row) {
|
|
3106
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
const ki = Math.trunc(k);
|
|
3110
|
+
if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "LARGE k out of range");
|
|
3111
|
+
nums.sort((a, b) => b - a);
|
|
3112
|
+
return nums[ki - 1];
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
registry.set("SMALL", {
|
|
3116
|
+
minArgs: 2,
|
|
3117
|
+
maxArgs: 2,
|
|
3118
|
+
evaluate(args, context, evaluator) {
|
|
3119
|
+
if (args[0].kind !== "range") {
|
|
3120
|
+
return new FormulaError("#VALUE!", "SMALL first argument must be a range");
|
|
3121
|
+
}
|
|
3122
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3123
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3124
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3125
|
+
const k = toNumber(rawK);
|
|
3126
|
+
if (k instanceof FormulaError) return k;
|
|
3127
|
+
const nums = [];
|
|
3128
|
+
for (const row of rangeData) {
|
|
3129
|
+
for (const cell of row) {
|
|
3130
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
const ki = Math.trunc(k);
|
|
3134
|
+
if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "SMALL k out of range");
|
|
3135
|
+
nums.sort((a, b) => a - b);
|
|
3136
|
+
return nums[ki - 1];
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
registry.set("RANK", {
|
|
3140
|
+
minArgs: 2,
|
|
3141
|
+
maxArgs: 3,
|
|
3142
|
+
evaluate(args, context, evaluator) {
|
|
3143
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3144
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3145
|
+
const num = toNumber(rawNum);
|
|
3146
|
+
if (num instanceof FormulaError) return num;
|
|
3147
|
+
if (args[1].kind !== "range") {
|
|
3148
|
+
return new FormulaError("#VALUE!", "RANK second argument must be a range");
|
|
3149
|
+
}
|
|
3150
|
+
const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3151
|
+
let order = 0;
|
|
3152
|
+
if (args.length >= 3) {
|
|
3153
|
+
const rawO = evaluator.evaluate(args[2], context);
|
|
3154
|
+
if (rawO instanceof FormulaError) return rawO;
|
|
3155
|
+
const o = toNumber(rawO);
|
|
3156
|
+
if (o instanceof FormulaError) return o;
|
|
3157
|
+
order = o;
|
|
3158
|
+
}
|
|
3159
|
+
const nums = [];
|
|
3160
|
+
for (const row of rangeData) {
|
|
3161
|
+
for (const cell of row) {
|
|
3162
|
+
if (typeof cell === "number") nums.push(cell);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (!nums.includes(num)) return new FormulaError("#N/A", "RANK value not found in range");
|
|
3166
|
+
let rank = 1;
|
|
3167
|
+
for (const n of nums) {
|
|
3168
|
+
if (order === 0 ? n > num : n < num) rank++;
|
|
3169
|
+
}
|
|
3170
|
+
return rank;
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
registry.set("SIGN", {
|
|
3174
|
+
minArgs: 1,
|
|
3175
|
+
maxArgs: 1,
|
|
3176
|
+
evaluate(args, context, evaluator) {
|
|
3177
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3178
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3179
|
+
const num = toNumber(rawVal);
|
|
3180
|
+
if (num instanceof FormulaError) return num;
|
|
3181
|
+
return num > 0 ? 1 : num < 0 ? -1 : 0;
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
registry.set("LOG", {
|
|
3185
|
+
minArgs: 1,
|
|
3186
|
+
maxArgs: 2,
|
|
3187
|
+
evaluate(args, context, evaluator) {
|
|
3188
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3189
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3190
|
+
const num = toNumber(rawVal);
|
|
3191
|
+
if (num instanceof FormulaError) return num;
|
|
3192
|
+
if (num <= 0) return new FormulaError("#NUM!", "LOG requires a positive number");
|
|
3193
|
+
let base = 10;
|
|
3194
|
+
if (args.length >= 2) {
|
|
3195
|
+
const rawB = evaluator.evaluate(args[1], context);
|
|
3196
|
+
if (rawB instanceof FormulaError) return rawB;
|
|
3197
|
+
const b = toNumber(rawB);
|
|
3198
|
+
if (b instanceof FormulaError) return b;
|
|
3199
|
+
if (b <= 0 || b === 1) return new FormulaError("#NUM!", "LOG base must be positive and not 1");
|
|
3200
|
+
base = b;
|
|
3201
|
+
}
|
|
3202
|
+
return Math.log(num) / Math.log(base);
|
|
3203
|
+
}
|
|
3204
|
+
});
|
|
3205
|
+
registry.set("LN", {
|
|
3206
|
+
minArgs: 1,
|
|
3207
|
+
maxArgs: 1,
|
|
3208
|
+
evaluate(args, context, evaluator) {
|
|
3209
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3210
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3211
|
+
const num = toNumber(rawVal);
|
|
3212
|
+
if (num instanceof FormulaError) return num;
|
|
3213
|
+
if (num <= 0) return new FormulaError("#NUM!", "LN requires a positive number");
|
|
3214
|
+
return Math.log(num);
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
registry.set("EXP", {
|
|
3218
|
+
minArgs: 1,
|
|
3219
|
+
maxArgs: 1,
|
|
3220
|
+
evaluate(args, context, evaluator) {
|
|
3221
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3222
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3223
|
+
const num = toNumber(rawVal);
|
|
3224
|
+
if (num instanceof FormulaError) return num;
|
|
3225
|
+
return Math.exp(num);
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
registry.set("PI", {
|
|
3229
|
+
minArgs: 0,
|
|
3230
|
+
maxArgs: 0,
|
|
3231
|
+
evaluate() {
|
|
3232
|
+
return Math.PI;
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
registry.set("RAND", {
|
|
3236
|
+
minArgs: 0,
|
|
3237
|
+
maxArgs: 0,
|
|
3238
|
+
evaluate() {
|
|
3239
|
+
return Math.random();
|
|
3240
|
+
}
|
|
3241
|
+
});
|
|
3242
|
+
registry.set("RANDBETWEEN", {
|
|
3243
|
+
minArgs: 2,
|
|
3244
|
+
maxArgs: 2,
|
|
3245
|
+
evaluate(args, context, evaluator) {
|
|
3246
|
+
const rawLow = evaluator.evaluate(args[0], context);
|
|
3247
|
+
if (rawLow instanceof FormulaError) return rawLow;
|
|
3248
|
+
const low = toNumber(rawLow);
|
|
3249
|
+
if (low instanceof FormulaError) return low;
|
|
3250
|
+
const rawHigh = evaluator.evaluate(args[1], context);
|
|
3251
|
+
if (rawHigh instanceof FormulaError) return rawHigh;
|
|
3252
|
+
const high = toNumber(rawHigh);
|
|
3253
|
+
if (high instanceof FormulaError) return high;
|
|
3254
|
+
const lo = Math.ceil(low);
|
|
3255
|
+
const hi = Math.floor(high);
|
|
3256
|
+
if (lo > hi) return new FormulaError("#NUM!", "RANDBETWEEN bottom must be <= top");
|
|
3257
|
+
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
function flattenArgs2(args, context, evaluator) {
|
|
3262
|
+
const result = [];
|
|
3263
|
+
for (const arg of args) {
|
|
3264
|
+
if (arg.kind === "range") {
|
|
3265
|
+
const values = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
3266
|
+
for (const row of values) {
|
|
3267
|
+
for (const cell of row) {
|
|
3268
|
+
result.push(cell);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
} else {
|
|
3272
|
+
result.push(evaluator.evaluate(arg, context));
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return result;
|
|
3276
|
+
}
|
|
3277
|
+
function registerLogicalFunctions(registry) {
|
|
3278
|
+
registry.set("IF", {
|
|
3279
|
+
minArgs: 2,
|
|
3280
|
+
maxArgs: 3,
|
|
3281
|
+
evaluate(args, context, evaluator) {
|
|
3282
|
+
const condition = evaluator.evaluate(args[0], context);
|
|
3283
|
+
if (condition instanceof FormulaError) return condition;
|
|
3284
|
+
if (condition) {
|
|
3285
|
+
return evaluator.evaluate(args[1], context);
|
|
3286
|
+
} else {
|
|
3287
|
+
if (args.length >= 3) {
|
|
3288
|
+
return evaluator.evaluate(args[2], context);
|
|
3289
|
+
}
|
|
3290
|
+
return false;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
registry.set("AND", {
|
|
3295
|
+
minArgs: 1,
|
|
3296
|
+
maxArgs: -1,
|
|
3297
|
+
evaluate(args, context, evaluator) {
|
|
3298
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3299
|
+
for (const val of values) {
|
|
3300
|
+
if (val instanceof FormulaError) return val;
|
|
3301
|
+
if (!val) return false;
|
|
3302
|
+
}
|
|
3303
|
+
return true;
|
|
3304
|
+
}
|
|
3305
|
+
});
|
|
3306
|
+
registry.set("OR", {
|
|
3307
|
+
minArgs: 1,
|
|
3308
|
+
maxArgs: -1,
|
|
3309
|
+
evaluate(args, context, evaluator) {
|
|
3310
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3311
|
+
for (const val of values) {
|
|
3312
|
+
if (val instanceof FormulaError) return val;
|
|
3313
|
+
if (val) return true;
|
|
3314
|
+
}
|
|
3315
|
+
return false;
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
registry.set("NOT", {
|
|
3319
|
+
minArgs: 1,
|
|
3320
|
+
maxArgs: 1,
|
|
3321
|
+
evaluate(args, context, evaluator) {
|
|
3322
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3323
|
+
if (val instanceof FormulaError) return val;
|
|
3324
|
+
return !val;
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
registry.set("IFERROR", {
|
|
3328
|
+
minArgs: 2,
|
|
3329
|
+
maxArgs: 2,
|
|
3330
|
+
evaluate(args, context, evaluator) {
|
|
3331
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3332
|
+
if (val instanceof FormulaError) {
|
|
3333
|
+
return evaluator.evaluate(args[1], context);
|
|
3334
|
+
}
|
|
3335
|
+
return val;
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
registry.set("IFNA", {
|
|
3339
|
+
minArgs: 2,
|
|
3340
|
+
maxArgs: 2,
|
|
3341
|
+
evaluate(args, context, evaluator) {
|
|
3342
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3343
|
+
if (val instanceof FormulaError && val.type === "#N/A") {
|
|
3344
|
+
return evaluator.evaluate(args[1], context);
|
|
3345
|
+
}
|
|
3346
|
+
return val;
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
registry.set("IFS", {
|
|
3350
|
+
minArgs: 2,
|
|
3351
|
+
maxArgs: -1,
|
|
3352
|
+
evaluate(args, context, evaluator) {
|
|
3353
|
+
if (args.length % 2 !== 0) {
|
|
3354
|
+
return new FormulaError("#VALUE!", "IFS requires pairs of condition, value");
|
|
3355
|
+
}
|
|
3356
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
3357
|
+
const condition = evaluator.evaluate(args[i], context);
|
|
3358
|
+
if (condition instanceof FormulaError) return condition;
|
|
3359
|
+
if (condition) {
|
|
3360
|
+
return evaluator.evaluate(args[i + 1], context);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
return new FormulaError("#N/A", "IFS no condition was TRUE");
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
registry.set("SWITCH", {
|
|
3367
|
+
minArgs: 3,
|
|
3368
|
+
maxArgs: -1,
|
|
3369
|
+
evaluate(args, context, evaluator) {
|
|
3370
|
+
const expr = evaluator.evaluate(args[0], context);
|
|
3371
|
+
if (expr instanceof FormulaError) return expr;
|
|
3372
|
+
const hasDefault = (args.length - 1) % 2 !== 0;
|
|
3373
|
+
const pairCount = hasDefault ? (args.length - 2) / 2 : (args.length - 1) / 2;
|
|
3374
|
+
for (let i = 0; i < pairCount; i++) {
|
|
3375
|
+
const caseVal = evaluator.evaluate(args[1 + i * 2], context);
|
|
3376
|
+
if (caseVal instanceof FormulaError) return caseVal;
|
|
3377
|
+
if (expr === caseVal) {
|
|
3378
|
+
return evaluator.evaluate(args[2 + i * 2], context);
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
if (hasDefault) {
|
|
3382
|
+
return evaluator.evaluate(args[args.length - 1], context);
|
|
3383
|
+
}
|
|
3384
|
+
return new FormulaError("#N/A", "SWITCH no match found");
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
registry.set("CHOOSE", {
|
|
3388
|
+
minArgs: 2,
|
|
3389
|
+
maxArgs: -1,
|
|
3390
|
+
evaluate(args, context, evaluator) {
|
|
3391
|
+
const rawIdx = evaluator.evaluate(args[0], context);
|
|
3392
|
+
if (rawIdx instanceof FormulaError) return rawIdx;
|
|
3393
|
+
if (typeof rawIdx !== "number") return new FormulaError("#VALUE!", "CHOOSE index must be a number");
|
|
3394
|
+
const idx = Math.trunc(rawIdx);
|
|
3395
|
+
if (idx < 1 || idx >= args.length) {
|
|
3396
|
+
return new FormulaError("#VALUE!", "CHOOSE index out of range");
|
|
3397
|
+
}
|
|
3398
|
+
return evaluator.evaluate(args[idx], context);
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
registry.set("XOR", {
|
|
3402
|
+
minArgs: 1,
|
|
3403
|
+
maxArgs: -1,
|
|
3404
|
+
evaluate(args, context, evaluator) {
|
|
3405
|
+
const values = flattenArgs2(args, context, evaluator);
|
|
3406
|
+
let trueCount = 0;
|
|
3407
|
+
for (const val of values) {
|
|
3408
|
+
if (val instanceof FormulaError) return val;
|
|
3409
|
+
if (val) trueCount++;
|
|
3410
|
+
}
|
|
3411
|
+
return trueCount % 2 === 1;
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
function registerLookupFunctions(registry) {
|
|
3416
|
+
registry.set("VLOOKUP", {
|
|
3417
|
+
minArgs: 3,
|
|
3418
|
+
maxArgs: 4,
|
|
3419
|
+
evaluate(args, context, evaluator) {
|
|
3420
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3421
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3422
|
+
if (args[1].kind !== "range") {
|
|
3423
|
+
return new FormulaError("#VALUE!", "VLOOKUP table_array must be a range");
|
|
3424
|
+
}
|
|
3425
|
+
const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3426
|
+
const rawColIndex = evaluator.evaluate(args[2], context);
|
|
3427
|
+
if (rawColIndex instanceof FormulaError) return rawColIndex;
|
|
3428
|
+
const colIndex = toNumber(rawColIndex);
|
|
3429
|
+
if (colIndex instanceof FormulaError) return colIndex;
|
|
3430
|
+
if (colIndex < 1) return new FormulaError("#VALUE!", "VLOOKUP col_index must be >= 1");
|
|
3431
|
+
if (tableData.length > 0 && colIndex > tableData[0].length) {
|
|
3432
|
+
return new FormulaError("#REF!", "VLOOKUP col_index exceeds table columns");
|
|
3433
|
+
}
|
|
3434
|
+
let rangeLookup = true;
|
|
3435
|
+
if (args.length >= 4) {
|
|
3436
|
+
const rawRL = evaluator.evaluate(args[3], context);
|
|
3437
|
+
if (rawRL instanceof FormulaError) return rawRL;
|
|
3438
|
+
rangeLookup = !!rawRL;
|
|
3439
|
+
}
|
|
3440
|
+
const col = Math.trunc(colIndex) - 1;
|
|
3441
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3442
|
+
if (rangeLookup) {
|
|
3443
|
+
let bestRow = -1;
|
|
3444
|
+
for (let r = 0; r < tableData.length; r++) {
|
|
3445
|
+
const cellVal = tableData[r][0];
|
|
3446
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3447
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3448
|
+
if (cellVal <= lookupValue) bestRow = r;
|
|
3449
|
+
else break;
|
|
3450
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3451
|
+
if (cellVal.toLowerCase() <= lookupLower) bestRow = r;
|
|
3452
|
+
else break;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
if (bestRow === -1) return new FormulaError("#N/A", "VLOOKUP no match found");
|
|
3456
|
+
return tableData[bestRow][col] ?? null;
|
|
3457
|
+
} else {
|
|
3458
|
+
for (let r = 0; r < tableData.length; r++) {
|
|
3459
|
+
const cellVal = tableData[r][0];
|
|
3460
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3461
|
+
if (cellVal.toLowerCase() === lookupLower) {
|
|
3462
|
+
return tableData[r][col] ?? null;
|
|
3463
|
+
}
|
|
3464
|
+
} else if (cellVal === lookupValue) {
|
|
3465
|
+
return tableData[r][col] ?? null;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return new FormulaError("#N/A", "VLOOKUP no exact match found");
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
});
|
|
3472
|
+
registry.set("INDEX", {
|
|
3473
|
+
minArgs: 2,
|
|
3474
|
+
maxArgs: 3,
|
|
3475
|
+
evaluate(args, context, evaluator) {
|
|
3476
|
+
if (args[0].kind !== "range") {
|
|
3477
|
+
return new FormulaError("#VALUE!", "INDEX first argument must be a range");
|
|
3478
|
+
}
|
|
3479
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
3480
|
+
const rawRow = evaluator.evaluate(args[1], context);
|
|
3481
|
+
if (rawRow instanceof FormulaError) return rawRow;
|
|
3482
|
+
const rowNum = toNumber(rawRow);
|
|
3483
|
+
if (rowNum instanceof FormulaError) return rowNum;
|
|
3484
|
+
let colNum = 1;
|
|
3485
|
+
if (args.length >= 3) {
|
|
3486
|
+
const rawCol = evaluator.evaluate(args[2], context);
|
|
3487
|
+
if (rawCol instanceof FormulaError) return rawCol;
|
|
3488
|
+
const c2 = toNumber(rawCol);
|
|
3489
|
+
if (c2 instanceof FormulaError) return c2;
|
|
3490
|
+
colNum = c2;
|
|
3491
|
+
}
|
|
3492
|
+
const r = Math.trunc(rowNum) - 1;
|
|
3493
|
+
const c = Math.trunc(colNum) - 1;
|
|
3494
|
+
if (r < 0 || r >= rangeData.length) {
|
|
3495
|
+
return new FormulaError("#REF!", "INDEX row out of bounds");
|
|
3496
|
+
}
|
|
3497
|
+
if (c < 0 || rangeData.length > 0 && c >= rangeData[0].length) {
|
|
3498
|
+
return new FormulaError("#REF!", "INDEX column out of bounds");
|
|
3499
|
+
}
|
|
3500
|
+
return rangeData[r][c] ?? null;
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
registry.set("HLOOKUP", {
|
|
3504
|
+
minArgs: 3,
|
|
3505
|
+
maxArgs: 4,
|
|
3506
|
+
evaluate(args, context, evaluator) {
|
|
3507
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3508
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3509
|
+
if (args[1].kind !== "range") {
|
|
3510
|
+
return new FormulaError("#VALUE!", "HLOOKUP table_array must be a range");
|
|
3511
|
+
}
|
|
3512
|
+
const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3513
|
+
const rawRowIndex = evaluator.evaluate(args[2], context);
|
|
3514
|
+
if (rawRowIndex instanceof FormulaError) return rawRowIndex;
|
|
3515
|
+
const rowIndex = toNumber(rawRowIndex);
|
|
3516
|
+
if (rowIndex instanceof FormulaError) return rowIndex;
|
|
3517
|
+
if (rowIndex < 1) return new FormulaError("#VALUE!", "HLOOKUP row_index must be >= 1");
|
|
3518
|
+
if (rowIndex > tableData.length) {
|
|
3519
|
+
return new FormulaError("#REF!", "HLOOKUP row_index exceeds table rows");
|
|
3520
|
+
}
|
|
3521
|
+
let rangeLookup = true;
|
|
3522
|
+
if (args.length >= 4) {
|
|
3523
|
+
const rawRL = evaluator.evaluate(args[3], context);
|
|
3524
|
+
if (rawRL instanceof FormulaError) return rawRL;
|
|
3525
|
+
rangeLookup = !!rawRL;
|
|
3526
|
+
}
|
|
3527
|
+
const row = Math.trunc(rowIndex) - 1;
|
|
3528
|
+
const firstRow = tableData[0] || [];
|
|
3529
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3530
|
+
if (rangeLookup) {
|
|
3531
|
+
let bestCol = -1;
|
|
3532
|
+
for (let c = 0; c < firstRow.length; c++) {
|
|
3533
|
+
const cellVal = firstRow[c];
|
|
3534
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3535
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3536
|
+
if (cellVal <= lookupValue) bestCol = c;
|
|
3537
|
+
else break;
|
|
3538
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3539
|
+
if (cellVal.toLowerCase() <= lookupLower) bestCol = c;
|
|
3540
|
+
else break;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
if (bestCol === -1) return new FormulaError("#N/A", "HLOOKUP no match found");
|
|
3544
|
+
return tableData[row][bestCol] ?? null;
|
|
3545
|
+
} else {
|
|
3546
|
+
for (let c = 0; c < firstRow.length; c++) {
|
|
3547
|
+
const cellVal = firstRow[c];
|
|
3548
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3549
|
+
if (cellVal.toLowerCase() === lookupLower) return tableData[row][c] ?? null;
|
|
3550
|
+
} else if (cellVal === lookupValue) {
|
|
3551
|
+
return tableData[row][c] ?? null;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return new FormulaError("#N/A", "HLOOKUP no exact match found");
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
registry.set("XLOOKUP", {
|
|
3559
|
+
minArgs: 3,
|
|
3560
|
+
maxArgs: 6,
|
|
3561
|
+
evaluate(args, context, evaluator) {
|
|
3562
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3563
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3564
|
+
if (args[1].kind !== "range") {
|
|
3565
|
+
return new FormulaError("#VALUE!", "XLOOKUP lookup_array must be a range");
|
|
3566
|
+
}
|
|
3567
|
+
const lookupArray = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3568
|
+
if (args[2].kind !== "range") {
|
|
3569
|
+
return new FormulaError("#VALUE!", "XLOOKUP return_array must be a range");
|
|
3570
|
+
}
|
|
3571
|
+
const returnArray = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
3572
|
+
let ifNotFound = new FormulaError("#N/A", "XLOOKUP no match found");
|
|
3573
|
+
if (args.length >= 4) {
|
|
3574
|
+
ifNotFound = evaluator.evaluate(args[3], context);
|
|
3575
|
+
}
|
|
3576
|
+
let matchMode = 0;
|
|
3577
|
+
if (args.length >= 5) {
|
|
3578
|
+
const rawMM = evaluator.evaluate(args[4], context);
|
|
3579
|
+
if (rawMM instanceof FormulaError) return rawMM;
|
|
3580
|
+
const mm = toNumber(rawMM);
|
|
3581
|
+
if (mm instanceof FormulaError) return mm;
|
|
3582
|
+
matchMode = Math.trunc(mm);
|
|
3583
|
+
}
|
|
3584
|
+
let searchMode = 1;
|
|
3585
|
+
if (args.length >= 6) {
|
|
3586
|
+
const rawSM = evaluator.evaluate(args[5], context);
|
|
3587
|
+
if (rawSM instanceof FormulaError) return rawSM;
|
|
3588
|
+
const sm = toNumber(rawSM);
|
|
3589
|
+
if (sm instanceof FormulaError) return sm;
|
|
3590
|
+
searchMode = Math.trunc(sm);
|
|
3591
|
+
}
|
|
3592
|
+
const isRow = lookupArray.length === 1;
|
|
3593
|
+
const len = isRow ? lookupArray[0].length : lookupArray.length;
|
|
3594
|
+
const getVal = isRow ? (i) => lookupArray[0][i] : (i) => lookupArray[i][0];
|
|
3595
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3596
|
+
const eq = (a, b) => {
|
|
3597
|
+
if (lookupLower !== null && typeof a === "string") return a.toLowerCase() === lookupLower;
|
|
3598
|
+
return a === b;
|
|
3599
|
+
};
|
|
3600
|
+
let foundIdx = -1;
|
|
3601
|
+
if (matchMode === 0) {
|
|
3602
|
+
const start = searchMode >= 0 ? 0 : len - 1;
|
|
3603
|
+
const end = searchMode >= 0 ? len : -1;
|
|
3604
|
+
const step = searchMode >= 0 ? 1 : -1;
|
|
3605
|
+
for (let i = start; i !== end; i += step) {
|
|
3606
|
+
if (eq(getVal(i), lookupValue)) {
|
|
3607
|
+
foundIdx = i;
|
|
3608
|
+
break;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
} else if (matchMode === -1) {
|
|
3612
|
+
let best = -1;
|
|
3613
|
+
for (let i = 0; i < len; i++) {
|
|
3614
|
+
const v = getVal(i);
|
|
3615
|
+
if (eq(v, lookupValue)) {
|
|
3616
|
+
foundIdx = i;
|
|
3617
|
+
break;
|
|
3618
|
+
}
|
|
3619
|
+
if (typeof lookupValue === "number" && typeof v === "number" && v < lookupValue) {
|
|
3620
|
+
if (best === -1 || v > getVal(best)) best = i;
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
if (foundIdx === -1) foundIdx = best;
|
|
3624
|
+
} else if (matchMode === 1) {
|
|
3625
|
+
let best = -1;
|
|
3626
|
+
for (let i = 0; i < len; i++) {
|
|
3627
|
+
const v = getVal(i);
|
|
3628
|
+
if (eq(v, lookupValue)) {
|
|
3629
|
+
foundIdx = i;
|
|
3630
|
+
break;
|
|
3631
|
+
}
|
|
3632
|
+
if (typeof lookupValue === "number" && typeof v === "number" && v > lookupValue) {
|
|
3633
|
+
if (best === -1 || v < getVal(best)) best = i;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
if (foundIdx === -1) foundIdx = best;
|
|
3637
|
+
}
|
|
3638
|
+
if (foundIdx === -1) return ifNotFound;
|
|
3639
|
+
const isReturnRow = returnArray.length === 1;
|
|
3640
|
+
if (isReturnRow) return returnArray[0][foundIdx] ?? null;
|
|
3641
|
+
return returnArray[foundIdx]?.[0] ?? null;
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
registry.set("MATCH", {
|
|
3645
|
+
minArgs: 2,
|
|
3646
|
+
maxArgs: 3,
|
|
3647
|
+
evaluate(args, context, evaluator) {
|
|
3648
|
+
const lookupValue = evaluator.evaluate(args[0], context);
|
|
3649
|
+
if (lookupValue instanceof FormulaError) return lookupValue;
|
|
3650
|
+
if (args[1].kind !== "range") {
|
|
3651
|
+
return new FormulaError("#VALUE!", "MATCH lookup_array must be a range");
|
|
3652
|
+
}
|
|
3653
|
+
const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
3654
|
+
const isSingleRow = rangeData.length === 1;
|
|
3655
|
+
const values = isSingleRow ? rangeData[0] : rangeData;
|
|
3656
|
+
const getValue = isSingleRow ? (i) => values[i] : (i) => values[i][0];
|
|
3657
|
+
const valuesLength = isSingleRow ? values.length : values.length;
|
|
3658
|
+
let matchType = 1;
|
|
3659
|
+
if (args.length >= 3) {
|
|
3660
|
+
const rawMT = evaluator.evaluate(args[2], context);
|
|
3661
|
+
if (rawMT instanceof FormulaError) return rawMT;
|
|
3662
|
+
const mt = toNumber(rawMT);
|
|
3663
|
+
if (mt instanceof FormulaError) return mt;
|
|
3664
|
+
matchType = mt;
|
|
3665
|
+
}
|
|
3666
|
+
const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
|
|
3667
|
+
if (matchType === 0) {
|
|
3668
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3669
|
+
const cellVal = getValue(i);
|
|
3670
|
+
if (lookupLower !== null && typeof cellVal === "string") {
|
|
3671
|
+
if (cellVal.toLowerCase() === lookupLower) return i + 1;
|
|
3672
|
+
} else if (cellVal === lookupValue) {
|
|
3673
|
+
return i + 1;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
return new FormulaError("#N/A", "MATCH no exact match found");
|
|
3677
|
+
} else if (matchType === 1) {
|
|
3678
|
+
let bestIndex = -1;
|
|
3679
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3680
|
+
const cellVal = getValue(i);
|
|
3681
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3682
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3683
|
+
if (cellVal <= lookupValue) bestIndex = i;
|
|
3684
|
+
else break;
|
|
3685
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3686
|
+
if (cellVal.toLowerCase() <= lookupLower) bestIndex = i;
|
|
3687
|
+
else break;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
|
|
3691
|
+
return bestIndex + 1;
|
|
3692
|
+
} else {
|
|
3693
|
+
let bestIndex = -1;
|
|
3694
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
3695
|
+
const cellVal = getValue(i);
|
|
3696
|
+
if (cellVal === null || cellVal === void 0) continue;
|
|
3697
|
+
if (typeof lookupValue === "number" && typeof cellVal === "number") {
|
|
3698
|
+
if (cellVal >= lookupValue) bestIndex = i;
|
|
3699
|
+
else break;
|
|
3700
|
+
} else if (lookupLower !== null && typeof cellVal === "string") {
|
|
3701
|
+
if (cellVal.toLowerCase() >= lookupLower) bestIndex = i;
|
|
3702
|
+
else break;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
|
|
3706
|
+
return bestIndex + 1;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
function registerTextFunctions(registry) {
|
|
3712
|
+
registry.set("CONCATENATE", {
|
|
3713
|
+
minArgs: 1,
|
|
3714
|
+
maxArgs: -1,
|
|
3715
|
+
evaluate(args, context, evaluator) {
|
|
3716
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3717
|
+
const parts = [];
|
|
3718
|
+
for (const val of values) {
|
|
3719
|
+
if (val instanceof FormulaError) return val;
|
|
3720
|
+
parts.push(toString(val));
|
|
3721
|
+
}
|
|
3722
|
+
return parts.join("");
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
registry.set("CONCAT", {
|
|
3726
|
+
minArgs: 1,
|
|
3727
|
+
maxArgs: -1,
|
|
3728
|
+
evaluate(args, context, evaluator) {
|
|
3729
|
+
const values = flattenArgs(args, context, evaluator);
|
|
3730
|
+
const parts = [];
|
|
3731
|
+
for (const val of values) {
|
|
3732
|
+
if (val instanceof FormulaError) return val;
|
|
3733
|
+
parts.push(toString(val));
|
|
3734
|
+
}
|
|
3735
|
+
return parts.join("");
|
|
3736
|
+
}
|
|
3737
|
+
});
|
|
3738
|
+
registry.set("UPPER", {
|
|
3739
|
+
minArgs: 1,
|
|
3740
|
+
maxArgs: 1,
|
|
3741
|
+
evaluate(args, context, evaluator) {
|
|
3742
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3743
|
+
if (val instanceof FormulaError) return val;
|
|
3744
|
+
return toString(val).toUpperCase();
|
|
3745
|
+
}
|
|
3746
|
+
});
|
|
3747
|
+
registry.set("LOWER", {
|
|
3748
|
+
minArgs: 1,
|
|
3749
|
+
maxArgs: 1,
|
|
3750
|
+
evaluate(args, context, evaluator) {
|
|
3751
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3752
|
+
if (val instanceof FormulaError) return val;
|
|
3753
|
+
return toString(val).toLowerCase();
|
|
3754
|
+
}
|
|
3755
|
+
});
|
|
3756
|
+
registry.set("TRIM", {
|
|
3757
|
+
minArgs: 1,
|
|
3758
|
+
maxArgs: 1,
|
|
3759
|
+
evaluate(args, context, evaluator) {
|
|
3760
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3761
|
+
if (val instanceof FormulaError) return val;
|
|
3762
|
+
return toString(val).trim();
|
|
3763
|
+
}
|
|
3764
|
+
});
|
|
3765
|
+
registry.set("LEFT", {
|
|
3766
|
+
minArgs: 1,
|
|
3767
|
+
maxArgs: 2,
|
|
3768
|
+
evaluate(args, context, evaluator) {
|
|
3769
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3770
|
+
if (val instanceof FormulaError) return val;
|
|
3771
|
+
const text = toString(val);
|
|
3772
|
+
let numChars = 1;
|
|
3773
|
+
if (args.length >= 2) {
|
|
3774
|
+
const rawNum = evaluator.evaluate(args[1], context);
|
|
3775
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3776
|
+
const n = toNumber(rawNum);
|
|
3777
|
+
if (n instanceof FormulaError) return n;
|
|
3778
|
+
numChars = Math.trunc(n);
|
|
3779
|
+
}
|
|
3780
|
+
if (numChars < 0) return new FormulaError("#VALUE!", "LEFT num_chars must be >= 0");
|
|
3781
|
+
return text.substring(0, numChars);
|
|
3782
|
+
}
|
|
3783
|
+
});
|
|
3784
|
+
registry.set("RIGHT", {
|
|
3785
|
+
minArgs: 1,
|
|
3786
|
+
maxArgs: 2,
|
|
3787
|
+
evaluate(args, context, evaluator) {
|
|
3788
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3789
|
+
if (val instanceof FormulaError) return val;
|
|
3790
|
+
const text = toString(val);
|
|
3791
|
+
let numChars = 1;
|
|
3792
|
+
if (args.length >= 2) {
|
|
3793
|
+
const rawNum = evaluator.evaluate(args[1], context);
|
|
3794
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3795
|
+
const n = toNumber(rawNum);
|
|
3796
|
+
if (n instanceof FormulaError) return n;
|
|
3797
|
+
numChars = Math.trunc(n);
|
|
3798
|
+
}
|
|
3799
|
+
if (numChars < 0) return new FormulaError("#VALUE!", "RIGHT num_chars must be >= 0");
|
|
3800
|
+
return text.substring(Math.max(0, text.length - numChars));
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
registry.set("MID", {
|
|
3804
|
+
minArgs: 3,
|
|
3805
|
+
maxArgs: 3,
|
|
3806
|
+
evaluate(args, context, evaluator) {
|
|
3807
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3808
|
+
if (val instanceof FormulaError) return val;
|
|
3809
|
+
const text = toString(val);
|
|
3810
|
+
const rawStart = evaluator.evaluate(args[1], context);
|
|
3811
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
3812
|
+
const startPos = toNumber(rawStart);
|
|
3813
|
+
if (startPos instanceof FormulaError) return startPos;
|
|
3814
|
+
const rawNum = evaluator.evaluate(args[2], context);
|
|
3815
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3816
|
+
const numChars = toNumber(rawNum);
|
|
3817
|
+
if (numChars instanceof FormulaError) return numChars;
|
|
3818
|
+
const start = Math.trunc(startPos);
|
|
3819
|
+
const count = Math.trunc(numChars);
|
|
3820
|
+
if (start < 1) return new FormulaError("#VALUE!", "MID start_num must be >= 1");
|
|
3821
|
+
if (count < 0) return new FormulaError("#VALUE!", "MID num_chars must be >= 0");
|
|
3822
|
+
return text.substring(start - 1, start - 1 + count);
|
|
3823
|
+
}
|
|
3824
|
+
});
|
|
3825
|
+
registry.set("LEN", {
|
|
3826
|
+
minArgs: 1,
|
|
3827
|
+
maxArgs: 1,
|
|
3828
|
+
evaluate(args, context, evaluator) {
|
|
3829
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3830
|
+
if (val instanceof FormulaError) return val;
|
|
3831
|
+
return toString(val).length;
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
registry.set("SUBSTITUTE", {
|
|
3835
|
+
minArgs: 3,
|
|
3836
|
+
maxArgs: 4,
|
|
3837
|
+
evaluate(args, context, evaluator) {
|
|
3838
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3839
|
+
if (val instanceof FormulaError) return val;
|
|
3840
|
+
const text = toString(val);
|
|
3841
|
+
const rawOld = evaluator.evaluate(args[1], context);
|
|
3842
|
+
if (rawOld instanceof FormulaError) return rawOld;
|
|
3843
|
+
const oldText = toString(rawOld);
|
|
3844
|
+
const rawNew = evaluator.evaluate(args[2], context);
|
|
3845
|
+
if (rawNew instanceof FormulaError) return rawNew;
|
|
3846
|
+
const newText = toString(rawNew);
|
|
3847
|
+
if (args.length >= 4) {
|
|
3848
|
+
const rawInstance = evaluator.evaluate(args[3], context);
|
|
3849
|
+
if (rawInstance instanceof FormulaError) return rawInstance;
|
|
3850
|
+
const instanceNum = toNumber(rawInstance);
|
|
3851
|
+
if (instanceNum instanceof FormulaError) return instanceNum;
|
|
3852
|
+
const n = Math.trunc(instanceNum);
|
|
3853
|
+
if (n < 1) return new FormulaError("#VALUE!", "SUBSTITUTE instance_num must be >= 1");
|
|
3854
|
+
let count = 0;
|
|
3855
|
+
let result = "";
|
|
3856
|
+
let searchFrom = 0;
|
|
3857
|
+
while (searchFrom <= text.length) {
|
|
3858
|
+
const idx = text.indexOf(oldText, searchFrom);
|
|
3859
|
+
if (idx === -1) {
|
|
3860
|
+
result += text.substring(searchFrom);
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
count++;
|
|
3864
|
+
if (count === n) {
|
|
3865
|
+
result += text.substring(searchFrom, idx) + newText;
|
|
3866
|
+
result += text.substring(idx + oldText.length);
|
|
3867
|
+
break;
|
|
3868
|
+
} else {
|
|
3869
|
+
result += text.substring(searchFrom, idx + oldText.length);
|
|
3870
|
+
searchFrom = idx + oldText.length;
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
return result;
|
|
3874
|
+
} else {
|
|
3875
|
+
if (oldText === "") return text;
|
|
3876
|
+
return text.split(oldText).join(newText);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
registry.set("FIND", {
|
|
3881
|
+
minArgs: 2,
|
|
3882
|
+
maxArgs: 3,
|
|
3883
|
+
evaluate(args, context, evaluator) {
|
|
3884
|
+
const rawFind = evaluator.evaluate(args[0], context);
|
|
3885
|
+
if (rawFind instanceof FormulaError) return rawFind;
|
|
3886
|
+
const findText = toString(rawFind);
|
|
3887
|
+
const rawWithin = evaluator.evaluate(args[1], context);
|
|
3888
|
+
if (rawWithin instanceof FormulaError) return rawWithin;
|
|
3889
|
+
const withinText = toString(rawWithin);
|
|
3890
|
+
let startNum = 1;
|
|
3891
|
+
if (args.length >= 3) {
|
|
3892
|
+
const rawStart = evaluator.evaluate(args[2], context);
|
|
3893
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
3894
|
+
const s = toNumber(rawStart);
|
|
3895
|
+
if (s instanceof FormulaError) return s;
|
|
3896
|
+
startNum = Math.trunc(s);
|
|
3897
|
+
}
|
|
3898
|
+
if (startNum < 1) return new FormulaError("#VALUE!", "FIND start_num must be >= 1");
|
|
3899
|
+
const idx = withinText.indexOf(findText, startNum - 1);
|
|
3900
|
+
if (idx === -1) return new FormulaError("#VALUE!", "FIND text not found");
|
|
3901
|
+
return idx + 1;
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
registry.set("SEARCH", {
|
|
3905
|
+
minArgs: 2,
|
|
3906
|
+
maxArgs: 3,
|
|
3907
|
+
evaluate(args, context, evaluator) {
|
|
3908
|
+
const rawFind = evaluator.evaluate(args[0], context);
|
|
3909
|
+
if (rawFind instanceof FormulaError) return rawFind;
|
|
3910
|
+
const findText = toString(rawFind).toLowerCase();
|
|
3911
|
+
const rawWithin = evaluator.evaluate(args[1], context);
|
|
3912
|
+
if (rawWithin instanceof FormulaError) return rawWithin;
|
|
3913
|
+
const withinText = toString(rawWithin).toLowerCase();
|
|
3914
|
+
let startNum = 1;
|
|
3915
|
+
if (args.length >= 3) {
|
|
3916
|
+
const rawStart = evaluator.evaluate(args[2], context);
|
|
3917
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
3918
|
+
const s = toNumber(rawStart);
|
|
3919
|
+
if (s instanceof FormulaError) return s;
|
|
3920
|
+
startNum = Math.trunc(s);
|
|
3921
|
+
}
|
|
3922
|
+
if (startNum < 1) return new FormulaError("#VALUE!", "SEARCH start_num must be >= 1");
|
|
3923
|
+
const idx = withinText.indexOf(findText, startNum - 1);
|
|
3924
|
+
if (idx === -1) return new FormulaError("#VALUE!", "SEARCH text not found");
|
|
3925
|
+
return idx + 1;
|
|
3926
|
+
}
|
|
3927
|
+
});
|
|
3928
|
+
registry.set("REPLACE", {
|
|
3929
|
+
minArgs: 4,
|
|
3930
|
+
maxArgs: 4,
|
|
3931
|
+
evaluate(args, context, evaluator) {
|
|
3932
|
+
const rawText = evaluator.evaluate(args[0], context);
|
|
3933
|
+
if (rawText instanceof FormulaError) return rawText;
|
|
3934
|
+
const text = toString(rawText);
|
|
3935
|
+
const rawStart = evaluator.evaluate(args[1], context);
|
|
3936
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
3937
|
+
const startPos = toNumber(rawStart);
|
|
3938
|
+
if (startPos instanceof FormulaError) return startPos;
|
|
3939
|
+
const rawNum = evaluator.evaluate(args[2], context);
|
|
3940
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3941
|
+
const numChars = toNumber(rawNum);
|
|
3942
|
+
if (numChars instanceof FormulaError) return numChars;
|
|
3943
|
+
const rawNew = evaluator.evaluate(args[3], context);
|
|
3944
|
+
if (rawNew instanceof FormulaError) return rawNew;
|
|
3945
|
+
const newText = toString(rawNew);
|
|
3946
|
+
const start = Math.trunc(startPos) - 1;
|
|
3947
|
+
const count = Math.trunc(numChars);
|
|
3948
|
+
return text.substring(0, start) + newText + text.substring(start + count);
|
|
3949
|
+
}
|
|
3950
|
+
});
|
|
3951
|
+
registry.set("REPT", {
|
|
3952
|
+
minArgs: 2,
|
|
3953
|
+
maxArgs: 2,
|
|
3954
|
+
evaluate(args, context, evaluator) {
|
|
3955
|
+
const rawText = evaluator.evaluate(args[0], context);
|
|
3956
|
+
if (rawText instanceof FormulaError) return rawText;
|
|
3957
|
+
const text = toString(rawText);
|
|
3958
|
+
const rawTimes = evaluator.evaluate(args[1], context);
|
|
3959
|
+
if (rawTimes instanceof FormulaError) return rawTimes;
|
|
3960
|
+
const times = toNumber(rawTimes);
|
|
3961
|
+
if (times instanceof FormulaError) return times;
|
|
3962
|
+
const n = Math.trunc(times);
|
|
3963
|
+
if (n < 0) return new FormulaError("#VALUE!", "REPT number must be >= 0");
|
|
3964
|
+
return text.repeat(n);
|
|
3965
|
+
}
|
|
3966
|
+
});
|
|
3967
|
+
registry.set("EXACT", {
|
|
3968
|
+
minArgs: 2,
|
|
3969
|
+
maxArgs: 2,
|
|
3970
|
+
evaluate(args, context, evaluator) {
|
|
3971
|
+
const rawA = evaluator.evaluate(args[0], context);
|
|
3972
|
+
if (rawA instanceof FormulaError) return rawA;
|
|
3973
|
+
const rawB = evaluator.evaluate(args[1], context);
|
|
3974
|
+
if (rawB instanceof FormulaError) return rawB;
|
|
3975
|
+
return toString(rawA) === toString(rawB);
|
|
3976
|
+
}
|
|
3977
|
+
});
|
|
3978
|
+
registry.set("PROPER", {
|
|
3979
|
+
minArgs: 1,
|
|
3980
|
+
maxArgs: 1,
|
|
3981
|
+
evaluate(args, context, evaluator) {
|
|
3982
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3983
|
+
if (val instanceof FormulaError) return val;
|
|
3984
|
+
return toString(val).toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3985
|
+
}
|
|
3986
|
+
});
|
|
3987
|
+
registry.set("CLEAN", {
|
|
3988
|
+
minArgs: 1,
|
|
3989
|
+
maxArgs: 1,
|
|
3990
|
+
evaluate(args, context, evaluator) {
|
|
3991
|
+
const val = evaluator.evaluate(args[0], context);
|
|
3992
|
+
if (val instanceof FormulaError) return val;
|
|
3993
|
+
return toString(val).replace(/[\x00-\x1F]/g, "");
|
|
3994
|
+
}
|
|
3995
|
+
});
|
|
3996
|
+
registry.set("CHAR", {
|
|
3997
|
+
minArgs: 1,
|
|
3998
|
+
maxArgs: 1,
|
|
3999
|
+
evaluate(args, context, evaluator) {
|
|
4000
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
4001
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
4002
|
+
const num = toNumber(rawVal);
|
|
4003
|
+
if (num instanceof FormulaError) return num;
|
|
4004
|
+
const n = Math.trunc(num);
|
|
4005
|
+
if (n < 1 || n > 65535) return new FormulaError("#VALUE!", "CHAR number must be 1-65535");
|
|
4006
|
+
return String.fromCharCode(n);
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
registry.set("CODE", {
|
|
4010
|
+
minArgs: 1,
|
|
4011
|
+
maxArgs: 1,
|
|
4012
|
+
evaluate(args, context, evaluator) {
|
|
4013
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4014
|
+
if (val instanceof FormulaError) return val;
|
|
4015
|
+
const text = toString(val);
|
|
4016
|
+
if (text.length === 0) return new FormulaError("#VALUE!", "CODE requires non-empty text");
|
|
4017
|
+
return text.charCodeAt(0);
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
4020
|
+
registry.set("TEXT", {
|
|
4021
|
+
minArgs: 2,
|
|
4022
|
+
maxArgs: 2,
|
|
4023
|
+
evaluate(args, context, evaluator) {
|
|
4024
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
4025
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
4026
|
+
const rawFmt = evaluator.evaluate(args[1], context);
|
|
4027
|
+
if (rawFmt instanceof FormulaError) return rawFmt;
|
|
4028
|
+
const fmt = toString(rawFmt);
|
|
4029
|
+
const num = toNumber(rawVal);
|
|
4030
|
+
if (num instanceof FormulaError) return toString(rawVal);
|
|
4031
|
+
if (fmt.includes("%")) {
|
|
4032
|
+
const decimals2 = (fmt.match(/0/g) || []).length - 1;
|
|
4033
|
+
return (num * 100).toFixed(Math.max(0, decimals2)) + "%";
|
|
4034
|
+
}
|
|
4035
|
+
const decimalMatch = fmt.match(/\.(0+)/);
|
|
4036
|
+
const decimals = decimalMatch ? decimalMatch[1].length : 0;
|
|
4037
|
+
const useCommas = fmt.includes(",");
|
|
4038
|
+
const result = num.toFixed(decimals);
|
|
4039
|
+
if (useCommas) {
|
|
4040
|
+
const [intPart, decPart] = result.split(".");
|
|
4041
|
+
const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4042
|
+
return decPart ? withCommas + "." + decPart : withCommas;
|
|
4043
|
+
}
|
|
4044
|
+
return result;
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
4047
|
+
registry.set("VALUE", {
|
|
4048
|
+
minArgs: 1,
|
|
4049
|
+
maxArgs: 1,
|
|
4050
|
+
evaluate(args, context, evaluator) {
|
|
4051
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4052
|
+
if (val instanceof FormulaError) return val;
|
|
4053
|
+
if (typeof val === "number") return val;
|
|
4054
|
+
const raw = toString(val).trim();
|
|
4055
|
+
const isPercent = raw.endsWith("%");
|
|
4056
|
+
const text = raw.replace(/[,$%\s]/g, "");
|
|
4057
|
+
const n = Number(text);
|
|
4058
|
+
if (isNaN(n)) return new FormulaError("#VALUE!", "VALUE cannot convert text to number");
|
|
4059
|
+
return isPercent ? n / 100 : n;
|
|
4060
|
+
}
|
|
4061
|
+
});
|
|
4062
|
+
registry.set("TEXTJOIN", {
|
|
4063
|
+
minArgs: 3,
|
|
4064
|
+
maxArgs: -1,
|
|
4065
|
+
evaluate(args, context, evaluator) {
|
|
4066
|
+
const rawDelim = evaluator.evaluate(args[0], context);
|
|
4067
|
+
if (rawDelim instanceof FormulaError) return rawDelim;
|
|
4068
|
+
const delimiter = toString(rawDelim);
|
|
4069
|
+
const rawIgnore = evaluator.evaluate(args[1], context);
|
|
4070
|
+
if (rawIgnore instanceof FormulaError) return rawIgnore;
|
|
4071
|
+
const ignoreEmpty = !!rawIgnore;
|
|
4072
|
+
const parts = [];
|
|
4073
|
+
for (let i = 2; i < args.length; i++) {
|
|
4074
|
+
const arg = args[i];
|
|
4075
|
+
if (arg.kind === "range") {
|
|
4076
|
+
const rangeData = context.getRangeValues({ start: arg.start, end: arg.end });
|
|
4077
|
+
for (const row of rangeData) {
|
|
4078
|
+
for (const cell of row) {
|
|
4079
|
+
if (cell instanceof FormulaError) return cell;
|
|
4080
|
+
const s = toString(cell);
|
|
4081
|
+
if (!ignoreEmpty || s !== "") parts.push(s);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
} else {
|
|
4085
|
+
const val = evaluator.evaluate(args[i], context);
|
|
4086
|
+
if (val instanceof FormulaError) return val;
|
|
4087
|
+
const s = toString(val);
|
|
4088
|
+
if (!ignoreEmpty || s !== "") parts.push(s);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return parts.join(delimiter);
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
function toDate(val) {
|
|
4096
|
+
if (val instanceof FormulaError) return val;
|
|
4097
|
+
if (val instanceof Date) {
|
|
4098
|
+
if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
|
|
4099
|
+
return val;
|
|
4100
|
+
}
|
|
4101
|
+
if (typeof val === "string") {
|
|
4102
|
+
const d = new Date(val);
|
|
4103
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
|
|
4104
|
+
return d;
|
|
4105
|
+
}
|
|
4106
|
+
if (typeof val === "number") {
|
|
4107
|
+
const d = new Date(val);
|
|
4108
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
|
|
4109
|
+
return d;
|
|
4110
|
+
}
|
|
4111
|
+
return new FormulaError("#VALUE!", "Cannot convert value to date");
|
|
4112
|
+
}
|
|
4113
|
+
function registerDateFunctions(registry) {
|
|
4114
|
+
registry.set("TODAY", {
|
|
4115
|
+
minArgs: 0,
|
|
4116
|
+
maxArgs: 0,
|
|
4117
|
+
evaluate(_args, context) {
|
|
4118
|
+
const now = context.now();
|
|
4119
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
4120
|
+
}
|
|
4121
|
+
});
|
|
4122
|
+
registry.set("NOW", {
|
|
4123
|
+
minArgs: 0,
|
|
4124
|
+
maxArgs: 0,
|
|
4125
|
+
evaluate(_args, context) {
|
|
4126
|
+
return context.now();
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
registry.set("YEAR", {
|
|
4130
|
+
minArgs: 1,
|
|
4131
|
+
maxArgs: 1,
|
|
4132
|
+
evaluate(args, context, evaluator) {
|
|
4133
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4134
|
+
if (val instanceof FormulaError) return val;
|
|
4135
|
+
const date = toDate(val);
|
|
4136
|
+
if (date instanceof FormulaError) return date;
|
|
4137
|
+
return date.getFullYear();
|
|
4138
|
+
}
|
|
4139
|
+
});
|
|
4140
|
+
registry.set("MONTH", {
|
|
4141
|
+
minArgs: 1,
|
|
4142
|
+
maxArgs: 1,
|
|
4143
|
+
evaluate(args, context, evaluator) {
|
|
4144
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4145
|
+
if (val instanceof FormulaError) return val;
|
|
4146
|
+
const date = toDate(val);
|
|
4147
|
+
if (date instanceof FormulaError) return date;
|
|
4148
|
+
return date.getMonth() + 1;
|
|
4149
|
+
}
|
|
4150
|
+
});
|
|
4151
|
+
registry.set("DAY", {
|
|
4152
|
+
minArgs: 1,
|
|
4153
|
+
maxArgs: 1,
|
|
4154
|
+
evaluate(args, context, evaluator) {
|
|
4155
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4156
|
+
if (val instanceof FormulaError) return val;
|
|
4157
|
+
const date = toDate(val);
|
|
4158
|
+
if (date instanceof FormulaError) return date;
|
|
4159
|
+
return date.getDate();
|
|
4160
|
+
}
|
|
4161
|
+
});
|
|
4162
|
+
registry.set("DATE", {
|
|
4163
|
+
minArgs: 3,
|
|
4164
|
+
maxArgs: 3,
|
|
4165
|
+
evaluate(args, context, evaluator) {
|
|
4166
|
+
const rawY = evaluator.evaluate(args[0], context);
|
|
4167
|
+
if (rawY instanceof FormulaError) return rawY;
|
|
4168
|
+
const y = toNumber(rawY);
|
|
4169
|
+
if (y instanceof FormulaError) return y;
|
|
4170
|
+
const rawM = evaluator.evaluate(args[1], context);
|
|
4171
|
+
if (rawM instanceof FormulaError) return rawM;
|
|
4172
|
+
const m = toNumber(rawM);
|
|
4173
|
+
if (m instanceof FormulaError) return m;
|
|
4174
|
+
const rawD = evaluator.evaluate(args[2], context);
|
|
4175
|
+
if (rawD instanceof FormulaError) return rawD;
|
|
4176
|
+
const d = toNumber(rawD);
|
|
4177
|
+
if (d instanceof FormulaError) return d;
|
|
4178
|
+
return new Date(Math.trunc(y), Math.trunc(m) - 1, Math.trunc(d));
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
registry.set("DATEDIF", {
|
|
4182
|
+
minArgs: 3,
|
|
4183
|
+
maxArgs: 3,
|
|
4184
|
+
evaluate(args, context, evaluator) {
|
|
4185
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
4186
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4187
|
+
const startDate = toDate(rawStart);
|
|
4188
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
4189
|
+
const rawEnd = evaluator.evaluate(args[1], context);
|
|
4190
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
4191
|
+
const endDate = toDate(rawEnd);
|
|
4192
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
4193
|
+
const rawUnit = evaluator.evaluate(args[2], context);
|
|
4194
|
+
if (rawUnit instanceof FormulaError) return rawUnit;
|
|
4195
|
+
const unit = String(rawUnit).toUpperCase();
|
|
4196
|
+
if (startDate > endDate) return new FormulaError("#NUM!", "DATEDIF start date must be <= end date");
|
|
4197
|
+
switch (unit) {
|
|
4198
|
+
case "Y": {
|
|
4199
|
+
let years = endDate.getFullYear() - startDate.getFullYear();
|
|
4200
|
+
if (endDate.getMonth() < startDate.getMonth() || endDate.getMonth() === startDate.getMonth() && endDate.getDate() < startDate.getDate()) {
|
|
4201
|
+
years--;
|
|
4202
|
+
}
|
|
4203
|
+
return years;
|
|
4204
|
+
}
|
|
4205
|
+
case "M": {
|
|
4206
|
+
let months = (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth();
|
|
4207
|
+
if (endDate.getDate() < startDate.getDate()) months--;
|
|
4208
|
+
return months;
|
|
4209
|
+
}
|
|
4210
|
+
case "D":
|
|
4211
|
+
return Math.floor((endDate.getTime() - startDate.getTime()) / 864e5);
|
|
4212
|
+
default:
|
|
4213
|
+
return new FormulaError("#VALUE!", "DATEDIF unit must be Y, M, or D");
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
});
|
|
4217
|
+
registry.set("EDATE", {
|
|
4218
|
+
minArgs: 2,
|
|
4219
|
+
maxArgs: 2,
|
|
4220
|
+
evaluate(args, context, evaluator) {
|
|
4221
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4222
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4223
|
+
const date = toDate(rawDate);
|
|
4224
|
+
if (date instanceof FormulaError) return date;
|
|
4225
|
+
const rawMonths = evaluator.evaluate(args[1], context);
|
|
4226
|
+
if (rawMonths instanceof FormulaError) return rawMonths;
|
|
4227
|
+
const months = toNumber(rawMonths);
|
|
4228
|
+
if (months instanceof FormulaError) return months;
|
|
4229
|
+
const result = new Date(date);
|
|
4230
|
+
result.setMonth(result.getMonth() + Math.trunc(months));
|
|
4231
|
+
return result;
|
|
4232
|
+
}
|
|
4233
|
+
});
|
|
4234
|
+
registry.set("EOMONTH", {
|
|
4235
|
+
minArgs: 2,
|
|
4236
|
+
maxArgs: 2,
|
|
4237
|
+
evaluate(args, context, evaluator) {
|
|
4238
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4239
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4240
|
+
const date = toDate(rawDate);
|
|
4241
|
+
if (date instanceof FormulaError) return date;
|
|
4242
|
+
const rawMonths = evaluator.evaluate(args[1], context);
|
|
4243
|
+
if (rawMonths instanceof FormulaError) return rawMonths;
|
|
4244
|
+
const months = toNumber(rawMonths);
|
|
4245
|
+
if (months instanceof FormulaError) return months;
|
|
4246
|
+
const result = new Date(date.getFullYear(), date.getMonth() + Math.trunc(months) + 1, 0);
|
|
4247
|
+
return result;
|
|
4248
|
+
}
|
|
4249
|
+
});
|
|
4250
|
+
registry.set("WEEKDAY", {
|
|
4251
|
+
minArgs: 1,
|
|
4252
|
+
maxArgs: 2,
|
|
4253
|
+
evaluate(args, context, evaluator) {
|
|
4254
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
4255
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
4256
|
+
const date = toDate(rawDate);
|
|
4257
|
+
if (date instanceof FormulaError) return date;
|
|
4258
|
+
let returnType = 1;
|
|
4259
|
+
if (args.length >= 2) {
|
|
4260
|
+
const rawRT = evaluator.evaluate(args[1], context);
|
|
4261
|
+
if (rawRT instanceof FormulaError) return rawRT;
|
|
4262
|
+
const rt = toNumber(rawRT);
|
|
4263
|
+
if (rt instanceof FormulaError) return rt;
|
|
4264
|
+
returnType = Math.trunc(rt);
|
|
4265
|
+
}
|
|
4266
|
+
const day = date.getDay();
|
|
4267
|
+
switch (returnType) {
|
|
4268
|
+
case 1:
|
|
4269
|
+
return day + 1;
|
|
4270
|
+
// 1=Sun, 7=Sat
|
|
4271
|
+
case 2:
|
|
4272
|
+
return day === 0 ? 7 : day;
|
|
4273
|
+
// 1=Mon, 7=Sun
|
|
4274
|
+
case 3:
|
|
4275
|
+
return day === 0 ? 6 : day - 1;
|
|
4276
|
+
// 0=Mon, 6=Sun
|
|
4277
|
+
default:
|
|
4278
|
+
return new FormulaError("#VALUE!", "WEEKDAY return_type must be 1, 2, or 3");
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
registry.set("HOUR", {
|
|
4283
|
+
minArgs: 1,
|
|
4284
|
+
maxArgs: 1,
|
|
4285
|
+
evaluate(args, context, evaluator) {
|
|
4286
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4287
|
+
if (val instanceof FormulaError) return val;
|
|
4288
|
+
const date = toDate(val);
|
|
4289
|
+
if (date instanceof FormulaError) return date;
|
|
4290
|
+
return date.getHours();
|
|
4291
|
+
}
|
|
4292
|
+
});
|
|
4293
|
+
registry.set("MINUTE", {
|
|
4294
|
+
minArgs: 1,
|
|
4295
|
+
maxArgs: 1,
|
|
4296
|
+
evaluate(args, context, evaluator) {
|
|
4297
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4298
|
+
if (val instanceof FormulaError) return val;
|
|
4299
|
+
const date = toDate(val);
|
|
4300
|
+
if (date instanceof FormulaError) return date;
|
|
4301
|
+
return date.getMinutes();
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
4304
|
+
registry.set("SECOND", {
|
|
4305
|
+
minArgs: 1,
|
|
4306
|
+
maxArgs: 1,
|
|
4307
|
+
evaluate(args, context, evaluator) {
|
|
4308
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4309
|
+
if (val instanceof FormulaError) return val;
|
|
4310
|
+
const date = toDate(val);
|
|
4311
|
+
if (date instanceof FormulaError) return date;
|
|
4312
|
+
return date.getSeconds();
|
|
4313
|
+
}
|
|
4314
|
+
});
|
|
4315
|
+
registry.set("NETWORKDAYS", {
|
|
4316
|
+
minArgs: 2,
|
|
4317
|
+
maxArgs: 2,
|
|
4318
|
+
evaluate(args, context, evaluator) {
|
|
4319
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
4320
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
4321
|
+
const startDate = toDate(rawStart);
|
|
4322
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
4323
|
+
const rawEnd = evaluator.evaluate(args[1], context);
|
|
4324
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
4325
|
+
const endDate = toDate(rawEnd);
|
|
4326
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
4327
|
+
const start = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
4328
|
+
const end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
|
4329
|
+
const sign = end >= start ? 1 : -1;
|
|
4330
|
+
const [from, to] = sign === 1 ? [start, end] : [end, start];
|
|
4331
|
+
let count = 0;
|
|
4332
|
+
const current = new Date(from);
|
|
4333
|
+
while (current <= to) {
|
|
4334
|
+
const day = current.getDay();
|
|
4335
|
+
if (day !== 0 && day !== 6) count++;
|
|
4336
|
+
current.setDate(current.getDate() + 1);
|
|
4337
|
+
}
|
|
4338
|
+
return count * sign;
|
|
4339
|
+
}
|
|
4340
|
+
});
|
|
4341
|
+
}
|
|
4342
|
+
function makeCriteria(op, value) {
|
|
4343
|
+
return { op, value, valueLower: typeof value === "string" ? value.toLowerCase() : null };
|
|
4344
|
+
}
|
|
4345
|
+
function parseCriteria(criteria) {
|
|
4346
|
+
if (typeof criteria === "number") {
|
|
4347
|
+
return makeCriteria("=", criteria);
|
|
4348
|
+
}
|
|
4349
|
+
if (typeof criteria !== "string") {
|
|
4350
|
+
return makeCriteria("=", criteria);
|
|
4351
|
+
}
|
|
4352
|
+
const str = criteria.trim();
|
|
4353
|
+
if (str.startsWith(">=")) {
|
|
4354
|
+
return makeCriteria(">=", parseNumericOrString(str.substring(2).trim()));
|
|
4355
|
+
}
|
|
4356
|
+
if (str.startsWith("<=")) {
|
|
4357
|
+
return makeCriteria("<=", parseNumericOrString(str.substring(2).trim()));
|
|
4358
|
+
}
|
|
4359
|
+
if (str.startsWith("<>")) {
|
|
4360
|
+
return makeCriteria("<>", parseNumericOrString(str.substring(2).trim()));
|
|
4361
|
+
}
|
|
4362
|
+
if (str.startsWith(">")) {
|
|
4363
|
+
return makeCriteria(">", parseNumericOrString(str.substring(1).trim()));
|
|
4364
|
+
}
|
|
4365
|
+
if (str.startsWith("<")) {
|
|
4366
|
+
return makeCriteria("<", parseNumericOrString(str.substring(1).trim()));
|
|
4367
|
+
}
|
|
4368
|
+
if (str.startsWith("=")) {
|
|
4369
|
+
return makeCriteria("=", parseNumericOrString(str.substring(1).trim()));
|
|
4370
|
+
}
|
|
4371
|
+
return makeCriteria("=", parseNumericOrString(str));
|
|
4372
|
+
}
|
|
4373
|
+
function parseNumericOrString(s) {
|
|
4374
|
+
const n = Number(s);
|
|
4375
|
+
if (!isNaN(n) && s !== "") return n;
|
|
4376
|
+
return s;
|
|
4377
|
+
}
|
|
4378
|
+
function matchesCriteria(cellValue, criteria) {
|
|
4379
|
+
const { op, value } = criteria;
|
|
4380
|
+
let comparableCell = cellValue;
|
|
4381
|
+
if (typeof value === "number" && typeof cellValue !== "number") {
|
|
4382
|
+
const n = toNumber(cellValue);
|
|
4383
|
+
if (n instanceof FormulaError) return false;
|
|
4384
|
+
comparableCell = n;
|
|
4385
|
+
}
|
|
4386
|
+
if (typeof comparableCell === "string" && criteria.valueLower !== null) {
|
|
4387
|
+
const a = comparableCell.toLowerCase();
|
|
4388
|
+
const b = criteria.valueLower;
|
|
4389
|
+
switch (op) {
|
|
4390
|
+
case "=":
|
|
4391
|
+
return a === b;
|
|
4392
|
+
case "<>":
|
|
4393
|
+
return a !== b;
|
|
4394
|
+
case ">":
|
|
4395
|
+
return a > b;
|
|
4396
|
+
case "<":
|
|
4397
|
+
return a < b;
|
|
4398
|
+
case ">=":
|
|
4399
|
+
return a >= b;
|
|
4400
|
+
case "<=":
|
|
4401
|
+
return a <= b;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
if (typeof comparableCell === "number" && typeof value === "number") {
|
|
4405
|
+
switch (op) {
|
|
4406
|
+
case "=":
|
|
4407
|
+
return comparableCell === value;
|
|
4408
|
+
case "<>":
|
|
4409
|
+
return comparableCell !== value;
|
|
4410
|
+
case ">":
|
|
4411
|
+
return comparableCell > value;
|
|
4412
|
+
case "<":
|
|
4413
|
+
return comparableCell < value;
|
|
4414
|
+
case ">=":
|
|
4415
|
+
return comparableCell >= value;
|
|
4416
|
+
case "<=":
|
|
4417
|
+
return comparableCell <= value;
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
if (op === "=") return comparableCell === value;
|
|
4421
|
+
if (op === "<>") return comparableCell !== value;
|
|
4422
|
+
return false;
|
|
4423
|
+
}
|
|
4424
|
+
function registerStatsFunctions(registry) {
|
|
4425
|
+
registry.set("SUMIF", {
|
|
4426
|
+
minArgs: 2,
|
|
4427
|
+
maxArgs: 3,
|
|
4428
|
+
evaluate(args, context, evaluator) {
|
|
4429
|
+
if (args[0].kind !== "range") {
|
|
4430
|
+
return new FormulaError("#VALUE!", "SUMIF range must be a cell range");
|
|
4431
|
+
}
|
|
4432
|
+
const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4433
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4434
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4435
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4436
|
+
let sumRange;
|
|
4437
|
+
if (args.length >= 3) {
|
|
4438
|
+
if (args[2].kind !== "range") {
|
|
4439
|
+
return new FormulaError("#VALUE!", "SUMIF sum_range must be a cell range");
|
|
4440
|
+
}
|
|
4441
|
+
sumRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
4442
|
+
} else {
|
|
4443
|
+
sumRange = criteriaRange;
|
|
4444
|
+
}
|
|
4445
|
+
let sum = 0;
|
|
4446
|
+
for (let r = 0; r < criteriaRange.length; r++) {
|
|
4447
|
+
for (let c = 0; c < criteriaRange[r].length; c++) {
|
|
4448
|
+
if (matchesCriteria(criteriaRange[r][c], criteria)) {
|
|
4449
|
+
const sumVal = sumRange[r] && sumRange[r][c] !== void 0 ? sumRange[r][c] : null;
|
|
4450
|
+
const n = toNumber(sumVal);
|
|
4451
|
+
if (typeof n === "number") {
|
|
4452
|
+
sum += n;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
return sum;
|
|
4458
|
+
}
|
|
4459
|
+
});
|
|
4460
|
+
registry.set("COUNTIF", {
|
|
4461
|
+
minArgs: 2,
|
|
4462
|
+
maxArgs: 2,
|
|
4463
|
+
evaluate(args, context, evaluator) {
|
|
4464
|
+
if (args[0].kind !== "range") {
|
|
4465
|
+
return new FormulaError("#VALUE!", "COUNTIF range must be a cell range");
|
|
4466
|
+
}
|
|
4467
|
+
const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4468
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4469
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4470
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4471
|
+
let count = 0;
|
|
4472
|
+
for (let r = 0; r < rangeData.length; r++) {
|
|
4473
|
+
for (let c = 0; c < rangeData[r].length; c++) {
|
|
4474
|
+
if (matchesCriteria(rangeData[r][c], criteria)) {
|
|
4475
|
+
count++;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
return count;
|
|
4480
|
+
}
|
|
4481
|
+
});
|
|
4482
|
+
registry.set("AVERAGEIF", {
|
|
4483
|
+
minArgs: 2,
|
|
4484
|
+
maxArgs: 3,
|
|
4485
|
+
evaluate(args, context, evaluator) {
|
|
4486
|
+
if (args[0].kind !== "range") {
|
|
4487
|
+
return new FormulaError("#VALUE!", "AVERAGEIF range must be a cell range");
|
|
4488
|
+
}
|
|
4489
|
+
const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4490
|
+
const rawCriteria = evaluator.evaluate(args[1], context);
|
|
4491
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4492
|
+
const criteria = parseCriteria(rawCriteria);
|
|
4493
|
+
let avgRange;
|
|
4494
|
+
if (args.length >= 3) {
|
|
4495
|
+
if (args[2].kind !== "range") {
|
|
4496
|
+
return new FormulaError("#VALUE!", "AVERAGEIF avg_range must be a cell range");
|
|
4497
|
+
}
|
|
4498
|
+
avgRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
|
|
4499
|
+
} else {
|
|
4500
|
+
avgRange = criteriaRange;
|
|
4501
|
+
}
|
|
4502
|
+
let sum = 0;
|
|
4503
|
+
let count = 0;
|
|
4504
|
+
for (let r = 0; r < criteriaRange.length; r++) {
|
|
4505
|
+
for (let c = 0; c < criteriaRange[r].length; c++) {
|
|
4506
|
+
if (matchesCriteria(criteriaRange[r][c], criteria)) {
|
|
4507
|
+
const avgVal = avgRange[r] && avgRange[r][c] !== void 0 ? avgRange[r][c] : null;
|
|
4508
|
+
const n = toNumber(avgVal);
|
|
4509
|
+
if (typeof n === "number") {
|
|
4510
|
+
sum += n;
|
|
4511
|
+
count++;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIF");
|
|
4517
|
+
return sum / count;
|
|
4518
|
+
}
|
|
4519
|
+
});
|
|
4520
|
+
registry.set("SUMIFS", {
|
|
4521
|
+
minArgs: 3,
|
|
4522
|
+
maxArgs: -1,
|
|
4523
|
+
evaluate(args, context, evaluator) {
|
|
4524
|
+
if ((args.length - 1) % 2 !== 0) {
|
|
4525
|
+
return new FormulaError("#VALUE!", "SUMIFS requires sum_range + pairs of criteria_range, criteria");
|
|
4526
|
+
}
|
|
4527
|
+
if (args[0].kind !== "range") {
|
|
4528
|
+
return new FormulaError("#VALUE!", "SUMIFS sum_range must be a cell range");
|
|
4529
|
+
}
|
|
4530
|
+
const sumRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4531
|
+
const pairs = [];
|
|
4532
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4533
|
+
const rangeArg = args[i];
|
|
4534
|
+
if (rangeArg.kind !== "range") {
|
|
4535
|
+
return new FormulaError("#VALUE!", "SUMIFS criteria_range must be a cell range");
|
|
4536
|
+
}
|
|
4537
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4538
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4539
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4540
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4541
|
+
}
|
|
4542
|
+
let sum = 0;
|
|
4543
|
+
for (let r = 0; r < sumRange.length; r++) {
|
|
4544
|
+
for (let c = 0; c < sumRange[r].length; c++) {
|
|
4545
|
+
let allMatch = true;
|
|
4546
|
+
for (const pair of pairs) {
|
|
4547
|
+
const cellVal = pair.range[r]?.[c];
|
|
4548
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4549
|
+
allMatch = false;
|
|
4550
|
+
break;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
if (allMatch) {
|
|
4554
|
+
const n = toNumber(sumRange[r][c]);
|
|
4555
|
+
if (typeof n === "number") sum += n;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return sum;
|
|
4560
|
+
}
|
|
4561
|
+
});
|
|
4562
|
+
registry.set("COUNTIFS", {
|
|
4563
|
+
minArgs: 2,
|
|
4564
|
+
maxArgs: -1,
|
|
4565
|
+
evaluate(args, context, evaluator) {
|
|
4566
|
+
if (args.length % 2 !== 0) {
|
|
4567
|
+
return new FormulaError("#VALUE!", "COUNTIFS requires pairs of criteria_range, criteria");
|
|
4568
|
+
}
|
|
4569
|
+
const pairs = [];
|
|
4570
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
4571
|
+
const rangeArg = args[i];
|
|
4572
|
+
if (rangeArg.kind !== "range") {
|
|
4573
|
+
return new FormulaError("#VALUE!", "COUNTIFS criteria_range must be a cell range");
|
|
4574
|
+
}
|
|
4575
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4576
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4577
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4578
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4579
|
+
}
|
|
4580
|
+
const firstRange = pairs[0].range;
|
|
4581
|
+
let count = 0;
|
|
4582
|
+
for (let r = 0; r < firstRange.length; r++) {
|
|
4583
|
+
for (let c = 0; c < firstRange[r].length; c++) {
|
|
4584
|
+
let allMatch = true;
|
|
4585
|
+
for (const pair of pairs) {
|
|
4586
|
+
const cellVal = pair.range[r]?.[c];
|
|
4587
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4588
|
+
allMatch = false;
|
|
4589
|
+
break;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
if (allMatch) count++;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
return count;
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
registry.set("AVERAGEIFS", {
|
|
4599
|
+
minArgs: 3,
|
|
4600
|
+
maxArgs: -1,
|
|
4601
|
+
evaluate(args, context, evaluator) {
|
|
4602
|
+
if ((args.length - 1) % 2 !== 0) {
|
|
4603
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS requires avg_range + pairs of criteria_range, criteria");
|
|
4604
|
+
}
|
|
4605
|
+
if (args[0].kind !== "range") {
|
|
4606
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS avg_range must be a cell range");
|
|
4607
|
+
}
|
|
4608
|
+
const avgRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
4609
|
+
const pairs = [];
|
|
4610
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4611
|
+
const rangeArg = args[i];
|
|
4612
|
+
if (rangeArg.kind !== "range") {
|
|
4613
|
+
return new FormulaError("#VALUE!", "AVERAGEIFS criteria_range must be a cell range");
|
|
4614
|
+
}
|
|
4615
|
+
const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
|
|
4616
|
+
const rawCriteria = evaluator.evaluate(args[i + 1], context);
|
|
4617
|
+
if (rawCriteria instanceof FormulaError) return rawCriteria;
|
|
4618
|
+
pairs.push({ range, criteria: parseCriteria(rawCriteria) });
|
|
4619
|
+
}
|
|
4620
|
+
let sum = 0;
|
|
4621
|
+
let count = 0;
|
|
4622
|
+
for (let r = 0; r < avgRange.length; r++) {
|
|
4623
|
+
for (let c = 0; c < avgRange[r].length; c++) {
|
|
4624
|
+
let allMatch = true;
|
|
4625
|
+
for (const pair of pairs) {
|
|
4626
|
+
const cellVal = pair.range[r]?.[c];
|
|
4627
|
+
if (!matchesCriteria(cellVal, pair.criteria)) {
|
|
4628
|
+
allMatch = false;
|
|
4629
|
+
break;
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
if (allMatch) {
|
|
4633
|
+
const n = toNumber(avgRange[r][c]);
|
|
4634
|
+
if (typeof n === "number") {
|
|
4635
|
+
sum += n;
|
|
4636
|
+
count++;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIFS");
|
|
4642
|
+
return sum / count;
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4646
|
+
function registerInfoFunctions(registry) {
|
|
4647
|
+
registry.set("ISBLANK", {
|
|
4648
|
+
minArgs: 1,
|
|
4649
|
+
maxArgs: 1,
|
|
4650
|
+
evaluate(args, context, evaluator) {
|
|
4651
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4652
|
+
return val === null || val === void 0 || val === "";
|
|
4653
|
+
}
|
|
4654
|
+
});
|
|
4655
|
+
registry.set("ISNUMBER", {
|
|
4656
|
+
minArgs: 1,
|
|
4657
|
+
maxArgs: 1,
|
|
4658
|
+
evaluate(args, context, evaluator) {
|
|
4659
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4660
|
+
return typeof val === "number" && !isNaN(val);
|
|
4661
|
+
}
|
|
4662
|
+
});
|
|
4663
|
+
registry.set("ISTEXT", {
|
|
4664
|
+
minArgs: 1,
|
|
4665
|
+
maxArgs: 1,
|
|
4666
|
+
evaluate(args, context, evaluator) {
|
|
4667
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4668
|
+
return typeof val === "string";
|
|
4669
|
+
}
|
|
4670
|
+
});
|
|
4671
|
+
registry.set("ISERROR", {
|
|
4672
|
+
minArgs: 1,
|
|
4673
|
+
maxArgs: 1,
|
|
4674
|
+
evaluate(args, context, evaluator) {
|
|
4675
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4676
|
+
return val instanceof FormulaError;
|
|
4677
|
+
}
|
|
4678
|
+
});
|
|
4679
|
+
registry.set("ISNA", {
|
|
4680
|
+
minArgs: 1,
|
|
4681
|
+
maxArgs: 1,
|
|
4682
|
+
evaluate(args, context, evaluator) {
|
|
4683
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4684
|
+
return val instanceof FormulaError && val.type === "#N/A";
|
|
4685
|
+
}
|
|
4686
|
+
});
|
|
4687
|
+
registry.set("TYPE", {
|
|
4688
|
+
minArgs: 1,
|
|
4689
|
+
maxArgs: 1,
|
|
4690
|
+
evaluate(args, context, evaluator) {
|
|
4691
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4692
|
+
if (val instanceof FormulaError) return 16;
|
|
4693
|
+
if (typeof val === "number") return 1;
|
|
4694
|
+
if (typeof val === "string") return 2;
|
|
4695
|
+
if (typeof val === "boolean") return 4;
|
|
4696
|
+
if (val === null || val === void 0) return 1;
|
|
4697
|
+
return 1;
|
|
4698
|
+
}
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
function createBuiltInFunctions() {
|
|
4702
|
+
const registry = /* @__PURE__ */ new Map();
|
|
4703
|
+
registerMathFunctions(registry);
|
|
4704
|
+
registerLogicalFunctions(registry);
|
|
4705
|
+
registerLookupFunctions(registry);
|
|
4706
|
+
registerTextFunctions(registry);
|
|
4707
|
+
registerDateFunctions(registry);
|
|
4708
|
+
registerStatsFunctions(registry);
|
|
4709
|
+
registerInfoFunctions(registry);
|
|
4710
|
+
return registry;
|
|
4711
|
+
}
|
|
4712
|
+
function extractDependencies(node) {
|
|
4713
|
+
const deps = /* @__PURE__ */ new Set();
|
|
4714
|
+
function walk(n) {
|
|
4715
|
+
switch (n.kind) {
|
|
4716
|
+
case "cellRef":
|
|
4717
|
+
deps.add(toCellKey(n.address.col, n.address.row, n.address.sheet));
|
|
4718
|
+
break;
|
|
4719
|
+
case "range": {
|
|
4720
|
+
const sheet = n.start.sheet;
|
|
4721
|
+
const minRow = Math.min(n.start.row, n.end.row);
|
|
4722
|
+
const maxRow = Math.max(n.start.row, n.end.row);
|
|
4723
|
+
const minCol = Math.min(n.start.col, n.end.col);
|
|
4724
|
+
const maxCol = Math.max(n.start.col, n.end.col);
|
|
4725
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
4726
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
4727
|
+
deps.add(toCellKey(c, r, sheet));
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
break;
|
|
4731
|
+
}
|
|
4732
|
+
case "functionCall":
|
|
4733
|
+
for (const arg of n.args) walk(arg);
|
|
4734
|
+
break;
|
|
4735
|
+
case "binaryOp":
|
|
4736
|
+
walk(n.left);
|
|
4737
|
+
walk(n.right);
|
|
4738
|
+
break;
|
|
4739
|
+
case "unaryOp":
|
|
4740
|
+
walk(n.operand);
|
|
4741
|
+
break;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
walk(node);
|
|
4745
|
+
return deps;
|
|
4746
|
+
}
|
|
4747
|
+
var FormulaEngine = class {
|
|
4748
|
+
constructor(config) {
|
|
4749
|
+
this.formulas = /* @__PURE__ */ new Map();
|
|
4750
|
+
this.parsedFormulas = /* @__PURE__ */ new Map();
|
|
4751
|
+
this.values = /* @__PURE__ */ new Map();
|
|
4752
|
+
this.depGraph = new DependencyGraph();
|
|
4753
|
+
this.namedRanges = /* @__PURE__ */ new Map();
|
|
4754
|
+
this.sheetAccessors = /* @__PURE__ */ new Map();
|
|
4755
|
+
const builtIns = createBuiltInFunctions();
|
|
4756
|
+
if (config?.customFunctions) {
|
|
4757
|
+
for (const [name, fn] of Object.entries(config.customFunctions)) {
|
|
4758
|
+
builtIns.set(name.toUpperCase(), fn);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
if (config?.namedRanges) {
|
|
4762
|
+
for (const [name, ref] of Object.entries(config.namedRanges)) {
|
|
4763
|
+
this.namedRanges.set(name.toUpperCase(), ref);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
this.evaluator = new FormulaEvaluator(builtIns);
|
|
4767
|
+
this.maxChainLength = config?.maxChainLength ?? 1e3;
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Set or clear a formula for a cell.
|
|
4771
|
+
*/
|
|
4772
|
+
setFormula(col, row, formula, accessor) {
|
|
4773
|
+
const key = toCellKey(col, row);
|
|
4774
|
+
if (formula === null || formula === "") {
|
|
4775
|
+
const oldValue2 = this.values.get(key);
|
|
4776
|
+
this.formulas.delete(key);
|
|
4777
|
+
this.parsedFormulas.delete(key);
|
|
4778
|
+
this.values.delete(key);
|
|
4779
|
+
this.depGraph.removeDependencies(key);
|
|
4780
|
+
return {
|
|
4781
|
+
updatedCells: oldValue2 !== void 0 ? [{ cellKey: key, col, row, oldValue: oldValue2, newValue: void 0 }] : []
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
const expression = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
4785
|
+
let ast;
|
|
4786
|
+
try {
|
|
4787
|
+
const tokens = tokenize(expression);
|
|
4788
|
+
ast = parse(tokens, this.namedRanges);
|
|
4789
|
+
} catch (err) {
|
|
4790
|
+
const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
|
|
4791
|
+
ast = { kind: "error", error };
|
|
4792
|
+
}
|
|
4793
|
+
const deps = extractDependencies(ast);
|
|
4794
|
+
if (deps.has(key) || this.depGraph.wouldCreateCycle(key, deps)) {
|
|
4795
|
+
const oldValue2 = this.values.get(key);
|
|
4796
|
+
const circError = new FormulaError("#CIRC!", "Circular reference detected");
|
|
4797
|
+
this.formulas.set(key, formula);
|
|
4798
|
+
this.parsedFormulas.set(key, ast);
|
|
4799
|
+
this.values.set(key, circError);
|
|
4800
|
+
this.depGraph.setDependencies(key, deps);
|
|
4801
|
+
return {
|
|
4802
|
+
updatedCells: [{ cellKey: key, col, row, oldValue: oldValue2, newValue: circError }]
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
this.depGraph.setDependencies(key, deps);
|
|
4806
|
+
this.formulas.set(key, formula);
|
|
4807
|
+
this.parsedFormulas.set(key, ast);
|
|
4808
|
+
const oldValue = this.values.get(key);
|
|
4809
|
+
const context = this.createContext(accessor);
|
|
4810
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
4811
|
+
this.values.set(key, newValue);
|
|
4812
|
+
const updatedCells = [
|
|
4813
|
+
{ cellKey: key, col, row, oldValue, newValue }
|
|
4814
|
+
];
|
|
4815
|
+
const recalcOrder = this.depGraph.getRecalcOrder(key);
|
|
4816
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4817
|
+
return { updatedCells };
|
|
4818
|
+
}
|
|
4819
|
+
/**
|
|
4820
|
+
* Notify the engine that a non-formula cell's value changed.
|
|
4821
|
+
*/
|
|
4822
|
+
onCellChanged(col, row, accessor) {
|
|
4823
|
+
const key = toCellKey(col, row);
|
|
4824
|
+
const recalcOrder = this.depGraph.getRecalcOrder(key);
|
|
4825
|
+
if (recalcOrder.length === 0) return { updatedCells: [] };
|
|
4826
|
+
const updatedCells = [];
|
|
4827
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4828
|
+
return { updatedCells };
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Batch notify: multiple cells changed.
|
|
4832
|
+
*/
|
|
4833
|
+
onCellsChanged(cells, accessor) {
|
|
4834
|
+
const keys = cells.map((c) => toCellKey(c.col, c.row));
|
|
4835
|
+
const recalcOrder = this.depGraph.getRecalcOrderBatch(keys);
|
|
4836
|
+
if (recalcOrder.length === 0) return { updatedCells: [] };
|
|
4837
|
+
const updatedCells = [];
|
|
4838
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4839
|
+
return { updatedCells };
|
|
4840
|
+
}
|
|
4841
|
+
/**
|
|
4842
|
+
* Get the current computed value for a cell.
|
|
4843
|
+
*/
|
|
4844
|
+
getValue(col, row) {
|
|
4845
|
+
return this.values.get(toCellKey(col, row));
|
|
4846
|
+
}
|
|
4847
|
+
/**
|
|
4848
|
+
* Get the formula string for a cell.
|
|
4849
|
+
*/
|
|
4850
|
+
getFormula(col, row) {
|
|
4851
|
+
return this.formulas.get(toCellKey(col, row));
|
|
4852
|
+
}
|
|
4853
|
+
/**
|
|
4854
|
+
* Check if a cell has a formula.
|
|
4855
|
+
*/
|
|
4856
|
+
hasFormula(col, row) {
|
|
4857
|
+
return this.formulas.has(toCellKey(col, row));
|
|
4858
|
+
}
|
|
4859
|
+
/**
|
|
4860
|
+
* Register a custom function at runtime.
|
|
4861
|
+
*/
|
|
4862
|
+
registerFunction(name, fn) {
|
|
4863
|
+
this.evaluator.registerFunction(name, fn);
|
|
4864
|
+
}
|
|
4865
|
+
/**
|
|
4866
|
+
* Full recalculation of all formulas.
|
|
4867
|
+
*/
|
|
4868
|
+
recalcAll(accessor) {
|
|
4869
|
+
const updatedCells = [];
|
|
4870
|
+
const context = this.createContext(accessor);
|
|
4871
|
+
if (this.formulas.size === 0) return { updatedCells };
|
|
4872
|
+
const allFormulaKeys = [];
|
|
4873
|
+
for (const key of this.formulas.keys()) allFormulaKeys.push(key);
|
|
4874
|
+
const recalcOrder = this.depGraph.getRecalcOrderBatch(allFormulaKeys);
|
|
4875
|
+
const ordered = new Set(recalcOrder);
|
|
4876
|
+
for (const key of allFormulaKeys) {
|
|
4877
|
+
if (!ordered.has(key)) {
|
|
4878
|
+
const { col, row } = fromCellKey(key);
|
|
4879
|
+
const ast = this.parsedFormulas.get(key);
|
|
4880
|
+
if (!ast) continue;
|
|
4881
|
+
const oldValue = this.values.get(key);
|
|
4882
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
4883
|
+
this.values.set(key, newValue);
|
|
4884
|
+
updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
|
|
1390
4885
|
}
|
|
1391
|
-
this.redoStack.length = 0;
|
|
1392
4886
|
}
|
|
4887
|
+
this.recalcCells(recalcOrder, accessor, updatedCells);
|
|
4888
|
+
return { updatedCells };
|
|
1393
4889
|
}
|
|
1394
4890
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
* If a batch is open, accumulates into the batch instead.
|
|
4891
|
+
* Clear all formulas and cached values.
|
|
1397
4892
|
*/
|
|
1398
|
-
|
|
1399
|
-
this.
|
|
4893
|
+
clear() {
|
|
4894
|
+
this.formulas.clear();
|
|
4895
|
+
this.parsedFormulas.clear();
|
|
4896
|
+
this.values.clear();
|
|
4897
|
+
this.depGraph.clear();
|
|
1400
4898
|
}
|
|
1401
4899
|
/**
|
|
1402
|
-
*
|
|
1403
|
-
* Has no effect if a batch is already open.
|
|
4900
|
+
* Get all formula entries for serialization.
|
|
1404
4901
|
*/
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
4902
|
+
getAllFormulas() {
|
|
4903
|
+
const result = [];
|
|
4904
|
+
for (const [key, formula] of this.formulas) {
|
|
4905
|
+
const { col, row } = fromCellKey(key);
|
|
4906
|
+
result.push({ col, row, formula });
|
|
1408
4907
|
}
|
|
4908
|
+
return result;
|
|
1409
4909
|
}
|
|
1410
4910
|
/**
|
|
1411
|
-
*
|
|
1412
|
-
* Has no effect if no batch is open or if the batch is empty.
|
|
4911
|
+
* Bulk-load formulas. Recalculates everything.
|
|
1413
4912
|
*/
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
4913
|
+
loadFormulas(formulas, accessor) {
|
|
4914
|
+
this.clear();
|
|
4915
|
+
for (const { col, row, formula } of formulas) {
|
|
4916
|
+
const key = toCellKey(col, row);
|
|
4917
|
+
const expression = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
4918
|
+
let ast;
|
|
4919
|
+
try {
|
|
4920
|
+
const tokens = tokenize(expression);
|
|
4921
|
+
ast = parse(tokens, this.namedRanges);
|
|
4922
|
+
} catch (err) {
|
|
4923
|
+
const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
|
|
4924
|
+
ast = { kind: "error", error };
|
|
4925
|
+
}
|
|
4926
|
+
this.formulas.set(key, formula);
|
|
4927
|
+
this.parsedFormulas.set(key, ast);
|
|
4928
|
+
const deps = extractDependencies(ast);
|
|
4929
|
+
this.depGraph.setDependencies(key, deps);
|
|
1421
4930
|
}
|
|
1422
|
-
this.
|
|
4931
|
+
return this.recalcAll(accessor);
|
|
1423
4932
|
}
|
|
4933
|
+
// --- Named Ranges ---
|
|
1424
4934
|
/**
|
|
1425
|
-
*
|
|
1426
|
-
* Returns the batch of events (in original order) to be reversed by the caller,
|
|
1427
|
-
* or null if there is nothing to undo.
|
|
1428
|
-
*
|
|
1429
|
-
* The caller is responsible for applying the events in reverse order.
|
|
4935
|
+
* Define a named range (e.g. "Revenue" → "A1:A10").
|
|
1430
4936
|
*/
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (!lastBatch) return null;
|
|
1434
|
-
this.redoStack.push(lastBatch);
|
|
1435
|
-
return lastBatch;
|
|
4937
|
+
defineNamedRange(name, ref) {
|
|
4938
|
+
this.namedRanges.set(name.toUpperCase(), ref);
|
|
1436
4939
|
}
|
|
1437
4940
|
/**
|
|
1438
|
-
*
|
|
1439
|
-
* Returns the batch of events (in original order) to be re-applied by the caller,
|
|
1440
|
-
* or null if there is nothing to redo.
|
|
4941
|
+
* Remove a named range by name.
|
|
1441
4942
|
*/
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (!nextBatch) return null;
|
|
1445
|
-
this.history.push(nextBatch);
|
|
1446
|
-
return nextBatch;
|
|
4943
|
+
removeNamedRange(name) {
|
|
4944
|
+
this.namedRanges.delete(name.toUpperCase());
|
|
1447
4945
|
}
|
|
1448
4946
|
/**
|
|
1449
|
-
*
|
|
1450
|
-
* Does not affect any open batch — call endBatch() first if needed.
|
|
4947
|
+
* Get all named ranges as a Map (name → ref).
|
|
1451
4948
|
*/
|
|
1452
|
-
|
|
1453
|
-
this.
|
|
1454
|
-
this.redoStack = [];
|
|
4949
|
+
getNamedRanges() {
|
|
4950
|
+
return this.namedRanges;
|
|
1455
4951
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
4952
|
+
// --- Sheet Accessors ---
|
|
4953
|
+
/**
|
|
4954
|
+
* Register a data accessor for a named sheet (for cross-sheet references).
|
|
4955
|
+
*/
|
|
4956
|
+
registerSheet(name, accessor) {
|
|
4957
|
+
this.sheetAccessors.set(name, accessor);
|
|
1461
4958
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
4959
|
+
/**
|
|
4960
|
+
* Unregister a sheet accessor.
|
|
4961
|
+
*/
|
|
4962
|
+
unregisterSheet(name) {
|
|
4963
|
+
this.sheetAccessors.delete(name);
|
|
4964
|
+
}
|
|
4965
|
+
// --- Formula Auditing ---
|
|
4966
|
+
/**
|
|
4967
|
+
* Get all cells that a cell depends on (deep, transitive precedents).
|
|
4968
|
+
*/
|
|
4969
|
+
getPrecedents(col, row) {
|
|
4970
|
+
const key = toCellKey(col, row);
|
|
4971
|
+
const result = [];
|
|
4972
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4973
|
+
const queue = [];
|
|
4974
|
+
const directDeps = this.depGraph.getDependencies(key);
|
|
4975
|
+
for (const dep of directDeps) {
|
|
4976
|
+
if (!visited.has(dep)) {
|
|
4977
|
+
visited.add(dep);
|
|
4978
|
+
queue.push(dep);
|
|
4979
|
+
}
|
|
1467
4980
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
4981
|
+
let head = 0;
|
|
4982
|
+
while (head < queue.length) {
|
|
4983
|
+
const current = queue[head++];
|
|
4984
|
+
const parsed = fromCellKey(current);
|
|
4985
|
+
result.push({
|
|
4986
|
+
cellKey: current,
|
|
4987
|
+
col: parsed.col,
|
|
4988
|
+
row: parsed.row,
|
|
4989
|
+
formula: this.formulas.get(current),
|
|
4990
|
+
value: this.values.has(current) ? this.values.get(current) : void 0
|
|
4991
|
+
});
|
|
4992
|
+
const deps = this.depGraph.getDependencies(current);
|
|
4993
|
+
for (const dep of deps) {
|
|
4994
|
+
if (!visited.has(dep)) {
|
|
4995
|
+
visited.add(dep);
|
|
4996
|
+
queue.push(dep);
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
1470
4999
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
5000
|
+
return result;
|
|
5001
|
+
}
|
|
5002
|
+
/**
|
|
5003
|
+
* Get all cells that depend on this cell (deep, transitive dependents).
|
|
5004
|
+
*/
|
|
5005
|
+
getDependents(col, row) {
|
|
5006
|
+
const key = toCellKey(col, row);
|
|
5007
|
+
const result = [];
|
|
5008
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5009
|
+
const queue = [];
|
|
5010
|
+
const directDeps = this.depGraph.getDependents(key);
|
|
5011
|
+
for (const dep of directDeps) {
|
|
5012
|
+
if (!visited.has(dep)) {
|
|
5013
|
+
visited.add(dep);
|
|
5014
|
+
queue.push(dep);
|
|
5015
|
+
}
|
|
1476
5016
|
}
|
|
5017
|
+
let head = 0;
|
|
5018
|
+
while (head < queue.length) {
|
|
5019
|
+
const current = queue[head++];
|
|
5020
|
+
const parsed = fromCellKey(current);
|
|
5021
|
+
result.push({
|
|
5022
|
+
cellKey: current,
|
|
5023
|
+
col: parsed.col,
|
|
5024
|
+
row: parsed.row,
|
|
5025
|
+
formula: this.formulas.get(current),
|
|
5026
|
+
value: this.values.has(current) ? this.values.get(current) : void 0
|
|
5027
|
+
});
|
|
5028
|
+
const deps = this.depGraph.getDependents(current);
|
|
5029
|
+
for (const dep of deps) {
|
|
5030
|
+
if (!visited.has(dep)) {
|
|
5031
|
+
visited.add(dep);
|
|
5032
|
+
queue.push(dep);
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
return result;
|
|
1477
5037
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
5038
|
+
/**
|
|
5039
|
+
* Get a full audit trail for a cell: target + precedents + dependents.
|
|
5040
|
+
*/
|
|
5041
|
+
getAuditTrail(col, row) {
|
|
5042
|
+
const key = toCellKey(col, row);
|
|
5043
|
+
const target = {
|
|
5044
|
+
cellKey: key,
|
|
5045
|
+
col,
|
|
5046
|
+
row,
|
|
5047
|
+
formula: this.formulas.get(key),
|
|
5048
|
+
value: this.values.has(key) ? this.values.get(key) : void 0
|
|
5049
|
+
};
|
|
5050
|
+
return {
|
|
5051
|
+
target,
|
|
5052
|
+
precedents: this.getPrecedents(col, row),
|
|
5053
|
+
dependents: this.getDependents(col, row)
|
|
5054
|
+
};
|
|
1486
5055
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
)
|
|
1503
|
-
|
|
5056
|
+
// --- Private methods ---
|
|
5057
|
+
createContext(accessor) {
|
|
5058
|
+
const contextNow = /* @__PURE__ */ new Date();
|
|
5059
|
+
return {
|
|
5060
|
+
getCellValue: (addr) => {
|
|
5061
|
+
const key = toCellKey(addr.col, addr.row, addr.sheet);
|
|
5062
|
+
const cached = this.values.get(key);
|
|
5063
|
+
if (cached !== void 0) return cached;
|
|
5064
|
+
if (addr.sheet) {
|
|
5065
|
+
const sheetAccessor = this.sheetAccessors.get(addr.sheet);
|
|
5066
|
+
if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
|
|
5067
|
+
return sheetAccessor.getCellValue(addr.col, addr.row);
|
|
5068
|
+
}
|
|
5069
|
+
return accessor.getCellValue(addr.col, addr.row);
|
|
5070
|
+
},
|
|
5071
|
+
getRangeValues: (range) => {
|
|
5072
|
+
const result = [];
|
|
5073
|
+
const sheet = range.start.sheet;
|
|
5074
|
+
const rangeAccessor = sheet ? this.sheetAccessors.get(sheet) : accessor;
|
|
5075
|
+
if (sheet && !rangeAccessor) {
|
|
5076
|
+
return [[new FormulaError("#REF!", `Unknown sheet: ${sheet}`)]];
|
|
5077
|
+
}
|
|
5078
|
+
const minRow = Math.min(range.start.row, range.end.row);
|
|
5079
|
+
const maxRow = Math.max(range.start.row, range.end.row);
|
|
5080
|
+
const minCol = Math.min(range.start.col, range.end.col);
|
|
5081
|
+
const maxCol = Math.max(range.start.col, range.end.col);
|
|
5082
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
5083
|
+
const row = [];
|
|
5084
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
5085
|
+
const key = toCellKey(c, r, sheet);
|
|
5086
|
+
const cached = this.values.get(key);
|
|
5087
|
+
if (cached !== void 0) {
|
|
5088
|
+
row.push(cached);
|
|
5089
|
+
} else {
|
|
5090
|
+
row.push(rangeAccessor.getCellValue(c, r));
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
result.push(row);
|
|
5094
|
+
}
|
|
5095
|
+
return result;
|
|
5096
|
+
},
|
|
5097
|
+
now: () => contextNow
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
recalcCells(order, accessor, updatedCells) {
|
|
5101
|
+
const context = this.createContext(accessor);
|
|
5102
|
+
let count = 0;
|
|
5103
|
+
for (const key of order) {
|
|
5104
|
+
if (count++ > this.maxChainLength) {
|
|
5105
|
+
const { col: col2, row: row2 } = fromCellKey(key);
|
|
5106
|
+
const oldValue2 = this.values.get(key);
|
|
5107
|
+
const circError = new FormulaError("#CIRC!", "Dependency chain too long");
|
|
5108
|
+
this.values.set(key, circError);
|
|
5109
|
+
updatedCells.push({ cellKey: key, col: col2, row: row2, oldValue: oldValue2, newValue: circError });
|
|
5110
|
+
continue;
|
|
5111
|
+
}
|
|
5112
|
+
const ast = this.parsedFormulas.get(key);
|
|
5113
|
+
if (!ast) continue;
|
|
5114
|
+
const { col, row } = fromCellKey(key);
|
|
5115
|
+
const oldValue = this.values.get(key);
|
|
5116
|
+
const newValue = this.evaluator.evaluate(ast, context);
|
|
5117
|
+
this.values.set(key, newValue);
|
|
5118
|
+
updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
|
|
1504
5119
|
}
|
|
1505
|
-
ids.add(id);
|
|
1506
5120
|
}
|
|
1507
|
-
}
|
|
1508
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1509
|
-
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
5121
|
+
};
|
|
1510
5122
|
function useLatestRef(value) {
|
|
1511
5123
|
const ref = useRef(value);
|
|
1512
5124
|
ref.current = value;
|
|
@@ -1569,8 +5181,8 @@ function useFilterOptions(dataSource, fields) {
|
|
|
1569
5181
|
function resolveCellDisplayContent2(col, item, displayValue) {
|
|
1570
5182
|
return resolveCellDisplayContent(col, item, displayValue);
|
|
1571
5183
|
}
|
|
1572
|
-
function resolveCellStyle2(col, item) {
|
|
1573
|
-
return resolveCellStyle(col, item);
|
|
5184
|
+
function resolveCellStyle2(col, item, displayValue) {
|
|
5185
|
+
return resolveCellStyle(col, item, displayValue);
|
|
1574
5186
|
}
|
|
1575
5187
|
function buildInlineEditorProps2(item, col, descriptor, callbacks) {
|
|
1576
5188
|
const result = buildInlineEditorProps(item, col, descriptor, callbacks);
|
|
@@ -1584,6 +5196,7 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
|
|
|
1584
5196
|
return {
|
|
1585
5197
|
"data-row-index": descriptor.rowIndex,
|
|
1586
5198
|
"data-col-index": descriptor.globalColIndex,
|
|
5199
|
+
...descriptor.isActive ? { "data-active-cell": "true" } : {},
|
|
1587
5200
|
...descriptor.isInRange ? { "data-in-range": "true" } : {},
|
|
1588
5201
|
tabIndex: descriptor.isActive ? 0 : -1,
|
|
1589
5202
|
onMouseDown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
|
|
@@ -1595,6 +5208,127 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
|
|
|
1595
5208
|
} : {}
|
|
1596
5209
|
};
|
|
1597
5210
|
}
|
|
5211
|
+
var NOOP_RESULT = {
|
|
5212
|
+
getFormulaValue: () => void 0,
|
|
5213
|
+
hasFormula: () => false,
|
|
5214
|
+
getFormula: () => void 0,
|
|
5215
|
+
setFormula: () => {
|
|
5216
|
+
},
|
|
5217
|
+
onCellChanged: () => {
|
|
5218
|
+
},
|
|
5219
|
+
getPrecedents: () => [],
|
|
5220
|
+
getDependents: () => [],
|
|
5221
|
+
getAuditTrail: () => null,
|
|
5222
|
+
enabled: false
|
|
5223
|
+
};
|
|
5224
|
+
function useFormulaEngine(params) {
|
|
5225
|
+
const {
|
|
5226
|
+
formulas,
|
|
5227
|
+
items,
|
|
5228
|
+
flatColumns,
|
|
5229
|
+
initialFormulas,
|
|
5230
|
+
onFormulaRecalc,
|
|
5231
|
+
formulaFunctions,
|
|
5232
|
+
namedRanges,
|
|
5233
|
+
sheets
|
|
5234
|
+
} = params;
|
|
5235
|
+
const itemsRef = useLatestRef(items);
|
|
5236
|
+
const flatColumnsRef = useLatestRef(flatColumns);
|
|
5237
|
+
const onFormulaRecalcRef = useLatestRef(onFormulaRecalc);
|
|
5238
|
+
const engineRef = useRef(null);
|
|
5239
|
+
if (formulas && !engineRef.current) {
|
|
5240
|
+
engineRef.current = new FormulaEngine({
|
|
5241
|
+
customFunctions: formulaFunctions,
|
|
5242
|
+
namedRanges
|
|
5243
|
+
});
|
|
5244
|
+
} else if (!formulas && engineRef.current) {
|
|
5245
|
+
engineRef.current = null;
|
|
5246
|
+
}
|
|
5247
|
+
useEffect(() => {
|
|
5248
|
+
if (!engineRef.current || !sheets) return;
|
|
5249
|
+
for (const [name, accessor] of Object.entries(sheets)) {
|
|
5250
|
+
engineRef.current.registerSheet(name, accessor);
|
|
5251
|
+
}
|
|
5252
|
+
return () => {
|
|
5253
|
+
if (!engineRef.current || !sheets) return;
|
|
5254
|
+
for (const name of Object.keys(sheets)) {
|
|
5255
|
+
engineRef.current.unregisterSheet(name);
|
|
5256
|
+
}
|
|
5257
|
+
};
|
|
5258
|
+
}, [sheets]);
|
|
5259
|
+
const createAccessor = useCallback(() => {
|
|
5260
|
+
const currentItems = itemsRef.current;
|
|
5261
|
+
const currentCols = flatColumnsRef.current;
|
|
5262
|
+
return {
|
|
5263
|
+
getCellValue: (col, row) => {
|
|
5264
|
+
if (row < 0 || row >= currentItems.length) return null;
|
|
5265
|
+
if (col < 0 || col >= currentCols.length) return null;
|
|
5266
|
+
return getCellValue(currentItems[row], currentCols[col]);
|
|
5267
|
+
},
|
|
5268
|
+
getRowCount: () => currentItems.length,
|
|
5269
|
+
getColumnCount: () => currentCols.length
|
|
5270
|
+
};
|
|
5271
|
+
}, [itemsRef, flatColumnsRef]);
|
|
5272
|
+
const initialLoadedRef = useRef(false);
|
|
5273
|
+
useEffect(() => {
|
|
5274
|
+
if (formulas && engineRef.current && initialFormulas && !initialLoadedRef.current) {
|
|
5275
|
+
initialLoadedRef.current = true;
|
|
5276
|
+
const accessor = createAccessor();
|
|
5277
|
+
const result = engineRef.current.loadFormulas(initialFormulas, accessor);
|
|
5278
|
+
if (result.updatedCells.length > 0) {
|
|
5279
|
+
onFormulaRecalcRef.current?.(result);
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
}, [formulas, initialFormulas, createAccessor, onFormulaRecalcRef]);
|
|
5283
|
+
const getFormulaValue = useCallback((col, row) => {
|
|
5284
|
+
return engineRef.current?.getValue(col, row);
|
|
5285
|
+
}, []);
|
|
5286
|
+
const hasFormula = useCallback((col, row) => {
|
|
5287
|
+
return engineRef.current?.hasFormula(col, row) ?? false;
|
|
5288
|
+
}, []);
|
|
5289
|
+
const getFormula = useCallback((col, row) => {
|
|
5290
|
+
return engineRef.current?.getFormula(col, row);
|
|
5291
|
+
}, []);
|
|
5292
|
+
const setFormula = useCallback((col, row, formula) => {
|
|
5293
|
+
if (!engineRef.current) return;
|
|
5294
|
+
const accessor = createAccessor();
|
|
5295
|
+
const result = engineRef.current.setFormula(col, row, formula, accessor);
|
|
5296
|
+
if (result.updatedCells.length > 0) {
|
|
5297
|
+
onFormulaRecalcRef.current?.(result);
|
|
5298
|
+
}
|
|
5299
|
+
}, [createAccessor, onFormulaRecalcRef]);
|
|
5300
|
+
const onCellChanged = useCallback((col, row) => {
|
|
5301
|
+
if (!engineRef.current) return;
|
|
5302
|
+
const accessor = createAccessor();
|
|
5303
|
+
const result = engineRef.current.onCellChanged(col, row, accessor);
|
|
5304
|
+
if (result.updatedCells.length > 0) {
|
|
5305
|
+
onFormulaRecalcRef.current?.(result);
|
|
5306
|
+
}
|
|
5307
|
+
}, [createAccessor, onFormulaRecalcRef]);
|
|
5308
|
+
const getPrecedents = useCallback((col, row) => {
|
|
5309
|
+
return engineRef.current?.getPrecedents(col, row) ?? [];
|
|
5310
|
+
}, []);
|
|
5311
|
+
const getDependents = useCallback((col, row) => {
|
|
5312
|
+
return engineRef.current?.getDependents(col, row) ?? [];
|
|
5313
|
+
}, []);
|
|
5314
|
+
const getAuditTrail = useCallback((col, row) => {
|
|
5315
|
+
return engineRef.current?.getAuditTrail(col, row) ?? null;
|
|
5316
|
+
}, []);
|
|
5317
|
+
return useMemo(() => {
|
|
5318
|
+
if (!formulas) return NOOP_RESULT;
|
|
5319
|
+
return {
|
|
5320
|
+
getFormulaValue,
|
|
5321
|
+
hasFormula,
|
|
5322
|
+
getFormula,
|
|
5323
|
+
setFormula,
|
|
5324
|
+
onCellChanged,
|
|
5325
|
+
getPrecedents,
|
|
5326
|
+
getDependents,
|
|
5327
|
+
getAuditTrail,
|
|
5328
|
+
enabled: true
|
|
5329
|
+
};
|
|
5330
|
+
}, [formulas, getFormulaValue, hasFormula, getFormula, setFormula, onCellChanged, getPrecedents, getDependents, getAuditTrail]);
|
|
5331
|
+
}
|
|
1598
5332
|
function useOGridPagination(params) {
|
|
1599
5333
|
const { controlledPage, controlledPageSize, defaultPageSize, onPageChange, onPageSizeChange } = params;
|
|
1600
5334
|
const [internalPage, setInternalPage] = useState(1);
|
|
@@ -1719,11 +5453,13 @@ function useOGridDataFetching(params) {
|
|
|
1719
5453
|
page,
|
|
1720
5454
|
pageSize,
|
|
1721
5455
|
onError,
|
|
1722
|
-
onFirstDataRendered
|
|
5456
|
+
onFirstDataRendered,
|
|
5457
|
+
workerSort
|
|
1723
5458
|
} = params;
|
|
1724
5459
|
const isClientSide = !isServerSide;
|
|
5460
|
+
const useWorker = workerSort === true || workerSort === "auto" && displayData.length > 5e3;
|
|
1725
5461
|
const clientItemsAndTotal = useMemo(() => {
|
|
1726
|
-
if (!isClientSide) return null;
|
|
5462
|
+
if (!isClientSide || useWorker) return null;
|
|
1727
5463
|
const rows = processClientSideData(
|
|
1728
5464
|
displayData,
|
|
1729
5465
|
columns,
|
|
@@ -1735,7 +5471,29 @@ function useOGridDataFetching(params) {
|
|
|
1735
5471
|
const start = (page - 1) * pageSize;
|
|
1736
5472
|
const paged = rows.slice(start, start + pageSize);
|
|
1737
5473
|
return { items: paged, totalCount: total };
|
|
1738
|
-
}, [isClientSide, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
|
|
5474
|
+
}, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
|
|
5475
|
+
const [asyncItems, setAsyncItems] = useState(null);
|
|
5476
|
+
const asyncIdRef = useRef(0);
|
|
5477
|
+
useEffect(() => {
|
|
5478
|
+
if (!isClientSide || !useWorker) {
|
|
5479
|
+
setAsyncItems(null);
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
const id = ++asyncIdRef.current;
|
|
5483
|
+
processClientSideDataAsync(
|
|
5484
|
+
displayData,
|
|
5485
|
+
columns,
|
|
5486
|
+
stableFilters,
|
|
5487
|
+
sort.field,
|
|
5488
|
+
sort.direction
|
|
5489
|
+
).then((rows) => {
|
|
5490
|
+
if (id !== asyncIdRef.current) return;
|
|
5491
|
+
const total = rows.length;
|
|
5492
|
+
const start = (page - 1) * pageSize;
|
|
5493
|
+
const paged = rows.slice(start, start + pageSize);
|
|
5494
|
+
setAsyncItems({ items: paged, totalCount: total });
|
|
5495
|
+
});
|
|
5496
|
+
}, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
|
|
1739
5497
|
const [serverItems, setServerItems] = useState([]);
|
|
1740
5498
|
const [serverTotalCount, setServerTotalCount] = useState(0);
|
|
1741
5499
|
const [serverLoading, setServerLoading] = useState(true);
|
|
@@ -1769,8 +5527,9 @@ function useOGridDataFetching(params) {
|
|
|
1769
5527
|
if (id === fetchIdRef.current) setServerLoading(false);
|
|
1770
5528
|
});
|
|
1771
5529
|
}, [isServerSide, page, pageSize, sort.field, sort.direction, stableFilters, refreshCounter, dataSourceRef, onErrorRef]);
|
|
1772
|
-
const
|
|
1773
|
-
const
|
|
5530
|
+
const clientResult = clientItemsAndTotal ?? asyncItems;
|
|
5531
|
+
const displayItems = isClientSide && clientResult ? clientResult.items : serverItems;
|
|
5532
|
+
const displayTotalCount = isClientSide && clientResult ? clientResult.totalCount : serverTotalCount;
|
|
1774
5533
|
const onFirstDataRenderedRef = useLatestRef(onFirstDataRendered);
|
|
1775
5534
|
const firstDataRenderedRef = useRef(false);
|
|
1776
5535
|
useEffect(() => {
|
|
@@ -1823,6 +5582,20 @@ function useSideBarState(params) {
|
|
|
1823
5582
|
// src/hooks/useOGrid.ts
|
|
1824
5583
|
var DEFAULT_PAGE_SIZE = 25;
|
|
1825
5584
|
var EMPTY_LOADING_OPTIONS2 = {};
|
|
5585
|
+
var NAME_BOX_STYLE = {
|
|
5586
|
+
fontFamily: "monospace",
|
|
5587
|
+
fontSize: "12px",
|
|
5588
|
+
fontWeight: 500,
|
|
5589
|
+
padding: "2px 8px",
|
|
5590
|
+
border: "1px solid var(--ogrid-border, #e0e0e0)",
|
|
5591
|
+
borderRadius: 3,
|
|
5592
|
+
background: "var(--ogrid-bg, #fff)",
|
|
5593
|
+
color: "var(--ogrid-fg, #242424)",
|
|
5594
|
+
minWidth: 48,
|
|
5595
|
+
textAlign: "center",
|
|
5596
|
+
lineHeight: "20px",
|
|
5597
|
+
userSelect: "none"
|
|
5598
|
+
};
|
|
1826
5599
|
function useOGrid(props, ref) {
|
|
1827
5600
|
const {
|
|
1828
5601
|
columns: columnsProp,
|
|
@@ -1865,6 +5638,7 @@ function useOGrid(props, ref) {
|
|
|
1865
5638
|
selectedRows,
|
|
1866
5639
|
onSelectionChange,
|
|
1867
5640
|
showRowNumbers,
|
|
5641
|
+
cellReferences,
|
|
1868
5642
|
statusBar,
|
|
1869
5643
|
pageSizeOptions,
|
|
1870
5644
|
sideBar,
|
|
@@ -1877,6 +5651,13 @@ function useOGrid(props, ref) {
|
|
|
1877
5651
|
virtualScroll,
|
|
1878
5652
|
rowHeight,
|
|
1879
5653
|
density = "normal",
|
|
5654
|
+
workerSort,
|
|
5655
|
+
formulas,
|
|
5656
|
+
initialFormulas,
|
|
5657
|
+
onFormulaRecalc,
|
|
5658
|
+
formulaFunctions,
|
|
5659
|
+
namedRanges,
|
|
5660
|
+
sheets,
|
|
1880
5661
|
"aria-label": ariaLabel,
|
|
1881
5662
|
"aria-labelledby": ariaLabelledBy
|
|
1882
5663
|
} = props;
|
|
@@ -1950,7 +5731,8 @@ function useOGrid(props, ref) {
|
|
|
1950
5731
|
page: paginationState.page,
|
|
1951
5732
|
pageSize: paginationState.pageSize,
|
|
1952
5733
|
onError,
|
|
1953
|
-
onFirstDataRendered
|
|
5734
|
+
onFirstDataRendered,
|
|
5735
|
+
workerSort
|
|
1954
5736
|
});
|
|
1955
5737
|
useEffect(() => {
|
|
1956
5738
|
const items = dataFetchingState.displayItems;
|
|
@@ -2008,7 +5790,13 @@ function useOGrid(props, ref) {
|
|
|
2008
5790
|
[selectedRows, onSelectionChange]
|
|
2009
5791
|
);
|
|
2010
5792
|
const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
|
|
2011
|
-
const [pinnedOverrides, setPinnedOverrides] = useState({
|
|
5793
|
+
const [pinnedOverrides, setPinnedOverrides] = useState(() => {
|
|
5794
|
+
const initial = {};
|
|
5795
|
+
for (const col of flattenColumns(columnsProp)) {
|
|
5796
|
+
if (col.pinned) initial[col.columnId] = col.pinned;
|
|
5797
|
+
}
|
|
5798
|
+
return initial;
|
|
5799
|
+
});
|
|
2012
5800
|
const handleColumnResized = useCallback(
|
|
2013
5801
|
(columnId, width) => {
|
|
2014
5802
|
setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
|
|
@@ -2176,8 +5964,22 @@ function useOGrid(props, ref) {
|
|
|
2176
5964
|
filtersState.handleFilterChange,
|
|
2177
5965
|
filtersState.clientFilterOptions
|
|
2178
5966
|
]);
|
|
5967
|
+
const formulaEngine = useFormulaEngine({
|
|
5968
|
+
formulas,
|
|
5969
|
+
items: dataFetchingState.displayItems,
|
|
5970
|
+
flatColumns: columns,
|
|
5971
|
+
initialFormulas,
|
|
5972
|
+
onFormulaRecalc,
|
|
5973
|
+
formulaFunctions,
|
|
5974
|
+
namedRanges,
|
|
5975
|
+
sheets
|
|
5976
|
+
});
|
|
2179
5977
|
const clearAllFilters = useCallback(() => filtersState.setFilters({}), [filtersState]);
|
|
2180
5978
|
const isLoadingResolved = isServerSide && dataFetchingState.serverLoading || displayLoading;
|
|
5979
|
+
const [activeCellRef, setActiveCellRef] = useState(null);
|
|
5980
|
+
const onActiveCellChange = useCallback((ref2) => {
|
|
5981
|
+
setActiveCellRef(ref2);
|
|
5982
|
+
}, []);
|
|
2181
5983
|
const dataGridProps = useMemo(() => ({
|
|
2182
5984
|
items: dataFetchingState.displayItems,
|
|
2183
5985
|
columns: columnsProp,
|
|
@@ -2202,7 +6004,10 @@ function useOGrid(props, ref) {
|
|
|
2202
6004
|
rowSelection,
|
|
2203
6005
|
selectedRows: effectiveSelectedRows,
|
|
2204
6006
|
onSelectionChange: handleSelectionChange,
|
|
2205
|
-
showRowNumbers,
|
|
6007
|
+
showRowNumbers: showRowNumbers || cellReferences,
|
|
6008
|
+
showColumnLetters: !!cellReferences,
|
|
6009
|
+
showNameBox: !!cellReferences,
|
|
6010
|
+
onActiveCellChange: cellReferences ? onActiveCellChange : void 0,
|
|
2206
6011
|
currentPage: paginationState.page,
|
|
2207
6012
|
pageSize: paginationState.pageSize,
|
|
2208
6013
|
statusBar: statusBarConfig,
|
|
@@ -2227,7 +6032,16 @@ function useOGrid(props, ref) {
|
|
|
2227
6032
|
onClearAll: clearAllFilters,
|
|
2228
6033
|
message: emptyState?.message,
|
|
2229
6034
|
render: emptyState?.render
|
|
2230
|
-
}
|
|
6035
|
+
},
|
|
6036
|
+
formulas,
|
|
6037
|
+
getFormulaValue: formulaEngine.enabled ? formulaEngine.getFormulaValue : void 0,
|
|
6038
|
+
hasFormula: formulaEngine.enabled ? formulaEngine.hasFormula : void 0,
|
|
6039
|
+
getFormula: formulaEngine.enabled ? formulaEngine.getFormula : void 0,
|
|
6040
|
+
setFormula: formulaEngine.enabled ? formulaEngine.setFormula : void 0,
|
|
6041
|
+
onFormulaCellChanged: formulaEngine.enabled ? formulaEngine.onCellChanged : void 0,
|
|
6042
|
+
getPrecedents: formulaEngine.enabled ? formulaEngine.getPrecedents : void 0,
|
|
6043
|
+
getDependents: formulaEngine.enabled ? formulaEngine.getDependents : void 0,
|
|
6044
|
+
getAuditTrail: formulaEngine.enabled ? formulaEngine.getAuditTrail : void 0
|
|
2231
6045
|
}), [
|
|
2232
6046
|
dataFetchingState.displayItems,
|
|
2233
6047
|
columnsProp,
|
|
@@ -2253,6 +6067,8 @@ function useOGrid(props, ref) {
|
|
|
2253
6067
|
effectiveSelectedRows,
|
|
2254
6068
|
handleSelectionChange,
|
|
2255
6069
|
showRowNumbers,
|
|
6070
|
+
cellReferences,
|
|
6071
|
+
onActiveCellChange,
|
|
2256
6072
|
paginationState.page,
|
|
2257
6073
|
paginationState.pageSize,
|
|
2258
6074
|
statusBarConfig,
|
|
@@ -2273,7 +6089,9 @@ function useOGrid(props, ref) {
|
|
|
2273
6089
|
ariaLabelledBy,
|
|
2274
6090
|
filtersState.hasActiveFilters,
|
|
2275
6091
|
clearAllFilters,
|
|
2276
|
-
emptyState
|
|
6092
|
+
emptyState,
|
|
6093
|
+
formulas,
|
|
6094
|
+
formulaEngine
|
|
2277
6095
|
]);
|
|
2278
6096
|
const pagination = useMemo(() => ({
|
|
2279
6097
|
page: paginationState.page,
|
|
@@ -2291,14 +6109,20 @@ function useOGrid(props, ref) {
|
|
|
2291
6109
|
onSetVisibleColumns: setVisibleColumns,
|
|
2292
6110
|
placement: columnChooserPlacement
|
|
2293
6111
|
}), [columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns, columnChooserPlacement]);
|
|
6112
|
+
const showNameBox = !!cellReferences;
|
|
6113
|
+
const nameBoxEl = useMemo(() => showNameBox ? React5.createElement("div", {
|
|
6114
|
+
style: NAME_BOX_STYLE,
|
|
6115
|
+
"aria-label": "Active cell reference"
|
|
6116
|
+
}, activeCellRef ?? "\u2014") : null, [showNameBox, activeCellRef]);
|
|
6117
|
+
const resolvedToolbar = useMemo(() => showNameBox ? React5.createElement(React5.Fragment, null, nameBoxEl, toolbar) : toolbar, [showNameBox, nameBoxEl, toolbar]);
|
|
2294
6118
|
const layout = useMemo(() => ({
|
|
2295
|
-
toolbar,
|
|
6119
|
+
toolbar: resolvedToolbar,
|
|
2296
6120
|
toolbarBelow,
|
|
2297
6121
|
className,
|
|
2298
6122
|
emptyState,
|
|
2299
6123
|
sideBarProps,
|
|
2300
6124
|
fullScreen
|
|
2301
|
-
}), [
|
|
6125
|
+
}), [resolvedToolbar, toolbarBelow, className, emptyState, sideBarProps, fullScreen]);
|
|
2302
6126
|
const filtersResult = useMemo(() => ({
|
|
2303
6127
|
hasActiveFilters: filtersState.hasActiveFilters,
|
|
2304
6128
|
setFilters: filtersState.setFilters
|
|
@@ -2724,6 +6548,11 @@ function useClipboard(params) {
|
|
|
2724
6548
|
const activeCellRef = useLatestRef(params.activeCell);
|
|
2725
6549
|
const editableRef = useLatestRef(params.editable);
|
|
2726
6550
|
const onCellValueChangedRef = useLatestRef(params.onCellValueChanged);
|
|
6551
|
+
const formulasRef = useLatestRef(params.formulas);
|
|
6552
|
+
const flatColumnsRef = useLatestRef(params.flatColumns);
|
|
6553
|
+
const getFormulaRef = useLatestRef(params.getFormula);
|
|
6554
|
+
const hasFormulaRef = useLatestRef(params.hasFormula);
|
|
6555
|
+
const setFormulaRef = useLatestRef(params.setFormula);
|
|
2727
6556
|
const cutRangeRef = useRef(null);
|
|
2728
6557
|
const [cutRange, setCutRange] = useState(null);
|
|
2729
6558
|
const [copyRange, setCopyRange] = useState(null);
|
|
@@ -2741,12 +6570,18 @@ function useClipboard(params) {
|
|
|
2741
6570
|
const range = getEffectiveRange();
|
|
2742
6571
|
if (range == null) return;
|
|
2743
6572
|
const norm = normalizeSelectionRange(range);
|
|
2744
|
-
const
|
|
6573
|
+
const formulaOptions = formulasRef.current && flatColumnsRef.current ? {
|
|
6574
|
+
colOffset,
|
|
6575
|
+
flatColumns: flatColumnsRef.current,
|
|
6576
|
+
getFormula: getFormulaRef.current,
|
|
6577
|
+
hasFormula: hasFormulaRef.current
|
|
6578
|
+
} : void 0;
|
|
6579
|
+
const tsv = formatSelectionAsTsv(itemsRef.current, visibleColsRef.current, norm, formulaOptions);
|
|
2745
6580
|
internalClipboardRef.current = tsv;
|
|
2746
6581
|
setCopyRange(norm);
|
|
2747
6582
|
void navigator.clipboard.writeText(tsv).catch(() => {
|
|
2748
6583
|
});
|
|
2749
|
-
}, [getEffectiveRange, itemsRef, visibleColsRef]);
|
|
6584
|
+
}, [getEffectiveRange, itemsRef, visibleColsRef, formulasRef, flatColumnsRef, getFormulaRef, hasFormulaRef, colOffset]);
|
|
2750
6585
|
const handleCut = useCallback(() => {
|
|
2751
6586
|
if (editableRef.current === false) return;
|
|
2752
6587
|
const range = getEffectiveRange();
|
|
@@ -2779,8 +6614,13 @@ function useClipboard(params) {
|
|
|
2779
6614
|
const items = itemsRef.current;
|
|
2780
6615
|
const visibleCols = visibleColsRef.current;
|
|
2781
6616
|
const parsedRows = parseTsvClipboard(text);
|
|
6617
|
+
const formulaOptions = formulasRef.current && flatColumnsRef.current ? {
|
|
6618
|
+
colOffset,
|
|
6619
|
+
flatColumns: flatColumnsRef.current,
|
|
6620
|
+
setFormula: setFormulaRef.current
|
|
6621
|
+
} : void 0;
|
|
2782
6622
|
beginBatch?.();
|
|
2783
|
-
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
|
|
6623
|
+
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions);
|
|
2784
6624
|
for (const evt of pasteEvents) onCellValueChanged(evt);
|
|
2785
6625
|
if (cutRangeRef.current) {
|
|
2786
6626
|
const cutEvents = applyCutClear(cutRangeRef.current, items, visibleCols);
|
|
@@ -2790,7 +6630,7 @@ function useClipboard(params) {
|
|
|
2790
6630
|
}
|
|
2791
6631
|
endBatch?.();
|
|
2792
6632
|
setCopyRange(null);
|
|
2793
|
-
}, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch]);
|
|
6633
|
+
}, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch, formulasRef, flatColumnsRef, setFormulaRef, colOffset]);
|
|
2794
6634
|
const clearClipboardRanges = useCallback(() => {
|
|
2795
6635
|
setCopyRange(null);
|
|
2796
6636
|
setCutRange(null);
|
|
@@ -2896,7 +6736,9 @@ function useKeyboardNavigation(params) {
|
|
|
2896
6736
|
"Tab",
|
|
2897
6737
|
"Enter",
|
|
2898
6738
|
"Home",
|
|
2899
|
-
"End"
|
|
6739
|
+
"End",
|
|
6740
|
+
"PageDown",
|
|
6741
|
+
"PageUp"
|
|
2900
6742
|
].includes(e.key)) {
|
|
2901
6743
|
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
2902
6744
|
e.preventDefault();
|
|
@@ -3000,6 +6842,42 @@ function useKeyboardNavigation(params) {
|
|
|
3000
6842
|
setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
|
|
3001
6843
|
break;
|
|
3002
6844
|
}
|
|
6845
|
+
case "PageDown":
|
|
6846
|
+
case "PageUp": {
|
|
6847
|
+
e.preventDefault();
|
|
6848
|
+
const wrapper = wrapperRef.current;
|
|
6849
|
+
let pageSize = 10;
|
|
6850
|
+
let rowHeight = 36;
|
|
6851
|
+
if (wrapper) {
|
|
6852
|
+
const firstRow = wrapper.querySelector("tbody tr");
|
|
6853
|
+
if (firstRow && firstRow.offsetHeight > 0) {
|
|
6854
|
+
rowHeight = firstRow.offsetHeight;
|
|
6855
|
+
pageSize = Math.max(1, Math.floor(wrapper.clientHeight / rowHeight));
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
const pgDirection = e.key === "PageDown" ? 1 : -1;
|
|
6859
|
+
const newRowPage = Math.max(0, Math.min(rowIndex + pgDirection * pageSize, maxRowIndex));
|
|
6860
|
+
if (shift) {
|
|
6861
|
+
setSelectionRange({
|
|
6862
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
6863
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
6864
|
+
endRow: newRowPage,
|
|
6865
|
+
endCol: selectionRange?.endCol ?? dataColIndex
|
|
6866
|
+
});
|
|
6867
|
+
} else {
|
|
6868
|
+
setSelectionRange({
|
|
6869
|
+
startRow: newRowPage,
|
|
6870
|
+
startCol: dataColIndex,
|
|
6871
|
+
endRow: newRowPage,
|
|
6872
|
+
endCol: dataColIndex
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
setActiveCell({ rowIndex: newRowPage, columnIndex });
|
|
6876
|
+
if (wrapper) {
|
|
6877
|
+
wrapper.scrollTop = getScrollTopForRow(newRowPage, rowHeight, wrapper.clientHeight, "center");
|
|
6878
|
+
}
|
|
6879
|
+
break;
|
|
6880
|
+
}
|
|
3003
6881
|
case "Enter":
|
|
3004
6882
|
case "F2": {
|
|
3005
6883
|
e.preventDefault();
|
|
@@ -3222,7 +7100,8 @@ function useFillHandle(params) {
|
|
|
3222
7100
|
colOffset,
|
|
3223
7101
|
wrapperRef,
|
|
3224
7102
|
beginBatch,
|
|
3225
|
-
endBatch
|
|
7103
|
+
endBatch,
|
|
7104
|
+
formulaOptions
|
|
3226
7105
|
} = params;
|
|
3227
7106
|
const onCellValueChangedRef = useLatestRef(onCellValueChangedProp);
|
|
3228
7107
|
const [fillDrag, setFillDrag] = useState(null);
|
|
@@ -3328,7 +7207,7 @@ function useFillHandle(params) {
|
|
|
3328
7207
|
});
|
|
3329
7208
|
setSelectionRange(norm);
|
|
3330
7209
|
setActiveCell({ rowIndex: fillDrag.startRow, columnIndex: fillDrag.startCol + colOffsetRef.current });
|
|
3331
|
-
const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols);
|
|
7210
|
+
const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols, formulaOptions);
|
|
3332
7211
|
if (fillEvents.length > 0) {
|
|
3333
7212
|
beginBatch?.();
|
|
3334
7213
|
for (const evt of fillEvents) onCellValueChangedRef.current?.(evt);
|
|
@@ -3383,7 +7262,8 @@ function useFillHandle(params) {
|
|
|
3383
7262
|
norm.startRow,
|
|
3384
7263
|
norm.startCol,
|
|
3385
7264
|
itemsRef.current,
|
|
3386
|
-
visibleColsRef.current
|
|
7265
|
+
visibleColsRef.current,
|
|
7266
|
+
formulaOptions
|
|
3387
7267
|
);
|
|
3388
7268
|
if (fillEvents.length > 0) {
|
|
3389
7269
|
beginBatch?.();
|
|
@@ -3854,14 +7734,38 @@ function useDataGridEditing(params) {
|
|
|
3854
7734
|
onCellValueChanged,
|
|
3855
7735
|
setActiveCell,
|
|
3856
7736
|
setSelectionRange,
|
|
3857
|
-
colOffset
|
|
7737
|
+
colOffset,
|
|
7738
|
+
setFormula,
|
|
7739
|
+
onFormulaCellChanged,
|
|
7740
|
+
formulas,
|
|
7741
|
+
flatColumns
|
|
3858
7742
|
} = params;
|
|
3859
7743
|
const [popoverAnchorEl, setPopoverAnchorEl] = useState(null);
|
|
3860
7744
|
const visibleColsRef = useLatestRef(params.visibleCols);
|
|
3861
7745
|
const itemsLengthRef = useLatestRef(params.itemsLength);
|
|
3862
7746
|
const onCellValueChangedRef = useLatestRef(onCellValueChanged);
|
|
7747
|
+
const setFormulaRef = useLatestRef(setFormula);
|
|
7748
|
+
const onFormulaCellChangedRef = useLatestRef(onFormulaCellChanged);
|
|
7749
|
+
const flatColumnsRef = useLatestRef(flatColumns);
|
|
3863
7750
|
const commitCellEdit = useCallback(
|
|
3864
7751
|
(item, columnId, oldValue, newValue, rowIndex, globalColIndex) => {
|
|
7752
|
+
if (formulas && typeof newValue === "string" && newValue.startsWith("=") && setFormulaRef.current) {
|
|
7753
|
+
const cols = flatColumnsRef.current;
|
|
7754
|
+
const colIndex = cols ? cols.findIndex((c) => c.columnId === columnId) : -1;
|
|
7755
|
+
if (colIndex >= 0) {
|
|
7756
|
+
setFormulaRef.current(colIndex, rowIndex, newValue);
|
|
7757
|
+
setEditingCell(null);
|
|
7758
|
+
setPopoverAnchorEl(null);
|
|
7759
|
+
setPendingEditorValue(void 0);
|
|
7760
|
+
if (rowIndex < itemsLengthRef.current - 1) {
|
|
7761
|
+
const newRow = rowIndex + 1;
|
|
7762
|
+
const localCol = globalColIndex - colOffset;
|
|
7763
|
+
setActiveCell({ rowIndex: newRow, columnIndex: globalColIndex });
|
|
7764
|
+
setSelectionRange({ startRow: newRow, startCol: localCol, endRow: newRow, endCol: localCol });
|
|
7765
|
+
}
|
|
7766
|
+
return;
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
3865
7769
|
const col = visibleColsRef.current.find((c) => c.columnId === columnId);
|
|
3866
7770
|
if (col) {
|
|
3867
7771
|
const result = parseValue(newValue, oldValue, item, col);
|
|
@@ -3880,6 +7784,12 @@ function useDataGridEditing(params) {
|
|
|
3880
7784
|
newValue,
|
|
3881
7785
|
rowIndex
|
|
3882
7786
|
});
|
|
7787
|
+
if (formulas && onFormulaCellChangedRef.current && flatColumnsRef.current) {
|
|
7788
|
+
const colIndex = flatColumnsRef.current.findIndex((c) => c.columnId === columnId);
|
|
7789
|
+
if (colIndex >= 0) {
|
|
7790
|
+
onFormulaCellChangedRef.current(colIndex, rowIndex);
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
3883
7793
|
setEditingCell(null);
|
|
3884
7794
|
setPopoverAnchorEl(null);
|
|
3885
7795
|
setPendingEditorValue(void 0);
|
|
@@ -3890,7 +7800,7 @@ function useDataGridEditing(params) {
|
|
|
3890
7800
|
setSelectionRange({ startRow: newRow, startCol: localCol, endRow: newRow, endCol: localCol });
|
|
3891
7801
|
}
|
|
3892
7802
|
},
|
|
3893
|
-
[setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef, onCellValueChangedRef]
|
|
7803
|
+
[formulas, setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef, onCellValueChangedRef, setFormulaRef, onFormulaCellChangedRef, flatColumnsRef]
|
|
3894
7804
|
);
|
|
3895
7805
|
const cancelPopoverEdit = useCallback(() => {
|
|
3896
7806
|
setEditingCell(null);
|
|
@@ -3937,7 +7847,12 @@ function useDataGridInteraction(params) {
|
|
|
3937
7847
|
handleRowCheckboxChange,
|
|
3938
7848
|
setContextMenuPosition,
|
|
3939
7849
|
wrapperRef,
|
|
3940
|
-
onKeyDown
|
|
7850
|
+
onKeyDown,
|
|
7851
|
+
formulas,
|
|
7852
|
+
flatColumns,
|
|
7853
|
+
getFormula,
|
|
7854
|
+
hasFormula,
|
|
7855
|
+
setFormula
|
|
3941
7856
|
} = params;
|
|
3942
7857
|
const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
|
|
3943
7858
|
const onCellValueChanged = undoRedo.onCellValueChanged;
|
|
@@ -3963,7 +7878,12 @@ function useDataGridInteraction(params) {
|
|
|
3963
7878
|
editable,
|
|
3964
7879
|
onCellValueChanged,
|
|
3965
7880
|
beginBatch: undoRedo.beginBatch,
|
|
3966
|
-
endBatch: undoRedo.endBatch
|
|
7881
|
+
endBatch: undoRedo.endBatch,
|
|
7882
|
+
formulas,
|
|
7883
|
+
flatColumns,
|
|
7884
|
+
getFormula,
|
|
7885
|
+
hasFormula,
|
|
7886
|
+
setFormula
|
|
3967
7887
|
});
|
|
3968
7888
|
const handleCellMouseDown = useCallback(
|
|
3969
7889
|
(e, rowIndex, globalColIndex) => {
|
|
@@ -3974,6 +7894,10 @@ function useDataGridInteraction(params) {
|
|
|
3974
7894
|
},
|
|
3975
7895
|
[handleCellMouseDownBase, clearClipboardRanges, wrapperRef]
|
|
3976
7896
|
);
|
|
7897
|
+
const fillFormulaOptions = useMemo(() => {
|
|
7898
|
+
if (!formulas || !flatColumns) return void 0;
|
|
7899
|
+
return { flatColumns, getFormula, hasFormula, setFormula };
|
|
7900
|
+
}, [formulas, flatColumns, getFormula, hasFormula, setFormula]);
|
|
3977
7901
|
const { handleFillHandleMouseDown, fillDown } = useFillHandle({
|
|
3978
7902
|
items,
|
|
3979
7903
|
visibleCols,
|
|
@@ -3985,7 +7909,8 @@ function useDataGridInteraction(params) {
|
|
|
3985
7909
|
colOffset,
|
|
3986
7910
|
wrapperRef,
|
|
3987
7911
|
beginBatch: undoRedo.beginBatch,
|
|
3988
|
-
endBatch: undoRedo.endBatch
|
|
7912
|
+
endBatch: undoRedo.endBatch,
|
|
7913
|
+
formulaOptions: fillFormulaOptions
|
|
3989
7914
|
});
|
|
3990
7915
|
const { handleGridKeyDown } = useKeyboardNavigation({
|
|
3991
7916
|
data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
|
|
@@ -4165,7 +8090,12 @@ function useDataGridState(params) {
|
|
|
4165
8090
|
handleRowCheckboxChange,
|
|
4166
8091
|
setContextMenuPosition,
|
|
4167
8092
|
wrapperRef,
|
|
4168
|
-
onKeyDown
|
|
8093
|
+
onKeyDown,
|
|
8094
|
+
formulas: props.formulas,
|
|
8095
|
+
flatColumns: layoutResult.layout.flatColumns,
|
|
8096
|
+
getFormula: props.getFormula,
|
|
8097
|
+
hasFormula: props.hasFormula,
|
|
8098
|
+
setFormula: props.setFormula
|
|
4169
8099
|
});
|
|
4170
8100
|
const {
|
|
4171
8101
|
selectionRange,
|
|
@@ -4185,7 +8115,11 @@ function useDataGridState(params) {
|
|
|
4185
8115
|
onCellValueChanged,
|
|
4186
8116
|
setActiveCell,
|
|
4187
8117
|
setSelectionRange,
|
|
4188
|
-
colOffset
|
|
8118
|
+
colOffset,
|
|
8119
|
+
setFormula: props.setFormula,
|
|
8120
|
+
onFormulaCellChanged: props.onFormulaCellChanged,
|
|
8121
|
+
formulas: props.formulas,
|
|
8122
|
+
flatColumns: layoutResult.layout.flatColumns
|
|
4189
8123
|
});
|
|
4190
8124
|
const {
|
|
4191
8125
|
sortBy,
|
|
@@ -4714,7 +8648,7 @@ function useInlineCellEditorState(params) {
|
|
|
4714
8648
|
e.stopPropagation();
|
|
4715
8649
|
onCancel();
|
|
4716
8650
|
}
|
|
4717
|
-
if (e.key === "Enter" && editorType === "text") {
|
|
8651
|
+
if (e.key === "Enter" && (editorType === "text" || editorType === "date")) {
|
|
4718
8652
|
e.preventDefault();
|
|
4719
8653
|
e.stopPropagation();
|
|
4720
8654
|
onCommit(localValue);
|
|
@@ -4723,7 +8657,7 @@ function useInlineCellEditorState(params) {
|
|
|
4723
8657
|
[onCancel, onCommit, localValue, editorType]
|
|
4724
8658
|
);
|
|
4725
8659
|
const handleBlur = useCallback(() => {
|
|
4726
|
-
if (editorType === "text") {
|
|
8660
|
+
if (editorType === "text" || editorType === "date") {
|
|
4727
8661
|
onCommit(localValue);
|
|
4728
8662
|
}
|
|
4729
8663
|
}, [editorType, localValue, onCommit]);
|
|
@@ -5975,12 +9909,12 @@ function calculateRange({
|
|
|
5975
9909
|
}
|
|
5976
9910
|
|
|
5977
9911
|
// ../../node_modules/@tanstack/react-virtual/dist/esm/index.js
|
|
5978
|
-
var useIsomorphicLayoutEffect = typeof document !== "undefined" ?
|
|
9912
|
+
var useIsomorphicLayoutEffect = typeof document !== "undefined" ? React5.useLayoutEffect : React5.useEffect;
|
|
5979
9913
|
function useVirtualizerBase({
|
|
5980
9914
|
useFlushSync = true,
|
|
5981
9915
|
...options
|
|
5982
9916
|
}) {
|
|
5983
|
-
const rerender =
|
|
9917
|
+
const rerender = React5.useReducer(() => ({}), {})[1];
|
|
5984
9918
|
const resolvedOptions = {
|
|
5985
9919
|
...options,
|
|
5986
9920
|
onChange: (instance2, sync) => {
|
|
@@ -5993,7 +9927,7 @@ function useVirtualizerBase({
|
|
|
5993
9927
|
(_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
|
|
5994
9928
|
}
|
|
5995
9929
|
};
|
|
5996
|
-
const [instance] =
|
|
9930
|
+
const [instance] = React5.useState(
|
|
5997
9931
|
() => new Virtualizer(resolvedOptions)
|
|
5998
9932
|
);
|
|
5999
9933
|
instance.setOptions(resolvedOptions);
|
|
@@ -6023,7 +9957,10 @@ function useVirtualScroll(params) {
|
|
|
6023
9957
|
enabled,
|
|
6024
9958
|
overscan = 5,
|
|
6025
9959
|
threshold = DEFAULT_PASSTHROUGH_THRESHOLD,
|
|
6026
|
-
containerRef
|
|
9960
|
+
containerRef,
|
|
9961
|
+
columnVirtualization = false,
|
|
9962
|
+
columnWidths,
|
|
9963
|
+
columnOverscan = 2
|
|
6027
9964
|
} = params;
|
|
6028
9965
|
useEffect(() => {
|
|
6029
9966
|
validateVirtualScrollConfig({ enabled, rowHeight });
|
|
@@ -6082,11 +10019,51 @@ function useVirtualScroll(params) {
|
|
|
6082
10019
|
},
|
|
6083
10020
|
[isActive, containerRef, rowHeight]
|
|
6084
10021
|
);
|
|
10022
|
+
const [scrollLeft, setScrollLeft] = useState(0);
|
|
10023
|
+
const scrollLeftRaf = useRef(0);
|
|
10024
|
+
const onHorizontalScroll = useCallback(
|
|
10025
|
+
(sl) => {
|
|
10026
|
+
if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
|
|
10027
|
+
scrollLeftRaf.current = requestAnimationFrame(() => {
|
|
10028
|
+
scrollLeftRaf.current = 0;
|
|
10029
|
+
setScrollLeft(sl);
|
|
10030
|
+
});
|
|
10031
|
+
},
|
|
10032
|
+
[]
|
|
10033
|
+
);
|
|
10034
|
+
useEffect(() => {
|
|
10035
|
+
return () => {
|
|
10036
|
+
if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
|
|
10037
|
+
};
|
|
10038
|
+
}, []);
|
|
10039
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
10040
|
+
useEffect(() => {
|
|
10041
|
+
if (!columnVirtualization) return;
|
|
10042
|
+
const el = containerRef.current;
|
|
10043
|
+
if (!el) return;
|
|
10044
|
+
setContainerWidth(el.clientWidth);
|
|
10045
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
10046
|
+
const ro = new ResizeObserver((entries) => {
|
|
10047
|
+
if (entries.length > 0) {
|
|
10048
|
+
setContainerWidth(entries[0].contentRect.width);
|
|
10049
|
+
}
|
|
10050
|
+
});
|
|
10051
|
+
ro.observe(el);
|
|
10052
|
+
return () => ro.disconnect();
|
|
10053
|
+
}, [columnVirtualization, containerRef]);
|
|
10054
|
+
const columnRange = useMemo(() => {
|
|
10055
|
+
if (!columnVirtualization || !columnWidths || columnWidths.length === 0 || containerWidth <= 0) {
|
|
10056
|
+
return null;
|
|
10057
|
+
}
|
|
10058
|
+
return computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, columnOverscan);
|
|
10059
|
+
}, [columnVirtualization, columnWidths, containerWidth, scrollLeft, columnOverscan]);
|
|
6085
10060
|
return {
|
|
6086
10061
|
virtualizer: isActive ? virtualizer : null,
|
|
6087
10062
|
totalHeight,
|
|
6088
10063
|
visibleRange: activeRange,
|
|
6089
|
-
scrollToIndex
|
|
10064
|
+
scrollToIndex,
|
|
10065
|
+
columnRange,
|
|
10066
|
+
onHorizontalScroll: columnVirtualization ? onHorizontalScroll : void 0
|
|
6090
10067
|
};
|
|
6091
10068
|
}
|
|
6092
10069
|
function useListVirtualizer(opts) {
|
|
@@ -6177,12 +10154,26 @@ function useDataGridTableOrchestration(params) {
|
|
|
6177
10154
|
density = "normal",
|
|
6178
10155
|
pinnedColumns,
|
|
6179
10156
|
currentPage = 1,
|
|
6180
|
-
pageSize: propPageSize = 25
|
|
10157
|
+
pageSize: propPageSize = 25,
|
|
10158
|
+
showColumnLetters = false,
|
|
10159
|
+
showNameBox = false,
|
|
10160
|
+
onActiveCellChange
|
|
6181
10161
|
} = props;
|
|
6182
10162
|
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
6183
10163
|
const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
|
|
6184
10164
|
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
6185
10165
|
const fitToContent = layoutMode === "content";
|
|
10166
|
+
const onActiveCellChangeRef = useRef(onActiveCellChange);
|
|
10167
|
+
onActiveCellChangeRef.current = onActiveCellChange;
|
|
10168
|
+
useEffect(() => {
|
|
10169
|
+
if (!onActiveCellChangeRef.current) return;
|
|
10170
|
+
const ac = interaction.activeCell;
|
|
10171
|
+
if (ac) {
|
|
10172
|
+
onActiveCellChangeRef.current(formatCellReference(ac.columnIndex, rowNumberOffset + ac.rowIndex + 1));
|
|
10173
|
+
} else {
|
|
10174
|
+
onActiveCellChangeRef.current(null);
|
|
10175
|
+
}
|
|
10176
|
+
}, [interaction.activeCell, rowNumberOffset]);
|
|
6186
10177
|
const { handleResizeStart, handleResizeDoubleClick, getColumnWidth } = useColumnResize({
|
|
6187
10178
|
columnSizingOverrides,
|
|
6188
10179
|
setColumnSizingOverrides
|
|
@@ -6197,13 +10188,28 @@ function useDataGridTableOrchestration(params) {
|
|
|
6197
10188
|
});
|
|
6198
10189
|
const virtualScrollEnabled = virtualScroll?.enabled === true;
|
|
6199
10190
|
const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
|
|
6200
|
-
const
|
|
10191
|
+
const columnVirtualization = virtualScroll?.columns === true;
|
|
10192
|
+
const unpinnedColumnWidths = useMemo(() => {
|
|
10193
|
+
if (!columnVirtualization) return void 0;
|
|
10194
|
+
const widths = [];
|
|
10195
|
+
for (const col of visibleCols) {
|
|
10196
|
+
const pin = pinnedColumns?.[col.columnId];
|
|
10197
|
+
if (!pin) {
|
|
10198
|
+
widths.push(getColumnWidth(col));
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
return widths;
|
|
10202
|
+
}, [columnVirtualization, visibleCols, pinnedColumns, getColumnWidth]);
|
|
10203
|
+
const { visibleRange, columnRange, onHorizontalScroll } = useVirtualScroll({
|
|
6201
10204
|
totalRows: items.length,
|
|
6202
10205
|
rowHeight: virtualRowHeight,
|
|
6203
10206
|
enabled: virtualScrollEnabled,
|
|
6204
10207
|
overscan: virtualScroll?.overscan,
|
|
6205
10208
|
threshold: virtualScroll?.threshold,
|
|
6206
|
-
containerRef: wrapperRef
|
|
10209
|
+
containerRef: wrapperRef,
|
|
10210
|
+
columnVirtualization,
|
|
10211
|
+
columnWidths: unpinnedColumnWidths,
|
|
10212
|
+
columnOverscan: virtualScroll?.columnOverscan
|
|
6207
10213
|
});
|
|
6208
10214
|
const editCallbacks = useMemo(
|
|
6209
10215
|
() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }),
|
|
@@ -6259,6 +10265,8 @@ function useDataGridTableOrchestration(params) {
|
|
|
6259
10265
|
virtualScrollEnabled,
|
|
6260
10266
|
virtualRowHeight,
|
|
6261
10267
|
visibleRange,
|
|
10268
|
+
columnRange,
|
|
10269
|
+
onHorizontalScroll,
|
|
6262
10270
|
// Derived from props
|
|
6263
10271
|
items,
|
|
6264
10272
|
columns,
|
|
@@ -6285,6 +10293,8 @@ function useDataGridTableOrchestration(params) {
|
|
|
6285
10293
|
headerRows,
|
|
6286
10294
|
allowOverflowX,
|
|
6287
10295
|
fitToContent,
|
|
10296
|
+
showColumnLetters,
|
|
10297
|
+
showNameBox,
|
|
6288
10298
|
// Memoized callback groups
|
|
6289
10299
|
editCallbacks,
|
|
6290
10300
|
interactionHandlers,
|
|
@@ -6869,7 +10879,8 @@ var richSelectDropdownStyle = {
|
|
|
6869
10879
|
background: "var(--ogrid-bg, #fff)",
|
|
6870
10880
|
border: "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))",
|
|
6871
10881
|
zIndex: 10,
|
|
6872
|
-
boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))"
|
|
10882
|
+
boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))",
|
|
10883
|
+
textAlign: "left"
|
|
6873
10884
|
};
|
|
6874
10885
|
var richSelectOptionStyle = {
|
|
6875
10886
|
padding: "6px 8px",
|
|
@@ -6911,7 +10922,7 @@ var selectChevronStyle = {
|
|
|
6911
10922
|
};
|
|
6912
10923
|
function BaseInlineCellEditor(props) {
|
|
6913
10924
|
const { value, column, editorType, onCommit, onCancel, renderCheckbox } = props;
|
|
6914
|
-
const wrapperRef =
|
|
10925
|
+
const wrapperRef = React5.useRef(null);
|
|
6915
10926
|
const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
|
|
6916
10927
|
const editorValues = column.cellEditorParams?.values ?? [];
|
|
6917
10928
|
const editorFormatValue = column.cellEditorParams?.formatValue;
|
|
@@ -6928,8 +10939,8 @@ function BaseInlineCellEditor(props) {
|
|
|
6928
10939
|
onCommit,
|
|
6929
10940
|
onCancel
|
|
6930
10941
|
});
|
|
6931
|
-
const [fixedDropdownStyle, setFixedDropdownStyle] =
|
|
6932
|
-
|
|
10942
|
+
const [fixedDropdownStyle, setFixedDropdownStyle] = React5.useState(null);
|
|
10943
|
+
React5.useLayoutEffect(() => {
|
|
6933
10944
|
if (editorType !== "select" && editorType !== "richSelect") return;
|
|
6934
10945
|
const wrapper = wrapperRef.current;
|
|
6935
10946
|
if (!wrapper) return;
|
|
@@ -6947,17 +10958,25 @@ function BaseInlineCellEditor(props) {
|
|
|
6947
10958
|
background: "var(--ogrid-bg, #fff)",
|
|
6948
10959
|
border: "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))",
|
|
6949
10960
|
zIndex: 9999,
|
|
6950
|
-
boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))"
|
|
10961
|
+
boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))",
|
|
10962
|
+
textAlign: "left"
|
|
6951
10963
|
});
|
|
6952
10964
|
}, [editorType]);
|
|
6953
10965
|
const computedDropdownStyle = fixedDropdownStyle ?? richSelectDropdownStyle;
|
|
6954
|
-
|
|
10966
|
+
React5.useEffect(() => {
|
|
6955
10967
|
const wrapper = wrapperRef.current;
|
|
6956
10968
|
if (!wrapper) return;
|
|
6957
10969
|
const input = wrapper.querySelector("input");
|
|
6958
10970
|
if (input) {
|
|
6959
10971
|
input.focus();
|
|
6960
|
-
|
|
10972
|
+
if (editorType === "date") {
|
|
10973
|
+
try {
|
|
10974
|
+
input.showPicker();
|
|
10975
|
+
} catch {
|
|
10976
|
+
}
|
|
10977
|
+
} else {
|
|
10978
|
+
input.select();
|
|
10979
|
+
}
|
|
6961
10980
|
} else {
|
|
6962
10981
|
wrapper.focus();
|
|
6963
10982
|
}
|
|
@@ -7045,12 +11064,12 @@ function BaseInlineCellEditor(props) {
|
|
|
7045
11064
|
var menuPositionStyle = (x, y) => ({ left: x, top: y });
|
|
7046
11065
|
function GridContextMenu(props) {
|
|
7047
11066
|
const { x, y, hasSelection, canUndo, canRedo, onClose, onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, classNames } = props;
|
|
7048
|
-
const ref =
|
|
7049
|
-
const handlers =
|
|
11067
|
+
const ref = React5.useRef(null);
|
|
11068
|
+
const handlers = React5.useMemo(
|
|
7050
11069
|
() => getContextMenuHandlers({ onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose }),
|
|
7051
11070
|
[onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose]
|
|
7052
11071
|
);
|
|
7053
|
-
const isDisabled =
|
|
11072
|
+
const isDisabled = React5.useCallback(
|
|
7054
11073
|
(item) => {
|
|
7055
11074
|
if (item.disabledWhenNoSelection && !hasSelection) return true;
|
|
7056
11075
|
if (item.id === "undo" && !canUndo) return true;
|
|
@@ -7059,7 +11078,7 @@ function GridContextMenu(props) {
|
|
|
7059
11078
|
},
|
|
7060
11079
|
[hasSelection, canUndo, canRedo]
|
|
7061
11080
|
);
|
|
7062
|
-
|
|
11081
|
+
React5.useEffect(() => {
|
|
7063
11082
|
const handleClickOutside = (e) => {
|
|
7064
11083
|
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
7065
11084
|
};
|
|
@@ -7081,7 +11100,7 @@ function GridContextMenu(props) {
|
|
|
7081
11100
|
role: "menu",
|
|
7082
11101
|
style: menuPositionStyle(x, y),
|
|
7083
11102
|
"aria-label": "Grid context menu",
|
|
7084
|
-
children: GRID_CONTEXT_MENU_ITEMS.map((item) => /* @__PURE__ */ jsxs(
|
|
11103
|
+
children: GRID_CONTEXT_MENU_ITEMS.map((item) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
|
|
7085
11104
|
item.dividerBefore && /* @__PURE__ */ jsx("div", { className: classNames?.contextMenuDivider }),
|
|
7086
11105
|
/* @__PURE__ */ jsxs(
|
|
7087
11106
|
"button",
|
|
@@ -7242,9 +11261,9 @@ function BaseColumnHeaderMenu(props) {
|
|
|
7242
11261
|
classNames,
|
|
7243
11262
|
getPortalTarget
|
|
7244
11263
|
} = props;
|
|
7245
|
-
const [position, setPosition] =
|
|
7246
|
-
const menuRef =
|
|
7247
|
-
|
|
11264
|
+
const [position, setPosition] = React5.useState(null);
|
|
11265
|
+
const menuRef = React5.useRef(null);
|
|
11266
|
+
React5.useEffect(() => {
|
|
7248
11267
|
if (!isOpen || !anchorElement) {
|
|
7249
11268
|
setPosition(null);
|
|
7250
11269
|
return;
|
|
@@ -7273,7 +11292,7 @@ function BaseColumnHeaderMenu(props) {
|
|
|
7273
11292
|
document.removeEventListener("keydown", handleEscape);
|
|
7274
11293
|
};
|
|
7275
11294
|
}, [isOpen, anchorElement, onClose]);
|
|
7276
|
-
const menuInput =
|
|
11295
|
+
const menuInput = React5.useMemo(
|
|
7277
11296
|
() => ({
|
|
7278
11297
|
canPinLeft,
|
|
7279
11298
|
canPinRight,
|
|
@@ -7284,8 +11303,8 @@ function BaseColumnHeaderMenu(props) {
|
|
|
7284
11303
|
}),
|
|
7285
11304
|
[canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]
|
|
7286
11305
|
);
|
|
7287
|
-
const items =
|
|
7288
|
-
const handlers =
|
|
11306
|
+
const items = React5.useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
|
|
11307
|
+
const handlers = React5.useMemo(
|
|
7289
11308
|
() => ({
|
|
7290
11309
|
pinLeft: onPinLeft,
|
|
7291
11310
|
pinRight: onPinRight,
|
|
@@ -7312,7 +11331,7 @@ function BaseColumnHeaderMenu(props) {
|
|
|
7312
11331
|
left: position.left,
|
|
7313
11332
|
zIndex: 1e3
|
|
7314
11333
|
},
|
|
7315
|
-
children: items.map((item, idx) => /* @__PURE__ */ jsxs(
|
|
11334
|
+
children: items.map((item, idx) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
|
|
7316
11335
|
/* @__PURE__ */ jsx(
|
|
7317
11336
|
"button",
|
|
7318
11337
|
{
|
|
@@ -7378,14 +11397,14 @@ function createOGrid(components) {
|
|
|
7378
11397
|
);
|
|
7379
11398
|
});
|
|
7380
11399
|
OGridInner.displayName = "OGrid";
|
|
7381
|
-
return
|
|
11400
|
+
return React5.memo(OGridInner);
|
|
7382
11401
|
}
|
|
7383
11402
|
var DEFAULT_FALLBACK_STYLE = {
|
|
7384
11403
|
color: "var(--ogrid-error, #d32f2f)",
|
|
7385
11404
|
fontSize: "0.75rem",
|
|
7386
11405
|
padding: "2px 4px"
|
|
7387
11406
|
};
|
|
7388
|
-
var CellErrorBoundary = class extends
|
|
11407
|
+
var CellErrorBoundary = class extends React5.Component {
|
|
7389
11408
|
constructor(props) {
|
|
7390
11409
|
super(props);
|
|
7391
11410
|
this.state = { hasError: false };
|
|
@@ -7561,4 +11580,4 @@ function renderFilterContent(filterType, state, options, isLoadingOptions, selec
|
|
|
7561
11580
|
return null;
|
|
7562
11581
|
}
|
|
7563
11582
|
|
|
7564
|
-
export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
|
|
11583
|
+
export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellReference, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, indexToColumnLetter, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useFormulaEngine, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
|