xaizek / uncov (License: AGPLv3+) (since 2018-12-07)
Uncov(er) is a tool that collects and processes code coverage reports.
<root> / src / TablePrinter.cpp (ab2b79dee4e5692cdc19fb407efcdc7f47681046) (10KiB) (mode 100644) [raw]
// Copyright (C) 2016 xaizek <xaizek@posteo.net>
//
// This file is part of uncov.
//
// uncov is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public License as
// published by the Free Software Foundation.
//
// uncov is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with uncov.  If not, see <http://www.gnu.org/licenses/>.

#include "TablePrinter.hpp"

#include <cstring>

#include <algorithm>
#include <functional>
#include <iomanip>
#include <iterator>
#include <ostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#include <boost/algorithm/string/case_conv.hpp>
#include <boost/range/adaptor/reversed.hpp>

#include "printing.hpp"

static unsigned int measureWidth(const std::string &s);
static unsigned int measurePrefixLength(const std::string &s,
                                        unsigned int prefixWidth);

/**
 * @brief Helper class that represents single column of a table.
 */
class TablePrinter::Column
{
public:
    /**
     * @brief Constructs empty column.
     *
     * @param idx Index of the column.
     * @param heading Key of the column.
     * @param alignLeft This column should be left-aligned.
     * @param hiddenHeader Do not print column header.
     */
    Column(int idx, std::string heading, bool alignLeft, bool hiddenHeader)
        : idx(idx), alignLeft(alignLeft), heading(std::move(heading)),
          width(hiddenHeader ? 0U : this->heading.size())
    {
    }

public:
    /**
     * @brief Retrieves index of the column.
     *
     * @returns The index.
     */
    int getIdx() const
    {
        return idx;
    }

    /**
     * @brief Returns whether this column should be aligned to the left.
     *
     * @returns @c true if so, @c false otherwise.
     */
    bool leftAlign() const
    {
        return alignLeft;
    }

    /**
     * @brief Retrieves heading of the column.
     *
     * @returns The heading.
     */
    const std::string getHeading() const
    {
        return truncate(heading);
    }

    /**
     * @brief Adds the value to the column.
     *
     * @param val Value to add.
     */
    void append(std::string val)
    {
        width = std::max(width, measureWidth(val));
        values.emplace_back(std::move(val));
    }

    /**
     * @brief Retrieves widths of the column.
     *
     * @returns The width.
     */
    unsigned int getWidth() const
    {
        return width;
    }

    /**
     * @brief Reduces width of the column by @p by positions.
     *
     * @param by Amount by which to adjust column width.
     */
    void reduceWidthBy(unsigned int by)
    {
        width -= std::min(width, by);
    }

    /**
     * @brief Retrieves printable value of the column by index.
     *
     * The value can be truncated to fit limited width, which is indicated by
     * trailing ellipsis.
     *
     * @param i Index of the value (no range checks are performed).
     *
     * @returns The value.
     */
    std::string operator[](unsigned int i) const
    {
        return truncate(values[i]);
    }

private:
    /**
     * @brief Truncates a string with ellipsis to fit into column width.
     *
     * @param s The string to truncate.
     *
     * @returns Truncated string, which is the same as @p s if it already fits.
     */
    std::string truncate(const std::string &s) const
    {
        if (measureWidth(s) <= width) {
            return s;
        }
        if (width <= 3U) {
            return std::string("...").substr(0U, width);
        }
        const unsigned int prefixLen = measurePrefixLength(s, width - 3U);
        std::ostringstream oss;
        oss << s.substr(0U, prefixLen)
            << (prefixLen > width - 3U ? "\033[1m\033[0m" : "") << "...";
        return oss.str();
    }

private:
    //! Index of the column.
    const int idx;
    //! Whether this column should be aligned to the left.
    bool alignLeft;
    //! Title of the column for printing.
    const std::string heading;
    //! Width of the column.
    unsigned int width;
    //! Contents of the column.
    std::vector<std::string> values;
};

static const std::string gap = "  ";

TablePrinter::TablePrinter(const std::vector<std::string> &headings,
                           unsigned int maxWidth, bool hiddenHeader)
    : maxWidth(maxWidth), hiddenHeader(hiddenHeader)
{
    for (unsigned int i = 0U; i < headings.size(); ++i) {
        std::string heading = headings[i];

        bool left = false;
        if (!heading.empty() && heading.front() == '-') {
            heading = heading.substr(1U);
            left = true;
        }

        boost::algorithm::to_upper(heading);
        cols.emplace_back(i, std::move(heading), left, hiddenHeader);
    }
}

TablePrinter::~TablePrinter()
{
}

void
TablePrinter::append(const std::vector<std::string> &item)
{
    if (item.size() != cols.size()) {
        throw std::invalid_argument("Invalid item added to the table.");
    }
    items.emplace_back(item);
}

