Цель: закрепление навыков работы с текстовыми файлами средствами Стандартной библиотеки C++.
Критерии полноты
“Рецепт” с командой fc
Пусть в текущем каталоге есть два текстовых файла result.txt и ref.txt и мы хотим сравнить их на равенство средствами командной строки. В этом случае удобно воспользоваться следующей командой (синтаксис cmd.exe):
fc result.txt ref.txt >nul && echo pass || echo fail
Команда выведет pass
, если файлы совпадают и fail
, если не совпадают.
В случае двоичных файлов достаточно добавить параметр /b
:
fc /b result.bin ref.bin >nul && echo pass || echo fail
Нужную команду можно сохранить в виде .cmd файла, чтобы не вводить её вручную. Задержка экрана выполняется командой pause
.
По использованию объектов fstream см. также здесь.
Для конкатенации заданных входных файлов вывести последовательность строк, пропуская (т.е. не сохраняя в новом файле) подряд идущие повторяющиеся строки из конкатенации исходных файлов.
Если очередной входной файл заканчивается не переводом строки, то следующий файл начинать с новой строки.
Для решения данной задачи удобно определить класс, по мере надобности открывающий нужные нам файлы.
class File_sequence
{
public:
/// Конструктор по умолчанию == пустая последовательность файлов.
File_sequence();
/// Инициализировать очередь имён, передав диапазон имён файлов.
template <class InIt>
File_sequence(InIt from, InIt to);
/// Добавить в конец очереди имя ещё одного файла.
void push(const std::string &filename);
/// Обратиться к текущему файлу как к потоку ввода (эту ссылку нельзя сохранять).
std::istream& access_once();
/// Проверить состояние текущего файла.
/// Возвращает false либо из-за ошибки чтения (access_once().fail()), либо из-за того, что очередь закончилась.
operator bool();
};
Фактически, данный интерфейс предлагает всего две возможности: добавить имена файлов, конкатенацию которых мы хотим получить (второй конструктор и push), и обратиться к текущему файлу (access_once). Если файл “кончился”, то функция access_once откроет следующий доступный файл.
Использовать данный класс можно так:
Для удобства определим обёртку стандартной функции getline так:
/// Считать одну строку, замена std::getline для File_sequence.
inline File_sequence& getline(File_sequence &fs, std::string &line)
{
std::getline(fs.access_once(), line);
return fs;
}
Теперь можно читать строки с помощью обычной конструкции на основе цикла for:
File_sequence fs;
fs.push(filename);
for (string line; getline(fs, line);)
cout << line << '\n'; // выводим файл построчно на экран
В случае последовательности файлов:
File_sequence fs;
// Читать файлы с именами 1.txt, 2.txt, ..., 100.txt.
for (int i = 1; i < 101; ++i)
fs.push(to_string(i) + ".txt");
for (string line; getline(fs, line);)
cout << line << '\n'; // выводим всё построчно на экран
Для реализации File_sequence удобно использовать стандартный класс “очередь” (queue).
private:
std::queue<std::string> filenames; // очередь имён файлов, ожидающих открытия.
std::ifstream current_file; // текущий файл.
В случае, если текущий файл кончился, или ещё ни один файл не был открыт, нужно открыть некоторый “следующий” файл, либо определить, что больше доступных файлов нет.
Определим функцию open_next_file, которая будет пытаться открыть файл, начиная с текущего имени и до конца, извлекая имена файлов из очереди filenames.
private:
// Открыть следующий файл из очереди.
void open_next_file()
{
current_file.close();
while (!filenames.empty() && !current_file.is_open())
{
current_file.open(filenames.front());
filenames.pop();
}
}
Теперь можно определить функции открытого интерфейса.
Конструкторы:
public:
/// Конструктор по умолчанию.
File_sequence() {}
/// Задать набор файлов диапазоном имён.
template <class InIt>
File_sequence(InIt from, InIt to)
: filenames(std::deque<std::string>(from, to)) {}
// std::queue по умолчанию работает поверх объекта std::deque,
// который можно передать конструктору queue.
// Мы используем конструктор deque для создания объекта deque из диапазона элементов.
По умолчанию объект класса File_sequence нельзя скопировать, потому что std::ifstream не имеет копирующих конструктора и оператора присваивания. Впрочем, если требуется, можно реализовать нужные операции копирования “руками”, потому что к одному файлу можно привязать несколько объектов ifstream (поток не владеет файлом).
Добавление нового имени файла в конец списка:
/// Добавить в конец набора файлов заданный файл.
void push(const std::string &filename)
{
filenames.push(filename);
}
Обращение к вложенному объекту потока выполняет проверку состояния текущего файлового потока и пытается открыть следующий файл в случае необходимости:
/// Обратиться к текущему файлу.
std::istream& access_once()
{
if (!current_file.is_open() || current_file.eof())
open_next_file();
return current_file;
}
/// Проверить состояние текущего файла.
/// Возвращает false либо из-за ошибки чтения (access_once().fail()), либо из-за того, что очередь закончилась.
operator bool() { return access_once().good(); }
Итак, теперь можно написать код решения исходной задачи:
int main(int argc, const char *argv[])
{
using namespace std;
File_sequence lines(argv + 1, argv + argc);
if (argc < 2)
{
// Прочитать имена файлов с потока ввода.
for (string filename; getline(cin, filename);)
lines.push(filename);
cin.clear();
}
// Do the job.
string prev;
if (getline(lines, prev))
{
cout << prev << '\n';
for (string line; getline(lines, line); )
if (prev != line)
cout << (prev = line) << '\n';
}
return EXIT_SUCCESS;
}
Для конкатенации набора текстовых файлов (как последовательностей строк) вычислить минимальное, максимальное и среднее количество непробельных символов в строке.
Позаимствуем из примера 1 часть решения, в частности, класс File_sequence и каркас консольной программы.
Определим функцию count_nonspace, вычисляющую количество непробельных символов в заданной строке:
std::size_t count_nonspace(const std::string &line, const std::locale &gl)
{
std::size_t result = 0;
for (auto ch : line)
result += !std::isspace(ch, gl);
return result;
}
Для классификации символов используются средства локализации из Стандартной библиотеки C++.
Для подсчёта минимума, максимума и среднего определим класс “статистики”, который будет функтором, принимающим числа (т.е. замеры, статистику по которым отслеживает объект этого класса) и отслеживающим текущие минимум, максимум, подсчитывающим количество замеров и их сумму.
В C++ объекты, определяющие оператор () и позволяющие синтаксически обращаться к себе как к функции, называются функторы functors (термин специфичен для C++).
Функторы широко используется и являются аналогом “замыканий” closures, популярных в функциональном программировании. В отличие от обычной функции, объект-функтор имеет собственное локальное состояние, сохраняемое между вызовами.
/// Функтор, сохраняющий "статистику" переданных ему значений
/// (минимум, максимум, общее количество).
template <class Num>
class Statistics
{
Num minv, maxv, sumv; // минимум, максимум, сумма
std::size_t n; // количество замеров
using Limits = std::numeric_limits<Num>;
public:
Statistics()
: minv(Limits::max()), maxv(Limits::lowest()), sumv(), n(0) {}
/// Обработать следующий замер.
Statistics& operator()(Num num)
{
++n;
sumv += num;
if (num < minv)
minv = num;
else if (maxv < num)
maxv = num;
return *this;
}
/// Получить минимум из всех замеров.
Num min() const { return minv; }
/// Получить максимум из всех замеров.
Num max() const { return maxv; }
/// Получить накопленную сумму.
Num sum() const { return sumv; }
/// Получить количество всех замеров.
std::size_t count() const { return n; }
};
Наконец, само консольное приложение:
int main(int argc, const char *argv[])
{
using namespace std;
File_sequence lines(argv + 1, argv + argc);
if (argc < 2)
{
// Прочитать имена файлов с потока ввода.
for (string filename; getline(cin, filename);)
lines.push(filename);
cin.clear();
}
// NEW
locale gl; // глобальная локаль по умолчанию.
Statistics<size_t> stats;
for (string line; getline(lines, line); )
stats(count_nonspace(line, gl));
// Вывести результат
cout << "Count: " << stats.count();
cout << "\nMin: " << stats.min();
cout << "\nMax: " << stats.max();
cout << "\nAvg: " << double(stats.sum()) / stats.count();
cout << endl;
return EXIT_SUCCESS;
}
Дополнительные изменения: добавлен вывод сообщений о невозможности открыть файл (см. File_sequence::open_next_file).
Для заданных файлов вывести строки, встречающихся в каждом из исходных файлов. Иными словами, построить пересечение заданных файлов как множеств строк. Алгоритм должен быть не хуже квадратичного по числу строк.
Для заданных файлов вывести строки, встречающихся только в одном из исходных файлов (хотя, возможно, и неоднократно в этом одном файле). Алгоритм должен быть не хуже квадратичного по числу строк.
Для заданных файлов вывести строки, встречающихся строго один раз. Алгоритм должен быть не хуже квадратичного по числу строк.
Для заданных файлов вывести строки, встречающихся по крайней мере в двух из исходных файлов. Повторения строки внутри одного файла не должны влиять на результат работы. Алгоритм должен быть не хуже квадратичного по числу строк.
Для последовательности файлов, каждая строка которых представляет собой запись вектора произвольной размерности (не указанной явно), вычислить среднее геометрическое евклидовых длин всех векторов.
Координаты векторов записаны как числа с плавающей запятой и отделены друг от друга пробельными символами (кроме перевода строки, который отделяет друг от друга векторы).
Для конкатенации последовательности файлов определить частоты (количества) всех встречающихся в нём пар идущих друг за другом букв без учёта регистра. Пары с нулевыми частотами не выводить.
Для конкатенации последовательности файлов определить частоты (количества) всех встречающихся в нём троек идущих друг за другом букв без учёта регистра. Тройки с нулевыми частотами не выводить.
Для конкатенации последовательности файлов и заданной ширины колонки (табуляции) в символах заменить каждый символ табуляции на пробелы, выравнивающие положение следующего символа для попадания на следующую колонку (их может быть от нуля до заданной ширины). Остальные символы оставить неизменными.
Пример
Символы табуляции обозначены стрелкой →
. Исходный файл содержит:
template
→<
→→class String→→= std::string,
→→class ErrorPolicy→= Formatter_error_policy_ignore,
→→class Traits→→= String_traits<String>
→>
Пусть ширина колонки равна 4, тогда результирующий файл будет иметь вид:
template
<
class String = std::string,
class ErrorPolicy = Formatter_error_policy_ignore,
class Traits = String_traits<String>
>
Для конкатенации последовательности файлов сформировать набор новых файлов, извлекая из него блоки вида:
--(new-file-name
contents
contents
...
contents
--new-file-name)
где new-file-name — любое “слово”, состоящее из непробельных символов. Это слово следует использовать в качестве имени нового файла. В этот новый файл должны попасть строки старого файла, находящиеся между строкой
--(new-file-name
и строкой
--new-file-name)
Таких блоков может быть произвольное число с произвольными именами (об ошибочных именах выводить сообщение в стандартный поток сообщений об ошибках), но блоки не могут быть вложенными, т.е. даже если аналогичные метки встречаются внутри блока, они сохраняются в новый файл вместе с прочим содержимым блока.
Макроподстановка. В отдельном файле заданы определения подстановок в виде последовательности строк
макрос = слово
где макрос
и слово
— последовательности символов, не включающих пробельные символы и знак $.
Данные пары определяют текстовые подстановки, которые надо осуществить в конкатенации файлов, заменяя последовательности $
макрос$
на соответствующие слово. Данную замену можно выполнять построчно (так как перевод строки не может содержаться в макрос).
Пример
Дан файл макроподстановок:
d = <div class=
. = >
/d = </div>
s = <span class=
/s = </span>
Дан исходный текст (например, фрагмент конкатенации файлов):
$d$"def"$.$
Алгоритм $s$"eng"$.$algorithm$/s$ --- точное предписание, задающее последовательность действий.
$/d$
Результат:
<div class="def">
Алгоритм <span class="eng">algorithm</span> --- точное предписание, задающее последовательность действий.
</div>
Для последовательности файлов, каждая строка которых представляет собой запись координат точки произвольной размерности (не указанной явно), вычислить наименьший параллелотоп со сторонами, параллельными координатным плоскостям, вмещающий все перечисленные точки. То есть ни что иное, как последовательность минимальных значений координат и последовательность максимальных последовательностей координат (две строчки).
Если точки имеют разную размерность, то точки малой размерности не влияют на на результат в дополнительных измерениях.
Пример
Дана последовательность точек:
1 2 3
-1 4 6 -5
3 -1.5
Результат (выравнивание по колонкам желательно, но не обязательно):
-1 -1.5 3 -5
3 4 6 -5
Для последовательности файлов исходного кода определить максимальное число отступов. Строки, содержащие только пробельные символы, не учитывать. Один символ табуляции — один отступ. Ширину отступа в пробелах задавать именованной константой.
Кувшинов Д.Р. © 2016