Skip to content

Commit

Permalink
fix: add array to expression
Browse files Browse the repository at this point in the history
  • Loading branch information
Adi Fatkhurozi committed Jun 10, 2023
1 parent 5724940 commit 68c84cf
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 40 deletions.
89 changes: 52 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type VariableMap = { [key: string]: number | string | boolean };
export type VariableMap = { [key: string]: number | string | boolean | any[] };
export type FunctionMap = { [key: string]: (...args: (any)[]) => any };
export type OperatorMap = { [key: string]: (a: any, b: any) => any };

Expand All @@ -10,7 +10,7 @@ type OwnState = {
variables: VariableMap;
functions: FunctionMap;
operators: OperatorMap;
}
};

class ExpressionParser {
private variables: VariableMap;
Expand All @@ -30,6 +30,7 @@ class ExpressionParser {
'-': (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,
Expand All @@ -39,12 +40,12 @@ class ExpressionParser {
'==': (a, b) => a === b,
'!=': (a, b) => a !== b,
'^': (a, b) => Math.pow(a, b),
// '%': (a, b) => a % b,
...operators,
};
}

private tokenize(expression: string): string[] {
const regex = /([-+*/(),<>!=])|\b(?:\d+(\.\d+)?)|(?:"[^"]*")|(?:\w+)/g;
const regex = /([-+*/(),<>!=%^\[\]])|\b(?:\d+(\.\d+)?)|(?:"[^"]*")|(?:\w+)/g;
return expression.match(regex) || [];
}

Expand All @@ -54,7 +55,7 @@ class ExpressionParser {
throw new Error('Invalid expression');
}

ownState.nextToken()
ownState.nextToken();
return parseFloat(token);
}

Expand All @@ -64,7 +65,7 @@ class ExpressionParser {
throw new Error('Invalid expression');
}

ownState.nextToken()
ownState.nextToken();
return token.slice(1, -1);
}

Expand All @@ -74,66 +75,87 @@ class ExpressionParser {
throw new Error('Invalid expression');
}

ownState.nextToken()
ownState.nextToken();
return token === 'true';
}


