Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
векторов: // каждый элемент в файле содержит транзакции // для определенного магазина vector<vector<Sales_data>> files; Давайте напишем функцию, которая будет просматривать файлы в поисках магазина, продавшего заданную книгу. Для каждого магазина, у которого есть соответствующая транзакция, необходимо создать кортеж для содержания индекса этого магазина и двух итераторов. Индекс будет позицией соответствующего магазина в файлах, а итераторы отметят первую и следующую после последней записи по заданной книге в векторе vector<Sales_data> этого магазина. Функция, возвращающая кортеж Для начала напишем функции поиска заданной книги. Аргументами этой функции будет только что описанный вектор векторов и строка, представляющая ISBN книги. Функция будет возвращать вектор кортежей с записями по каждому магазину, где была продана по крайней мере одна заданная книга: // matches имеет три члена: индекс магазина и итераторы в его векторе typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator, vector<Sales_data>::const_iterator> matches; // files хранит транзакции по каждому магазину // findBook() возвращает вектор с записями для каждого магазина, // продавшего данную книгу vector<matches> findBook(const vector<vector<Sales_data>> &files, const string &book) { vector<matches> ret; // изначально пуст // для каждого магазина найти диапазон, соответствующий книге Page 904/1103 // (если он есть) for (auto it = files.cbegin(); it != files.cend(); ++it) { // найти диапазон Sales_data с тем же ISBN auto found = equal_range(it->cbegin(), it->cend(), book, compareIsbn); if (found.first != found.second) // у этого магазина есть продажи // запомнить индекс этого магазина и диапазона соответствий ret.push_back(make_tuple(it - files.cbegin(), found.first, found.second)); } return ret; // пуст, если соответствий не найдено } Цикл for перебирает элементы вектора files, которые сами являются векторами. В цикле for происходит вызов библиотечного алгоритма equal_range(), работающего как одноименная функция-член ассоциативного контейнера (см. раздел 11.3.5). Первые два аргумента функции equal_range() являются итераторами, обозначающими исходную последовательность (см. раздел 10.1). Третий аргумент — значение. По умолчанию для сравнения элементов функция equal_range() использует оператор <. Поскольку тип Sales_data не имеет оператора <, передаем указатель на функцию compareIsbn() (см. раздел 11.2.2). Алгоритм equal_range() возвращает пару итераторов, обозначающих диапазон элементов. Если книга не будет найдена, то итераторы окажутся равны, означая, что диапазон пуст. В противном случае первый член возвращенной пары обозначит первую соответствующую транзакцию, а второй — следующую после последней. Использование возвращенного функцией кортежа После создания вектора магазинов с соответствующей транзакцией эти транзакции необходимо обработать. В данной программе следует сообщить результаты общего объема продаж для каждого магазина, у которого была такая продажа: void reportResults(istream &in, ostream &os, const vector<vector<Sales_data>> &files) { string s; // искомая книга Page 905/1103 while (in >> s) { auto trans = findBook(files, s); // магазин, продавший эту книгу if (trans.empty()) { cout << s << " not found in any stores" << endl; continue; // получить следующую книгу для поиска } for (const auto &store : trans) // для каждого магазина с // продажей // get<n> возвращает указанный элемент кортежа в store os << "store " << get<0>(store) << " sales: " << accumulate(get<1>(store), get<2>(store), Sales_data(s)) << endl; } } Цикл while последовательно читает поток istream по имени in, чтобы запустить обработку следующей книги. Вызов функции findBook() позволяет выяснить, присутствует ли строка s, и присваивает результаты вектору trans. Чтобы упростить написание типа trans, являющегося вектором кортежей, используем ключевое слово auto. Если вектор trans пуст, значит, по книге s никаких продаж не было. В таком случае выводится сообщение и происходит возврат к циклу while, чтобы обработать следующую книгу. Цикл for свяжет ссылку store с каждым элементом вектора trans. Поскольку изменять элементы вектора trans не нужно, объявим ссылку store ссылкой на константу. Для вывода результатов используем get: get<0> — индекс соответствующего магазина; get<1> — итератор на первую транзакцию; get<2> — на следующую после последней. Поскольку класс Sales_data определяет оператор суммы (см. раздел 14.3), для суммирования транзакций можно использовать библиотечный алгоритм accumulate() (см. раздел 10.2.1). Как отправную точку суммирования используем объект класса Sales_data, инициализированный конструктором Sales_data(), получающим строку (см. раздел 7.1.4). Этот конструктор инициализирует переменную-член bookNo переданной строкой, а переменные-члены Page 906/1103 units_sold и revenue — нулем. Упражнения раздела 17.1.2 Упражнение 17.4. Напишите и проверьте собственную версию функции findBook(). Упражнение 17.5. Перепишите функцию findBook() так, чтобы она возвращала пару, содержащую индекс и пару итераторов. Упражнение 17.6. Перепишите функцию findBook() так, чтобы она не использовала кортеж или пару. Упражнение 17.7. Объясните, какую версию функции findBook() вы предпочитаете и почему. Упражнение 17.8. Что будет, если в качестве третьего параметра алгоритма accumulate() в последнем примере кода этого раздела передать объект класса Sales_data? 17.2. Тип bitset В разделе 4.8 приводились встроенные операторы, рассматривающие целочисленный операнд как коллекцию битов. Для облегчения использования битовых операций и обеспечения возможности работы с коллекциями битов, размер которых больше самого длинного целочисленного типа, стандартная библиотека определяет класс bitset (набор битов). Класс bitset определен в заголовке bitset. 17.2.1. Определение и инициализация наборов битов Список конструкторов типа bitset приведен в табл. 17.2. Тип bitset — это шаблон класса, который, подобно классу array, имеет фиксированный размер (см. раздел 3.3.6). При определении набора битов следует указать в угловых скобках количество битов, которые он будет содержать: bitset<32> bitvec(1U); // 32 бита; младший бит 1, остальные биты 0 Размер должен быть указан константным выражением (см. раздел 2.4.4). Этот оператор определяет набор битов bitvec, содержащий 32 бита. Подобно элементам вектора, биты в наборе битов не имеют имен. Доступ к ним осуществляется по позиции. Нумерация битов начинается с 0. Таким образом, биты набора bitvec пронумерованы от 0 до 31. Биты, расположенные ближе к началу (к 0), называются младшими битами (low-order), а ближе к концу (к 31) — старшими битами (high-order). Таблица 17.2. Способы инициализации набора битов bitset<n> b; Набор b содержит n битов, каждый из которых содержит значение 0. Это конструктор constexpr (см. раздел 7.5.6) bitset<n> b(u); Набор b содержит копию n младших битов значения u типа unsigned long long. Если значение n больше размера типа unsigned long long, остальные старшие биты Page 907/1103 устанавливаются на нуль. Это конструктор constexpr (см. раздел 7.5.6) bitset<n> b(s, рos, m, zero, one); Набор b содержит копию m символов из строки s, начиная с позиции pos. Строка s может содержать только символы для нулей и единиц; если строка s содержит любой другой символ, передается исключение invalid_argument. Символы хранятся в наборе b как нули и единицы соответственно. По умолчанию параметр pos имеет значение 0, параметр m — string::npos, zero — '0' и one — '1' bitset<n> b(cp, pos, m, zero, one); Подобен предыдущему конструктору, но копируется символьный массив, на который указывает cp. Если значение m не предоставлено, cp должен указывать на строку в стиле С. Если m предоставлено, то начиная с позиции cp в массиве должно быть по крайней мере m символов, соответствующих нулям или единицам Конструкторы, получающие строку или символьный указатель, являются явными (см. раздел 7.5.4). В новом стандарте была добавлена возможность определять альтернативные символы для 0 и 1. Инициализация набора битов беззнаковым значением При использовании для инициализации набора битов целочисленного значения оно преобразуется в тип unsigned long long и рассматривается как битовая схема. Биты в наборе битов являются копией этой схемы. Если размер набора битов превосходит количество битов в типе unsigned long long, то остальные старшие биты устанавливаются в нуль. Если размер набора битов меньше количества битов, то будут использованы только младшие биты предоставленного значения, а старшие биты вне размера объекта набора битов отбрасываются: // bitvec1 меньше инициализатора; старшие биты инициализатора // отбрасываются bitset<13> bitvec1(0xbeef); // биты 1111011101111 // bitvec2 больше инициализатора; старшие биты bitvec2 // устанавливаются в нуль bitset<20> bitvec2(0xbeef); // биты 00001011111011101111 // на машинах с 64-битовым long long, 0ULL - это 64 бита из 0, // a 0ULL - 64 единицы bitset<128> bitvec3(0ULL); // биты 0...63 - единицы; 63...121 - нули Инициализация набора битов из строки Page 908/1103 Набор битов можно инициализировать из строки или указателя на элемент в символьном массиве. В любом случае символы непосредственно представляют битовую схему. Как обычно, при использовании строки для представления числа символы с самыми низкими индексами в строке соответствуют старшим битам, и наоборот: bitset<32> bitvec4("1100"); // биты 2 и 3 - единицы, остальные - 0 Если строка содержит меньше символов, чем битов в наборе, старшие биты устанавливаются в нуль. Соглашения по индексации строк и наборов битов прямо противоположны: символ строки с самым высоким индексом (крайний правый символ) используется для инициализации младшего бита в наборе битов (бит с индексом 0). При инициализации набора битов из строки следует помнить об этом различии. Необязательно использовать всю строку в качестве исходного значения для набора битов, вполне можно использовать часть строки: string str("1111111000000011001101"); bitset<32> bitvec5(str, 5, 4); // четыре бита, начиная с str[5] - 1100 bitset<32> bitvec6(str, str.size()-4); // использует четыре последних // символа Здесь набор битов bitvec5 инициализируется подстрокой str, начиная с символа str[5], и четырьмя символами далее. Как обычно, крайний справа символ подстроки представляет бит самого низкого порядка. Таким образом, набор bitvec5 инициализируется битами с позиции 3 до 0 и получает значение 1100, а остальные биты — 0. Инициализатор набора битов bitvec6 передает строку и отправную точку, поэтому он инициализируется символами строки str, начиная с четвертого и до конца строки str. Остаток битов набора bitvec6 инициализируется нулями. Эти инициализации можно представить так: Упражнения раздела 17.2.1 Упражнение 17.9. Объясните битовую схему, которую содержит каждый из следующих объектов bitset: (a) bitset<64> bitvec(32); (b) bitset<32> bv(1010101); (c) string bstr; cin >> bstr; bitset<8> bv(bstr); 17.2.2. Операции с наборами битов Операции с наборами битов (табл. 17.3) определяют различные способы проверки и установки одного или нескольких битов. Класс bitset поддерживает также побитовые операторы, которые рассматривались в разделе 4.8. Применительно к объектам bitset эти Page 909/1103 операторы имеют тот же смысл, что и таковые встроенные операторы для типа unsigned. Таблица 17.3. Операции с наборами битов b.any() Установлен ли в наборе b хоть какой-нибудь бит? b.all() Все ли биты набора b установлены? b.none() Нет ли в наборе b установленных битов? b.count() Количество установленных битов в наборе b b.size() Функция constexpr (см. раздел 2.4.4), возвращающая количество битов набора b b.test(pos) Возвращает значение true, если бит в позиции pos установлен, и значение false в противном случае b.set(pos, v) b.set() Устанавливает для бита в позиции pos логическое значение v. По умолчанию v имеет значение true. Без аргументов устанавливает все биты набора b b.reset(pos) b.reset() Сбрасывает бит в позиции pos или все биты набора b b.flip(pos) b.flip() Изменяет состояние бита в позиции pos или все биты набора b b[pos] Предоставляет доступ к биту набора b в позиции pos; если набор b константен и бит установлен, то b[pos] возвращает логическое значение true, а в противном случае — значение false b.to_ulong() b.to_ullong() Возвращает значение типа unsigned long или типа unsigned long long с теми же битами, что и в наборе b. Если битовая схема в наборе b не соответствует указанному типу результата, передается исключение overflow_error b.to_string(zero, one) Возвращает строку, представляющую битовую схему набора b. Параметры zero и one имеют по умолчанию значения '0' и '1'. Они используют для представления битов 0 и 1 в наборе b os << b Выводит в поток os биты набора b как символы '0' и '1' is >> b Читает символы из потока is в набор b. Чтение прекращается, когда следующий символ отличается от 1 или 0 либо когда прочитано b.size() битов Некоторые из функций, count(), size(), all(), any() и none(), не получают аргументов и возвращают информацию о состоянии всего набора битов. Другие, set(), reset() и flip(), изменяют состояние набора битов. Функции-члены, изменяющие набор битов, допускают перегрузку. В любом случае версия функции без аргументов применяет соответствующую операцию ко всему набору, а версии функций, получающих позицию, применяют операцию к заданному биту: bitset<32> bitvec(1U); // 32 бита; младший бит 1, остальные биты - 0 bool is_set = bitvec.any(); // true, установлен один бит bool is_not_set = bitvec.none(); // false, установлен один бит bool all_set = bitvec.all(); // false, только один бит установлен size_t onBits = bitvec.count(); // возвращает 1 size_t sz = bitvec.size(); // возвращает 32 bitvec.flip(); // Page 910/1103 инвертирует значения всех битов в bitvec bitvec.reset(); // сбрасывает все биты в 0 bitvec.set(); // устанавливает все биты в 1 Функция any() возвращает значение true, если один или несколько битов объекта класса bitset установлены, т.е. равны 1. Функция none(), наоборот, возвращает значение true, если все биты содержат нуль. Новый стандарт ввел функцию all(), возвращающую значение true, если все биты установлены. Функции count() и size() возвращают значение типа size_t (см. раздел 3.5.2), равное количеству установленных битов, или общее количество битов в объекте соответственно. Функция size() — constexpr, а значит, она применима там, где требуется константное выражение (см. раздел 2.4.4). Функции flip(), set(), reset() и test() позволяют читать и записывать биты в заданную позицию: bitvec.flip(0); // инвертирует значение первого бита bitvec.set(bitvec.size() - 1); // устанавливает последний бит bitvec.set(0, 0); // сбрасывает первый бит bitvec.reset(i); // сбрасывает i-й бит bitvec.test(0); // возвращает false, поскольку первый бит сброшен Оператор индексирования перегружается как константный. Константная версия возвращает логическое значение true, если бит по заданному индексу установлен, и значение false в противном случае. Неконстантная версия возвращает специальный тип, определенный классом bitset, позволяющий манипулировать битовым значением в позиции, заданной индексом: bitvec[0] = 0; // сбрасывает бит в позиции 0 bitvec[31] = bitvec[0]; // устанавливает последний бит в то же // состояние, что и первый bitvec[0].flip(); // инвертирует значение бита в позиции 0 Page 911/1103 bitvec[0]; // эквивалентная операция; инвертирует бит // в позиции 0 bool b = bitvec[0]; // преобразует значение bitvec[0] в тип bool Возвращение значений из набора битов Функции to_ulong() и to_ullong() возвращают значение, содержащее ту же битовую схему, что и объект класса bitset. Эти функции можно использовать, только если размер набора битов меньше или равен размеру типа unsigned long для функции to_ulong() и типа unsigned long long для функции to_ullong() соответственно: unsigned long ulong = bitvec3.to_ulong(); cout << "ulong = " << ulong << endl; Если значение в наборе битов не соответствует заданному типу, эти функции передают исключение overflow_error (см. раздел 5.6). Операторы ввода-вывода типа bitset Оператор ввода читает символы из входного потока во временный объект типа string. Чтение продолжается, пока не будет заполнен соответствующий набор битов, или пока не встретится символ, отличный от 1 или 0, или не встретится конец файла, или ошибка ввода. Затем этой временной строкой (см. раздел 17.2.1) инициализируется набор битов. Если прочитано меньше символов, чем насчитывает набор битов, старшие биты, как обычно, устанавливаются в 0. Оператор вывода выводит битовую схему объекта bitset: bitset<16> bits; cin >> bits; // читать до 16 символов 1 или 0 из cin cout << "bits: " << bits << endl; // вывести прочитанное Использование наборов битов Для иллюстрации применения наборов битов повторно реализуем код оценки из раздела 4.8, использовавший тип unsigned long для представления результатов контрольных вопросов (сдал/не сдал) для 30 учеников: bool status; // версия, использующая побитовые операторы unsigned long quizA = 0; // это значение используется // Page 912/1103 как коллекция битов quizA |= 1UL << 27; // отметить ученика номер 27 как сдавшего status = quizA & (1UL << 27); // проверить оценку ученика номер 27 quizA &= (1UL << 27); // ученик номер 27 не сдал // эквивалентные действия с использованием набора битов bitset<30> quizB; // зарезервировать по одному биту на студента; все // биты инициализированы 0 quizB.set(27); // отметить ученика номер 27 как сдавшего status = quizB[27]; // проверить оценку ученика номер 27 quizB.reset(27); // ученик номер 27 не сдал Упражнения раздела 17.2.2 Упражнение 17.10. Используя последовательность 1, 2, 3, 5, 8, 13, 21, инициализируйте набор битов, у которого установлена 1 в каждой позиции, соответствующей числу в этой последовательности. Инициализируйте по умолчанию другой набор битов и напишите небольшую программу для установки каждого из соответствующих битов. Упражнение 17.11. Определите структуру данных, которая содержит целочисленный объект, позволяющий отследить (сдал/не сдал) ответы на контрольную из 10 вопросов. Какие изменения (если они вообще понадобятся) необходимо внести в структуру данных, если в контрольной станет 100 вопросов? Упражнение 17.12. Используя структуру данных из предыдущего вопроса, напишите функцию, получающую номер вопроса и значение, означающее правильный/неправильный ответ, и изменяющую результаты контрольной соответственно. Упражнение 17.13. Создайте целочисленный объект, содержащий правильные ответы (да/нет) на вопросы контрольной. Используйте его для создания оценок контрольных вопросов для структуры данных из предыдущих двух упражнений. 17.3. Регулярные выражения Page 913/1103 Регулярное выражение (regular expression) — это способ описания последовательности символов. Это чрезвычайно мощное средство программирования. Однако описание языков, используемых для определения регулярных выражений, выходит за рамки этой книги. Лучше сосредоточиться на использовании библиотеки регулярных выражений языка С++ (библиотеки RE), являющейся частью новой библиотеки. Библиотека RE определена в заголовке regex и задействует несколько компонентов, перечисленных в табл. 17.4. Таблица 17.4. Компоненты библиотеки регулярных выражений regex Класс, представляющий регулярное выражение regex_match() Сравнивает последовательность символов с регулярным выражением regex_search() Находит первую последовательность, соответствующую регулярному выражению regex_replace() Заменяет регулярное выражение, используя заданный формат sregex_iterator Адаптер итератора, вызывающий функцию regex_search() для перебора совпадений в строке smatch Класс контейнера, содержащего результаты поиска в строке ssub_match Результаты совпадения выражений в строке Если вы еще не знакомы с использованием регулярных выражений, то имеет смысл просмотреть этот раздел и выяснить, на что способны регулярные выражения. Класс regex представляет регулярное выражение. Кроме инициализации и присвоения, с классом regex допустимо немного операций. Они перечислены в табл. 17.6. Функции regex_match() и regex_search() определяют, соответствует ли заданная последовательность символов предоставленному объекту класса regex. Функция regex_match() возвращает значение true, если вся исходная последовательность соответствует выражению; функция regex_search() возвращает значение true, если в исходной последовательности выражению соответствует подстрока. Есть также функция regex_replace(), описываемая в разделе 17.3.4. Аргументы функции regex описаны в табл. 17.5. Эти функции возвращают логическое значение и допускают перегрузку: одна версия получает дополнительный аргумент типа smatch. Если он есть, эти функции сохраняют дополнительную информацию об успехе обнаружения соответствия в предоставленном объекте класса smatch. 17.3.1. Использование библиотеки регулярных выражений В качестве довольно простого примера рассмотрим поиск слов, нарушающих известное правило правописания " i перед е , кроме как после с ": // найти символы ei, следующие за любым символом, кроме с string pattern("[^с]ei"); // Page 914/1103 искомая схема должна присутствовать в целом слове pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*"; regex r(pattern); // создать regex для поиска схемы smatch results; // определить объект для содержания результатов поиска // определить строку, содержащую текст, соответствующий и не // соответствующий схеме string test_str = "receipt freind theif receive"; // использовать r для поиска соответствия в test_str if (regex_search(test_str, results, r)) // если соответствие есть cout << results.str() << endl; // вывести соответствующее слово Таблица 17.5. Аргументы функций regex_search() и regex_match() Обратите внимание: функции возвращают логическое значение, означающее, было ли найдено соответствие. ( seq , m, r, mft) ( seq , r, mft) Поиск регулярного выражения объекта r класса regex в символьной последовательности seq . Последовательность seq может быть строкой, парой итераторов, обозначающих диапазон, или указателем на символьный массив с нулевым символом в конце, m — это объект соответствия, используемый для хранения подробностей о соответствии. Типы объекта m и последовательности seq должны быть совместимы (см. раздел 17.3.1). mft — это необязательное значение regex_constants::match_flag_type. Это значение, описанное в табл. 17.13, влияет на процесс поиска соответствия Таблица 17.6. Операции с классом regex (и wregex) regex r( Page 915/1103 re ) regex r( re , f) Параметр re представляет регулярное выражение и может быть строкой, парой итераторов, обозначающих диапазон символов, указателем на символьный массив с нулевым символом в конце, указателем на символ и количеством или списком символов в скобках, f — это флаги, определяющие выполнение объекта. Флаги f устанавливаются исходя из упомянутых ниже значений. Если флаги f не определены, по умолчанию применяется ECMAScript r1 = re Заменяет регулярное выражение в r1 регулярным выражением re . re — это регулярное выражение, которое может быть другим объектом класса regex, строкой, указателем на символьный массив с нулевым символом в конце или списком символов в скобках r1.assign( re , f) То же самое, что и оператор присвоения (=). Параметр re и необязательный флаг f имеют тот же смысл, что и соответствующие аргументы конструктора regex() r.mark_count() Количество подвыражений (рассматриваются в разделе 17.3.3) в объекте r r.flags() Возвращает набор флагов для объекта r Примечание : конструкторы и операторы присвоения могут передавать исключение типа regex_error. Флаги, применяемые при определении объекта класса regex. Определены в типах regex и regex_constants::syntax_option_type icase Игнорировать регистр при поиске соответствия nosubs Не хранить соответствия подвыражений optimize Предпочтение скорости выполнения скорости создания ECMAScript Использование грамматики согласно ЕСМА-262 basic Использование базовой грамматики регулярных выражений POSIX extended Использование расширенной грамматики регулярных выражения POSIX awk Использование грамматики POSIX версии языка awk grep Использование грамматики POSIX версии языка grep egrep Использование грамматики POSIX версии языка egrep Начнем с определения строки для хранения искомого регулярного выражения. Регулярное выражение [^с] означает любой символ, отличный от символа 'c', a [^c]ei — любой такой символ, сопровождаемый символами 'ei'. Эта схема описывает строки, содержащие только три символа. Необходимо найти целое слово, содержащее эту схему. Для соответствия слову необходимо регулярное выражение, которое будет соответствовать символам, расположенным прежде и после заданной трехсимвольной схемы. Это регулярное выражение состоит из любого количества символов, сопровождаемых первоначальной трехсимвольной схемой и любым количеством дополнительных символов. По умолчанию объекты класса regex используют язык регулярных выражений ECMAScript. На языке ECMAScript схема [[:alpha:]] соответствует любому алфавитному символу, а символы + и * означают "один или несколько" и "нуль или более" соответственно. Таким образом, схема [[:alpha:]]* будет соответствовать любому количеству символов. Регулярное выражение, сохраненное в строке pattern, используется для инициализации объекта r класса regex. Затем определяется строка, которая будет использована для проверки регулярного выражения. Строка test_str инициализируется словами, которые соответствуют схеме (например, "freind" и "theif"), и словами, которые ей не соответствуют (например, "receipt" и "receive"). Определим также объект results класса smatch, передаваемый функции regex_search(). Если соответствие будет найдено, то объект results будет содержать подробности о том, где оно найдено. Page 916/1103 Затем происходит вызов функции regex_search(). Если она находит соответствие, то возвращает значение true. Для вывода части строки test_str, соответствующей заданной схеме, используется функция-член str() объекта results. Функция regex_search() прекращает поиск, как только находит в исходной последовательности соответствующую подстроку. В результате вывод будет таким: freind Поиск всех соответствий во вводе представлен в разделе 17.3.2. Определение параметров объекта regex При определении объекта класса regex или вызове его функции assign() для присвоения ему нового значения можно применить один или несколько флагов, влияющих на работу объекта класса regex. Эти флаги контролируют обработку, осуществляемую этим объектом. Последние шесть флагов, указанных в табл. 17.6, задают язык, на котором написано регулярное выражение. Установлен должен быть только один из флагов определения языка. По умолчанию установлен флаг ECMAScript, задающий использование объектом класса regex спецификации ЕСМА-262, являющейся языком регулярных выражений большинства веб-браузеров. Другие три флага позволяют определять независимые от языка аспекты обработки регулярного выражения. Например, можно указать, что поиск регулярного выражения не будет зависеть от регистра символов. В качестве примера используем флаг icase для поиска имен файлов с указанными расширениями. Большинство операционных систем распознают расширения без учета регистра символов: программа С++ может быть сохранена в файле с расширением .cc, .Cc, .cC или .CC. Давайте напишем регулярное выражение для распознавания любого из них наряду с другими общепринятыми расширениями файлов: // один или несколько алфавитно-цифровые символов, сопровождаемых // и "cpp", "cxx" или "cc" regex r("[[:alnum:]]+\\.(cpp|схх|cc)$", regex::icase); smatch results; string filename; while (cin >> filename) if (regex_search(filename, results, r)) cout << results.str() << endl; // вывод текущего соответствия Это выражение будет соответствовать строке из одного или нескольких символов или цифр, сопровождаемых точкой и одним из трех расширений файла. Регулярное выражение будет соответствовать расширению файлов независимо от регистра. Подобно тому, как специальные символы есть в языке С++ (см. раздел 2.1.3), у языков Page 917/1103 регулярных выражений, как правило, тоже есть специальные символы. Например, точка (.) обычно соответствует любому символу. Как и в языке С++, для обозначения специального характера символа его предваряют символом наклонной черты. Поскольку наклонная черта влево является также специальным символом в языке С++, в строковом литерале языка С++, означающем наклонную черту влево следует использовать вторую наклонную черту влево. Следовательно, чтобы представить точку в регулярном выражении, необходимо написать \\.. Ошибки в определении и использовании регулярного выражения Регулярное выражение можно считать самостоятельной "программой" на простом языке программирования. Этот язык не интерпретируется компилятором С++, и "компилируется" только во время выполнения, когда объект класса regex инициализируется или присваивается. Как и в любой написанной программе, в регулярных выражениях вполне возможны ошибки. Важно понимать, что правильность синтаксиса регулярного выражения проверяется во время выполнения. Если допустить ошибку в записи регулярного выражения, то передача исключения (см. раздел 5.6) типа regex_error произойдет только во время выполнения . Подобно всем стандартным типам исключений, у исключения regex_error есть функция what(), описывающая произошедшую ошибку (см. раздел 5.6.2). У исключения regex_error есть также функция-член code(), возвращающая числовой код (зависящий от реализации), соответствующий типу произошедшей ошибки. Стандартные сообщения об ошибках, которые могут быть переданы библиотекой RE, приведены в табл. 17.7. Таблица 17.7. Причины ошибок в регулярном выражении Определены в типах regex и regex_constants::syntax_option_type error_collate Недопустимый запрос объединения элементов error_ctype Недопустимый класс символов error_escape Недопустимый управляющий или замыкающий символ error_backref Недопустимая обратная ссылка error_brack Несоответствие квадратных скобок ([ или ]) error_paren Несоответствие круглых скобок (( или )) error_brace Несоответствие фигурных скобок ({ или }) error_badbrace Недопустимый диапазон в фигурных скобках ({}) error_range Недопустимый диапазон символов (например, [z-a]) error_space Недостаточно памяти для выполнения этого регулярного выражения error_badrepeat Повторяющийся символ (*?, + или {) не предваряется допустимым регулярным выражением error_complexity Затребованное соответствие слишком сложно error_stack Недостаточно памяти для вычисления соответствия Например, в схеме вполне можно пропустить по неосторожности скобку: try { // ошибка: пропущена закрывающая скобка после alnum; конструктор // передаст исключение regex r("[[:alnum:]+\\.(cpp|схх|cc)$", regex::icase); } catch (regex_error e) Page 918/1103 { cout << e.what() << "\ncode: " << e.code() << endl; } При запуске на системе авторов эта программа выводит следующее: regex_error(error_brack): The expression contained mismatched [ and ]. code: 4 Компилятор определяет функцию-член code() для возвращения позиции ошибок, перечисленных в табл. 17.7, счет которых, как обычно, начинается с нуля. Совет. Избегайте создания ненужных регулярных выражений Как уже упоминалось, представляющая регулярное выражение "программа" компилируется во время выполнения, а не во время компиляции. Компиляция регулярного выражения может быть на удивление медленной операцией, особенно если используется расширенная грамматика регулярного выражения или выражение слишком сложно. В результате создание объекта класса regex и присвоение нового регулярного выражения уже существующему объекту класса regex может занять много времени. Для минимизации этих дополнительных затрат не создавайте больше объектов класса regex, чем необходимо. В частности, если регулярное выражение используются в цикле, его следует создать вне цикла, избежав перекомпиляции при каждой итерации. Классы регулярного выражения и тип исходной последовательности Поиск возможен в любой из исходных последовательностей нескольких типов. Входные данные могут быть обычными символами типа char или wchar_t, и эти символы могут храниться в библиотечной строке или в массиве символов (или в его версии для wchar_t, или wstring). Библиотека RE определяет отдельные типы, соответствующие этим разным типам исходных последовательностей. Предположим, например, что класс regex содержит регулярное выражение типа char. Для типа wchar_t библиотека определяет также класс wregex, поддерживающий все операции класса regex. Единственное различие в том, что инициализаторы класса wregex должны использовать тип wchar_t вместо типа char. Типы соответствий и итераторов (они рассматриваться в следующих разделах) более специфичны. Они отличаются не только типом символов, но и тем, является ли последовательность библиотечным типом или массивом: класс smatch представляет исходные последовательности типа string; класс cmatch — символьные массивы; wsmatch — строки Unicode (wstring); wcmatch — массивы символов wchar_t. Таблица 17.8. Библиотечные классы регулярных выражений Тип исходной последовательности Используемый класс регулярного выражения string regex, smatch, ssub_match и sregex_iterator const char* regex, cmatch, csub_match и cregex_iterator wstring wregex, wsmatch, wssub_match и wsregex_iterator const wchar_t* wregex, wcmatch, wcsub_match и wcregex_iterator Важный момент: используемый тип библиотеки RE должен соответствовать типу исходной последовательности. Соответствие классов видам исходных последовательностей приведено в табл. 17.8. Например: regex r("[[:alnum:]]+\\.(cpp|схх|cc)$", regex::icase); smatch results; // Page 919/1103 будет соответствовать последовательности типа // string, но не char* if (regex_search("myfile.cc", results, r)) // ошибка: ввод char* cout << results.str() << endl; Компилятор С++ отклонит этот код, поскольку тип аргумента и тип исходной последовательности не совпадают. Если необходимо искать в символьном массиве, то следует использовать объект класса cmatch: cmatch results; // будет соответствовать последовательности символьного // массива if (regex_search("myfile.cc", results, r)) cout << results.str() << endl; // вывод текущего соответствия Обычно программы используют исходные последовательности типа string и соответствующие ему версии компонентов библиотеки RE. Упражнения раздела 17.3.1 Упражнение 17.14. Напишите несколько регулярных выражений, предназначенных для создания различных ошибок. Запустите программу и посмотрите, какие сообщения выводит ваш компилятор для каждой ошибки. Упражнение 17.15. Напишите программу, используя схему поиска слов, нарушающих правило " i перед е , кроме как после c ". Организуйте приглашение для ввода пользователем слова и вывод результата его проверки. Проверьте свою программу на примере слов, которые нарушают и не нарушают это правило. Упражнение 17.16. Что будет при инициализации объекта класса regex в предыдущей программе значением "[^c]ei"? Проверьте свою программу, используя эту схему, и убедитесь в правильности своих ожиданий. 17.3.2. Типы итераторов классов соответствия и regex Программа проверки правила " Page 920/1103 i перед е , кроме как после с " из раздела 17.3.1 выводила только первое соответствие в исходной последовательности. Используя итератор sregex_iterator, можно получить все соответствия. Итераторы класса regex являются адаптерами итератора (см. раздел 9.6), привязанные к исходной последовательности и объекту класса regex. Как было описано в табл. 17.8, для каждого типа исходной последовательности используется специфический тип итератора. Операции с итераторами описаны в табл. 17.9. Когда итератор sregex_iterator связывается со строкой и объектом класса regex, итератор автоматически позиционируется на первое соответствие в заданной строке. Таким образом, конструктор sregex_iterator() вызывает функцию regex_search() для данной строки и объекта класса regex. При обращении к значению итератора возвращается объект класса smatch, соответствующий результатам самого последнего поиска. При приращении итератора для поиска следующего соответствия в исходной строке вызывается функция regex_search(). Таблица 17.9. Операции с итератором sregex_iterator Эти операции применимы также к итераторам cregex_iterator, wsregex_iterator и wcregex_iterator sregex_iterator it(b, e, r); it — это итератор sregex_iterator, перебирающий строку, обозначенную итераторами b и е. Вызов regex_search(b, е, r) устанавливает итератор it на первое соответствие во вводе sregex_iterator end; Итератор sregex_iterator, указывающий на позицию после конца *it it-> Возвращает ссылку на объект класса smatch или указатель на объект класса smatch от самого последнего вызова функции regex_search() ++it it++ Вызывает функцию regex_search() для исходной последовательности, начиная сразу после текущего соответствия. Префиксная версия возвращает ссылку на приращенный итератор, а постфиксная возвращает прежнее значение it1 == it2 it1 != it2 Два итератора sregex_iterator равны, если оба они итераторы после конца. Два не конечных итератора равны, если они созданы из той же исходной последовательности и объекта класса regex Использование итератора sregex_iterator В качестве примера дополним программу поиска нарушения правила " i перед е , кроме как после с " в текстовом файле. Подразумевается, что file класса string содержит все содержимое исходного файла, на котором осуществляется поиск. Новая версия программы будет использовать ту же схему, что и ранее, но для поиска применим итератор sregex_iterator: // найти символы ei, следующие за любым символом, кроме с string pattern("[^с]ei"); // искомая схема должна присутствовать в целом слове pattern = "[[:alpha:]]*" + pattern + "[[ :alpha:]]*"; regex r(pattern, regex::icase); // Page 921/1103 игнорируем случай выполнения // соответствия // будет последовательно вызывать regex_search() для поиска всех // соответствий в файле for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) cout << it->str() << endl; // соответствующее слово Цикл for перебирает все соответствия r в строке file. Инициализатор в цикле for определяет итераторы it и end_it. При определении итератора it конструктор sregex_iterator() вызывает функцию regex_search() для позиционирования итератора it на первое соответствие в строке file. Пустой итератор sregex_iterator, end_it действует как итератор после конца. Приращение в цикле for "перемещает" итератор, вызвав функцию regex_search(). При обращении к значению итератора возвращается объект класса smatch, представляющий текущее соответствие. Для вывода соответствующего слова вызывается функция-член str(). Данный цикл for как бы перепрыгивает с одного соответствия на другое, как показано на рис. 17.1. Рис. 17.1. Использование итератора sregex_iterator Использование данных соответствия Если запустить этот цикл для строки test_str из первоначальной программы, вывод был бы таким: freind theif Однако вывод только самого слова, соответствующего заданному выражению, не очень полезен. При запуске программы для большой исходной последовательности, например для текста этой главы, имело бы смысл увидеть контекст, в котором встретилось слово. Например: hey read or write according to the type >>> being <<< handled. The input operators ignore whi Кроме возможности вывода части исходной строки, в которой встретилось соответствие, классы соответствия предоставляют более подробную информацию о соответствии. Возможные операции с этими типами перечислены в табл. 17.10 и 17.11. Page 922/1103 Более подробная информация о smatch и ssub_match приведена в следующем разделе, а пока достаточно знать, что они предоставляют доступ к контексту соответствия. У типов соответствия есть функции-члены prefix() и suffix(), возвращающие объект класса ssub_match, представляющий часть исходной последовательности перед и после текущего соответствия соответственно. У класса ssub_match есть функции-члены str() и length(), возвращающие соответствующую строку и ее размер соответственно. Используя эти функции, можно переписать цикл программы проверки правописания: // тот же заголовок цикла for, что и прежде for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) { auto pos = it->prefix().length(); // размер префикса pos = pos > 40 ? pos - 40 : 0; // необходимо до 40 символов cout << it->prefix().str().substr(pos) // последняя часть префикса << "\n\t\t>>> " << it->str() << " <<<\n" // соответствующее // слово << it->suffix().str().substr(0, 40) // первая часть суффикса << endl; } Таблица 17.10. Операции с типом smatch Эти операции применимы также к типам cmatch, wsmatch, wcmatch и соответствующим типам csub_match, wssub_match и wcsub_match. m.ready() Возвращает значение true, если m был установлен вызовом функции regex_search() или regex_match(), в противном случае — значение false (в этом случае результат операции с m непредсказуем) m.size() Возвращает значение 0, если соответствия не найдено, в противном случае — на единицу больше, чем количество подвыражений в последнем соответствующем регулярном выражении m.empty() Возвращает значение true, если размер нулевой m.prefix() Возвращает объект класса ssub_match, представляющий последовательность перед соответствием m.suffix() Возвращает объект класса ssub_match, представляющий часть после конца соответствия m.format(...) См. табл. 17.12 В функциях, получающих индекс, n по умолчанию имеет значение нуль и должно быть меньше m.size(). Первое соответствие (с индексом 0) представляет общее соответствие. m.length(n) Возвращает размер соответствующего подвыражения номер n m.position(n) Дистанция подвыражения номер n от начала последовательности m.str(n) Соответствующая строка для Page 923/1103 подвыражения номер n m[n] Объект ssub_match, соответствующий подвыражению номер n m.begin(), m.end() m.cbegin(), m.cend() Итераторы элементов sub_match в m. Как обычно, функции cbegin() и cend() возвращают итераторы const_iterator Более подробная информация о smatch и ssub_match приведена в следующем разделе, а пока достаточно знать, что они предоставляют доступ к контексту соответствия. У типов соответствия есть функции-члены prefix() и suffix(), возвращающие объект класса ssub_match, представляющий часть исходной последовательности перед и после текущего соответствия соответственно. У класса ssub_match есть функции-члены str() и length(), возвращающие соответствующую строку и ее размер соответственно. Используя эти функции, можно переписать цикл программы проверки правописания: // тот же заголовок цикла for, что и прежде for (sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it) { auto pos = it->prefix().length(); // размер префикса pos = pos > 40 ? pos - 40 : 0; // необходимо до 40 символов cout << it->prefix().str().substr(pos) // последняя часть префикса << "\n\t\t>>> " << it->str () << " <<<\n" // соответствующее // слово << it->suffix().str().substr(0, 40) // первая часть суффикса << endl; } Сам цикл работает, как и прежде. Изменился процесс в цикле for, представленный на рис. 17.2. Рис. 17.2. Объект класса smatch, представляющий некое соответствие Здесь происходит вызов функции prefix(), возвращающий объект класса ssub_match, представляющий часть строки file перед текущим соответствием. Чтобы выяснить, сколько символов находится в части строки file перед соответствием, вызовем функцию length() для этого объекта класса ssub_match. Затем скорректируем значение pos так, чтобы оно было индексом 40-го символа от конца префикса. Если у префикса меньше 40 символов, Page 924/1103 устанавливаем pos в 0, означая, что выведен весь префикс. Функция substr() (см. раздел 9.5.1) используется для вывода от данной позиции до конца префикса. После вывода символов, предшествующих соответствию, выводится само соответствие с некоторым дополнительным оформлением, чтобы соответствующее слово выделилось в выводе. После вывода соответствующей части выводится до 40 следующих после соответствия символов строки file. Упражнения раздела 17.3.2 Упражнение 17.17. Измените свою программу так, чтобы она находила все слова в исходной последовательности, нарушающие правило " i перед е , кроме как после с ". Упражнение 17.18. Пересмотрите свою программу так, чтобы игнорировать слова, содержащие сочетание "ei", но не являющиеся ошибочными, такие как "albeit" и "neighbor". 17.3.3. Использование подвыражений Схема в регулярном выражении зачастую содержит одно или несколько подвыражений (subexpression). Подвыражение — это часть схемы, которая сама имеет значение. Для обозначения подвыражения в регулярном выражении, как правило, используют круглые скобки. Например, в схеме для поиска соответствий расширений файлов языка С++ (см. раздел 16.1.2) круглые скобки используются для группировки возможных расширений. Каждый раз, когда альтернативы группируются с использованием круглых скобок, одновременно объявляется, что эти альтернативы формируют подвыражение. Это выражение можно переписать так, чтобы оно предоставило доступ к имени файла, являющемуся той частью схемы, которая предшествует точке: // r содержит два подвыражения: // первое - часть имени файла перед точкой, // второе - расширение файла regex r("([[:alnum:]]+)\\.(cpp|схх|cc)$", regex::icase); Теперь в схеме два заключенных в скобки подвыражения: • ([[:alnum:]]+) — представляет последовательность из одного или нескольких символов; • (cpp|схх|cc) — представляет расширения файлов. Page 925/1103 Теперь программу из раздела 16.1.2 можно переписать так (изменив оператора вывода), чтобы выводить только имя файла: if (regex_search(filename, results, r)) cout << results.str(1) << endl; // вывести первое подвыражение В первоначальной программе для поиска схемы r в строке filename использовался вызов функции regex_search(), а также объект results класса smatch для содержания результата поиска соответствия. Если вызов успешен, выводится результат. Но в этой программе выводится str(1), т.е. соответствие для первого подвыражения. Кроме информации об общем соответствии, объекты соответствия предоставляют доступ к каждому соответствию подвыражению в схеме. К соответствиям подвыражению обращаются по позиции. Первое соответствие подвыражению, расположенное в позиции 0, представляет соответствие для всей схемы. После него располагается каждое подвыражение. Следовательно, имя файла, являющееся первым подвыражением в схеме, находится в позиции 1, а расширение файла — в позиции 2. Например, если именем файла будет foo.cpp, то results.str(0) содержит строку "foo.cpp"; results.str(1) — "foo", a results.str(2) — "cpp". В этой программе требуется часть имени перед точкой, что является первым подвыражением, поэтому следует вывести results.str(1). Подвыражения для проверки правильности данных Подвыражения обычно используются для проверки данных, которые должны соответствовать некоему определенному формату. Например, в Америке номера телефонов имеют десять цифр, включая код города и местный номер из семи цифр. Код города зачастую, но не всегда, заключен в круглые скобки. Остальные семь цифр могут быть отделены тире, точкой или пробелом либо не отделяться вообще. Данные в некоторых из этих форматов могли бы быть приемлемы, а в других — нет. Процесс будет состоять из двух этапов: сначала используем регулярное выражение для поиска последовательностей, которые могли бы быть номерами телефонов, а затем вызовем функцию для окончательной проверки правильности данных. Прежде чем написать схему номеров телефона, необходимо рассмотреть еще несколько аспектов языка регулярных выражений на языке ECMAScript. • \{d} представляет одиночную цифру, а \{d}{n} — последовательность из n цифр. (Например, \{d}{3} соответствует последовательности из трех цифр.) • Набор символов в квадратных скобках позволяет задать соответствие любому из трех символов. (Например, [-.] соответствует тире, точке или пробелу. Обратите внимание: у точки в квадратных скобках нет никакого специального смысла.) • Компонент, следующий за символом '?', не обязательный. (Например, \{d}{3}[-. ]?\{d}{4} соответствует трем цифрам, сопровождаемым опциональными тире, точкой или пробелом и еще четырьмя цифрами. Этой схеме соответствовало бы 555-0132, или 555.0132, или 555 0132, или 5550132). • Как и в языке С++, в ECMAScript символ за наклонной чертой означает, что он представляет себя, а не специальное значение. Поскольку данная схема включает круглые скобки, являющиеся специальными символами в языке ECMAScript, круглые скобки, являющиеся частью схемы, следует представить как \( или \). Page 926/1103 Поскольку наклонная черта влево является специальным символом в языке С++, когда он встречается в схеме, следует добавить вторую наклонную черту, чтобы указать языку С++, что имеется в виду символ \. Следовательно, чтобы представить регулярное выражение \{d}{3}, нужно написать \\{d}{3}. Для проверки номеров телефонов следует обратиться к компонентам схемы. Например, необходимо проверить, что если номер использует открывающую круглую скобку для кода города, то он использует также закрывающую скобку после него. В результате такой номер, как (908.555.1800, следует отклонить. Для определения такого соответствия необходимо регулярное выражение, использующее подвыражения. Каждое подвыражение заключается в пару круглых скобок: // все выражение состоит из семи подвыражений: (ddd) разделитель ddd // разделитель dddd // подвыражения 1, 3, 4 и 6 опциональны; а 2, 5 и 7 содержат цифры "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})"; Поскольку схема использует круглые скобки, а также из-за использования наклонных черт, эту схему трудно прочитать (и написать!). Проще всего прочитать ее по каждому отдельному (заключенному в скобки) подвыражению. 1. (\\()? необязательная открывающая скобка для кода города. 2. (\\d{3}) код города. 3. (\\))? необязательная закрывающая скобка для кода города. 4. ([-. ])? необязательный разделитель после кода города. 5. (\\d{3}) следующие три цифры номера. 6. ([-. ])? другой необязательный разделитель. 7. (\\d{4}) последние четыре цифры номера. Следующий код использует эту схему для чтения файла и находит данные, соответствующие общей схеме телефонных номеров. Для проверки допустимости формата номеров используется функция valid(): string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})"; regex r(phone); // объект regex для поиска схемы smatch m; Page 927/1103 string s; // прочитать все записи из входного файла while (getline(cin, s)) { // для каждого подходящего номера телефона for (sregex_iterator it(s.begin(), s.end(), r), end_it; it != end_it; ++it) // проверить допустимость формата номера if (valid(*it)) cout << "valid: " << it->str() << endl; else cout << "not valid: " << it->str() << endl; } Операции с типом соответствия Напишем функцию valid(), используя операции типа соответствия, приведенные в табл. 17.11. Не следует забывать, что схема pattern состоит из семи подвыражений. В результате каждый объект класса smatch будет содержать восемь элементов ssub_match. Элемент [0] представляет общее соответствие, а элементы [1] - [7] представляют каждое из соответствующих подвыражений. Таблица 17.11. Операции с типом соответствия Эти операции применимы к типам ssub_match, csub_match, wssub_match и wcsub_match matched Открытая логическая переменная-член, означающая соответствие объекта класса ssub_match first second Открытые переменные-члены, являющиеся итераторами на начало последовательности соответствия и ее следующий элемент после последнего. Если соответствия нет, то first и second равны length() Размер текущего объекта соответствия. Возвращает 0, если переменная-член matched содержит значение false str() Возвращает строку, содержащую соответствующую часть ввода. Возвращает пустую строку, если переменная-член matched содержит значение false s = ssub Преобразует объект ssub класса ssub_match в строку s. Эквивалент вызова s = ssub.str(). Оператор преобразования не является явным (см. раздел 14.9.1) Когда происходит вызов функции valid(), известно, что общее соответствие имеется, но неизвестно, какие из необязательных подвыражений являются частью этого соответствия. Переменная-член matched класса ssub_match, соответствующая определенному подвыражению, содержит значение true, если это подвыражение является частью общего соответствия. В правильном номере телефона код города либо полностью заключается в скобки, либо не заключается в них вообще. Поэтому действие функции valid() зависит от того, начинается ли номер с круглой скобки или нет: Page 928/1103 bool valid(const smatch& m) { // если перед кодом города есть открывающая скобка if (m[1].matched) // за кодом города должна быть закрывающая скобка // и остальная часть номера непосредственно или через пробел return m[3].matched && (m[4].matched == 0 || m[4].str() == " "); else // здесь после кода города не может быть закрывающей скобки // но разделители между другими двумя компонентами должны быть // корректны return !m[3].matched && m[4].str() == m[6].str(); } Начнем с проверки соответствия первому подвыражению (т.е. открывающей скобки). Это подвыражение находится в элементе m[1]. Если это соответствие есть, то номер начинается с открывающей скобки. В таком случае номер будет допустимым, только если подвыражение после кода города также будет соответствующим (т.е. будет закрывающая скобка после кода города). Кроме того, если скобки в начале номера корректны, то следующим символом должен быть пробел или первая цифра следующей части номера. Если элемент m[1] не соответствует (т.е. открывающей скобки нет), то подвыражение после кода города также должно быть пустым. Если это так и если остальные разделители совпадают, то номер допустим, но не в противном случае. Упражнения раздела 17.3.3 |