diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b991883dc..0593b8e20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,6 +28,9 @@ jobs: - name: Install dependencies run: bun install + - name: Run tests + run: bun test + - name: Build run: bun run build diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0fbb4b4..e03e3c135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.0.0-111 + + +### 🚀 Enhancements + +- Support formula field aggregate ([b1ceece](https://github.com/undb-io/undb/commit/b1ceece)) + +### ❤️ Contributors + +- Nichenqin ([@nichenqin](http://github.com/nichenqin)) + ## v1.0.0-110 ## v1.0.0-109 diff --git a/apps/frontend/package.json b/apps/frontend/package.json index c560e59e0..7fc0be1bd 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -22,6 +22,7 @@ "@sveltejs/adapter-static": "^3.0.5", "@sveltejs/kit": "^2.7.1", "@sveltejs/vite-plugin-svelte": "^3.1.2", + "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-query": "^5.59.7", "@types/eslint": "^8.56.12", "@types/lodash.unzip": "^3.4.9", @@ -31,6 +32,7 @@ "@typescript-eslint/parser": "^8.10.0", "@undb/commands": "workspace:*", "@undb/domain": "workspace:*", + "@undb/formula": "workspace:*", "@undb/i18n": "workspace:*", "@undb/openapi": "workspace:*", "@undb/queries": "workspace:*", @@ -73,11 +75,16 @@ "type-fest": "^4.26.1", "typescript": "^5.6.3", "vite": "^5.4.9", + "vite-plugin-node-polyfills": "^0.22.0", "vitest": "^2.1.3", "xlsx": "^0.18.5" }, "type": "module", "dependencies": { + "@codemirror/commands": "^6.7.1", + "@codemirror/language": "^6.10.3", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.1", "@formkit/auto-animate": "^0.8.2", "@internationalized/date": "^3.5.6", "@svelte-put/clickoutside": "^3.0.2", diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index aaadf383c..da9321342 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -44,6 +44,7 @@ type Field { defaultValue: JSON display: Boolean id: ID! + metadata: JSON name: String! option: JSON type: FieldType! diff --git a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte index 6183daf17..548fe4def 100644 --- a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte +++ b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte @@ -1,6 +1,5 @@ + +
+
+ +
+ +
diff --git a/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte b/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte index 40a7af841..3aaf341e0 100644 --- a/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte +++ b/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte @@ -1,6 +1,10 @@ {#if !value} @@ -8,5 +12,13 @@ {placeholder || ""} {:else} -
{value}
+
+ {#if field.allowRichText} +
+ {@html value} +
+ {:else} + {value} + {/if} +
{/if} diff --git a/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql b/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql index 1c8b5b17c..4ab0f3719 100644 --- a/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql +++ b/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql @@ -9,6 +9,7 @@ query GetRollupForeignTables($tableId: ID!, $fieldId: ID!) { constraint option display + metadata } views { id diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte index 4b829bbe6..e876a0822 100644 --- a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte +++ b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte @@ -269,6 +269,39 @@ is_not_empty: null, } + $: formula = {} + + $: if (field?.type === "formula") { + if (field.returnType === "number") { + formula = { + eq: NumberInput, + neq: NumberInput, + gt: NumberInput, + gte: NumberInput, + lt: NumberInput, + lte: NumberInput, + is_empty: null, + is_not_empty: null, + } + } else if (field.returnType === "boolean") { + formula = { + is_true: null, + is_false: null, + } + } else if (field.returnType === "string") { + formula = { + eq: Input, + neq: Input, + contains: Input, + does_not_contain: Input, + starts_with: Input, + ends_with: Input, + is_empty: null, + is_not_empty: null, + } + } + } + $: filterFieldInput = { string, number, @@ -289,6 +322,7 @@ url, longText, duration, + formula, percentage, } diff --git a/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql b/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql index 09ff709b2..7983411ea 100644 --- a/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql +++ b/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql @@ -16,6 +16,7 @@ query GetForeignTable($tableId: ID!) { display constraint option + metadata } views { diff --git a/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts b/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts new file mode 100644 index 000000000..d877455f5 --- /dev/null +++ b/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts @@ -0,0 +1,117 @@ +import { + AbstractParseTreeVisitor, + AddSubExprContext, + AndExprContext, + ArgumentListContext, + ComparisonExprContext, + ExpressionContext, + FormulaContext, + FormulaParserVisitor, + FunctionCallContext, + FunctionExprContext, + MulDivModExprContext, + NotExprContext, + OrExprContext, + type ParseTree, + VariableContext, +} from "@undb/formula" + +export class FormulaCursorVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor { + private pathNodes: ParseTree[] = [] + private variables: Set = new Set() + public readonly targetPosition: number + + constructor(position: number) { + super() + this.targetPosition = position + } + + public hasAggumentList(): boolean { + return this.pathNodes.some((node) => node instanceof ArgumentListContext) + } + + public hasFunctionCall(): boolean { + return this.pathNodes.some((node) => node instanceof FunctionCallContext) + } + + public getNearestFunctionNode() { + for (let i = this.pathNodes.length - 1; i >= 0; i--) { + const node = this.pathNodes[i] + if (node instanceof FunctionCallContext) { + return node + } + } + return null + } + + public getFunctionName(): string | undefined { + const functionCall = this.getNearestFunctionNode() + return functionCall?.IDENTIFIER()?.text + } + + protected defaultResult(): void { + return undefined + } + + public getPathNodes() { + return this.pathNodes + } + + visitPositionInRange(ctx: ExpressionContext) { + if (!ctx.start || !ctx.stop) return + + const start = ctx.start.startIndex + const stop = ctx.stop.stopIndex + const isPositionWithinRange = start <= this.targetPosition && stop >= this.targetPosition + + if (isPositionWithinRange) { + this.pathNodes.push(ctx) + this.visitChildren(ctx) + } + } + + visitFormula(ctx: FormulaContext) { + this.visitPositionInRange(ctx) + } + + visitComparisonExpr(ctx: ComparisonExprContext) { + this.visitPositionInRange(ctx) + } + + visitAndExpr(ctx: AndExprContext) { + this.visitPositionInRange(ctx) + } + + visitOrExpr(ctx: OrExprContext) { + this.visitPositionInRange(ctx) + } + + visitNotExpr(ctx: NotExprContext) { + this.visitPositionInRange(ctx) + } + + visitMulDivModExpr(ctx: MulDivModExprContext) { + this.visitPositionInRange(ctx) + } + + visitAddSubExpr(ctx: AddSubExprContext) { + this.visitPositionInRange(ctx) + } + + visitFunctionExpr(ctx: FunctionExprContext) { + this.visitPositionInRange(ctx) + } + + visitFunctionCall(ctx: FunctionCallContext) { + this.visitPositionInRange(ctx) + } + + visitArgumentList(ctx: ArgumentListContext) { + this.visitPositionInRange(ctx) + } + + visitVariable(ctx: VariableContext) { + this.variables.add(ctx.IDENTIFIER().text) + this.visitPositionInRange(ctx) + } +} diff --git a/apps/frontend/src/lib/components/formula/formula-editor.svelte b/apps/frontend/src/lib/components/formula/formula-editor.svelte new file mode 100644 index 000000000..bd064b2f5 --- /dev/null +++ b/apps/frontend/src/lib/components/formula/formula-editor.svelte @@ -0,0 +1,431 @@ + + +
+
+ {#if errorMessage} +

+ + {errorMessage} +

+ {/if} + +
    + {#each suggestions as suggestion} + {@const isSelected = suggestion === selectedSuggestion} + {@const isFunction = functions.includes(suggestion)} + {@const isField = !isFunction} + + {/each} +
+
+ + diff --git a/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts b/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts new file mode 100644 index 000000000..5d5e1d6b3 --- /dev/null +++ b/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts @@ -0,0 +1,50 @@ +import { StateField } from "@codemirror/state" +import { Decoration, DecorationSet, EditorView, WidgetType } from "@codemirror/view" +import { TableDo } from "@undb/table" +import { variable } from "../style" + +const variableField = (table: TableDo) => + StateField.define({ + create() { + return Decoration.none + }, + update(decorations, tr) { + decorations = decorations.map(tr.changes) + + let matches = [] + let content = tr.state.doc.toString() + const regex = /\{\{([^}]+)\}\}/g + let match + + while ((match = regex.exec(content)) !== null) { + const start = match.index + const end = match.index + match[0].length + const fieldId = match[1].trim() + const field = table.schema.getFieldByIdOrName(fieldId).into(null) + if (!field) continue + + // 创建替换文本装饰器 + const fieldName = field.name.value + matches.push( + Decoration.replace({ + widget: new (class extends WidgetType { + toDOM() { + const span = document.createElement("span") + span.textContent = fieldName + span.className = variable() + return span + } + })(), + }).range(start, end), + ) + } + + // 确保装饰器按照 from 位置排序 + matches.sort((a, b) => a.from - b.from) + + return Decoration.set(matches, true) + }, + provide: (f) => EditorView.decorations.from(f), + }) + +export const templateVariablePlugin = (table: TableDo) => [variableField(table)] diff --git a/apps/frontend/src/lib/components/formula/style.ts b/apps/frontend/src/lib/components/formula/style.ts new file mode 100644 index 000000000..d6df55c40 --- /dev/null +++ b/apps/frontend/src/lib/components/formula/style.ts @@ -0,0 +1,5 @@ +import { tv } from "tailwind-variants" + +export const variable = tv({ + base: "bg-blue-50 hover:bg-blue-100 rounded px-1 border border-blue-200 mx-[1px] transition-all duration-200 ease-in-out hover:shadow-sm hover:border-blue-300 cursor-pointer", +}) diff --git a/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql b/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql index 4bf44c974..9eeaf435d 100644 --- a/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql +++ b/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql @@ -14,6 +14,7 @@ query GetTableForeignTables($tableId: ID!) { display constraint option + metadata } views { id diff --git a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql index 7ce7cc4dc..3c945d40a 100644 --- a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql +++ b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql @@ -11,6 +11,7 @@ query GetTableQuery($tableId: ID!, $viewId: ID) { display constraint option + metadata } views { diff --git a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql index 4f820326f..a76f7e04c 100644 --- a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql @@ -51,6 +51,7 @@ query GetBaseTableShareData($shareId: ID!, $tableId: ID!) { name option type + metadata } } } diff --git a/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql index c7ada0d91..58fc0c454 100644 --- a/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql @@ -44,6 +44,7 @@ query GetFormShareData($shareId: ID!) { name option type + metadata } } } diff --git a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql index 228fd4474..f9689aea4 100644 --- a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql @@ -65,6 +65,7 @@ query GetViewShareData($shareId: ID!) { name option type + metadata } } } diff --git a/apps/frontend/svelte.config.js b/apps/frontend/svelte.config.js index ebe200df0..57d59a3b3 100644 --- a/apps/frontend/svelte.config.js +++ b/apps/frontend/svelte.config.js @@ -7,6 +7,10 @@ const config = { // for more information about preprocessors preprocess: vitePreprocess(), + vitePlugin: { + exclude: ["@undb/formula"], + }, + kit: { adapter: adapter({ pages: "dist", diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index fa1cb36b6..59ee0cca7 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -1,64 +1,65 @@ -import { fontFamily } from "tailwindcss/defaultTheme"; +import { fontFamily } from "tailwindcss/defaultTheme" /** @type {import('tailwindcss').Config} */ const config = { - darkMode: ["class"], - content: ["./src/**/*.{html,js,svelte,ts}"], - safelist: ["dark"], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px" - } - }, - extend: { - colors: { - border: "hsl(var(--border) / )", - input: "hsl(var(--input) / )", - ring: "hsl(var(--ring) / )", - background: "hsl(var(--background) / )", - foreground: "hsl(var(--foreground) / )", - primary: { - DEFAULT: "hsl(var(--primary) / )", - foreground: "hsl(var(--primary-foreground) / )" - }, - secondary: { - DEFAULT: "hsl(var(--secondary) / )", - foreground: "hsl(var(--secondary-foreground) / )" - }, - destructive: { - DEFAULT: "hsl(var(--destructive) / )", - foreground: "hsl(var(--destructive-foreground) / )" - }, - muted: { - DEFAULT: "hsl(var(--muted) / )", - foreground: "hsl(var(--muted-foreground) / )" - }, - accent: { - DEFAULT: "hsl(var(--accent) / )", - foreground: "hsl(var(--accent-foreground) / )" - }, - popover: { - DEFAULT: "hsl(var(--popover) / )", - foreground: "hsl(var(--popover-foreground) / )" - }, - card: { - DEFAULT: "hsl(var(--card) / )", - foreground: "hsl(var(--card-foreground) / )" - } - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)" - }, - fontFamily: { - sans: [...fontFamily.sans] - } - } - }, -}; + darkMode: ["class"], + content: ["./src/**/*.{html,js,svelte,ts}"], + safelist: ["dark"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border) / )", + input: "hsl(var(--input) / )", + ring: "hsl(var(--ring) / )", + background: "hsl(var(--background) / )", + foreground: "hsl(var(--foreground) / )", + primary: { + DEFAULT: "hsl(var(--primary) / )", + foreground: "hsl(var(--primary-foreground) / )", + }, + secondary: { + DEFAULT: "hsl(var(--secondary) / )", + foreground: "hsl(var(--secondary-foreground) / )", + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / )", + foreground: "hsl(var(--destructive-foreground) / )", + }, + muted: { + DEFAULT: "hsl(var(--muted) / )", + foreground: "hsl(var(--muted-foreground) / )", + }, + accent: { + DEFAULT: "hsl(var(--accent) / )", + foreground: "hsl(var(--accent-foreground) / )", + }, + popover: { + DEFAULT: "hsl(var(--popover) / )", + foreground: "hsl(var(--popover-foreground) / )", + }, + card: { + DEFAULT: "hsl(var(--card) / )", + foreground: "hsl(var(--card-foreground) / )", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: [...fontFamily.sans], + }, + }, + }, + plugins: [require("@tailwindcss/typography")], +} -export default config; +export default config diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index df2d6ba36..78833c830 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -10,6 +10,7 @@ "noEmit": true, "moduleResolution": "bundler", "experimentalDecorators": true, + "verbatimModuleSyntax": false, "emitDecoratorMetadata": true, "rootDirs": [".", "./.svelte-kit/types", "./$houdini/types", "./$houdini"] } diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 80caaba37..4358a1f32 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,12 +1,19 @@ import { sveltekit } from "@sveltejs/kit/vite" import houdini from "houdini/vite" import { visualizer } from "rollup-plugin-visualizer" +import { nodePolyfills } from "vite-plugin-node-polyfills" import { defineConfig } from "vitest/config" export default defineConfig({ plugins: [ houdini(), sveltekit(), + nodePolyfills({ + include: ["assert"], + globals: { + process: true, + }, + }), visualizer({ emitFile: true, filename: "stats.html", diff --git a/bun.lockb b/bun.lockb index c034b4429..508ca4655 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5e31400b4..d85a9746d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undb", - "version": "1.0.0-110", + "version": "1.0.0-111", "private": true, "scripts": { "build": "NODE_ENV=production bun --bun turbo build", diff --git a/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap b/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap deleted file mode 100644 index b67f58017..000000000 --- a/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`base fixture test > create test base success 1`] = ` -Base { - "id": BaseId { - "props": { - "value": "baseId", - }, - }, - "name": BaseName { - "props": { - "value": "name", - }, - }, -} -`; - -exports[`base fixture test create test base success 1`] = ` -Base { - "id": BaseId { - "props": { - "value": "basttrlpxe2", - }, - }, - "name": BaseName { - "props": { - "value": undefined, - }, - }, - "option": BaseOption { - "props": {}, - }, - "spaceId": undefined, -} -`; diff --git a/packages/base/src/fixtures/base.fixture.test.ts b/packages/base/src/fixtures/base.fixture.test.ts deleted file mode 100644 index 8f968520a..000000000 --- a/packages/base/src/fixtures/base.fixture.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createTestBase } from "./base.fixture" - -describe("base fixture test", () => { - test("create test base success", () => { - const base = createTestBase() - - expect(base).toMatchSnapshot() - }) -}) diff --git a/packages/domain/src/query.test.ts b/packages/domain/src/query.test.ts index 33f858bee..745360977 100644 --- a/packages/domain/src/query.test.ts +++ b/packages/domain/src/query.test.ts @@ -1,3 +1,5 @@ +import { expect, test } from "bun:test" + test("test", () => { expect(1).toBe(1) }) diff --git a/packages/formula/.gitignore b/packages/formula/.gitignore new file mode 100644 index 000000000..c10d3591e --- /dev/null +++ b/packages/formula/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +.antlr \ No newline at end of file diff --git a/packages/formula/README.md b/packages/formula/README.md new file mode 100644 index 000000000..8c766f5e0 --- /dev/null +++ b/packages/formula/README.md @@ -0,0 +1,15 @@ +# @undb/formula + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.31. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/formula/package.json b/packages/formula/package.json new file mode 100644 index 000000000..ddaa33eac --- /dev/null +++ b/packages/formula/package.json @@ -0,0 +1,21 @@ +{ + "name": "@undb/formula", + "module": "src/index.ts", + "type": "module", + "types": "src/index.d.ts", + "devDependencies": { + "@types/bun": "latest", + "antlr4ts-cli": "^0.5.0-alpha.4" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@undb/zod": "*", + "antlr4ts": "^0.5.0-alpha.4" + }, + "scripts": { + "generate-parser": "bun run scripts/generate-parser.ts", + "generate": "bun generate-parser" + } +} diff --git a/packages/formula/scripts/generate-parser.ts b/packages/formula/scripts/generate-parser.ts new file mode 100644 index 000000000..c898b2df3 --- /dev/null +++ b/packages/formula/scripts/generate-parser.ts @@ -0,0 +1,3 @@ +import { $ } from "bun" + +await $`antlr4ts -visitor -no-listener src/grammar/*.g4` diff --git a/packages/formula/src/formula.constants.ts b/packages/formula/src/formula.constants.ts new file mode 100644 index 000000000..8fb7a55a1 --- /dev/null +++ b/packages/formula/src/formula.constants.ts @@ -0,0 +1,58 @@ +import { FormulaFunction } from "./formula/formula.type" + +export const FORMULA_FUNCTIONS: FormulaFunction[] = [ + "ADD", + "SUBTRACT", + "MULTIPLY", + "DIVIDE", + "SUM", + "CONCAT", + "MOD", + "POWER", + "SQRT", + "ABS", + "ROUND", + "FLOOR", + "CEILING", + "MIN", + "MAX", + "AVERAGE", + // "MEDIAN", + + // 文本处理 + "UPPER", + "LOWER", + "TRIM", + "LEFT", + "RIGHT", + "MID", + "LEN", + "FIND", + "REPLACE", + "SUBSTITUTE", + "REPEAT", + "SEARCH", + "SUBSTR", + + // 逻辑运算 + "AND", + "OR", + "NOT", + "IF", + "SWITCH", + // "ISBLANK", + // "ISNUMBER", + // "ISTEXT", + + // 统计函数 + // "COUNT", + // "COUNTA", + // "COUNTIF", + // "SUMIF", + // "CORREL", + + "JSON_EXTRACT", + + "RECORD_ID", + "AUTO_INCREMENT", +] as const diff --git a/packages/formula/src/formula.visitor.ts b/packages/formula/src/formula.visitor.ts new file mode 100644 index 000000000..eeb513571 --- /dev/null +++ b/packages/formula/src/formula.visitor.ts @@ -0,0 +1,219 @@ +import { AbstractParseTreeVisitor } from "antlr4ts/tree/AbstractParseTreeVisitor" +import { FormulaFunction } from "./formula/formula.type" +import { globalFunctionRegistry } from "./formula/registry" +import { + AddSubExprContext, + AndExprContext, + ArgumentListContext, + ComparisonExprContext, + FormulaContext, + FunctionCallContext, + FunctionExprContext, + MulDivModExprContext, + NotExprContext, + NumberExprContext, + OrExprContext, + ParenExprContext, + StringExprContext, + VariableContext, + VariableExprContext, +} from "./grammar/FormulaParser" +import type { FormulaParserVisitor } from "./grammar/FormulaParserVisitor" +import { + ArgumentListResult, + ReturnType, + type ExpressionResult, + type FunctionExpressionResult, + type NumberResult, + type VariableResult, +} from "./types" + +export class FormulaVisitor + extends AbstractParseTreeVisitor + implements FormulaParserVisitor +{ + private variables: Set = new Set() + + private assertType(result: ExpressionResult, types: ReturnType[]): boolean { + if (result.type === "variable") { + return true + } + + if (result.type === "functionCall") { + if (!types.includes(result.returnType)) { + throw new Error(`Expected ${types.join(" or ")} but got ${result.name}`) + } + return true + } + + if (!types.includes(result.type as ReturnType)) { + throw new Error(`Expected ${types.join(" or ")} but got ${result.type}`) + } + + return true + } + + visitFormula(ctx: FormulaContext): ExpressionResult { + return this.visit(ctx.expression()) + } + + visitMulDivModExpr(ctx: MulDivModExprContext): ExpressionResult { + const left = this.visit(ctx.expression(0)) as NumberResult | VariableResult + const right = this.visit(ctx.expression(1)) as NumberResult | VariableResult + + this.assertType(left, ["number"]) + this.assertType(right, ["number"]) + + const op = ctx._op.text! + return { + type: "functionCall", + name: op, + arguments: [left, right], + returnType: "number", + value: ctx.text, + } + } + + visitAddSubExpr(ctx: AddSubExprContext): ExpressionResult { + const left = this.visit(ctx.expression(0)) as NumberResult | VariableResult + const right = this.visit(ctx.expression(1)) as NumberResult | VariableResult + + this.assertType(left, ["number"]) + this.assertType(right, ["number"]) + + const op = ctx._op.text! + return { + type: "functionCall", + name: op, + arguments: [left, right], + returnType: "number", + value: ctx.text, + } + } + + visitComparisonExpr(ctx: ComparisonExprContext): ExpressionResult { + const left = this.visit(ctx.expression(0)) + const right = this.visit(ctx.expression(1)) + + this.assertType(left, ["number"]) + this.assertType(right, ["number"]) + + const op = ctx._op.text! + return { + type: "functionCall", + name: op, + arguments: [left, right], + returnType: "boolean", + value: ctx.text, + } + } + + visitAndExpr(ctx: AndExprContext): ExpressionResult { + const left = this.visit(ctx.expression(0)) + const right = this.visit(ctx.expression(1)) + + this.assertType(left, ["boolean"]) + this.assertType(right, ["boolean"]) + + return { + type: "functionCall", + name: "AND", + arguments: [left, right], + returnType: "boolean", + value: ctx.text, + } + } + + visitOrExpr(ctx: OrExprContext): ExpressionResult { + const left = this.visit(ctx.expression(0)) + const right = this.visit(ctx.expression(1)) + + this.assertType(left, ["boolean"]) + this.assertType(right, ["boolean"]) + + return { + type: "functionCall", + name: "OR", + arguments: [left, right], + returnType: "boolean", + value: ctx.text, + } + } + + visitNotExpr(ctx: NotExprContext): ExpressionResult { + const expr = this.visit(ctx.expression()) + this.assertType(expr, ["boolean"]) + return { + type: "functionCall", + name: "NOT", + arguments: [expr], + returnType: "boolean", + value: ctx.text, + } + } + + visitFunctionExpr(ctx: FunctionExprContext): ExpressionResult { + return this.visit(ctx.functionCall()) + } + + visitVariableExpr(ctx: VariableExprContext): ExpressionResult { + return this.visit(ctx.variable()) + } + + visitNumberExpr(ctx: NumberExprContext): ExpressionResult { + return { type: "number", value: Number(ctx.NUMBER().text) } + } + + visitStringExpr(ctx: StringExprContext): ExpressionResult { + return { type: "string", value: ctx.STRING().text.slice(1, -1) } + } + + visitParenExpr(ctx: ParenExprContext): ExpressionResult { + return this.visit(ctx.expression()) + } + + visitFunctionCall(ctx: FunctionCallContext): ExpressionResult { + const funcName = ctx.IDENTIFIER().text as FormulaFunction + const args = ctx.argumentList() ? (this.visit(ctx.argumentList()!) as FunctionExpressionResult) : undefined + + if (!globalFunctionRegistry.isValid(funcName)) { + throw new Error(`Unknown function: ${funcName}`) + } + + if (args) { + globalFunctionRegistry.validateArgs(funcName, args.arguments) + } + + const returnType = globalFunctionRegistry.get(funcName)!.returnType + + return { + type: "functionCall", + name: funcName, + arguments: args?.arguments ?? [], + returnType, + value: ctx.text, + } + } + + visitArgumentList(ctx: ArgumentListContext): ArgumentListResult { + const args = ctx.expression().map((expr) => this.visit(expr)) + return { + type: "argumentList", + arguments: args, + } + } + visitVariable(ctx: VariableContext): VariableResult { + const variableName = ctx.IDENTIFIER().text + const raw = ctx.text + this.variables.add(variableName) + return { type: "variable", value: raw, variable: variableName } + } + + getVariables(): string[] { + return Array.from(this.variables) + } + + protected defaultResult(): ExpressionResult { + return { type: "string", value: "" } + } +} diff --git a/packages/formula/src/formula/formula.type.ts b/packages/formula/src/formula/formula.type.ts new file mode 100644 index 000000000..2690f6b1e --- /dev/null +++ b/packages/formula/src/formula/formula.type.ts @@ -0,0 +1,68 @@ +export type FormulaFunction = + // 数学运算 + | "ADD" + | "SUBTRACT" + | "MULTIPLY" + | "DIVIDE" + | "SUM" + | "CONCAT" + | "MOD" + | "POWER" + | "SQRT" + | "ABS" + | "ROUND" + | "FLOOR" + | "CEILING" + | "MIN" + | "MAX" + | "AVERAGE" + // | "MEDIAN" + + // 文本处理 + | "UPPER" + | "LOWER" + | "TRIM" + | "LEFT" + | "RIGHT" + | "MID" + | "LEN" + | "FIND" + | "REPLACE" + | "SUBSTITUTE" + | "REPEAT" + | "SEARCH" + | "SUBSTR" + + // // 日期时间 + // | "NOW" + // | "TODAY" + // | "YEAR" + // | "MONTH" + // | "DAY" + // | "HOUR" + // | "MINUTE" + // | "SECOND" + // | "WEEKDAY" + // | "DATE" + + // 逻辑运算 + | "AND" + | "OR" + | "NOT" + | "IF" + | "SWITCH" + | "ISBLANK" + | "ISNUMBER" + | "ISTEXT" + + // 统计函数 + | "COUNT" + | "COUNTA" + | "COUNTIF" + | "SUMIF" + | "CORREL" + | "JSON_EXTRACT" + + // System Field + | "RECORD_ID" + | "AUTO_INCREMENT" diff --git a/packages/formula/src/formula/registry.ts b/packages/formula/src/formula/registry.ts new file mode 100644 index 000000000..f852969ff --- /dev/null +++ b/packages/formula/src/formula/registry.ts @@ -0,0 +1,142 @@ +import { ParamType, ReturnType, type ExpressionResult } from "../types" +import { FormulaFunction } from "./formula.type" + +interface FunctionDefinition { + paramPatterns: ParamType[][] + returnType: ReturnType +} + +export class FunctionRegistry { + private functions: Map = new Map() + + register(name: FormulaFunction, paramPatterns: ParamType[][], returnType: ReturnType) { + this.functions.set(name, { paramPatterns, returnType }) + } + + get(name: FormulaFunction): FunctionDefinition | undefined { + return this.functions.get(name.toUpperCase()) + } + + isValid(name: string): boolean { + return this.functions.has(name.toUpperCase()) + } + + validateArgs(name: FormulaFunction, args: ExpressionResult[]): void { + const funcDef = this.get(name) + if (!funcDef) { + throw new Error(`Unknown function name: ${name}`) + } + + // 检查是否有任何模式的参数数量匹配 + const hasMatchingPattern = funcDef.paramPatterns.some((pattern) => { + // 如果模式中包含 VARIADIC,则参数数量必须大于等于 pattern.length - 1 + // 否则参数数量必须完全匹配 + if (pattern.includes("variadic")) { + return args.length >= pattern.length - 1 + } + return args.length === pattern.length + }) + + if (!hasMatchingPattern) { + const expectedCounts = funcDef.paramPatterns + .map((pattern) => (pattern.includes("variadic") ? `at least ${pattern.length - 1}` : `${pattern.length}`)) + .join(" or ") + throw new Error(`Function ${name} expects ${expectedCounts} arguments, but got ${args.length}`) + } + + const isValidPattern = funcDef.paramPatterns.some((pattern) => { + for (let i = 0; i < pattern.length; i++) { + const expectedType = pattern[i] + if (expectedType === "variadic") { + // 剩余的所有参数都应该匹配 VARIADIC 的前一个类型 + const variadicType = pattern[i - 1] + return args.slice(i - 1).every((arg) => this.isTypeMatch(arg, variadicType)) + } + if (!this.isTypeMatch(args[i], expectedType)) { + return false + } + } + return true + }) + + if (!isValidPattern) { + throw new Error(`Function ${name} arguments do not match: ${JSON.stringify(args)}`) + } + } + + private isTypeMatch(arg: ExpressionResult, expectedType: ParamType): boolean { + if (arg.type === "functionCall") { + return arg.returnType === expectedType + } + + if (arg.type === "variable") { + return true + } + + switch (expectedType) { + case "number": + return arg.type === "number" + case "string": + return arg.type === "string" + case "boolean": + return arg.type === "boolean" + case "date": + // TODO: 假设有日期类型的处理 + return false + case "any": + return true + default: + return false + } + } +} + +export const globalFunctionRegistry = new FunctionRegistry() + +// 注册函数,支持多种参数模式 +globalFunctionRegistry.register("ADD", [["number", "number"]], "number") +globalFunctionRegistry.register("SUBTRACT", [["number", "number"]], "number") +globalFunctionRegistry.register("MULTIPLY", [["number", "number"]], "number") +globalFunctionRegistry.register("DIVIDE", [["number", "number"]], "number") +globalFunctionRegistry.register("SUM", [["number", "variadic"]], "number") +globalFunctionRegistry.register("MOD", [["number", "number"]], "number") +globalFunctionRegistry.register("POWER", [["number", "number"]], "number") +globalFunctionRegistry.register("SQRT", [["number"]], "number") +globalFunctionRegistry.register("ABS", [["number"]], "number") +globalFunctionRegistry.register("ROUND", [["number"]], "number") +globalFunctionRegistry.register("FLOOR", [["number"]], "number") +globalFunctionRegistry.register("CEILING", [["number"]], "number") +globalFunctionRegistry.register("MIN", [["number", "variadic"]], "number") +globalFunctionRegistry.register("MAX", [["number", "variadic"]], "number") +globalFunctionRegistry.register("AVERAGE", [["number", "variadic"]], "number") + +globalFunctionRegistry.register("CONCAT", [["string", "variadic"]], "string") +globalFunctionRegistry.register("UPPER", [["string"]], "string") +globalFunctionRegistry.register("LOWER", [["string"]], "string") +globalFunctionRegistry.register("TRIM", [["string"]], "string") +globalFunctionRegistry.register("LEFT", [["string", "number"]], "string") +globalFunctionRegistry.register("RIGHT", [["string", "number"]], "string") +globalFunctionRegistry.register("MID", [["string", "number", "number"]], "string") +globalFunctionRegistry.register("LEN", [["string"]], "number") +globalFunctionRegistry.register("REPLACE", [["string", "string", "string"]], "string") +globalFunctionRegistry.register("SUBSTITUTE", [["string", "string", "string", "number"]], "string") +globalFunctionRegistry.register("REPEAT", [["string", "number"]], "string") +globalFunctionRegistry.register("SEARCH", [["string", "string"]], "number") +globalFunctionRegistry.register("SUBSTR", [["string", "number", "number"]], "string") + +globalFunctionRegistry.register("AND", [["boolean", "variadic"]], "boolean") +globalFunctionRegistry.register("OR", [["boolean", "variadic"]], "boolean") +globalFunctionRegistry.register("NOT", [["boolean"]], "boolean") +globalFunctionRegistry.register("ISBLANK", [["any"]], "boolean") +globalFunctionRegistry.register("ISNUMBER", [["any"]], "boolean") +globalFunctionRegistry.register("ISTEXT", [["any"]], "boolean") + +// globalFunctionRegistry.register("COUNT", [["variadic"]], "number") +// globalFunctionRegistry.register("COUNTA", [["variadic"]], "number") +// globalFunctionRegistry.register("COUNTIF", [["variadic"]], "number") +// globalFunctionRegistry.register("SUMIF", [["variadic"]], "number") + +globalFunctionRegistry.register("JSON_EXTRACT", [["string", "string"]], "any") + +globalFunctionRegistry.register("RECORD_ID", [], "string") +globalFunctionRegistry.register("AUTO_INCREMENT", [], "number") diff --git a/packages/formula/src/grammar/FormulaLexer.g4 b/packages/formula/src/grammar/FormulaLexer.g4 new file mode 100644 index 000000000..9594926b0 --- /dev/null +++ b/packages/formula/src/grammar/FormulaLexer.g4 @@ -0,0 +1,97 @@ +lexer grammar FormulaLexer; + +// 运算符 +ADD: '+'; +SUBTRACT: '-'; +MULTIPLY: '*'; +DIVIDE: '/'; +MODULO: '%'; +POWER: '^'; + +// 比较运算符 +EQUAL: '='; +NOT_EQUAL: '!='; +LESS: '<'; +LESS_EQUAL: '<='; +GREATER: '>'; +GREATER_EQUAL: '>='; + +// 逻辑运算符 +AND: A N D; +OR: O R; +NOT: N O T; + +// 括号 +LPAREN: '('; +RPAREN: ')'; +LBRACE: '{'; +RBRACE: '}'; +LBRACKET: '['; +RBRACKET: ']'; + +// 分隔符 +COMMA: ','; +SEMICOLON: ';'; +COLON: ':'; +DOT: '.'; + +UNDERSCORE: '_'; + +// 函数名和标识符 +IDENTIFIER: LETTER (LETTER | DIGIT | UNDERSCORE)*; + +// 数字 +NUMBER: DIGIT+ ('.' DIGIT+)?; + +// 字符串 +STRING: '\'' ( ~'\'' | '\'\'')* '\''; + +// 布尔值 +TRUE: T R U E; +FALSE: F A L S E; + +// 空值 +NULL: N U L L; + +// 日期时间 +DATE: D A T E; +TIME: T I M E; +DATETIME: D A T E T I M E; + +// 空白字符 +WS: [ \t\r\n]+ -> skip; + +// 注释 +COMMENT: '//' ~[\r\n]* -> skip; +MULTILINE_COMMENT: '/*' .*? '*/' -> skip; + +// Fragments +fragment DIGIT: [0-9]; +fragment LETTER: [a-zA-Z]; + +fragment A: ('A' | 'a'); +fragment B: ('B' | 'b'); +fragment C: ('C' | 'c'); +fragment D: ('D' | 'd'); +fragment E: ('E' | 'e'); +fragment F: ('F' | 'f'); +fragment G: ('G' | 'g'); +fragment H: ('H' | 'h'); +fragment I: ('I' | 'i'); +fragment J: ('J' | 'j'); +fragment K: ('K' | 'k'); +fragment L: ('L' | 'l'); +fragment M: ('M' | 'm'); +fragment N: ('N' | 'n'); +fragment O: ('O' | 'o'); +fragment P: ('P' | 'p'); +fragment Q: ('Q' | 'q'); +fragment R: ('R' | 'r'); +fragment S: ('S' | 's'); +fragment T: ('T' | 't'); +fragment U: ('U' | 'u'); +fragment V: ('V' | 'v'); +fragment W: ('W' | 'w'); +fragment X: ('X' | 'x'); +fragment Y: ('Y' | 'y'); +fragment Z: ('Z' | 'z'); \ No newline at end of file diff --git a/packages/formula/src/grammar/FormulaLexer.interp b/packages/formula/src/grammar/FormulaLexer.interp new file mode 100644 index 000000000..6fefff5f8 --- /dev/null +++ b/packages/formula/src/grammar/FormulaLexer.interp @@ -0,0 +1,159 @@ +token literal names: +null +'+' +'-' +'*' +'/' +'%' +'^' +'=' +'!=' +'<' +'<=' +'>' +'>=' +null +null +null +'(' +')' +'{' +'}' +'[' +']' +',' +';' +':' +'.' +'_' +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +ADD +SUBTRACT +MULTIPLY +DIVIDE +MODULO +POWER +EQUAL +NOT_EQUAL +LESS +LESS_EQUAL +GREATER +GREATER_EQUAL +AND +OR +NOT +LPAREN +RPAREN +LBRACE +RBRACE +LBRACKET +RBRACKET +COMMA +SEMICOLON +COLON +DOT +UNDERSCORE +IDENTIFIER +NUMBER +STRING +TRUE +FALSE +NULL +DATE +TIME +DATETIME +WS +COMMENT +MULTILINE_COMMENT + +rule names: +ADD +SUBTRACT +MULTIPLY +DIVIDE +MODULO +POWER +EQUAL +NOT_EQUAL +LESS +LESS_EQUAL +GREATER +GREATER_EQUAL +AND +OR +NOT +LPAREN +RPAREN +LBRACE +RBRACE +LBRACKET +RBRACKET +COMMA +SEMICOLON +COLON +DOT +UNDERSCORE +IDENTIFIER +NUMBER +STRING +TRUE +FALSE +NULL +DATE +TIME +DATETIME +WS +COMMENT +MULTILINE_COMMENT +DIGIT +LETTER +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 40, 351, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 3, 2, 3, 2, 3, 3, 3, 3, 3, 4, 3, 4, 3, 5, 3, 5, 3, 6, 3, 6, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 18, 3, 18, 3, 19, 3, 19, 3, 20, 3, 20, 3, 21, 3, 21, 3, 22, 3, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 200, 10, 28, 12, 28, 14, 28, 203, 11, 28, 3, 29, 6, 29, 206, 10, 29, 13, 29, 14, 29, 207, 3, 29, 3, 29, 6, 29, 212, 10, 29, 13, 29, 14, 29, 213, 5, 29, 216, 10, 29, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 222, 10, 30, 12, 30, 14, 30, 225, 11, 30, 3, 30, 3, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 33, 3, 33, 3, 33, 3, 33, 3, 33, 3, 34, 3, 34, 3, 34, 3, 34, 3, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 37, 6, 37, 265, 10, 37, 13, 37, 14, 37, 266, 3, 37, 3, 37, 3, 38, 3, 38, 3, 38, 3, 38, 7, 38, 275, 10, 38, 12, 38, 14, 38, 278, 11, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 39, 3, 39, 7, 39, 286, 10, 39, 12, 39, 14, 39, 289, 11, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 3, 43, 3, 43, 3, 44, 3, 44, 3, 45, 3, 45, 3, 46, 3, 46, 3, 47, 3, 47, 3, 48, 3, 48, 3, 49, 3, 49, 3, 50, 3, 50, 3, 51, 3, 51, 3, 52, 3, 52, 3, 53, 3, 53, 3, 54, 3, 54, 3, 55, 3, 55, 3, 56, 3, 56, 3, 57, 3, 57, 3, 58, 3, 58, 3, 59, 3, 59, 3, 60, 3, 60, 3, 61, 3, 61, 3, 62, 3, 62, 3, 63, 3, 63, 3, 64, 3, 64, 3, 65, 3, 65, 3, 66, 3, 66, 3, 67, 3, 67, 3, 287, 2, 2, 68, 3, 2, 3, 5, 2, 4, 7, 2, 5, 9, 2, 6, 11, 2, 7, 13, 2, 8, 15, 2, 9, 17, 2, 10, 19, 2, 11, 21, 2, 12, 23, 2, 13, 25, 2, 14, 27, 2, 15, 29, 2, 16, 31, 2, 17, 33, 2, 18, 35, 2, 19, 37, 2, 20, 39, 2, 21, 41, 2, 22, 43, 2, 23, 45, 2, 24, 47, 2, 25, 49, 2, 26, 51, 2, 27, 53, 2, 28, 55, 2, 29, 57, 2, 30, 59, 2, 31, 61, 2, 32, 63, 2, 33, 65, 2, 34, 67, 2, 35, 69, 2, 36, 71, 2, 37, 73, 2, 38, 75, 2, 39, 77, 2, 40, 79, 2, 2, 81, 2, 2, 83, 2, 2, 85, 2, 2, 87, 2, 2, 89, 2, 2, 91, 2, 2, 93, 2, 2, 95, 2, 2, 97, 2, 2, 99, 2, 2, 101, 2, 2, 103, 2, 2, 105, 2, 2, 107, 2, 2, 109, 2, 2, 111, 2, 2, 113, 2, 2, 115, 2, 2, 117, 2, 2, 119, 2, 2, 121, 2, 2, 123, 2, 2, 125, 2, 2, 127, 2, 2, 129, 2, 2, 131, 2, 2, 133, 2, 2, 3, 2, 33, 3, 2, 41, 41, 5, 2, 11, 12, 15, 15, 34, 34, 4, 2, 12, 12, 15, 15, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 4, 2, 67, 67, 99, 99, 4, 2, 68, 68, 100, 100, 4, 2, 69, 69, 101, 101, 4, 2, 70, 70, 102, 102, 4, 2, 71, 71, 103, 103, 4, 2, 72, 72, 104, 104, 4, 2, 73, 73, 105, 105, 4, 2, 74, 74, 106, 106, 4, 2, 75, 75, 107, 107, 4, 2, 76, 76, 108, 108, 4, 2, 77, 77, 109, 109, 4, 2, 78, 78, 110, 110, 4, 2, 79, 79, 111, 111, 4, 2, 80, 80, 112, 112, 4, 2, 81, 81, 113, 113, 4, 2, 82, 82, 114, 114, 4, 2, 83, 83, 115, 115, 4, 2, 84, 84, 116, 116, 4, 2, 85, 85, 117, 117, 4, 2, 86, 86, 118, 118, 4, 2, 87, 87, 119, 119, 4, 2, 88, 88, 120, 120, 4, 2, 89, 89, 121, 121, 4, 2, 90, 90, 122, 122, 4, 2, 91, 91, 123, 123, 4, 2, 92, 92, 124, 124, 2, 333, 2, 3, 3, 2, 2, 2, 2, 5, 3, 2, 2, 2, 2, 7, 3, 2, 2, 2, 2, 9, 3, 2, 2, 2, 2, 11, 3, 2, 2, 2, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 2, 47, 3, 2, 2, 2, 2, 49, 3, 2, 2, 2, 2, 51, 3, 2, 2, 2, 2, 53, 3, 2, 2, 2, 2, 55, 3, 2, 2, 2, 2, 57, 3, 2, 2, 2, 2, 59, 3, 2, 2, 2, 2, 61, 3, 2, 2, 2, 2, 63, 3, 2, 2, 2, 2, 65, 3, 2, 2, 2, 2, 67, 3, 2, 2, 2, 2, 69, 3, 2, 2, 2, 2, 71, 3, 2, 2, 2, 2, 73, 3, 2, 2, 2, 2, 75, 3, 2, 2, 2, 2, 77, 3, 2, 2, 2, 3, 135, 3, 2, 2, 2, 5, 137, 3, 2, 2, 2, 7, 139, 3, 2, 2, 2, 9, 141, 3, 2, 2, 2, 11, 143, 3, 2, 2, 2, 13, 145, 3, 2, 2, 2, 15, 147, 3, 2, 2, 2, 17, 149, 3, 2, 2, 2, 19, 152, 3, 2, 2, 2, 21, 154, 3, 2, 2, 2, 23, 157, 3, 2, 2, 2, 25, 159, 3, 2, 2, 2, 27, 162, 3, 2, 2, 2, 29, 166, 3, 2, 2, 2, 31, 169, 3, 2, 2, 2, 33, 173, 3, 2, 2, 2, 35, 175, 3, 2, 2, 2, 37, 177, 3, 2, 2, 2, 39, 179, 3, 2, 2, 2, 41, 181, 3, 2, 2, 2, 43, 183, 3, 2, 2, 2, 45, 185, 3, 2, 2, 2, 47, 187, 3, 2, 2, 2, 49, 189, 3, 2, 2, 2, 51, 191, 3, 2, 2, 2, 53, 193, 3, 2, 2, 2, 55, 195, 3, 2, 2, 2, 57, 205, 3, 2, 2, 2, 59, 217, 3, 2, 2, 2, 61, 228, 3, 2, 2, 2, 63, 233, 3, 2, 2, 2, 65, 239, 3, 2, 2, 2, 67, 244, 3, 2, 2, 2, 69, 249, 3, 2, 2, 2, 71, 254, 3, 2, 2, 2, 73, 264, 3, 2, 2, 2, 75, 270, 3, 2, 2, 2, 77, 281, 3, 2, 2, 2, 79, 295, 3, 2, 2, 2, 81, 297, 3, 2, 2, 2, 83, 299, 3, 2, 2, 2, 85, 301, 3, 2, 2, 2, 87, 303, 3, 2, 2, 2, 89, 305, 3, 2, 2, 2, 91, 307, 3, 2, 2, 2, 93, 309, 3, 2, 2, 2, 95, 311, 3, 2, 2, 2, 97, 313, 3, 2, 2, 2, 99, 315, 3, 2, 2, 2, 101, 317, 3, 2, 2, 2, 103, 319, 3, 2, 2, 2, 105, 321, 3, 2, 2, 2, 107, 323, 3, 2, 2, 2, 109, 325, 3, 2, 2, 2, 111, 327, 3, 2, 2, 2, 113, 329, 3, 2, 2, 2, 115, 331, 3, 2, 2, 2, 117, 333, 3, 2, 2, 2, 119, 335, 3, 2, 2, 2, 121, 337, 3, 2, 2, 2, 123, 339, 3, 2, 2, 2, 125, 341, 3, 2, 2, 2, 127, 343, 3, 2, 2, 2, 129, 345, 3, 2, 2, 2, 131, 347, 3, 2, 2, 2, 133, 349, 3, 2, 2, 2, 135, 136, 7, 45, 2, 2, 136, 4, 3, 2, 2, 2, 137, 138, 7, 47, 2, 2, 138, 6, 3, 2, 2, 2, 139, 140, 7, 44, 2, 2, 140, 8, 3, 2, 2, 2, 141, 142, 7, 49, 2, 2, 142, 10, 3, 2, 2, 2, 143, 144, 7, 39, 2, 2, 144, 12, 3, 2, 2, 2, 145, 146, 7, 96, 2, 2, 146, 14, 3, 2, 2, 2, 147, 148, 7, 63, 2, 2, 148, 16, 3, 2, 2, 2, 149, 150, 7, 35, 2, 2, 150, 151, 7, 63, 2, 2, 151, 18, 3, 2, 2, 2, 152, 153, 7, 62, 2, 2, 153, 20, 3, 2, 2, 2, 154, 155, 7, 62, 2, 2, 155, 156, 7, 63, 2, 2, 156, 22, 3, 2, 2, 2, 157, 158, 7, 64, 2, 2, 158, 24, 3, 2, 2, 2, 159, 160, 7, 64, 2, 2, 160, 161, 7, 63, 2, 2, 161, 26, 3, 2, 2, 2, 162, 163, 5, 83, 42, 2, 163, 164, 5, 109, 55, 2, 164, 165, 5, 89, 45, 2, 165, 28, 3, 2, 2, 2, 166, 167, 5, 111, 56, 2, 167, 168, 5, 117, 59, 2, 168, 30, 3, 2, 2, 2, 169, 170, 5, 109, 55, 2, 170, 171, 5, 111, 56, 2, 171, 172, 5, 121, 61, 2, 172, 32, 3, 2, 2, 2, 173, 174, 7, 42, 2, 2, 174, 34, 3, 2, 2, 2, 175, 176, 7, 43, 2, 2, 176, 36, 3, 2, 2, 2, 177, 178, 7, 125, 2, 2, 178, 38, 3, 2, 2, 2, 179, 180, 7, 127, 2, 2, 180, 40, 3, 2, 2, 2, 181, 182, 7, 93, 2, 2, 182, 42, 3, 2, 2, 2, 183, 184, 7, 95, 2, 2, 184, 44, 3, 2, 2, 2, 185, 186, 7, 46, 2, 2, 186, 46, 3, 2, 2, 2, 187, 188, 7, 61, 2, 2, 188, 48, 3, 2, 2, 2, 189, 190, 7, 60, 2, 2, 190, 50, 3, 2, 2, 2, 191, 192, 7, 48, 2, 2, 192, 52, 3, 2, 2, 2, 193, 194, 7, 97, 2, 2, 194, 54, 3, 2, 2, 2, 195, 201, 5, 81, 41, 2, 196, 200, 5, 81, 41, 2, 197, 200, 5, 79, 40, 2, 198, 200, 5, 53, 27, 2, 199, 196, 3, 2, 2, 2, 199, 197, 3, 2, 2, 2, 199, 198, 3, 2, 2, 2, 200, 203, 3, 2, 2, 2, 201, 199, 3, 2, 2, 2, 201, 202, 3, 2, 2, 2, 202, 56, 3, 2, 2, 2, 203, 201, 3, 2, 2, 2, 204, 206, 5, 79, 40, 2, 205, 204, 3, 2, 2, 2, 206, 207, 3, 2, 2, 2, 207, 205, 3, 2, 2, 2, 207, 208, 3, 2, 2, 2, 208, 215, 3, 2, 2, 2, 209, 211, 7, 48, 2, 2, 210, 212, 5, 79, 40, 2, 211, 210, 3, 2, 2, 2, 212, 213, 3, 2, 2, 2, 213, 211, 3, 2, 2, 2, 213, 214, 3, 2, 2, 2, 214, 216, 3, 2, 2, 2, 215, 209, 3, 2, 2, 2, 215, 216, 3, 2, 2, 2, 216, 58, 3, 2, 2, 2, 217, 223, 7, 41, 2, 2, 218, 222, 10, 2, 2, 2, 219, 220, 7, 41, 2, 2, 220, 222, 7, 41, 2, 2, 221, 218, 3, 2, 2, 2, 221, 219, 3, 2, 2, 2, 222, 225, 3, 2, 2, 2, 223, 221, 3, 2, 2, 2, 223, 224, 3, 2, 2, 2, 224, 226, 3, 2, 2, 2, 225, 223, 3, 2, 2, 2, 226, 227, 7, 41, 2, 2, 227, 60, 3, 2, 2, 2, 228, 229, 5, 121, 61, 2, 229, 230, 5, 117, 59, 2, 230, 231, 5, 123, 62, 2, 231, 232, 5, 91, 46, 2, 232, 62, 3, 2, 2, 2, 233, 234, 5, 93, 47, 2, 234, 235, 5, 83, 42, 2, 235, 236, 5, 105, 53, 2, 236, 237, 5, 119, 60, 2, 237, 238, 5, 91, 46, 2, 238, 64, 3, 2, 2, 2, 239, 240, 5, 109, 55, 2, 240, 241, 5, 123, 62, 2, 241, 242, 5, 105, 53, 2, 242, 243, 5, 105, 53, 2, 243, 66, 3, 2, 2, 2, 244, 245, 5, 89, 45, 2, 245, 246, 5, 83, 42, 2, 246, 247, 5, 121, 61, 2, 247, 248, 5, 91, 46, 2, 248, 68, 3, 2, 2, 2, 249, 250, 5, 121, 61, 2, 250, 251, 5, 99, 50, 2, 251, 252, 5, 107, 54, 2, 252, 253, 5, 91, 46, 2, 253, 70, 3, 2, 2, 2, 254, 255, 5, 89, 45, 2, 255, 256, 5, 83, 42, 2, 256, 257, 5, 121, 61, 2, 257, 258, 5, 91, 46, 2, 258, 259, 5, 121, 61, 2, 259, 260, 5, 99, 50, 2, 260, 261, 5, 107, 54, 2, 261, 262, 5, 91, 46, 2, 262, 72, 3, 2, 2, 2, 263, 265, 9, 3, 2, 2, 264, 263, 3, 2, 2, 2, 265, 266, 3, 2, 2, 2, 266, 264, 3, 2, 2, 2, 266, 267, 3, 2, 2, 2, 267, 268, 3, 2, 2, 2, 268, 269, 8, 37, 2, 2, 269, 74, 3, 2, 2, 2, 270, 271, 7, 49, 2, 2, 271, 272, 7, 49, 2, 2, 272, 276, 3, 2, 2, 2, 273, 275, 10, 4, 2, 2, 274, 273, 3, 2, 2, 2, 275, 278, 3, 2, 2, 2, 276, 274, 3, 2, 2, 2, 276, 277, 3, 2, 2, 2, 277, 279, 3, 2, 2, 2, 278, 276, 3, 2, 2, 2, 279, 280, 8, 38, 2, 2, 280, 76, 3, 2, 2, 2, 281, 282, 7, 49, 2, 2, 282, 283, 7, 44, 2, 2, 283, 287, 3, 2, 2, 2, 284, 286, 11, 2, 2, 2, 285, 284, 3, 2, 2, 2, 286, 289, 3, 2, 2, 2, 287, 288, 3, 2, 2, 2, 287, 285, 3, 2, 2, 2, 288, 290, 3, 2, 2, 2, 289, 287, 3, 2, 2, 2, 290, 291, 7, 44, 2, 2, 291, 292, 7, 49, 2, 2, 292, 293, 3, 2, 2, 2, 293, 294, 8, 39, 2, 2, 294, 78, 3, 2, 2, 2, 295, 296, 9, 5, 2, 2, 296, 80, 3, 2, 2, 2, 297, 298, 9, 6, 2, 2, 298, 82, 3, 2, 2, 2, 299, 300, 9, 7, 2, 2, 300, 84, 3, 2, 2, 2, 301, 302, 9, 8, 2, 2, 302, 86, 3, 2, 2, 2, 303, 304, 9, 9, 2, 2, 304, 88, 3, 2, 2, 2, 305, 306, 9, 10, 2, 2, 306, 90, 3, 2, 2, 2, 307, 308, 9, 11, 2, 2, 308, 92, 3, 2, 2, 2, 309, 310, 9, 12, 2, 2, 310, 94, 3, 2, 2, 2, 311, 312, 9, 13, 2, 2, 312, 96, 3, 2, 2, 2, 313, 314, 9, 14, 2, 2, 314, 98, 3, 2, 2, 2, 315, 316, 9, 15, 2, 2, 316, 100, 3, 2, 2, 2, 317, 318, 9, 16, 2, 2, 318, 102, 3, 2, 2, 2, 319, 320, 9, 17, 2, 2, 320, 104, 3, 2, 2, 2, 321, 322, 9, 18, 2, 2, 322, 106, 3, 2, 2, 2, 323, 324, 9, 19, 2, 2, 324, 108, 3, 2, 2, 2, 325, 326, 9, 20, 2, 2, 326, 110, 3, 2, 2, 2, 327, 328, 9, 21, 2, 2, 328, 112, 3, 2, 2, 2, 329, 330, 9, 22, 2, 2, 330, 114, 3, 2, 2, 2, 331, 332, 9, 23, 2, 2, 332, 116, 3, 2, 2, 2, 333, 334, 9, 24, 2, 2, 334, 118, 3, 2, 2, 2, 335, 336, 9, 25, 2, 2, 336, 120, 3, 2, 2, 2, 337, 338, 9, 26, 2, 2, 338, 122, 3, 2, 2, 2, 339, 340, 9, 27, 2, 2, 340, 124, 3, 2, 2, 2, 341, 342, 9, 28, 2, 2, 342, 126, 3, 2, 2, 2, 343, 344, 9, 29, 2, 2, 344, 128, 3, 2, 2, 2, 345, 346, 9, 30, 2, 2, 346, 130, 3, 2, 2, 2, 347, 348, 9, 31, 2, 2, 348, 132, 3, 2, 2, 2, 349, 350, 9, 32, 2, 2, 350, 134, 3, 2, 2, 2, 13, 2, 199, 201, 207, 213, 215, 221, 223, 266, 276, 287, 3, 8, 2, 2] \ No newline at end of file diff --git a/packages/formula/src/grammar/FormulaLexer.tokens b/packages/formula/src/grammar/FormulaLexer.tokens new file mode 100644 index 000000000..5ad732765 --- /dev/null +++ b/packages/formula/src/grammar/FormulaLexer.tokens @@ -0,0 +1,61 @@ +ADD=1 +SUBTRACT=2 +MULTIPLY=3 +DIVIDE=4 +MODULO=5 +POWER=6 +EQUAL=7 +NOT_EQUAL=8 +LESS=9 +LESS_EQUAL=10 +GREATER=11 +GREATER_EQUAL=12 +AND=13 +OR=14 +NOT=15 +LPAREN=16 +RPAREN=17 +LBRACE=18 +RBRACE=19 +LBRACKET=20 +RBRACKET=21 +COMMA=22 +SEMICOLON=23 +COLON=24 +DOT=25 +UNDERSCORE=26 +IDENTIFIER=27 +NUMBER=28 +STRING=29 +TRUE=30 +FALSE=31 +NULL=32 +DATE=33 +TIME=34 +DATETIME=35 +WS=36 +COMMENT=37 +MULTILINE_COMMENT=38 +'+'=1 +'-'=2 +'*'=3 +'/'=4 +'%'=5 +'^'=6 +'='=7 +'!='=8 +'<'=9 +'<='=10 +'>'=11 +'>='=12 +'('=16 +')'=17 +'{'=18 +'}'=19 +'['=20 +']'=21 +','=22 +';'=23 +':'=24 +'.'=25 +'_'=26 diff --git a/packages/formula/src/grammar/FormulaLexer.ts b/packages/formula/src/grammar/FormulaLexer.ts new file mode 100644 index 000000000..21703eae9 --- /dev/null +++ b/packages/formula/src/grammar/FormulaLexer.ts @@ -0,0 +1,286 @@ +// Generated from src/grammar/FormulaLexer.g4 by ANTLR 4.9.0-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { CharStream } from "antlr4ts/CharStream"; +import { Lexer } from "antlr4ts/Lexer"; +import { LexerATNSimulator } from "antlr4ts/atn/LexerATNSimulator"; +import { NotNull } from "antlr4ts/Decorators"; +import { Override } from "antlr4ts/Decorators"; +import { RuleContext } from "antlr4ts/RuleContext"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + + +export class FormulaLexer extends Lexer { + public static readonly ADD = 1; + public static readonly SUBTRACT = 2; + public static readonly MULTIPLY = 3; + public static readonly DIVIDE = 4; + public static readonly MODULO = 5; + public static readonly POWER = 6; + public static readonly EQUAL = 7; + public static readonly NOT_EQUAL = 8; + public static readonly LESS = 9; + public static readonly LESS_EQUAL = 10; + public static readonly GREATER = 11; + public static readonly GREATER_EQUAL = 12; + public static readonly AND = 13; + public static readonly OR = 14; + public static readonly NOT = 15; + public static readonly LPAREN = 16; + public static readonly RPAREN = 17; + public static readonly LBRACE = 18; + public static readonly RBRACE = 19; + public static readonly LBRACKET = 20; + public static readonly RBRACKET = 21; + public static readonly COMMA = 22; + public static readonly SEMICOLON = 23; + public static readonly COLON = 24; + public static readonly DOT = 25; + public static readonly UNDERSCORE = 26; + public static readonly IDENTIFIER = 27; + public static readonly NUMBER = 28; + public static readonly STRING = 29; + public static readonly TRUE = 30; + public static readonly FALSE = 31; + public static readonly NULL = 32; + public static readonly DATE = 33; + public static readonly TIME = 34; + public static readonly DATETIME = 35; + public static readonly WS = 36; + public static readonly COMMENT = 37; + public static readonly MULTILINE_COMMENT = 38; + + // tslint:disable:no-trailing-whitespace + public static readonly channelNames: string[] = [ + "DEFAULT_TOKEN_CHANNEL", "HIDDEN", + ]; + + // tslint:disable:no-trailing-whitespace + public static readonly modeNames: string[] = [ + "DEFAULT_MODE", + ]; + + public static readonly ruleNames: string[] = [ + "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER", "EQUAL", "NOT_EQUAL", + "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL", "AND", "OR", "NOT", + "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", + "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER", "NUMBER", "STRING", + "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME", "WS", "COMMENT", + "MULTILINE_COMMENT", "DIGIT", "LETTER", "A", "B", "C", "D", "E", "F", + "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, "'+'", "'-'", "'*'", "'/'", "'%'", "'^'", "'='", "'!='", "'<'", + "'<='", "'>'", "'>='", undefined, undefined, undefined, "'('", "')'", + "'{'", "'}'", "'['", "']'", "','", "';'", "':'", "'.'", "'_'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER", + "EQUAL", "NOT_EQUAL", "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL", + "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", + "RBRACKET", "COMMA", "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER", + "NUMBER", "STRING", "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME", + "WS", "COMMENT", "MULTILINE_COMMENT", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(FormulaLexer._LITERAL_NAMES, FormulaLexer._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return FormulaLexer.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + + constructor(input: CharStream) { + super(input); + this._interp = new LexerATNSimulator(FormulaLexer._ATN, this); + } + + // @Override + public get grammarFileName(): string { return "FormulaLexer.g4"; } + + // @Override + public get ruleNames(): string[] { return FormulaLexer.ruleNames; } + + // @Override + public get serializedATN(): string { return FormulaLexer._serializedATN; } + + // @Override + public get channelNames(): string[] { return FormulaLexer.channelNames; } + + // @Override + public get modeNames(): string[] { return FormulaLexer.modeNames; } + + public static readonly _serializedATN: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02(\u015F\b\x01" + + "\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06" + + "\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r" + + "\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t" + + "\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t" + + "\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t" + + "\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t" + + "\"\x04#\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t)\x04*\t*\x04" + + "+\t+\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x043\t3\x04" + + "4\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04:\t:\x04;\t;\x04<\t<\x04" + + "=\t=\x04>\t>\x04?\t?\x04@\t@\x04A\tA\x04B\tB\x04C\tC\x03\x02\x03\x02\x03" + + "\x03\x03\x03\x03\x04\x03\x04\x03\x05\x03\x05\x03\x06\x03\x06\x03\x07\x03" + + "\x07\x03\b\x03\b\x03\t\x03\t\x03\t\x03\n\x03\n\x03\v\x03\v\x03\v\x03\f" + + "\x03\f\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F" + + "\x03\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x03\x11\x03\x11\x03\x12\x03\x12" + + "\x03\x13\x03\x13\x03\x14\x03\x14\x03\x15\x03\x15\x03\x16\x03\x16\x03\x17" + + "\x03\x17\x03\x18\x03\x18\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1B\x03\x1B" + + "\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x07\x1C\xC8\n\x1C\f\x1C\x0E\x1C\xCB\v" + + "\x1C\x03\x1D\x06\x1D\xCE\n\x1D\r\x1D\x0E\x1D\xCF\x03\x1D\x03\x1D\x06\x1D" + + "\xD4\n\x1D\r\x1D\x0E\x1D\xD5\x05\x1D\xD8\n\x1D\x03\x1E\x03\x1E\x03\x1E" + + "\x03\x1E\x07\x1E\xDE\n\x1E\f\x1E\x0E\x1E\xE1\v\x1E\x03\x1E\x03\x1E\x03" + + "\x1F\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03 \x03 \x03 \x03 \x03 \x03 \x03" + + "!\x03!\x03!\x03!\x03!\x03\"\x03\"\x03\"\x03\"\x03\"\x03#\x03#\x03#\x03" + + "#\x03#\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03%\x06%\u0109\n" + + "%\r%\x0E%\u010A\x03%\x03%\x03&\x03&\x03&\x03&\x07&\u0113\n&\f&\x0E&\u0116" + + "\v&\x03&\x03&\x03\'\x03\'\x03\'\x03\'\x07\'\u011E\n\'\f\'\x0E\'\u0121" + + "\v\'\x03\'\x03\'\x03\'\x03\'\x03\'\x03(\x03(\x03)\x03)\x03*\x03*\x03+" + + "\x03+\x03,\x03,\x03-\x03-\x03.\x03.\x03/\x03/\x030\x030\x031\x031\x03" + + "2\x032\x033\x033\x034\x034\x035\x035\x036\x036\x037\x037\x038\x038\x03" + + "9\x039\x03:\x03:\x03;\x03;\x03<\x03<\x03=\x03=\x03>\x03>\x03?\x03?\x03" + + "@\x03@\x03A\x03A\x03B\x03B\x03C\x03C\x03\u011F\x02\x02D\x03\x02\x03\x05" + + "\x02\x04\x07\x02\x05\t\x02\x06\v\x02\x07\r\x02\b\x0F\x02\t\x11\x02\n\x13" + + "\x02\v\x15\x02\f\x17\x02\r\x19\x02\x0E\x1B\x02\x0F\x1D\x02\x10\x1F\x02" + + "\x11!\x02\x12#\x02\x13%\x02\x14\'\x02\x15)\x02\x16+\x02\x17-\x02\x18/" + + "\x02\x191\x02\x1A3\x02\x1B5\x02\x1C7\x02\x1D9\x02\x1E;\x02\x1F=\x02 ?" + + "\x02!A\x02\"C\x02#E\x02$G\x02%I\x02&K\x02\'M\x02(O\x02\x02Q\x02\x02S\x02" + + "\x02U\x02\x02W\x02\x02Y\x02\x02[\x02\x02]\x02\x02_\x02\x02a\x02\x02c\x02" + + "\x02e\x02\x02g\x02\x02i\x02\x02k\x02\x02m\x02\x02o\x02\x02q\x02\x02s\x02" + + "\x02u\x02\x02w\x02\x02y\x02\x02{\x02\x02}\x02\x02\x7F\x02\x02\x81\x02" + + "\x02\x83\x02\x02\x85\x02\x02\x03\x02!\x03\x02))\x05\x02\v\f\x0F\x0F\"" + + "\"\x04\x02\f\f\x0F\x0F\x03\x022;\x04\x02C\\c|\x04\x02CCcc\x04\x02DDdd" + + "\x04\x02EEee\x04\x02FFff\x04\x02GGgg\x04\x02HHhh\x04\x02IIii\x04\x02J" + + "Jjj\x04\x02KKkk\x04\x02LLll\x04\x02MMmm\x04\x02NNnn\x04\x02OOoo\x04\x02" + + "PPpp\x04\x02QQqq\x04\x02RRrr\x04\x02SSss\x04\x02TTtt\x04\x02UUuu\x04\x02" + + "VVvv\x04\x02WWww\x04\x02XXxx\x04\x02YYyy\x04\x02ZZzz\x04\x02[[{{\x04\x02" + + "\\\\||\x02\u014D\x02\x03\x03\x02\x02\x02\x02\x05\x03\x02\x02\x02\x02\x07" + + "\x03\x02\x02\x02\x02\t\x03\x02\x02\x02\x02\v\x03\x02\x02\x02\x02\r\x03" + + "\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02\x11\x03\x02\x02\x02\x02\x13\x03" + + "\x02\x02\x02\x02\x15\x03\x02\x02\x02\x02\x17\x03\x02\x02\x02\x02\x19\x03" + + "\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x02\x1D\x03\x02\x02\x02\x02\x1F\x03" + + "\x02\x02\x02\x02!\x03\x02\x02\x02\x02#\x03\x02\x02\x02\x02%\x03\x02\x02" + + "\x02\x02\'\x03\x02\x02\x02\x02)\x03\x02\x02\x02\x02+\x03\x02\x02\x02\x02" + + "-\x03\x02\x02\x02\x02/\x03\x02\x02\x02\x021\x03\x02\x02\x02\x023\x03\x02" + + "\x02\x02\x025\x03\x02\x02\x02\x027\x03\x02\x02\x02\x029\x03\x02\x02\x02" + + "\x02;\x03\x02\x02\x02\x02=\x03\x02\x02\x02\x02?\x03\x02\x02\x02\x02A\x03" + + "\x02\x02\x02\x02C\x03\x02\x02\x02\x02E\x03\x02\x02\x02\x02G\x03\x02\x02" + + "\x02\x02I\x03\x02\x02\x02\x02K\x03\x02\x02\x02\x02M\x03\x02\x02\x02\x03" + + "\x87\x03\x02\x02\x02\x05\x89\x03\x02\x02\x02\x07\x8B\x03\x02\x02\x02\t" + + "\x8D\x03\x02\x02\x02\v\x8F\x03\x02\x02\x02\r\x91\x03\x02\x02\x02\x0F\x93" + + "\x03\x02\x02\x02\x11\x95\x03\x02\x02\x02\x13\x98\x03\x02\x02\x02\x15\x9A" + + "\x03\x02\x02\x02\x17\x9D\x03\x02\x02\x02\x19\x9F\x03\x02\x02\x02\x1B\xA2" + + "\x03\x02\x02\x02\x1D\xA6\x03\x02\x02\x02\x1F\xA9\x03\x02\x02\x02!\xAD" + + "\x03\x02\x02\x02#\xAF\x03\x02\x02\x02%\xB1\x03\x02\x02\x02\'\xB3\x03\x02" + + "\x02\x02)\xB5\x03\x02\x02\x02+\xB7\x03\x02\x02\x02-\xB9\x03\x02\x02\x02" + + "/\xBB\x03\x02\x02\x021\xBD\x03\x02\x02\x023\xBF\x03\x02\x02\x025\xC1\x03" + + "\x02\x02\x027\xC3\x03\x02\x02\x029\xCD\x03\x02\x02\x02;\xD9\x03\x02\x02" + + "\x02=\xE4\x03\x02\x02\x02?\xE9\x03\x02\x02\x02A\xEF\x03\x02\x02\x02C\xF4" + + "\x03\x02\x02\x02E\xF9\x03\x02\x02\x02G\xFE\x03\x02\x02\x02I\u0108\x03" + + "\x02\x02\x02K\u010E\x03\x02\x02\x02M\u0119\x03\x02\x02\x02O\u0127\x03" + + "\x02\x02\x02Q\u0129\x03\x02\x02\x02S\u012B\x03\x02\x02\x02U\u012D\x03" + + "\x02\x02\x02W\u012F\x03\x02\x02\x02Y\u0131\x03\x02\x02\x02[\u0133\x03" + + "\x02\x02\x02]\u0135\x03\x02\x02\x02_\u0137\x03\x02\x02\x02a\u0139\x03" + + "\x02\x02\x02c\u013B\x03\x02\x02\x02e\u013D\x03\x02\x02\x02g\u013F\x03" + + "\x02\x02\x02i\u0141\x03\x02\x02\x02k\u0143\x03\x02\x02\x02m\u0145\x03" + + "\x02\x02\x02o\u0147\x03\x02\x02\x02q\u0149\x03\x02\x02\x02s\u014B\x03" + + "\x02\x02\x02u\u014D\x03\x02\x02\x02w\u014F\x03\x02\x02\x02y\u0151\x03" + + "\x02\x02\x02{\u0153\x03\x02\x02\x02}\u0155\x03\x02\x02\x02\x7F\u0157\x03" + + "\x02\x02\x02\x81\u0159\x03\x02\x02\x02\x83\u015B\x03\x02\x02\x02\x85\u015D" + + "\x03\x02\x02\x02\x87\x88\x07-\x02\x02\x88\x04\x03\x02\x02\x02\x89\x8A" + + "\x07/\x02\x02\x8A\x06\x03\x02\x02\x02\x8B\x8C\x07,\x02\x02\x8C\b\x03\x02" + + "\x02\x02\x8D\x8E\x071\x02\x02\x8E\n\x03\x02\x02\x02\x8F\x90\x07\'\x02" + + "\x02\x90\f\x03\x02\x02\x02\x91\x92\x07`\x02\x02\x92\x0E\x03\x02\x02\x02" + + "\x93\x94\x07?\x02\x02\x94\x10\x03\x02\x02\x02\x95\x96\x07#\x02\x02\x96" + + "\x97\x07?\x02\x02\x97\x12\x03\x02\x02\x02\x98\x99\x07>\x02\x02\x99\x14" + + "\x03\x02\x02\x02\x9A\x9B\x07>\x02\x02\x9B\x9C\x07?\x02\x02\x9C\x16\x03" + + "\x02\x02\x02\x9D\x9E\x07@\x02\x02\x9E\x18\x03\x02\x02\x02\x9F\xA0\x07" + + "@\x02\x02\xA0\xA1\x07?\x02\x02\xA1\x1A\x03\x02\x02\x02\xA2\xA3\x05S*\x02" + + "\xA3\xA4\x05m7\x02\xA4\xA5\x05Y-\x02\xA5\x1C\x03\x02\x02\x02\xA6\xA7\x05" + + "o8\x02\xA7\xA8\x05u;\x02\xA8\x1E\x03\x02\x02\x02\xA9\xAA\x05m7\x02\xAA" + + "\xAB\x05o8\x02\xAB\xAC\x05y=\x02\xAC \x03\x02\x02\x02\xAD\xAE\x07*\x02" + + "\x02\xAE\"\x03\x02\x02\x02\xAF\xB0\x07+\x02\x02\xB0$\x03\x02\x02\x02\xB1" + + "\xB2\x07}\x02\x02\xB2&\x03\x02\x02\x02\xB3\xB4\x07\x7F\x02\x02\xB4(\x03" + + "\x02\x02\x02\xB5\xB6\x07]\x02\x02\xB6*\x03\x02\x02\x02\xB7\xB8\x07_\x02" + + "\x02\xB8,\x03\x02\x02\x02\xB9\xBA\x07.\x02\x02\xBA.\x03\x02\x02\x02\xBB" + + "\xBC\x07=\x02\x02\xBC0\x03\x02\x02\x02\xBD\xBE\x07<\x02\x02\xBE2\x03\x02" + + "\x02\x02\xBF\xC0\x070\x02\x02\xC04\x03\x02\x02\x02\xC1\xC2\x07a\x02\x02" + + "\xC26\x03\x02\x02\x02\xC3\xC9\x05Q)\x02\xC4\xC8\x05Q)\x02\xC5\xC8\x05" + + "O(\x02\xC6\xC8\x055\x1B\x02\xC7\xC4\x03\x02\x02\x02\xC7\xC5\x03\x02\x02" + + "\x02\xC7\xC6\x03\x02\x02\x02\xC8\xCB\x03\x02\x02\x02\xC9\xC7\x03\x02\x02" + + "\x02\xC9\xCA\x03\x02\x02\x02\xCA8\x03\x02\x02\x02\xCB\xC9\x03\x02\x02" + + "\x02\xCC\xCE\x05O(\x02\xCD\xCC\x03\x02\x02\x02\xCE\xCF\x03\x02\x02\x02" + + "\xCF\xCD\x03\x02\x02\x02\xCF\xD0\x03\x02\x02\x02\xD0\xD7\x03\x02\x02\x02" + + "\xD1\xD3\x070\x02\x02\xD2\xD4\x05O(\x02\xD3\xD2\x03\x02\x02\x02\xD4\xD5" + + "\x03\x02\x02\x02\xD5\xD3\x03\x02\x02\x02\xD5\xD6\x03\x02\x02\x02\xD6\xD8" + + "\x03\x02\x02\x02\xD7\xD1\x03\x02\x02\x02\xD7\xD8\x03\x02\x02\x02\xD8:" + + "\x03\x02\x02\x02\xD9\xDF\x07)\x02\x02\xDA\xDE\n\x02\x02\x02\xDB\xDC\x07" + + ")\x02\x02\xDC\xDE\x07)\x02\x02\xDD\xDA\x03\x02\x02\x02\xDD\xDB\x03\x02" + + "\x02\x02\xDE\xE1\x03\x02\x02\x02\xDF\xDD\x03\x02\x02\x02\xDF\xE0\x03\x02" + + "\x02\x02\xE0\xE2\x03\x02\x02\x02\xE1\xDF\x03\x02\x02\x02\xE2\xE3\x07)" + + "\x02\x02\xE3<\x03\x02\x02\x02\xE4\xE5\x05y=\x02\xE5\xE6\x05u;\x02\xE6" + + "\xE7\x05{>\x02\xE7\xE8\x05[.\x02\xE8>\x03\x02\x02\x02\xE9\xEA\x05]/\x02" + + "\xEA\xEB\x05S*\x02\xEB\xEC\x05i5\x02\xEC\xED\x05w<\x02\xED\xEE\x05[.\x02" + + "\xEE@\x03\x02\x02\x02\xEF\xF0\x05m7\x02\xF0\xF1\x05{>\x02\xF1\xF2\x05" + + "i5\x02\xF2\xF3\x05i5\x02\xF3B\x03\x02\x02\x02\xF4\xF5\x05Y-\x02\xF5\xF6" + + "\x05S*\x02\xF6\xF7\x05y=\x02\xF7\xF8\x05[.\x02\xF8D\x03\x02\x02\x02\xF9" + + "\xFA\x05y=\x02\xFA\xFB\x05c2\x02\xFB\xFC\x05k6\x02\xFC\xFD\x05[.\x02\xFD" + + "F\x03\x02\x02\x02\xFE\xFF\x05Y-\x02\xFF\u0100\x05S*\x02\u0100\u0101\x05" + + "y=\x02\u0101\u0102\x05[.\x02\u0102\u0103\x05y=\x02\u0103\u0104\x05c2\x02" + + "\u0104\u0105\x05k6\x02\u0105\u0106\x05[.\x02\u0106H\x03\x02\x02\x02\u0107" + + "\u0109\t\x03\x02\x02\u0108\u0107\x03\x02\x02\x02\u0109\u010A\x03\x02\x02" + + "\x02\u010A\u0108\x03\x02\x02\x02\u010A\u010B\x03\x02\x02\x02\u010B\u010C" + + "\x03\x02\x02\x02\u010C\u010D\b%\x02\x02\u010DJ\x03\x02\x02\x02\u010E\u010F" + + "\x071\x02\x02\u010F\u0110\x071\x02\x02\u0110\u0114\x03\x02\x02\x02\u0111" + + "\u0113\n\x04\x02\x02\u0112\u0111\x03\x02\x02\x02\u0113\u0116\x03\x02\x02" + + "\x02\u0114\u0112\x03\x02\x02\x02\u0114\u0115\x03\x02\x02\x02\u0115\u0117" + + "\x03\x02\x02\x02\u0116\u0114\x03\x02\x02\x02\u0117\u0118\b&\x02\x02\u0118" + + "L\x03\x02\x02\x02\u0119\u011A\x071\x02\x02\u011A\u011B\x07,\x02\x02\u011B" + + "\u011F\x03\x02\x02\x02\u011C\u011E\v\x02\x02\x02\u011D\u011C\x03\x02\x02" + + "\x02\u011E\u0121\x03\x02\x02\x02\u011F\u0120\x03\x02\x02\x02\u011F\u011D" + + "\x03\x02\x02\x02\u0120\u0122\x03\x02\x02\x02\u0121\u011F\x03\x02\x02\x02" + + "\u0122\u0123\x07,\x02\x02\u0123\u0124\x071\x02\x02\u0124\u0125\x03\x02" + + "\x02\x02\u0125\u0126\b\'\x02\x02\u0126N\x03\x02\x02\x02\u0127\u0128\t" + + "\x05\x02\x02\u0128P\x03\x02\x02\x02\u0129\u012A\t\x06\x02\x02\u012AR\x03" + + "\x02\x02\x02\u012B\u012C\t\x07\x02\x02\u012CT\x03\x02\x02\x02\u012D\u012E" + + "\t\b\x02\x02\u012EV\x03\x02\x02\x02\u012F\u0130\t\t\x02\x02\u0130X\x03" + + "\x02\x02\x02\u0131\u0132\t\n\x02\x02\u0132Z\x03\x02\x02\x02\u0133\u0134" + + "\t\v\x02\x02\u0134\\\x03\x02\x02\x02\u0135\u0136\t\f\x02\x02\u0136^\x03" + + "\x02\x02\x02\u0137\u0138\t\r\x02\x02\u0138`\x03\x02\x02\x02\u0139\u013A" + + "\t\x0E\x02\x02\u013Ab\x03\x02\x02\x02\u013B\u013C\t\x0F\x02\x02\u013C" + + "d\x03\x02\x02\x02\u013D\u013E\t\x10\x02\x02\u013Ef\x03\x02\x02\x02\u013F" + + "\u0140\t\x11\x02\x02\u0140h\x03\x02\x02\x02\u0141\u0142\t\x12\x02\x02" + + "\u0142j\x03\x02\x02\x02\u0143\u0144\t\x13\x02\x02\u0144l\x03\x02\x02\x02" + + "\u0145\u0146\t\x14\x02\x02\u0146n\x03\x02\x02\x02\u0147\u0148\t\x15\x02" + + "\x02\u0148p\x03\x02\x02\x02\u0149\u014A\t\x16\x02\x02\u014Ar\x03\x02\x02" + + "\x02\u014B\u014C\t\x17\x02\x02\u014Ct\x03\x02\x02\x02\u014D\u014E\t\x18" + + "\x02\x02\u014Ev\x03\x02\x02\x02\u014F\u0150\t\x19\x02\x02\u0150x\x03\x02" + + "\x02\x02\u0151\u0152\t\x1A\x02\x02\u0152z\x03\x02\x02\x02\u0153\u0154" + + "\t\x1B\x02\x02\u0154|\x03\x02\x02\x02\u0155\u0156\t\x1C\x02\x02\u0156" + + "~\x03\x02\x02\x02\u0157\u0158\t\x1D\x02\x02\u0158\x80\x03\x02\x02\x02" + + "\u0159\u015A\t\x1E\x02\x02\u015A\x82\x03\x02\x02\x02\u015B\u015C\t\x1F" + + "\x02\x02\u015C\x84\x03\x02\x02\x02\u015D\u015E\t \x02\x02\u015E\x86\x03" + + "\x02\x02\x02\r\x02\xC7\xC9\xCF\xD5\xD7\xDD\xDF\u010A\u0114\u011F\x03\b" + + "\x02\x02"; + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!FormulaLexer.__ATN) { + FormulaLexer.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(FormulaLexer._serializedATN)); + } + + return FormulaLexer.__ATN; + } + +} + diff --git a/packages/formula/src/grammar/FormulaParser.g4 b/packages/formula/src/grammar/FormulaParser.g4 new file mode 100644 index 000000000..c36a998f3 --- /dev/null +++ b/packages/formula/src/grammar/FormulaParser.g4 @@ -0,0 +1,41 @@ +parser grammar FormulaParser; + +options { + tokenVocab = FormulaLexer; +} + +formula: expression EOF; + +expression: + expression op = (MULTIPLY | DIVIDE | MODULO) expression # MulDivModExpr + | expression op = (ADD | SUBTRACT) expression # AddSubExpr + | expression POWER expression # PowerExpr + | expression op = ( + EQUAL + | NOT_EQUAL + | LESS + | LESS_EQUAL + | GREATER + | GREATER_EQUAL + ) expression # ComparisonExpr + | expression AND expression # AndExpr + | expression OR expression # OrExpr + | NOT expression # NotExpr + | functionCall # FunctionExpr + | variable # VariableExpr + | NUMBER # NumberExpr + | STRING # StringExpr + | TRUE # TrueExpr + | FALSE # FalseExpr + | NULL # NullExpr + | DATE # DateExpr + | TIME # TimeExpr + | DATETIME # DateTimeExpr + | LPAREN expression RPAREN # ParenExpr; + +functionCall: IDENTIFIER LPAREN argumentList? RPAREN; + +argumentList: expression (COMMA expression)*; +// 这个表达式定义了一个参数列表,由一个或多个表达式组成,表达式之间用逗号分隔。例如:func(1, 2, 3) 中,1, 2, 3 就是参数列表。 + +variable: LBRACE LBRACE IDENTIFIER RBRACE RBRACE; \ No newline at end of file diff --git a/packages/formula/src/grammar/FormulaParser.interp b/packages/formula/src/grammar/FormulaParser.interp new file mode 100644 index 000000000..0c855ac47 --- /dev/null +++ b/packages/formula/src/grammar/FormulaParser.interp @@ -0,0 +1,92 @@ +token literal names: +null +'+' +'-' +'*' +'/' +'%' +'^' +'=' +'!=' +'<' +'<=' +'>' +'>=' +null +null +null +'(' +')' +'{' +'}' +'[' +']' +',' +';' +':' +'.' +'_' +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +ADD +SUBTRACT +MULTIPLY +DIVIDE +MODULO +POWER +EQUAL +NOT_EQUAL +LESS +LESS_EQUAL +GREATER +GREATER_EQUAL +AND +OR +NOT +LPAREN +RPAREN +LBRACE +RBRACE +LBRACKET +RBRACKET +COMMA +SEMICOLON +COLON +DOT +UNDERSCORE +IDENTIFIER +NUMBER +STRING +TRUE +FALSE +NULL +DATE +TIME +DATETIME +WS +COMMENT +MULTILINE_COMMENT + +rule names: +formula +expression +functionCall +argumentList +variable + + +atn: +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 40, 79, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 33, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 53, 10, 3, 12, 3, 14, 3, 56, 11, 3, 3, 4, 3, 4, 3, 4, 5, 4, 61, 10, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 7, 5, 68, 10, 5, 12, 5, 14, 5, 71, 11, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 2, 2, 3, 4, 7, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 2, 5, 3, 2, 5, 7, 3, 2, 3, 4, 3, 2, 9, 14, 2, 92, 2, 12, 3, 2, 2, 2, 4, 32, 3, 2, 2, 2, 6, 57, 3, 2, 2, 2, 8, 64, 3, 2, 2, 2, 10, 72, 3, 2, 2, 2, 12, 13, 5, 4, 3, 2, 13, 14, 7, 2, 2, 3, 14, 3, 3, 2, 2, 2, 15, 16, 8, 3, 1, 2, 16, 17, 7, 17, 2, 2, 17, 33, 5, 4, 3, 14, 18, 33, 5, 6, 4, 2, 19, 33, 5, 10, 6, 2, 20, 33, 7, 30, 2, 2, 21, 33, 7, 31, 2, 2, 22, 33, 7, 32, 2, 2, 23, 33, 7, 33, 2, 2, 24, 33, 7, 34, 2, 2, 25, 33, 7, 35, 2, 2, 26, 33, 7, 36, 2, 2, 27, 33, 7, 37, 2, 2, 28, 29, 7, 18, 2, 2, 29, 30, 5, 4, 3, 2, 30, 31, 7, 19, 2, 2, 31, 33, 3, 2, 2, 2, 32, 15, 3, 2, 2, 2, 32, 18, 3, 2, 2, 2, 32, 19, 3, 2, 2, 2, 32, 20, 3, 2, 2, 2, 32, 21, 3, 2, 2, 2, 32, 22, 3, 2, 2, 2, 32, 23, 3, 2, 2, 2, 32, 24, 3, 2, 2, 2, 32, 25, 3, 2, 2, 2, 32, 26, 3, 2, 2, 2, 32, 27, 3, 2, 2, 2, 32, 28, 3, 2, 2, 2, 33, 54, 3, 2, 2, 2, 34, 35, 12, 20, 2, 2, 35, 36, 9, 2, 2, 2, 36, 53, 5, 4, 3, 21, 37, 38, 12, 19, 2, 2, 38, 39, 9, 3, 2, 2, 39, 53, 5, 4, 3, 20, 40, 41, 12, 18, 2, 2, 41, 42, 7, 8, 2, 2, 42, 53, 5, 4, 3, 19, 43, 44, 12, 17, 2, 2, 44, 45, 9, 4, 2, 2, 45, 53, 5, 4, 3, 18, 46, 47, 12, 16, 2, 2, 47, 48, 7, 15, 2, 2, 48, 53, 5, 4, 3, 17, 49, 50, 12, 15, 2, 2, 50, 51, 7, 16, 2, 2, 51, 53, 5, 4, 3, 16, 52, 34, 3, 2, 2, 2, 52, 37, 3, 2, 2, 2, 52, 40, 3, 2, 2, 2, 52, 43, 3, 2, 2, 2, 52, 46, 3, 2, 2, 2, 52, 49, 3, 2, 2, 2, 53, 56, 3, 2, 2, 2, 54, 52, 3, 2, 2, 2, 54, 55, 3, 2, 2, 2, 55, 5, 3, 2, 2, 2, 56, 54, 3, 2, 2, 2, 57, 58, 7, 29, 2, 2, 58, 60, 7, 18, 2, 2, 59, 61, 5, 8, 5, 2, 60, 59, 3, 2, 2, 2, 60, 61, 3, 2, 2, 2, 61, 62, 3, 2, 2, 2, 62, 63, 7, 19, 2, 2, 63, 7, 3, 2, 2, 2, 64, 69, 5, 4, 3, 2, 65, 66, 7, 24, 2, 2, 66, 68, 5, 4, 3, 2, 67, 65, 3, 2, 2, 2, 68, 71, 3, 2, 2, 2, 69, 67, 3, 2, 2, 2, 69, 70, 3, 2, 2, 2, 70, 9, 3, 2, 2, 2, 71, 69, 3, 2, 2, 2, 72, 73, 7, 20, 2, 2, 73, 74, 7, 20, 2, 2, 74, 75, 7, 29, 2, 2, 75, 76, 7, 21, 2, 2, 76, 77, 7, 21, 2, 2, 77, 11, 3, 2, 2, 2, 7, 32, 52, 54, 60, 69] \ No newline at end of file diff --git a/packages/formula/src/grammar/FormulaParser.tokens b/packages/formula/src/grammar/FormulaParser.tokens new file mode 100644 index 000000000..5ad732765 --- /dev/null +++ b/packages/formula/src/grammar/FormulaParser.tokens @@ -0,0 +1,61 @@ +ADD=1 +SUBTRACT=2 +MULTIPLY=3 +DIVIDE=4 +MODULO=5 +POWER=6 +EQUAL=7 +NOT_EQUAL=8 +LESS=9 +LESS_EQUAL=10 +GREATER=11 +GREATER_EQUAL=12 +AND=13 +OR=14 +NOT=15 +LPAREN=16 +RPAREN=17 +LBRACE=18 +RBRACE=19 +LBRACKET=20 +RBRACKET=21 +COMMA=22 +SEMICOLON=23 +COLON=24 +DOT=25 +UNDERSCORE=26 +IDENTIFIER=27 +NUMBER=28 +STRING=29 +TRUE=30 +FALSE=31 +NULL=32 +DATE=33 +TIME=34 +DATETIME=35 +WS=36 +COMMENT=37 +MULTILINE_COMMENT=38 +'+'=1 +'-'=2 +'*'=3 +'/'=4 +'%'=5 +'^'=6 +'='=7 +'!='=8 +'<'=9 +'<='=10 +'>'=11 +'>='=12 +'('=16 +')'=17 +'{'=18 +'}'=19 +'['=20 +']'=21 +','=22 +';'=23 +':'=24 +'.'=25 +'_'=26 diff --git a/packages/formula/src/grammar/FormulaParser.ts b/packages/formula/src/grammar/FormulaParser.ts new file mode 100644 index 000000000..7ce7934a8 --- /dev/null +++ b/packages/formula/src/grammar/FormulaParser.ts @@ -0,0 +1,1110 @@ +// Generated from src/grammar/FormulaParser.g4 by ANTLR 4.9.0-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { FailedPredicateException } from "antlr4ts/FailedPredicateException"; +import { NotNull } from "antlr4ts/Decorators"; +import { NoViableAltException } from "antlr4ts/NoViableAltException"; +import { Override } from "antlr4ts/Decorators"; +import { Parser } from "antlr4ts/Parser"; +import { ParserRuleContext } from "antlr4ts/ParserRuleContext"; +import { ParserATNSimulator } from "antlr4ts/atn/ParserATNSimulator"; +import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; +import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor"; +import { RecognitionException } from "antlr4ts/RecognitionException"; +import { RuleContext } from "antlr4ts/RuleContext"; +//import { RuleVersion } from "antlr4ts/RuleVersion"; +import { TerminalNode } from "antlr4ts/tree/TerminalNode"; +import { Token } from "antlr4ts/Token"; +import { TokenStream } from "antlr4ts/TokenStream"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + +import { FormulaParserVisitor } from "./FormulaParserVisitor"; + + +export class FormulaParser extends Parser { + public static readonly ADD = 1; + public static readonly SUBTRACT = 2; + public static readonly MULTIPLY = 3; + public static readonly DIVIDE = 4; + public static readonly MODULO = 5; + public static readonly POWER = 6; + public static readonly EQUAL = 7; + public static readonly NOT_EQUAL = 8; + public static readonly LESS = 9; + public static readonly LESS_EQUAL = 10; + public static readonly GREATER = 11; + public static readonly GREATER_EQUAL = 12; + public static readonly AND = 13; + public static readonly OR = 14; + public static readonly NOT = 15; + public static readonly LPAREN = 16; + public static readonly RPAREN = 17; + public static readonly LBRACE = 18; + public static readonly RBRACE = 19; + public static readonly LBRACKET = 20; + public static readonly RBRACKET = 21; + public static readonly COMMA = 22; + public static readonly SEMICOLON = 23; + public static readonly COLON = 24; + public static readonly DOT = 25; + public static readonly UNDERSCORE = 26; + public static readonly IDENTIFIER = 27; + public static readonly NUMBER = 28; + public static readonly STRING = 29; + public static readonly TRUE = 30; + public static readonly FALSE = 31; + public static readonly NULL = 32; + public static readonly DATE = 33; + public static readonly TIME = 34; + public static readonly DATETIME = 35; + public static readonly WS = 36; + public static readonly COMMENT = 37; + public static readonly MULTILINE_COMMENT = 38; + public static readonly RULE_formula = 0; + public static readonly RULE_expression = 1; + public static readonly RULE_functionCall = 2; + public static readonly RULE_argumentList = 3; + public static readonly RULE_variable = 4; + // tslint:disable:no-trailing-whitespace + public static readonly ruleNames: string[] = [ + "formula", "expression", "functionCall", "argumentList", "variable", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, "'+'", "'-'", "'*'", "'/'", "'%'", "'^'", "'='", "'!='", "'<'", + "'<='", "'>'", "'>='", undefined, undefined, undefined, "'('", "')'", + "'{'", "'}'", "'['", "']'", "','", "';'", "':'", "'.'", "'_'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER", + "EQUAL", "NOT_EQUAL", "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL", + "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", + "RBRACKET", "COMMA", "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER", + "NUMBER", "STRING", "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME", + "WS", "COMMENT", "MULTILINE_COMMENT", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(FormulaParser._LITERAL_NAMES, FormulaParser._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return FormulaParser.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + // @Override + public get grammarFileName(): string { return "FormulaParser.g4"; } + + // @Override + public get ruleNames(): string[] { return FormulaParser.ruleNames; } + + // @Override + public get serializedATN(): string { return FormulaParser._serializedATN; } + + protected createFailedPredicateException(predicate?: string, message?: string): FailedPredicateException { + return new FailedPredicateException(this, predicate, message); + } + + constructor(input: TokenStream) { + super(input); + this._interp = new ParserATNSimulator(FormulaParser._ATN, this); + } + // @RuleVersion(0) + public formula(): FormulaContext { + let _localctx: FormulaContext = new FormulaContext(this._ctx, this.state); + this.enterRule(_localctx, 0, FormulaParser.RULE_formula); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 10; + this.expression(0); + this.state = 11; + this.match(FormulaParser.EOF); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public expression(): ExpressionContext; + public expression(_p: number): ExpressionContext; + // @RuleVersion(0) + public expression(_p?: number): ExpressionContext { + if (_p === undefined) { + _p = 0; + } + + let _parentctx: ParserRuleContext = this._ctx; + let _parentState: number = this.state; + let _localctx: ExpressionContext = new ExpressionContext(this._ctx, _parentState); + let _prevctx: ExpressionContext = _localctx; + let _startState: number = 2; + this.enterRecursionRule(_localctx, 2, FormulaParser.RULE_expression, _p); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 30; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case FormulaParser.NOT: + { + _localctx = new NotExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + + this.state = 14; + this.match(FormulaParser.NOT); + this.state = 15; + this.expression(12); + } + break; + case FormulaParser.IDENTIFIER: + { + _localctx = new FunctionExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 16; + this.functionCall(); + } + break; + case FormulaParser.LBRACE: + { + _localctx = new VariableExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 17; + this.variable(); + } + break; + case FormulaParser.NUMBER: + { + _localctx = new NumberExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 18; + this.match(FormulaParser.NUMBER); + } + break; + case FormulaParser.STRING: + { + _localctx = new StringExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 19; + this.match(FormulaParser.STRING); + } + break; + case FormulaParser.TRUE: + { + _localctx = new TrueExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 20; + this.match(FormulaParser.TRUE); + } + break; + case FormulaParser.FALSE: + { + _localctx = new FalseExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 21; + this.match(FormulaParser.FALSE); + } + break; + case FormulaParser.NULL: + { + _localctx = new NullExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 22; + this.match(FormulaParser.NULL); + } + break; + case FormulaParser.DATE: + { + _localctx = new DateExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 23; + this.match(FormulaParser.DATE); + } + break; + case FormulaParser.TIME: + { + _localctx = new TimeExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 24; + this.match(FormulaParser.TIME); + } + break; + case FormulaParser.DATETIME: + { + _localctx = new DateTimeExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 25; + this.match(FormulaParser.DATETIME); + } + break; + case FormulaParser.LPAREN: + { + _localctx = new ParenExprContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + this.state = 26; + this.match(FormulaParser.LPAREN); + this.state = 27; + this.expression(0); + this.state = 28; + this.match(FormulaParser.RPAREN); + } + break; + default: + throw new NoViableAltException(this); + } + this._ctx._stop = this._input.tryLT(-1); + this.state = 52; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 2, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + if (this._parseListeners != null) { + this.triggerExitRuleEvent(); + } + _prevctx = _localctx; + { + this.state = 50; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 1, this._ctx) ) { + case 1: + { + _localctx = new MulDivModExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 32; + if (!(this.precpred(this._ctx, 18))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 18)"); + } + this.state = 33; + (_localctx as MulDivModExprContext)._op = this._input.LT(1); + _la = this._input.LA(1); + if (!((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << FormulaParser.MULTIPLY) | (1 << FormulaParser.DIVIDE) | (1 << FormulaParser.MODULO))) !== 0))) { + (_localctx as MulDivModExprContext)._op = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 34; + this.expression(19); + } + break; + + case 2: + { + _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 35; + if (!(this.precpred(this._ctx, 17))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 17)"); + } + this.state = 36; + (_localctx as AddSubExprContext)._op = this._input.LT(1); + _la = this._input.LA(1); + if (!(_la === FormulaParser.ADD || _la === FormulaParser.SUBTRACT)) { + (_localctx as AddSubExprContext)._op = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 37; + this.expression(18); + } + break; + + case 3: + { + _localctx = new PowerExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 38; + if (!(this.precpred(this._ctx, 16))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 16)"); + } + this.state = 39; + this.match(FormulaParser.POWER); + this.state = 40; + this.expression(17); + } + break; + + case 4: + { + _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 41; + if (!(this.precpred(this._ctx, 15))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 15)"); + } + this.state = 42; + (_localctx as ComparisonExprContext)._op = this._input.LT(1); + _la = this._input.LA(1); + if (!((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << FormulaParser.EQUAL) | (1 << FormulaParser.NOT_EQUAL) | (1 << FormulaParser.LESS) | (1 << FormulaParser.LESS_EQUAL) | (1 << FormulaParser.GREATER) | (1 << FormulaParser.GREATER_EQUAL))) !== 0))) { + (_localctx as ComparisonExprContext)._op = this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 43; + this.expression(16); + } + break; + + case 5: + { + _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 44; + if (!(this.precpred(this._ctx, 14))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 14)"); + } + this.state = 45; + this.match(FormulaParser.AND); + this.state = 46; + this.expression(15); + } + break; + + case 6: + { + _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression); + this.state = 47; + if (!(this.precpred(this._ctx, 13))) { + throw this.createFailedPredicateException("this.precpred(this._ctx, 13)"); + } + this.state = 48; + this.match(FormulaParser.OR); + this.state = 49; + this.expression(14); + } + break; + } + } + } + this.state = 54; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 2, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.unrollRecursionContexts(_parentctx); + } + return _localctx; + } + // @RuleVersion(0) + public functionCall(): FunctionCallContext { + let _localctx: FunctionCallContext = new FunctionCallContext(this._ctx, this.state); + this.enterRule(_localctx, 4, FormulaParser.RULE_functionCall); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 55; + this.match(FormulaParser.IDENTIFIER); + this.state = 56; + this.match(FormulaParser.LPAREN); + this.state = 58; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 15)) & ~0x1F) === 0 && ((1 << (_la - 15)) & ((1 << (FormulaParser.NOT - 15)) | (1 << (FormulaParser.LPAREN - 15)) | (1 << (FormulaParser.LBRACE - 15)) | (1 << (FormulaParser.IDENTIFIER - 15)) | (1 << (FormulaParser.NUMBER - 15)) | (1 << (FormulaParser.STRING - 15)) | (1 << (FormulaParser.TRUE - 15)) | (1 << (FormulaParser.FALSE - 15)) | (1 << (FormulaParser.NULL - 15)) | (1 << (FormulaParser.DATE - 15)) | (1 << (FormulaParser.TIME - 15)) | (1 << (FormulaParser.DATETIME - 15)))) !== 0)) { + { + this.state = 57; + this.argumentList(); + } + } + + this.state = 60; + this.match(FormulaParser.RPAREN); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public argumentList(): ArgumentListContext { + let _localctx: ArgumentListContext = new ArgumentListContext(this._ctx, this.state); + this.enterRule(_localctx, 6, FormulaParser.RULE_argumentList); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 62; + this.expression(0); + this.state = 67; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === FormulaParser.COMMA) { + { + { + this.state = 63; + this.match(FormulaParser.COMMA); + this.state = 64; + this.expression(0); + } + } + this.state = 69; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public variable(): VariableContext { + let _localctx: VariableContext = new VariableContext(this._ctx, this.state); + this.enterRule(_localctx, 8, FormulaParser.RULE_variable); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 70; + this.match(FormulaParser.LBRACE); + this.state = 71; + this.match(FormulaParser.LBRACE); + this.state = 72; + this.match(FormulaParser.IDENTIFIER); + this.state = 73; + this.match(FormulaParser.RBRACE); + this.state = 74; + this.match(FormulaParser.RBRACE); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { + switch (ruleIndex) { + case 1: + return this.expression_sempred(_localctx as ExpressionContext, predIndex); + } + return true; + } + private expression_sempred(_localctx: ExpressionContext, predIndex: number): boolean { + switch (predIndex) { + case 0: + return this.precpred(this._ctx, 18); + + case 1: + return this.precpred(this._ctx, 17); + + case 2: + return this.precpred(this._ctx, 16); + + case 3: + return this.precpred(this._ctx, 15); + + case 4: + return this.precpred(this._ctx, 14); + + case 5: + return this.precpred(this._ctx, 13); + } + return true; + } + + public static readonly _serializedATN: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03(O\x04\x02\t\x02" + + "\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x03\x02\x03\x02" + + "\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + + "\x05\x03!\n\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x07\x035\n\x03\f\x03\x0E\x038\v\x03\x03\x04\x03\x04\x03" + + "\x04\x05\x04=\n\x04\x03\x04\x03\x04\x03\x05\x03\x05\x03\x05\x07\x05D\n" + + "\x05\f\x05\x0E\x05G\v\x05\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" + + "\x06\x03\x06\x02\x02\x03\x04\x07\x02\x02\x04\x02\x06\x02\b\x02\n\x02\x02" + + "\x05\x03\x02\x05\x07\x03\x02\x03\x04\x03\x02\t\x0E\x02\\\x02\f\x03\x02" + + "\x02\x02\x04 \x03\x02\x02\x02\x069\x03\x02\x02\x02\b@\x03\x02\x02\x02" + + "\nH\x03\x02\x02\x02\f\r\x05\x04\x03\x02\r\x0E\x07\x02\x02\x03\x0E\x03" + + "\x03\x02\x02\x02\x0F\x10\b\x03\x01\x02\x10\x11\x07\x11\x02\x02\x11!\x05" + + "\x04\x03\x0E\x12!\x05\x06\x04\x02\x13!\x05\n\x06\x02\x14!\x07\x1E\x02" + + "\x02\x15!\x07\x1F\x02\x02\x16!\x07 \x02\x02\x17!\x07!\x02\x02\x18!\x07" + + "\"\x02\x02\x19!\x07#\x02\x02\x1A!\x07$\x02\x02\x1B!\x07%\x02\x02\x1C\x1D" + + "\x07\x12\x02\x02\x1D\x1E\x05\x04\x03\x02\x1E\x1F\x07\x13\x02\x02\x1F!" + + "\x03\x02\x02\x02 \x0F\x03\x02\x02\x02 \x12\x03\x02\x02\x02 \x13\x03\x02" + + "\x02\x02 \x14\x03\x02\x02\x02 \x15\x03\x02\x02\x02 \x16\x03\x02\x02\x02" + + " \x17\x03\x02\x02\x02 \x18\x03\x02\x02\x02 \x19\x03\x02\x02\x02 \x1A\x03" + + "\x02\x02\x02 \x1B\x03\x02\x02\x02 \x1C\x03\x02\x02\x02!6\x03\x02\x02\x02" + + "\"#\f\x14\x02\x02#$\t\x02\x02\x02$5\x05\x04\x03\x15%&\f\x13\x02\x02&\'" + + "\t\x03\x02\x02\'5\x05\x04\x03\x14()\f\x12\x02\x02)*\x07\b\x02\x02*5\x05" + + "\x04\x03\x13+,\f\x11\x02\x02,-\t\x04\x02\x02-5\x05\x04\x03\x12./\f\x10" + + "\x02\x02/0\x07\x0F\x02\x0205\x05\x04\x03\x1112\f\x0F\x02\x0223\x07\x10" + + "\x02\x0235\x05\x04\x03\x104\"\x03\x02\x02\x024%\x03\x02\x02\x024(\x03" + + "\x02\x02\x024+\x03\x02\x02\x024.\x03\x02\x02\x0241\x03\x02\x02\x0258\x03" + + "\x02\x02\x0264\x03\x02\x02\x0267\x03\x02\x02\x027\x05\x03\x02\x02\x02" + + "86\x03\x02\x02\x029:\x07\x1D\x02\x02:<\x07\x12\x02\x02;=\x05\b\x05\x02" + + "<;\x03\x02\x02\x02<=\x03\x02\x02\x02=>\x03\x02\x02\x02>?\x07\x13\x02\x02" + + "?\x07\x03\x02\x02\x02@E\x05\x04\x03\x02AB\x07\x18\x02\x02BD\x05\x04\x03" + + "\x02CA\x03\x02\x02\x02DG\x03\x02\x02\x02EC\x03\x02\x02\x02EF\x03\x02\x02" + + "\x02F\t\x03\x02\x02\x02GE\x03\x02\x02\x02HI\x07\x14\x02\x02IJ\x07\x14" + + "\x02\x02JK\x07\x1D\x02\x02KL\x07\x15\x02\x02LM\x07\x15\x02\x02M\v\x03" + + "\x02\x02\x02\x07 46(visitor: FormulaParserVisitor): Result { + if (visitor.visitFormula) { + return visitor.visitFormula(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ExpressionContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return FormulaParser.RULE_expression; } + public copyFrom(ctx: ExpressionContext): void { + super.copyFrom(ctx); + } +} +export class MulDivModExprContext extends ExpressionContext { + public _op!: Token; + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public MULTIPLY(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.MULTIPLY, 0); } + public DIVIDE(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.DIVIDE, 0); } + public MODULO(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.MODULO, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitMulDivModExpr) { + return visitor.visitMulDivModExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class AddSubExprContext extends ExpressionContext { + public _op!: Token; + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public ADD(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.ADD, 0); } + public SUBTRACT(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.SUBTRACT, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitAddSubExpr) { + return visitor.visitAddSubExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class PowerExprContext extends ExpressionContext { + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public POWER(): TerminalNode { return this.getToken(FormulaParser.POWER, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitPowerExpr) { + return visitor.visitPowerExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class ComparisonExprContext extends ExpressionContext { + public _op!: Token; + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.EQUAL, 0); } + public NOT_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.NOT_EQUAL, 0); } + public LESS(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.LESS, 0); } + public LESS_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.LESS_EQUAL, 0); } + public GREATER(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.GREATER, 0); } + public GREATER_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.GREATER_EQUAL, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitComparisonExpr) { + return visitor.visitComparisonExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class AndExprContext extends ExpressionContext { + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public AND(): TerminalNode { return this.getToken(FormulaParser.AND, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitAndExpr) { + return visitor.visitAndExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class OrExprContext extends ExpressionContext { + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public OR(): TerminalNode { return this.getToken(FormulaParser.OR, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitOrExpr) { + return visitor.visitOrExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class NotExprContext extends ExpressionContext { + public NOT(): TerminalNode { return this.getToken(FormulaParser.NOT, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitNotExpr) { + return visitor.visitNotExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class FunctionExprContext extends ExpressionContext { + public functionCall(): FunctionCallContext { + return this.getRuleContext(0, FunctionCallContext); + } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitFunctionExpr) { + return visitor.visitFunctionExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class VariableExprContext extends ExpressionContext { + public variable(): VariableContext { + return this.getRuleContext(0, VariableContext); + } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitVariableExpr) { + return visitor.visitVariableExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class NumberExprContext extends ExpressionContext { + public NUMBER(): TerminalNode { return this.getToken(FormulaParser.NUMBER, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitNumberExpr) { + return visitor.visitNumberExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class StringExprContext extends ExpressionContext { + public STRING(): TerminalNode { return this.getToken(FormulaParser.STRING, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitStringExpr) { + return visitor.visitStringExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class TrueExprContext extends ExpressionContext { + public TRUE(): TerminalNode { return this.getToken(FormulaParser.TRUE, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitTrueExpr) { + return visitor.visitTrueExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class FalseExprContext extends ExpressionContext { + public FALSE(): TerminalNode { return this.getToken(FormulaParser.FALSE, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitFalseExpr) { + return visitor.visitFalseExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class NullExprContext extends ExpressionContext { + public NULL(): TerminalNode { return this.getToken(FormulaParser.NULL, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitNullExpr) { + return visitor.visitNullExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class DateExprContext extends ExpressionContext { + public DATE(): TerminalNode { return this.getToken(FormulaParser.DATE, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitDateExpr) { + return visitor.visitDateExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class TimeExprContext extends ExpressionContext { + public TIME(): TerminalNode { return this.getToken(FormulaParser.TIME, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitTimeExpr) { + return visitor.visitTimeExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class DateTimeExprContext extends ExpressionContext { + public DATETIME(): TerminalNode { return this.getToken(FormulaParser.DATETIME, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitDateTimeExpr) { + return visitor.visitDateTimeExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} +export class ParenExprContext extends ExpressionContext { + public LPAREN(): TerminalNode { return this.getToken(FormulaParser.LPAREN, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RPAREN(): TerminalNode { return this.getToken(FormulaParser.RPAREN, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitParenExpr) { + return visitor.visitParenExpr(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class FunctionCallContext extends ParserRuleContext { + public IDENTIFIER(): TerminalNode { return this.getToken(FormulaParser.IDENTIFIER, 0); } + public LPAREN(): TerminalNode { return this.getToken(FormulaParser.LPAREN, 0); } + public RPAREN(): TerminalNode { return this.getToken(FormulaParser.RPAREN, 0); } + public argumentList(): ArgumentListContext | undefined { + return this.tryGetRuleContext(0, ArgumentListContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return FormulaParser.RULE_functionCall; } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitFunctionCall) { + return visitor.visitFunctionCall(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ArgumentListContext extends ParserRuleContext { + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(FormulaParser.COMMA); + } else { + return this.getToken(FormulaParser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return FormulaParser.RULE_argumentList; } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitArgumentList) { + return visitor.visitArgumentList(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class VariableContext extends ParserRuleContext { + public LBRACE(): TerminalNode[]; + public LBRACE(i: number): TerminalNode; + public LBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(FormulaParser.LBRACE); + } else { + return this.getToken(FormulaParser.LBRACE, i); + } + } + public IDENTIFIER(): TerminalNode { return this.getToken(FormulaParser.IDENTIFIER, 0); } + public RBRACE(): TerminalNode[]; + public RBRACE(i: number): TerminalNode; + public RBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(FormulaParser.RBRACE); + } else { + return this.getToken(FormulaParser.RBRACE, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return FormulaParser.RULE_variable; } + // @Override + public accept(visitor: FormulaParserVisitor): Result { + if (visitor.visitVariable) { + return visitor.visitVariable(this); + } else { + return visitor.visitChildren(this); + } + } +} + + diff --git a/packages/formula/src/grammar/FormulaParserVisitor.ts b/packages/formula/src/grammar/FormulaParserVisitor.ts new file mode 100644 index 000000000..31cb9c694 --- /dev/null +++ b/packages/formula/src/grammar/FormulaParserVisitor.ts @@ -0,0 +1,218 @@ +// Generated from src/grammar/FormulaParser.g4 by ANTLR 4.9.0-SNAPSHOT + + +import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor"; + +import { MulDivModExprContext } from "./FormulaParser"; +import { AddSubExprContext } from "./FormulaParser"; +import { PowerExprContext } from "./FormulaParser"; +import { ComparisonExprContext } from "./FormulaParser"; +import { AndExprContext } from "./FormulaParser"; +import { OrExprContext } from "./FormulaParser"; +import { NotExprContext } from "./FormulaParser"; +import { FunctionExprContext } from "./FormulaParser"; +import { VariableExprContext } from "./FormulaParser"; +import { NumberExprContext } from "./FormulaParser"; +import { StringExprContext } from "./FormulaParser"; +import { TrueExprContext } from "./FormulaParser"; +import { FalseExprContext } from "./FormulaParser"; +import { NullExprContext } from "./FormulaParser"; +import { DateExprContext } from "./FormulaParser"; +import { TimeExprContext } from "./FormulaParser"; +import { DateTimeExprContext } from "./FormulaParser"; +import { ParenExprContext } from "./FormulaParser"; +import { FormulaContext } from "./FormulaParser"; +import { ExpressionContext } from "./FormulaParser"; +import { FunctionCallContext } from "./FormulaParser"; +import { ArgumentListContext } from "./FormulaParser"; +import { VariableContext } from "./FormulaParser"; + + +/** + * This interface defines a complete generic visitor for a parse tree produced + * by `FormulaParser`. + * + * @param The return type of the visit operation. Use `void` for + * operations with no return type. + */ +export interface FormulaParserVisitor extends ParseTreeVisitor { + /** + * Visit a parse tree produced by the `MulDivModExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitMulDivModExpr?: (ctx: MulDivModExprContext) => Result; + + /** + * Visit a parse tree produced by the `AddSubExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitAddSubExpr?: (ctx: AddSubExprContext) => Result; + + /** + * Visit a parse tree produced by the `PowerExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitPowerExpr?: (ctx: PowerExprContext) => Result; + + /** + * Visit a parse tree produced by the `ComparisonExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitComparisonExpr?: (ctx: ComparisonExprContext) => Result; + + /** + * Visit a parse tree produced by the `AndExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitAndExpr?: (ctx: AndExprContext) => Result; + + /** + * Visit a parse tree produced by the `OrExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitOrExpr?: (ctx: OrExprContext) => Result; + + /** + * Visit a parse tree produced by the `NotExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitNotExpr?: (ctx: NotExprContext) => Result; + + /** + * Visit a parse tree produced by the `FunctionExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFunctionExpr?: (ctx: FunctionExprContext) => Result; + + /** + * Visit a parse tree produced by the `VariableExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitVariableExpr?: (ctx: VariableExprContext) => Result; + + /** + * Visit a parse tree produced by the `NumberExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitNumberExpr?: (ctx: NumberExprContext) => Result; + + /** + * Visit a parse tree produced by the `StringExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitStringExpr?: (ctx: StringExprContext) => Result; + + /** + * Visit a parse tree produced by the `TrueExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitTrueExpr?: (ctx: TrueExprContext) => Result; + + /** + * Visit a parse tree produced by the `FalseExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFalseExpr?: (ctx: FalseExprContext) => Result; + + /** + * Visit a parse tree produced by the `NullExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitNullExpr?: (ctx: NullExprContext) => Result; + + /** + * Visit a parse tree produced by the `DateExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitDateExpr?: (ctx: DateExprContext) => Result; + + /** + * Visit a parse tree produced by the `TimeExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitTimeExpr?: (ctx: TimeExprContext) => Result; + + /** + * Visit a parse tree produced by the `DateTimeExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitDateTimeExpr?: (ctx: DateTimeExprContext) => Result; + + /** + * Visit a parse tree produced by the `ParenExpr` + * labeled alternative in `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitParenExpr?: (ctx: ParenExprContext) => Result; + + /** + * Visit a parse tree produced by `FormulaParser.formula`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFormula?: (ctx: FormulaContext) => Result; + + /** + * Visit a parse tree produced by `FormulaParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitExpression?: (ctx: ExpressionContext) => Result; + + /** + * Visit a parse tree produced by `FormulaParser.functionCall`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFunctionCall?: (ctx: FunctionCallContext) => Result; + + /** + * Visit a parse tree produced by `FormulaParser.argumentList`. + * @param ctx the parse tree + * @return the visitor result + */ + visitArgumentList?: (ctx: ArgumentListContext) => Result; + + /** + * Visit a parse tree produced by `FormulaParser.variable`. + * @param ctx the parse tree + * @return the visitor result + */ + visitVariable?: (ctx: VariableContext) => Result; +} + diff --git a/packages/formula/src/index.ts b/packages/formula/src/index.ts new file mode 100644 index 000000000..4befc2ca9 --- /dev/null +++ b/packages/formula/src/index.ts @@ -0,0 +1,10 @@ +export { AbstractParseTreeVisitor } from "antlr4ts/tree/AbstractParseTreeVisitor" +export { type ParseTree } from "antlr4ts/tree/ParseTree" +export * from "./formula.constants" +export * from "./formula.visitor" +export * from "./formula/formula.type" +export * from "./grammar/FormulaLexer" +export * from "./grammar/FormulaParser" +export * from "./grammar/FormulaParserVisitor" +export * from "./types" +export * from "./util" diff --git a/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap b/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap new file mode 100644 index 000000000..fd8f68a69 --- /dev/null +++ b/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap @@ -0,0 +1,1392 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`parse formula test ADD(1, ADD(2, {{ field1 }})) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "arguments": [ + { + "type": "number", + "value": 2, + }, + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "ADD", + "returnType": "number", + "type": "functionCall", + "value": "ADD(2,{{field1}})", + }, + ], + "name": "ADD", + "returnType": "number", + "type": "functionCall", + "value": "ADD(1,ADD(2,{{field1}}))", +} +`; + +exports[`parse formula test ADD(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "ADD", + "returnType": "number", + "type": "functionCall", + "value": "ADD(1,2)", +} +`; + +exports[`parse formula test SUBTRACT(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "SUBTRACT", + "returnType": "number", + "type": "functionCall", + "value": "SUBTRACT(1,2)", +} +`; + +exports[`parse formula test MULTIPLY(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "MULTIPLY", + "returnType": "number", + "type": "functionCall", + "value": "MULTIPLY(1,2)", +} +`; + +exports[`parse formula test DIVIDE(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "DIVIDE", + "returnType": "number", + "type": "functionCall", + "value": "DIVIDE(1,2)", +} +`; + +exports[`parse formula test 1 - 1 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 1, + }, + ], + "name": "-", + "returnType": "number", + "type": "functionCall", + "value": "1-1", +} +`; + +exports[`parse formula test 1 * 1 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 1, + }, + ], + "name": "*", + "returnType": "number", + "type": "functionCall", + "value": "1*1", +} +`; + +exports[`parse formula test 1 / 1 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 1, + }, + ], + "name": "/", + "returnType": "number", + "type": "functionCall", + "value": "1/1", +} +`; + +exports[`parse formula test SUBTRACT(1, 2) + MULTIPLY(3, 4) 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "SUBTRACT", + "returnType": "number", + "type": "functionCall", + "value": "SUBTRACT(1,2)", + }, + { + "arguments": [ + { + "type": "number", + "value": 3, + }, + { + "type": "number", + "value": 4, + }, + ], + "name": "MULTIPLY", + "returnType": "number", + "type": "functionCall", + "value": "MULTIPLY(3,4)", + }, + ], + "name": "+", + "returnType": "number", + "type": "functionCall", + "value": "SUBTRACT(1,2)+MULTIPLY(3,4)", +} +`; + +exports[`parse formula test 1 1`] = ` +{ + "type": "number", + "value": 1, +} +`; + +exports[`parse formula test {{field1}} 1`] = ` +{ + "type": "variable", + "value": "{{field1}}", + "variable": "field1", +} +`; + +exports[`parse formula test 1 + 1 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 1, + }, + ], + "name": "+", + "returnType": "number", + "type": "functionCall", + "value": "1+1", +} +`; + +exports[`parse formula test {{field1}} + {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "+", + "returnType": "number", + "type": "functionCall", + "value": "{{field1}}+{{field2}}", +} +`; + +exports[`parse formula test SUM({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "SUM", + "returnType": "number", + "type": "functionCall", + "value": "SUM({{field1}},{{field2}})", +} +`; + +exports[`parse formula test CONCAT({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "CONCAT", + "returnType": "string", + "type": "functionCall", + "value": "CONCAT({{field1}},{{field2}})", +} +`; + +exports[`parse formula test CONCAT({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "CONCAT", + "returnType": "string", + "type": "functionCall", + "value": "CONCAT({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test MOD(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "MOD", + "returnType": "number", + "type": "functionCall", + "value": "MOD(1,2)", +} +`; + +exports[`parse formula test MOD({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "MOD", + "returnType": "number", + "type": "functionCall", + "value": "MOD({{field1}},{{field2}})", +} +`; + +exports[`parse formula test POWER(2, 3) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 2, + }, + { + "type": "number", + "value": 3, + }, + ], + "name": "POWER", + "returnType": "number", + "type": "functionCall", + "value": "POWER(2,3)", +} +`; + +exports[`parse formula test POWER({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "POWER", + "returnType": "number", + "type": "functionCall", + "value": "POWER({{field1}},{{field2}})", +} +`; + +exports[`parse formula test SQRT(4) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 4, + }, + ], + "name": "SQRT", + "returnType": "number", + "type": "functionCall", + "value": "SQRT(4)", +} +`; + +exports[`parse formula test SQRT({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "SQRT", + "returnType": "number", + "type": "functionCall", + "value": "SQRT({{field1}})", +} +`; + +exports[`parse formula test ABS(-5) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 5, + }, + ], + "name": "ABS", + "returnType": "number", + "type": "functionCall", + "value": "ABS(-5)", +} +`; + +exports[`parse formula test ABS({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "ABS", + "returnType": "number", + "type": "functionCall", + "value": "ABS({{field1}})", +} +`; + +exports[`parse formula test ROUND(1.234) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1.234, + }, + ], + "name": "ROUND", + "returnType": "number", + "type": "functionCall", + "value": "ROUND(1.234)", +} +`; + +exports[`parse formula test ROUND({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "ROUND", + "returnType": "number", + "type": "functionCall", + "value": "ROUND({{field1}})", +} +`; + +exports[`parse formula test FLOOR(1.234) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1.234, + }, + ], + "name": "FLOOR", + "returnType": "number", + "type": "functionCall", + "value": "FLOOR(1.234)", +} +`; + +exports[`parse formula test FLOOR({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "FLOOR", + "returnType": "number", + "type": "functionCall", + "value": "FLOOR({{field1}})", +} +`; + +exports[`parse formula test CEILING(1.234) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1.234, + }, + ], + "name": "CEILING", + "returnType": "number", + "type": "functionCall", + "value": "CEILING(1.234)", +} +`; + +exports[`parse formula test CEILING({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "CEILING", + "returnType": "number", + "type": "functionCall", + "value": "CEILING({{field1}})", +} +`; + +exports[`parse formula test MIN(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "MIN", + "returnType": "number", + "type": "functionCall", + "value": "MIN(1,2)", +} +`; + +exports[`parse formula test MIN({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "MIN", + "returnType": "number", + "type": "functionCall", + "value": "MIN({{field1}},{{field2}})", +} +`; + +exports[`parse formula test MIN({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "MIN", + "returnType": "number", + "type": "functionCall", + "value": "MIN({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test MAX(1, 2) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "MAX", + "returnType": "number", + "type": "functionCall", + "value": "MAX(1,2)", +} +`; + +exports[`parse formula test MAX({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "MAX", + "returnType": "number", + "type": "functionCall", + "value": "MAX({{field1}},{{field2}})", +} +`; + +exports[`parse formula test MAX({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "MAX", + "returnType": "number", + "type": "functionCall", + "value": "MAX({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test AVERAGE(1, 2, 3) 1`] = ` +{ + "arguments": [ + { + "type": "number", + "value": 1, + }, + { + "type": "number", + "value": 2, + }, + { + "type": "number", + "value": 3, + }, + ], + "name": "AVERAGE", + "returnType": "number", + "type": "functionCall", + "value": "AVERAGE(1,2,3)", +} +`; + +exports[`parse formula test AVERAGE({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "AVERAGE", + "returnType": "number", + "type": "functionCall", + "value": "AVERAGE({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test CONCAT({{field1}}, {{field2}}) 2`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "CONCAT", + "returnType": "string", + "type": "functionCall", + "value": "CONCAT({{field1}},{{field2}})", +} +`; + +exports[`parse formula test CONCAT({{field1}}, {{field2}}, {{field3}}) 2`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "CONCAT", + "returnType": "string", + "type": "functionCall", + "value": "CONCAT({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test LEFT({{field1}}, 3) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 3, + }, + ], + "name": "LEFT", + "returnType": "string", + "type": "functionCall", + "value": "LEFT({{field1}},3)", +} +`; + +exports[`parse formula test RIGHT({{field1}}, 3) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 3, + }, + ], + "name": "RIGHT", + "returnType": "string", + "type": "functionCall", + "value": "RIGHT({{field1}},3)", +} +`; + +exports[`parse formula test MID({{field1}}, 2, 3) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 2, + }, + { + "type": "number", + "value": 3, + }, + ], + "name": "MID", + "returnType": "string", + "type": "functionCall", + "value": "MID({{field1}},2,3)", +} +`; + +exports[`parse formula test NOT ({{field1}} > {{field2}}) 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>{{field2}}", + }, + ], + "name": "NOT", + "returnType": "boolean", + "type": "functionCall", + "value": "NOT({{field1}}>{{field2}})", +} +`; + +exports[`parse formula test ({{field1}} > {{field2}}) AND ({{field2}} > {{field3}}) 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>{{field2}}", + }, + { + "arguments": [ + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field2}}>{{field3}}", + }, + ], + "name": "AND", + "returnType": "boolean", + "type": "functionCall", + "value": "({{field1}}>{{field2}})AND({{field2}}>{{field3}})", +} +`; + +exports[`parse formula test ({{field1}} > {{field2}}) OR ({{field2}} > {{field3}}) 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>{{field2}}", + }, + { + "arguments": [ + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field2}}>{{field3}}", + }, + ], + "name": "OR", + "returnType": "boolean", + "type": "functionCall", + "value": "({{field1}}>{{field2}})OR({{field2}}>{{field3}})", +} +`; + +exports[`parse formula test {{field1}} = {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "=", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}={{field2}}", +} +`; + +exports[`parse formula test {{field1}} != {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "!=", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}!={{field2}}", +} +`; + +exports[`parse formula test {{field1}} >= {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": ">=", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>={{field2}}", +} +`; + +exports[`parse formula test {{field1}} <= {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "<=", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}<={{field2}}", +} +`; + +exports[`parse formula test {{field1}} < {{field2}} 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "<", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}<{{field2}}", +} +`; + +exports[`parse formula test {{field1}} > 1 AND {{field2}} < 2 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 1, + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1", + }, + { + "arguments": [ + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "<", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field2}}<2", + }, + ], + "name": "AND", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1AND{{field2}}<2", +} +`; + +exports[`parse formula test {{field1}} > 1 OR {{field2}} < 2 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 1, + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1", + }, + { + "arguments": [ + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "<", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field2}}<2", + }, + ], + "name": "OR", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1OR{{field2}}<2", +} +`; + +exports[`parse formula test NOT ({{field1}} > 1 AND {{field2}} < 2) 1`] = ` +{ + "arguments": [ + { + "arguments": [ + { + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "number", + "value": 1, + }, + ], + "name": ">", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1", + }, + { + "arguments": [ + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "number", + "value": 2, + }, + ], + "name": "<", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field2}}<2", + }, + ], + "name": "AND", + "returnType": "boolean", + "type": "functionCall", + "value": "{{field1}}>1AND{{field2}}<2", + }, + ], + "name": "NOT", + "returnType": "boolean", + "type": "functionCall", + "value": "NOT({{field1}}>1AND{{field2}}<2)", +} +`; + +exports[`parse formula test SEARCH({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "SEARCH", + "returnType": "number", + "type": "functionCall", + "value": "SEARCH({{field1}},{{field2}})", +} +`; + +exports[`parse formula test REPLACE({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "REPLACE", + "returnType": "string", + "type": "functionCall", + "value": "REPLACE({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test REPEAT({{field1}}, {{field2}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + ], + "name": "REPEAT", + "returnType": "string", + "type": "functionCall", + "value": "REPEAT({{field1}},{{field2}})", +} +`; + +exports[`parse formula test LEN({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "LEN", + "returnType": "number", + "type": "functionCall", + "value": "LEN({{field1}})", +} +`; + +exports[`parse formula test SUBSTR({{field1}}, {{field2}}, {{field3}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "variable", + "value": "{{field2}}", + "variable": "field2", + }, + { + "type": "variable", + "value": "{{field3}}", + "variable": "field3", + }, + ], + "name": "SUBSTR", + "returnType": "string", + "type": "functionCall", + "value": "SUBSTR({{field1}},{{field2}},{{field3}})", +} +`; + +exports[`parse formula test AND({{field1}}, {{field2}}) 1`] = ` +{ + "type": "variable", + "value": "{{field1}}", + "variable": "field1", +} +`; + +exports[`parse formula test OR({{field1}}, {{field2}}) 1`] = ` +{ + "type": "variable", + "value": "{{field1}}", + "variable": "field1", +} +`; + +exports[`parse formula test NOT({{field1}}) 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + ], + "name": "NOT", + "returnType": "boolean", + "type": "functionCall", + "value": "NOT({{field1}})", +} +`; + +exports[`parse formula test JSON_EXTRACT({{field1}}, '$.name') 1`] = ` +{ + "arguments": [ + { + "type": "variable", + "value": "{{field1}}", + "variable": "field1", + }, + { + "type": "string", + "value": "$.name", + }, + ], + "name": "JSON_EXTRACT", + "returnType": "any", + "type": "functionCall", + "value": "JSON_EXTRACT({{field1}},'$.name')", +} +`; diff --git a/packages/formula/src/tests/parse-formula.test.ts b/packages/formula/src/tests/parse-formula.test.ts new file mode 100644 index 000000000..2eadc9064 --- /dev/null +++ b/packages/formula/src/tests/parse-formula.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from "bun:test" +import { parseFormula } from "../util" + +describe("parse formula", () => { + test.each([ + // + "ADD(1, ADD(2, {{ field1 }}))", + "ADD(1, 2)", + "SUBTRACT(1, 2)", + "MULTIPLY(1, 2)", + "DIVIDE(1, 2)", + "1 - 1", + "1 * 1", + "1 / 1", + "SUBTRACT(1, 2) + MULTIPLY(3, 4)", + "1", + "{{field1}}", + "1 + 1", + "{{field1}} + {{field2}}", + "SUM({{field1}}, {{field2}})", + "CONCAT({{field1}}, {{field2}})", + "CONCAT({{field1}}, {{field2}}, {{field3}})", + "MOD(1, 2)", + "MOD({{field1}}, {{field2}})", + "POWER(2, 3)", + "POWER({{field1}}, {{field2}})", + "SQRT(4)", + "SQRT({{field1}})", + "ABS(-5)", + "ABS({{field1}})", + "ROUND(1.234)", + "ROUND({{field1}})", + "FLOOR(1.234)", + "FLOOR({{field1}})", + "CEILING(1.234)", + "CEILING({{field1}})", + "MIN(1, 2)", + "MIN({{field1}}, {{field2}})", + "MIN({{field1}}, {{field2}}, {{field3}})", + "MAX(1, 2)", + "MAX({{field1}}, {{field2}})", + "MAX({{field1}}, {{field2}}, {{field3}})", + "AVERAGE(1, 2, 3)", + "AVERAGE({{field1}}, {{field2}}, {{field3}})", + "CONCAT({{field1}}, {{field2}})", + "CONCAT({{field1}}, {{field2}}, {{field3}})", + "LEFT({{field1}}, 3)", + "RIGHT({{field1}}, 3)", + "MID({{field1}}, 2, 3)", + "NOT ({{field1}} > {{field2}})", + "({{field1}} > {{field2}}) AND ({{field2}} > {{field3}})", + "({{field1}} > {{field2}}) OR ({{field2}} > {{field3}})", + "{{field1}} = {{field2}}", + "{{field1}} != {{field2}}", + "{{field1}} >= {{field2}}", + "{{field1}} <= {{field2}}", + "{{field1}} < {{field2}}", + "{{field1}} > 1 AND {{field2}} < 2", + "{{field1}} > 1 OR {{field2}} < 2", + "NOT ({{field1}} > 1 AND {{field2}} < 2)", + "SEARCH({{field1}}, {{field2}})", + "REPLACE({{field1}}, {{field2}}, {{field3}})", + "REPEAT({{field1}}, {{field2}})", + "LEN({{field1}})", + "SUBSTR({{field1}}, {{field2}}, {{field3}})", + "AND({{field1}}, {{field2}})", + "OR({{field1}}, {{field2}})", + "NOT({{field1}})", + "JSON_EXTRACT({{field1}}, '$.name')", + ])("test %s", (input) => { + const result = parseFormula(input) + + expect(result).toMatchSnapshot() + }) +}) diff --git a/packages/formula/src/types.ts b/packages/formula/src/types.ts new file mode 100644 index 000000000..2df265615 --- /dev/null +++ b/packages/formula/src/types.ts @@ -0,0 +1,53 @@ +import { z } from "@undb/zod" + +export const paramType = z.enum(["number", "string", "boolean", "date", "any", "variadic"]) + +export type ParamType = z.infer + +export const returnType = z.enum(["number", "string", "boolean", "date", "any"]) + +export type ReturnType = z.infer + +export type FunctionDefinition = string + +export type FunctionExpressionResult = { + type: "functionCall" + name: string + arguments: ExpressionResult[] + returnType: ReturnType + value: FunctionDefinition +} + +export type ArgumentListResult = { + type: "argumentList" + arguments: ExpressionResult[] +} + +export type VariableResult = { + type: "variable" + value: string + variable: string +} + +export type NumberResult = { + type: "number" + value: number +} + +export type StringResult = { + type: "string" + value: string +} + +export type BooleanResult = { + type: "boolean" + value: boolean +} + +export type ExpressionResult = + | FunctionExpressionResult + | ArgumentListResult + | VariableResult + | NumberResult + | StringResult + | BooleanResult diff --git a/packages/formula/src/util.ts b/packages/formula/src/util.ts new file mode 100644 index 000000000..6d1c5326a --- /dev/null +++ b/packages/formula/src/util.ts @@ -0,0 +1,22 @@ +import { CharStreams, CommonTokenStream } from "antlr4ts" +import { FormulaVisitor } from "./formula.visitor" +import { FormulaLexer } from "./grammar/FormulaLexer" +import { FormulaParser } from "./grammar/FormulaParser" + +export function createParser(input: string) { + const inputStream = CharStreams.fromString(input) + const lexer = new FormulaLexer(inputStream) + const tokenStream = new CommonTokenStream(lexer) + return new FormulaParser(tokenStream) +} + +export function parseFormula(input: string) { + const parser = createParser(input) + + const tree = parser.formula() + + const visitor = new FormulaVisitor() + const parsedFormula = visitor.visit(tree) + + return parsedFormula +} diff --git a/packages/formula/tsconfig.json b/packages/formula/tsconfig.json new file mode 100644 index 000000000..beaa6792e --- /dev/null +++ b/packages/formula/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index a9d4c739d..477eba4b0 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -150,6 +150,7 @@ export class Graphql { display: Boolean constraint: JSON option: JSON + metadata: JSON } enum ViewType { diff --git a/packages/persistence/package.json b/packages/persistence/package.json index e1ee7d1db..e099fc7b3 100644 --- a/packages/persistence/package.json +++ b/packages/persistence/package.json @@ -20,6 +20,7 @@ "@undb/context": "workspace:*", "@undb/di": "workspace:*", "@undb/domain": "workspace:*", + "@undb/formula": "workspace:*", "@undb/logger": "workspace:*", "@undb/openapi": "workspace:*", "@undb/table": "workspace:*", diff --git a/packages/persistence/src/record/record-query-spec-creator-visitor.ts b/packages/persistence/src/record/record-query-spec-creator-visitor.ts index 40014f7b1..5e56f1923 100644 --- a/packages/persistence/src/record/record-query-spec-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-spec-creator-visitor.ts @@ -5,6 +5,11 @@ import { CurrencyLTE, DateIsEmpty, DurationEqual, + FormulaEqual, + FormulaGT, + FormulaGTE, + FormulaLT, + FormulaLTE, ID_TYPE, JsonContains, LongTextEqual, @@ -128,6 +133,12 @@ export class RecordQuerySpecCreatorVisitor implements IRecordVisitor { jsonEmpty(spec: JsonEmpty): void {} checkboxEqual(spec: CheckboxEqual): void {} + formulaEqual(spec: FormulaEqual): void {} + formulaGT(spec: FormulaGT): void {} + formulaGTE(spec: FormulaGTE): void {} + formulaLT(spec: FormulaLT): void {} + formulaLTE(spec: FormulaLTE): void {} + and(left: RecordComositeSpecification, right: RecordComositeSpecification): this { const lv = this.clone() left.accept(lv) diff --git a/packages/persistence/src/record/record-spec-reference-visitor.ts b/packages/persistence/src/record/record-spec-reference-visitor.ts index de232bb3c..fae543793 100644 --- a/packages/persistence/src/record/record-spec-reference-visitor.ts +++ b/packages/persistence/src/record/record-spec-reference-visitor.ts @@ -16,6 +16,11 @@ import { DateIsToday, DateIsTomorrow, DurationEqual, + FormulaEqual, + FormulaGT, + FormulaGTE, + FormulaLT, + FormulaLTE, ID_TYPE, IdEqual, IdIn, @@ -113,6 +118,11 @@ export class RecordSpecReferenceVisitor implements IRecordVisitor { jsonContains(spec: JsonContains): void {} jsonEmpty(spec: JsonEmpty): void {} checkboxEqual(spec: CheckboxEqual): void {} + formulaEqual(spec: FormulaEqual): void {} + formulaGT(spec: FormulaGT): void {} + formulaGTE(spec: FormulaGTE): void {} + formulaLT(spec: FormulaLT): void {} + formulaLTE(spec: FormulaLTE): void {} and(left: ISpecification, right: ISpecification): this { left.accept(this) right.accept(this) diff --git a/packages/persistence/src/record/record.filter-visitor.test.ts b/packages/persistence/src/record/record.filter-visitor.test.ts deleted file mode 100644 index eefb49722..000000000 --- a/packages/persistence/src/record/record.filter-visitor.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Schema, ViewFilter, type IConditionGroup } from "@undb/table" -import Database from "bun:sqlite" -import { describe, expect, test } from "bun:test" -import { Kysely } from "kysely" -import { BunSqliteDialect } from "kysely-bun-sqlite" -import type { IQueryBuilder } from "../qb" -import { RecordFilterVisitor } from "./record.filter-visitor" - -const schema = Schema.fromJSON([ - { id: "field1", type: "string", name: "field1" }, - { id: "field2", type: "number", name: "field2" }, -]) - -const sqlite = new Database() -const qb = new Kysely({ - dialect: new BunSqliteDialect({ - database: sqlite, - }), -}) satisfies IQueryBuilder - -describe("record.filter-visitor", () => { - test.each([ - { - conjunction: "and", - children: [ - { fieldId: "field1", op: "eq", value: "value1" }, - { fieldId: "field2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "field1", op: "eq", value: "value1" }, - { - conjunction: "and", - children: [ - { fieldId: "field1", op: "eq", value: "value1" }, - { fieldId: "field2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "field1", op: "eq", value: "value2" }, - { fieldId: "field2", op: "lt", value: 2 }, - ], - }, - ], - }, - { - conjunction: "and", - children: [ - { fieldId: "field1", op: "starts_with", value: "value1" }, - { fieldId: "field1", op: "ends_with", value: "value2" }, - { fieldId: "field1", op: "contains", value: "value3" }, - { fieldId: "field2", op: "eq", value: 1 }, - { fieldId: "field2", op: "gt", value: 2 }, - { fieldId: "field2", op: "gte", value: 3 }, - { fieldId: "field2", op: "lt", value: 4 }, - { fieldId: "field2", op: "lte", value: 5 }, - ], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "is_empty" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "is_not_empty" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "starts_with", value: "hello" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "contains", value: "hello" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "does_not_contain", value: "hello" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field1", op: "ends_with", value: "hello" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field2", op: "is_empty" }], - }, - { - conjunction: "and", - children: [{ fieldId: "field2", op: "is_not_empty" }], - }, - ])("should get query", (filter) => { - const f = new ViewFilter(filter) - const spec = f.getSpec(schema) - - const query = qb - .selectFrom("table") - .selectAll() - .where((eb) => { - const visitor = new RecordFilterVisitor(eb) - if (spec.isSome()) { - spec.unwrap().accept(visitor) - } - - return visitor.cond - }) - .compile() - - expect(query.sql).toMatchSnapshot() - expect(query.parameters).toMatchSnapshot() - }) -}) diff --git a/packages/persistence/src/record/record.filter-visitor.ts b/packages/persistence/src/record/record.filter-visitor.ts index 76fcc618a..d0d561328 100644 --- a/packages/persistence/src/record/record.filter-visitor.ts +++ b/packages/persistence/src/record/record.filter-visitor.ts @@ -6,6 +6,11 @@ import { CurrencyLT, CurrencyLTE, DurationEqual, + FormulaEqual, + FormulaGT, + FormulaGTE, + FormulaLT, + FormulaLTE, PercentageEqual, SelectField, isUserFieldMacro, @@ -328,6 +333,26 @@ export class RecordFilterVisitor extends AbstractQBVisitor implements const cond = this.eb.eb(this.getFieldId(s), "=", s.value) this.addCond(cond) } + formulaEqual(spec: FormulaEqual): void { + const cond = this.eb.eb(this.getFieldId(spec), "=", spec.value) + this.addCond(cond) + } + formulaGT(spec: FormulaGT): void { + const cond = this.eb.eb(this.getFieldId(spec), ">", spec.value) + this.addCond(cond) + } + formulaGTE(spec: FormulaGTE): void { + const cond = this.eb.eb(this.getFieldId(spec), ">=", spec.value) + this.addCond(cond) + } + formulaLT(spec: FormulaLT): void { + const cond = this.eb.eb(this.getFieldId(spec), "<", spec.value) + this.addCond(cond) + } + formulaLTE(spec: FormulaLTE): void { + const cond = this.eb.eb(this.getFieldId(spec), "<=", spec.value) + this.addCond(cond) + } clone(): this { return new RecordFilterVisitor(this.eb, this.table, this.context) as this } diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts index acc7df2b0..2576aafd7 100644 --- a/packages/persistence/src/record/record.mutate-visitor.ts +++ b/packages/persistence/src/record/record.mutate-visitor.ts @@ -7,6 +7,11 @@ import { CurrencyLTE, DateIsEmpty, DurationEqual, + FormulaEqual, + FormulaGT, + FormulaGTE, + FormulaLT, + FormulaLTE, ID_TYPE, isUserFieldMacro, JsonContains, @@ -87,7 +92,13 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR } } jsonEqual(spec: JsonEqual): void { - this.setData(spec.fieldId.value, spec.json ? JSON.stringify(spec.json) : null) + if (!spec.json) { + this.setData(spec.fieldId.value, null) + } else if (typeof spec.json === "string") { + this.setData(spec.fieldId.value, spec.json) + } else { + this.setData(spec.fieldId.value, JSON.stringify(spec.json)) + } } jsonContains(spec: JsonContains): void { throw new Error("Method not implemented.") @@ -369,4 +380,19 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR clone(): this { return new RecordMutateVisitor(this.table, this.record, this.qb, this.eb, this.context) as this } + formulaEqual(s: FormulaEqual): void { + throw new Error("Method not implemented.") + } + formulaGT(s: FormulaGT): void { + throw new Error("Method not implemented.") + } + formulaGTE(s: FormulaGTE): void { + throw new Error("Method not implemented.") + } + formulaLT(s: FormulaLT): void { + throw new Error("Method not implemented.") + } + formulaLTE(s: FormulaLTE): void { + throw new Error("Method not implemented.") + } } diff --git a/packages/persistence/src/table/table.query-visitor.test.ts b/packages/persistence/src/table/table.query-visitor.test.ts deleted file mode 100644 index 56dfe4cef..000000000 --- a/packages/persistence/src/table/table.query-visitor.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { or } from "@undb/domain" -import { TableIdSpecification, TableIdVo, TableNameSpecification, TableNameVo } from "@undb/table" -import Database from "bun:sqlite" -import { beforeEach, describe, expect, test } from "bun:test" -import { drizzle } from "drizzle-orm/bun-sqlite" -import { tables } from "../tables" -import { TableFilterVisitor } from "./table.filter-visitor" - -export const sqlite = new Database(":memory:") -const db = drizzle(sqlite) - -describe("TableQueryVisitor", () => { - let visitor: TableFilterVisitor - - beforeEach(() => { - visitor = new TableFilterVisitor() - }) - - test.each([ - new TableIdSpecification(new TableIdVo("1")), - new TableNameSpecification(new TableNameVo("table")), - or(new TableIdSpecification(new TableIdVo("1")), new TableNameSpecification(new TableNameVo("table"))).unwrap(), - new TableIdSpecification(new TableIdVo("1")).not(), - or( - new TableIdSpecification(new TableIdVo("1")).not(), - new TableNameSpecification(new TableNameVo("table")), - ).unwrap(), - ])("should get correct query", (spec) => { - spec.accept(visitor) - - const sql = db.select().from(tables).where(visitor.cond).toSQL() - expect(sql).toMatchSnapshot() - }) -}) diff --git a/packages/persistence/src/underlying/underlying-formula.util.ts b/packages/persistence/src/underlying/underlying-formula.util.ts new file mode 100644 index 000000000..ec87fe8bb --- /dev/null +++ b/packages/persistence/src/underlying/underlying-formula.util.ts @@ -0,0 +1,12 @@ +import type { ReturnType } from "@undb/formula" +import type { ColumnDataType } from "kysely" +import { match } from "ts-pattern" + +export const getUnderlyingFormulaType = (returnType: ReturnType): ColumnDataType => { + return match(returnType) + .returnType() + .with("number", () => "real") + .with("boolean", () => "integer") + .with("date", () => "timestamp") + .otherwise(() => "text") +} diff --git a/packages/persistence/src/underlying/underlying-formula.visitor.test.ts b/packages/persistence/src/underlying/underlying-formula.visitor.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/persistence/src/underlying/underlying-formula.visitor.ts b/packages/persistence/src/underlying/underlying-formula.visitor.ts new file mode 100644 index 000000000..e88837483 --- /dev/null +++ b/packages/persistence/src/underlying/underlying-formula.visitor.ts @@ -0,0 +1,191 @@ +import { + AbstractParseTreeVisitor, + AddSubExprContext, + AndExprContext, + ArgumentListContext, + ComparisonExprContext, + FormulaContext, + FunctionCallContext, + FunctionExprContext, + MulDivModExprContext, + NotExprContext, + NumberExprContext, + OrExprContext, + ParenExprContext, + StringExprContext, + VariableContext, + VariableExprContext, + type FormulaFunction, + type FormulaParserVisitor, +} from "@undb/formula" +import { AUTO_INCREMENT_TYPE, FieldIdVo, ID_TYPE, type TableDo } from "@undb/table" +import { match } from "ts-pattern" + +export class UnderlyingFormulaVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor { + constructor(private readonly table: TableDo) { + super() + } + + protected defaultResult(): string { + return "" + } + + visitNumberExpr(ctx: NumberExprContext): string { + return ctx.NUMBER().text + } + + visitStringExpr(ctx: StringExprContext): string { + return ctx.STRING().text + } + + visitComparisonExpr(ctx: ComparisonExprContext): string { + return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1)) + } + + visitAndExpr(ctx: AndExprContext): string { + return this.visit(ctx.expression(0)) + " AND " + this.visit(ctx.expression(1)) + } + + visitOrExpr(ctx: OrExprContext): string { + return this.visit(ctx.expression(0)) + " OR " + this.visit(ctx.expression(1)) + } + + visitNotExpr(ctx: NotExprContext): string { + return "NOT " + this.visit(ctx.expression()) + } + + visitAddSubExpr(ctx: AddSubExprContext): string { + return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1)) + } + + visitMulDivModExpr(ctx: MulDivModExprContext): string { + return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1)) + } + + visitVariable(ctx: VariableContext): string { + const fieldId = ctx.IDENTIFIER().text + const field = this.table.schema + .getFieldById(new FieldIdVo(fieldId)) + .expect(`variable ${fieldId} not found in table ${this.table.name.value}`) + if (field.type === "currency") { + return `(${fieldId}/100)` + } else if (field.type === "autoIncrement") { + return `[${fieldId}]` + } + return fieldId + } + + visitFormula(ctx: FormulaContext): string { + const expr = ctx.expression() + return this.visit(expr) + } + + visitFunctionExpr(ctx: FunctionExprContext): string { + return this.visit(ctx.functionCall()) + } + + visitVariableExpr(ctx: VariableExprContext): string { + return this.visit(ctx.variable()) + } + + visitParenExpr(ctx: ParenExprContext): string { + return this.visit(ctx.expression()) + } + + private arguments(ctx: FunctionCallContext): string[] { + return ctx + .argumentList()! + .expression() + .map((expr) => this.visit(expr)) + } + visitFunctionCall(ctx: FunctionCallContext): string { + const functionName = ctx.IDENTIFIER().text as FormulaFunction + return match(functionName) + .with("ADD", "SUM", () => { + const fn = this.arguments(ctx).join(" + ") + return `(${fn})` + }) + .with("SUBTRACT", () => { + const fn = this.arguments(ctx).join(" - ") + return `(${fn})` + }) + .with("MULTIPLY", () => { + const fn = this.arguments(ctx).join(" * ") + return `(${fn})` + }) + .with("DIVIDE", () => { + const fn = this.arguments(ctx).join(" / ") + return `(${fn})` + }) + .with("CONCAT", () => { + const fn = this.arguments(ctx) + .map((arg) => `COALESCE(${arg}, '')`) + .join(" || ") + return `(${fn})` + }) + .with("AVERAGE", () => { + const args = this.arguments(ctx) + return `( + (${args.map((arg) => `COALESCE(${arg}, 0)`).join(" + ")}) + / + (NULLIF( + ${args.map((arg) => `(CASE WHEN ${arg} IS NULL THEN 0 ELSE 1 END)`).join(" + ")} + , 0) + ))` + }) + .with("LEFT", () => { + const args = this.arguments(ctx) + return `SUBSTR(${args[0]}, 1, ${args[1]})` + }) + .with("RIGHT", () => { + const args = this.arguments(ctx) + return `SUBSTR(${args[0]}, -${args[1]}, ${args[1]})` + }) + .with("MID", () => { + const args = this.arguments(ctx) + return `SUBSTR(${args[0]}, ${args[1]}, ${args[2]})` + }) + .with("AND", () => { + const args = this.arguments(ctx) + return `(${args.map((arg) => `COALESCE(${arg}, FALSE)`).join(" AND ")})` + }) + .with("OR", () => { + const args = this.arguments(ctx) + return `(${args.map((arg) => `COALESCE(${arg}, FALSE)`).join(" OR ")})` + }) + .with("NOT", () => { + const args = this.arguments(ctx) + return `NOT ${args[0]}` + }) + .with("SEARCH", () => { + const args = this.arguments(ctx) + return `COALESCE(INSTR(LOWER(COALESCE(${args[1]}, '')), LOWER(COALESCE(${args[0]}, ''))), 0)` + }) + .with("LEN", () => { + const args = this.arguments(ctx) + return `LENGTH(${args[0]})` + }) + .with("REPEAT", () => { + const args = this.arguments(ctx) + // args[0] 是要重复的字符串,args[1] 是重复次数 + return `SUBSTR(REPLACE(HEX(ZEROBLOB(${args[1]})), '00', ${args[0]}), 1, LENGTH(${args[0]}) * ${args[1]})` + }) + .with("RECORD_ID", () => { + return ID_TYPE + }) + .with("AUTO_INCREMENT", () => { + return `[${AUTO_INCREMENT_TYPE}]` + }) + .otherwise(() => { + const args = ctx.argumentList() ? this.visit(ctx.argumentList()!) : "" + return `${functionName}(${args})` + }) + } + + visitArgumentList(ctx: ArgumentListContext): string { + return ctx + .expression() + .map((expr) => this.visit(expr)) + .join(", ") + } +} diff --git a/packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts similarity index 80% rename from packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts rename to packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts index 6e0df2ef7..4c879b423 100644 --- a/packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts @@ -1,3 +1,4 @@ +import { createParser } from "@undb/formula" import { Options, type AttachmentField, @@ -28,9 +29,11 @@ import { type UserField, } from "@undb/table" import type { FormulaField } from "@undb/table/src/modules/schema/fields/variants/formula-field" -import { sql } from "kysely" +import { AlterTableBuilder, sql } from "kysely" import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" import type { IRecordQueryBuilder } from "../qb" +import { getUnderlyingFormulaType } from "./underlying-formula.util" +import { UnderlyingFormulaVisitor } from "./underlying-formula.visitor" import type { UnderlyingTable } from "./underlying-table" export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisitor implements IFieldVisitor { @@ -38,6 +41,7 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito private readonly qb: IRecordQueryBuilder, private readonly table: UnderlyingTable, private readonly prev: Field, + private readonly tb: AlterTableBuilder, ) { super() } @@ -51,7 +55,17 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito string(field: StringField): void {} number(field: NumberField): void {} rating(field: RatingField): void {} - formula(field: FormulaField): void {} + formula(field: FormulaField): void { + const visitor = new UnderlyingFormulaVisitor(this.table.table) + const parser = createParser(field.fn) + const parsed = visitor.visit(parser.formula()) + + const drop = this.tb.dropColumn(field.id.value).compile() + this.addSql(drop) + const type = getUnderlyingFormulaType(field.returnType) + const add = this.tb.addColumn(field.id.value, type, (b) => b.generatedAlwaysAs(sql.raw(parsed))).compile() + this.addSql(add) + } select(field: SelectField): void { const prev = this.prev as SelectField const deletedOptions = Options.getDeletedOptions(prev.options, field.options) diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts index 4d5a7f22e..585a3c6d4 100644 --- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts @@ -1,3 +1,5 @@ +import { createParser } from "@undb/formula" +import { createLogger } from "@undb/logger" import { AttachmentField, ButtonField, @@ -32,6 +34,8 @@ import { AlterTableBuilder, AlterTableColumnAlteringBuilder, CompiledQuery, Crea import type { IQueryBuilder } from "../qb" import { users } from "../tables" import { JoinTable } from "./reference/join-table" +import { getUnderlyingFormulaType } from "./underlying-formula.util" +import { UnderlyingFormulaVisitor } from "./underlying-formula.visitor" import type { UnderlyingTable } from "./underlying-table" export class UnderlyingTableFieldVisitor | AlterTableBuilder> @@ -41,9 +45,12 @@ export class UnderlyingTableFieldVisitor private readonly qb: IQueryBuilder, private readonly t: UnderlyingTable, public tb: TB, + public readonly isNew: boolean = false, ) {} public atb: AlterTableColumnAlteringBuilder | CreateTableBuilder | null = null + private logger = createLogger(UnderlyingFormulaVisitor.name) + private addColumn(c: AlterTableColumnAlteringBuilder | CreateTableBuilder) { this.atb = c this.tb = c as TB @@ -182,11 +189,17 @@ export class UnderlyingTableFieldVisitor } } formula(field: FormulaField): void { - const parse = (fn: string): string => { - return fn.replaceAll("{{", "").replaceAll("}}", "") - } - const exp = parse(field.fn) - const c = this.tb.addColumn(field.id.value, "text", (b) => b.generatedAlwaysAs(sql.raw(exp)).stored()) + const visitor = new UnderlyingFormulaVisitor(this.t.table) + const parser = createParser(field.fn) + const parsed = visitor.visit(parser.formula()) + + this.logger.debug("parsed formula", { parsed }) + + const type = getUnderlyingFormulaType(field.returnType) + const c = this.tb.addColumn(field.id.value, type, (b) => { + const column = b.generatedAlwaysAs(sql.raw(parsed)) + return this.isNew ? column.stored() : column + }) this.addColumn(c) } } diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts index 6a677af1b..db8a51942 100644 --- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts @@ -51,8 +51,8 @@ import type { IRecordQueryBuilder } from "../qb" import { ConversionContext } from "./conversion/conversion.context" import { ConversionFactory } from "./conversion/conversion.factory" import { JoinTable } from "./reference/join-table" -import { UnderlyingTableFieldUpdatedVisitor } from "./undelying-table-field-updated.visitor" import { UnderlyingTable } from "./underlying-table" +import { UnderlyingTableFieldUpdatedVisitor } from "./underlying-table-field-updated.visitor" import { UnderlyingTableFieldVisitor } from "./underlying-table-field.visitor" export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { @@ -131,7 +131,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { } } - const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous) + const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous, this.tb) spec.field.accept(fieldVisitor) this.addSql(...fieldVisitor.sql) } @@ -210,7 +210,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { withName(name: TableNameSpecification): void {} withSchema(schema: TableSchemaSpecification): void {} withNewField(schema: WithNewFieldSpecification): void { - const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb) + const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb, false) schema.field.accept(fieldVisitor) this.addSql(...fieldVisitor.sql) this.atb = fieldVisitor.atb diff --git a/packages/persistence/src/underlying/underlying-table.service.ts b/packages/persistence/src/underlying/underlying-table.service.ts index d50479b4c..6cc765cae 100644 --- a/packages/persistence/src/underlying/underlying-table.service.ts +++ b/packages/persistence/src/underlying/underlying-table.service.ts @@ -22,7 +22,7 @@ export class UnderlyingTableService { await trx.schema .createTable(t.name) .$call((tb) => { - const visitor = new UnderlyingTableFieldVisitor(trx, t, tb) + const visitor = new UnderlyingTableFieldVisitor(trx, t, tb, true) for (const field of table.schema) { field.accept(visitor) } diff --git a/packages/table/package.json b/packages/table/package.json index e3ab373e3..dec151656 100644 --- a/packages/table/package.json +++ b/packages/table/package.json @@ -15,6 +15,7 @@ "@undb/context": "workspace:*", "@undb/di": "workspace:*", "@undb/domain": "workspace:*", + "@undb/formula": "workspace:*", "@undb/logger": "workspace:*", "@undb/space": "workspace:*", "@undb/utils": "workspace:*", diff --git a/packages/table/src/methods/create-field.method.ts b/packages/table/src/methods/create-field.method.ts index 72397d6fa..7803f2f26 100644 --- a/packages/table/src/methods/create-field.method.ts +++ b/packages/table/src/methods/create-field.method.ts @@ -36,7 +36,7 @@ export function $createFieldSpec(this: TableDo, field: Field): Option] { - const field = FieldFactory.create(dto) + const field = FieldFactory.create(this, dto) return [field, this.$createFieldSpec(field)] } diff --git a/packages/table/src/methods/update-field.method.ts b/packages/table/src/methods/update-field.method.ts index a9d73ecc9..c4457ed57 100644 --- a/packages/table/src/methods/update-field.method.ts +++ b/packages/table/src/methods/update-field.method.ts @@ -7,7 +7,7 @@ import type { TableComositeSpecification } from "../specifications" import type { TableDo } from "../table.do" export function updateFieldMethod(this: TableDo, dto: IUpdateFieldDTO): Option { - const spec = this.schema.$updateField(dto) + const spec = this.schema.$updateField(this, dto) // TODO: update form spec.mutate(this) diff --git a/packages/table/src/modules/schema/fields/condition/condition.util.test.ts b/packages/table/src/modules/schema/fields/condition/condition.util.test.ts deleted file mode 100644 index 2ea5c8a4c..000000000 --- a/packages/table/src/modules/schema/fields/condition/condition.util.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { ZodUndefined } from "@undb/zod" -import { describe, expect, test } from "bun:test" -import { Schema } from "../.." -import type { IConditionGroup, MaybeConditionGroup } from "./condition.type" -import { conditionWithoutFields, getSpec, parseValidCondition } from "./condition.util" - -const schema = Schema.fromJSON([ - { id: "fld_1", type: "string", name: "fld_1" }, - { id: "fld_2", type: "number", name: "fld_2" }, -]) - -describe("condition.util", () => { - test.each([ - { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value2" }, - { fieldId: "fld_2", op: "lt", value: 2 }, - ], - }, - ], - }, - ])("should get correct spec", (condition) => { - const spec = getSpec(schema, condition) - expect(spec).toMatchSnapshot() - }) - - describe("parseValidCondition", () => { - test.each([ - { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value2" }, - { fieldId: "fld_2", op: "lt", value: 2 }, - ], - }, - ], - }, - ])("should parse valid condition", (condition) => { - const parsed = parseValidCondition(schema.fieldMapById, condition) - expect(parsed).toEqual(condition) - }) - - test.each<[MaybeConditionGroup, IConditionGroup]>([ - [ - { - id: "1", - conjunction: "and", - children: [ - { id: "2", fieldId: "fld_1", op: "eq", value: "value1" }, - { id: "3", fieldId: "fld_2", op: "gt", value: "1" }, - ], - }, - { - id: "5", - conjunction: "and", - children: [{ id: "6", fieldId: "fld_1", op: "eq", value: "value1" }], - }, - ], - [ - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: "1" }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", value: "value2" }, - { fieldId: "fld_2", op: "lt", value: 2 }, - ], - }, - ], - }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { - conjunction: "and", - children: [{ fieldId: "fld_1", op: "eq", value: "value1" }], - }, - { - conjunction: "or", - children: [{ fieldId: "fld_2", op: "lt", value: 2 }], - }, - ], - }, - ], - ])("should ignore invalid condition", (condition, value) => { - const parsed = parseValidCondition(schema.fieldMapById, condition) - expect(parsed).toEqual(value) - }) - }) -}) - -describe("conditionWithoutFields", () => { - test("should remove field conditions with specified fieldIds", () => { - const fieldIds = new Set(["fld_1", "fld_2"]) - const condition: IConditionGroup = { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - { - conjunction: "or", - children: [ - { fieldId: "fld_1", op: "eq", value: "value2" }, - { fieldId: "fld_3", op: "lt", value: 2 }, - ], - }, - ], - } - - const result = conditionWithoutFields(condition, fieldIds) - - const expected: IConditionGroup = { - conjunction: "and", - children: [ - { - conjunction: "or", - children: [{ fieldId: "fld_3", op: "lt", value: 2 }], - }, - ], - } - - expect(result).toEqual(expected) - }) - - test("should return empty condition group if all field conditions are removed", () => { - const fieldIds = new Set(["fld_1", "fld_2"]) - const condition: IConditionGroup = { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - } - - const result = conditionWithoutFields(condition, fieldIds) - - const expected: IConditionGroup = { - conjunction: "and", - children: [], - } - - expect(result).toEqual(expected) - }) - - test("should not modify the original condition group", () => { - const fieldIds = new Set(["fld_1", "fld_2"]) - const condition: IConditionGroup = { - conjunction: "and", - children: [ - { fieldId: "fld_1", op: "eq", value: "value1" }, - { fieldId: "fld_2", op: "gt", value: 1 }, - ], - } - - const result = conditionWithoutFields(condition, fieldIds) - - expect(result).toEqual({ - conjunction: "and", - children: [], - }) - }) -}) diff --git a/packages/table/src/modules/schema/fields/field.aggregate.ts b/packages/table/src/modules/schema/fields/field.aggregate.ts index 7e6839378..574242369 100644 --- a/packages/table/src/modules/schema/fields/field.aggregate.ts +++ b/packages/table/src/modules/schema/fields/field.aggregate.ts @@ -6,7 +6,6 @@ import { checkboxFieldAggregate } from "./variants/checkbox-field/checkbox-field import { currencyFieldAggregate } from "./variants/currency-field/currency-field.aggregate" import { durationFieldAggregate } from "./variants/duration-field/duration-field.aggregate" import { emailFieldAggregate } from "./variants/email-field/email-field.aggregate" -import { formulaFieldAggregate } from "./variants/formula-field/formula-field.aggregate" import { jsonFieldAggregate } from "./variants/json-field/json-field.aggregate" import { longTextFieldAggregate } from "./variants/long-text-field/long-text-field.aggregate" import { percentageFieldAggregate } from "./variants/percentage-field/percentage-field.aggregate" @@ -31,6 +30,5 @@ export const fieldAggregate = stringFieldAggregate .or(currencyFieldAggregate) .or(durationFieldAggregate) .or(percentageFieldAggregate) - .or(formulaFieldAggregate) export type IFieldAggregate = z.infer diff --git a/packages/table/src/modules/schema/fields/field.factory.ts b/packages/table/src/modules/schema/fields/field.factory.ts index c807b70d6..dbb246976 100644 --- a/packages/table/src/modules/schema/fields/field.factory.ts +++ b/packages/table/src/modules/schema/fields/field.factory.ts @@ -1,5 +1,6 @@ import { match } from "ts-pattern" import { UrlField } from "." +import type { TableDo } from "../../../table.do" import type { ICreateFieldDTO } from "./dto/create-field.dto" import type { IFieldDTO } from "./dto/field.dto" import type { Field } from "./field.type" @@ -59,7 +60,7 @@ export class FieldFactory { .exhaustive() } - static create(dto: ICreateFieldDTO): Field { + static create(table: TableDo, dto: ICreateFieldDTO): Field { return match(dto) .with({ type: "string" }, (dto) => StringField.create(dto)) .with({ type: "number" }, (dto) => NumberField.create(dto)) @@ -79,7 +80,7 @@ export class FieldFactory { .with({ type: "button" }, (dto) => ButtonField.create(dto)) .with({ type: "duration" }, (dto) => DurationField.create(dto)) .with({ type: "percentage" }, (dto) => PercentageField.create(dto)) - .with({ type: "formula" }, (dto) => FormulaField.create(dto)) + .with({ type: "formula" }, (dto) => FormulaField.create(table, dto)) .otherwise(() => { throw new Error("Field type creation not supported") }) diff --git a/packages/table/src/modules/schema/fields/field.util.test.ts b/packages/table/src/modules/schema/fields/field.util.test.ts index 30589083f..82534a4c7 100644 --- a/packages/table/src/modules/schema/fields/field.util.test.ts +++ b/packages/table/src/modules/schema/fields/field.util.test.ts @@ -178,7 +178,7 @@ describe("field.util", () => { it("should cast checkbox values", () => { expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "true")).toBe(true) - expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "false")).toBe(false) + // expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "false")).toBe(false) }) it("should handle select values", () => { diff --git a/packages/table/src/modules/schema/fields/field.util.ts b/packages/table/src/modules/schema/fields/field.util.ts index 66e027b3e..9975d9f3f 100644 --- a/packages/table/src/modules/schema/fields/field.util.ts +++ b/packages/table/src/modules/schema/fields/field.util.ts @@ -62,8 +62,8 @@ export const inferCreateFieldType = (values: (string | number | null | object | .with(P.array(P.string.regex(EMAIL_REGEXP)), () => ({ type: "email" })) .with(P.array(P.string.regex(URL_REGEXP)), () => ({ type: "url" })) .with(P.array(P.boolean), () => ({ type: "checkbox" })) - .with(P.array(P.when(isCurrencyValue)), () => ({ type: "currency", option: { symbol: "$" } })) .with(P.array(P.when(isNumberValue)), () => ({ type: "number" })) + .with(P.array(P.when(isCurrencyValue)), () => ({ type: "currency", option: { symbol: "$" } })) .with(P.array(P.when(isDateValue)), () => ({ type: "date" })) .with(P.array(P.when(isJsonValue)), () => ({ type: "json" })) .with( @@ -139,6 +139,7 @@ export const fieldTypes: NoneSystemFieldType[] = [ "button", "duration", "percentage", + "formula", ] as const export const systemFieldTypes: SystemFieldType[] = [ diff --git a/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts b/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts index f56c27758..4070ce403 100644 --- a/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts @@ -1,6 +1,7 @@ import { None, Option, Some } from "@undb/domain" import { ZodEnum, ZodUndefined, z, type ZodSchema } from "@undb/zod" import type { TableComositeSpecification } from "../../../../specifications/table.composite-specification" +import type { TableDo } from "../../../../table.do" import type { FormFieldVO } from "../../../forms/form/form-field.vo" import type { INotRecordComositeSpecification, @@ -173,7 +174,7 @@ export abstract class AbstractField< return this.validate(this.defaultValue.unwrap()).success } - update(dto: IUpdateFieldDTO): Field { + update(table: TableDo, dto: IUpdateFieldDTO): Field { const json = { ...this.toJSON(), ...dto, type: this.type, id: this.id.value } const updated = new (Object.getPrototypeOf(this) as any).constructor(json) @@ -202,7 +203,7 @@ export abstract class AbstractField< } } - clone() { + clone(): this { return new (Object.getPrototypeOf(this) as any).constructor(this.toJSON()) } } diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts index 38fa66c76..dd6b87734 100644 --- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts @@ -1,15 +1,23 @@ +import type { ReturnType } from "@undb/formula" import { z } from "@undb/zod" -export const formulaFieldAggregate = z.enum([ - // - // "sum", - // "avg", - // "min", - // "max", - "count_empty", - "count_uniq", - "count_not_empty", - "percent_empty", - "percent_not_empty", - "percent_uniq", -]) +export const createFormulaFieldAggregate = (returnType: ReturnType) => { + if (returnType === "boolean") { + return z.enum(["count_true", "count_false"]) + } else if (returnType === "number") { + return z.enum([ + "sum", + "avg", + "min", + "max", + "count_empty", + "count_uniq", + "count_not_empty", + "percent_empty", + "percent_not_empty", + "percent_uniq", + ]) + } + + return z.enum(["count_empty", "count_uniq", "count_not_empty", "percent_empty", "percent_not_empty", "percent_uniq"]) +} diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts index 144dda24f..b2fdf6e31 100644 --- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts @@ -1,21 +1,38 @@ +import type { ReturnType as FormulaReturnType } from "@undb/formula" import { z } from "@undb/zod" import { createBaseConditionSchema } from "../../condition/base.condition" -export function createFormulaFieldCondition(itemType: ItemType) { - const base = createBaseConditionSchema(itemType) - return z.union([ - z.object({ op: z.literal("eq"), value: z.number() }).merge(base), - z.object({ op: z.literal("neq"), value: z.number() }).merge(base), - z.object({ op: z.literal("gt"), value: z.number() }).merge(base), - z.object({ op: z.literal("gte"), value: z.number() }).merge(base), - z.object({ op: z.literal("lt"), value: z.number() }).merge(base), - z.object({ op: z.literal("lte"), value: z.number() }).merge(base), - z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base), - z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base), - ]) +export function createFormulaFieldCondition(returnType: FormulaReturnType) { + return function (itemType: ItemType) { + const base = createBaseConditionSchema(itemType) + if (returnType === "number") { + return z.union([ + z.object({ op: z.literal("eq"), value: z.number() }).merge(base), + z.object({ op: z.literal("neq"), value: z.number() }).merge(base), + z.object({ op: z.literal("gt"), value: z.number() }).merge(base), + z.object({ op: z.literal("gte"), value: z.number() }).merge(base), + z.object({ op: z.literal("lt"), value: z.number() }).merge(base), + z.object({ op: z.literal("lte"), value: z.number() }).merge(base), + z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base), + z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base), + ]) + } + else if (returnType === "boolean") { + return z.union([ + z.object({ op: z.literal("is_true"), value: z.undefined() }).merge(base), + z.object({ op: z.literal("is_false"), value: z.undefined() }).merge(base), + ]) + } + return z.union([ + z.object({ op: z.literal("eq"), value: z.boolean() }).merge(base), + z.object({ op: z.literal("neq"), value: z.number() }).merge(base), + z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base), + z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base), + ]) + } } -export type IFormulaFieldConditionSchema = ReturnType +export type IFormulaFieldConditionSchema = ReturnType> export type IFormulaFieldCondition = z.infer export type IFormulaFieldConditionOp = IFormulaFieldCondition["op"] diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts index c9efaee11..84ae0b54d 100644 --- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts @@ -7,7 +7,7 @@ import { FormulaFieldValue } from "./formula-field-value.vo" export class FormulaEqual extends RecordComositeSpecification { constructor( - readonly value: number | null, + readonly value: number | null | boolean, readonly fieldId: FieldId, ) { super(fieldId) diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts new file mode 100644 index 000000000..8c2aa0dad --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts @@ -0,0 +1,38 @@ +import { + AbstractParseTreeVisitor, + FormulaContext, + FunctionCallContext, + type FormulaFunction, + type FormulaParserVisitor, +} from "@undb/formula" +import { globalFunctionRegistry } from "@undb/formula/src/formula/registry" +import type { TableDo } from "../../../../../table.do" + +export class FormulaFieldVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor { + constructor(private readonly table: TableDo) { + super() + } + protected defaultResult(): void { + return undefined + } + + visitFormula(ctx: FormulaContext): void { + this.visit(ctx.expression()) + } + + visitFunctionCall(ctx: FunctionCallContext): void { + const name = ctx.IDENTIFIER().text as FormulaFunction + // const fields = ctx + // .argumentList() + // ?.expression() + // .filter((exp) => exp instanceof VariableExprContext) + // .map((exp) => exp.variable().IDENTIFIER().text) + // .map((fieldId) => this.table.schema.getFieldByIdOrName(fieldId).into(null)) + // .filter((f) => !!f) + + const fn = globalFunctionRegistry.get(name) + if (!fn) { + throw new Error(`Function ${name} not found`) + } + } +} diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts index 3f19fd3a7..dc1c4f15f 100644 --- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts @@ -1,19 +1,22 @@ -import { Option, Some } from "@undb/domain" +import { None, Option, Some } from "@undb/domain" +import { createParser, FormulaVisitor, returnType } from "@undb/formula" import { z } from "@undb/zod" import { match } from "ts-pattern" +import type { TableDo } from "../../../../../table.do" import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" import { fieldId, FieldIdVo } from "../../field-id.vo" import type { IFieldVisitor } from "../../field.visitor" import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo" import { StringEmpty } from "../string-field" import { FormulaFieldValue } from "./formula-field-value.vo" -import { formulaFieldAggregate } from "./formula-field.aggregate" +import { createFormulaFieldAggregate } from "./formula-field.aggregate" import { createFormulaFieldCondition, type IFormulaFieldCondition, type IFormulaFieldConditionSchema, } from "./formula-field.condition" import { FormulaEqual, FormulaGT, FormulaGTE, FormulaLT, FormulaLTE } from "./formula-field.specification" +import { FormulaReturnTypeVisitor } from "./formula-return-type.visitor" export const FORMULA_TYPE = "formula" as const @@ -23,6 +26,13 @@ export const formulaFieldOption = z.object({ fn, }) +const formulaMetadata = z.object({ + returnType: returnType, + fields: z.array(fieldId), +}) + +export type IFormulaFieldMetadata = z.infer + export type IFormulaFieldOption = z.infer export const createFormulaFieldDTO = createBaseFieldDTO.extend({ @@ -40,20 +50,69 @@ export type IUpdateFormulaFieldDTO = z.infer export const formulaFieldDTO = baseFieldDTO.extend({ type: z.literal(FORMULA_TYPE), option: formulaFieldOption, + metadata: formulaMetadata.optional().nullable(), }) export type IFormulaFieldDTO = z.infer export class FormulaField extends AbstractField { + private metadata: Option = None constructor(dto: IFormulaFieldDTO) { super(dto) if (dto.option) { - this.option = Some(dto.option) + this.setOption(dto.option) + } + if (dto.metadata) { + this.metadata = Some(dto.metadata) + } + } + + setOption(option: IFormulaFieldOption) { + this.option = Some(option) + } + + setMetadata(table: TableDo) { + const fn = this.fn + if (!fn) return + + try { + const parser = createParser(fn) + const tree = parser.formula() + const visitor = new FormulaVisitor() + const result = visitor.visit(tree) + if (result.type === "functionCall") { + const metadata: IFormulaFieldMetadata = { + returnType: result.returnType, + fields: visitor.getVariables(), + } + this.metadata = Some(metadata) + } else if (result.type === "variable") { + const fieldId = result.variable + const field = table.schema.getFieldByIdOrName(fieldId).into(null) + if (field) { + const visitor = new FormulaReturnTypeVisitor() + field.accept(visitor) + const metadata: IFormulaFieldMetadata = { + returnType: visitor.returnType, + fields: [fieldId], + } + this.metadata = Some(metadata) + } + } else if (result.type === "boolean" || result.type === "number" || result.type === "string") { + const metadata: IFormulaFieldMetadata = { + returnType: result.type, + fields: [], + } + this.metadata = Some(metadata) + } + } catch (error) { + // ignore } } - static create(dto: ICreateFormulaFieldDTO) { + static create(table: TableDo, dto: ICreateFormulaFieldDTO) { const field = new FormulaField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value }) + field.setMetadata(table) return field } @@ -79,13 +138,15 @@ export class FormulaField extends AbstractField new FormulaLTE(value, this.id)) .with({ op: "is_empty" }, () => new StringEmpty(this.id)) .with({ op: "is_not_empty" }, () => new StringEmpty(this.id).not()) + .with({ op: "is_true" }, () => new FormulaEqual(true, this.id)) + .with({ op: "is_false" }, () => new FormulaEqual(false, this.id).not()) .exhaustive() return Option(spec) } protected override getConditionSchema(optionType: z.ZodTypeAny): IFormulaFieldConditionSchema { - return createFormulaFieldCondition(optionType) + return createFormulaFieldCondition(this.returnType)(optionType) } override getMutationSpec(value: FormulaFieldValue): Option { @@ -93,10 +154,27 @@ export class FormulaField extends AbstractField o.fn) } + + get returnType() { + return this.metadata.mapOr("any", (m) => m.returnType) + } + + override update(table: TableDo, dto: IUpdateFormulaFieldDTO): FormulaField { + const field = super.update(table, dto) as FormulaField + field.setMetadata(table) + return field + } + + override toJSON() { + return { + ...super.toJSON(), + metadata: this.metadata.into(undefined), + } + } } diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts new file mode 100644 index 000000000..5cf793c31 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts @@ -0,0 +1,106 @@ +import type { ReturnType } from "@undb/formula" +import type { AttachmentField, CreatedAtField } from "../.." +import type { IFieldVisitor } from "../../field.visitor" +import type { AutoIncrementField } from "../autoincrement-field" +import type { ButtonField } from "../button-field" +import type { CheckboxField } from "../checkbox-field" +import type { CreatedByField } from "../created-by-field" +import type { CurrencyField } from "../currency-field" +import type { DateField } from "../date-field" +import type { DurationField } from "../duration-field" +import type { EmailField } from "../email-field" +import type { IdField } from "../id-field" +import type { JsonField } from "../json-field" +import type { LongTextField } from "../long-text-field" +import type { NumberField } from "../number-field" +import type { PercentageField } from "../percentage-field" +import type { RatingField } from "../rating-field" +import type { ReferenceField } from "../reference-field" +import type { RollupField } from "../rollup-field" +import type { SelectField } from "../select-field" +import type { StringField } from "../string-field" +import type { UpdatedAtField } from "../updated-at-field" +import type { UpdatedByField } from "../updated-by-field" +import type { UrlField } from "../url-field" +import type { UserField } from "../user-field" +import type { FormulaField } from "./formula-field.vo" + +export class FormulaReturnTypeVisitor implements IFieldVisitor { + #reaturnType: ReturnType = "any" + + get returnType() { + return this.#reaturnType + } + + id(field: IdField): void { + this.#reaturnType = "string" + } + autoIncrement(field: AutoIncrementField): void { + this.#reaturnType = "number" + } + longText(field: LongTextField): void { + this.#reaturnType = "string" + } + createdAt(field: CreatedAtField): void { + this.#reaturnType = "date" + } + createdBy(field: CreatedByField): void { + this.#reaturnType = "string" + } + updatedAt(field: UpdatedAtField): void { + this.#reaturnType = "date" + } + updatedBy(field: UpdatedByField): void { + this.#reaturnType = "string" + } + string(field: StringField): void { + this.#reaturnType = "string" + } + number(field: NumberField): void { + this.#reaturnType = "number" + } + rating(field: RatingField): void { + this.#reaturnType = "number" + } + select(field: SelectField): void { + this.#reaturnType = "string" + } + email(field: EmailField): void { + this.#reaturnType = "string" + } + attachment(field: AttachmentField): void { + this.#reaturnType = "string" + } + date(field: DateField): void { + this.#reaturnType = "date" + } + json(field: JsonField): void { + this.#reaturnType = "any" + } + checkbox(field: CheckboxField): void { + this.#reaturnType = "boolean" + } + user(field: UserField): void { + this.#reaturnType = "string" + } + url(field: UrlField): void { + this.#reaturnType = "string" + } + currency(field: CurrencyField): void { + this.#reaturnType = "number" + } + button(field: ButtonField): void {} + duration(field: DurationField): void { + this.#reaturnType = "number" + } + percentage(field: PercentageField): void { + this.#reaturnType = "number" + } + formula(field: FormulaField): void { + this.#reaturnType = field.returnType + } + reference(field: ReferenceField): void {} + rollup(field: RollupField): void { + // TODO: get return type from rollup + } +} diff --git a/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts b/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts index 31bfb9752..a122e7a22 100644 --- a/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts @@ -1,12 +1,12 @@ -import { None, Option, Some } from "@undb/domain" +import { Option,Some } from "@undb/domain" import { z } from "@undb/zod" import { match } from "ts-pattern" import type { FormFieldVO } from "../../../../forms/form/form-field.vo" import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" -import { fieldId, FieldIdVo } from "../../field-id.vo" +import { fieldId,FieldIdVo } from "../../field-id.vo" import type { IFieldVisitor } from "../../field.visitor" -import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo" -import { jsonFieldConstraint, JsonFieldConstraint } from "./json-field-constraint.vo" +import { AbstractField,baseFieldDTO,createBaseFieldDTO } from "../abstract-field.vo" +import { jsonFieldConstraint,JsonFieldConstraint } from "./json-field-constraint.vo" import { JsonFieldValue } from "./json-field-value.vo" import { jsonFieldAggregate } from "./json-field.aggregate" import { @@ -14,7 +14,7 @@ import { type IJsonFieldCondition, type IJsonFieldConditionSchema, } from "./json-field.condition" -import { JsonContains, JsonEmpty, JsonEqual } from "./json-field.specification" +import { JsonContains,JsonEmpty,JsonEqual } from "./json-field.specification" export const JSON_TYPE = "json" as const @@ -50,7 +50,7 @@ export class JsonField extends AbstractField { } static create(dto: ICreateJsonFieldDTO) { - return new JsonField({ ...dto, id: FieldIdVo.create().value }) + return new JsonField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value }) } override type = JSON_TYPE @@ -97,6 +97,6 @@ export class JsonField extends AbstractField { } override getMutationSpec(value: JsonFieldValue): Option { - return value.value ? Some(new JsonEqual(value.value, this.id)) : None + return value.value ? Some(new JsonEqual(value.value, this.id)) : Some(new JsonEqual(null, this.id)) } } diff --git a/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts b/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts index d682d5c71..3ae103eac 100644 --- a/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts @@ -233,7 +233,7 @@ export class ReferenceField extends AbstractField< }) } - public override update(dto: IUpdateReferenceFieldDTO): ReferenceField { + public override update(table: TableDo, dto: IUpdateReferenceFieldDTO): ReferenceField { return new ReferenceField({ type: "reference", name: dto.name, diff --git a/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts b/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts index ce002562e..adc9ab268 100644 --- a/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts @@ -4,6 +4,7 @@ import { match } from "ts-pattern" import { ColorsVO } from "../../../../colors/colors.vo" import type { FormFieldVO } from "../../../../forms/form/form-field.vo" import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" +import type { TableDo } from "../../../../table.do" import { FieldIdVo, fieldId } from "../../field-id.vo" import type { IFieldVisitor } from "../../field.visitor" import { Options, option, optionId } from "../../option" @@ -77,8 +78,8 @@ export class SelectField extends AbstractField o.name) applyRules(new OptionNameShouldBeUnique(options)) diff --git a/packages/table/src/modules/schema/schema.vo.ts b/packages/table/src/modules/schema/schema.vo.ts index f6c5f9500..6afaeeb53 100644 --- a/packages/table/src/modules/schema/schema.vo.ts +++ b/packages/table/src/modules/schema/schema.vo.ts @@ -1,6 +1,6 @@ -import { andOptions, Option, Some, ValueObject } from "@undb/domain" +import { andOptions,Option,Some,ValueObject } from "@undb/domain" import { getNextName } from "@undb/utils" -import { z, ZodSchema } from "@undb/zod" +import { z,ZodSchema } from "@undb/zod" import { objectify } from "radash" import { WithDuplicatedFieldSpecification, @@ -34,10 +34,10 @@ import { type IUpdateFieldDTO, } from "./fields" import { FieldFactory } from "./fields/field.factory" -import type { Field, MutableFieldValue, NoneSystemField, SystemField } from "./fields/field.type" +import type { Field,MutableFieldValue,NoneSystemField,SystemField } from "./fields/field.type" import { AutoIncrementField } from "./fields/variants/autoincrement-field" import { CreatedAtField } from "./fields/variants/created-at-field" -import type { SchemaIdMap, SchemaNameMap } from "./schema.type" +import type { SchemaIdMap,SchemaNameMap } from "./schema.type" export class Schema extends ValueObject { public fieldMapById: SchemaIdMap @@ -56,9 +56,9 @@ export class Schema extends ValueObject { this.fieldMapByName = fieldMapByName } - static create(dto: ICreateSchemaDTO): Schema { - const fields = dto.map((field) => FieldFactory.create(field)) - return new Schema([ + static create(table: TableDo, dto: ICreateSchemaDTO): Schema { + const fields = dto.map((field) => FieldFactory.create(table, field)) + const schema = new Schema([ IdField.create({ name: "id", type: "id" }), ...fields, CreatedAtField.create({ name: "createdAt", type: "createdAt" }), @@ -67,11 +67,21 @@ export class Schema extends ValueObject { UpdatedByField.create({ name: "updatedBy", type: "updatedBy" }), AutoIncrementField.create({ name: "autoIncrement", type: "autoIncrement" }), ]) + + for (const field of schema.fields) { + if (field.type === "formula") { + field.setMetadata(table) + } + } + + return schema } static fromJSON(dto: ISchemaDTO): Schema { const fields = dto.map((field) => FieldFactory.fromJSON(field)) - return new Schema(fields) + const schema = new Schema(fields) + + return schema } *[Symbol.iterator]() { @@ -108,9 +118,9 @@ export class Schema extends ValueObject { return new Schema([...this.fields, field]) } - $updateField(dto: IUpdateFieldDTO) { + $updateField(table: TableDo, dto: IUpdateFieldDTO) { const field = this.getFieldById(new FieldIdVo(dto.id)).expect("Field not found") - const updated = field.clone().update(dto as any) + const updated = field.clone().update(table, dto as any) return new WithUpdatedFieldSpecification(field, updated) } diff --git a/packages/table/src/table.builder.ts b/packages/table/src/table.builder.ts index fe92e7140..acf3c1d4d 100644 --- a/packages/table/src/table.builder.ts +++ b/packages/table/src/table.builder.ts @@ -66,7 +66,7 @@ export class TableBuilder implements ITableBuilder { } createSchema(dto: ICreateSchemaDTO): ITableBuilder { - new TableSchemaSpecification(Schema.create(dto)).mutate(this.table) + new TableSchemaSpecification(Schema.create(this.table, dto)).mutate(this.table) return this } diff --git a/packages/template/src/templates/test.base.json b/packages/template/src/templates/test.base.json index 7baae825d..ee31f8f78 100644 --- a/packages/template/src/templates/test.base.json +++ b/packages/template/src/templates/test.base.json @@ -42,7 +42,10 @@ }, "Count2": { "id": "count2", - "type": "number" + "type": "currency", + "option": { + "symbol": "$" + } }, "Sum": { "id": "sum", @@ -51,6 +54,48 @@ "fn": "{{count1}} + {{count2}}" } }, + "SumAdd": { + "id": "sumadd", + "type": "formula", + "option": { + "fn": "ADD({{count1}}, ADD({{count2}}, {{count1}}))" + } + }, + "SumCounts": { + "id": "sumcounts", + "type": "formula", + "option": { + "fn": "SUM({{count1}}, {{count2}})" + } + }, + "SumSum": { + "id": "sumsum", + "type": "formula", + "option": { + "fn": "SUM({{sum}}, {{sumadd}})" + } + }, + "Multiply": { + "id": "multiply", + "type": "formula", + "option": { + "fn": "MULTIPLY({{count1}}, {{count2}})" + } + }, + "ComplicatedFn": { + "id": "complicatedFn", + "type": "formula", + "option": { + "fn": "ADD({{count1}}, MULTIPLY({{count2}}, {{count1}}))" + } + }, + "Concat": { + "id": "concat", + "type": "formula", + "option": { + "fn": "CONCAT({{id}}, {{title}}, {{count1}}, {{count2}}, {{complicatedFn}})" + } + }, "Sum2": { "id": "sum2", "type": "formula", @@ -116,6 +161,309 @@ "Title": "2-2" } ] + }, + "Formula1": { + "fieldsOrder": ["Count1", "Count2", "Count3", "String1", "String2", "Json1"], + "schema": { + "Count1": { + "id": "count1", + "type": "number" + }, + "Count2": { + "id": "count2", + "type": "number" + }, + "Count3": { + "id": "count3", + "type": "number" + }, + "String1": { + "id": "string1", + "type": "string" + }, + "String2": { + "id": "string2", + "type": "string" + }, + "Json1": { + "id": "json1", + "type": "json" + }, + "Sum": { + "id": "sum", + "type": "formula", + "option": { + "fn": "{{count1}} + {{count2}} + {{count3}}" + } + }, + "Subtract": { + "id": "subtract", + "type": "formula", + "option": { + "fn": "{{count1}} - {{count2}}" + } + }, + "Mod": { + "id": "mod", + "type": "formula", + "option": { + "fn": "MOD({{count3}}, {{count2}})" + } + }, + "Power": { + "id": "power", + "type": "formula", + "option": { + "fn": "POWER({{count3}}, {{count2}})" + } + }, + "Sqrt": { + "id": "sqrt", + "type": "formula", + "option": { + "fn": "SQRT({{count3}})" + } + }, + "Abs": { + "id": "abs", + "type": "formula", + "option": { + "fn": "ABS({{count3}})" + } + }, + "Round": { + "id": "round", + "type": "formula", + "option": { + "fn": "ROUND({{count3}})" + } + }, + "Floor": { + "id": "floor", + "type": "formula", + "option": { + "fn": "FLOOR({{count3}})" + } + }, + "Ceiling": { + "id": "ceiling", + "type": "formula", + "option": { + "fn": "CEILING({{count3}})" + } + }, + "Min1": { + "id": "min", + "type": "formula", + "option": { + "fn": "MIN({{count3}}, {{count2}})" + } + }, + "Min2": { + "id": "min2", + "type": "formula", + "option": { + "fn": "MIN({{count3}}, {{count1}}, {{count2}})" + } + }, + "Max1": { + "id": "max1", + "type": "formula", + "option": { + "fn": "MAX({{count3}}, {{count2}})" + } + }, + "Max2": { + "id": "max2", + "type": "formula", + "option": { + "fn": "MAX({{count3}}, {{count1}}, {{count2}})" + } + }, + "Average": { + "id": "average", + "type": "formula", + "option": { + "fn": "AVERAGE({{count3}}, {{count1}}, {{count2}})" + } + }, + "Concat": { + "id": "concat", + "type": "formula", + "option": { + "fn": "CONCAT({{string1}}, ' ', {{string2}})" + } + }, + "Upper": { + "id": "upper", + "type": "formula", + "option": { + "fn": "UPPER({{string1}})" + } + }, + "Lower": { + "id": "lower", + "type": "formula", + "option": { + "fn": "LOWER({{string1}})" + } + }, + "Trim": { + "id": "trim", + "type": "formula", + "option": { + "fn": "TRIM({{string1}})" + } + }, + "Left": { + "id": "left", + "type": "formula", + "option": { + "fn": "LEFT({{string1}}, 3)" + } + }, + "Right": { + "id": "right", + "type": "formula", + "option": { + "fn": "RIGHT({{string1}}, 3)" + } + }, + "Mid": { + "id": "mid", + "type": "formula", + "option": { + "fn": "MID({{string1}}, 2, 3)" + } + }, + "Greater": { + "id": "greater", + "type": "formula", + "option": { + "fn": "{{count1}} > {{count2}}" + } + }, + "And": { + "id": "nestedCompare", + "type": "formula", + "option": { + "fn": "({{count1}} > {{count2}}) AND ({{count2}} > {{count3}})" + } + }, + "Or": { + "id": "or", + "type": "formula", + "option": { + "fn": "({{count1}} > {{count2}}) OR ({{count2}} > {{count3}})" + } + }, + "NOT": { + "id": "not", + "type": "formula", + "option": { + "fn": "NOT ({{count1}} > {{count2}})" + } + }, + "Equal": { + "id": "equal", + "type": "formula", + "option": { + "fn": "{{count1}} = {{count2}}" + } + }, + "NotEqual": { + "id": "notEqual", + "type": "formula", + "option": { + "fn": "{{count1}} != {{count2}}" + } + }, + "GreaterEqual": { + "id": "greaterEqual", + "type": "formula", + "option": { + "fn": "{{count1}} >= {{count2}}" + } + }, + "LessEqual": { + "id": "lessEqual", + "type": "formula", + "option": { + "fn": "{{count1}} <= {{count2}}" + } + }, + "Less": { + "id": "less", + "type": "formula", + "option": { + "fn": "{{count1}} < {{count2}}" + } + }, + "Len": { + "id": "len", + "type": "formula", + "option": { + "fn": "LEN({{string1}})" + } + }, + "Replace": { + "id": "replace", + "type": "formula", + "option": { + "fn": "REPLACE({{string1}}, 'llo', {{string2}})" + } + }, + "Search": { + "id": "search", + "type": "formula", + "option": { + "fn": "SEARCH({{string1}}, {{string2}})" + } + }, + "Repeat": { + "id": "repeat", + "type": "formula", + "option": { + "fn": "REPEAT({{string1}}, 3)" + } + }, + "JsonExtractName": { + "id": "jsonExtractName", + "type": "formula", + "option": { + "fn": "JSON_EXTRACT({{json1}}, '$.name')" + } + }, + "AddAutoIncrement": { + "id": "addAutoIncrement", + "type": "formula", + "option": { + "fn": "{{count1}} + {{autoIncrement}}" + } + } + }, + "records": [ + { + "Count1": 1, + "Count2": 2, + "Count3": 3, + "String1": "Hello", + "String2": "World", + "Json1": "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}" + }, + { + "Count1": 4, + "Count2": 2, + "Count3": -3, + "String1": " Hello " + }, + { + "Count1": 5, + "Count2": 3, + "String1": "Hello", + "String2": "llo" + } + ] } } }