private parseFactor(ownState: OwnState): number | string | boolean {
let value: number | string | boolean = 0;
private parseFactor(ownState: OwnState): number | string | boolean | any[] {
let value: number | string | boolean | any[] = 0;
const token = ownState.currentToken;

if (token === undefined) {
throw new Error('Invalid expression');
}

if (token === '(') {
ownState.nextToken()
ownState.nextToken();
value = this.parseExpression(ownState);

if (ownState.currentToken !== ')') {
throw new Error('Invalid expression');
}

ownState.nextToken()
ownState.nextToken();
} else if (!isNaN(Number(token))) {
value = this.parseNumber(ownState);
} else if (token.startsWith('"') && token.endsWith('"')) {
value = this.parseString(ownState);
} else if (token === 'true' || token === 'false') {
value = this.parseBoolean(ownState);
} else if (token === '[') {
ownState.nextToken();
const array: any[] = [];

while (ownState.currentToken !== ']') {
array.push(this.parseExpression(ownState));

if (ownState.currentToken === ',') {
ownState.nextToken();
}
}

if (ownState.currentToken !== ']') {
throw new Error('Invalid expression');
}

ownState.nextToken();
value = array;
} else if (ownState.variables.hasOwnProperty(token)) {
value = ownState.variables[token];
ownState.nextToken()
ownState.nextToken();
} else if (ownState.functions.hasOwnProperty(token)) {
const func = ownState.functions[token];
ownState.nextToken()
ownState.nextToken();

if (ownState.currentToken !== '(') {
throw new Error('Invalid expression');
}

ownState.nextToken()
ownState.nextToken();

const args: (number | string | boolean)[] = [];
const args: (number | string | boolean | any[])[] = [];
while (ownState.currentToken as any !== ')') {
args.push(this.parseExpression(ownState));

if (ownState.currentToken as any === ',') {
ownState.nextToken()
ownState.nextToken();
}
}

ownState.nextToken()
if (ownState.currentToken as any !== ')') {
throw new Error('Invalid expression');
}

ownState.nextToken();

value = func(...args);
} else if (ownState.operators.hasOwnProperty(token)) {
const operator = ownState.operators[token];
ownState.nextToken()
ownState.nextToken();

const factor = this.parseFactor(ownState);

value = operator(0, factor as number | boolean);
value = operator(0, factor);
} else {
throw new Error('Invalid expression');
}
Expand All @@ -143,12 +165,12 @@ class ExpressionParser {

private parseTerm(ownState: OwnState): number {
let value = this.parseFactor(ownState) as number;

while (true) {
const token = ownState.currentToken;
console.log(token, value)
if (token === '*' || token === '/') {
const operator = token;
ownState.nextToken()
ownState.nextToken();
const factor = this.parseFactor(ownState);
if (operator === '*') {
value *= factor as number;
Expand All @@ -163,36 +185,30 @@ class ExpressionParser {
return value;
}

private parseExpression(ownState: OwnState): number | string | boolean {
private parseExpression(ownState: OwnState): any {
let value = this.parseTerm(ownState);

while (true) {
const token = ownState.currentToken as string;
if (token in ownState.operators) {
const operator = token;
ownState.nextToken()
ownState.nextToken();
const term = this.parseTerm(ownState);
if (operator === '+') {
value += term as number;
} else if (operator === '-') {
value -= term as number;
} else {
value = ownState.operators[operator as any](value, term);
}
value = ownState.operators[operator as any](value, term);
} else {
break;
}
}

return value as number | string | boolean;
return value;
}

public evaluate(
expression: string,
tempVariables?: VariableMap,
tempFunctions?: FunctionMap,
tempOperators?: OperatorMap,
): number | string | boolean {
): any {
const variables = { ...this.variables, ...(tempVariables || {}) };
const functions = { ...this.functions, ...(tempFunctions || {}) };
const operators = { ...this.operators, ...(tempOperators || {}) };
Expand All @@ -201,15 +217,15 @@ class ExpressionParser {
tokens: this.tokenize(expression),
currentTokenIndex: 0,
get currentToken() {
return this.tokens[this.currentTokenIndex]
return this.tokens[this.currentTokenIndex];
},
nextToken() {
this.currentTokenIndex++;
},
variables,
functions,
operators
}
};

const result = this.parseExpression(ownState);

Expand All @@ -221,5 +237,4 @@ class ExpressionParser {
}
}


export default ExpressionParser
export default ExpressionParser;
14 changes: 11 additions & 3 deletions src/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ describe('Example', () => {
const parser = new ExpressionParser()
expect(parser.evaluate('(2 + 3) * 4 - 4')).toBe(16)
expect(parser.evaluate('-4 + 5')).toBe(1)
expect(parser.evaluate('4 < 5')).toBe(true)
expect(parser.evaluate('4 < (5 + 2)')).toBe(true)
expect(parser.evaluate('4 > 5')).toBe(false)
expect(parser.evaluate('5 + 4 * 5')).toBe(25)
expect(parser.evaluate('5 ^ 2 + 2')).toBe(27)
expect(parser.evaluate('5 + 4 * 4')).toBe(21)
expect(parser.evaluate('5 % 3')).toBe(2)
});
it('basic infix operator exp', () => {
const parser = new ExpressionParser()
Expand All @@ -19,7 +21,7 @@ describe('Example', () => {
ADD: (a: number, b: number) => a + b,
};
const parser = new ExpressionParser(variables, functions);
const result = parser.evaluate('ADD(2, 5) + x');
const result = parser.evaluate('ADD(1 + 1, 5) + x');
expect(result).toBe(12)
});
it('string', () => {
Expand All @@ -37,4 +39,10 @@ describe('Example', () => {
const result = parser.evaluate('true AND false');
expect(result).toBe(false)
})
it('array', () => {
const parser = new ExpressionParser();
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])
})
});

0 comments on commit 68c84cf

Please sign in to comment.