|
2 недель назад | |
---|---|---|
.. | ||
assets | 2 недель назад | |
README.md | 2 недель назад |
README.md
Структурирование программ
Цель работы
- Уметь определять структуры.
- Уметь структурировать программу при помощи функций.
- Уметь писать программы из нескольких единиц трансляции.
- Уметь писать модульные тесты.
Задание
Работа ведется на основе кода лабораторной работы № 1.
-
Структурировать программу при помощи функций:
- Определить структуру
Input
для хранения исходных данных. - Вынести ввод данных в функцию
input_data()
. - Вынести поиск минимума и максимума в функцию
find_minmax()
. - Вынести расчет количества чисел в корзинах в функцию
make_histogram()
. - Вынести отображение гистограммы в функцию
show_histogram_text()
.
- Определить структуру
-
Разделить программу на единицы трансляции:
main.cpp
: основная программа;histogram.cpp
: функции для расчетов;text.cpp
: отображение гистограммы в виде текста.
-
Написать программу с модульными тестами функции
find_minmax()
. -
Перевести программу на отображение гистограммы в формате SVG.
В результате должно быть две программы с частично общим кодом.
Основная программа при запуске без параметров работает так же, как ЛР № 1
в базовом виде (не своего варианта), но выводит гистограмму в формате SVG.
Вторая программа выполняет модульные тесты функции find_minmax()
.
Код должен быть загружен в репозитарий cs-lab34
.
Начальный коммит должен содержать код ЛР № 1 без изменений.
Должны быть коммиты, фиксирующие выполнение пунктов задания,
с номером и описанием пункта в первой строке сообщения к коммиту.
Можно делать больше промежуточных коммитов на свое усмотрение.
Отчета не нужно.
Указания к выполнению
1. Импорт кода ЛР № 1 в Git
- В каталоге с файлами ЛР № 1 инициализируйте репозитарий Git.
- Настройте свое имя пользователя и почту МЭИ.
- Закоммитьте файл исходного кода (
*.cpp
) и проекта (*.cbp
для CodeBlocks). - Настройте игнорирование артефактов сборки (
bin/
иobj/
для CodeBlocks). - Создайте пустой репозитарий
cs-lab34
на Git УИТ. - Загрузите код на сервер (при необходимости настройте доступ по SSH).
2. Структурирование программы при помощи функций
Программа для построения гистограммы из ЛР № 1 состоит из одной функции
main()
на более чем 100 строк, из-за чего в ней неудобно ориентироваться.
Структура для входных данных
Входные данные включают вектор чисел и количество корзин:
struct Input {
vector<double> numbers;
size_t bin_count{};
};
Если ваш вариант ЛР № 1 требует ввода дополнительных данных, на этом этапе удаляйте весь специфичный для варианта код. Когда будете выполнять индивидуальный вариант к этой ЛР, добавьте в эту структуру дополнительные поля входных данных, если вариант того требует.
Поля примитивных типов (не vector
и не string
) рекомендуется
инициализировать нулевыми значениями, как это сделано для bin_count
.
Тогда при объявлении переменной типа Input
без инициализации
у полей будут удобные нулевые значения по умолчанию.
Функция ввода данных
Код для ввода данных в базовом варианте:
size_t number_count;
cin >> number_count;
vector<double> numbers(number_count);
for (size_t i = 0; i < number_count; i++) {
cin >> numbers[i];
}
size_t bin_count;
cin >> bin_count;
Требуется вынести его в функцию input_data()
,
чтобы в main()
вместо строк выше осталась одна:
Input in = input_data();
Функция не принимает параметров, а возвращает структуру Input
:
Input
input_data() {
Предлагается в ЛР писать тип возвращаемого значения на отдельной строке. В общем случае это регламентируется стилем кодирования в проекте.
Количество чисел не входит напрямую в Input
— входит вектор чисел,
который уже несет в себе количество элементов.
Тем не менее, ввод количества нужен для того, чтобы задать размер вектора.
Поэтому number_count
объявляется как локальная переменная
и вводится так же, как вводилась в функции main()
:
size_t number_count;
cin >> number_count;
Далее вводятся те данные, которые нужно будет возвращать из функции.
Это поля структуры Input
, поэтому нужно объявить переменную-экземпляр:
Input in;
По умолчанию вектор чисел in.numbers
инициализируется пустым,
а количество корзин in.bin_count
инициализируется нулем.
Нужно изменить размер вектора на введенный:
in.numbers.resize(number_count);
Элементы in.numbers
вводятся так же,
как элементы numbers
вводились в main()
:
for (size_t i = 0; i < number_count; i++) {
cin >> in.numbers[i];
}
Самостоятельно.
Напишите ввод in.bin_count
.
Если ваш вариант этой ЛР потребует ввода дополнительных данных, нужно будет добавлять его в эту часть, когда будете делать индивидуальную часть.
В конце функции нужно вернуть результат:
return in;
}
На этом этапе структура кода должна получаться такой:
#include ...
struct Input {
...
};
Input
input_data() {
...
}
int
main() {
Input in = input_data();
...
}
Функция поиска минимума и максимума
При поиске минимума и максимума результатов два.
Оператор return
не позволяет вернуть два значения,
если только они не упакованы в одно составное, например, в структуру.
Однако для тренировки используем выходные параметры:
double min, max;
find_minmax(in.numbers, min, max);
Нужно сделать так, чтобы этот код работал следующим образом: в теле функции
find_minmax()
изменялись бы аргументы min
и max
, и эти изменения
должны отразиться на переменных min
и max
в функции main()
.
Предположим, функция определена так:
void
find_minmax(vector<double> numbers, double min, double max) {
min = numbers[0];
// (здесь код поиска минимума и максимума)
}
Желаемого поведения не будет: параметры min
и max
в функции — это отдельные
переменные, хотя они и называются так же, как переменные в main()
. Когда
функция find_minmax()
вызывается, параметры min
и max
получают значения,
которым были равны второй и третий аргумент (говорят: передаются по значению).
Любые изменения min
и max
в теле функции не отразятся на аргументах.
Чтобы из find_minmax()
менять внешние переменные,
можно использовать указатели или ссылки.
С точки зрения организации выходных параметров, указатели отличаются тем,
что у них есть особое значение, нулевой указатель (nullptr
/0
/NULL
/{}
).
Однако в данном случае мы не заинтересованы в том, чтобы такое значение было:
функция всегда ищет минимум и максимум и всегда должна мочь их записать.
Используем ссылки (см. амперсанды после double
):
void
find_minmax(vector<double> numbers, double& min, double& max) {
min = numbers[0];
// (здесь код поиска минимума и максимума)
}
Выделите find_minmax()
, замените код в main()
её вызовом.
Добейтесь компиляции программы и проверьте её работу.
Оптимально ли выбран тип входного параметра numbers
?
Во-первых, изменять его не требуется, он может быть константным.
Во-вторых, поскольку он не объявлен ссылкой или указателем,
это отдельная переменная, которая получает значением копию вектора,
который передается в функцию.
Однако копия не нужна.
Итого выгоднее использовать ссылку, чтобы не было копирования,
но константную, чтобы пользователь функции (программист) понимал,
что она не изменяет этот вектор: const vector<double>& numbers
.
Измените тип параметра numbers
функции find_minmax()
.
Проверьте, что программа компилируется, а ее поведение не изменилось.
Функция расчета гистограммы
Функция make_histogram()
принимает вектор чисел и количество корзин.
Она возвращает количества чисел в корзинах, то есть вектор bins
.
В своей работе она вызывает find_minmax()
.
Прочие функции
В итоге структура кода в main()
должна получиться такой:
int
main() {
auto in = input_data();
auto bins = make_histogram(in.numbers, in.bin_count);
show_histogram_text(bins, ...);
}
Если ваш вариант ЛР № 1 требует дополнительных данных для вывода, на этом этапе удаляйте весь специфичный для варианта код.
Добейтесь корректной работы программы и сделайте коммит.
3. Разделение программы на файлы
Хорошей практикой является отделять часть программы, которая выполняет расчеты, от частей, которые занимаются вводом и выводом. Разделим программу на файлы так:
+-----------------------------+
| histogram.h |
+-----------------------------+
| объявление make_histogram() |
+-----------------------------+
↑ ↑
включает файл, включает файл,
вызывает функцию реализует функцию
↑ ↑
+------------------+ +------------------+
| main.cpp | | histogram.cpp |
+------------------+ +------------------+
| вызов | | определение |
| make_histogram() | | make_histogram() |
+------------------+ +------------------+
Создание заголовочного файла
При помощи меню File → New → File... добавьте к проекту заголовочный файл (C/C++ header). Он должен быть расположен в каталоге проекта:
Filename with full path: C:\cs-lab34\histogram.h
Add file to active project: (отмечена)
Примечание.
Здесь и далее каталог проекта обозначен как C:\cs-lab34
.
Всюду нужно заменять его на тот, который реально используется.
Заготовка файла включает «стража включения» (см. лекцию):
#ifndef HISTOGRAM_H_INCLUDED
#define HISTOGRAM_H_INCLUDED
#endif // HISTOGRAM_H_INCLUDED
Можно заменить его на более простой вариант #pragma once
или писать код в этом файле между #define
и #endif
.
Добавьте в этот файл объявление make_histogram()
:
#include <vector>
std::vector<size_t>
make_histogram(const std::vector<double>& numbers, size_t bin_count);
В заголовочных файлах не рекомендуется писать using namespace std;
,
потому что в файле реализации, куда этот заголовочный файл подключается,
нет возможности отменить эту директиву, а она нужна на всегда.
Поэтому в примере std::vector<T>
используется с указанием пространства имен.
Создание файла реализации
-
При помощи меню File → New → File... добавьте к проекту файл реализации (C/C++ source). Необходимые настройки:
Filename with full path:
C:\cs-lab34\histogram.cpp
Add file to active project in build target(s): нажать All, либо проставить флажки в пунктах Debug и Release -
Перенесите определения
find_minmax()
иmake_histogram()
вhistogram.cpp
. -
Сделайте функцию
find_minmax()
статической, так как она не нужна за пределамиhistogram.cpp
. -
Подключите
histogram.h
вmain.cpp
иhistogram.cpp
:#include "histogram.h"
Нужно использовать двойные кавычки, так как файл находится не в стандартном каталоге, а ищется относительно файла
*.cpp
. -
Убедитесь, что проект собирается и запускается.
Самостоятельно.
Выделите show_histogram_text()
в text.h
и text.cpp
.
4. Модульные тесты
Ранее для тестирования программы применялось перенаправление ввода и вывода. Такое тестирование называется функциональным. Однако функционального тестирования недостаточно для больших программ. Предположим, большая программа перестала выдавать правильный результат — как найти место ошибки? Необязательно это последнее изменение: возможно, другая часть и ранее работала неверно в некоторых случаях, и последняя правка лишь обнажила проблему.
Модульное тестирование (unit testing) проверяет не работу всей программы, а работу отдельных ее компонент, например, отдельных функций. Модульные тесты пишутся программистами для собственного кода. Функциональные тесты могут писаться или проводится вручную другими специалистами.
Модульный тест — это отдельная программа, которая изолированно проверяет части кода основной программы. Если желательно протестировать части сложного алгоритма, эти части должны быть оформлены в виде отдельных функций (говорят: код должен быть тестируемым).
Как сделать код тестируемым?
Функция find_minmax()
, которую нужно протестировать, скрыта в histogram.cpp
.
Даже если разместить в другом файле её объявление, компоновка не прошла бы,
потому что эта функция статическая.
Это довольно типичная ситуация, когда желательно протестировать функции,
которые нет смысла описывать в интерфейсе модуля.
Она означает, что модуль достаточно сложный,
и имеет смысл выделить его части в подмодули.
С другой стороны, если перенести find_minmax()
в отдельный файл,
читать код станет только сложнее — придется переходить по файлам.
Поэтому можно только объявление тестируемой функции описать в отдельном файле.
Подключив его, к ней можно будет обращаться из теста,
а сама функция будет в histogram.cpp
, чтобы код оставался понятным.
-
Сделайте функцию
find_minmax()
нестатической. -
Добавьте файл
histogram_internal.h
с объявлениемfind_minmax()
.
Создание проекта для модульных тестов
-
При помощи меню File → New → Project... создайте новый проект типа Empty project.
Важно. Не выбирайте Console application, иначе содержимое
main.cpp
будет заменено на пустую заготовку программы.Необходимые настройки:
Project title:
unittest
Folder to create project in:C:\cs-lab34
Project filename:unittest.cbp
Resulting filename:C:\cs-lab34\unittest.cbp
В последнем параметре подкаталога
unittest
быть не должно. -
Дважды щелкните по проекту
unittest
, чтобы сделать его активным. -
Из контекстного меню проекта
unittest
выберите пункт Add files... и добавьте к проектуhistogram_internal.h
иhistogram.cpp
. В открывшемся диалоге Select the targets this file should belong to: проставьте все флажки. -
При помощи меню File → New → File... добавьте к проекту
unittest
файл реализации (C/C++ source)unittest.cpp
. Необходимые настройки:Filename with full path:
C:\cs-lab34\unittest.cpp
Add file to active project in build target(s) — нажать All, либо проставить флажки в пунктах Debug и Release
Подключение библиотеки для модульного тестирования
Модульные тесты представляют собой проверки утверждений о том, как ведет себе код при различных условиях. Технически это набор коротких функций, каждая их которых:
-
Готовит условия, например, формирует исходные данные:
vector<double> numbers{1, 2};
-
Вызывает тестируемый код, например:
double min, max; find_minmax(numbers, min, max);
-
Выполняет ряд проверок, например:
if (min != 1) { puts("min != 1"); } if (max != 2) { puts("max != 2"); }
Важно, чтобы последний пункт в случае ошибок давал внятную диагностику: в каком тесте какая проверка не прошла, какое значение было ожидаемым, какое актуальное значение выдала функция. Печатать все эти сведения вручную для каждого теста накладно и чревато ошибками. Поэтому модульные тесты пишут с использованием библиотек.
Мы предлагаем использовать библиотеку doctest
,
потому что её очень просто подключить к проекту — это один заголовочный файл.
На практике самой популярной является Google Test, но её сложнее подключать.
В целом, все такие библиотеки похожи, но отличаются продвинутыми возможностями.
Загрузите doctest.h и сохраните в каталоге проекта. Закоммитьте этот файл.
Написание модульных тестов
Напишем тест для простейшего случая в файле unittest.cpp
:
#define DOCTEST_CONFIG_NO_MULTITHREADING
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "histogram_internal.h"
TEST_CASE("distinct positive numbers") {
double min = 0;
double max = 0;
find_minmax({1, 2}, min, max);
CHECK(min == 1);
CHECK(max == 2);
}
Функцию main()
писать не нужно — doctest автоматически определит её.
Каждый тест должен определяться через макрос TEST_CASE()
, как показано выше.
В скобках описывается смысл теста, doctest будет показывать его в сообщениях.
Тест всегда включает один или несколько вызовов макроса CHECK()
.
При ошибках doctest покажет, какая из проверок не сработала,
и всю дополнительную информацию.
Вводная документация doctest.
Соберите проект модульных тестов, запустите его и убедитесь, что он работает без ошибок.
Проверим, что будет, если тест выявляет ошибку.
Замените CHECK(min == 1)
на CHECK(min == 3)
,
соберите проект и запустите его.
Верните код к правильному варианту.
Самостоятельно. Придумайте больше тестовых случаев и напишите для них тесты. В частности, можно проверить случаи:
- пустого вектора
- вектора с одним элементом
- вектора отрицательных элементов
- вектора из одинаковых элементов
5. Вывод гистограммы как изображения в формате SVG
Требуется вместо текстовой гистограммы рисовать картинку, например:
Это изображение описывается таким текстом, который должна выводить программа:
<?xml version='1.0' encoding='UTF-8' ?>
<svg width='300' height='100' viewBox='0 0 300 100' xmlns='http://www.w3.org/2000/svg'>
<text x='20' y='20'>2</text>
<rect x='50' y='0' width='100' height='30' fill='#ffaaaa' />
<text x='20' y='50'>5</text>
<rect x='50' y='30' width='250' height='30' fill='#aaffaa' />
<text x='20' y='80'>3</text>
<rect x='50' y='60' width='150' height='30' fill='#aaaaff' />
</svg>
Первые две строки и последняя строка — фиксированные.
Строки с отступом — это подписи к столбцам (элементы text
)
и столбцы в виде цветных прямоугольников (rectangle, элементы rect
).
На каждый столбец программа должна печатать строку <text...
и строку <rect...
со значениями x
, y
, width
и height
,
вычисленными в зависимости от номера столбца и количества попавших в него чисел.
Форматы разметки
Возьмем любую страницу в интернете, например, эту. На ней присутствует визуальное оформление: заголовки крупные и жирные, ссылки подчеркнуты и по ним можно перейти, между абзацами есть отступы. Можно догадаться, что изображение на экране не приходит с сервера в готовом виде, потому что страница выглядит по-разному на телефоне и на компьютере, например. Страница передается в виде разметки — текста со специальными элементами — на основании которой браузер рисует на экране один текст как заголовок, второй — как ссылку, третий — отдельным абзацем.
Например, возьмем такой абзац:
Текст с полужирными словами и ссылкой в никуда.
Бразуер отображает его из следующего фрагмента разметки (язык разметки называется HTML, hyper-text markup language):
<p>Текст <strong>с полужирными</strong> словами и <a href="#">ссылкой</a> в никуда.</p>
Всегда можно нажать Ctrl+U и увидеть исходный код страницы (разметку).
SVG работает так же, только разметка описывает не текст с оформлением, а изображение. HTML указывает, как отображать определенные блоки текста, а SVG указывает, какие геометрические фигуры нарисовать.
Если щелкнуть правой кнопкой мыши по изображению выше и выбрать «Показать изображение», оно откроется отдельно. Тем же Ctrl+U можно посмотреть код разметки гистограммы, который в итоге будет выводить программа.
Если если программа будет печатать размету SVG вместо звездочек,
можно перенаправить вывод программы в файл с расширением *.svg
и открыть его браузером — будет нарисовано изображение.
В случае ошибки оно нарисовано не будет.
В этом случае можно Ctrl+U посмотреть, что программа вывела не так.
Как правило, в окне кода, которое открывается по Ctrl+U,
красным цветом выделяются синтаксические ошибки, то есть некорректная разметка.
Формат SVG
Разберем заголовок SVG:
<?xml version='1.0' encoding='UTF-8' ?>
<svg width='300' height='100' viewBox='0 0 300 100' xmlns='http://www.w3.org/2000/svg'>
Так описывается изображение размером 300×100 точек.
Система координат SVG отличается от математической: ось Y направлена вниз, то есть точка (0,0) находится в верхнем левом углу. Координаты в SVG действительные, то есть может быть точка (0.5, 3.14).
Фрагменты вида <svg>
, <text>
или <rect>
называются элементами.
Параметры вида height='300'
называются атрибутами элементов.
Элементы имеют открывающий тэг (<svg>
), закрывающий тэг (</svg>
)
с косой чертой и содержимое между ними (<text>содержимое</text>
).
Если содержимого нет, то тэг может быть сразу закрыт: <rect ... />
.
Подробнее об SVG можно прочитать в спецификации, но все нужные для ЛР сведения даны в этих указаниях.
SVG поддерживает множество элементов, среди них:
<text x='20' y='35'>anything you want</text>
: текст «anything you want», левый нижний угол которого в точке (20,35);<rect x='0' y='0' width='100' height='200' />
: прямоугольник 100×200 с верхним левым углом в точке (0,0).
Цикл отладки функций вывода SVG
Вывод отдельных элементов SVG (заголовка, окончания, текста, прямоугольника)
стоит оформить в виде функций, который будут принимать параметры геометрии
и печатать соответствующий текст.
Функция show_histogram_svg()
будет выводить гистограмму в формате SVG.
Функции для работы с SVG рекомендуется выделить в модуль svg.h
/svg.cpp
.
Отработаем цикл модификации и проверки программы. Функции вывода заголовка и окончания SVG:
void
svg_begin(double width, double height) {
cout << "<?xml version='1.0' encoding='UTF-8'?>\n";
cout << "<svg ";
cout << "width='" << width << "' ";
cout << "height='" << height << "' ";
cout << "viewBox='0 0 " << width << " " << height << "' ";
cout << "xmlns='http://www.w3.org/2000/svg'>\n";
}
void
svg_end() {
cout << "</svg>\n";
}
Обратите внимание на пробелы в строках, например, перед закрывающей кавычкой
во фрагменте "viewBox='0 0 "
.
Они необходимы между атрибутами.
Также обратите внимание на использование одинарных кавычек.
Двойные кавычки ограничивают в C++ строковые литералы;
одинарные кавычки внутри них выводятся «как есть» в результирующую строку.
Путаница кавычек или отсутствие некоторых пробелов сделает SVG некорректным.
Пусть начальная реализация графического вывода гистограммы всегда выводит пустое изображение фиксированного размера:
void
show_histogram_svg(const vector<size_t>& bins) {
svg_begin(400, 300);
svg_end();
}
Заменим вызов show_histogram_text(bins)
вызовом show_histogram_svg(bins)
.
Создать файл изображения можно так
(marks.txt
можно скачать или создать):
C:\cs-lab34\bin\Debug> cs-lab34.exe <marks.txt >marks.svg
Эту команду нужно вводить в консоли, открытой в папке с exe-файлом проекта.
Если проект называется не cs-lab34
, название файла нужно написать свое.
Просмотреть, какие файлы есть в текущем каталоге, можно командой dir
.
Чтобы быстро просмотреть из консоли содержимое marks.svg
(разметку),
можно использовать команду:
C:\cs-lab34\bin\Debug> type marks.svg
В браузере marks.svg
открывается из меню Файл → Открыть...
Отобразится пустая страница (пустой рисунок).
Можно нажать Ctrl+U или пункт «Исходный код страницы»
в контекстном меню любого места страницы, чтобы увидеть результирующий код SVG
(контекстное меню — это то, что появляется по щелчку
правой кнопкой мыши в любом пустом месте страницы).
Функции вывода элементов SVG
Для вывода подписей к столбцам напишем функцию вывода текста в SVG, которая
принимает координату по горизонтали (left
), координату нижнего края текста
по вертикали (baseline
) и сам текст:
void svg_text(double left, double baseline, string text);
Из введения в SVG выше известно, что она должна выводить строку такого формата:
<text x='20' y='35'>anything you want</text>
На C++ точно такую строку можно вывести следующим образом (svg_text()
)
должна размещаться выше show_histogram_svg()
:
void
svg_text(double left, double baseline, string text) {
cout << "<text x='20' y='35'>anything you want</text>";
}
Чтобы вместо координаты x
(числа 20) выводить значение left
:
cout << "<text x='" << left << "' y='35'>anything you want</text>";
Обратите внимание на то, что сохранены одинарные кавычки внутри строк C++ (в двойных кавычках) — они необходимы по правилам SVG.
Самостоятельно.
Закончите реализацию svg_text()
,
чтобы подставлять значение координаты baseline
и текст надписи text
.
Для проверки выведем высоту первого столбца гистограммы:
void
show_histogram_svg(const vector<size_t>& bins) {
svg_begin(400, 300);
svg_text(20, 20, to_string(bins[0]));
svg_end();
}
Функция to_string()
преобразует значения разных типов в строки.
Чтобы проверить модифицированную программу, её нужно запустить повторно:
C:\cs-lab34\bin\Debug> cs-lab34.exe <marks.txt >marks.svg
После повторного запуска программы открытый в браузере файл можно обновить клавишей F5. На странице должен быть виден текст «2». Если в файле ошибка, он не отобразится. В этом случае нужно просмотреть код страницы на предмет ошибок в SVG, исправить код программы, перезапустить ее и проверить результат.
Самостоятельно. Напишите функцию для вывода прямоугольника в SVG:
void svg_rect(double x, double y, double width, double height);
Для проверки выведем первый столбец гистограммы справа от подписи к нему:
svg_rect(50, 0, bins[0] * 10, 30);
Убедитесь, что вывод первого столбца гистограммы работает.
Исключение «магических констант»
Текст выводится в координатах (20, 20), смещение левого края столбца 50 (оно же — ширина подписей), а высота столбца 30. Эти числа подобраны так, чтобы элементы гистограммы не накладывались друг на друга, но они разбросаны по коду и их смысл неясен.
Прямо внутри функции show_histogram_svg()
стоит завести константы,
чтобы пользоваться ими при выводе:
const auto IMAGE_WIDTH = 400;
const auto IMAGE_HEIGHT = 300;
const auto TEXT_LEFT = 20;
const auto TEXT_BASELINE = 20;
const auto TEXT_WIDTH = 50;
const auto BIN_HEIGHT = 30;
const auto BLOCK_WIDTH = 10;
Вывод гистограммы
Логика вывода гистограммы следующая: каждая корзина выводится так же, как первая, но к вертикальной координате добавляется смещение — высота столбца:
double top = 0;
for (size_t bin : bins) {
const double bin_width = BLOCK_WIDTH * bin;
svg_text(TEXT_LEFT, top + TEXT_BASELINE, to_string(bin));
svg_rect(TEXT_WIDTH, top, bin_width, BIN_HEIGHT);
top += BIN_HEIGHT;
}
Оформление гистограммы. Значения параметров по умолчанию
Черная гистограмма не слишком эстетична или экономична при печати.
За цвет линий в SVG отвечает атрибут stroke
, а за цвет заливки — fill
.
Можно задать один из стандартных цветов или выбрать цвет
в формате #RRGGBB
из палитры. Пример прямоугольника с красными
границами и бледно-розовой заливкой:
<rect x='50' y='0' width='30' height='30' stroke='red' fill='#ffeeee'/>
Функция svg_rect()
может быть доработана для указания цвета линий и заливки:
void svg_rect(double x, double y, double width, double height,
string stroke, string fill);
Самостоятельно. Измените цвета вывода и проверьте работу программы.
Значения параметров по умолчанию
Цвета элементов нужно задавать не всегда.
В текущей реализации у функции svg_rect()
шесть параметров,
два из которых отвечают за цвет.
Можно было бы перегрузить функцию:
void svg_rect(double x, double y, double width, double height);
void svg_rect(double x, double y, double width, double height,
string stroke, string fill);
Однако в этом случае пришлось бы в каждой функции писать похожий код. В данном случае выгоднее применить значения параметров по умолчанию:
void svg_rect(double x, double y, double width, double height,
string stroke = "black", string fill = "black");
Значения по умолчанию указываются только один раз в объявлении функции
(если объявление и определение отделены).
Новая версия svg_rect()
работает так:
svg_rect(0, 0, 100, 200); // svg_rect(0, 0, 100, 200, "black", "black");
svg_rect(0, 0, 100, 200, "red"); // svg_rect(0, 0, 100, 200, "red", "black");
svg_rect(0, 0, 100, 200, "blue", "#aaffaa");
Самостоятельно.
Реализуйте для гистограммы в SVG масштабирование:
самый длинный столбец должен занимать (IMAGE_WIDTH - TEXT_WIDTH)
точек.
Индивидуальные задания
Результат выполнения нужно оформить отдельным коммитом на GitHub. Доработку следует делать с использованием функции, для который нужно добавить unit-тесты из не менее двух существенно отличающихся случаев.
Пример 1.
По заданию нужно запросить у пользователя цвет заливки столбцов,
а если введен красный, отказаться его использовать и запросить повторно.
Можно выделить функцию bool check_color(string color)
, которая принимает
введенную пользователем строку цвета и возвращает true
, если цвет
подходящий (не красный). Для нее можно придумать положительный тест,
что check_color("blue") == true
и отрицательный, что check_color("red") == false
.
Пример 2. По заданию требуется добавить гистограмме заголовок Гистограмма по центру. Можно выделить функцию, которая принимает ширину и высоту изображения и вычисляет координаты надписи. Тестовые случаи — горизонтальное и вертикальное изображение (ширина больше или меньше высоты соответственно).
В вариантах примеры приведены для текстовых гистограмм, но решение должно выдавать SVG.
Вариант 1
Дайте пользователю возможность задавать произвольную ширину столбца
гистограммы вместо 400. Считайте некорректной ширину менее 70, более 800
или менее трети количества чисел, умноженных на ширину блока (BLOCK_WIDTH
) —
предлагайте пользователю ввести ширину заново с указанием причины.
Вариант 2
Задавать автоматически яркость заливки каждого столбца гистограммы в градациях серого в зависимости от высоты столбца. Чем больше столбец, тем темнее заливка.
Сделать это можно, передавая цвет в параметр fill в формате "#RGB"
(red,
green, blue). "#111"
— самый темный,
"#222"
— чуть менее темный, ..., "#EEE"
— практически белый, "#FFF"
—
белый. В лабораторной работе использовать диапазон цветов от "#111"
для самого большого столбца до "#999"
для самого маленького столбца.
Поскольку используются градации серого, расчет сводится к вычислению только
одного значения и дублированию этого значения в качестве цвета каждого
из каналов (Red, Green, Blue). Для расчета цвета i-го столбца bins[i]
использовать формулу (10 - (bins[i] * 9) / max_count)
. По ней мы получаем
значение цвета одного канала (от 1 до 9), который затем записываем три раза.
Пример с текстом вместо SVG:
1|
▮
— цвет #999
5|
▮▮▮▮▮
— цвет #111
3|
▮▮▮
— цвет #555
Вариант 3
Измените высоту изображения IMAGE_HEIGHT = 700
. Дайте пользователю
возможность задавать высоту столбца гистограммы. Если итоговая высота
гистограммы больше IMAGE_HEIGHT
, рассчитывать высоту столбца как
(IMAGE_HEIGHT / bins_count)
.
Вариант 4
Подсчитайте процент элементов, попавших в столбец, как целое двузначное число
с %
в конце и отображайте этот процент после столбца гистограммы
с выравниванием:
3|*** 25%
5|***** 42%
4|**** 33%
Вариант 5
Отображайте гистограмму зеркально по аналогии с заданием этого варианта в лабораторной работе № 1.
Вариант 6
После запроса количества столбцов запросите цвет для каждого столбца.
Вариант 7
Вычислите среднюю высоту столбца. Если столбец выше, цвет столбца должен быть красным, если ниже или равен средней высоте, цвет зеленый.
Вариант 8
Запрашивать у пользователя размер шрифта. За размер шрифта отвечает атрибут
font-size
. Считать 12 значением по умолчанию. Не позволять вводить
значения менее 8 и более 32. В этом случае предлагайте пользователю ввести
значение заново с указанием причины.
Вариант 9
Запросите у пользователя ширину одного «блока» гистограммы BLOCK_WIDTH
.
Не позволяйте вводить ширину блока менее 3px и более 30px. В этом случае
предлагайте пользователю ввести ее заново с указанием причины.
Вариант 10
Отображайте гистограмму вертикально, с подписями сверху, по аналогии
с заданием этого варианта в лабораторной работе 1. Предусмотреть расчет
IMAGE_HEIGHT
таким образом, чтобы вся гистограмма вмещалась в область
рисунка.
Вариант 11
Добавьте рамку вокруг гистограммы, используя пунктирные линии. Для отрисовки
пунктирной линии можно использовать стандартный элемент <line>
, установив
в нем атрибут stroke-dasharray = '10 10'
Вариант 12
Добавьте на ось подписей границы столбцов по аналогии с заданием этого варианта в лабораторной работе 1.
Вариант 13
Отображайте гистограмму вертикально с подписями снизу.
*
*
* *
* *
* * *
_ _ _
3 5 1
Предусмотреть расчет IMAGE_HEIGHT
таким образом, чтобы вся гистограмма
вмещалась в область рисунка.
Вариант 14
Разделяйте каждый столбец пунктирными линиями длиной IMAGE_WIDTH
.
Запрашивайте у пользователя шаблон пунктира. Шаблон пунктира задается
в атрибуте stroke-dasharray
блока <line>
в виде stroke-dasharray = '20 10'
, где 20 означает длину черточки, 10 - длину промежутка. У пользователя
нужно запросить как длину черточки, так и длину промежутка.
Вариант 15
Добавьте горизонтальную шкалу под гистограммой по аналогии с заданием
лабораторной работы 1. Шкалу нужно разбить на интервалы, размер которых вводит
пользователь. Допустимы размеры от 2 до 9 BLOCK_WIDTH
, при некорректном
вводе печатайте сообщение со словом «ERROR» и завершайте работу программы.
Под нулевой, первой и последней отметкой шкалы требуется напечатать
соответствующие числа. Шкала должна быть во всю ширину гистограммы.
Вариант 16
После запроса количества столбцов запросить цвет линий для каждого столбца.
Проверять ввод: цвет должен либо начинаться с #
, либо не иметь внутри
пробелов.
Вариант 17
Задавать автоматически прозрачность заливки каждого столбца гистограммы
в зависимости от высоты столбца. Чем больше столбец, тем темнее заливка.
Сделать это можно, передавая процент прозрачности в параметр fill-opacity
в формате "0.7"
. 1 соответствует отсутствию прозрачности, 0 соответствует
полной прозрачности (отсутствию цвета)
Для расчета прозрачности каждого i-го столбца bins[i]
использовать формулу
(bins[i]) / max_count)
.
Пример:
1|
▮
— прозрачность 0.2
5|
▮▮▮▮▮
— прозрачность 1.0
3|
▮▮▮
— прозрачность 0.6
Вариант 18
Позволять пользователю делать оформление текста -
подчеркивание,
надчеркивание, зачеркивание
текста. За оформление шрифта отвечает атрибут text-decoration
. Сделать
'none'
значением по умолчанию. Допустимые значения: none
, underline
,
overline
, line-through
. Проверять введенное пользователем значение, и если
оно не одно из допустимых, запрашивать значение заново, выдавая предупреждение.