//////////////////////////////////////////////////////////////////////////////// // task - a command line task list manager. // // Copyright 2006 - 2009, Paul Beckingham. // All rights reserved. // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software // Foundation; either version 2 of the License, or (at your option) any later // version. // // This program 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 General Public License for more // details. // // You should have received a copy of the GNU General Public License along with // this program; if not, write to the // // Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, // Boston, MA // 02110-1301 // USA // // // // Attributes Table Row Column Cell // ---------------------------------------------------- // foreground color Y Y Y Y // background color Y Y Y Y // padding Y Y Y Y // wrap Y Y Y Y // width Y - Y Y // height - Y - Y // justification Y Y Y Y // // Precedence // If attributes conflict, the precedence order is: // cell // row // column // table // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include //////////////////////////////////////////////////////////////////////////////// Table::Table () : mRows (0) , mIntraPadding (1) , mDashedUnderline (false) , mTablePadding (0) , mTableWidth (0) , mSuppressWS (false) { } //////////////////////////////////////////////////////////////////////////////// Table::~Table () { } //////////////////////////////////////////////////////////////////////////////// void Table::setTableColor (Text::color fg, Text::color bg) { mFg["table"] = Text::colorName (fg); mBg["table"] = Text::colorName (bg); } //////////////////////////////////////////////////////////////////////////////// void Table::setTableFg (Text::color c) { mFg["table"] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setTableBg (Text::color c) { mBg["table"] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setTablePadding (int padding) { mTablePadding = padding; } //////////////////////////////////////////////////////////////////////////////// void Table::setTableIntraPadding (int padding) { mIntraPadding = padding; } //////////////////////////////////////////////////////////////////////////////// void Table::setTableWidth (int width) { assert (width > 0); mTableWidth = width; } //////////////////////////////////////////////////////////////////////////////// void Table::setTableDashedUnderline () { mDashedUnderline = true; } //////////////////////////////////////////////////////////////////////////////// int Table::addColumn (const std::string& col) { mSpecifiedWidth.push_back (minimum); mMaxDataWidth.push_back (col.length ()); mCalculatedWidth.push_back (0); mColumnPadding.push_back (0); mColumns.push_back (col); return mColumns.size () - 1; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnColor (int column, Text::color fg, Text::color bg) { char id[12]; sprintf (id, "col:%d", column); mFg[id] = Text::colorName (fg); mBg[id] = Text::colorName (bg); } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnFg (int column, Text::color c) { char id[12]; sprintf (id, "col:%d", column); mFg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnBg (int column, Text::color c) { char id[12]; sprintf (id, "col:%d", column); mBg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnUnderline (int column) { char id[12]; sprintf (id, "col:%d", column); mUnderline[id] = Text::underline; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnPadding (int column, int padding) { mColumnPadding[column] = padding; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnWidth (int column, int width) { assert (width > 0); mSpecifiedWidth[column] = width; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnWidth (int column, sizing s) { mSpecifiedWidth[column] = (int) s; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnCommify (int column) { mCommify[column] = true; } //////////////////////////////////////////////////////////////////////////////// void Table::setColumnJustification (int column, just j) { mJustification[column] = j; } //////////////////////////////////////////////////////////////////////////////// void Table::sortOn (int column, order o) { mSortColumns.push_back (column); mSortOrder[column] = o; } //////////////////////////////////////////////////////////////////////////////// int Table::addRow () { return mRows++; } //////////////////////////////////////////////////////////////////////////////// void Table::setRowColor (const int row, const Text::color fg, const Text::color bg) { char id[12]; sprintf (id, "row:%d", row); mFg[id] = Text::colorName (fg); mBg[id] = Text::colorName (bg); } //////////////////////////////////////////////////////////////////////////////// void Table::setRowFg (const int row, const Text::color c) { char id[12]; sprintf (id, "row:%d", row); mFg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setRowBg (const int row, const Text::color c) { char id[12]; sprintf (id, "row:%d", row); mBg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::addCell (const int row, const int col, const std::string& data) { int length = 0; if (mSuppressWS) { std::string data2; if (mCommify.find (col) != mCommify.end ()) data2 = commify (data); else data2 = data; clean (data2); length = data2.length (); mData.add (row, col, data2); } else { if (mCommify.find (col) != mCommify.end ()) mData.add (row, col, commify (data)); else mData.add (row, col, data); length = data.length (); } // Automatically maintain max width. mMaxDataWidth[col] = max (mMaxDataWidth[col], length); } //////////////////////////////////////////////////////////////////////////////// void Table::addCell (const int row, const int col, const char data) { mData.add (row, col, data); // Automatically maintain max width. mMaxDataWidth[col] = max (mMaxDataWidth[col], 1); } //////////////////////////////////////////////////////////////////////////////// void Table::addCell (const int row, const int col, const int data) { char value[12]; sprintf (value, "%d", data); if (mCommify.find (col) != mCommify.end ()) mData.add (row, col, commify (value)); else mData.add (row, col, value); // Automatically maintain max width. mMaxDataWidth[col] = max (mMaxDataWidth[col], (signed) ::strlen (value)); } //////////////////////////////////////////////////////////////////////////////// void Table::addCell (const int row, const int col, const float data) { char value[24]; sprintf (value, "%.2f", data); if (mCommify.find (col) != mCommify.end ()) mData.add (row, col, commify (value)); else mData.add (row, col, value); // Automatically maintain max width. mMaxDataWidth[col] = max (mMaxDataWidth[col], (signed) ::strlen (value)); } //////////////////////////////////////////////////////////////////////////////// void Table::addCell (const int row, const int col, const double data) { char value[24]; sprintf (value, "%.6f", data); if (mCommify.find (col) != mCommify.end ()) mData.add (row, col, commify (value)); else mData.add (row, col, value); // Automatically maintain max width. mMaxDataWidth[col] = max (mMaxDataWidth[col], (signed) ::strlen (value)); } //////////////////////////////////////////////////////////////////////////////// void Table::setCellColor (const int row, const int col, const Text::color fg, const Text::color bg) { char id[24]; sprintf (id, "cell:%d,%d", row, col); mFg[id] = Text::colorName (fg); mBg[id] = Text::colorName (bg); } //////////////////////////////////////////////////////////////////////////////// void Table::setCellFg (const int row, const int col, const Text::color c) { char id[24]; sprintf (id, "cell:%d,%d", row, col); mFg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// void Table::setCellBg (const int row, const int col, const Text::color c) { char id[24]; sprintf (id, "cell:%d,%d", row, col); mBg[id] = Text::colorName (c); } //////////////////////////////////////////////////////////////////////////////// std::string Table::getCell (const int row, const int col) { Grid::Cell* c = mData.byRow (row, col); if (c) return (std::string) *c; return ""; } //////////////////////////////////////////////////////////////////////////////// Text::color Table::getFg (const int row, const int col) { char idCell[24]; sprintf (idCell, "cell:%d,%d", row, col); if (mFg.find (idCell) != mFg.end ()) return Text::colorCode (mFg[idCell]); char idRow[12]; sprintf (idRow, "row:%d", row); if (mFg.find (idRow) != mFg.end ()) return Text::colorCode (mFg[idRow]); char idCol[12]; sprintf (idCol, "col:%d", col); if (mFg.find (idCol) != mFg.end ()) return Text::colorCode (mFg[idCol]); if (mFg.find ("table") != mFg.end ()) return Text::colorCode (mFg["table"]); return Text::nocolor; } //////////////////////////////////////////////////////////////////////////////// Text::color Table::getHeaderFg (int col) { char idCol[12]; sprintf (idCol, "col:%d", col); return mFg.find (idCol) != mFg.end () ? Text::colorCode (mFg[idCol]) : mFg.find ("table") != mFg.end () ? Text::colorCode (mFg["table"]) : Text::nocolor; } //////////////////////////////////////////////////////////////////////////////// Text::color Table::getBg (const int row, const int col) { char idCell[24]; sprintf (idCell, "cell:%d,%d", row, col); if (mBg.find (idCell) != mBg.end ()) return Text::colorCode (mBg[idCell]); char idRow[12]; sprintf (idRow, "row:%d", row); if (mBg.find (idRow) != mBg.end ()) return Text::colorCode (mBg[idRow]); char idCol[12]; sprintf (idCol, "col:%d", col); if (mBg.find (idCol) != mBg.end ()) return Text::colorCode (mBg[idCol]); if (mBg.find ("table") != mBg.end ()) return Text::colorCode (mBg["table"]); return Text::nocolor; } //////////////////////////////////////////////////////////////////////////////// Text::color Table::getHeaderBg (int col) { char idCol[12]; sprintf (idCol, "col:%d", col); return mBg.find (idCol) != mBg.end () ? Text::colorCode (mBg[idCol]) : mBg.find ("table") != mBg.end () ? Text::colorCode (mBg["table"]) : Text::nocolor; } //////////////////////////////////////////////////////////////////////////////// Text::color Table::getHeaderUnderline (int col) { char idCol[12]; sprintf (idCol, "col:%d", col); return mUnderline.find (idCol) != mUnderline.end () ? Text::underline : Text::nocolor; } //////////////////////////////////////////////////////////////////////////////// int Table::getIntraPadding () { return mIntraPadding; } //////////////////////////////////////////////////////////////////////////////// int Table::getPadding (int col) { return max (mColumnPadding[col], mTablePadding); } //////////////////////////////////////////////////////////////////////////////// // Using mSpecifiedWidth, mMaxDataWidth, generate mCalculatedWidth. void Table::calculateColumnWidths () { // Ideal case: either no table width is specified, or everything fits without // wrapping into mTableWidth. std::vector ideal = mMaxDataWidth; int width = 0; int countFlexible = 0; for (size_t c = 0; c < mColumns.size (); ++c) { if (mSpecifiedWidth[c] == flexible) ++countFlexible; else if (mSpecifiedWidth[c] > 0) ideal[c] = mSpecifiedWidth[c]; width += mColumnPadding[c] + ideal[c] + mColumnPadding[c] + (c > 0 ? mIntraPadding : 0); } if (!mTableWidth || width < mTableWidth) { mCalculatedWidth = ideal; return; } // Try again, with available space divided among the flexible columns. if (countFlexible) { ideal = mMaxDataWidth; width = 0; for (size_t c = 0; c < mColumns.size (); ++c) { if (mSpecifiedWidth[c] > 0) ideal[c] = mSpecifiedWidth[c]; else if (mSpecifiedWidth[c] == flexible) { ideal[c] = 0; } width += mColumnPadding[c] + ideal[c] + mColumnPadding[c] + (c > 0 ? mIntraPadding : 0); } int available = mTableWidth - width; if (width < mTableWidth) // if there is room to wiggle in { int shared = available / countFlexible; int remainder = available % countFlexible; int lastFlexible = mColumns.size () - 1; for (size_t c = 0; c < mColumns.size (); ++c) { if (mSpecifiedWidth[c] == flexible) { lastFlexible = c; ideal[c] += shared; } } // Remainder goes to last column. ideal[lastFlexible] += remainder; mCalculatedWidth = ideal; return; } else { // std::cout << "# insufficient room, considering only flexible columns." << std::endl; // The fallback position is to assume no width was specificed, and just // calculate widths accordingly. mTableWidth = 0; calculateColumnWidths (); return; } } // Try again, treating minimum columns as flexible. // std::cout << "# no flexible columns. Now what?" << std::endl; } //////////////////////////////////////////////////////////////////////////////// Table::just Table::getJustification (const int row, const int col) { if (mJustification.find (col) != mJustification.end ()) return mJustification[col]; return left; } //////////////////////////////////////////////////////////////////////////////// Table::just Table::getHeaderJustification (int col) { return mJustification.find (col) != mJustification.end () ? mJustification[col] : left; } //////////////////////////////////////////////////////////////////////////////// // data One Data to be rendered // width 8 Max data width for column/specified width // padding 1 Extra padding around data // intraPadding 0 Extra padding between columns only // justification right Alignment withing padding // // Returns: // " One " // One data // ^ ^ padding // ^ intraPadding // ^^^^^^^^ width // ^ ^ fg/bg // const std::string Table::formatHeader ( int col, int width, int padding) { assert (width > 0); Text::color fg = getHeaderFg (col); Text::color bg = getHeaderBg (col); std::string data = mColumns[col]; Text::color decoration = getHeaderUnderline (col); std::string pad = ""; std::string intraPad = ""; std::string preJust = ""; std::string attrOn = ""; std::string attrOff = ""; std::string postJust = ""; for (int i = 0; i < padding; ++i) pad += " "; // Place the value within the available space - justify. int gap = width - data.length (); for (int i = 0; i < gap; ++i) postJust += " "; if (col < (signed) mColumns.size () - 1) for (int i = 0; i < getIntraPadding (); ++i) intraPad += " "; return Text::colorize ( fg, bg, Text::colorize ( decoration, Text::nocolor, pad + preJust + data + postJust + pad) + intraPad); } //////////////////////////////////////////////////////////////////////////////// // data One Data to be rendered // width 8 Max data width for column/specified width // padding 1 Extra padding around data // intraPadding 0 Extra padding between columns only // justification right Alignment withing padding // // Returns: // "------- " // ------- data // ^ ^ padding // ^ intraPadding // ^^^^^^^^ width // ^ ^ fg/bg // const std::string Table::formatHeaderDashedUnderline ( int col, int width, int padding) { assert (width > 0); Text::color fg = getHeaderFg (col); Text::color bg = getHeaderBg (col); Text::color decoration = getHeaderUnderline (col); std::string data = ""; for (int i = 0; i < width; ++i) data += '-'; std::string pad = ""; std::string intraPad = ""; std::string attrOn = ""; std::string attrOff = ""; for (int i = 0; i < padding; ++i) pad += " "; // Place the value within the available space - justify. if (col < (signed) mColumns.size () - 1) for (int i = 0; i < getIntraPadding (); ++i) intraPad += " "; return Text::colorize ( fg, bg, Text::colorize ( decoration, Text::nocolor, pad + data + pad) + intraPad); } //////////////////////////////////////////////////////////////////////////////// void Table::formatCell ( const int row, const int col, const int width, const int padding, std::vector & lines, std::string& blank) { assert (width > 0); Text::color fg = getFg (row, col); Text::color bg = getBg (row, col); just justification = getJustification (row, col); std::string data = getCell (row, col); std::string pad = ""; std::string intraPad = ""; for (int i = 0; i < padding; ++i) pad += " "; if (col < (signed) mColumns.size () - 1) for (int i = 0; i < getIntraPadding (); ++i) intraPad += " "; // Break the text into chunks of width characters. std::string preJust; std::string postJust; std::vector chunks; wrapText (chunks, data, width); for (size_t chunk = 0; chunk < chunks.size (); ++chunk) { // Place the data within the available space - justify. int gap = width - chunks[chunk].length (); preJust = ""; postJust = ""; if (justification == left) for (int i = 0; i < gap; ++i) postJust += " "; else if (justification == right) for (int i = 0; i < gap; ++i) preJust += " "; else if (justification == center) { for (int i = 0; i < gap / 2; ++i) preJust += " "; for (size_t i = 0; i < gap - preJust.length (); ++i) postJust += " "; } lines.push_back ( Text::colorize (fg, bg, pad + preJust + chunks[chunk] + postJust + pad + intraPad)); } // The blank is used to vertically pad cells that have blank lines. pad = ""; for (int i = 0; i < width; ++i) pad += " "; blank = Text::colorize (fg, bg, pad + intraPad); } //////////////////////////////////////////////////////////////////////////////// void Table::suppressWS () { mSuppressWS = true; } //////////////////////////////////////////////////////////////////////////////// void Table::setDateFormat (const std::string& dateFormat) { mDateFormat = dateFormat; } //////////////////////////////////////////////////////////////////////////////// int Table::rowCount () { return mRows; } //////////////////////////////////////////////////////////////////////////////// int Table::columnCount () { return mColumns.size (); } //////////////////////////////////////////////////////////////////////////////// // Removes extraneous output characters, such as: // - removal of redundant color codes: // ^[[31mName^[[0m ^[[31mValue^[[0m -> ^[[31mName Value^[[0m // // This method is a work in progress. void Table::optimize (std::string& output) const { // int start = output.length (); /* Well, how about that! The benchmark.t unit test adds a 1000 tasks, fiddles with some of them, then runs a series of reports. The results are timed, and look like this: 1000 tasks added in 3 seconds 600 tasks altered in 32 seconds 'task ls' in 26 seconds 'task list' in 17 seconds 'task list pri:H' in 19 seconds 'task list +tag' in 0 seconds 'task list project_A' in 0 seconds 'task long' in 29 seconds 'task completed' in 2 seconds 'task history' in 0 seconds 'task ghistory' in 0 seconds This performance is terrible. To identify the worst offender, Various Timer objects were added in Table::render, assuming that table sorting is the major bottleneck. But no, it is Table::optimize that is the problem. After commenting out this method, the results are now: 1000 tasks added in 3 seconds 600 tasks altered in 29 seconds 'task ls' in 0 seconds 'task list' in 0 seconds 'task list pri:H' in 1 seconds 'task list +tag' in 0 seconds 'task list project_A' in 0 seconds 'task long' in 0 seconds 'task completed' in 0 seconds 'task history' in 0 seconds 'task ghistory' in 0 seconds Much better. */ // std::cout << int ((100 * (start - output.length ()) / start)) // << "%" << std::endl; } //////////////////////////////////////////////////////////////////////////////// // Combsort11, with O(n log n) average, O(n log n) worst case performance. // // function combsort11(array input) // gap := input.size // // loop until gap <= 1 and swaps = 0 // if gap > 1 // gap := gap / 1.3 // if gap = 10 or gap = 9 // gap := 11 // end if // end if // // i := 0 // swaps := 0 // // loop until i + gap >= input.size // if input[i] > input[i+gap] // swap(input[i], input[i+gap]) // swaps := 1 // end if // i := i + 1 // end loop // // end loop // end function #define SWAP \ { \ int temp = order[r]; \ order[r] = order[r + gap]; \ order[r + gap] = temp; \ swaps = 1; \ } void Table::sort (std::vector & order) { int gap = order.size (); int swaps = 1; while (gap > 1 || swaps > 0) { if (gap > 1) { gap = (int) ((float)gap / 1.3); if (gap == 10 or gap == 9) gap = 11; } int r = 0; swaps = 0; while (r + gap < (int) order.size ()) { bool keepScanning = true; for (size_t c = 0; keepScanning && c < mSortColumns.size (); ++c) { keepScanning = false; Grid::Cell* left = mData.byRow (order[r], mSortColumns[c]); Grid::Cell* right = mData.byRow (order[r + gap], mSortColumns[c]); // Data takes precedence over missing data. if (left == NULL && right != NULL) { SWAP break; } // No data - try comparing the next column. else if (left == NULL && right == NULL) { keepScanning = true; } // Identical data - try comparing the next column. else if (left && right && *left == *right) { keepScanning = true; } // Differing data - do a proper comparison. else if (left && right && *left != *right) { switch (mSortOrder[mSortColumns[c]]) { case ascendingNumeric: if ((float)*left > (float)*right) SWAP break; case descendingNumeric: if ((float)*left < (float)*right) SWAP break; case ascendingCharacter: if ((std::string)*left > (std::string)*right) SWAP break; case descendingCharacter: if ((std::string)*left < (std::string)*right) SWAP break; case ascendingDate: { if ((std::string)*left != "" && (std::string)*right == "") break; else if ((std::string)*left == "" && (std::string)*right != "") SWAP else { Date dl ((std::string)*left, mDateFormat); Date dr ((std::string)*right, mDateFormat); if (dl > dr) SWAP } } break; case descendingDate: { if ((std::string)*left != "" && (std::string)*right == "") break; else if ((std::string)*left == "" && (std::string)*right != "") SWAP else { Date dl ((std::string)*left, mDateFormat); Date dr ((std::string)*right, mDateFormat); if (dl < dr) SWAP } } break; case ascendingPriority: if (((std::string)*left == "" && (std::string)*right != "") || ((std::string)*left == "M" && (std::string)*right == "L") || ((std::string)*left == "H" && ((std::string)*right == "L" || (std::string)*right == "M"))) SWAP break; case descendingPriority: if (((std::string)*left == "" && (std::string)*right != "") || ((std::string)*left == "L" && ((std::string)*right == "M" || (std::string)*right == "H")) || ((std::string)*left == "M" && (std::string)*right == "H")) SWAP break; case ascendingPeriod: if ((std::string)*left == "" && (std::string)*right != "") break; else if ((std::string)*left != "" && (std::string)*right == "") SWAP else if (convertDuration ((std::string)*left) > convertDuration ((std::string)*right)) SWAP break; case descendingPeriod: if ((std::string)*left != "" && (std::string)*right == "") break; else if ((std::string)*left == "" && (std::string)*right != "") SWAP else if (convertDuration ((std::string)*left) < convertDuration ((std::string)*right)) SWAP break; } } } ++r; } } } //////////////////////////////////////////////////////////////////////////////// void Table::clean (std::string& value) { size_t start = 0; size_t pos; while ((pos = value.find ('\t', start)) != std::string::npos) { value.replace (pos, 1, " "); start = pos; // Not pos + 1, because we have a destructive operation, and // this is ultimately safer. } while ((pos = value.find ('\r', start)) != std::string::npos) { value.replace (pos, 1, " "); start = pos; } while ((pos = value.find ('\n', start)) != std::string::npos) { value.replace (pos, 1, " "); start = pos; } } //////////////////////////////////////////////////////////////////////////////// const std::string Table::render (int maximum /* = 0 */) { calculateColumnWidths (); // Print column headers in column order. std::string output; std::string underline; for (size_t col = 0; col < mColumns.size (); ++col) { output += formatHeader ( col, mCalculatedWidth[col], mColumnPadding[col]); if (mDashedUnderline) underline += formatHeaderDashedUnderline ( col, mCalculatedWidth[col], mColumnPadding[col]); } output += "\n"; if (underline.length ()) output += underline + "\n"; // Determine row order, according to sort options. std::vector order; for (int row = 0; row < mRows; ++row) order.push_back (row); // Only sort if necessary. if (mSortColumns.size ()) sort (order); // If a non-zero maximum is specified, then it limits the number of rows of // the table that are rendered. int limit = mRows; if (maximum != 0) limit = maximum; // Print all rows. for (int row = 0; row < limit; ++row) { std::vector > columns; std::vector blanks; size_t maxHeight = 0; for (size_t col = 0; col < mColumns.size (); ++col) { std::vector lines; std::string blank; formatCell ( order[row], col, mCalculatedWidth[col], mColumnPadding[col], lines, blank); columns.push_back (lines); blanks.push_back (blank); maxHeight = max (maxHeight, columns[col].size ()); } if (maxHeight) { for (size_t lines = 0; lines < maxHeight; ++lines) { for (size_t col = 0; col < mColumns.size (); ++col) if (lines < columns[col].size ()) output += columns[col][lines]; else output += blanks[col]; // Trim right. output.erase (output.find_last_not_of (" ") + 1); output += "\n"; } } else { // Trim right. output.erase (output.find_last_not_of (" ") + 1); output += "\n"; } } return output; } ////////////////////////////////////////////////////////////////////////////////