wip: parser
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
|
||||
|
||||
type TokenType = 'number' | 'operator' | 'parenthesis';
|
||||
type TokenValue = number | '+' | '-' | '*' | '/' | '(' | ')';
|
||||
type TokenType = 'number' | 'operator' | 'parenthesis' | 'decimal';
|
||||
type TokenValue = number | '+' | '-' | '*' | '/' | '(' | ')' | '.';
|
||||
|
||||
type Token = {
|
||||
type: TokenType,
|
||||
value: TokenValue,
|
||||
}
|
||||
type Token = NumberToken | OperatorToken | ParenthesisToken | DecimalToken;
|
||||
|
||||
type OperatorChars = '+' | '-' | '*' | '/';
|
||||
type OperatorToken = Token & {
|
||||
type OperatorToken = {
|
||||
type: 'operator',
|
||||
value: OperatorChars,
|
||||
}
|
||||
@@ -18,13 +15,13 @@ const isOperatorChar = (char: string): char is OperatorChars => {
|
||||
return char === '+' || char === '-' || char === '*' || char === '/';
|
||||
}
|
||||
|
||||
type NumberToken = Token & {
|
||||
type NumberToken = {
|
||||
type: 'number',
|
||||
value: number,
|
||||
value: string,
|
||||
}
|
||||
|
||||
type ParenthesisChars = '(' | ')';
|
||||
type ParenthesisToken = Token & {
|
||||
type ParenthesisToken = {
|
||||
type: 'parenthesis',
|
||||
value: ParenthesisChars,
|
||||
}
|
||||
@@ -33,7 +30,17 @@ const isParenthesisChar = (char: string): char is ParenthesisChars => {
|
||||
return char === '(' || char === ')';
|
||||
}
|
||||
|
||||
const tokenize = (expression: string): Token[] => {
|
||||
type DecimalChars = '.';
|
||||
type DecimalToken = {
|
||||
type: 'decimal',
|
||||
value: DecimalChars,
|
||||
}
|
||||
|
||||
const isDecimalChar = (char: string): char is DecimalChars => {
|
||||
return char === '.';
|
||||
}
|
||||
|
||||
export const tokenize = (expression: string): Token[] => {
|
||||
const tokens: Token[] = [];
|
||||
const chars = expression.split('');
|
||||
let done = false;
|
||||
@@ -64,14 +71,21 @@ const tokenize = (expression: string): Token[] => {
|
||||
// number
|
||||
if (char.match(/\d/)) {
|
||||
let number = '';
|
||||
while (chars[i].match(/\d/)) {
|
||||
while (chars[i] && chars[i].match(/\d/)) {
|
||||
number += chars[i];
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: 'number', value: parseInt(number) });
|
||||
} else {
|
||||
throw new Error('Invalid character');
|
||||
tokens.push({ type: 'number', value: number });
|
||||
continue;
|
||||
}
|
||||
// decimal
|
||||
if (isDecimalChar(char)) {
|
||||
tokens.push({ type: 'decimal', value: char });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error('Invalid character');
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
@@ -85,8 +99,8 @@ type ASTNode = NumberNode | OperatorNode | GroupNode | NegationNode;
|
||||
|
||||
class NumberNode extends ASTNodeCommon {
|
||||
private _value: number;
|
||||
type = 'number';
|
||||
constructor(public numValue: number) {
|
||||
type = 'numbernode';
|
||||
constructor(numValue: number) {
|
||||
super();
|
||||
this._value = numValue;
|
||||
}
|
||||
@@ -94,17 +108,25 @@ class NumberNode extends ASTNodeCommon {
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
static fromToken(token: NumberToken): NumberNode {
|
||||
return new NumberNode(parseFloat(token.value));
|
||||
}
|
||||
|
||||
static fromTokens(tokens: [NumberToken, DecimalToken, NumberToken]): NumberNode {
|
||||
return new NumberNode(parseFloat(tokens[0].value + '.' + tokens[2].value));
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorNode extends ASTNodeCommon {
|
||||
type = 'operator';
|
||||
type = 'operatornode';
|
||||
constructor(public operator: OperatorChars, public left: ASTNode, public right: ASTNode) {
|
||||
super();
|
||||
}
|
||||
|
||||
get value() {
|
||||
const left = this.left.value;
|
||||
const right = this.right.value;
|
||||
get value(): number {
|
||||
const left = this.left.value as number;
|
||||
const right = this.right.value as number;
|
||||
switch (this.operator) {
|
||||
case '+':
|
||||
return left + right;
|
||||
@@ -121,21 +143,210 @@ class OperatorNode extends ASTNodeCommon {
|
||||
}
|
||||
|
||||
class GroupNode extends ASTNodeCommon {
|
||||
type = 'group';
|
||||
|
||||
constructor(public value: ASTNode) {
|
||||
type = 'groupnode';
|
||||
|
||||
constructor(public child: ASTNode) {
|
||||
super();
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this.child.value as number;
|
||||
}
|
||||
|
||||
static fromTokens(tokens: (Token | ASTNode)[]): GroupNode {
|
||||
const node = parse(tokens);
|
||||
return new GroupNode(node);
|
||||
}
|
||||
}
|
||||
class NegationNode extends ASTNodeCommon {
|
||||
type = 'negationnode';
|
||||
|
||||
constructor(public child: NumberNode | GroupNode) {
|
||||
super();
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return -(this.child.value as number);
|
||||
}
|
||||
}
|
||||
|
||||
type NegationNode = {
|
||||
type: 'negation',
|
||||
value: ASTNode,
|
||||
/**
|
||||
* Transforms number tokens into NumberNodes
|
||||
*/
|
||||
function parseNumbers(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type !== 'number') {
|
||||
i++;
|
||||
continue;
|
||||
};
|
||||
|
||||
// check if it is a decimal
|
||||
const nextToken = tokens[i + 1];
|
||||
const nextNextToken = tokens[i + 2];
|
||||
const nextTokenType = nextToken?.type;
|
||||
const nextNextTokenType = nextNextToken?.type;
|
||||
|
||||
if (nextTokenType === 'decimal' && nextNextTokenType === 'number') {
|
||||
// it is a decimal number
|
||||
const numberNode = NumberNode.fromTokens([
|
||||
token as NumberToken,
|
||||
nextToken as DecimalToken,
|
||||
nextNextToken as NumberToken
|
||||
]);
|
||||
tokens.splice(i, 3, numberNode);
|
||||
i += 3;
|
||||
continue;
|
||||
} else {
|
||||
// it is a regular number
|
||||
const numberNode = NumberNode.fromToken(token as NumberToken);
|
||||
tokens.splice(i, 1, numberNode);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches highest level parenthesis and replaces them with GroupNodes.
|
||||
* Only the highest level parenthesis are required because GroupNode will
|
||||
* recursively parse the rest of the expression.
|
||||
*/
|
||||
function parseGroups(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
|
||||
// match highest level parenthesis
|
||||
let i = 0;
|
||||
const parenthesisPairs: [number, number][] = [];
|
||||
const startParenthesis: number[] = [];
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type !== 'parenthesis') {
|
||||
i++;
|
||||
continue;
|
||||
};
|
||||
|
||||
// it is a parenthesis
|
||||
if (token.value === '(') {
|
||||
startParenthesis.push(i);
|
||||
} else {
|
||||
const start = startParenthesis.pop();
|
||||
if (startParenthesis.length === 0) {
|
||||
if (start === undefined) {
|
||||
throw new Error('Invalid parenthesis');
|
||||
}
|
||||
parenthesisPairs.push([start, i]);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// slice out parenthesis pairs and replace with GroupNode
|
||||
for (const [start, end] of parenthesisPairs) {
|
||||
const groupNode = GroupNode.fromTokens(tokens.slice(start + 1, end));
|
||||
tokens.splice(start, end - start + 1, groupNode);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function parseNegation(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type !== 'operator') {
|
||||
i++;
|
||||
continue;
|
||||
};
|
||||
|
||||
if (token.value === '-') {
|
||||
const prevToken = tokens[i - 1] as Token;
|
||||
if (!prevToken || isOperatorChar(prevToken.value)) {
|
||||
// it is a negation
|
||||
const child = tokens[i + 1] as NumberNode | GroupNode;
|
||||
const negationNode = new NegationNode(child);
|
||||
tokens.splice(i, 2, negationNode);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function parseMultDiv(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type !== 'operator') {
|
||||
i++;
|
||||
continue;
|
||||
};
|
||||
|
||||
if (token.value === '*' || token.value === '/') {
|
||||
const left = tokens[i - 1] as ASTNode;
|
||||
const right = tokens[i + 1] as ASTNode;
|
||||
const operator = token.value as OperatorChars;
|
||||
const operatorNode = new OperatorNode(operator, left, right);
|
||||
tokens.splice(i - 1, 3, operatorNode);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function parseAddSub(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type !== 'operator') {
|
||||
i++;
|
||||
continue;
|
||||
};
|
||||
|
||||
if (token.value === '+' || token.value === '-') {
|
||||
const left = tokens[i - 1] as ASTNode;
|
||||
const right = tokens[i + 1] as ASTNode;
|
||||
const operator = token.value as OperatorChars;
|
||||
const operatorNode = new OperatorNode(operator, left, right);
|
||||
tokens.splice(i - 1, 3, operatorNode);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function parse(tokens: (Token | ASTNode)[]): ASTNode {
|
||||
// array that we will save intermediate results in
|
||||
const transform = [...tokens];
|
||||
|
||||
// first pass: parse groups (parenthesis)
|
||||
parseGroups(transform);
|
||||
|
||||
// second pass: parse numbers and decimals
|
||||
parseNumbers(transform);
|
||||
|
||||
// third pass: negation
|
||||
parseNegation(transform);
|
||||
|
||||
// fourth pass: multiplication and division
|
||||
parseMultDiv(transform);
|
||||
|
||||
// fifth pass: addition and subtraction
|
||||
parseAddSub(transform);
|
||||
|
||||
console.log(JSON.stringify(transform[0], null, 2));
|
||||
return transform[0] as ASTNode;
|
||||
}
|
||||
|
||||
export function calc(expression: string): number {
|
||||
// evaluate `expression` and return result
|
||||
return 0;
|
||||
return parse(tokenize(expression)).value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user