Как разобрать строку в std :: map и проверить ее формат? - PullRequest
2 голосов
/ 20 июня 2019

Я бы хотел разобрать строку типа "{{0, 1}, {2, 3}}" в std::map. Я могу написать небольшую функцию для разбора строки, используя библиотеку <regex>, но я не знаю, как проверить, находится ли данная строка в допустимом формате. Как я могу проверить формат строки?

#include <list>
#include <map>
#include <regex>
#include <iostream>

void f(const std::string& s) {
  std::map<int, int> m;
  std::regex p {"[\\[\\{\\(](\\d+),\\s*(\\d+)[\\)\\}\\]]"};
  auto begin = std::sregex_iterator(s.begin(), s.end(), p);
  auto end = std::sregex_iterator();
  for (auto x = begin; x != end; ++x) {
    std::cout << x->str() << '\n';
    m[std::stoi(x->str(1))] = std::stoi(x->str(2));
  }
  std::cout << m.size() << '\n';
}

int main() {
  std::list<std::string> l {
    "{{0, 1},   (2,    3)}",
    "{{4,  5, {6, 7}}" // Ill-formed, so need to throw an excpetion.
  };
  for (auto x : l) {
    f(x);
  }
}

ПРИМЕЧАНИЕ: я не чувствую себя обязанным использовать regex для решения этой проблемы. Будут признательны любые решения, включая некоторые способы проверки и вставки сразу путем вычитания подстрок.

Ответы [ 5 ]

3 голосов
/ 21 июня 2019

По моему мнению, основанный на Spirit анализатор всегда намного более устойчив и читабелен. Также гораздо интереснее разбираться с Духом :-). Итак, в дополнение к ответу @ Aleph0, я хотел бы предоставить компактное решение на основе Spirit-X3 :

#include <string>
#include <map>
#include <iostream>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/spirit/home/x3.hpp>

int main() {
    std::string input ="{{0, 1},  {2, 3}}";
    using namespace boost::spirit::x3;
    const auto pair = '{' > int_ > ',' > int_ > '}';
    const auto pairs = '{' > (pair % ',')  > '}';
    std::map<int, int> output;
    // ignore spaces, tabs, newlines
    phrase_parse(input.begin(), input.end(), pairs, space, output);

    for (const auto [key, value] : output) {
        std::cout << key << ":" << value << std::endl;
    }
}

Обратите внимание, что я использовал оператор >, что означает «ожидать». Таким образом, если ввод не соответствует ожидаемому, Spirit генерирует исключение. Если вы предпочитаете тихий сбой, используйте оператор >>.

3 голосов
/ 21 июня 2019

Так как Хан упомянул в своих комментариях, что он хотел бы дождаться дальнейших идей, я покажу дополнительное решение.

И, как и все ранее, я думаю, что это наиболее подходящее решение: -)

Кроме того, я распакую "большой молоток" и поговорю о "языках" и "грамматике" иО, Хомский Иерачи.

Сначала очень простой ответ: чистые регулярные выражения не могут рассчитывать.Таким образом, они не могут проверять совпадающие фигурные скобки, например, 3 открытых и 3 закрытых.

Они в основном реализованы как DFA (детерминированный конечный автомат), также известный как FSA (конечный автомат).Одним из важных свойств здесь является то, что они знают только о своем текущем состоянии.Они не могут «помнить» предыдущие состояния.У них нет памяти.

Языки, которые они могут создавать, являются так называемыми "обычными языками".В иерархии Хомского грамматика для создания такого регулярного языка относится к типу 3.И «регулярные выражения» могут быть использованы для создания таких языков.

Однако существуют расширения для регулярных выражений, которые также можно использовать для сопоставления сбалансированных фигурных скобок.См. Здесь: Регулярное выражение для соответствия сбалансированным скобкам

Но это не регулярное выражение в соответствии с исходным определением.

Что нам действительно нужно, так это Chomsky-Type-2 грамматики.Так называемая контекстно-свободная грамматика.И это обычно будет реализовано с помощью pushdown-автомата.Стек используется для хранения дополнительного состояния.Это та «память», которой нет в регулярных выражениях.

