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