14b3ddbf5SSyntaxseed<?php 24b3ddbf5SSyntaxseed 34b3ddbf5SSyntaxseed/** 44b3ddbf5SSyntaxseed * Plugin AVMathTable 54b3ddbf5SSyntaxseed * 64b3ddbf5SSyntaxseed * Adds math to columns for Dokuwiki tables. 74b3ddbf5SSyntaxseed * Supported Math: 84b3ddbf5SSyntaxseed * AVG - Calculate average of the column. 9*07316ee8SSyntaxseed * SUM - Calculate total of the entire column so far. 10*07316ee8SSyntaxseed * TOT - Calculate the total since the last total (TOT) was shown. Like a section subtotal. 11*07316ee8SSyntaxseed * ROW - Works like SUM but for the current row. Sums the cells to the left of the one where this command is called. Does not work with other commands in the row. Ie can't sum a row of totals (yet). 124b3ddbf5SSyntaxseed * CNT - Number of numeric values in the column above this cell. 134a9b1e96SSyntaxseed * MAX - Maximum value in the column. 144a9b1e96SSyntaxseed * MIN - Minimum value in the column. 154b3ddbf5SSyntaxseed * 164b3ddbf5SSyntaxseed * USAGE: 174b3ddbf5SSyntaxseed<mathtable> 184b3ddbf5SSyntaxseed^ Name ^ Deposited ^ Balance ^ 194b3ddbf5SSyntaxseed| John | 25 | 500 | 204b3ddbf5SSyntaxseed| Mary | 40 | 680 | 214b3ddbf5SSyntaxseed| Lex | 10 | 140 | 224b3ddbf5SSyntaxseed| TOTAL| =AVG | =SUM | 234b3ddbf5SSyntaxseed</mathtable> 244b3ddbf5SSyntaxseed * 254b3ddbf5SSyntaxseed * @license GPL-2.0 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) 264b3ddbf5SSyntaxseed * @author Sherri W. (http://syntaxseed.com) 274b3ddbf5SSyntaxseed */ 284b3ddbf5SSyntaxseed 294b3ddbf5SSyntaxseedif (!defined('DOKU_INC')) { 304b3ddbf5SSyntaxseed define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/'); 314b3ddbf5SSyntaxseed} 324b3ddbf5SSyntaxseedif (!defined('DOKU_PLUGIN')) { 334b3ddbf5SSyntaxseed define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 344b3ddbf5SSyntaxseed} 354b3ddbf5SSyntaxseed 364b3ddbf5SSyntaxseed 374b3ddbf5SSyntaxseed/** 384b3ddbf5SSyntaxseed * All DokuWiki plugins to extend the parser/rendering mechanism 394b3ddbf5SSyntaxseed * need to inherit from this class 404b3ddbf5SSyntaxseed */ 414b3ddbf5SSyntaxseedclass syntax_plugin_avmathtable extends DokuWiki_Syntax_Plugin 424b3ddbf5SSyntaxseed{ 434b3ddbf5SSyntaxseed private array $infoTable = []; 444b3ddbf5SSyntaxseed 454b3ddbf5SSyntaxseed /** 464b3ddbf5SSyntaxseed * What kind of syntax are we? 474b3ddbf5SSyntaxseed */ 484b3ddbf5SSyntaxseed public function getType() 494b3ddbf5SSyntaxseed { 504b3ddbf5SSyntaxseed return 'substition'; 514b3ddbf5SSyntaxseed } 524b3ddbf5SSyntaxseed 534b3ddbf5SSyntaxseed /** 544b3ddbf5SSyntaxseed * Where to sort in? 554b3ddbf5SSyntaxseed */ 564b3ddbf5SSyntaxseed public function getSort() 574b3ddbf5SSyntaxseed { 584b3ddbf5SSyntaxseed return 999; 594b3ddbf5SSyntaxseed } 604b3ddbf5SSyntaxseed 614b3ddbf5SSyntaxseed 624b3ddbf5SSyntaxseed /** 634b3ddbf5SSyntaxseed * Connect pattern to lexer 644b3ddbf5SSyntaxseed */ 654b3ddbf5SSyntaxseed public function connectTo($mode) 664b3ddbf5SSyntaxseed { 674b3ddbf5SSyntaxseed $this->Lexer->addEntryPattern('\<mathtable\>', $mode, 'plugin_avmathtable'); 684b3ddbf5SSyntaxseed } 694b3ddbf5SSyntaxseed 704b3ddbf5SSyntaxseed public function postConnect() 714b3ddbf5SSyntaxseed { 724b3ddbf5SSyntaxseed $this->Lexer->addExitPattern('\</mathtable\>', 'plugin_avmathtable'); 734b3ddbf5SSyntaxseed } 744b3ddbf5SSyntaxseed 754b3ddbf5SSyntaxseed 764b3ddbf5SSyntaxseed /** 774b3ddbf5SSyntaxseed * Handle the match 784b3ddbf5SSyntaxseed */ 794b3ddbf5SSyntaxseed public function handle($match, $state, $pos, Doku_Handler $handler) 804b3ddbf5SSyntaxseed { 814b3ddbf5SSyntaxseed switch ($state) { 824b3ddbf5SSyntaxseed case DOKU_LEXER_ENTER: 834b3ddbf5SSyntaxseed return array($state, ''); 844b3ddbf5SSyntaxseed case DOKU_LEXER_MATCHED: 854b3ddbf5SSyntaxseed break; 864b3ddbf5SSyntaxseed case DOKU_LEXER_UNMATCHED: 874b3ddbf5SSyntaxseed 884b3ddbf5SSyntaxseed $tables = $this->parseTable($match); 894b3ddbf5SSyntaxseed [$table, $info] = $tables; 904b3ddbf5SSyntaxseed 914b3ddbf5SSyntaxseed $this->infoTable = $info; 924b3ddbf5SSyntaxseed 934b3ddbf5SSyntaxseed return array($state, $tables); 944b3ddbf5SSyntaxseed 954b3ddbf5SSyntaxseed case DOKU_LEXER_EXIT: 964b3ddbf5SSyntaxseed return array($state, ''); 974b3ddbf5SSyntaxseed case DOKU_LEXER_SPECIAL: 984b3ddbf5SSyntaxseed break; 994b3ddbf5SSyntaxseed } 1004b3ddbf5SSyntaxseed return array(); 1014b3ddbf5SSyntaxseed } 1024b3ddbf5SSyntaxseed 1034b3ddbf5SSyntaxseed 1044b3ddbf5SSyntaxseed /** 1054b3ddbf5SSyntaxseed * Create output 1064b3ddbf5SSyntaxseed */ 1074b3ddbf5SSyntaxseed public function render($mode, Doku_Renderer $renderer, $data) 1084b3ddbf5SSyntaxseed { 1094b3ddbf5SSyntaxseed if ($mode == 'xhtml') { 1104b3ddbf5SSyntaxseed 1114b3ddbf5SSyntaxseed if (empty($data[1])) { 1124b3ddbf5SSyntaxseed return; 1134b3ddbf5SSyntaxseed } 1144b3ddbf5SSyntaxseed 1154b3ddbf5SSyntaxseed list($state, $tables) = $data; 1164b3ddbf5SSyntaxseed 1174b3ddbf5SSyntaxseed [$match, $info] = $tables; 1184b3ddbf5SSyntaxseed $this->infoTable = $info; 1194b3ddbf5SSyntaxseed 1204b3ddbf5SSyntaxseed switch ($state) { 1214b3ddbf5SSyntaxseed case DOKU_LEXER_ENTER: 1224b3ddbf5SSyntaxseed //$renderer->doc .= "<div class='avMathTable'>"; 1234b3ddbf5SSyntaxseed break; 1244b3ddbf5SSyntaxseed 1254b3ddbf5SSyntaxseed case DOKU_LEXER_MATCHED: 1264b3ddbf5SSyntaxseed break; 1274b3ddbf5SSyntaxseed 1284b3ddbf5SSyntaxseed case DOKU_LEXER_UNMATCHED: 1294b3ddbf5SSyntaxseed $info = []; 1304b3ddbf5SSyntaxseed 1314b3ddbf5SSyntaxseed $output = $this->renderArrayIntoTable($match); 1324b3ddbf5SSyntaxseed $html = p_render('xhtml', p_get_instructions($output), $info); 1334b3ddbf5SSyntaxseed 1344b3ddbf5SSyntaxseed $renderer->doc .= "<div class='avmathtable'>" . $html . "</div>"; 1354b3ddbf5SSyntaxseed 1364b3ddbf5SSyntaxseed break; 1374b3ddbf5SSyntaxseed 1384b3ddbf5SSyntaxseed case DOKU_LEXER_EXIT: 1394b3ddbf5SSyntaxseed //$renderer->doc .= "</div>"; 1404b3ddbf5SSyntaxseed break; 1414b3ddbf5SSyntaxseed 1424b3ddbf5SSyntaxseed case DOKU_LEXER_SPECIAL: 1434b3ddbf5SSyntaxseed break; 1444b3ddbf5SSyntaxseed } 1454b3ddbf5SSyntaxseed return true; 1464b3ddbf5SSyntaxseed } 1474b3ddbf5SSyntaxseed return false; 1484b3ddbf5SSyntaxseed } 1494b3ddbf5SSyntaxseed 1504b3ddbf5SSyntaxseed /** 1514b3ddbf5SSyntaxseed * Parse the table syntax into an array. 1524b3ddbf5SSyntaxseed */ 1534b3ddbf5SSyntaxseed private function parseTable(string $tableSyntax): array 1544b3ddbf5SSyntaxseed { 1554a9b1e96SSyntaxseed // Parse the wiki table text into a collection of instructions. 1564b3ddbf5SSyntaxseed $calls = p_get_instructions($tableSyntax); 1574b3ddbf5SSyntaxseed 1584a9b1e96SSyntaxseed // Convert to a multidimensional array 1594b3ddbf5SSyntaxseed $table = []; 1604b3ddbf5SSyntaxseed $row = []; 1614b3ddbf5SSyntaxseed $cell = null; 1624b3ddbf5SSyntaxseed 1634b3ddbf5SSyntaxseed $infoTable = []; // Keep track of things like if it's a header cell or a regular cell, alignment, etc. 1644b3ddbf5SSyntaxseed $infoRow = []; 1654b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => 'left']; 1664b3ddbf5SSyntaxseed 1674b3ddbf5SSyntaxseed foreach ($calls as $call) { 1684b3ddbf5SSyntaxseed [$cmd, $data] = $call; 1694b3ddbf5SSyntaxseed 1704b3ddbf5SSyntaxseed switch ($cmd) { 1714b3ddbf5SSyntaxseed 1724b3ddbf5SSyntaxseed case 'tableheader_open': 1734b3ddbf5SSyntaxseed $cell = ''; 1744b3ddbf5SSyntaxseed $infoCell = ['type' => 'header', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 1754b3ddbf5SSyntaxseed 1764b3ddbf5SSyntaxseed break; 1774b3ddbf5SSyntaxseed case 'tablecell_open': 1784b3ddbf5SSyntaxseed $cell = ''; 1794b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => (is_null($data[1]) ? 'left' : $data[1])]; 1804b3ddbf5SSyntaxseed break; 1814b3ddbf5SSyntaxseed 1824b3ddbf5SSyntaxseed case 'cdata': 1834b3ddbf5SSyntaxseed if ($cell !== null) { 1844b3ddbf5SSyntaxseed $cell .= $data[0]; 1854b3ddbf5SSyntaxseed } 1864b3ddbf5SSyntaxseed break; 1874b3ddbf5SSyntaxseed 1884b3ddbf5SSyntaxseed case 'tableheader_close': 1894b3ddbf5SSyntaxseed case 'tablecell_close': 1904b3ddbf5SSyntaxseed $row[] = trim($cell); 1914b3ddbf5SSyntaxseed $infoRow[] = $infoCell; 1924b3ddbf5SSyntaxseed $cell = null; // Reset 1934b3ddbf5SSyntaxseed $infoCell = ['type' => 'plain', 'alignment' => 'left']; // Reset 1944b3ddbf5SSyntaxseed break; 1954b3ddbf5SSyntaxseed 1964b3ddbf5SSyntaxseed case 'tablerow_close': 1974b3ddbf5SSyntaxseed $table[] = $row; 1984b3ddbf5SSyntaxseed $infoTable[] = $infoRow; 1994b3ddbf5SSyntaxseed $row = []; 2004b3ddbf5SSyntaxseed $infoRow = []; 2014b3ddbf5SSyntaxseed break; 2024b3ddbf5SSyntaxseed } 2034b3ddbf5SSyntaxseed } 2044b3ddbf5SSyntaxseed 2054b3ddbf5SSyntaxseed return [$table, $infoTable]; 2064b3ddbf5SSyntaxseed } 2074b3ddbf5SSyntaxseed 2084b3ddbf5SSyntaxseed 2094b3ddbf5SSyntaxseed private function renderArrayIntoTable(array $table): string 2104b3ddbf5SSyntaxseed { 2114b3ddbf5SSyntaxseed $output = ''; 2124b3ddbf5SSyntaxseed 2134b3ddbf5SSyntaxseed $columnData = []; 214*07316ee8SSyntaxseed $rowData = []; 2154b3ddbf5SSyntaxseed $rowNum = 1; 216*07316ee8SSyntaxseed 2174b3ddbf5SSyntaxseed // Create each row: 2184b3ddbf5SSyntaxseed foreach ($table as $i => $row) { 219*07316ee8SSyntaxseed $colNum = 1; // First column of a new row. 2204b3ddbf5SSyntaxseed // Create each cell: 2214b3ddbf5SSyntaxseed foreach ($row as $j => $cell) { 222*07316ee8SSyntaxseed 2234b3ddbf5SSyntaxseed // Initialize info about this column. 2244b3ddbf5SSyntaxseed if ($rowNum == 1) { 225*07316ee8SSyntaxseed $columnData[$j] = [ 226*07316ee8SSyntaxseed 'sum' => 0, 227*07316ee8SSyntaxseed 'count' => 0, 228*07316ee8SSyntaxseed 'total' => 0, 229*07316ee8SSyntaxseed 'precision' => 0, 230*07316ee8SSyntaxseed 'min' => null, 231*07316ee8SSyntaxseed 'max' => null 232*07316ee8SSyntaxseed ]; 233*07316ee8SSyntaxseed } 234*07316ee8SSyntaxseed 235*07316ee8SSyntaxseed // Initialize info about this row. 236*07316ee8SSyntaxseed if ($colNum == 1) { 237*07316ee8SSyntaxseed $rowData[$i] = [ 238*07316ee8SSyntaxseed 'sum' => 0, 239*07316ee8SSyntaxseed 'count' => 0, 240*07316ee8SSyntaxseed 'precision' => 0, 241*07316ee8SSyntaxseed 'min' => null, 242*07316ee8SSyntaxseed 'max' => null 243*07316ee8SSyntaxseed ]; 2444b3ddbf5SSyntaxseed } 2454b3ddbf5SSyntaxseed 2464b3ddbf5SSyntaxseed // Open up the cell wiki syntax. 2474b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['type'] == 'header') { 2484b3ddbf5SSyntaxseed $output .= "^ "; 2494b3ddbf5SSyntaxseed } else { 2504b3ddbf5SSyntaxseed $output .= "| "; 2514b3ddbf5SSyntaxseed } 2524b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['alignment'] == 'right' || $this->infoTable[$i][$j]['alignment'] == 'center') { 2534b3ddbf5SSyntaxseed $output .= " "; 2544b3ddbf5SSyntaxseed } 2554b3ddbf5SSyntaxseed 2564b3ddbf5SSyntaxseed // Gather info about the numbers in this cell. 2574b3ddbf5SSyntaxseed if (is_numeric($cell)) { 2584a9b1e96SSyntaxseed $columnData[$j]['count'] += 1; 259*07316ee8SSyntaxseed $rowData[$i]['count'] += 1; 2604a9b1e96SSyntaxseed $columnData[$j]['precision'] = max($columnData[$j]['precision'], $this->countDecimalPlaces($cell)); 261*07316ee8SSyntaxseed $rowData[$i]['precision'] = max($rowData[$i]['precision'], $this->countDecimalPlaces($cell)); 2624b3ddbf5SSyntaxseed if ((int)$cell == $cell) { 2634a9b1e96SSyntaxseed $numericCell = intval($cell); 2644b3ddbf5SSyntaxseed } elseif ((float)$cell == $cell) { 2654a9b1e96SSyntaxseed $numericCell = floatval($cell); 2664b3ddbf5SSyntaxseed } 2674a9b1e96SSyntaxseed $columnData[$j]['sum'] += $numericCell; 268*07316ee8SSyntaxseed $rowData[$i]['sum'] += $numericCell; 269*07316ee8SSyntaxseed $columnData[$j]['total'] += $numericCell; 2704a9b1e96SSyntaxseed $columnData[$j]['max'] = is_null($columnData[$j]['max']) ? $numericCell : max($columnData[$j]['max'], $numericCell); 2714a9b1e96SSyntaxseed $columnData[$j]['min'] = is_null($columnData[$j]['min']) ? $numericCell : min($columnData[$j]['min'], $numericCell); 272*07316ee8SSyntaxseed $rowData[$i]['max'] = is_null($rowData[$i]['max']) ? $numericCell : max($rowData[$i]['max'], $numericCell); 273*07316ee8SSyntaxseed $rowData[$i]['min'] = is_null($rowData[$i]['min']) ? $numericCell : min($rowData[$i]['min'], $numericCell); 2744b3ddbf5SSyntaxseed } 2754b3ddbf5SSyntaxseed 2764b3ddbf5SSyntaxseed // Insert the cell value. TODO : Handle special math features. 277*07316ee8SSyntaxseed $output .= $this->insertCellData($cell, $columnData, $rowData, $j, $i); 2784b3ddbf5SSyntaxseed 2794b3ddbf5SSyntaxseed 2804b3ddbf5SSyntaxseed // Close up the cell wiki syntax. 2814b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['alignment'] == 'left' || $this->infoTable[$i][$j]['alignment'] == 'center') { 2824b3ddbf5SSyntaxseed $output .= " "; 2834b3ddbf5SSyntaxseed } 2844b3ddbf5SSyntaxseed if ($this->infoTable[$i][$j]['type'] == 'header') { 2854b3ddbf5SSyntaxseed $output .= " ^"; 2864b3ddbf5SSyntaxseed } else { 2874b3ddbf5SSyntaxseed $output .= " |"; 2884b3ddbf5SSyntaxseed } 289*07316ee8SSyntaxseed $colNum++; 2904b3ddbf5SSyntaxseed } 2914b3ddbf5SSyntaxseed $output .= "\n"; // End of a row. 2924b3ddbf5SSyntaxseed $rowNum++; 2934b3ddbf5SSyntaxseed } 2944b3ddbf5SSyntaxseed 2954b3ddbf5SSyntaxseed //$dump = var_export($table, true); 2964b3ddbf5SSyntaxseed 2974b3ddbf5SSyntaxseed return $output; 2984b3ddbf5SSyntaxseed } 2994b3ddbf5SSyntaxseed 3004b3ddbf5SSyntaxseed /** 3014b3ddbf5SSyntaxseed * Put the value back in the cell. Substitute math where applicable. 3024b3ddbf5SSyntaxseed */ 303*07316ee8SSyntaxseed private function insertCellData(mixed $cell, array &$columnData, array &$rowData, int $colNum, int $rowNum,) 3044b3ddbf5SSyntaxseed { 3054b3ddbf5SSyntaxseed 3064b3ddbf5SSyntaxseed // echo('<pre>'); 3074b3ddbf5SSyntaxseed // var_dump($columnData); 3084b3ddbf5SSyntaxseed // echo('</pre>'); 3094b3ddbf5SSyntaxseed 3104b3ddbf5SSyntaxseed switch (trim($cell)) { 3114b3ddbf5SSyntaxseed case '=SUM': 3124a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . number_format($columnData[$colNum]['sum'], $columnData[$colNum]['precision']) .'</span>'; 3134b3ddbf5SSyntaxseed break; 314*07316ee8SSyntaxseed case '=ROW': 315*07316ee8SSyntaxseed return '<span class="avmathtablevalue">' . number_format($rowData[$rowNum]['sum'], $rowData[$rowNum]['precision']) .'</span>'; 316*07316ee8SSyntaxseed break; 317*07316ee8SSyntaxseed case '=TOT': 318*07316ee8SSyntaxseed $temp = number_format($columnData[$colNum]['total'], $columnData[$colNum]['precision']); 319*07316ee8SSyntaxseed $columnData[$colNum]['total'] = 0; // Reset to begin a new total. 320*07316ee8SSyntaxseed return '<span class="avmathtablevalue">' . $temp .'</span>'; 321*07316ee8SSyntaxseed break; 3224b3ddbf5SSyntaxseed case '=CNT': 3234b3ddbf5SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['count'] . '</span>'; 3244b3ddbf5SSyntaxseed break; 3254b3ddbf5SSyntaxseed case '=AVG': 3264a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . number_format(round(($columnData[$colNum]['sum'] / $columnData[$colNum]['count']), $columnData[$colNum]['precision']+1), $columnData[$colNum]['precision']+1) . '</span>'; 3274a9b1e96SSyntaxseed break; 3284a9b1e96SSyntaxseed case '=MAX': 3294a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['max'] . '</span>'; 3304a9b1e96SSyntaxseed break; 3314a9b1e96SSyntaxseed case '=MIN': 3324a9b1e96SSyntaxseed return '<span class="avmathtablevalue">' . $columnData[$colNum]['min'] . '</span>'; 3334b3ddbf5SSyntaxseed break; 3344b3ddbf5SSyntaxseed default: 3354b3ddbf5SSyntaxseed return $cell; 3364b3ddbf5SSyntaxseed } 3374b3ddbf5SSyntaxseed } 3384b3ddbf5SSyntaxseed 3394a9b1e96SSyntaxseed /** 3404a9b1e96SSyntaxseed * Count the decimal places after the period. 3414a9b1e96SSyntaxseed * Note that 50.00 gets treated as 50, so we need to count zeros separately and take the largest number. 3424a9b1e96SSyntaxseed */ 3434a9b1e96SSyntaxseed private function countDecimalPlaces(mixed $num): int 3444b3ddbf5SSyntaxseed { 3454a9b1e96SSyntaxseed // Number of 0s after the decimal: 3463fbc2f2aSSyntaxseed $numZeros = 0; 3473fbc2f2aSSyntaxseed if (strpos($num, '.') !== false) { 3484a9b1e96SSyntaxseed preg_match("/^(0+)/", explode('.', $num)[1], $matches); 3494a9b1e96SSyntaxseed $numZeros = strlen($matches[0]); 3503fbc2f2aSSyntaxseed } 3514a9b1e96SSyntaxseed 3524a9b1e96SSyntaxseed // Count number of significant digits after the decimal: 3534b3ddbf5SSyntaxseed $fNumber = floatval($num); 3544b3ddbf5SSyntaxseed for ($iDecimals = 0; $fNumber != round($fNumber, $iDecimals); $iDecimals++); 3554a9b1e96SSyntaxseed return max($iDecimals, $numZeros); 3564b3ddbf5SSyntaxseed } 3574b3ddbf5SSyntaxseed} // End class 358