Skip to content

Commit

Permalink
Parser: Add options to configure parser behaviour
Browse files Browse the repository at this point in the history
Move multiplyWithParentheses flag into options and rename to
implicitMultiplication.

Add lexer tests for implicit multiplication.
  • Loading branch information
fkleon committed Jul 31, 2024
1 parent 35992ba commit f196cbc
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 28 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 19 additions & 13 deletions lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ 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<String, dynamic> functionHandlers = <String, dynamic>{};

/// 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.');
}

final List<Expression> exprStack = <Expression>[];
final List<Token> inputStream = lex.tokenizeToRPN(
inputString,
multiplyWithParentheses: multiplyWithParentheses,
);

for (Token currToken in inputStream) {
Expand Down Expand Up @@ -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).
Expand All @@ -173,14 +180,16 @@ class Parser {
class Lexer {
final Map<String, TokenType> keywords = <String, TokenType>{};

final ParserOptions options;

/// Buffer for numbers
String intBuffer = '';

/// Buffer for variable and function names
String varBuffer = '';

/// Creates a new lexer.
Lexer() {
Lexer([this.options = const ParserOptions()]) {
keywords['+'] = TokenType.PLUS;
keywords['-'] = TokenType.MINUS;
keywords['*'] = TokenType.TIMES;
Expand Down Expand Up @@ -212,8 +221,7 @@ class Lexer {

/// Tokenizes a given input string.
/// Returns a list of [Token] in infix notation.
List<Token> tokenize(String inputString,
{bool multiplyWithParentheses = false}) {
List<Token> tokenize(String inputString) {
final List<Token> tempTokenStream = <Token>[];
final String clearedString = inputString.replaceAll(' ', '').trim();
final RuneIterator iter = clearedString.runes.iterator;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Token> tokenizeToRPN(String inputString,
{bool multiplyWithParentheses = false}) {
List<Token> tokenizeToRPN(String inputString) {
final List<Token> infixStream = tokenize(
inputString,
multiplyWithParentheses: multiplyWithParentheses,
);
return shuntingYard(infixStream);
}
Expand Down
56 changes: 50 additions & 6 deletions test/lexer_test_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class LexerTests extends TestSet {
'Power': tokenizePower,
'Modulo': tokenizeModulo,
'Multiplication': tokenizeMultiplication,
'ImplicitMultiplication': tokenizeImplicitMultiplication,
'Division': tokenizeDivision,
'Plus': tokenizePlus,
'Minus': tokenizeMinus,
Expand All @@ -39,7 +40,7 @@ class LexerTests extends TestSet {
@override
void initTests() {}

Lexer lex = Lexer();
final Lexer lex = Lexer();

// Test RPN
void parameterizedRpn(Map<String, List<Token>> cases) {
Expand All @@ -50,14 +51,18 @@ class LexerTests extends TestSet {
}

/// Test infix and RPN
void parameterized(Map<String, (List<Token>, List<Token>)> cases) {
void parameterized(Map<String, (List<Token> infix, List<Token> 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");
});
});
}
Expand Down Expand Up @@ -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': (
Expand Down
14 changes: 7 additions & 7 deletions test/parser_test_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ParserTests extends TestSet {
'Power': parsePower,
'Modulo': parseModulo,
'Multiplication': parseMultiplication,
'MultiplicationWithParentheses': parseMultiplicationWithParentheses,
'ImplicitMultiplication': parseImplicitMultiplication,
'Division': parseDivision,
'Addition': parsePlus,
'Subtraction': parseMinus,
Expand All @@ -50,16 +50,15 @@ class ParserTests extends TestSet {

Parser parser = Parser();

void parameterized(Map<String, Expression> cases,
{bool multiplyWithParentheses = false}) {
void parameterized(Map<String, Expression> cases, {Parser? parser}) {
parser ??= this.parser;
cases.forEach((key, value) {
test(
'$key -> $value',
() => expect(
parser
parser!
.parse(
key,
multiplyWithParentheses: multiplyWithParentheses,
)
.toString(),
value.toString()));
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit f196cbc

Please sign in to comment.