root/src/kits/shared/TextTable.cpp
/*
 * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Distributed under the terms of the MIT License.
 */


#include <TextTable.h>

#include <stdio.h>
#include <ctype.h>
#include <utf8_functions.h>

#include <algorithm>


namespace BPrivate {


// #pragma mark - Column


struct TextTable::Column {
        Column(const BString& title, enum alignment align, bool canTruncate)
                :
                fTitle(title),
                fAlignment(align),
                fCanBeTruncated(canTruncate),
                fNeededWidth(0),
                fWidth(0)
        {
                UpdateNeededWidth(fTitle);
                fMinWidth = fNeededWidth;
        }

        const BString& Title() const
        {
                return fTitle;
        }

        enum alignment Alignment() const
        {
                return fAlignment;
        }

        bool CanBeTruncated() const
        {
                return fCanBeTruncated;
        }

        int32 NeededWidth() const
        {
                return fNeededWidth;
        }

        int32 MinWidth() const
        {
                return fMinWidth;
        }

        int32 Width() const
        {
                return fWidth;
        }

        void SetWidth(int32 width)
        {
                fWidth = width;
        }

        static int32 TextWidth(const BString& text)
        {
                // TODO: Full-width character support.
                int32 textWidth = 0;
                const char* string = text.String(), *stringEnd = text.String() + text.Length();
                while (string < stringEnd) {
                        uint32 charLen = UTF8NextCharLen(string, stringEnd - string);
                        if (charLen == 1 && string[0] == '\033') {
                                // ANSI escape code.
                                charLen++;
                                if (string[charLen - 1] == '[') {
                                        // Keep going until we hit an end character.
                                        while (!isalpha(string[charLen - 1]) && string[charLen - 1] != '\0')
                                                charLen++;
                                }
                        } else {
                                textWidth++;
                        }
                        string += charLen;
                }
                return textWidth;
        }

        void UpdateNeededWidth(const BString& text)
        {
                int32 textWidth = TextWidth(text);
                if (textWidth > fNeededWidth)
                        fNeededWidth = textWidth;
        }

        BString Format(const BString& text)
        {
                int32 textWidth = TextWidth(text);
                if (textWidth == fWidth)
                        return text;

                // truncate, if too long
                if (textWidth > fWidth) {
                        BString result(text);
                        result.TruncateChars(fWidth);
                        return result;
                }

                // align, if too short
                int32 missing = fWidth - textWidth;
                switch (fAlignment) {
                        case B_ALIGN_LEFT:
                        default:
                        {
                                BString result(text);
                                result.Append(' ', missing);
                                return result;
                        }

                        case B_ALIGN_RIGHT:
                        {
                                BString result;
                                result.Append(' ', missing);
                                result.Append(text);
                                return result;
                        }

                        case B_ALIGN_CENTER:
                        {
                                BString result;
                                result.Append(' ', missing / 2);
                                result.Append(text);
                                result.Append(' ', missing - missing / 2);
                                return result;
                        }
                }
        }

private:
        BString                 fTitle;
        enum alignment fAlignment;
        bool                    fCanBeTruncated;
        int32                   fNeededWidth;
        int32                   fMinWidth;
        int32                   fWidth;
};


// #pragma mark - TextTable


TextTable::TextTable()
        :
        fColumns(10),
        fRows(100)
{
}


TextTable::~TextTable()
{
}


int32
TextTable::CountColumns() const
{
        return fColumns.CountItems();
}


void
TextTable::AddColumn(const BString& title, enum alignment align,
        bool canTruncate)
{
        Column* column = new Column(title, align, canTruncate);
        if (!fColumns.AddItem(column)) {
                delete column;
                throw std::bad_alloc();
        }
}


int32
TextTable::CountRows() const
{
        return fRows.CountItems();
}


BString
TextTable::TextAt(int32 rowIndex, int32 columnIndex) const
{
        BStringList* row = fRows.ItemAt(rowIndex);
        if (row == NULL)
                return BString();
        return row->StringAt(columnIndex);
}


void
TextTable::SetTextAt(int32 rowIndex, int32 columnIndex, const BString& text)
{
        // If necessary append empty rows up to the specified row index.
        while (rowIndex >= fRows.CountItems()) {
                BStringList* row = new BStringList();
                if (!fRows.AddItem(row)) {
                        delete row;
                        throw std::bad_alloc();
                }
        }

        // If necessary append empty strings up to the specified column index.
        BStringList* row = fRows.ItemAt(rowIndex);
        while (columnIndex >= row->CountStrings()) {
                if (!row->Add(BString()))
                        throw std::bad_alloc();
        }

        // set the text
        if (!row->Replace(columnIndex, text))
                throw std::bad_alloc();
}


void
TextTable::Print(int32 maxWidth)
{
        int32 columnCount = fColumns.CountItems();
        if (columnCount == 0)
                return;

        // determine the column widths
        int32 rowCount = fRows.CountItems();
        for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                BStringList* row = fRows.ItemAt(rowIndex);
                int32 rowColumnCount = std::min(row->CountStrings(), columnCount);
                for (int32 columnIndex = 0; columnIndex < rowColumnCount;
                        columnIndex++) {
                        fColumns.ItemAt(columnIndex)->UpdateNeededWidth(
                                row->StringAt(columnIndex));
                }
        }

        int32 neededWidth = (columnCount - 1) * 2;
                // spacing
        for (int32 i = 0; i < columnCount; i++)
                neededWidth += fColumns.ItemAt(i)->NeededWidth();

        int32 width = neededWidth;
        int32 missingWidth = neededWidth - std::min(maxWidth, neededWidth);

        for (int32 i = 0; i < columnCount; i++) {
                Column* column = fColumns.ItemAt(i);
                if (missingWidth > 0 && column->CanBeTruncated()) {
                        int32 truncateBy = std::min(missingWidth,
                                column->NeededWidth() - column->MinWidth());
                        column->SetWidth(column->NeededWidth() - truncateBy);
                        missingWidth -= truncateBy;
                        width -= truncateBy;
                } else
                        column->SetWidth(column->NeededWidth());
        }

        // print the header
        BString line;
        for (int32 i = 0; i < columnCount; i++) {
                if (i > 0)
                        line << "  ";

                Column* column = fColumns.ItemAt(i);
                line << column->Format(column->Title());
        }
        line << '\n';
        fputs(line.String(), stdout);

        line.SetTo('-', width);
        line << '\n';
        fputs(line.String(), stdout);

        // print the rows
        for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                line.Truncate(0);
                BStringList* row = fRows.ItemAt(rowIndex);
                for (int32 columnIndex = 0; columnIndex < columnCount; columnIndex++) {
                        if (columnIndex > 0)
                                line << "  ";

                        line << fColumns.ItemAt(columnIndex)->Format(
                                row->StringAt(columnIndex));
                }

                line << '\n';
                fputs(line.String(), stdout);
        }
}


} // namespace BPrivate