⛏️ index : haiku.git

/*
 * 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