* @author Jan Schneider * @copyright 2002-2005 Richard Heyes * @copyright 2006-2008 Jan Schneider * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) * @version CVS: $Id$ * @link http://pear.php.net/package/Console_Table */ define('CONSOLE_TABLE_HORIZONTAL_RULE', 1); define('CONSOLE_TABLE_ALIGN_LEFT', -1); define('CONSOLE_TABLE_ALIGN_CENTER', 0); define('CONSOLE_TABLE_ALIGN_RIGHT', 1); define('CONSOLE_TABLE_BORDER_ASCII', -1); /** * The main class. * * @category Console * @package Console_Table * @author Jan Schneider * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) * @link http://pear.php.net/package/Console_Table */ class Console_Table { /** * The table headers. * * @var array */ var $_headers = array(); /** * The data of the table. * * @var array */ var $_data = array(); /** * The maximum number of columns in a row. * * @var integer */ var $_max_cols = 0; /** * The maximum number of rows in the table. * * @var integer */ var $_max_rows = 0; /** * Lengths of the columns, calculated when rows are added to the table. * * @var array */ var $_cell_lengths = array(); /** * Heights of the rows. * * @var array */ var $_row_heights = array(); /** * How many spaces to use to pad the table. * * @var integer */ var $_padding = 1; /** * Column filters. * * @var array */ var $_filters = array(); /** * Columns to calculate totals for. * * @var array */ var $_calculateTotals; /** * Alignment of the columns. * * @var array */ var $_col_align = array(); /** * Default alignment of columns. * * @var integer */ var $_defaultAlign; /** * Character set of the data. * * @var string */ var $_charset = 'utf-8'; /** * Border characters. * Allowed keys: * - intersection - intersection ("+") * - horizontal - horizontal rule character ("-") * - vertical - vertical rule character ("|") * * @var array */ var $_border = array( 'intersection' => '+', 'horizontal' => '-', 'vertical' => '|', ); /** * If borders are shown or not * Allowed keys: top, right, bottom, left, inner: true and false * * @var array */ var $_borderVisibility = array( 'top' => true, 'right' => true, 'bottom' => true, 'left' => true, 'inner' => true ); /** * Whether the data has ANSI colors. * * @var Console_Color2 */ var $_ansiColor = false; /** * Constructor. * * @param integer $align Default alignment. One of * CONSOLE_TABLE_ALIGN_LEFT, * CONSOLE_TABLE_ALIGN_CENTER or * CONSOLE_TABLE_ALIGN_RIGHT. * @param string $border The character used for table borders or * CONSOLE_TABLE_BORDER_ASCII. * @param integer $padding How many spaces to use to pad the table. * @param string $charset A charset supported by the mbstring PHP * extension. * @param boolean $color Whether the data contains ansi color codes. */ function __construct($align = CONSOLE_TABLE_ALIGN_LEFT, $border = CONSOLE_TABLE_BORDER_ASCII, $padding = 1, $charset = null, $color = false) { $this->_defaultAlign = $align; $this->setBorder($border); $this->_padding = $padding; if ($color) { if (!class_exists('Console_Color2')) { include_once 'Console/Color2.php'; } $this->_ansiColor = new Console_Color2(); } if (!empty($charset)) { $this->setCharset($charset); } } /** * Converts an array to a table. * * @param array $headers Headers for the table. * @param array $data A two dimensional array with the table * data. * @param boolean $returnObject Whether to return the Console_Table object * instead of the rendered table. * * @static * * @return Console_Table|string A Console_Table object or the generated * table. */ function fromArray($headers, $data, $returnObject = false) { if (!is_array($headers) || !is_array($data)) { return false; } $table = new Console_Table(); $table->setHeaders($headers); foreach ($data as $row) { $table->addRow($row); } return $returnObject ? $table : $table->getTable(); } /** * Adds a filter to a column. * * Filters are standard PHP callbacks which are run on the data before * table generation is performed. Filters are applied in the order they * are added. The callback function must accept a single argument, which * is a single table cell. * * @param integer $col Column to apply filter to. * @param mixed &$callback PHP callback to apply. * * @return void */ function addFilter($col, &$callback) { $this->_filters[] = array($col, &$callback); } /** * Sets the charset of the provided table data. * * @param string $charset A charset supported by the mbstring PHP * extension. * * @return void */ function setCharset($charset) { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'en_US'); $this->_charset = strtolower($charset); setlocale(LC_CTYPE, $locale); } /** * Set the table border settings * * Border definition modes: * - CONSOLE_TABLE_BORDER_ASCII: Default border with +, - and | * - array with keys "intersection", "horizontal" and "vertical" * - single character string that sets all three of the array keys * * @param mixed $border Border definition * * @return void * @see $_border */ function setBorder($border) { if ($border === CONSOLE_TABLE_BORDER_ASCII) { $intersection = '+'; $horizontal = '-'; $vertical = '|'; } else if (is_string($border)) { $intersection = $horizontal = $vertical = $border; } else if ($border == '') { $intersection = $horizontal = $vertical = ''; } else { extract($border); } $this->_border = array( 'intersection' => $intersection, 'horizontal' => $horizontal, 'vertical' => $vertical, ); } /** * Set which borders shall be shown. * * @param array $visibility Visibility settings. * Allowed keys: left, right, top, bottom, inner * * @return void * @see $_borderVisibility */ function setBorderVisibility($visibility) { $this->_borderVisibility = array_merge( $this->_borderVisibility, array_intersect_key( $visibility, $this->_borderVisibility ) ); } /** * Sets the alignment for the columns. * * @param integer $col_id The column number. * @param integer $align Alignment to set for this column. One of * CONSOLE_TABLE_ALIGN_LEFT * CONSOLE_TABLE_ALIGN_CENTER * CONSOLE_TABLE_ALIGN_RIGHT. * * @return void */ function setAlign($col_id, $align = CONSOLE_TABLE_ALIGN_LEFT) { switch ($align) { case CONSOLE_TABLE_ALIGN_CENTER: $pad = STR_PAD_BOTH; break; case CONSOLE_TABLE_ALIGN_RIGHT: $pad = STR_PAD_LEFT; break; default: $pad = STR_PAD_RIGHT; break; } $this->_col_align[$col_id] = $pad; } /** * Specifies which columns are to have totals calculated for them and * added as a new row at the bottom. * * @param array $cols Array of column numbers (starting with 0). * * @return void */ function calculateTotalsFor($cols) { $this->_calculateTotals = $cols; } /** * Sets the headers for the columns. * * @param array $headers The column headers. * * @return void */ function setHeaders($headers) { $this->_headers = array(array_values($headers)); $this->_updateRowsCols($headers); } /** * Adds a row to the table. * * @param array $row The row data to add. * @param boolean $append Whether to append or prepend the row. * * @return void */ function addRow($row, $append = true) { if ($append) { $this->_data[] = array_values($row); } else { array_unshift($this->_data, array_values($row)); } $this->_updateRowsCols($row); } /** * Inserts a row after a given row number in the table. * * If $row_id is not given it will prepend the row. * * @param array $row The data to insert. * @param integer $row_id Row number to insert before. * * @return void */ function insertRow($row, $row_id = 0) { array_splice($this->_data, $row_id, 0, array($row)); $this->_updateRowsCols($row); } /** * Adds a column to the table. * * @param array $col_data The data of the column. * @param integer $col_id The column index to populate. * @param integer $row_id If starting row is not zero, specify it here. * * @return void */ function addCol($col_data, $col_id = 0, $row_id = 0) { foreach ($col_data as $col_cell) { $this->_data[$row_id++][$col_id] = $col_cell; } $this->_updateRowsCols(); $this->_max_cols = max($this->_max_cols, $col_id + 1); } /** * Adds data to the table. * * @param array $data A two dimensional array with the table data. * @param integer $col_id Starting column number. * @param integer $row_id Starting row number. * * @return void */ function addData($data, $col_id = 0, $row_id = 0) { foreach ($data as $row) { if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) { $this->_data[$row_id] = CONSOLE_TABLE_HORIZONTAL_RULE; $row_id++; continue; } $starting_col = $col_id; foreach ($row as $cell) { $this->_data[$row_id][$starting_col++] = $cell; } $this->_updateRowsCols(); $this->_max_cols = max($this->_max_cols, $starting_col); $row_id++; } } /** * Adds a horizontal seperator to the table. * * @return void */ function addSeparator() { $this->_data[] = CONSOLE_TABLE_HORIZONTAL_RULE; } /** * Returns the generated table. * * @return string The generated table. */ function getTable() { $this->_applyFilters(); $this->_calculateTotals(); $this->_validateTable(); return $this->_buildTable(); } /** * Calculates totals for columns. * * @return void */ function _calculateTotals() { if (empty($this->_calculateTotals)) { return; } $this->addSeparator(); $totals = array(); foreach ($this->_data as $row) { if (is_array($row)) { foreach ($this->_calculateTotals as $columnID) { $totals[$columnID] += $row[$columnID]; } } } $this->_data[] = $totals; $this->_updateRowsCols(); } /** * Applies any column filters to the data. * * @return void */ function _applyFilters() { if (empty($this->_filters)) { return; } foreach ($this->_filters as $filter) { $column = $filter[0]; $callback = $filter[1]; foreach ($this->_data as $row_id => $row_data) { if ($row_data !== CONSOLE_TABLE_HORIZONTAL_RULE) { $this->_data[$row_id][$column] = call_user_func($callback, $row_data[$column]); } } } } /** * Ensures that column and row counts are correct. * * @return void */ function _validateTable() { if (!empty($this->_headers)) { $this->_calculateRowHeight(-1, $this->_headers[0]); } for ($i = 0; $i < $this->_max_rows; $i++) { for ($j = 0; $j < $this->_max_cols; $j++) { if (!isset($this->_data[$i][$j]) && (!isset($this->_data[$i]) || $this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE)) { $this->_data[$i][$j] = ''; } } $this->_calculateRowHeight($i, $this->_data[$i]); if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) { ksort($this->_data[$i]); } } $this->_splitMultilineRows(); // Update cell lengths. for ($i = 0; $i < count($this->_headers); $i++) { $this->_calculateCellLengths($this->_headers[$i]); } for ($i = 0; $i < $this->_max_rows; $i++) { $this->_calculateCellLengths($this->_data[$i]); } ksort($this->_data); } /** * Splits multiline rows into many smaller one-line rows. * * @return void */ function _splitMultilineRows() { ksort($this->_data); $sections = array(&$this->_headers, &$this->_data); $max_rows = array(count($this->_headers), $this->_max_rows); $row_height_offset = array(-1, 0); for ($s = 0; $s <= 1; $s++) { $inserted = 0; $new_data = $sections[$s]; for ($i = 0; $i < $max_rows[$s]; $i++) { // Process only rows that have many lines. $height = $this->_row_heights[$i + $row_height_offset[$s]]; if ($height > 1) { // Split column data into one-liners. $split = array(); for ($j = 0; $j < $this->_max_cols; $j++) { $split[$j] = preg_split('/\r?\n|\r/', $sections[$s][$i][$j]); } $new_rows = array(); // Construct new 'virtual' rows - insert empty strings for // columns that have less lines that the highest one. for ($i2 = 0; $i2 < $height; $i2++) { for ($j = 0; $j < $this->_max_cols; $j++) { $new_rows[$i2][$j] = !isset($split[$j][$i2]) ? '' : $split[$j][$i2]; } } // Replace current row with smaller rows. $inserted is // used to take account of bigger array because of already // inserted rows. array_splice($new_data, $i + $inserted, 1, $new_rows); $inserted += count($new_rows) - 1; } } // Has the data been modified? if ($inserted > 0) { $sections[$s] = $new_data; $this->_updateRowsCols(); } } } /** * Builds the table. * * @return string The generated table string. */ function _buildTable() { if (!count($this->_data)) { return ''; } $vertical = $this->_border['vertical']; $separator = $this->_getSeparator(); $return = array(); for ($i = 0; $i < count($this->_data); $i++) { for ($j = 0; $j < count($this->_data[$i]); $j++) { if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE && $this->_strlen($this->_data[$i][$j]) < $this->_cell_lengths[$j]) { $this->_data[$i][$j] = $this->_strpad($this->_data[$i][$j], $this->_cell_lengths[$j], ' ', $this->_col_align[$j]); } } if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) { $row_begin = $this->_borderVisibility['left'] ? $vertical . str_repeat(' ', $this->_padding) : ''; $row_end = $this->_borderVisibility['right'] ? str_repeat(' ', $this->_padding) . $vertical : ''; $implode_char = str_repeat(' ', $this->_padding) . $vertical . str_repeat(' ', $this->_padding); $return[] = $row_begin . implode($implode_char, $this->_data[$i]) . $row_end; } elseif (!empty($separator)) { $return[] = $separator; } } $return = implode(PHP_EOL, $return); if (!empty($separator)) { if ($this->_borderVisibility['inner']) { $return = $separator . PHP_EOL . $return; } if ($this->_borderVisibility['bottom']) { $return .= PHP_EOL . $separator; } } $return .= PHP_EOL; if (!empty($this->_headers)) { $return = $this->_getHeaderLine() . PHP_EOL . $return; } return $return; } /** * Creates a horizontal separator for header separation and table * start/end etc. * * @return string The horizontal separator. */ function _getSeparator() { if (!$this->_border) { return; } $horizontal = $this->_border['horizontal']; $intersection = $this->_border['intersection']; $return = array(); foreach ($this->_cell_lengths as $cl) { $return[] = str_repeat($horizontal, $cl); } $row_begin = $this->_borderVisibility['left'] ? $intersection . str_repeat($horizontal, $this->_padding) : ''; $row_end = $this->_borderVisibility['right'] ? str_repeat($horizontal, $this->_padding) . $intersection : ''; $implode_char = str_repeat($horizontal, $this->_padding) . $intersection . str_repeat($horizontal, $this->_padding); return $row_begin . implode($implode_char, $return) . $row_end; } /** * Returns the header line for the table. * * @return string The header line of the table. */ function _getHeaderLine() { // Make sure column count is correct for ($j = 0; $j < count($this->_headers); $j++) { for ($i = 0; $i < $this->_max_cols; $i++) { if (!isset($this->_headers[$j][$i])) { $this->_headers[$j][$i] = ''; } } } for ($j = 0; $j < count($this->_headers); $j++) { for ($i = 0; $i < count($this->_headers[$j]); $i++) { if ($this->_strlen($this->_headers[$j][$i]) < $this->_cell_lengths[$i]) { $this->_headers[$j][$i] = $this->_strpad($this->_headers[$j][$i], $this->_cell_lengths[$i], ' ', $this->_col_align[$i]); } } } $vertical = $this->_border['vertical']; $row_begin = $this->_borderVisibility['left'] ? $vertical . str_repeat(' ', $this->_padding) : ''; $row_end = $this->_borderVisibility['right'] ? str_repeat(' ', $this->_padding) . $vertical : ''; $implode_char = str_repeat(' ', $this->_padding) . $vertical . str_repeat(' ', $this->_padding); $separator = $this->_getSeparator(); if (!empty($separator) && $this->_borderVisibility['top']) { $return[] = $separator; } for ($j = 0; $j < count($this->_headers); $j++) { $return[] = $row_begin . implode($implode_char, $this->_headers[$j]) . $row_end; } return implode(PHP_EOL, $return); } /** * Updates values for maximum columns and rows. * * @param array $rowdata Data array of a single row. * * @return void */ function _updateRowsCols($rowdata = null) { // Update maximum columns. $this->_max_cols = max($this->_max_cols, count($rowdata)); // Update maximum rows. ksort($this->_data); $keys = array_keys($this->_data); $this->_max_rows = end($keys) + 1; switch ($this->_defaultAlign) { case CONSOLE_TABLE_ALIGN_CENTER: $pad = STR_PAD_BOTH; break; case CONSOLE_TABLE_ALIGN_RIGHT: $pad = STR_PAD_LEFT; break; default: $pad = STR_PAD_RIGHT; break; } // Set default column alignments for ($i = 0; $i < $this->_max_cols; $i++) { if (!isset($this->_col_align[$i])) { $this->_col_align[$i] = $pad; } } } /** * Calculates the maximum length for each column of a row. * * @param array $row The row data. * * @return void */ function _calculateCellLengths($row) { for ($i = 0; $i < count($row); $i++) { if (!isset($this->_cell_lengths[$i])) { $this->_cell_lengths[$i] = 0; } $this->_cell_lengths[$i] = max($this->_cell_lengths[$i], $this->_strlen($row[$i])); } } /** * Calculates the maximum height for all columns of a row. * * @param integer $row_number The row number. * @param array $row The row data. * * @return void */ function _calculateRowHeight($row_number, $row) { if (!isset($this->_row_heights[$row_number])) { $this->_row_heights[$row_number] = 1; } // Do not process horizontal rule rows. if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) { return; } for ($i = 0, $c = count($row); $i < $c; ++$i) { $lines = preg_split('/\r?\n|\r/', $row[$i]); $this->_row_heights[$row_number] = max($this->_row_heights[$row_number], count($lines)); } } /** * Returns the character length of a string. * * @param string $str A multibyte or singlebyte string. * * @return integer The string length. */ function _strlen($str) { static $mbstring; // Strip ANSI color codes if requested. if ($this->_ansiColor) { $str = $this->_ansiColor->strip($str); } // Cache expensive function_exists() calls. if (!isset($mbstring)) { $mbstring = function_exists('mb_strwidth'); } if ($mbstring) { return mb_strwidth($str, $this->_charset); } return strlen($str); } /** * Returns part of a string. * * @param string $string The string to be converted. * @param integer $start The part's start position, zero based. * @param integer $length The part's length. * * @return string The string's part. */ function _substr($string, $start, $length = null) { static $mbstring; // Cache expensive function_exists() calls. if (!isset($mbstring)) { $mbstring = function_exists('mb_substr'); } if (is_null($length)) { $length = $this->_strlen($string); } if ($mbstring) { $ret = @mb_substr($string, $start, $length, $this->_charset); if (!empty($ret)) { return $ret; } } return substr($string, $start, $length); } /** * Returns a string padded to a certain length with another string. * * This method behaves exactly like str_pad but is multibyte safe. * * @param string $input The string to be padded. * @param integer $length The length of the resulting string. * @param string $pad The string to pad the input string with. Must * be in the same charset like the input string. * @param const $type The padding type. One of STR_PAD_LEFT, * STR_PAD_RIGHT, or STR_PAD_BOTH. * * @return string The padded string. */ function _strpad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT) { $mb_length = $this->_strlen($input); $sb_length = strlen($input); $pad_length = $this->_strlen($pad); /* Return if we already have the length. */ if ($mb_length >= $length) { return $input; } /* Shortcut for single byte strings. */ if ($mb_length == $sb_length && $pad_length == strlen($pad)) { return str_pad($input, $length, $pad, $type); } switch ($type) { case STR_PAD_LEFT: $left = $length - $mb_length; $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $this->_charset) . $input; break; case STR_PAD_BOTH: $left = floor(($length - $mb_length) / 2); $right = ceil(($length - $mb_length) / 2); $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $this->_charset) . $input . $this->_substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $this->_charset); break; case STR_PAD_RIGHT: $right = $length - $mb_length; $output = $input . $this->_substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $this->_charset); break; } return $output; } }