Skip to content

Commit

Permalink
Added some helpers for pretty printing a table to cout.
Browse files Browse the repository at this point in the history
The table can be created from a container of values with formatting strings
used to define how each value is printed within a cell (supports multiple entries per cell).
When doing this the table column and row headers will be based on offsets within
the container.
Alternatively a table can be constructed manually to display whatever strings you wish.
The table supports fixed column sizes (values will be truncated) or automatic column sizing.
  • Loading branch information
parnham committed Oct 2, 2024
1 parent 6072b81 commit b2cfee5
Showing 1 changed file with 337 additions and 0 deletions.
337 changes: 337 additions & 0 deletions include/emergent/Console.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <string>
#include <string_view>
#include <iomanip>
#include <optional>
// #include <cstring>

#ifdef __linux
Expand All @@ -12,6 +13,8 @@
#endif


// #include <iostream>

namespace emergent
{
// Console helper functions
Expand Down Expand Up @@ -107,4 +110,338 @@ namespace emergent
// Console commands
static constexpr const char *Erase = "\x1B[2K\r";
};


namespace console
{
// Draw a hozizontal line of a given length
struct Line { size_t length = 0; };

inline std::ostream &operator <<(std::ostream &dst, const Line &l)
{
for (size_t i=0; i<l.length; i++)
{
dst << "\u2500";
}

return dst;
}

// struct Centre { std::string_view item; size_t width; };

// inline std::ostream &operator <<(std::ostream &dst, const Centre &c)
// {
// if (c.width > c.item.length())
// {
// const size_t space = c.width - c.item.length();

// dst << std::setw(space - space / 2) << ""
// << c.item
// << std::setw(space / 2) << "";
// }
// else
// {
// dst << c.item.substr(0, c.width);
// }

// return dst;
// }


// Return the size of the largest item in the values container
template <typename T> inline size_t MaxItemSize(const T &values)
{
if (values.empty())
{
return 0;
}

#if defined(__cpp_lib_ranges)
return std::ranges::max(values, {}, &T::value_type::size).size();
#else
return std::max_element(
values.begin(), values.end(),
[](auto a, auto b) { return a.size() < b.size(); }
)->size();
#endif
}


// Helper class for printing a table of data.
// Column and row headers can be defined where required.
// Each cell can contain multiple strings - placed on separate lines.
// Column width can be fixed or calculated automatically.
template <size_t Columns> struct Table
{
struct Row
{
std::string header;
std::array<std::vector<std::string>, Columns> cells;


Row() = default;
Row(const std::string &header) : header(header) {}

size_t Height() const
{
// Not actually widest item here, but largest vector in the cells array
return MaxItemSize(cells);
}
};

std::array<std::string, Columns> headers;
std::vector<Row> rows;
size_t width = 0; // fixed column width, leave as 0 for auto column sizing
size_t padding = 1; // horizontal cell padding

Table &Width(size_t value)
{
this->width = value;
return *this;
}

Table &Padding(size_t value)
{
this->padding = value;
return *this;
}

Table &Headers(const std::array<std::string, Columns> &values)
{
this->headers = values;
return *this;
}

Row &AddRow(const std::string &header = "")
{
return this->rows.emplace_back(header);
}


size_t Widest(const size_t column) const
{
if (this->width)
{
return this->width;
}

size_t max = this->headers[column].size();

for (auto &r : this->rows)
{
max = std::max(max, MaxItemSize(r.cells[column]));
}

return max;
}


std::array<size_t, Columns + 1> ColumnWidths() const
{
std::array<size_t, Columns + 1> widths = { 0 };

// find the maximum row header width
for (auto &r : this->rows)
{
widths[0] = std::max(widths[0], r.header.size());
}

// find the maximum string width for each column
for (size_t i=0; i<Columns; i++)
{
widths[i+1] = this->Widest(i);
}

return widths;
}


void ColumnHeaders(std::ostream &dst, const std::array<size_t, Columns + 1> &widths, const bool rowHeader) const
{
if (MaxItemSize(this->headers) == 0)
{
// There are no column headers, so return
return;
}

dst << Console::Blue << "\u2502";

if (rowHeader)
{
dst << std::setw(widths[0] + 2 * this->padding) << "" << "\u2502";
}

for (size_t i=0; i<Columns; i++)
{
// dst << std::setw(this->padding) << ""
// << Centre { this->headers[i], widths[i+1] }
// << std::setw(this->padding) << "";

dst << std::setw(widths[i + 1] + this->padding)
<< this->headers[i]
<< std::setw(this->padding)
<< "";
}

dst << "\u2502\n";
}


void RowHeader(std::ostream &dst, const std::array<size_t, Columns + 1> &widths, const bool first, std::optional<std::string_view> header) const
{
dst << Console::Blue << "\u2502";

if (header)
{
dst << std::setw(widths[0] + this->padding)
<< (first ? header.value() : "")
<< std::setw(this->padding) << "" << "\u2502";
}

dst << Console::Reset;
}


static inline std::string_view RowItem(std::string_view item, const size_t width)
{
// truncate the string for when using fixed with columns
return item.substr(0, width);
}


void RowMain(std::ostream &dst, const std::array<size_t, Columns + 1> &widths, const Row &r, const size_t item) const
{
static constexpr std::array PALLETTE = {
Console::Reset, Console::Yellow, Console::Cyan, Console::Magenta
};

for (size_t i=0; i<Columns; i++)
{
auto &cell = r.cells[i];

if (item < cell.size())
{
dst << PALLETTE[item % sizeof(PALLETTE)]
<< std::setw(widths[i+1] + this->padding)
<< RowItem(cell[item], widths[i+1])
<< std::setw(this->padding) << "";
}
else
{
// you could potentially have different amounts of data
// in each cell - so leave blank entries for anything missing
dst << std::setw(widths[i+1] + 2 * this->padding) << "";
}
}

dst << Console::Blue << "\u2502\n";
}


std::ostream &Render(std::ostream &dst) const
{
const auto widths = this->ColumnWidths();
const size_t bothPads = this->padding * 2;
const size_t total = std::accumulate(widths.begin(), widths.end(), widths.size() * bothPads);
const size_t header = widths[0] ? widths[0] + bothPads : 0;
const size_t main = total - (header ? header : bothPads);

Table<Columns>::Separator(dst, header, main, "\u250c", "\u252c", "\u2510");

this->ColumnHeaders(dst, widths, header);

for (auto &r : this->rows)
{
Table<Columns>::Separator(dst, header, main, "\u251c", "\u253c", "\u2524");

const size_t height = r.Height();

for (size_t y=0; y<height; y++)
{
this->RowHeader(dst, widths, y == 0, header
? std::make_optional(r.header)
: std::nullopt
);

this->RowMain(dst, widths, r, y);
}
}

Table<Columns>::Separator(dst, header, main, "\u2514", "\u2534", "\u2518");

return dst;
}


// Draw a table row separator - the left/middle/right indicate which unicode box drawing symbols
// to use so that it can be customised to also print the top and bottom of the table
static void Separator(std::ostream &dst, const size_t header, const size_t main, std::string_view left, std::string_view middle, std::string_view right)
{
dst << Console::Blue << left;

if (header)
{
dst << Line { .length = header } << middle;
}

dst << Line { .length = main } << right << '\n' << Console::Reset;
}


// Produce a table from a vector of values. The column headers will be the column index and the row headers will be the
// offset in the vector. The formatters will convert a value for a specific cell, multiple formatters will result in multiple
// representations within a cell drawn on different lines.
// For example:
//
// std::cout << Table<10>::From(data, { "%d", "0x%04x" });
//
// which given a vector of uint16_t will print the decimal and hex values in each cell as follows
// ┌────┬────────────────────────────────────────────────────────────────────────────────┐
// │ │ 0 1 2 3 4 5 6 7 8 9 │
// ├────┼────────────────────────────────────────────────────────────────────────────────┤
// │ 0 │ 1 2 3 4095 42 8192 12 435 223 1 │
// │ │ 0x0001 0x0002 0x0003 0x0fff 0x002a 0x2000 0x000c 0x01b3 0x00df 0x0001 │
// ├────┼────────────────────────────────────────────────────────────────────────────────┤
// │ 10 │ 2 3 4095 42 8192 12 435 223 1 2 │
// │ │ 0x0002 0x0003 0x0fff 0x002a 0x2000 0x000c 0x01b3 0x00df 0x0001 0x0002 │
// ...
template <typename T> static Table From(const T &values, std::initializer_list<const char *> formatters)
{
const size_t size = values.size();

Table table;

for (size_t i=0; i<Columns; i++)
{
table.headers[i] = std::to_string(i);
}

for (size_t offset=0; offset<size; offset+=Columns)
{
Row row { std::to_string(offset) };

const size_t remaining = std::min(Columns, size - offset);

for (size_t i=0; i<remaining; i++)
{
for (auto &f : formatters)
{
row.cells[i].push_back(String::format(f, values[offset + i]));
}
}

table.rows.push_back(row);
}

return table;
}
};


template <size_t Columns> std::ostream &operator <<(std::ostream &dst, const Table<Columns> &table)
{
return table.Render(dst);
}
}

}

0 comments on commit b2cfee5

Please sign in to comment.