Давно приходилось выводить чего-нибудь в табличной форме на экран терминала? Ничего не расползалось? :).
Придумал крошечную библиотечку, которая умеет рисовать таблички.
Типа таких:
Ниже - код, который ее нарисовал (simple_table_example.c):
#include <stddef.h>
#include "simple_table.h"
// Пример:
// Нарисуем табличку , состоящую из 4 столбцов, которые мы подпишем как
// "Column 1", "Colun 2", "Col 3" и "Colum Number Four", как то так:
//
// Column 1|Colun 2|Col 3|Colum Number Four
// --------+-------+-----+-----------------
// | | |
//
// Заполним табличку разной чепухой
//
int main() {
// Дескриптор таблички.
table_layout_t t;
// Задаем настройки: использовать символы UTF8 для рамок, печатать "% " (процент и пробел)
// перед каждой новой строчкой и символ # с пробелом в конце каждой строчки
table_layout(&t, true, "% ", " #\r\n");
// Рисуем заголовок нашей таблички с названиями стобцов
table_header(&t, "Column 1", "Colun 2","Col 3", "Colum Number Four", NULL);
// Начинаем заполнять данными. Заполнение идет слева направо, сверху вниз.
// Если значение не влазит по ширине, то будет обрезано
table_data(&t, "Long Text");
table_data(&t, "Short");
table_data(&t, "Exact");
table_data(&t, "Very Long Text");
table_data(&t, 10000.123456f);
table_data(&t, 655355555);
table_data(&t, -65535);
table_data(&t, "Another quite long text");
table_data(&t, 1);
table_data(&t, -1);
table_data(&t, 666.666f);
table_data(&t, 777.777);
// На экране (в терминале) должно появится что-то вроде этого:
//
// % Column 1│Colun 2│Col 3│Colum Number Four #
// % ────────┼───────┼─────┼───────────────── #
// % Long Tex│Short │Exact│Very Long Text #
// % 10000.12│6553555│-6553│Another quite lon #
// % 1 │-1 │666.6│777.776978 #
//
// Если вместо символов рамок на экране отображается не пойми что, то замените второй аргумент
// в вызове table_layout() на false, что означает - не использовать UTF8, а использовать +,- и|
// для рисования табличек
//
return 0;
}
Используемая “библиотека“ написана на Си, должна сожительствовать без проблем с Си++ кодом
simple_table.h:
#ifndef simple_table_h
#define simple_table_h
#include <stdbool.h>
#define TABLE_COLUMN_MAX 16 // Максимальное число столбцов
#define TABLE_COLUMN_MAX_LENGTH 64 // Максимальная ширина одного столбца
// Прототипы. Запиханы под extern "C", на всякий случай.
//
#ifdef __cplusplus
extern "C" {
#endif
// Всякая таблица начинается с ее дескриптора - структуры для временного хранения настроек таблицы.
// Все функции работы с таблицой своим первым аргументом ожидают указатель на экземпляр этой структуры.
// Пример определения и использования:
//
// table_layout_t t; // Дескриптор. Можно, но не нужно инициализировать.
// // Можно использовать многократно, для вывода разных таблиц. Scratch area
//
// table_layout( &t, ... ); // Задаем параметры таблицы
// table_header( &t, ... ); // Рисуем заголовок
// table_data( &t, ... ); // Посылаем данные..
// table_data( &t, ... ); // Посылаем данные..
// table_data( &t, ... ); // Посылаем данные..
// ....
typedef struct {
unsigned char cols;
unsigned char cur;
unsigned char width[TABLE_COLUMN_MAX];
const char *hvc[3];
const char *tr[2];
} table_layout_t;
// Общие настройки таблички. Эту функцию можно вызывать в любое время на любом этапе печати таблички,
// но нужно вызвать хотя бы один раз ДО вызова остальных функций библиотечки (см. пример simple_table_example.c)
//
// Можно менять настройки для каждой выводимой строки таблицы, если вызывать эту функцию перед выводом очередной
// строки таблицы.
//
// Аргументы:
//
// t : Указатель на переменную типа table_layout_t, можно - неинициализированную.
// utf8_enable : Использовать +-| (false) или псевдографику (true)
// pre : Указатель на строку, которая будет печататься _перед_ каждой новой строкой таблицы
// (включая заголовок таблицы). Может быть пустой строкой или NULL, что то же самое,
// что и пустая строка, может содержать ANSI ESC последовательности для управления цветом
// выводимого текста и его атрибуты, может содержать UTF8 символы (смайлики, стрелочки и т.п.
// например)
// post : Указатель на строку, которая будет печататься _после_ каждой новой строкой таблицы
// (включая заголовок таблицы). Обычно тут что-то вроде "\r\n" или NULL, что то же самое,
// что и "\r\n", но может быть что угодно, главное, не забыть \r\n в конце а то таблица
// превратится в строчку
//
// Пример:
// table_layout_t t;
// table_layout(&t, false, NULL, NULL); // используем ASCII рамки
// table_layout(&t, true, "# ", " #\r\n"); // используем utf8 рамки, рисуем # в начале и конце каждой
// // строки таблицы
//
void table_layout(table_layout_t *t,bool utf8_enable,const char *pre,const char *post);
// Печать заголовка таблички - названия столбцов с разделителями и горизонтальной линией
// Количество столбцов определяется тем, сколько аргументов было использовано, например:
//
// table_header(&t, "Column 1", "Colun 2","Col 3", "Colum Number Four", NULL);
//
// Этот код нарисует заголовок таблицы, состоящей из четырех колонок, ширина каждой колонки
// определяется длинной (в байтах) названия колонки. В примере выше ширина первой колонки
// будет 8 символов, а третьей - пять.
//
// Аргументы:
// t : Указатель на табличку,
// first_col : Название первого столбца таблички
// ... : произвольное количество const char * аргументов, задающих названия столбцов
//
// Последний аргумент всегда должен быть NULL, а то все грохнется
//
void table_header( table_layout_t *t, const char *first_col, ... );
// Эти четыре функции определены в table.c через самодельный "темплейт" table_data_template()
// Когда компилятор будет встречать вызов вида table_data(...) то будет вызываться одна из функций ниже
// (какая именно - определяется типом аргмента)
//
void table_data_asciiz(table_layout_t *t,const char *data);
void table_data_unsigned(table_layout_t *t,unsigned int data);
void table_data_signed(table_layout_t *t,signed int data);
void table_data_float(table_layout_t *t,float data);
void __attribute__((noreturn)) table_data_bug(table_layout_t *t,unsigned int data);
#ifdef __cplusplus
};
#endif
// void table_data(table_layout_t *t, ARG)
//
// В зависимости от типа входных данных (ARG), на этапе компиляции, вызов table_data(...) будет преобразован
// к одному из вызовов ниже.
//
// Пример:
// int a = 10; float b = 2.2f; const char *c = "!!!"
// table_layout_t t;
// table_layout(&t, true,NULL,NULL);
// table_header(&t, "Col1", "Col2", "Col3", NULL);
// table_data(&t, a);
// table_data(&t, b);
// table_data(&t, c);
//
// Код ниже компилируется GCC и совместимыми с ним компиляторами, с поддержкой C11
// Ключевое слово _Generic выбирает выражение из списка по типу переменной _Item:
// Если _Item является float-переменной, то будет выбрано table_data_float и т.д.
// Если тип переменной _Item такой, что ничего из списка не подходит (например, _Item
// у нас определен как uint64_t ), то будет вызвана table_data_bug(), которая вызывает abort()
//
#define table_data(_Table, _Item) _Generic((_Item), \
const char * : table_data_asciiz, \
char * : table_data_asciiz, \
unsigned int : table_data_unsigned, \
unsigned short : table_data_unsigned, \
unsigned long : table_data_unsigned, \
unsigned char : table_data_unsigned, \
signed int : table_data_signed, \
signed short : table_data_signed, \
signed long : table_data_signed, \
signed char : table_data_signed, \
float : table_data_float, \
double : table_data_float, \
default : table_data_bug \
)(_Table, _Item)
#endif // simple_table_h
simple_table.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdarg.h>
#include "simple_table.h"
// Хелпер #1: Печать обычной Си-строчки на экран. В данном случае - на stdout
// (на ESP32 так можно, на других платформах - понятия не имею)
//
// переопределите putsn(), если fputs(..., stdout) не поддерживается на вашей платформе,
// например, так:
//
// #define putsn(_String) printf("%s", _String)
//
#define putsn(_String) fputs(_String, stdout)
// Функция-шаблон. Ну, такие вот у нас шаблоны в Си.
// Генерирует функцию печати элемента данных таблицы.
//
#define table_data_template(_Name, _TypeName, _Format) \
\
void table_data_ ## _Name (table_layout_t *t, _TypeName col ) { \
\
char tmp[16]; \
char tmp2[TABLE_COLUMN_MAX_LENGTH]; \
\
if (!t) \
return ; \
\
if (!t->cur) \
putsn(t->tr[0]); \
\
sprintf(tmp, _Format, t->width[t->cur], t->width[t->cur]); \
snprintf(tmp2,t->width[t->cur] + 1,tmp,col); \
putsn(tmp2); \
\
t->cur++; \
if (t->cur >= t->cols) { \
t->cur = 0; \
putsn(t->tr[1]); \
} else \
putsn(t->hvc[1]); \
}
// Создадим несколько функций по шаблону выше: для печати строчки, знаковых,
// беззнаковых и чисел с плавающей точкой. Если сильно приспичит - можно добавить
// еще какой-нибудь свой собственный тип, по образу и подобию (см. ниже)
//
// Неиспользуемые функции не будут слинкованы в результирующий бинарник
// (по крайней мере в среде Arduino)
//
//
#pragma GCC diagnostic ignored "-Wformat-extra-args" // Выключаем предупреждение о лишнем аргументе в *printf
table_data_template(asciiz, const char *, "%%-%u.%us"); //table_data_asciiz()
table_data_template(unsigned, unsigned int, "%%-%uu"); //table_data_unsigned()
table_data_template(signed, signed int, "%%-%ud"); //table_data_signed()
table_data_template(float, float, "%%-%uf"); //table_data_float()
#pragma GCC diagnostic warning "-Wformat-extra-args" // Включаем назад
// Будет время - придумаю что-нибудь получше. А пока пусть будет так:
// при попытке добавить очередной элемент, если тип данных не поддерживается, то будет вызван abort()
//
void table_data_bug(table_layout_t *t, unsigned int col) {
t = t;
col = col;
abort();
}
//
//
void table_layout(table_layout_t *t,
bool utf8_enable,
const char *pre,
const char *post) {
if (t) {
t->hvc[0] = utf8_enable ? "─" : "-";
t->hvc[1] = utf8_enable ? "│" : "|";
t->hvc[2] = utf8_enable ? "┼" : "+";
t->tr[0] = pre ? pre : "";
t->tr[1] = post ? post : "\r\n";
}
}
//
//
void table_header( table_layout_t *t, const char *first_col, ... ) {
int i = 0;
const char *col = first_col;
va_list arg;
va_start(arg, first_col);
putsn(t->tr[0]);
while(col) {
putsn(col);
t->width[i++] = strlen(col);
if (i >= sizeof(t->width)/sizeof(t->width[0]))
col = NULL;
else
col = va_arg(arg, const char *);
if (col)
putsn(t->hvc[1]); // | - vertical bar
}
t->cols = i;
t->cur = 0;
va_end(arg);
putsn(t->tr[1]);
putsn(t->tr[0]);
for (i = 0; i < t->cols; i++) {
for (int j=0;j<t->width[i];j++)
putsn(t->hvc[0]); // horisontal bar
if (i + 1 < t->cols)
putsn(t->hvc[2]); // cross
}
putsn(t->tr[1]);
}
Скомпилировать simple_table_example.c можно так:
gcc -Wall simple_table.c simple_table_example.c
Чтобы в Arduino Serial Monitor рисовать таблички, нужно заменить /true/ на /false/ в вызове table_layout(…)
Удачи в рисовании табличек!

