From f196cbc03935c104c122bbe69514d61a55459efa Mon Sep 17 00:00:00 2001 From: Frederik Leonhardt Date: Wed, 31 Jul 2024 22:06:36 +1200 Subject: [PATCH] Parser: Add options to configure parser behaviour Move multiplyWithParentheses flag into options and rename to implicitMultiplication. Add lexer tests for implicit multiplication. --- CHANGELOG.md | 4 +-- lib/src/parser.dart | 32 +++++++++++++--------- test/lexer_test_set.dart | 56 ++++++++++++++++++++++++++++++++++----- test/parser_test_set.dart | 14 +++++----- 4 files changed, 78 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bda8fc..7353390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.5.1] - 2024-05-23 +## [unreleased] - 2024-05-23 ### Added -- Add support for multiplying parentheses +- Add parser support for implicit multiplication (thanks [juca1331](https://github.com/juca1331)) ## [2.5.0] - 2024-04-16 diff --git a/lib/src/parser.dart b/lib/src/parser.dart index e524c19..fa11f8f 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -13,15 +13,16 @@ class Parser { final Lexer lex; /// Creates a new parser. - Parser() : lex = Lexer(); + /// The given [options] can be used to configure the behaviour. + Parser([ParserOptions options = const ParserOptions()]) + : lex = Lexer(options); Map functionHandlers = {}; - /// Parses the given input string into an [Expression]. If - /// [multiplyWithParentheses] is true you can multiply using - /// parentheses. Throws [ArgumentError] if the given [inputString] + /// Parses the given input string into an [Expression]. + /// Throws a [ArgumentError] if the given [inputString] /// is empty. Throws a [StateError] if the token stream is /// invalid. Returns a valid [Expression]. - Expression parse(String inputString, {bool multiplyWithParentheses = false}) { + Expression parse(String inputString) { if (inputString.trim().isEmpty) { throw FormatException('The given input string was empty.'); } @@ -29,7 +30,6 @@ class Parser { final List exprStack = []; final List inputStream = lex.tokenizeToRPN( inputString, - multiplyWithParentheses: multiplyWithParentheses, ); for (Token currToken in inputStream) { @@ -164,6 +164,13 @@ class Parser { } } +class ParserOptions { + /// If [implicitMultiplication] is true the parser will allow + /// implicit multiplication using parentheses. + final bool implicitMultiplication; + const ParserOptions({this.implicitMultiplication = false}); +} + /// The lexer creates tokens (see [TokenType] and [Token]) from an input string. /// The input string is expected to be in /// [infix notation form](https://en.wikipedia.org/wiki/Infix_notation). @@ -173,6 +180,8 @@ class Parser { class Lexer { final Map keywords = {}; + final ParserOptions options; + /// Buffer for numbers String intBuffer = ''; @@ -180,7 +189,7 @@ class Lexer { String varBuffer = ''; /// Creates a new lexer. - Lexer() { + Lexer([this.options = const ParserOptions()]) { keywords['+'] = TokenType.PLUS; keywords['-'] = TokenType.MINUS; keywords['*'] = TokenType.TIMES; @@ -212,8 +221,7 @@ class Lexer { /// Tokenizes a given input string. /// Returns a list of [Token] in infix notation. - List tokenize(String inputString, - {bool multiplyWithParentheses = false}) { + List tokenize(String inputString) { final List tempTokenStream = []; final String clearedString = inputString.replaceAll(' ', '').trim(); final RuneIterator iter = clearedString.runes.iterator; @@ -304,7 +312,7 @@ class Lexer { // There are no more symbols in the input string but there is still a variable or keyword in the varBuffer _doVarBuffer(tempTokenStream); } - if (multiplyWithParentheses) { + if (options.implicitMultiplication) { for (int i = 0; i < tempTokenStream.length; i++) { if (tempTokenStream[i].type == TokenType.RBRACE && i != tempTokenStream.length - 1) { @@ -491,11 +499,9 @@ class Lexer { /// This method invokes the createTokenStream methode to create an infix token /// stream and then invokes the shunting yard method to transform this stream /// into a RPN (reverse polish notation) token stream. - List tokenizeToRPN(String inputString, - {bool multiplyWithParentheses = false}) { + List tokenizeToRPN(String inputString) { final List infixStream = tokenize( inputString, - multiplyWithParentheses: multiplyWithParentheses, ); return shuntingYard(infixStream); } diff --git a/test/lexer_test_set.dart b/test/lexer_test_set.dart index d055eb9..d874801 100644 --- a/test/lexer_test_set.dart +++ b/test/lexer_test_set.dart @@ -21,6 +21,7 @@ class LexerTests extends TestSet { 'Power': tokenizePower, 'Modulo': tokenizeModulo, 'Multiplication': tokenizeMultiplication, + 'ImplicitMultiplication': tokenizeImplicitMultiplication, 'Division': tokenizeDivision, 'Plus': tokenizePlus, 'Minus': tokenizeMinus, @@ -39,7 +40,7 @@ class LexerTests extends TestSet { @override void initTests() {} - Lexer lex = Lexer(); + final Lexer lex = Lexer(); // Test RPN void parameterizedRpn(Map> cases) { @@ -50,14 +51,18 @@ class LexerTests extends TestSet { } /// Test infix and RPN - void parameterized(Map, List)> cases) { + void parameterized(Map infix, List rpn)> cases, + {Lexer? lexer}) { + lexer ??= this.lex; cases.forEach((expression, value) { var (infix, rpn) = value; test('$expression -> $infix -> $rpn', () { - var infixStream = lex.tokenize(expression); - expect(infixStream, orderedEquals(infix)); - var rpnStream = lex.shuntingYard(infixStream); - expect(rpnStream, orderedEquals(rpn)); + var infixStream = lexer!.tokenize(expression); + expect(infixStream, orderedEquals(infix), + reason: 'Incorrect infix notation'); + var rpnStream = lexer.shuntingYard(infixStream); + expect(rpnStream, orderedEquals(rpn), + reason: "Incorrect reverse polish notation"); }); }); } @@ -218,6 +223,45 @@ class LexerTests extends TestSet { parameterized(cases); } + void tokenizeImplicitMultiplication() { + var cases = { + '(0)(1)': ( + [ + Token('(', TokenType.LBRACE), + Token('0', TokenType.VAL), + Token(')', TokenType.RBRACE), + Token('*', TokenType.TIMES), + Token('(', TokenType.LBRACE), + Token('1', TokenType.VAL), + Token(')', TokenType.RBRACE), + ], + [ + Token('0', TokenType.VAL), + Token('1', TokenType.VAL), + Token('*', TokenType.TIMES) + ] + ), + '(-2.0)5': ( + [ + Token('(', TokenType.LBRACE), + Token('-', TokenType.MINUS), + Token('2.0', TokenType.VAL), + Token(')', TokenType.RBRACE), + Token('*', TokenType.TIMES), + Token('5', TokenType.VAL), + ], + [ + Token('2.0', TokenType.VAL), + Token('-', TokenType.UNMINUS), + Token('5', TokenType.VAL), + Token('*', TokenType.TIMES), + ] + ), + }; + var lexer = Lexer(ParserOptions(implicitMultiplication: true)); + parameterized(cases, lexer: lexer); + } + void tokenizeDivision() { var cases = { '0 / 1': ( diff --git a/test/parser_test_set.dart b/test/parser_test_set.dart index ea12b2e..56361e1 100644 --- a/test/parser_test_set.dart +++ b/test/parser_test_set.dart @@ -27,7 +27,7 @@ class ParserTests extends TestSet { 'Power': parsePower, 'Modulo': parseModulo, 'Multiplication': parseMultiplication, - 'MultiplicationWithParentheses': parseMultiplicationWithParentheses, + 'ImplicitMultiplication': parseImplicitMultiplication, 'Division': parseDivision, 'Addition': parsePlus, 'Subtraction': parseMinus, @@ -50,16 +50,15 @@ class ParserTests extends TestSet { Parser parser = Parser(); - void parameterized(Map cases, - {bool multiplyWithParentheses = false}) { + void parameterized(Map cases, {Parser? parser}) { + parser ??= this.parser; cases.forEach((key, value) { test( '$key -> $value', () => expect( - parser + parser! .parse( key, - multiplyWithParentheses: multiplyWithParentheses, ) .toString(), value.toString())); @@ -137,12 +136,13 @@ class ParserTests extends TestSet { parameterized(cases); } - void parseMultiplicationWithParentheses() { + void parseImplicitMultiplication() { var cases = { '(5)(5)': Number(5) * Number(5), '(-2.0)5': -Number(2.0) * Number(5), }; - parameterized(cases, multiplyWithParentheses: true); + var parser = Parser(ParserOptions(implicitMultiplication: true)); + parameterized(cases, parser: parser); } void parseDivision() {