From fcc22b42a24cfc252c521d5f39df915461a0ea8f Mon Sep 17 00:00:00 2001 From: Adi Fatkhurozi Date: Tue, 13 Jun 2023 02:16:32 +0700 Subject: [PATCH] fix: refactoring code for more clean --- README.md | 87 ++++---- src/ExpressionParser.ts | 331 ++++++++++++++++++++++++++++ src/createParser.ts | 129 +++++++++++ src/index.ts | 470 +--------------------------------------- src/test/index.test.ts | 75 +++---- 5 files changed, 535 insertions(+), 557 deletions(-) create mode 100644 src/ExpressionParser.ts create mode 100644 src/createParser.ts diff --git a/README.md b/README.md index e329dc4..d43cc82 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,17 @@ npm install @adifkz/exp-p To use ExpressionParser in your JavaScript code, you need to import the ExpressionParser class and create an instance of it: ```typescript -import ExpressionParser from '@adifkz/exp-p'; +import { createParser } from '@adifkz/exp-p'; -const parser = new ExpressionParser(); +const parser = createParser(); ``` ## Evaluating Expressions Once you have created an instance of ExpressionParser, you can use the evaluate method to evaluate expressions. The evaluate method takes two parameters: the expression to evaluate and an optional context object that contains variables and functions used in the expression. ```typescript +const parser = createParser(); + const result = parser.evaluate('2 + 3 * 4'); // 14 ``` @@ -43,7 +45,7 @@ const result = parser.evaluate('2 + 3 * 4'); // 14 You can define variables and use them in your expressions by providing a context object to the evaluate method. ```typescript -const parser = new ExpressionParser(); +const parser = createParser(); // Define variables const variables = { @@ -52,7 +54,7 @@ const variables = { }; // Evaluate expression with variables -const result = parser.evaluate("x + y", { variables }); +const result = parser.evaluate("x + y", variables); console.log(result); // Output: 15 ``` @@ -60,24 +62,24 @@ console.log(result); // Output: 15 The Expression Parser allows you to extend its functionality by adding custom functions. Here's an example of defining a custom function: ```typescript -const parser = new ExpressionParser(); +const parser = createParser(); // Define custom function const functions = { square: (_, value: number) => value * value, }; +parser.setFunctions(functions) // Evaluate expression with custom function -const result = parser.evaluate("square(5)", { functions }); +const result = parser.evaluate("square(5)"); console.log(result); // Output: 25 ``` - ## Examples ```typescript it('basic operator', () => { - const parser = new ExpressionParser() + const parser = createParser() expect(parser.evaluate('(2 + 3) * 4 - 4')).toBe(16) expect(parser.evaluate('-4 + 5')).toBe(1) expect(parser.evaluate('4 <= (5 + 2)')).toBe(true) @@ -92,36 +94,37 @@ it('function', () => { // Usage example const variables = { x: 5 }; const functions: FunctionMap = { - ADD: (_, a: number, b: number) => a + b, - LENGTH: (_, str: string) => str.length, - LENGTH_ALL: (_, str1: string, str2: string, str3: string) => [str1.length, str2.length, str3.length], + add: (_, a: number, b: number) => a + b, + length: (_, str: string) => str.length, + length_all: (_, str1: string, str2: string, str3: string) => [str1.length, str2.length, str3.length], }; - const parser = new ExpressionParser({ variables, functions }); - expect(parser.evaluate('ADD(1 + 1, 5) + x')).toBe(12) - expect(parser.evaluate('LENGTH("ADI") + 5', { functions, variables },)).toBe(8) - expect(parser.evaluate('LENGTH_ALL("ADI", "FA", "TK")', { variables, functions })).toEqual([3, 2, 2]) + const parser = createParser({ variables }); + parser.setFunctions(functions) + expect(parser.evaluate('add(1 + 1, 5) + x')).toBe(12) + expect(parser.evaluate('length("ADI") + 5', variables)).toBe(8) + expect(parser.evaluate('length_all("ADI", "FA", "TK")', variables)).toEqual([3, 2, 2]) }); it('string', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate('"ADI"')).toBe("ADI") expect(parser.evaluate('\'ADI\'')).toBe("ADI") }) it('boolean', () => { - const parser = new ExpressionParser(); - expect(parser.evaluate('true AND false')).toBe(false) - expect(parser.evaluate('true OR false')).toBe(true) + const parser = createParser(); + expect(parser.evaluate('true and false')).toBe(false) + expect(parser.evaluate('true or false')).toBe(true) expect(parser.evaluate('!true')).toBe(false) expect(parser.evaluate('!!true')).toBe(true) }) it('array', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate("[1, 2, 3, 4]")).toEqual([1, 2, 3, 4]) expect(parser.evaluate("[\"2\", 5]")).toEqual(["2", 5]) expect(parser.evaluate("[2 + 5, 5]")).toEqual([7, 5]) - expect(parser.evaluate("[5, x]", { variables: { x: 2 } })).toEqual([5, 2]) + expect(parser.evaluate("[5, x]", { x: 2 })).toEqual([5, 2]) }) it('array method', () => { - const parser = new ExpressionParser(); + const parser = createParser(); const products = [ { name: 'Product 1', price: 150, quantity: 2 }, { name: 'Product 2', price: 80, quantity: 0 }, @@ -129,10 +132,8 @@ it('array method', () => { { name: 'Product 4', price: 120, quantity: 1 } ]; expect( - parser.evaluate('FILTER(products, "__ITEM__.price > 100 AND __ITEM__.quantity > 0")', { - variables: { - products - } + parser.evaluate('filter(products, "_item_.price > 100 and _item_.quantity > 0")', { + products }) ).toEqual([ { name: 'Product 1', price: 150, quantity: 2 }, @@ -141,10 +142,8 @@ it('array method', () => { ]) expect( - parser.evaluate('MAP(products, "__ITEM__.name")', { - variables: { - products - } + parser.evaluate('map(products, "_item_.name")', { + products }) ).toEqual([ 'Product 1', @@ -154,10 +153,8 @@ it('array method', () => { ]) expect( - parser.evaluate('FIND(products, "__ITEM__.price > 0")', { - variables: { - products - } + parser.evaluate('find(products, "_item_.price > 0")', { + products }) ).toEqual({ "name": "Product 1", @@ -166,23 +163,19 @@ it('array method', () => { }) expect( - parser.evaluate('SOME(products, "__ITEM__.price == 200")', { - variables: { - products - } + parser.evaluate('some(products, "_item_.price == 200")', { + products }) ).toBe(true) expect( - parser.evaluate('REDUCE(products, "__CURR__ + __ITEM__.price", 0)', { - variables: { - products - } + parser.evaluate('reduce(products, "_curr_ + _item_.price", 0)', { + products }) ).toBe(550) }) it('object', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate("{ name: 'ADI', age: 20 }")).toEqual({ name: "ADI", age: 20 @@ -191,9 +184,9 @@ it('object', () => { name: "ADI", age: 7 }) - expect(parser.evaluate("object.name", { variables: { object: { name: 'ADI' } } })).toEqual('ADI') - expect(parser.evaluate("object.name", { variables: { object: { name: 'ADI' } } })).toEqual('ADI') - expect(parser.evaluate("object.0.name", { variables: { object: [{ name: 'ADI' }] } })).toEqual('ADI') - expect(parser.evaluate("object.0.object.0.name", { variables: { object: [{ name: 'ADI', object: [{ name: "ADI" }] }] } })).toEqual('ADI') + expect(parser.evaluate("object.name", { object: { name: 'ADI' } })).toEqual('ADI') + expect(parser.evaluate("object.name", { object: { name: 'ADI' } })).toEqual('ADI') + expect(parser.evaluate("object.0.name", { object: [{ name: 'ADI' }] })).toEqual('ADI') + expect(parser.evaluate("object.0.object.0.name", { object: [{ name: 'ADI', object: [{ name: "ADI" }] }] })).toEqual('ADI') }) ``` \ No newline at end of file diff --git a/src/ExpressionParser.ts b/src/ExpressionParser.ts new file mode 100644 index 0000000..daa3eb7 --- /dev/null +++ b/src/ExpressionParser.ts @@ -0,0 +1,331 @@ + +export interface ParserState { + tokens: string[]; + currentTokenIndex: number; + currentToken: string; + nextToken: () => void; + variables: VariableMap; +} + +type ValueType = number | string | boolean | any[] | object; +export type VariableMap = { [key: string]: ValueType }; +export type FunctionMap = { [key: string]: (state: ParserState, ...args: any[]) => any }; +export type OperatorMap = { [key: string]: (a: any, b: any) => any }; +export type ExpressionParserConstructor = { + variables?: VariableMap, + regex?: RegExp +} + +const comparisonOperatorRegex = /([<>]=|==|!=)/; +const specialCharacterRegex = /([-+*/():,<>!=%^\[\]\{\}])/; +const numberRegex = /\b(?:\d+(\.\d+)?)/; +const stringRegex = /(?:"[^"]*")|(?:'[^']*')/; +const identifierRegex = /(?:\w+(?:\.\w+)*(?:\[\d+\])?)/; + +export class ExpressionParser { + private variables: VariableMap = {}; + private functions: FunctionMap = {}; + private operators: OperatorMap = {}; + private regex: RegExp; + + constructor({ + variables, + regex + }: ExpressionParserConstructor = {}) { + let regexString = comparisonOperatorRegex.source + + '|' + + specialCharacterRegex.source + + '|' + + numberRegex.source + + '|' + + stringRegex.source + + '|' + + identifierRegex.source; + + if (regex) { + regexString += '|' + regex; + } + this.regex = new RegExp(regexString, + 'g' + ); + this.variables = { + ...this.variables, + ...(variables || {}) + } + } + + private tokenize(expression: string): string[] { + return expression.match(this.regex) || []; + } + + private parseNumber(state: ParserState): number { + const token = state.currentToken; + if (token === undefined || isNaN(Number(token))) { + throw new Error('Invalid expression'); + } + + state.nextToken(); + return parseFloat(token); + } + + private parseString(state: ParserState, tickType: string): string { + const token = state.currentToken; + if (token === undefined || !token.startsWith(tickType) || !token.endsWith(tickType)) { + throw new Error('Invalid expression'); + } + + state.nextToken(); + return token.slice(1, -1); + } + + private parseBoolean(state: ParserState): boolean { + const token = state.currentToken; + if (token === undefined || (token !== 'true' && token !== 'false')) { + throw new Error('Invalid expression'); + } + + state.nextToken(); + return token === 'true'; + } + + private parseArray(state: ParserState): any[] { + state.nextToken(); + const array: any[] = []; + + while (state.currentToken !== ']') { + array.push(this.parseExpression(state)); + + if (state.currentToken === ',') { + state.nextToken(); + } + } + + if (state.currentToken !== ']') { + throw new Error('Invalid expression'); + } + + state.nextToken(); + return array; + } + + private parseUnaryFactor(state: ParserState): any { + const token = state.currentToken; + + if (token === '!') { + state.nextToken(); + const factor = this.parseUnaryFactor(state); + return !factor; + } + + return this.parseFactor(state); + } + + private parseObject(state: ParserState): object { + const obj: { [key: string]: any } = {}; + while (true) { + const key = state.currentToken; + if (typeof key !== 'string') { + throw new Error('Invalid object literal'); + } + state.nextToken(); + if (state.currentToken !== ':') { + throw new Error('Invalid object literal'); + } + + state.nextToken(); + + const value = this.parseExpression(state); + obj[key] = value; + + if (state.currentToken as any === '}') { + break; + } + + if (state.currentToken as any !== ',') { + throw new Error('Invalid object literal'); + } + + state.nextToken(); + } + + if (state.currentToken as any !== '}') { + throw new Error('Invalid object literal'); + } + + state.nextToken(); + + return obj; + } + + private parseFunction(state: ParserState): any { + const token = state.currentToken; + const func = this.functions[token] + state.nextToken(); + + if (state.currentToken !== '(') { + throw new Error('Invalid expression'); + } + + state.nextToken(); + + const args: any[] = []; + while (state.currentToken as any !== ')') { + args.push(this.parseExpression(state)); + + if (state.currentToken as any === ',') { + state.nextToken(); + } + } + + if (state.currentToken as any !== ')') { + throw new Error('Invalid expression'); + } + + state.nextToken(); + + return func(state, ...args); + } + + private parseFactor(state: ParserState): ValueType { + let value: ValueType = 0; + const token = state.currentToken; + + if (token === undefined) { + throw new Error('Invalid expression'); + } + if (token === '(') { + state.nextToken(); + value = this.parseExpression(state); + + if (state.currentToken !== ')') { + throw new Error('Invalid expression'); + } + + state.nextToken(); + } else if (!isNaN(Number(token))) { + value = this.parseNumber(state); + } else if (token.startsWith('"') && token.endsWith('"')) { + value = this.parseString(state, '"'); + } else if (token.startsWith('\'') && token.endsWith('\'')) { + value = this.parseString(state, '\''); + } else if (token === 'true' || token === 'false') { + value = this.parseBoolean(state); + } else if (token === '[') { + value = this.parseArray(state); + } else if (token === '{') { + state.nextToken() + value = this.parseObject(state); + } else if (token.includes('.')) { + const objectPath = token.split('.'); + let objectValue = state.variables as any + for (const path of objectPath) { + if (typeof objectValue !== 'object' || objectValue === null || !objectValue.hasOwnProperty(path)) { + + throw new Error('Invalid object path'); + } else { + objectValue = objectValue[path]; + } + } + + value = objectValue; + state.nextToken(); + } else if (state.variables.hasOwnProperty(token)) { + value = state.variables[token]; + state.nextToken(); + } else if (this.functions.hasOwnProperty(token)) { + value = this.parseFunction(state); + } else if (this.operators.hasOwnProperty(token)) { + const operator = this.operators[token]; + state.nextToken(); + + const factor = this.parseFactor(state); + value = operator(0, factor); + } else { + throw new Error('Invalid expression'); + } + + return value; + } + + private parseTerm(state: ParserState): number { + let value = this.parseUnaryFactor(state) as any; + while (true) { + const token = state.currentToken; + if (token === '*' || token === '/') { + const operator = token; + state.nextToken(); + const factor = this.parseUnaryFactor(state); + if (operator === '*') { + value *= factor as number; + } else { + value /= factor as number; + } + } else { + break; + } + } + + return value; + } + + private parseExpression(state: ParserState): any { + let value = this.parseTerm(state); + + while (true) { + const token = state.currentToken; + if (token in this.operators) { + const operator = token; + state.nextToken(); + const term = this.parseTerm(state); + value = this.operators[operator](value, term); + } else { + break; + } + } + + return value; + } + + public evaluate( + expression: string, + variables?: VariableMap, + ): any { + const tempVariables = { ...this.variables, ...(variables || {}) }; + + const state: ParserState = { + tokens: this.tokenize(expression), + currentTokenIndex: 0, + get currentToken() { + return this.tokens[this.currentTokenIndex]; + }, + nextToken() { + this.currentTokenIndex++; + }, + variables: tempVariables, + }; + + const result = this.parseExpression(state); + + if (state.currentToken !== undefined) { + throw new Error('Invalid expression'); + } + + return result; + } + + public setFunctions(functions: FunctionMap): void { + this.functions = { + ...this.functions, + ...functions + }; + } + + public setOperators(operators: OperatorMap): void { + this.operators = { + ...this.operators, + ...operators + }; + } +} + +export default ExpressionParser; \ No newline at end of file diff --git a/src/createParser.ts b/src/createParser.ts new file mode 100644 index 0000000..f81f650 --- /dev/null +++ b/src/createParser.ts @@ -0,0 +1,129 @@ +import ExpressionParser, { ExpressionParserConstructor, FunctionMap, OperatorMap } from "./ExpressionParser" + +export function createParser(props: ExpressionParserConstructor = {}) { + const parser = new ExpressionParser({ + ...props, + variables: { + pi: Math.PI, + ...props.variables, + } + }); + const operators: OperatorMap = { + '+': (a, b) => a + b, + '-': (a, b) => a - b, + '*': (a, b) => a * b, + '/': (a, b) => a / b, + '%': (a, b) => a % b, + and: (a, b) => a && b, + or: (a, b) => a || b, + '>': (a, b) => a > b, + '>=': (a, b) => a >= b, + '<': (a, b) => a < b, + '<=': (a, b) => a <= b, + '==': (a, b) => a === b, + '!=': (a, b) => a !== b, + '^': (a, b) => Math.pow(a, b) + } + const functions: FunctionMap = { + // NUMBER + ceil: (_, value: number) => Math.ceil(value), + round: (_, value: number) => Math.round(value), + random: () => Math.random(), + floor: (_, value: number) => Math.floor(value), + abs: (_, value: number) => Math.abs(value), + // STRING + split: (_, arr: string, arg: string) => arr.split(arg), + // CONDITION + if: (_, condition: boolean, truthy: any, falsy) => { + return (condition) ? truthy : falsy + }, + // ARRAY + min: (_, ...args) => Math.min(...args), + max: (_, ...args) => Math.max(...args), + sum: (_, arr) => arr.reduce((prev: number, curr: number) => prev + curr, 0), + length: (_, value) => value.length, + join: (_, arr: string[], arg: string) => arr.join(arg), + filter: (state, items: any[], filterExpression: string) => { + const filteredItems = items?.filter((item: any, index) => { + const result = parser.evaluate( + filterExpression, + { + ...state.variables, + _item_: item, + _index_: index + }, + ); + return result === true; + }); + + return filteredItems || [] + }, + map: (state, items: any[], filterExpression: string) => { + const filteredItems = items?.map((item: any, index) => { + return parser.evaluate( + filterExpression, + { + ...state.variables, + _item_: item, + _index_: index + } + ); + }); + + return filteredItems || [] + }, + some: (state, items: any[], filterExpression: string) => { + const filteredItems = items?.some((item: any, index) => { + return parser.evaluate( + filterExpression, + { + ...state.variables, + _item_: item, + _index_: index + }, + + ); + }); + + return filteredItems; + }, + find: (state, items: any[], filterExpression: string) => { + const filteredItems = items?.find((item: any, index) => { + return parser.evaluate( + filterExpression, + { + ...state.variables, + _item_: item, + _index_: index + } + ); + }); + + return filteredItems; + }, + reduce: (state, items: any[], filterExpression: string, initial: any) => { + const filteredItems = items?.reduce( + (curr: any, item: any, index) => { + return parser.evaluate( + filterExpression, + { + ...state.variables, + _curr_: curr, + _item_: item, + _index_: index + } + ); + }, + initial + ); + + return filteredItems; + } + } + + parser.setFunctions(functions) + parser.setOperators(operators) + return parser; +} + +export default createParser; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index aa9fec2..5dc8056 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,468 +1,2 @@ - -interface ParserState { - tokens: string[]; - currentTokenIndex: number; - currentToken: string; - nextToken: () => void; - variables: VariableMap; - functions: FunctionMap; - operators: OperatorMap; -} - -type ValueType = number | string | boolean | any[] | object; -export type VariableMap = { [key: string]: ValueType }; -export type FunctionMap = { [key: string]: (state: ParserState, ...args: any[]) => any }; -export type OperatorMap = { [key: string]: (a: any, b: any) => any }; - -const comparisonOperatorRegex = /([<>]=|==|!=)/; -const specialCharacterRegex = /([-+*/():,<>!=%^\[\]\{\}])/; -const numberRegex = /\b(?:\d+(\.\d+)?)/; -const stringRegex = /(?:"[^"]*")|(?:'[^']*')/; -const identifierRegex = /(?:\w+(?:\.\w+)*(?:\[\d+\])?)/; - -class ExpressionParser { - private variables: VariableMap; - private functions: FunctionMap; - private operators: OperatorMap; - private regex: RegExp; - - constructor({ - variables, - functions, - operators, - regex - }: { - variables?: VariableMap, - functions?: FunctionMap, - operators?: OperatorMap, - regex?: RegExp - } = {}) { - let regexString = comparisonOperatorRegex.source + - '|' + - specialCharacterRegex.source + - '|' + - numberRegex.source + - '|' + - stringRegex.source + - '|' + - identifierRegex.source; - - if (regex) { - regexString += '|' + regex; - } - this.regex = new RegExp(regexString, - 'g' - ); - this.variables = { - PI: Math.PI, - ...(variables || {}) - }; - this.functions = { - // NUMBER - CEIL: (_, value: number) => Math.ceil(value), - ROUND: (_, value: number) => Math.round(value), - RANDOM: () => Math.random(), - FLOOR: (_, value: number) => Math.floor(value), - ABS: (_, value: number) => Math.abs(value), - // STRING - SPLIT: (_, arr: string, arg: string) => arr.split(arg), - // CONDITION - IF: (_, condition: boolean, truthy: any, falsy) => { - return (condition) ? truthy : falsy - }, - // ARRAY - MIN: (_, ...args) => Math.min(...args), - MAX: (_, ...args) => Math.max(...args), - SUM: (_, arr) => arr.reduce((prev: number, curr: number) => prev + curr, 0), - LENGTH: (_, value) => value.length, - JOIN: (_, arr: string[], arg: string) => arr.join(arg), - FILTER: (state, items: any[], filterExpression: string) => { - const filteredItems = items?.filter((item: any, index) => { - const result = this.evaluate( - filterExpression, - { - functions: state.functions, - operators: state.operators, - variables: { - ...state.variables, - __ITEM__: item, - __INDEX__: index - }, - } - ); - return result === true; - }); - - return filteredItems || [] - }, - MAP: (state, items: any[], filterExpression: string) => { - const filteredItems = items?.map((item: any, index) => { - return this.evaluate( - filterExpression, - { - functions: state.functions, - operators: state.operators, - variables: { - ...state.variables, - __ITEM__: item, - __INDEX__: index - } - } - ); - }); - - return filteredItems || [] - }, - SOME: (state, items: any[], filterExpression: string) => { - const filteredItems = items?.some((item: any, index) => { - return this.evaluate( - filterExpression, - { - variables: { - ...state.variables, - __ITEM__: item, - __INDEX__: index - }, - functions: state.functions - }, - - ); - }); - - return filteredItems; - }, - FIND: (state, items: any[], filterExpression: string) => { - const filteredItems = items?.find((item: any, index) => { - return this.evaluate( - filterExpression, - { - functions: state.functions, - operators: state.operators, - variables: { - ...state.variables, - __ITEM__: item, - __INDEX__: index - } - }, - ); - }); - - return filteredItems; - }, - REDUCE: (state, items: any[], filterExpression: string, initial: any) => { - const filteredItems = items?.reduce( - (curr: any, item: any, index) => { - return this.evaluate( - filterExpression, - { - functions: state.functions, - operators: state.operators, - variables: { - ...state.variables, - __CURR__: curr, - __ITEM__: item, - __INDEX__: index - } - } - ); - }, - initial - ); - - return filteredItems; - }, - ...(functions || {}) - }; - this.operators = { - '+': (a, b) => a + b, - '-': (a, b) => a - b, - '*': (a, b) => a * b, - '/': (a, b) => a / b, - '%': (a, b) => a % b, - AND: (a, b) => a && b, - OR: (a, b) => a || b, - '>': (a, b) => a > b, - '>=': (a, b) => a >= b, - '<': (a, b) => a < b, - '<=': (a, b) => a <= b, - '==': (a, b) => a === b, - '!=': (a, b) => a !== b, - '^': (a, b) => Math.pow(a, b), - ...(operators || {}) - }; - } - - private tokenize(expression: string): string[] { - return expression.match(this.regex) || []; - } - - private parseNumber(state: ParserState): number { - const token = state.currentToken; - if (token === undefined || isNaN(Number(token))) { - throw new Error('Invalid expression'); - } - - state.nextToken(); - return parseFloat(token); - } - - private parseString(state: ParserState, tickType: string): string { - const token = state.currentToken; - if (token === undefined || !token.startsWith(tickType) || !token.endsWith(tickType)) { - throw new Error('Invalid expression'); - } - - state.nextToken(); - return token.slice(1, -1); - } - - private parseBoolean(state: ParserState): boolean { - const token = state.currentToken; - if (token === undefined || (token !== 'true' && token !== 'false')) { - throw new Error('Invalid expression'); - } - - state.nextToken(); - return token === 'true'; - } - - private parseArray(state: ParserState): any[] { - state.nextToken(); - const array: any[] = []; - - while (state.currentToken !== ']') { - array.push(this.parseExpression(state)); - - if (state.currentToken === ',') { - state.nextToken(); - } - } - - if (state.currentToken !== ']') { - throw new Error('Invalid expression'); - } - - state.nextToken(); - return array; - } - - private parseUnaryFactor(state: ParserState): any { - const token = state.currentToken; - - if (token === '!') { - state.nextToken(); - const factor = this.parseUnaryFactor(state); - return !factor; - } - - return this.parseFactor(state); - } - - private parseObject(state: ParserState): object { - const obj: { [key: string]: any } = {}; - while (true) { - const key = state.currentToken; - if (typeof key !== 'string') { - throw new Error('Invalid object literal'); - } - state.nextToken(); - if (state.currentToken !== ':') { - throw new Error('Invalid object literal'); - } - - state.nextToken(); - - const value = this.parseExpression(state); - obj[key] = value; - - if (state.currentToken as any === '}') { - break; - } - - if (state.currentToken as any !== ',') { - throw new Error('Invalid object literal'); - } - - state.nextToken(); - } - - if (state.currentToken as any !== '}') { - throw new Error('Invalid object literal'); - } - - state.nextToken(); - - return obj; - } - - private parseFunction(state: ParserState): any { - const token = state.currentToken; - const func = state.functions[token] - state.nextToken(); - - if (state.currentToken !== '(') { - throw new Error('Invalid expression'); - } - - state.nextToken(); - - const args: any[] = []; - while (state.currentToken as any !== ')') { - args.push(this.parseExpression(state)); - - if (state.currentToken as any === ',') { - state.nextToken(); - } - } - - if (state.currentToken as any !== ')') { - throw new Error('Invalid expression'); - } - - state.nextToken(); - - return func(state, ...args); - } - - private parseFactor(state: ParserState): ValueType { - let value: ValueType = 0; - const token = state.currentToken; - - if (token === undefined) { - throw new Error('Invalid expression'); - } - if (token === '(') { - state.nextToken(); - value = this.parseExpression(state); - - if (state.currentToken !== ')') { - throw new Error('Invalid expression'); - } - - state.nextToken(); - } else if (!isNaN(Number(token))) { - value = this.parseNumber(state); - } else if (token.startsWith('"') && token.endsWith('"')) { - value = this.parseString(state, '"'); - } else if (token.startsWith('\'') && token.endsWith('\'')) { - value = this.parseString(state, '\''); - } else if (token === 'true' || token === 'false') { - value = this.parseBoolean(state); - } else if (token === '[') { - value = this.parseArray(state); - } else if (token === '{') { - state.nextToken() - value = this.parseObject(state); - } else if (token.includes('.')) { - const objectPath = token.split('.'); - let objectValue = state.variables as any - for (const path of objectPath) { - if (typeof objectValue !== 'object' || objectValue === null || !objectValue.hasOwnProperty(path)) { - - throw new Error('Invalid object path'); - } else { - objectValue = objectValue[path]; - } - } - - value = objectValue; - state.nextToken(); - } else if (state.variables.hasOwnProperty(token)) { - value = state.variables[token]; - state.nextToken(); - } else if (state.functions.hasOwnProperty(token)) { - value = this.parseFunction(state); - } else if (state.operators.hasOwnProperty(token)) { - const operator = state.operators[token]; - state.nextToken(); - - const factor = this.parseFactor(state); - value = operator(0, factor); - } else { - console.log(token) - throw new Error('Invalid expression'); - } - - return value; - } - - private parseTerm(state: ParserState): number { - let value = this.parseUnaryFactor(state) as any; - while (true) { - const token = state.currentToken; - if (token === '*' || token === '/') { - const operator = token; - state.nextToken(); - const factor = this.parseUnaryFactor(state); - if (operator === '*') { - value *= factor as number; - } else { - value /= factor as number; - } - } else { - break; - } - } - - return value; - } - - private parseExpression(state: ParserState): any { - let value = this.parseTerm(state); - - while (true) { - const token = state.currentToken; - if (token in state.operators) { - const operator = token; - state.nextToken(); - const term = this.parseTerm(state); - value = state.operators[operator](value, term); - } else { - break; - } - } - - return value; - } - - public evaluate( - expression: string, - { - variables, - functions, - operators, - }: { - variables?: VariableMap, - functions?: FunctionMap, - operators?: OperatorMap, - } = {} - ): any { - const tempVariables = { ...this.variables, ...(variables || {}) }; - const tempFunctions = { ...this.functions, ...(functions || {}) }; - const tempOperators = { ...this.operators, ...(operators || {}) }; - - const state: ParserState = { - tokens: this.tokenize(expression), - currentTokenIndex: 0, - get currentToken() { - return this.tokens[this.currentTokenIndex]; - }, - nextToken() { - this.currentTokenIndex++; - }, - variables: tempVariables, - functions: tempFunctions, - operators: tempOperators - }; - - const result = this.parseExpression(state); - - if (state.currentToken !== undefined) { - throw new Error('Invalid expression'); - } - - return result; - } -} - -export default ExpressionParser; \ No newline at end of file +export * from './ExpressionParser'; +export * from './createParser'; \ No newline at end of file diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 1662baf..d890bc3 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -1,8 +1,8 @@ -import ExpressionParser, { FunctionMap } from '..' +import { createParser, FunctionMap } from '../' -describe('Example', () => { +describe('example', () => { it('basic operator', () => { - const parser = new ExpressionParser() + const parser = createParser() expect(parser.evaluate('(2 + 3) * 4 - 4')).toBe(16) expect(parser.evaluate('-4 + 5')).toBe(1) expect(parser.evaluate('4 <= (5 + 2)')).toBe(true) @@ -17,36 +17,37 @@ describe('Example', () => { // Usage example const variables = { x: 5 }; const functions: FunctionMap = { - ADD: (_, a: number, b: number) => a + b, - LENGTH: (_, str: string) => str.length, - LENGTH_ALL: (_, str1: string, str2: string, str3: string) => [str1.length, str2.length, str3.length], + add: (_, a: number, b: number) => a + b, + length: (_, str: string) => str.length, + length_all: (_, str1: string, str2: string, str3: string) => [str1.length, str2.length, str3.length], }; - const parser = new ExpressionParser({ variables, functions }); - expect(parser.evaluate('ADD(1 + 1, 5) + x')).toBe(12) - expect(parser.evaluate('LENGTH("ADI") + 5', { functions, variables },)).toBe(8) - expect(parser.evaluate('LENGTH_ALL("ADI", "FA", "TK")', { variables, functions })).toEqual([3, 2, 2]) + const parser = createParser({ variables }); + parser.setFunctions(functions) + expect(parser.evaluate('add(1 + 1, 5) + x')).toBe(12) + expect(parser.evaluate('length("ADI") + 5', variables)).toBe(8) + expect(parser.evaluate('length_all("ADI", "FA", "TK")', variables)).toEqual([3, 2, 2]) }); it('string', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate('"ADI"')).toBe("ADI") expect(parser.evaluate('\'ADI\'')).toBe("ADI") }) it('boolean', () => { - const parser = new ExpressionParser(); - expect(parser.evaluate('true AND false')).toBe(false) - expect(parser.evaluate('true OR false')).toBe(true) + const parser = createParser(); + expect(parser.evaluate('true and false')).toBe(false) + expect(parser.evaluate('true or false')).toBe(true) expect(parser.evaluate('!true')).toBe(false) expect(parser.evaluate('!!true')).toBe(true) }) it('array', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate("[1, 2, 3, 4]")).toEqual([1, 2, 3, 4]) expect(parser.evaluate("[\"2\", 5]")).toEqual(["2", 5]) expect(parser.evaluate("[2 + 5, 5]")).toEqual([7, 5]) - expect(parser.evaluate("[5, x]", { variables: { x: 2 } })).toEqual([5, 2]) + expect(parser.evaluate("[5, x]", { x: 2 })).toEqual([5, 2]) }) it('array method', () => { - const parser = new ExpressionParser(); + const parser = createParser(); const products = [ { name: 'Product 1', price: 150, quantity: 2 }, { name: 'Product 2', price: 80, quantity: 0 }, @@ -54,10 +55,8 @@ describe('Example', () => { { name: 'Product 4', price: 120, quantity: 1 } ]; expect( - parser.evaluate('FILTER(products, "__ITEM__.price > 100 AND __ITEM__.quantity > 0")', { - variables: { - products - } + parser.evaluate('filter(products, "_item_.price > 100 and _item_.quantity > 0")', { + products }) ).toEqual([ { name: 'Product 1', price: 150, quantity: 2 }, @@ -66,10 +65,8 @@ describe('Example', () => { ]) expect( - parser.evaluate('MAP(products, "__ITEM__.name")', { - variables: { - products - } + parser.evaluate('map(products, "_item_.name")', { + products }) ).toEqual([ 'Product 1', @@ -79,10 +76,8 @@ describe('Example', () => { ]) expect( - parser.evaluate('FIND(products, "__ITEM__.price > 0")', { - variables: { - products - } + parser.evaluate('find(products, "_item_.price > 0")', { + products }) ).toEqual({ "name": "Product 1", @@ -91,23 +86,19 @@ describe('Example', () => { }) expect( - parser.evaluate('SOME(products, "__ITEM__.price == 200")', { - variables: { - products - } + parser.evaluate('some(products, "_item_.price == 200")', { + products }) ).toBe(true) expect( - parser.evaluate('REDUCE(products, "__CURR__ + __ITEM__.price", 0)', { - variables: { - products - } + parser.evaluate('reduce(products, "_curr_ + _item_.price", 0)', { + products }) ).toBe(550) }) it('object', () => { - const parser = new ExpressionParser(); + const parser = createParser(); expect(parser.evaluate("{ name: 'ADI', age: 20 }")).toEqual({ name: "ADI", age: 20 @@ -116,9 +107,9 @@ describe('Example', () => { name: "ADI", age: 7 }) - expect(parser.evaluate("object.name", { variables: { object: { name: 'ADI' } } })).toEqual('ADI') - expect(parser.evaluate("object.name", { variables: { object: { name: 'ADI' } } })).toEqual('ADI') - expect(parser.evaluate("object.0.name", { variables: { object: [{ name: 'ADI' }] } })).toEqual('ADI') - expect(parser.evaluate("object.0.object.0.name", { variables: { object: [{ name: 'ADI', object: [{ name: "ADI" }] }] } })).toEqual('ADI') + expect(parser.evaluate("object.name", { object: { name: 'ADI' } })).toEqual('ADI') + expect(parser.evaluate("object.name", { object: { name: 'ADI' } })).toEqual('ADI') + expect(parser.evaluate("object.0.name", { object: [{ name: 'ADI' }] })).toEqual('ADI') + expect(parser.evaluate("object.0.object.0.name", { object: [{ name: 'ADI', object: [{ name: "ADI" }] }] })).toEqual('ADI') }) }); \ No newline at end of file