xref: /haiku/src/kits/shared/TextTable.cpp (revision fc7456e9b1ec38c941134ed6d01c438cf289381e)
1 /*
2  * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de.
3  * Distributed under the terms of the MIT License.
4  */
5 
6 
7 #include <TextTable.h>
8 
9 #include <stdio.h>
10 #include <ctype.h>
11 #include <utf8_functions.h>
12 
13 #include <algorithm>
14 
15 
16 namespace BPrivate {
17 
18 
19 // #pragma mark - Column
20 
21 
22 struct TextTable::Column {
23 	Column(const BString& title, enum alignment align, bool canTruncate)
24 		:
25 		fTitle(title),
26 		fAlignment(align),
27 		fCanBeTruncated(canTruncate),
28 		fNeededWidth(0),
29 		fWidth(0)
30 	{
31 		UpdateNeededWidth(fTitle);
32 		fMinWidth = fNeededWidth;
33 	}
34 
35 	const BString& Title() const
36 	{
37 		return fTitle;
38 	}
39 
40 	enum alignment Alignment() const
41 	{
42 		return fAlignment;
43 	}
44 
45 	bool CanBeTruncated() const
46 	{
47 		return fCanBeTruncated;
48 	}
49 
50 	int32 NeededWidth() const
51 	{
52 		return fNeededWidth;
53 	}
54 
55 	int32 MinWidth() const
56 	{
57 		return fMinWidth;
58 	}
59 
60 	int32 Width() const
61 	{
62 		return fWidth;
63 	}
64 
65 	void SetWidth(int32 width)
66 	{
67 		fWidth = width;
68 	}
69 
70 	static int32 TextWidth(const BString& text)
71 	{
72 		// TODO: Full-width character support.
73 		int32 textWidth = 0;
74 		const char* string = text.String(), *stringEnd = text.String() + text.Length();
75 		while (string < stringEnd) {
76 			uint32 charLen = UTF8NextCharLen(string, stringEnd - string);
77 			if (charLen == 1 && string[0] == '\033') {
78 				// ANSI escape code.
79 				charLen++;
80 				if (string[charLen - 1] == '[') {
81 					// Keep going until we hit an end character.
82 					while (!isalpha(string[charLen - 1]) && string[charLen - 1] != '\0')
83 						charLen++;
84 				}
85 			} else {
86 				textWidth++;
87 			}
88 			string += charLen;
89 		}
90 		return textWidth;
91 	}
92 
93 	void UpdateNeededWidth(const BString& text)
94 	{
95 		int32 textWidth = TextWidth(text);
96 		if (textWidth > fNeededWidth)
97 			fNeededWidth = textWidth;
98 	}
99 
100 	BString Format(const BString& text)
101 	{
102 		int32 textWidth = TextWidth(text);
103 		if (textWidth == fWidth)
104 			return text;
105 
106 		// truncate, if too long
107 		if (textWidth > fWidth) {
108 			BString result(text);
109 			result.TruncateChars(fWidth);
110 			return result;
111 		}
112 
113 		// align, if too short
114 		int32 missing = fWidth - textWidth;
115 		switch (fAlignment) {
116 			case B_ALIGN_LEFT:
117 			default:
118 			{
119 				BString result(text);
120 				result.Append(' ', missing);
121 				return result;
122 			}
123 
124 			case B_ALIGN_RIGHT:
125 			{
126 				BString result;
127 				result.Append(' ', missing);
128 				result.Append(text);
129 				return result;
130 			}
131 
132 			case B_ALIGN_CENTER:
133 			{
134 				BString result;
135 				result.Append(' ', missing / 2);
136 				result.Append(text);
137 				result.Append(' ', missing - missing / 2);
138 				return result;
139 			}
140 		}
141 	}
142 
143 private:
144 	BString			fTitle;
145 	enum alignment fAlignment;
146 	bool			fCanBeTruncated;
147 	int32			fNeededWidth;
148 	int32			fMinWidth;
149 	int32			fWidth;
150 };
151 
152 
153 // #pragma mark - TextTable
154 
155 
156 TextTable::TextTable()
157 	:
158 	fColumns(10, true),
159 	fRows(100, true)
160 {
161 }
162 
163 
164 TextTable::~TextTable()
165 {
166 }
167 
168 
169 int32
170 TextTable::CountColumns() const
171 {
172 	return fColumns.CountItems();
173 }
174 
175 
176 void
177 TextTable::AddColumn(const BString& title, enum alignment align,
178 	bool canTruncate)
179 {
180 	Column* column = new Column(title, align, canTruncate);
181 	if (!fColumns.AddItem(column)) {
182 		delete column;
183 		throw std::bad_alloc();
184 	}
185 }
186 
187 
188 int32
189 TextTable::CountRows() const
190 {
191 	return fRows.CountItems();
192 }
193 
194 
195 BString
196 TextTable::TextAt(int32 rowIndex, int32 columnIndex) const
197 {
198 	BStringList* row = fRows.ItemAt(rowIndex);
199 	if (row == NULL)
200 		return BString();
201 	return row->StringAt(columnIndex);
202 }
203 
204 
205 void
206 TextTable::SetTextAt(int32 rowIndex, int32 columnIndex, const BString& text)
207 {
208 	// If necessary append empty rows up to the specified row index.
209 	while (rowIndex >= fRows.CountItems()) {
210 		BStringList* row = new BStringList();
211 		if (!fRows.AddItem(row)) {
212 			delete row;
213 			throw std::bad_alloc();
214 		}
215 	}
216 
217 	// If necessary append empty strings up to the specified column index.
218 	BStringList* row = fRows.ItemAt(rowIndex);
219 	while (columnIndex >= row->CountStrings()) {
220 		if (!row->Add(BString()))
221 			throw std::bad_alloc();
222 	}
223 
224 	// set the text
225 	if (!row->Replace(columnIndex, text))
226 		throw std::bad_alloc();
227 }
228 
229 
230 void
231 TextTable::Print(int32 maxWidth)
232 {
233 	int32 columnCount = fColumns.CountItems();
234 	if (columnCount == 0)
235 		return;
236 
237 	// determine the column widths
238 	int32 rowCount = fRows.CountItems();
239 	for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) {
240 		BStringList* row = fRows.ItemAt(rowIndex);
241 		int32 rowColumnCount = std::min(row->CountStrings(), columnCount);
242 		for (int32 columnIndex = 0; columnIndex < rowColumnCount;
243 			columnIndex++) {
244 			fColumns.ItemAt(columnIndex)->UpdateNeededWidth(
245 				row->StringAt(columnIndex));
246 		}
247 	}
248 
249 	int32 neededWidth = (columnCount - 1) * 2;
250 		// spacing
251 	for (int32 i = 0; i < columnCount; i++)
252 		neededWidth += fColumns.ItemAt(i)->NeededWidth();
253 
254 	int32 width = neededWidth;
255 	int32 missingWidth = neededWidth - std::min(maxWidth, neededWidth);
256 
257 	for (int32 i = 0; i < columnCount; i++) {
258 		Column* column = fColumns.ItemAt(i);
259 		if (missingWidth > 0 && column->CanBeTruncated()) {
260 			int32 truncateBy = std::min(missingWidth,
261 				column->NeededWidth() - column->MinWidth());
262 			column->SetWidth(column->NeededWidth() - truncateBy);
263 			missingWidth -= truncateBy;
264 			width -= truncateBy;
265 		} else
266 			column->SetWidth(column->NeededWidth());
267 	}
268 
269 	// print the header
270 	BString line;
271 	for (int32 i = 0; i < columnCount; i++) {
272 		if (i > 0)
273 			line << "  ";
274 
275 		Column* column = fColumns.ItemAt(i);
276 		line << column->Format(column->Title());
277 	}
278 	line << '\n';
279 	fputs(line.String(), stdout);
280 
281 	line.SetTo('-', width);
282 	line << '\n';
283 	fputs(line.String(), stdout);
284 
285 	// print the rows
286 	for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) {
287 		line.Truncate(0);
288 		BStringList* row = fRows.ItemAt(rowIndex);
289 		for (int32 columnIndex = 0; columnIndex < columnCount; columnIndex++) {
290 			if (columnIndex > 0)
291 				line << "  ";
292 
293 			line << fColumns.ItemAt(columnIndex)->Format(
294 				row->StringAt(columnIndex));
295 		}
296 
297 		line << '\n';
298 		fputs(line.String(), stdout);
299 	}
300 }
301 
302 
303 } // namespace BPrivate
304