Skip to content

Commit

Permalink
Merge pull request #88 from fkleon/feature-implicit-multiplication
Browse files Browse the repository at this point in the history
Parser: Add options to configure parser behaviour
  • Loading branch information
fkleon authored Jul 31, 2024
2 parents 35992ba + f196cbc commit 4ae1eec
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 4ae1eec

Please sign in to comment.