Пишем простой интерпретатор на C++ с помощью TDD, часть 3
В первой части был написан лексер, а во второй части — парсер. Далее будет рассмотрена разработка вычислителя и фасада для всего интерпретатора, а также рефакторинг всего кода для устранения дублирования.ВычислительПриступим к самому интересному. Вычисление выражения в постфиксной записи можно осуществить двумя способами: через рекурсию, неявно используя стек процесса, или используя явный стек. Реализуем второй вариант. Алгоритм с использованием явного стека такой: Если на вход подан операнд, он помещается на вершину стека.
Если на вход подан знак операции, то соответствующая операция выполняется над требуемым количеством значений, извлечённых из стека, взятых в порядке добавления. Результат выполненной операции кладётся на вершину стека.
После полной обработки входного набора символов результат вычисления выражения лежит на вершине стека.
В данной статье я не буду реализовывать контекст выполнения и вычисление нескольких выражений. Поэтому начальный список тестов будет коротким: Если на входе пустой список, возвращаем 0.
Если на входе список с одним числом, возвращаем это число.
Если на входе [1 2 +], возвращаем 3.
Создадим новый тестовый класс и добавим первый тест.
TEST_CLASS (EvaluatorTests) {
public:
TEST_METHOD (Should_return_zero_when_evaluate_empty_list) {
double result = Evaluator: Evaluate ({});
Assert: AreEqual (0.0, result);
}
};
Привычно добавим пустую функцию в необходимое пространство имён.
namespace Evaluator {
inline double Evaluate (Tokens) { return 0; }
} // namespace Evaluator
Напишем второй тест.
TEST_METHOD (Should_return_number_when_evaluate_list_with_number) {
double result = Evaluator: Evaluate ({ _1 });
Assert: AreEqual (1.0, result);
}
Просто возвращаем, то что было в списке последним. Можно было бы поступить проще, но потом всё равно нужно будет добавлять цикл.
inline double Evaluate (const Tokens &tokens) {
double result = 0;
for (const Token &token: tokens) {
result = token;
}
return result;
}
Дальше — сложнее. Попробуем вычислить выражение с одним оператором.
TEST_METHOD (Should_eval_expression_with_one_operator) {
double result = Evaluator: Evaluate ({ _1, _2, plus });
Assert: AreEqual (3.0, result);
}
Для того, чтобы этот тест прошёл, добавим всего один символ к коду.
for (const Token &token: tokens) {
if (token.Type () == TokenType: Number) {
result += token;
}
}
Этого было достаточно для прохождения теста. Попробуем сломать данный алгоритм, вычислив результат умножения.
TEST_METHOD (Should_eval_expression_with_one_multiplication) {
double result = Evaluator: Evaluate ({ _2, _3, mul });
Assert: AreEqual (6.0, result);
}
Тест не проходит, так как у нас жёстко забито сложение. Необходима более сложная реализация, учитывающая тип оператора. Добавим ветвление и заменим переменную с результатом на контейнер.
inline double Evaluate (const Tokens &tokens) {
std: vector
class StackEvaluator { public: StackEvaluator (const Tokens &tokens) : m_current (tokens.cbegin ()), m_end (tokens.cend ()) {}
void Evaluate () { for (; m_current!= m_end; ++m_current) { EvaluateCurrentToken (); } }
double Result () const { return m_stack.empty () ? 0: m_stack.back (); }
private: void EvaluateCurrentToken () { switch (m_current→Type ()) { case TokenType: Operator: EvaluateOperator (); break; case TokenType: Number: EvaluateNumber (); break; default: throw std: out_of_range («TokenType»); } }
void EvaluateOperator () { double second = PopOperand (); double first = PopOperand (); m_stack.push_back (BinaryFunctionFor (*m_current)(first, second)); }
void EvaluateNumber () { m_stack.push_back (*m_current); }
double PopOperand () { double back = m_stack.back (); m_stack.pop_back (); return back; }
static const std: function
Tokens: const_iterator m_current, m_end;
std: vector
} // namespace Detail Интерпретатор Для упрощения работы со стороны клиента, напишем небольшую функцию, являющуюся фасадом для всего интерпретатора. Добавим для начала пару интеграционных тестов. TEST_CLASS (InterpreterIntegrationTests) { public: TEST_METHOD (Should_interprete_empty_experssion) { double result = Interpreter: InterpreteExperssion (L» »); Assert: AreEqual (0.0, result); }
TEST_METHOD (Should_interprete_experssion) { double result = Interpreter: InterpreteExperssion (L»1+2»); Assert: AreEqual (3.0, result); } }; Напишем реализацию функции InterpreteExperssion в уже существующем пространстве имён Interpreter. inline double InterpreteExperssion (const std: wstring &expression) { return Evaluator: Evaluate (Parser: Parse (Lexer: Tokenize (expression))); } Ура, тесты проходят, значит все части интерпретатора взаимодействуют так, как и было запланировано.Рефакторинг Теперь, когда весь код покрыт тестами, можно посмотреть, есть ли дублирование в глобальном масштабе и устранить его. Сразу же бросается в глаза куча одинаковых операторов switch, выполняющих проверку на тип токена. Да и в самом токене одновременно хранится как число, так и оператор. Для того, чтобы уйти от условных операторов в каждом методе, воспользуемся шаблоном посетитель. Создадим абстрактный класс TokenVisitor: struct TokenVisitor { virtual void VisitNumber (double) {} virtual void VisitOperator (Operator) {}
protected: ~TokenVisitor () {} }; Для простоты, виртуальные методы будут иметь пустую реализацию по умолчанию. Для безопасности, объявим деструктор как защищённый и не виртуальный, чтобы предотвратить возможность удаления через указатель на этот класс. Добавим в токен метод, принимающий посетителя и перенесём в него злополучный switch. void Accept (TokenVisitor &visitor) const { switch (m_type) { case TokenType: Operator: visitor.VisitOperator (m_operator); break; case TokenType: Number: visitor.VisitNumber (m_number); break; default: throw std: out_of_range («TokenType»); } } Теперь посмотрим на класс ShuntingYardParser и его метод ParseCurrentToken. void ParseCurrentToken () { switch (m_current→Type ()) { case TokenType: Operator: ParseOperator (); break; case TokenType: Number: ParseNumber (); break; default: throw std: out_of_range («TokenType»); } } Сходство очевидно. Унаследуем это класс от абстрактного посетителя и переименуем методы Parse* в Visit*. После этого класс изрядно похудеет, а метод Parse примет такой вид: void Parse () { for (; m_current!= m_end; ++m_current) { m_current→Accept (*this); } PopToOutputUntil ([this]() {return StackHasNoOperators (); }); } Аналогично поступим с классом StackEvaluator. class StackEvaluator: TokenVisitor { public: void Evaluate (const Tokens &tokens) { for (const Token &token: tokens) { token.Accept (*this); } } … void VisitOperator (Operator op) override { double second = PopOperand (); double first = PopOperand (); m_stack.push_back (BinaryFunctionFor (op)(first, second)); }
void VisitNumber (double number) override { m_stack.push_back (number); } }; Можно было бы вообще не использовать наследование и виртуальные функции, заменив всё шаблонами, но тогда потеряется всякая поддержка со стороны IDE и придётся рассчитывать только на неявные соглашения. Теперь разберёмся с оставшимися операторами switch и union. Тут может пригодиться паттерн состояние, который, кстати, неявно использует std: function. Поступим по аналогии. Создадим закрытый абстрактный класс TokenConcept внутри класса токена, в котором будут располагаться операции, зависящие от типа токена. Конкретная реализация концепта будет храниться в std: shared_ptr, так как токен является неизменяемым, то делать состояние разделяемым совершенно безопасно. struct TokenConcept { virtual ~TokenConcept () {} virtual void Accept (TokenVisitor &) const = 0; virtual std: wstring ToString () const = 0; virtual bool Equals (const TokenConcept &) const = 0; virtual TokenType Type () const = 0; virtual double ToNumber () const { throw std: logic_error («Invalid token type»); } virtual Operator ToOperator () const { throw std: logic_error («Invalid token type»); } }; Не будем пока что избавляться от TokenType полностью, чтобы не ломать тесты. Теперь добавим реализации для числа и оператора, после чего заменим логику в методах токена на обращение к концепту.Класс Token после рефакторинга class Token { struct TokenConcept { virtual ~TokenConcept () {}
virtual void Accept (TokenVisitor &) const = 0;
virtual std: wstring ToString () const = 0;
virtual bool Equals (const TokenConcept &) const = 0;
virtual TokenType Type () const = 0;
virtual double ToNumber () const { throw std: logic_error («Invalid token type»); }
virtual Operator ToOperator () const { throw std: logic_error («Invalid token type»); } };
struct NumberToken: TokenConcept { NumberToken (double val) : m_number (val) {}
void Accept (TokenVisitor &visitor) const override { visitor.VisitNumber (m_number); }
std: wstring ToString () const override { return std: to_wstring (m_number); }
bool Equals (const TokenConcept &other) const override { return other.Type () == Type () && other.ToNumber () == m_number; }
TokenType Type () const override { return TokenType: Number; }
double ToNumber () const override { return m_number; }
private: double m_number; };
struct OperatorToken: TokenConcept { OperatorToken (Operator val) : m_operator (val) {}
void Accept (TokenVisitor &visitor) const override { visitor.VisitOperator (m_operator); }
std: wstring ToString () const override { return Interpreter: ToString (m_operator); }
bool Equals (const TokenConcept &other) const override { return other.Type () == Type () && other.ToOperator () == m_operator; }
TokenType Type () const override { return TokenType: Operator; }
Operator ToOperator () const override { return m_operator; }
private: Operator m_operator; };
public:
Token (Operator val) : m_concept (std: make_shared
Token (double val) : m_concept (std: make_shared
TokenType Type () const { return m_concept→Type (); }
void Accept (TokenVisitor &visitor) const { m_concept→Accept (visitor); }
operator Operator () const { return m_concept→ToOperator (); }
operator double () const { return m_concept→ToNumber (); }
friend inline bool operator==(const Token &left, const Token &right) { return left.m_concept→Equals (*right.m_concept); }
friend inline std: wstring ToString (const Token &token) { return token.m_concept→ToString (); }
private:
std: shared_ptr
TEST_METHOD (Should_check_for_equality_number_tokens) { Assert: AreEqual (_1, _1); Assert: AreNotEqual (_1, _2); Assert: AreNotEqual (_1, minus); } После удаления перечисления возникает проблема сравнения токенов разных типов. RTTY использовать не хочется, поэтому немного изменим базовый класс TokenConcept, добавив поддержку двойной диспетчеризации для операции Equals. struct TokenConcept { … virtual bool Equals (const TokenConcept &other) const = 0; virtual bool EqualsToNumber (double) const { return false; } virtual bool EqualsToOperator (Operator) const { return false; } };
struct NumberToken: TokenConcept { … bool EqualsToNumber (double value) const override { return value == m_number; } bool Equals (const TokenConcept &other) const { return other.EqualsToNumber (m_number); } };
struct OperatorToken: TokenConcept { … bool EqualsToOperator (Operator value) const override { return value == m_operator; } bool Equals (const TokenConcept &other) const { return other.EqualsToOperator (m_operator); } }; Производные классы в методе Equals выполняют первый шаг диспетчеризации для определения типа первого токена, после чего второй токен уже производит сравнение с конкретным типом токена. Токены разных типов не равны по умолчанию, поэтому производным классам нужно переопределить только один метод, принимающий аргумент подходящего типа.Заключение В этой статье я попытался отойти от привычной тематики материалов по TDD, сосредоточенных больше на теории и углубиться в практическое применение данной техники. Как было показано, даже на C++ можно вести разработку через тестирование без особых затруднений. Тем более, что начать это делать легко, учитывая наличие в Visual Studio встроенной поддержки тестов для C++ проектов. Конечно, для написания более сложных систем требуется и более сложные библиотеки, такие как Boost.Test, Google.Test, или CppUTest, а также библиотеки для создание mock-объектов, такие как Google.Mock, или Turtle. Да и не все сценарии можно протестировать только с помощью модульных тестов. Но, тем не менее, написание модульных тестов и разработка через тестирование заметно помогают в написании кода, упрощают его модификацию в будущем и придают уверенность в том, что всё работает именно так, как было запланировано.При наличии интереса у читателей я могу написать вторую часть в подобном стиле, в которой будет описана реализация оставшихся пунктов из списка в начале первой части этой статьи.
Ресурсы
Ниже приведён весь код в конечном варианте: Interpreter.h
#pragma once;
#include
namespace Interpreter {
enum class Operator: wchar_t { Plus = L'+', Minus = L'-', Mul = L'*', Div = L'/', LParen = L'(', RParen = L')', };
inline std: wstring ToString (const Operator &op) {
return{ static_cast
struct TokenVisitor { virtual void VisitNumber (double) {}
virtual void VisitOperator (Operator) {}
protected: ~TokenVisitor () {} };
class Token { struct TokenConcept { virtual ~TokenConcept () {}
virtual void Accept (TokenVisitor &) const = 0;
virtual std: wstring ToString () const = 0;
virtual bool Equals (const TokenConcept &other) const = 0;
virtual bool EqualsToNumber (double) const { return false; }
virtual bool EqualsToOperator (Operator) const { return false; }
virtual double ToNumber () const { throw std: logic_error («Invalid token type»); }
virtual Operator ToOperator () const { throw std: logic_error («Invalid token type»); } };
struct NumberToken: TokenConcept { NumberToken (double val) : m_number (val) {}
void Accept (TokenVisitor &visitor) const override { visitor.VisitNumber (m_number); }
std: wstring ToString () const override { return std: to_wstring (m_number); }
bool EqualsToNumber (double value) const override { return value == m_number; }
bool Equals (const TokenConcept &other) const { return other.EqualsToNumber (m_number); }
double ToNumber () const override { return m_number; }
private: double m_number; };
struct OperatorToken: TokenConcept { OperatorToken (Operator val) : m_operator (val) {}
void Accept (TokenVisitor &visitor) const override { visitor.VisitOperator (m_operator); }
std: wstring ToString () const override { return Interpreter: ToString (m_operator); }
bool EqualsToOperator (Operator value) const override { return value == m_operator; }
bool Equals (const TokenConcept &other) const { return other.EqualsToOperator (m_operator); }
Operator ToOperator () const override { return m_operator; }
private: Operator m_operator; };
public:
Token (Operator val) : m_concept (std: make_shared
Token (double val) : m_concept (std: make_shared
void Accept (TokenVisitor &visitor) const { m_concept→Accept (visitor); }
operator Operator () const { return m_concept→ToOperator (); }
operator double () const { return m_concept→ToNumber (); }
friend inline bool operator==(const Token &left, const Token &right) { return left.m_concept→Equals (*right.m_concept); }
friend inline std: wstring ToString (const Token &token) { return token.m_concept→ToString (); }
private:
std: shared_ptr
typedef std: vector
namespace Lexer {
namespace Detail {
class Tokenizer { public: Tokenizer (const std: wstring &expr) : m_current (expr.c_str ()) {}
void Tokenize () { while (! EndOfExperssion ()) { if (IsNumber ()) { ScanNumber (); } else if (IsOperator ()) { ScanOperator (); } else { MoveNext (); } } }
const Tokens &Result () const { return m_result; }
private: bool EndOfExperssion () const { return *m_current == L'\0'; }
bool IsNumber () const { return iswdigit (*m_current) != 0; }
void ScanNumber () { wchar_t *end = nullptr; m_result.push_back (wcstod (m_current, &end)); m_current = end; }
bool IsOperator () const {
auto all = { Operator: Plus, Operator: Minus, Operator: Mul, Operator: Div, Operator: LParen, Operator: RParen };
return std: any_of (all.begin (), all.end (), [this](Operator o) {return *m_current == static_cast
void ScanOperator () {
m_result.push_back (static_cast
void MoveNext () { ++m_current; }
const wchar_t *m_current; Tokens m_result; };
} // namespace Detail
inline Tokens Tokenize (const std: wstring &expr) { Detail: Tokenizer tokenizer (expr); tokenizer.Tokenize (); return tokenizer.Result (); }
} // namespace Lexer
namespace Parser {
inline int PrecedenceOf (Operator op) { return (op == Operator: Mul || op == Operator: Div) ? 1: 0; }
namespace Detail {
class ShuntingYardParser: TokenVisitor { public: void Parse (const Tokens &tokens) { for (const Token &token: tokens) { token.Accept (*this); } PopToOutputUntil ([this]() {return StackHasNoOperators (); }); }
const Tokens &Result () const { return m_output; }
private: void VisitOperator (Operator op) override { switch (op) { case Operator: LParen: PushCurrentToStack (op); break; case Operator: RParen: PopToOutputUntil ([this]() { return LeftParenOnTop (); }); PopLeftParen (); break; default: PopToOutputUntil ([&]() { return LeftParenOnTop () || OperatorWithLessPrecedenceOnTop (op); }); PushCurrentToStack (op); break; } }
void VisitNumber (double number) override { m_output.emplace_back (number); }
bool StackHasNoOperators () const { if (m_stack.back () == Token (Operator: LParen)) { throw std: logic_error («Closing paren not found.»); } return false; }
void PushCurrentToStack (Operator op) { return m_stack.emplace_back (op); }
void PopLeftParen () { if (m_stack.empty () || m_stack.back () != Operator: LParen) { throw std: logic_error («Opening paren not found.»); } m_stack.pop_back (); }
bool OperatorWithLessPrecedenceOnTop (Operator op) const { return PrecedenceOf (m_stack.back ()) < PrecedenceOf(op); }
bool LeftParenOnTop () const {
return static_cast
template
Tokens m_output, m_stack; };
} // namespace Detail
inline Tokens Parse (const Tokens &tokens) { Detail: ShuntingYardParser parser; parser.Parse (tokens); return parser.Result (); }
} // namespace Parser
namespace Evaluator {
namespace Detail {
class StackEvaluator: TokenVisitor { public: void Evaluate (const Tokens &tokens) { for (const Token &token: tokens) { token.Accept (*this); } }
double Result () const { return m_stack.empty () ? 0: m_stack.back (); }
private: void VisitOperator (Operator op) override { double second = PopOperand (); double first = PopOperand (); m_stack.push_back (BinaryFunctionFor (op)(first, second)); }
void VisitNumber (double number) override { m_stack.push_back (number); }
double PopOperand () { double back = m_stack.back (); m_stack.pop_back (); return back; }
static const std: function
std: vector
} // namespace Detail
inline double Evaluate (const Tokens &tokens) { Detail: StackEvaluator evaluator; evaluator.Evaluate (tokens); return evaluator.Result (); }
} // namespace Evaluator
inline double InterpreteExperssion (const std: wstring &expression) { return Evaluator: Evaluate (Parser: Parse (Lexer: Tokenize (expression))); }
} // namespace Interpreter InterpreterTests.cpp #include «stdafx.h» #include «CppUnitTest.h» #include «Interpreter.h» #pragma warning (disable: 4505)
namespace InterpreterTests { using namespace Microsoft: VisualStudio: CppUnitTestFramework; using namespace Interpreter; using namespace std;
namespace AssertRange {
template
Assert: AreEqual (distance (expectIter, end (expect)), distance (actualIter, end (actual)), L«Size differs.»);
for (; expectIter!= end (expect) && actualIter!= end (actual); ++expectIter, ++actualIter) {
auto message = L«Mismatch in position » + to_wstring (distance (begin (expect), expectIter));
Assert: AreEqual
} // namespace AssertRange
static const Token plus (Operator: Plus), minus (Operator: Minus); static const Token mul (Operator: Mul), div (Operator: Div); static const Token pLeft (Operator: LParen), pRight (Operator: RParen); static const Token _1(1), _2(2), _3(3), _4(4), _5(5);
TEST_CLASS (LexerTests) { public: TEST_METHOD (Should_return_empty_token_list_when_put_empty_expression) { Tokens tokens = Lexer: Tokenize (L»); Assert: IsTrue (tokens.empty ()); }
TEST_METHOD (Should_tokenize_single_plus_operator) { Tokens tokens = Lexer: Tokenize (L»+»); AssertRange: AreEqual ({ plus }, tokens); }
TEST_METHOD (Should_tokenize_single_digit) { Tokens tokens = Lexer: Tokenize (L»1»); AssertRange: AreEqual ({ _1 }, tokens); }
TEST_METHOD (Should_tokenize_floating_point_number) { Tokens tokens = Lexer: Tokenize (L»12.34»); AssertRange: AreEqual ({ 12.34 }, tokens); }
TEST_METHOD (Should_tokenize_plus_and_number) { Tokens tokens = Lexer: Tokenize (L»+12.34»); AssertRange: AreEqual ({ plus, Token (12.34) }, tokens); }
TEST_METHOD (Should_skip_spaces) { Tokens tokens = Lexer: Tokenize (L» 1 + 12.34 »); AssertRange: AreEqual ({ _1, plus, Token (12.34) }, tokens); }
TEST_METHOD (Should_tokenize_complex_experssion) { Tokens tokens = Lexer: Tokenize (L»1+2×3/(4–5)»); AssertRange: AreEqual ({ _1, plus, _2, mul, _3, div, pLeft, _4, minus, _5, pRight }, tokens); } };
TEST_CLASS (TokenTests) { public: TEST_METHOD (Should_check_for_equality_operator_tokens) { Assert: AreEqual (minus, minus); Assert: AreNotEqual (minus, plus); Assert: AreNotEqual (minus, _1); }
TEST_METHOD (Should_check_for_equality_number_tokens) { Assert: AreEqual (_1, _1); Assert: AreNotEqual (_1, _2); Assert: AreNotEqual (_1, minus); }
TEST_METHOD (Should_get_operator_code_from_operator_token) {
Token token (Operator: Plus);
Assert: AreEqual
TEST_METHOD (Should_get_number_value_from_number_token) {
Token token (1.23);
Assert: AreEqual
TEST_CLASS (ParserTests) { public: TEST_METHOD (Should_return_empty_list_when_put_empty_list) { Tokens tokens = Parser: Parse ({}); Assert: IsTrue (tokens.empty ()); }
TEST_METHOD (Should_parse_single_number) { Tokens tokens = Parser: Parse ({ _1 }); AssertRange: AreEqual ({ _1 }, tokens); }
TEST_METHOD (Should_parse_num_plus_num) { Tokens tokens = Parser: Parse ({ _1, plus, _2 }); AssertRange: AreEqual ({ _1, _2, plus }, tokens); }
TEST_METHOD (Should_parse_two_additions) { Tokens tokens = Parser: Parse ({ _1, plus, _2, plus, _3 }); AssertRange: AreEqual ({ _1, _2, plus, _3, plus }, tokens); }
TEST_METHOD (Should_get_same_precedence_for_operator_pairs) { Assert: AreEqual (Parser: PrecedenceOf (Operator: Plus), Parser: PrecedenceOf (Operator: Minus)); Assert: AreEqual (Parser: PrecedenceOf (Operator: Mul), Parser: PrecedenceOf (Operator: Div)); }
TEST_METHOD (Should_get_greater_precedence_for_multiplicative_operators) { Assert: IsTrue (Parser: PrecedenceOf (Operator: Mul) > Parser: PrecedenceOf (Operator: Plus)); }
TEST_METHOD (Should_parse_add_and_mul) { Tokens tokens = Parser: Parse ({ _1, plus, _2, mul, _3 }); AssertRange: AreEqual ({ _1, _2, _3, mul, plus }, tokens); }
TEST_METHOD (Should_parse_complex_experssion) { Tokens tokens = Parser: Parse ({ _1, plus, _2, div, _3, minus, _4, mul, _5 }); AssertRange: AreEqual ({ _1, _2, _3, div, plus, _4, _5, mul, minus }, tokens); }
TEST_METHOD (Should_skip_parens_around_number) { Tokens tokens = Parser: Parse ({ pLeft, _1, pRight }); AssertRange: AreEqual ({ _1 }, tokens); }
TEST_METHOD (Should_parse_expression_with_parens_in_beginning) { Tokens tokens = Parser: Parse ({ pLeft, _1, plus, _2, pRight, mul, _3 }); AssertRange: AreEqual ({ _1, _2, plus, _3, mul }, tokens); }
TEST_METHOD (Should_throw_when_opening_paren_not_found) {
Assert: ExpectException
TEST_METHOD (Should_throw_when_closing_paren_not_found) {
Assert: ExpectException
TEST_METHOD (Should_parse_complex_experssion_with_paren) { // (1+2)*(3/(4–5)) = 1 2 + 3 4 5 — / * Tokens tokens = Parser: Parse ({ pLeft, _1, plus, _2, pRight, mul, pLeft, _3, div, pLeft, _4, minus, _5, pRight, pRight }); AssertRange: AreEqual ({ _1, _2, plus, _3, _4, _5, minus, div, mul }, tokens); } };
TEST_CLASS (EvaluatorTests) { public: TEST_METHOD (Should_return_zero_when_evaluate_empty_list) { double result = Evaluator: Evaluate ({}); Assert: AreEqual (0.0, result); }
TEST_METHOD (Should_return_number_when_evaluate_list_with_number) { double result = Evaluator: Evaluate ({ _1 }); Assert: AreEqual (1.0, result); }
TEST_METHOD (Should_eval_expression_with_one_operator) { double result = Evaluator: Evaluate ({ _1, _2, plus }); Assert: AreEqual (3.0, result); }
TEST_METHOD (Should_eval_expression_with_one_multiplication) { double result = Evaluator: Evaluate ({ _2, _3, mul }); Assert: AreEqual (6.0, result); }
TEST_METHOD (Should_eval_expression_with_one_subtraction) { double result = Evaluator: Evaluate ({ _2, _3, minus }); Assert: AreEqual (-1.0, result); }
TEST_METHOD (Should_eval_expression_with_one_division) { double result = Evaluator: Evaluate ({ _5, _2, div }); Assert: AreEqual (2.5, result); }
TEST_METHOD (Should_eval_complex_expression) { // (4+1)*2/(4/(3–1)) = 4 1 + 2×4 3 1 — / / = 5 double result = Evaluator: Evaluate ({ _4, _1, plus, _2, mul, _4, _3, _1, minus, div, div }); Assert: AreEqual (5.0, result); } };
TEST_CLASS (InterpreterIntegrationTests) { public: TEST_METHOD (Should_interprete_empty_experssion) { double result = Interpreter: InterpreteExperssion (L» »); Assert: AreEqual (0.0, result); }
TEST_METHOD (Should_interprete_experssion) { double result = Interpreter: InterpreteExperssion (L»1+2»); Assert: AreEqual (3.0, result); } };
} Код к статье на GitHub. Названия коммитов и их порядок соответствуют тестам и рефакторингам, описанным выше.Более подробное описание алгоритма сортировочной станции на PEGWiki.
Советую к прочтению книгу Jeff Langr. Modern C++ Programming with Test-Driven Development. — The Pragmatic Programmers, 2013.