Пишем простой интерпретатор на 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<double> result{ 0 };     auto pop = [&]() { double d = result.back(); result.pop_back(); return d; };     for(const Token &token : tokens) {         if(token.Type() == TokenType::Number) {             result.push_back(token);         }         else if(token == Token(Operator::Plus)) {             result.push_back(pop() + pop());         }         else if(token == Token(Operator::Mul)) {             result.push_back(pop() * pop());         }     }     return pop(); } 

Заметим, что данный алгоритм уже может выполнять и более сложные вычисления. Сделаем тест оператора минус. Его сложность в том, что можно перепутать местами аргументы, извлекая их из стека.

    TEST_METHOD(Should_eval_expression_with_one_subtraction) {         double result = Evaluator::Evaluate({ _2, _3, minus });         Assert::AreEqual(-1.0, result);     } 

Вообще, согласно стандарту C++, нельзя делать какие-либо предположения относительно порядка вычисления аргументов функции. Поэтому извлекать операнды из стека нужно в явной последовательности.

        …         else if(token == Token(Operator::Minus)) {             double d = pop();             result.push_back(pop() - d);         } 

Аналогичный тест для деления.

    TEST_METHOD(Should_eval_expression_with_one_division) {         double result = Evaluator::Evaluate({ _5, _2, div });         Assert::AreEqual(2.5, result);     } 

В начале я написал тест на деление, используя выражение 4/2=2, и он сразу же прошёл, несмотря на то, что операция деления не была добавлена. Это произошло по той причине, что из стека извлекалось последнее добавленное в него число, которое, по совпадению, оказалось равно результату выражения. Именно поэтому тесты сразу после написания должны падать, иначе есть большая вероятность того, что тест ничего на самом деле не тестирует.

        …         else if(token == Token(Operator::Div)) {             double d = pop();             result.push_back(pop() / d);         } 

Чтобы удостовериться, что всё работает так как надо, добавим последний тест на вычисление сложного выражения.

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

Он проходит, но так и было задумано. В нашей функции довольно много дублирования. Вынесем её в отдельный класс и отрефакторим.

inline double Evaluate(const Tokens &tokens) {     Detail::StackEvaluator evaluator(tokens);     evaluator.Evaluate();     return evaluator.Result(); } 

Класс Detail::StackEvaluator

namespace Detail {  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<double(double, double)> &BinaryFunctionFor(Operator op) {         static const std::map<Operator, std::function<double(double, double)>> functions{                 { Operator::Plus, std::plus<double>() },                 { Operator::Minus, std::minus<double>() },                 { Operator::Mul, std::multiplies<double>() },                 { Operator::Div, std::divides<double>() },         };         auto found = functions.find(op);         if(found == functions.cend()) {             throw std::logic_error("Operator not found.");         }         return found->second;     }      Tokens::const_iterator m_current, m_end;     std::vector<double> m_stack; };  } // 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<OperatorToken>(val)) {}      Token(double val) : m_concept(std::make_shared<NumberToken>(val)) {}      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<const TokenConcept> m_concept; }; 

Заметим, что ни один тест в течение рефакторинга не сломался, хотя вид кода изменился довольно значительно. Пойдём дальше и удалим перечисление TokenType, так как оно используется только в классе Token. Перед этим изменим тесты Should_get_type_for_operator_token и Should_get_type_for_number_token так, чтобы они не использовали тип токена, немного подкорректировав их семантику.

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

После удаления перечисления возникает проблема сравнения токенов разных типов. 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 <vector> #include <wchar.h> #include <algorithm> #include <functional> #include <map> #include <memory>  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<wchar_t>(op) }; }  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<OperatorToken>(val)) {}      Token(double val) : m_concept(std::make_shared<NumberToken>(val)) {}      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<const TokenConcept> m_concept; };  typedef std::vector<Token> Tokens;   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<wchar_t>(o); });     }      void ScanOperator() {         m_result.push_back(static_cast<Operator>(*m_current));         MoveNext();     }      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<Operator>(m_stack.back()) == Operator::LParen;     }      template<class T>     void PopToOutputUntil(T whenToEnd) {         while(!m_stack.empty() && !whenToEnd()) {             m_output.push_back(m_stack.back());             m_stack.pop_back();         }     }      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<double(double, double)> &BinaryFunctionFor(Operator op) {         static const std::map<Operator, std::function<double(double, double)>> functions{                 { Operator::Plus, std::plus<double>() },                 { Operator::Minus, std::minus<double>() },                 { Operator::Mul, std::multiplies<double>() },                 { Operator::Div, std::divides<double>() },         };         auto found = functions.find(op);         if(found == functions.cend()) {             throw std::logic_error("Operator not found.");         }         return found->second;     }      std::vector<double> m_stack; };  } // 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<class T, class ActualRange> static void AreEqual(initializer_list<T> expect, const ActualRange &actual) {     auto actualIter = begin(actual);     auto expectIter = begin(expect);      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<T>(*expectIter, *actualIter, message.c_str());     } }  } // 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<Operator>(Operator::Plus, token);     }      TEST_METHOD(Should_get_number_value_from_number_token) {         Token token(1.23);         Assert::AreEqual<double>(1.23, token);     } };   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<std::logic_error>([]() {Parser::Parse({ _1, pRight }); });     }      TEST_METHOD(Should_throw_when_closing_paren_not_found) {         Assert::ExpectException<std::logic_error>([]() {Parser::Parse({ pLeft, _1 }); });     }      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.

ссылка на оригинал статьи http://habrahabr.ru/post/232097/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *