Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
Дмитрий Козлюк 0f7e8a2c02
lab03: use vendored doctest
2 недель назад
..
assets lab03: vendor doctest.h with old MinGW fix 2 недель назад
README.md lab03: use vendored doctest 2 недель назад

README.md

Структурирование программ

Цель работы

  1. Уметь определять структуры.
  2. Уметь структурировать программу при помощи функций.
  3. Уметь писать программы из нескольких единиц трансляции.
  4. Уметь писать модульные тесты.

Задание

Работа ведется на основе кода лабораторной работы № 1.

  1. Структурировать программу при помощи функций:

    • Определить структуру Input для хранения исходных данных.
    • Вынести ввод данных в функцию input_data().
    • Вынести поиск минимума и максимума в функцию find_minmax().
    • Вынести расчет количества чисел в корзинах в функцию make_histogram().
    • Вынести отображение гистограммы в функцию show_histogram_text().
  2. Разделить программу на единицы трансляции:

    • main.cpp: основная программа;
    • histogram.cpp: функции для расчетов;
    • text.cpp: отображение гистограммы в виде текста.
  3. Написать программу с модульными тестами функции find_minmax().

  4. Перевести программу на отображение гистограммы в формате SVG.

В результате должно быть две программы с частично общим кодом. Основная программа при запуске без параметров работает так же, как ЛР № 1 в базовом виде (не своего варианта), но выводит гистограмму в формате SVG. Вторая программа выполняет модульные тесты функции find_minmax().

Код должен быть загружен в репозитарий cs-lab34. Начальный коммит должен содержать код ЛР № 1 без изменений. Должны быть коммиты, фиксирующие выполнение пунктов задания, с номером и описанием пункта в первой строке сообщения к коммиту. Можно делать больше промежуточных коммитов на свое усмотрение.

Отчета не нужно.

Указания к выполнению

1. Импорт кода ЛР № 1 в Git

  1. В каталоге с файлами ЛР № 1 инициализируйте репозитарий Git.
  2. Настройте свое имя пользователя и почту МЭИ.
  3. Закоммитьте файл исходного кода (*.cpp) и проекта (*.cbp для CodeBlocks).
  4. Настройте игнорирование артефактов сборки (bin/ и obj/ для CodeBlocks).
  5. Создайте пустой репозитарий cs-lab34 на Git УИТ.
  6. Загрузите код на сервер (при необходимости настройте доступ по 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> используется с указанием пространства имен.

Создание файла реализации

  1. При помощи меню 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

  2. Перенесите определения find_minmax() и make_histogram() в histogram.cpp.

  3. Сделайте функцию find_minmax() статической, так как она не нужна за пределами histogram.cpp.

  4. Подключите histogram.h в main.cpp и histogram.cpp:

    #include "histogram.h"
    

    Нужно использовать двойные кавычки, так как файл находится не в стандартном каталоге, а ищется относительно файла *.cpp.

  5. Убедитесь, что проект собирается и запускается.

Самостоятельно. Выделите show_histogram_text() в text.h и text.cpp.

4. Модульные тесты

Ранее для тестирования программы применялось перенаправление ввода и вывода. Такое тестирование называется функциональным. Однако функционального тестирования недостаточно для больших программ. Предположим, большая программа перестала выдавать правильный результат — как найти место ошибки? Необязательно это последнее изменение: возможно, другая часть и ранее работала неверно в некоторых случаях, и последняя правка лишь обнажила проблему.

Модульное тестирование (unit testing) проверяет не работу всей программы, а работу отдельных ее компонент, например, отдельных функций. Модульные тесты пишутся программистами для собственного кода. Функциональные тесты могут писаться или проводится вручную другими специалистами.

Модульный тест — это отдельная программа, которая изолированно проверяет части кода основной программы. Если желательно протестировать части сложного алгоритма, эти части должны быть оформлены в виде отдельных функций (говорят: код должен быть тестируемым).

Как сделать код тестируемым?

Функция find_minmax(), которую нужно протестировать, скрыта в histogram.cpp. Даже если разместить в другом файле её объявление, компоновка не прошла бы, потому что эта функция статическая. Это довольно типичная ситуация, когда желательно протестировать функции, которые нет смысла описывать в интерфейсе модуля. Она означает, что модуль достаточно сложный, и имеет смысл выделить его части в подмодули. С другой стороны, если перенести find_minmax() в отдельный файл, читать код станет только сложнее — придется переходить по файлам. Поэтому можно только объявление тестируемой функции описать в отдельном файле. Подключив его, к ней можно будет обращаться из теста, а сама функция будет в histogram.cpp, чтобы код оставался понятным.

  1. Сделайте функцию find_minmax() нестатической.

  2. Добавьте файл histogram_internal.h с объявлением find_minmax().

Создание проекта для модульных тестов

  1. При помощи меню 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 быть не должно.

  2. Дважды щелкните по проекту unittest, чтобы сделать его активным.

  3. Из контекстного меню проекта unittest выберите пункт Add files... и добавьте к проекту histogram_internal.h и histogram.cpp. В открывшемся диалоге Select the targets this file should belong to: проставьте все флажки.

  4. При помощи меню 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

Подключение библиотеки для модульного тестирования

Модульные тесты представляют собой проверки утверждений о том, как ведет себе код при различных условиях. Технически это набор коротких функций, каждая их которых:

  1. Готовит условия, например, формирует исходные данные:

    vector<double> numbers{1, 2};
    
  2. Вызывает тестируемый код, например:

    double min, max;
    find_minmax(numbers, min, max);
    
  3. Выполняет ряд проверок, например:

    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. Проверять введенное пользователем значение, и если оно не одно из допустимых, запрашивать значение заново, выдавая предупреждение.