понедельник, 6 июня 2011 г.

Работа с регулярными выражениями в std::tr1

TR1 - крайне полезное расширение STL, и, что самое важное, есть во многих компиляторах.

Допустим, нам нужно распарсить строку с диапазонами страниц для печати, разделенными запятыми. Строка типа "1, 2, 4-7, 10". Первое, что пришло в голову:
using namespace std;
string str = "  1 , 2, 4 , 3 - 7 , 9 - 11";
//отдельная цифра может быть окружена пробелами со всех сторон
const string numb = "[\\s]*[0-9]+[\\s]*";
//диапазон -это либо одна цифра, либо две цифрыЮ разделенные тире
const string diapason = numb + "(\\-" + numb + "){0,1}";
const std::tr1::regex regex_diap(diapason);
//выводим все подстроки, соответствующие паттерну
const tr1::sregex_token_iterator end;
tr1::sregex_token_iterator iter = tr1::sregex_token_iterator(str.begin(), str.end(), regex_diap);
for (; iter != end; ++iter)
    std::cout << *iter << std::endl;
В результате программа выведет:
1
2
4
7 - 3
8 - 11

Все хорошо до тех пор, пока мы не попробуем вместо правильной строки ввести, например, такую неправильную строку: " 1 , 2, 4 , 3aaa - 7 , 9 - 11". В этом случае программа выведет следующее:
1
2
4
3
7
9 - 11

То есть программа распознала неправильный диапазон, как два отдельных числа. Нас такой результат не устраивает. Значит, надо указать, что диапазон должен содержать только цифры, пробелы и минус между цифрами. Порывшись в гугле, нашел такой вариант решения:
using namespace std;
//входная строка
string str = "  1 , 2, 4 , 5 - 7, 9 - 11";
//целое число, которое может быть обрамлено пробельными символами
const string numb = "[\\s]*[0-9]+[\\s]*";
//вот тут мы используем т.н. просмотр вперёд и назад
const string one_page = "(?<=(,|^))" + numb + "(?=(,|$))";
const string many_page = "(?<=(,|^))" + numb + "\\-" + numb + "(?=(,|$))";
//число или диапазон
const string one_many_page = "(" + one_page + "|" + many_page + ")";
const tr1::regex rx(one_many_page);
const tr1::sregex_token_iterator end;
tr1::sregex_token_iterator it;
for (it = tr1::sregex_token_iterator(str.begin(), str.end(), rx); it != end; ++it)
    std::cout << *it << std::endl;
Самое интересное тут - это т.н. просмотр вперед и назад без захвата выражения. Подробнее - в википедии. Благодаря чему мы указываем, что любому отдельному числу должна предшествовать либо запятая, либо начало строки, отделенные нулем или большим числом пробелов и за ним должна следовать либо запятая, либо конец строки, отделенные нулем или большим числом пробелов:
const string numb = "[\\s]*[0-9]+[\\s]*";
const string one_page = "(?<=(,|^))" + numb + "(?=(,|$))";
 
Аналогично с диапазоном чисел:
const string numb = "[\\s]*[0-9]+[\\s]*";
сonst string many_page = "(?<=(,|^))" + numb + "\\-" + numb + "(?=(,|$))";
 
Ну и регулярное выражение, которое выбирает ИЛИ число ИЛИ диапазон:
const string one_many_page = "(" + one_page + "|" + many_page + ")";
 
Теперь, если указать неверный диапазон чисел типа " 1 , 2, 4 , 7aaa - 3, 8 - 11", то на выходе получим:
1
2
4
8 - 11

То есть неверный диапазон мы просто пропустили, вместо неверной его интерпретации, что и требовалось.

Для подсветки синтаксиса использовал highlight.hohli.com