Итак, если мы хотим проверить синтаксис данного выражения, как в вашем случае вход для std :: map, мы можем определитьочень простая грамматика и синтаксический анализ входной строки с использованием стандартного классического подхода: синтаксический анализатор сдвига / уменьшения.

Необходимо выполнить несколько шагов: сначала входной поток будет разбит на лексемы и токены.Обычно это делается так называемым Lexer или сканером.Вы всегда найдете функцию вроде getNextToken или аналогичную.Тогда жетоны будут перемещены в стек.Stack Top будет сопоставлен с продукцией в грамматике.Если есть совпадение с правой стороной производства, элементы в стеке будут заменены нетерминалом на левой стороне производства.Эта процедура будет повторяться до тех пор, пока не будет нажата начальная буква грамматики (что означает, что все в порядке) или не будет найдена синтаксическая ошибка.

Относительно вашего вопроса:

Какразобрать строку в std :: map и проверить ее формат?

Я бы разделил ее на 2 задачи.

  1. Разобрать строку для проверки формата
  2. Если строка верна, поместите данные в карту

Задача 2 простая и обычно однострочная с использованием std :: istream_iterator.

Задача 1, к сожалениюнужен сдвиг-уменьшение-парсер.Это немного сложно.

В прилагаемом коде ниже я покажу одно возможное решение.Обратите внимание: это можно оптимизировать с помощью токена с атрибутами.Атрибутами будут целое число и тип фигурной скобки.Токен с атрибутами будет храниться в стеке разбора.С этим мы могли бы устранить необходимость иметь продукцию для всех видов фигурных скобок, и мы могли бы заполнить карту в синтаксическом анализаторе (в операции сокращения одного из «{Token :: Pair, {Token :: B1open, Token :: Integer,Token :: Comma, Token :: Integer, Token :: B1close}} ”

Пожалуйста, смотрите код ниже:

#include <iostream>
#include <iterator>
#include <sstream>
#include <map>
#include <vector>
#include <algorithm>

// Tokens:  Terminals and None-Terminals
enum class Token { Pair, PairList, End, OK, Integer, Comma, B1open, B1close, B2open, B2close, B3open, B3close };

// Production type for Grammar
struct Production { Token nonTerminal; std::vector<Token> rightSide; };

// The Context Free Grammar CFG
std::vector<Production>    grammar
{
       {Token::OK, { Token::B1open, Token::PairList, Token::B1close } },
       {Token::OK, { Token::B2open, Token::PairList, Token::B2close } },
       {Token::OK, { Token::B3open, Token::PairList, Token::B3close } },
       {Token::PairList, { Token::PairList, Token::Comma, Token::Pair}    },
       {Token::PairList, { Token::Pair } },
       {Token::Pair, { Token::B1open, Token::Integer, Token::Comma, Token::Integer, Token::B1close} },
       {Token::Pair, { Token::B2open, Token::Integer, Token::Comma, Token::Integer, Token::B2close} },
       {Token::Pair, { Token::B3open, Token::Integer, Token::Comma, Token::Integer, Token::B3close} }
};
// Helper for translating brace characters to Tokens
std::map<const char, Token> braceToToken{
 {'(',Token::B1open},{'[',Token::B2open},{'{',Token::B3open},{')',Token::B1close},{']',Token::B2close},{'}',Token::B3close},
};

// A classical    SHIFT - REDUCE  Parser
class Parser
{
public:
    Parser() : parseString(), parseStringPos(parseString.begin()) {}
    bool parse(const std::string& inputString);
protected:
    // String to be parsed
    std::string parseString{}; std::string::iterator parseStringPos{}; // Iterator for input string

    // The parse stack for the Shift Reduce Parser
    std::vector<Token> parseStack{};

    // Parser Step 1:   LEXER    (lexical analysis / scanner)
    Token getNextToken();
    // Parser Step 2:   SHIFT
    void shift(Token token) { parseStack.push_back(token); }
    // Parser Step 3:   MATCH / REDUCE
    bool matchAndReduce();
};

bool Parser::parse(const std::string& inputString)
{
    parseString = inputString; parseStringPos = parseString.begin(); parseStack.clear();
    Token token{ Token::End };
    do   // Read tokens untils end of string
    {
        token = getNextToken();     // Parser Step 1:   LEXER    (lexical analysis / scanner)                    
        shift(token);               // Parser Step 2:   SHIFT
        while (matchAndReduce())    // Parser Step 3:   MATCH / REDUCE
            ; // Empty body
    } while (token != Token::End);  // Do until end of string reached
    return (!parseStack.empty() && parseStack[0] == Token::OK);
}

Token Parser::getNextToken()
{
    Token token{ Token::End };
    // Eat all white spaces
    while ((parseStringPos != parseString.end()) && std::isspace(static_cast<int>(*parseStringPos))) {
        ++parseStringPos;
    }
    // Check for end of string
    if (parseStringPos == parseString.end()) {
        token = Token::End;
    }
    // Handle digits
    else if (std::isdigit(static_cast<int>(*parseStringPos))) {
        while ((((parseStringPos + 1) != parseString.end()) && std::isdigit(static_cast<int>(*(parseStringPos + 1)))))        ++parseStringPos;
        token = Token::Integer;
    }
    // Detect a comma
    else if (*parseStringPos == ',') {
        token = Token::Comma;
        // Else search for all kind of braces
    }
    else {
        std::map<const char, Token>::iterator foundBrace = braceToToken.find(*parseStringPos);
        if (foundBrace != braceToToken.end()) token = foundBrace->second;
    }
    // In next function invocation the next string element will be checked
    if (parseStringPos != parseString.end())
        ++parseStringPos;

    return token;
}


bool Parser::matchAndReduce()
{
    bool result{ false };
    // Iterate over all productions in the grammar
    for (const Production& production : grammar) {
        if (production.rightSide.size() <= parseStack.size()) {
            // If enough elements on the stack, match the top of the stack with a production
            if (std::equal(production.rightSide.begin(), production.rightSide.end(), parseStack.end() - production.rightSide.size())) {
                // Found production: Reduce
                parseStack.resize(parseStack.size() - production.rightSide.size());
                // Replace right side of production with left side
                parseStack.push_back(production.nonTerminal);
                result = true;
                break;
            }
        }
    }
    return result;
}

using IntMap = std::map<int, int>;
using IntPair = std::pair<int, int>;

namespace std {
    istream& operator >> (istream& is, IntPair& intPair)    {
        return is >> intPair.first >> intPair.second;
    }
    ostream& operator << (ostream& os, const pair<const int, int>& intPair) {
        return os << intPair.first << " --> " << intPair.second;
    }
}

int main()
{   // Test Data. Test Vector with different strings to test
    std::vector <std::string> testVector{
        "({10, 1 1},   (2,  3) , [5 ,6])",
        "({10, 1},   (2,  3) , [5 ,6])",
        "({10, 1})",
        "{10,1}"
    };
    // Define the Parser
    Parser parser{};
    for (std::string& test : testVector)
    {   // Give some nice info to the user
        std::cout << "\nChecking '" << test << "'\n";
        // Parse the test string and test, if it is valid
        bool inputStringIsValid = parser.parse(test);
        if (inputStringIsValid) {               // String is valid. Delete everything but digits
            std::replace_if(test.begin(), test.end(), [](const char c) {return !std::isdigit(static_cast<int>(c)); }, ' ');
            std::istringstream iss(test);       // Copy string with digits int a istringstream, so that we can read with istream_iterator
            IntMap intMap{ std::istream_iterator<IntPair>(iss),std::istream_iterator<IntPair>() };
            // Present the resulting data in the map to the user
            std::copy(intMap.begin(), intMap.end(), std::ostream_iterator<IntPair>(std::cout, "\n"));
        } else {
            std::cerr << "***** Invalid input data\n";
        }
    }
    return 0;
}

Я надеюсь, что это не слишком сложно. Но это«математическое» правильное решение. Веселитесь.

3 голосов
/ 20 июня 2019

Это может быть слишком много, но если у вас есть boost в ваших руках, вы можете использовать boost-spirit, чтобы выполнить работу за вас. Преимущество может состоять в том, что решение легко расширяется для анализа других типов карт, например, std::map<std::string, int>.

Еще одно преимущество, которое нельзя недооценивать, заключается в том, что boost-spirit оставляет вас с нормальными исключениями в случае, если строка не удовлетворяет вашей грамматике. Это довольно сложно достичь с помощью рукописного решения.

Место, где происходит ошибка, также определяется boost-spirit, чтобы вы могли вернуться к этому месту.

#include <map>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_stl.hpp>
#include <boost/fusion/adapted/std_pair.hpp>

template <typename Iterator, typename Skipper>
struct mapLiteral : boost::spirit::qi::grammar<Iterator, std::map<int,int>(), Skipper>
{
    mapLiteral() : mapLiteral::base_type(map)
    {
        namespace qi = boost::spirit::qi;
        using qi::lit;

        map = (lit("{") >> pair >> *(lit(",") >> pair) >> lit("}"))|(lit("{") >> lit("}"));
        pair = (lit("{") >> boost::spirit::int_ >> lit(",") >> boost::spirit::int_ >> lit("}"));
    }

    boost::spirit::qi::rule<Iterator, std::map<int, int>(), Skipper> map;
    boost::spirit::qi::rule<Iterator, std::pair<int, int>(), Skipper> pair;
};

std::map<int,int> parse(const std::string& expression, bool& ok)
{
    std::map<int, int>  result;
    try {
        std::string formula = expression;
        boost::spirit::qi::space_type space;
        mapLiteral<std::string::const_iterator, decltype(space)> parser;
        auto b = formula.begin();
        auto e = formula.end();
        ok = boost::spirit::qi::phrase_parse(b, e, parser, space, result);
        if (b != e) {
            ok = false;
            return std::map<int, int>();
        }
        return result;
    }
    catch (const boost::spirit::qi::expectation_failure<std::string::iterator>&) {
        ok = false;
        return result;
    }
}


int main(int argc, char** args)
{
    std::vector<std::pair<std::map<int, int>,std::string>> tests = {
        {{ },"{  \t\n}"},
        {{{5,2},{2,1}},"{ {5,2},{2,1} }"},
        {{},"{{2, 6}{}}"} // Bad food
    };
    for (auto iter :tests)
    {
        bool ok;
        auto result = parse(iter.second, ok);
        if (result == iter.first)
        {
            std::cout << "Equal:" << std::endl;
        }
    }
}
1 голос
/ 20 июня 2019

Вы можете проверять свои строки, проверяя только круглые скобки, как, например, это не очень эффективно, так как всегда повторяет каждую строку, но может быть оптимизировано.

#include <list>
#include <iostream>
#include <string>

bool validate(std::string s)
{
    std::list<char> parens;
    for (auto c : s) {
        if (c == '(' || c == '[' || c == '{') {
            parens.push_back(c);
        }

        if (c == ')' && parens.back() == '(') {
            parens.pop_back();
        } else if (c == ']' && parens.back() == '[') {
            parens.pop_back();
        } else if (c == '}' && parens.back() == '{') {
            parens.pop_back();
        }
    }
    return parens.size() == 0;
}


int main()
{
  std::list<std::string> l {
    "{{0, 1},   (2,    3)}",
    "{{4,  5, {6, 7}}" // Ill-formed, so need to throw an excpetion.
  };

  for (auto s : l) {
      std::cout << "'" << s << "' is " << (validate(s) ? "" : "not ") << "valid" << std::endl;
  }

  return 0;
}

Вывод вышеприведенного кода такой:

'{{0, 1},   (2,    3)}' is valid
'{{4,  5, {6, 7}}' is notvalid

EDIT:

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

bool validate(std::string s)
{
    std::list<char> parens;
    for (auto c : s) {
        if (c == '(' || c == '[' || c == '{') {
            parens.push_back(c);
        }

        if (c == ')') {
            if (parens.back() != '(') {
                return false;
            }
            parens.pop_back();
        } else if (c == ']') {
            if (parens.back() != '[') {
                return false;
            }
            parens.pop_back();
        } else if (c == '}') {
            if (parens.back() != '{') {
                return false;
            }
            parens.pop_back();
        }
    }
    return parens.size() == 0;
}
0 голосов
/ 20 июня 2019

Ваше регулярное выражение отлично разбирает один элемент карты. Я предлагаю вам проверить строку перед созданием карты и заполнением ее проанализированными элементами.

Давайте использовать немного улучшенную версию вашего регулярного выражения:

[\[\{\(](([\[\{\(](\d+),(\s*)(\d+)[\)\}\]])(,?)(\s*))*[\)\}\]]

Соответствует всей строке, если она действительна: она начинается с [\[\{\(], заканчивается [\)\}\]], содержит несколько (или нулевой) шаблон элемента карты внутри, за которым следует , и несколько (или ноль) пробелов.

Вот код:

#include <list>
#include <map>
#include <regex>
#include <sstream>
#include <iostream>

void f(const std::string& s) {
  // part 1: validate string
  std::regex valid_pattern {"[\\[\\{\\(](([\\[\\{\\(](\\d+),(\\s*)(\\d+)[\\)\\}\\]])(,?)(\\s*))*[\\)\\}\\]]"};
  auto valid_begin = std::sregex_iterator(s.begin(), s.end(), valid_pattern);
  auto valid_end = std::sregex_iterator();
  if (valid_begin == valid_end || valid_begin->str().size() != s.size ()) {
    std::stringstream res;
    res << "String \"" << s << "\" doesn't satisfy pattern!";
    throw std::invalid_argument (res.str ());
  } else {
    std::cout << "String \"" << s << "\" satisfies pattern!" << std::endl;
  }

  // part 2: parse map elements
  std::map<int, int> m;
  std::regex pattern {"[\\[\\{\\(](\\d+),\\s*(\\d+)[\\)\\}\\]]"};
  auto parsed_begin = std::sregex_iterator(s.begin(), s.end(), pattern);
  auto parsed_end = std::sregex_iterator();
  for (auto x = parsed_begin; x != parsed_end; ++x) {
    m[std::stoi(x->str(1))] = std::stoi(x->str(2));
  }

  std::cout << "Number of parsed elements: " << m.size() << '\n';
}

int main() {
  std::list<std::string> l {
      "{}",
      "[]",
      "{{0, 153}, (2, 3)}",
      "{{0,      153},   (2,    3)}",
      "{[0, 153],           (2, 3), [154, 33]   }",
      "{[0, 153],           (2, 3), [154, 33]   ", // Ill-formed, so need to throw an exception.
      "{{4,  5, {6, 7}}", // Ill-formed, so need to throw an exception.
      "{{4,  5, {x, 7}}" // Ill-formed, so need to throw an exception.
  };
  for (const auto &x : l) {
    try {
      f(x);
    }
    catch (std::invalid_argument &ex) {
      std::cout << ex.what () << std::endl;
    }
    std::cout << std::endl;
  }
}

Вот вывод:

String "{}" satisfies pattern!
Number of parsed elements: 0

String "[]" satisfies pattern!
Number of parsed elements: 0

String "{{0, 153}, (2, 3)}" satisfies pattern!
Number of parsed elements: 2

String "{{0,      153},   (2,    3)}" satisfies pattern!
Number of parsed elements: 2

String "{[0, 153],           (2, 3), [154, 33]   }" satisfies pattern!
Number of parsed elements: 3

String "{[0, 153],           (2, 3), [154, 33]   " doesn't satisfy pattern!

String "{{4,  5, {6, 7}}" doesn't satisfy pattern!

String "{{4,  5, {x, 7}}" doesn't satisfy pattern!

PS У него только один дефект. Он не проверяет, что соответствующая закрывающая скобка равна открывающей. Это соответствует следующему: {], {(1,2]) и т. Д. Если это не подходит для вас, самый простой способ исправить это - добавить дополнительный проверочный код перед помещением проанализированной пары в карту.

PPS Если вы можете избежать регулярных выражений, ваша проблема может быть решена гораздо эффективнее с помощью сканирования одной строки для каждой строки. @SilvanoCerza предложила реализацию для этого случая.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...