wip: parser

This commit is contained in:
2025-02-05 16:59:40 +01:00
parent 857a252fea
commit 69edf86b63
2 changed files with 332 additions and 31 deletions

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
import { calc } from './solution';
import { calc, tokenize } from './solution';
import { expect } from "chai";
var tests: [string, number][] = [
@@ -13,12 +13,102 @@ var tests: [string, number][] = [
['2 / (2 + 3) * 4.33 - -6', 7.732],
];
describe("calc", function() {
describe("calc", function () {
it("should evaluate correctly", () => {
tests.forEach(function(m) {
tests.forEach(function (m) {
var x = calc(m[0]);
var y = m[1];
expect(x).to.equal(y, 'Expected: "' + m[0] + '" to be ' + y + ' but got ' + x);
});
});
});
describe("tokenize", function () {
it("should tokenize correctly", () => {
expect(tokenize('1 + 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '+'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 - 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '-'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 * 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '*'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 / 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '/'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('-3.1415')).to.deep.equal([{
type: 'operator',
value: '-'
}, {
type: 'number',
value: '3'
}, {
type: 'decimal',
value: '.'
}, {
type: 'number',
value: '1415'
}]);
expect(tokenize('3.1415')).to.deep.equal([{
type: 'number',
value: '3'
}, {
type: 'decimal',
value: '.'
}, {
type: 'number',
value: '1415'
}]);
expect(tokenize('(3 + 5) / 3')).to.deep.equal([{
type: 'parenthesis',
value: '('
}, {
type: 'number',
value: '3'
}, {
type: 'operator',
value: '+'
}, {
type: 'number',
value: '5'
}, {
type: 'parenthesis',
value: ')'
}, {
type: 'operator',
value: '/'
}, {
type: 'number',
value: '3'
}]);
});
});