суббота, 3 декабря 2016 г.

C++: Разбивка строки на токены с составными разделителями и сохранением токенов и разделителей в векторе

Известно множество решений данной задачи. 

Однако довольно часто разбивка строки требуется несколько в более сложном варианте. А именно, требуется использовать составные разделители (более, чем один символ или пара одинаковых символов), и требуется в последовательности токенов сохранять разделители, например, для последующей обработки с заменой в качестве placeholders ну и тому подобное.

Действительно, наиболее простое решение - использовать регулярные выражения. Обработка строк в стиле C слишком замысловата и, зачастую, не является общим решением, а частности образуют массу граничных случаев.

В этой связи библиотека регулярных выражений C++ достаточно эффективна.

Данный алгоритм является видоизменением хорошо известного опубликованного алгоритма.

Допустим, что у нас есть вот такая строка:

 Quick#1brown#2fox#3jump#4over#5lazy#6white#7rabbid  

в которой разделителем является сочетание символа # и цифр от 0 до 99.

Для решения вышеописанной задачи модифицируем алгоритм с использованием std::sregex_token_iterator, сохраняющий токены в vector:

 #include <iostream>  
 #include <vector>  
 #include <string>  
 #include <regex>  
   
 // Split string with save separators  
 std::vector<std::string> split(const std::string & s, std::string rgx_str = "\\#([\\d][\\d]?)+")  
 {  
      std::vector<std::string> elems;  
      std::regex rgx (rgx_str);  
   
      std::sregex_token_iterator iter(s.begin(), s.end(), rgx, -1);     // Get token  
      std::sregex_token_iterator iter1(s.begin(), s.end(), rgx, 0);     // Get separator  
      std::sregex_token_iterator end;  
   
      while (iter != end) {  
           elems.push_back(*iter);               // Put token  
           if (iter1 != end) {  
                elems.push_back(*iter1);     // Put separator if found  
                ++iter1;  
           }  
      ++iter;  
      }  
      return elems;  
 }  
   
 int main() {  
   std::string test_string = "Quick#1brown#2fox#3jump#4over#5lazy#6white#7rabbid";  
   std::cout << test_string << std::endl;  
   
   std::vector<std::string> tokens = split(test_string);  
   for (std::vector<std::string>::const_iterator it = tokens.begin();   
     it != tokens.end(); it++)  
       std::cout << *it << std::endl;  
 }  

Скомпилируем его и выполним:

 root @ khorne /tmp # g++ -O3 -std=c++11 -m64 -c -o split1.o split1.cc  
 root @ khorne /tmp # g++ -s -m64 -o split1 split1.o  
 root @ khorne /tmp # split1  
 Quick#1brown#2fox#3jump#4over#5lazy#6white#7rabbid  
 Quick  
 #1  
 brown  
 #2  
 fox  
 #3  
 jump  
 #4  
 over  
 #5  
 lazy  
 #6  
 white  
 #7  
 rabbid  
   

Теперь можно очень легко идентифицировать разделители и выполнить дальнейшую обработку.

PS. В принципе, экранировать символ # в регулярном выражении в данном конкретном примере не обязательно, однако это не мешает правильному выполнению функции, а, в случае использования спецсимволов в регулярном выражении (такой случай возможен), например, $ вместо # - обязательно.