mulmoclaude 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logical Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { functionRegistry, type FunctionHandler } from "../registry";
|
|
6
|
+
|
|
7
|
+
const ifHandler: FunctionHandler = (args, context) => {
|
|
8
|
+
if (args.length !== 3) throw new Error("IF requires 3 arguments");
|
|
9
|
+
|
|
10
|
+
const condition = args[0];
|
|
11
|
+
const trueValue = args[1];
|
|
12
|
+
const falseValue = args[2];
|
|
13
|
+
|
|
14
|
+
// Evaluate condition - use evaluateFormula to handle nested functions like MONTH()
|
|
15
|
+
const conditionValue = context.evaluateFormula(condition);
|
|
16
|
+
|
|
17
|
+
// Convert to boolean
|
|
18
|
+
let conditionResult = false;
|
|
19
|
+
if (typeof conditionValue === "boolean") {
|
|
20
|
+
conditionResult = conditionValue;
|
|
21
|
+
} else if (typeof conditionValue === "number") {
|
|
22
|
+
conditionResult = conditionValue !== 0;
|
|
23
|
+
} else if (typeof conditionValue === "string") {
|
|
24
|
+
conditionResult = conditionValue.toLowerCase() === "true" || conditionValue !== "";
|
|
25
|
+
} else {
|
|
26
|
+
conditionResult = !!conditionValue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Return the appropriate value based on condition
|
|
30
|
+
const resultValue = conditionResult ? trueValue : falseValue;
|
|
31
|
+
|
|
32
|
+
// If result is a quoted string, return the string without quotes
|
|
33
|
+
if (/^["'](.*)["']$/.test(resultValue)) {
|
|
34
|
+
return resultValue.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If result is a nested formula, evaluate it recursively
|
|
38
|
+
if (/^(SUM|AVERAGE|MAX|MIN|COUNT|IF|AND|OR|NOT)\(/i.test(resultValue)) {
|
|
39
|
+
return context.evaluateFormula(resultValue);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Otherwise evaluate as expression
|
|
43
|
+
let expr = resultValue;
|
|
44
|
+
|
|
45
|
+
const refs = resultValue.match(/(?:'[^']+'|[^'!\s]+)![A-Z]+\d+|\$?[A-Z]+\$?\d+/g);
|
|
46
|
+
if (refs) {
|
|
47
|
+
for (const ref of refs) {
|
|
48
|
+
const value = context.getCellValue(ref);
|
|
49
|
+
const escapedRef = ref.replace(/\$/g, "\\$").replace(/'/g, "\\'");
|
|
50
|
+
expr = expr.replace(new RegExp(escapedRef.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), String(value));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const numResult = parseFloat(expr);
|
|
55
|
+
return isNaN(numResult) ? expr : numResult;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const andHandler: FunctionHandler = (args, context) => {
|
|
59
|
+
if (args.length === 0) throw new Error("AND requires at least 1 argument");
|
|
60
|
+
|
|
61
|
+
for (const arg of args) {
|
|
62
|
+
const value = context.evaluateFormula(arg.trim());
|
|
63
|
+
// Check if value is falsy (0, false, empty string, etc.)
|
|
64
|
+
// Note: !value already covers false, so we check for 0 and "0" explicitly
|
|
65
|
+
if (!value || value === 0 || value === "0") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const orHandler: FunctionHandler = (args, context) => {
|
|
73
|
+
if (args.length === 0) throw new Error("OR requires at least 1 argument");
|
|
74
|
+
|
|
75
|
+
for (const arg of args) {
|
|
76
|
+
const value = context.evaluateFormula(arg.trim());
|
|
77
|
+
// Check if value is truthy (non-zero, non-empty)
|
|
78
|
+
if (value && value !== 0 && value !== "0") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const notHandler: FunctionHandler = (args, context) => {
|
|
86
|
+
if (args.length !== 1) throw new Error("NOT requires 1 argument");
|
|
87
|
+
|
|
88
|
+
const value = context.evaluateFormula(args[0]);
|
|
89
|
+
// Note: !value already covers false
|
|
90
|
+
return !value || value === 0 || value === "0";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const iferrorHandler: FunctionHandler = (args, context) => {
|
|
94
|
+
if (args.length !== 2) throw new Error("IFERROR requires 2 arguments");
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = context.evaluateFormula(args[0]);
|
|
98
|
+
// Check if result is an error (NaN, Infinity, etc.)
|
|
99
|
+
if (result === null || result === undefined || (typeof result === "number" && (isNaN(result) || !isFinite(result)))) {
|
|
100
|
+
return context.evaluateFormula(args[1]);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
} catch {
|
|
104
|
+
// If evaluation throws an error, return the fallback value
|
|
105
|
+
return context.evaluateFormula(args[1]);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ifnaHandler: FunctionHandler = (args, context) => {
|
|
110
|
+
if (args.length !== 2) throw new Error("IFNA requires 2 arguments");
|
|
111
|
+
|
|
112
|
+
const result = context.evaluateFormula(args[0]);
|
|
113
|
+
// Check if result is N/A (could be represented as specific error value)
|
|
114
|
+
if (result === null || result === undefined || result === "#N/A") {
|
|
115
|
+
return context.evaluateFormula(args[1]);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const ifsHandler: FunctionHandler = (args, context) => {
|
|
121
|
+
if (args.length < 2 || args.length % 2 !== 0) {
|
|
122
|
+
throw new Error("IFS requires an even number of arguments (condition-value pairs)");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Iterate through condition-value pairs
|
|
126
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
127
|
+
const condition = args[i];
|
|
128
|
+
const value = args[i + 1];
|
|
129
|
+
|
|
130
|
+
// Evaluate condition
|
|
131
|
+
let condExpr = condition;
|
|
132
|
+
|
|
133
|
+
const cellRefs = condition.match(/(?:'[^']+'|[^'!\s]+)![A-Z]+\d+|\$?[A-Z]+\$?\d+/g);
|
|
134
|
+
if (cellRefs) {
|
|
135
|
+
for (const ref of cellRefs) {
|
|
136
|
+
const cellValue = context.getCellValue(ref);
|
|
137
|
+
const escapedRef = ref.replace(/\$/g, "\\$").replace(/'/g, "\\'");
|
|
138
|
+
condExpr = condExpr.replace(new RegExp(escapedRef.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), String(cellValue));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Evaluate the condition
|
|
143
|
+
let conditionResult = false;
|
|
144
|
+
|
|
145
|
+
if (/>=|<=|>|<|==|!=/.test(condExpr)) {
|
|
146
|
+
conditionResult = eval(condExpr);
|
|
147
|
+
} else {
|
|
148
|
+
conditionResult = !!eval(condExpr);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (conditionResult) {
|
|
152
|
+
// If result is a quoted string, return without quotes
|
|
153
|
+
|
|
154
|
+
if (/^["'](.*)["']$/.test(value)) {
|
|
155
|
+
return value.slice(1, -1);
|
|
156
|
+
}
|
|
157
|
+
// Otherwise evaluate as formula or expression
|
|
158
|
+
return context.evaluateFormula(value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If no conditions match, return error
|
|
163
|
+
return "#N/A";
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const trueHandler: FunctionHandler = (args) => {
|
|
167
|
+
if (args.length !== 0) throw new Error("TRUE requires 0 arguments");
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const falseHandler: FunctionHandler = (args) => {
|
|
172
|
+
if (args.length !== 0) throw new Error("FALSE requires 0 arguments");
|
|
173
|
+
return false;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Register all logical functions
|
|
177
|
+
functionRegistry.register({
|
|
178
|
+
name: "IF",
|
|
179
|
+
handler: ifHandler,
|
|
180
|
+
minArgs: 3,
|
|
181
|
+
maxArgs: 3,
|
|
182
|
+
description: "Returns one value if a condition is true and another if false",
|
|
183
|
+
examples: ['IF(A1>10, "High", "Low")', "IF(B2>=5, SUM(C1:C10), 0)"],
|
|
184
|
+
category: "Logical",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
functionRegistry.register({
|
|
188
|
+
name: "AND",
|
|
189
|
+
handler: andHandler,
|
|
190
|
+
minArgs: 1,
|
|
191
|
+
description: "Returns TRUE if all arguments are true",
|
|
192
|
+
examples: ["AND(A1>5, B1<10)", "AND(A1>0, B1>0, C1>0)"],
|
|
193
|
+
category: "Logical",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
functionRegistry.register({
|
|
197
|
+
name: "OR",
|
|
198
|
+
handler: orHandler,
|
|
199
|
+
minArgs: 1,
|
|
200
|
+
description: "Returns TRUE if any argument is true",
|
|
201
|
+
examples: ["OR(A1>5, B1<10)", "OR(A1>0, B1>0)"],
|
|
202
|
+
category: "Logical",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
functionRegistry.register({
|
|
206
|
+
name: "NOT",
|
|
207
|
+
handler: notHandler,
|
|
208
|
+
minArgs: 1,
|
|
209
|
+
maxArgs: 1,
|
|
210
|
+
description: "Reverses the logical value of its argument",
|
|
211
|
+
examples: ["NOT(A1>5)", "NOT(B1)"],
|
|
212
|
+
category: "Logical",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
functionRegistry.register({
|
|
216
|
+
name: "IFERROR",
|
|
217
|
+
handler: iferrorHandler,
|
|
218
|
+
minArgs: 2,
|
|
219
|
+
maxArgs: 2,
|
|
220
|
+
description: "Returns a value if expression is an error, otherwise returns the expression",
|
|
221
|
+
examples: ["IFERROR(A1/B1, 0)", 'IFERROR(VLOOKUP(A1, B1:C10, 2), "Not found")'],
|
|
222
|
+
category: "Logical",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
functionRegistry.register({
|
|
226
|
+
name: "IFNA",
|
|
227
|
+
handler: ifnaHandler,
|
|
228
|
+
minArgs: 2,
|
|
229
|
+
maxArgs: 2,
|
|
230
|
+
description: "Returns a value if expression is #N/A, otherwise returns the expression",
|
|
231
|
+
examples: ['IFNA(A1, "N/A")', "IFNA(MATCH(A1, B1:B10), 0)"],
|
|
232
|
+
category: "Logical",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
functionRegistry.register({
|
|
236
|
+
name: "IFS",
|
|
237
|
+
handler: ifsHandler,
|
|
238
|
+
minArgs: 2,
|
|
239
|
+
description: "Checks multiple conditions and returns the first true result",
|
|
240
|
+
examples: ['IFS(A1>90, "A", A1>80, "B", A1>70, "C")', 'IFS(B1="Yes", 1, B1="No", 0)'],
|
|
241
|
+
category: "Logical",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
functionRegistry.register({
|
|
245
|
+
name: "TRUE",
|
|
246
|
+
handler: trueHandler,
|
|
247
|
+
minArgs: 0,
|
|
248
|
+
maxArgs: 0,
|
|
249
|
+
description: "Returns the logical value TRUE",
|
|
250
|
+
examples: ["TRUE()", "IF(A1>0, TRUE(), FALSE())"],
|
|
251
|
+
category: "Logical",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
functionRegistry.register({
|
|
255
|
+
name: "FALSE",
|
|
256
|
+
handler: falseHandler,
|
|
257
|
+
minArgs: 0,
|
|
258
|
+
maxArgs: 0,
|
|
259
|
+
description: "Returns the logical value FALSE",
|
|
260
|
+
examples: ["FALSE()", "IF(A1>0, TRUE(), FALSE())"],
|
|
261
|
+
category: "Logical",
|
|
262
|
+
});
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lookup and Reference Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { functionRegistry, toNumber, parseCriteria, type FunctionHandler } from "../registry";
|
|
6
|
+
import type { CellValue } from "../types";
|
|
7
|
+
|
|
8
|
+
// Helper to convert Excel column letters to 0-based index (A=0, Z=25, AA=26, etc.)
|
|
9
|
+
const colToIndex = (col: string): number => {
|
|
10
|
+
let result = 0;
|
|
11
|
+
for (let i = 0; i < col.length; i++) {
|
|
12
|
+
result = result * 26 + (col.charCodeAt(i) - 64);
|
|
13
|
+
}
|
|
14
|
+
return result - 1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper to convert 0-based index to Excel column letters (0=A, 25=Z, 26=AA, etc.)
|
|
18
|
+
const indexToCol = (index: number): string => {
|
|
19
|
+
let col = "";
|
|
20
|
+
let num = index + 1;
|
|
21
|
+
while (num > 0) {
|
|
22
|
+
const remainder = (num - 1) % 26;
|
|
23
|
+
col = String.fromCharCode(65 + remainder) + col;
|
|
24
|
+
num = Math.floor((num - 1) / 26);
|
|
25
|
+
}
|
|
26
|
+
return col;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Helper to find match index
|
|
30
|
+
const findMatchIndex = (
|
|
31
|
+
lookupValue: CellValue,
|
|
32
|
+
lookupArray: CellValue[],
|
|
33
|
+
matchType: number = 1, // 1 = less than (sorted asc), 0 = exact, -1 = greater than (sorted desc)
|
|
34
|
+
searchMode: number = 1, // 1 = first to last, -1 = last to first (for XLOOKUP)
|
|
35
|
+
): number => {
|
|
36
|
+
const compare = (a: CellValue, b: CellValue) => {
|
|
37
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
38
|
+
return String(a).localeCompare(String(b));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Exact match
|
|
42
|
+
if (matchType === 0) {
|
|
43
|
+
// Handle wildcards for strings if it's an exact match request
|
|
44
|
+
if (typeof lookupValue === "string" && (lookupValue.includes("*") || lookupValue.includes("?"))) {
|
|
45
|
+
const criteriaFn = parseCriteria(lookupValue);
|
|
46
|
+
|
|
47
|
+
if (searchMode === 1) {
|
|
48
|
+
return lookupArray.findIndex((item) => criteriaFn(item));
|
|
49
|
+
} else {
|
|
50
|
+
for (let i = lookupArray.length - 1; i >= 0; i--) {
|
|
51
|
+
if (criteriaFn(lookupArray[i])) return i;
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (searchMode === 1) {
|
|
58
|
+
return lookupArray.findIndex((item) => item == lookupValue); // Loose equality for "10" == 10
|
|
59
|
+
} else {
|
|
60
|
+
for (let i = lookupArray.length - 1; i >= 0; i--) {
|
|
61
|
+
if (lookupArray[i] == lookupValue) return i;
|
|
62
|
+
}
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Approximate match (requires sorted array)
|
|
68
|
+
// We'll assume the user knows what they are doing regarding sorting, as per Excel behavior
|
|
69
|
+
|
|
70
|
+
if (matchType === 1) {
|
|
71
|
+
// Less than or equal to
|
|
72
|
+
// Array must be sorted ascending
|
|
73
|
+
let bestIdx = -1;
|
|
74
|
+
for (let i = 0; i < lookupArray.length; i++) {
|
|
75
|
+
const item = lookupArray[i];
|
|
76
|
+
if (compare(item, lookupValue) <= 0) {
|
|
77
|
+
bestIdx = i;
|
|
78
|
+
} else {
|
|
79
|
+
// Since it's sorted ascending, once we exceed, we can stop
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return bestIdx;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (matchType === -1) {
|
|
87
|
+
// Greater than or equal to
|
|
88
|
+
// Array must be sorted descending
|
|
89
|
+
let bestIdx = -1;
|
|
90
|
+
for (let i = 0; i < lookupArray.length; i++) {
|
|
91
|
+
const item = lookupArray[i];
|
|
92
|
+
if (compare(item, lookupValue) >= 0) {
|
|
93
|
+
bestIdx = i;
|
|
94
|
+
} else {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return bestIdx;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return -1;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const vlookupHandler: FunctionHandler = (args, context) => {
|
|
105
|
+
if (args.length < 3 || args.length > 4) {
|
|
106
|
+
throw new Error("VLOOKUP requires 3 or 4 arguments");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lookupValue = context.evaluateFormula(args[0]);
|
|
110
|
+
const tableArrayRange = args[1];
|
|
111
|
+
const colIndexNum = toNumber(context.evaluateFormula(args[2]));
|
|
112
|
+
const rangeLookup = args.length === 4 ? context.evaluateFormula(args[3]) : true;
|
|
113
|
+
|
|
114
|
+
// Convert rangeLookup to boolean/number logic
|
|
115
|
+
// TRUE/1/omitted = approximate match (default)
|
|
116
|
+
// FALSE/0 = exact match
|
|
117
|
+
const isApprox = rangeLookup === true || rangeLookup === 1 || rangeLookup === "1";
|
|
118
|
+
const matchType = isApprox ? 1 : 0;
|
|
119
|
+
|
|
120
|
+
// Get the full table data
|
|
121
|
+
// We need to parse the range string to get dimensions
|
|
122
|
+
const match = tableArrayRange.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
123
|
+
if (!match) throw new Error("Invalid table array range");
|
|
124
|
+
|
|
125
|
+
// We need to get the first column for looking up
|
|
126
|
+
// And the specific column for the result
|
|
127
|
+
// This is a bit tricky with the current getRangeValues which flattens everything
|
|
128
|
+
// We need to manually reconstruct the table structure or request specific cells
|
|
129
|
+
|
|
130
|
+
// Let's parse the range to get start/end col/row
|
|
131
|
+
// Note: This relies on the context.getCellValue implementation details or we need to implement
|
|
132
|
+
// a smarter way to get 2D data.
|
|
133
|
+
// For now, we will iterate row by row.
|
|
134
|
+
|
|
135
|
+
// Parse range manually to get boundaries
|
|
136
|
+
// We can't easily use context.getRangeValues because it flattens 2D arrays to 1D
|
|
137
|
+
// So we will iterate through the rows of the first column
|
|
138
|
+
|
|
139
|
+
// Extract sheet name if present
|
|
140
|
+
let sheetName = "";
|
|
141
|
+
let rangePart = tableArrayRange;
|
|
142
|
+
if (tableArrayRange.includes("!")) {
|
|
143
|
+
const parts = tableArrayRange.split("!");
|
|
144
|
+
sheetName = parts[0] + "!";
|
|
145
|
+
rangePart = parts[1];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
149
|
+
if (!rangeMatch) throw new Error("Invalid range format");
|
|
150
|
+
|
|
151
|
+
const startColStr = rangeMatch[1];
|
|
152
|
+
const startRow = parseInt(rangeMatch[2]);
|
|
153
|
+
const endRow = parseInt(rangeMatch[4]);
|
|
154
|
+
|
|
155
|
+
const startColIdx = colToIndex(startColStr);
|
|
156
|
+
const resultColIdx = startColIdx + colIndexNum - 1;
|
|
157
|
+
const resultColStr = indexToCol(resultColIdx);
|
|
158
|
+
|
|
159
|
+
// Build lookup array (first column)
|
|
160
|
+
const lookupArray: CellValue[] = [];
|
|
161
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
162
|
+
const cellRef = `${sheetName}${startColStr}${r}`;
|
|
163
|
+
lookupArray.push(context.getCellValue(cellRef));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const matchIdx = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
167
|
+
|
|
168
|
+
if (matchIdx === -1) return "#N/A";
|
|
169
|
+
|
|
170
|
+
// Get result
|
|
171
|
+
const resultRow = startRow + matchIdx;
|
|
172
|
+
const resultRef = `${sheetName}${resultColStr}${resultRow}`;
|
|
173
|
+
|
|
174
|
+
return context.getCellValue(resultRef);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const hlookupHandler: FunctionHandler = (args, context) => {
|
|
178
|
+
if (args.length < 3 || args.length > 4) {
|
|
179
|
+
throw new Error("HLOOKUP requires 3 or 4 arguments");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const lookupValue = context.evaluateFormula(args[0]);
|
|
183
|
+
const tableArrayRange = args[1];
|
|
184
|
+
const rowIndexNum = toNumber(context.evaluateFormula(args[2]));
|
|
185
|
+
const rangeLookup = args.length === 4 ? context.evaluateFormula(args[3]) : true;
|
|
186
|
+
|
|
187
|
+
const isApprox = rangeLookup === true || rangeLookup === 1 || rangeLookup === "1";
|
|
188
|
+
const matchType = isApprox ? 1 : 0;
|
|
189
|
+
|
|
190
|
+
// Extract sheet name if present
|
|
191
|
+
let sheetName = "";
|
|
192
|
+
let rangePart = tableArrayRange;
|
|
193
|
+
if (tableArrayRange.includes("!")) {
|
|
194
|
+
const parts = tableArrayRange.split("!");
|
|
195
|
+
sheetName = parts[0] + "!";
|
|
196
|
+
rangePart = parts[1];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
200
|
+
if (!rangeMatch) throw new Error("Invalid range format");
|
|
201
|
+
|
|
202
|
+
const startColStr = rangeMatch[1];
|
|
203
|
+
const endColStr = rangeMatch[3];
|
|
204
|
+
const startRow = parseInt(rangeMatch[2]);
|
|
205
|
+
|
|
206
|
+
const startColIdx = colToIndex(startColStr);
|
|
207
|
+
const endColIdx = colToIndex(endColStr);
|
|
208
|
+
|
|
209
|
+
// Build lookup array (first row)
|
|
210
|
+
const lookupArray: CellValue[] = [];
|
|
211
|
+
for (let c = startColIdx; c <= endColIdx; c++) {
|
|
212
|
+
const colStr = indexToCol(c);
|
|
213
|
+
const cellRef = `${sheetName}${colStr}${startRow}`;
|
|
214
|
+
lookupArray.push(context.getCellValue(cellRef));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const matchIdx = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
218
|
+
|
|
219
|
+
if (matchIdx === -1) return "#N/A";
|
|
220
|
+
|
|
221
|
+
// Get result
|
|
222
|
+
const resultColIdx = startColIdx + matchIdx;
|
|
223
|
+
const resultColStr = indexToCol(resultColIdx);
|
|
224
|
+
const resultRow = startRow + rowIndexNum - 1;
|
|
225
|
+
const resultRef = `${sheetName}${resultColStr}${resultRow}`;
|
|
226
|
+
|
|
227
|
+
return context.getCellValue(resultRef);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const matchHandler: FunctionHandler = (args, context) => {
|
|
231
|
+
if (args.length < 2 || args.length > 3) {
|
|
232
|
+
throw new Error("MATCH requires 2 or 3 arguments");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const lookupValue = context.evaluateFormula(args[0]);
|
|
236
|
+
const lookupArrayRange = args[1];
|
|
237
|
+
const matchType = args.length === 3 ? toNumber(context.evaluateFormula(args[2])) : 1;
|
|
238
|
+
|
|
239
|
+
const lookupArray = context.getRangeValues(lookupArrayRange);
|
|
240
|
+
|
|
241
|
+
const index = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
242
|
+
|
|
243
|
+
return index === -1 ? "#N/A" : index + 1; // 1-based index
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const indexHandler: FunctionHandler = (args, context) => {
|
|
247
|
+
if (args.length < 2 || args.length > 4) {
|
|
248
|
+
throw new Error("INDEX requires 2 to 4 arguments");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const arrayRange = args[0];
|
|
252
|
+
const rowNum = toNumber(context.evaluateFormula(args[1]));
|
|
253
|
+
const colNum = args.length >= 3 ? toNumber(context.evaluateFormula(args[2])) : 1; // Default to 1 if omitted (for 1D arrays)
|
|
254
|
+
|
|
255
|
+
// Parse range to find the specific cell
|
|
256
|
+
let sheetName = "";
|
|
257
|
+
let rangePart = arrayRange;
|
|
258
|
+
if (arrayRange.includes("!")) {
|
|
259
|
+
const parts = arrayRange.split("!");
|
|
260
|
+
sheetName = parts[0] + "!";
|
|
261
|
+
rangePart = parts[1];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
265
|
+
if (!rangeMatch) throw new Error("Invalid range format");
|
|
266
|
+
|
|
267
|
+
const startColStr = rangeMatch[1];
|
|
268
|
+
const startRow = parseInt(rangeMatch[2]);
|
|
269
|
+
|
|
270
|
+
const startColIdx = colToIndex(startColStr);
|
|
271
|
+
|
|
272
|
+
// Calculate target cell
|
|
273
|
+
// rowNum and colNum are 1-based relative to the range
|
|
274
|
+
const targetRow = startRow + rowNum - 1;
|
|
275
|
+
const targetColIdx = startColIdx + colNum - 1;
|
|
276
|
+
const targetColStr = indexToCol(targetColIdx);
|
|
277
|
+
|
|
278
|
+
const ref = `${sheetName}${targetColStr}${targetRow}`;
|
|
279
|
+
return context.getCellValue(ref);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const xlookupHandler: FunctionHandler = (args, context) => {
|
|
283
|
+
if (args.length < 3 || args.length > 6) {
|
|
284
|
+
throw new Error("XLOOKUP requires 3 to 6 arguments");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const lookupValue = context.evaluateFormula(args[0]);
|
|
288
|
+
const lookupArrayRange = args[1];
|
|
289
|
+
const returnArrayRange = args[2];
|
|
290
|
+
const ifNotFound = args.length >= 4 ? context.evaluateFormula(args[3]) : "#N/A";
|
|
291
|
+
const matchMode = args.length >= 5 ? toNumber(context.evaluateFormula(args[4])) : 0;
|
|
292
|
+
const searchMode = args.length >= 6 ? toNumber(context.evaluateFormula(args[5])) : 1;
|
|
293
|
+
|
|
294
|
+
const lookupArray = context.getRangeValues(lookupArrayRange);
|
|
295
|
+
const returnArray = context.getRangeValues(returnArrayRange);
|
|
296
|
+
|
|
297
|
+
// XLOOKUP match modes:
|
|
298
|
+
// 0 = Exact match (default)
|
|
299
|
+
// -1 = Exact match or next smaller
|
|
300
|
+
// 1 = Exact match or next larger
|
|
301
|
+
// 2 = Wildcard match
|
|
302
|
+
|
|
303
|
+
// Map XLOOKUP modes to our internal findMatchIndex modes
|
|
304
|
+
// Our internal: 0=exact, 1=less than (sorted), -1=greater than (sorted)
|
|
305
|
+
// XLOOKUP is more complex because it doesn't require sorted arrays for next smaller/larger
|
|
306
|
+
// For now, we'll implement exact (0) and wildcard (2 -> handled by exact with wildcard logic in findMatchIndex)
|
|
307
|
+
// For -1 and 1, we'll do a linear search for best match if not sorted
|
|
308
|
+
|
|
309
|
+
let matchIdx = -1;
|
|
310
|
+
|
|
311
|
+
if (matchMode === 0 || matchMode === 2) {
|
|
312
|
+
matchIdx = findMatchIndex(lookupValue, lookupArray, 0, searchMode);
|
|
313
|
+
} else {
|
|
314
|
+
// Implement exact or next smaller/larger for unsorted arrays
|
|
315
|
+
// This is O(N)
|
|
316
|
+
let bestDiff = Infinity;
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < lookupArray.length; i++) {
|
|
319
|
+
const idx = searchMode === 1 ? i : lookupArray.length - 1 - i;
|
|
320
|
+
const item = lookupArray[idx];
|
|
321
|
+
|
|
322
|
+
if (item == lookupValue) {
|
|
323
|
+
matchIdx = idx;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (typeof item === "number" && typeof lookupValue === "number") {
|
|
328
|
+
const diff = item - lookupValue;
|
|
329
|
+
if (matchMode === -1 && diff < 0 && Math.abs(diff) < bestDiff) {
|
|
330
|
+
// Next smaller (closest negative difference)
|
|
331
|
+
bestDiff = Math.abs(diff);
|
|
332
|
+
matchIdx = idx;
|
|
333
|
+
} else if (matchMode === 1 && diff > 0 && diff < bestDiff) {
|
|
334
|
+
// Next larger (closest positive difference)
|
|
335
|
+
bestDiff = diff;
|
|
336
|
+
matchIdx = idx;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (matchIdx === -1) return ifNotFound;
|
|
343
|
+
|
|
344
|
+
if (matchIdx >= 0 && matchIdx < returnArray.length) {
|
|
345
|
+
return returnArray[matchIdx];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return "#N/A";
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Register functions
|
|
352
|
+
functionRegistry.register({
|
|
353
|
+
name: "VLOOKUP",
|
|
354
|
+
handler: vlookupHandler,
|
|
355
|
+
minArgs: 3,
|
|
356
|
+
maxArgs: 4,
|
|
357
|
+
description: "Looks for a value in the leftmost column of a table, and then returns a value in the same row from a column you specify",
|
|
358
|
+
examples: ["VLOOKUP(105, A2:C10, 2)", 'VLOOKUP("Smith", A2:E10, 5, FALSE)'],
|
|
359
|
+
category: "Lookup & Reference",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
functionRegistry.register({
|
|
363
|
+
name: "HLOOKUP",
|
|
364
|
+
handler: hlookupHandler,
|
|
365
|
+
minArgs: 3,
|
|
366
|
+
maxArgs: 4,
|
|
367
|
+
description: "Looks for a value in the top row of a table, and then returns a value in the same column from a row you specify",
|
|
368
|
+
examples: ['HLOOKUP("Axles", A1:C10, 2, TRUE)'],
|
|
369
|
+
category: "Lookup & Reference",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
functionRegistry.register({
|
|
373
|
+
name: "MATCH",
|
|
374
|
+
handler: matchHandler,
|
|
375
|
+
minArgs: 2,
|
|
376
|
+
maxArgs: 3,
|
|
377
|
+
description: "Returns the relative position of an item in an array that matches a specified value",
|
|
378
|
+
examples: ["MATCH(25, A1:A10, 0)", 'MATCH("b", A1:A5, 0)'],
|
|
379
|
+
category: "Lookup & Reference",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
functionRegistry.register({
|
|
383
|
+
name: "INDEX",
|
|
384
|
+
handler: indexHandler,
|
|
385
|
+
minArgs: 2,
|
|
386
|
+
maxArgs: 4,
|
|
387
|
+
description: "Returns the value of an element in a table or an array, selected by the row and column number indexes",
|
|
388
|
+
examples: ["INDEX(A1:B5, 2, 2)", "INDEX(A1:A10, 5)"],
|
|
389
|
+
category: "Lookup & Reference",
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
functionRegistry.register({
|
|
393
|
+
name: "XLOOKUP",
|
|
394
|
+
handler: xlookupHandler,
|
|
395
|
+
minArgs: 3,
|
|
396
|
+
maxArgs: 6,
|
|
397
|
+
description: "Searches a range or an array, and returns an item corresponding to the first match it finds",
|
|
398
|
+
examples: ["XLOOKUP(A1, B1:B10, C1:C10)", 'XLOOKUP("USA", Countries, Populations)'],
|
|
399
|
+
category: "Lookup & Reference",
|
|
400
|
+
});
|