void
TablePrinter::print(std::ostream &os)
{
    fillColumns();

    if (!adjustColumnsWidths()) {
        // Available width is not enough to display table.
        return;
    }

    printTableHeader(os);
    printTableRows(os);
}

void
TablePrinter::fillColumns()
{
    for (const std::vector<std::string> &item : items) {
        for (Column &col : cols) {
            col.append(item[col.getIdx()]);
        }
    }
}

bool
TablePrinter::adjustColumnsWidths()
{
    // The code below assumes that there is at least one column.
    if (cols.empty()) {
        return false;
    }

    // Calculate real width of the table.
    unsigned int realWidth = 0U;
    for (Column &col : cols) {
        realWidth += col.getWidth();
    }
    realWidth += gap.length()*(cols.size() - 1U);

    // Make ordering of columns that goes from widest to narrowest.
    std::vector<std::reference_wrapper<Column>> sorted {
        cols.begin(), cols.end()
    };
    std::sort(sorted.begin(), sorted.end(),
              [](const Column &a, const Column &b) {
                  return a.getWidth() >= b.getWidth();
              });

    // Repeatedly reduce columns until we reach target width.
    // At each iteration: reduce width of (at most all, but not necessarily) the
    // widest columns by making them at most as wide as the narrower columns
    // that directly follow them.
    while (realWidth > maxWidth) {
        unsigned int toReduce = realWidth - maxWidth;

        // Make list of the widest columns as well as figure out by which amount
        // we can adjust the width (difference between column widths).
        std::vector<std::reference_wrapper<Column>> widest;
        unsigned int maxAdj = static_cast<Column&>(sorted.front()).getWidth();
        for (Column &col : sorted) {
            const unsigned int w = col.getWidth();
            if (w != maxAdj) {
                maxAdj -= w;
                break;
            }
            widest.push_back(col);
        }

        // Reversed order of visiting to ensure that ordering invariant is
        // intact: last visited element can be reduced by smaller amount, which
        // will leave it the biggest.  Actually it doesn't matter because we
        // reach target width at the same time, still it might matter later.
        for (Column &col : boost::adaptors::reverse(widest)) {
            const unsigned int by = std::min(maxAdj, toReduce);
            col.reduceWidthBy(by);
            toReduce -= by;
        }

        // We could exhaust possibilities to reduce column width and all that's
        // left is padding between columns.
        if (maxAdj == 0) {
            break;
        }

        // Update current width of the table.
        realWidth = maxWidth + toReduce;
    }

    return realWidth <= maxWidth;
}

void
TablePrinter::printTableHeader(std::ostream &os)
{
    if (hiddenHeader) {
        return;
    }

    for (Column &col : cols) {
        os << TableHeader{alignCell(col.getHeading(), col)};

        if (&col != &cols.back()) {
            os << gap;
        }
    }
    os << '\n';
}

void
TablePrinter::printTableRows(std::ostream &os)
{
    for (unsigned int i = 0, n = items.size(); i < n; ++i) {
        for (Column &col : cols) {
            os << alignCell(col[i], col);
            if (&col != &cols.back()) {
                os << gap;
            }
        }
        os << '\n';
    }
}

std::string
TablePrinter::alignCell(std::string s, const Column &col) const
{
    unsigned int lineWidth = measureWidth(s);
    if (lineWidth >= col.getWidth()) {
        return s;
    }

    const unsigned int padWidth = col.getWidth() - lineWidth;
    if (col.leftAlign()) {
        s.insert(s.length(), padWidth, ' ');
    } else {
        s.insert(0, padWidth, ' ');
    }
    return s;
}

/**
 * @brief Calculates width of a string ignoring embedded escape sequences.
 *
 * @param s String to measure.
 *
 * @returns The width.
 */
static unsigned int
measureWidth(const std::string &s)
{
    unsigned int valWidth = 0U;
    const char *str = s.c_str();
    while (*str != '\0') {
        if (*str != '\033') {
            ++valWidth;
            ++str;
            continue;
        }

        const char *const next = std::strchr(str, 'm');
        str = (next == nullptr) ? (str + std::strlen(str)) : (next + 1);
    }
    return valWidth;
}

/**
 * @brief Calculates length of string prefix ignoring embedded escape sequences.
 *
 * @param s String to measure.
 * @param prefixWidth Width of the prefix.
 *
 * @returns Lengths of byte sequence that matches specified prefix width.
 */
static unsigned int
measurePrefixLength(const std::string &s, unsigned int prefixWidth)
{
    const char *str = s.c_str();
    while (*str != '\0' && prefixWidth> 0U) {
        if (*str != '\033') {
            --prefixWidth;
            ++str;
            continue;
        }

        const char *const next = std::strchr(str, 'm');
        str = (next == nullptr) ? (str + std::strlen(str)) : (next + 1);
    }
    return str - s.c_str();
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/uncov

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/uncov

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master