Support arbitrary group field count and different grouping types, generate table outside templates

master
vitalif 2010-08-13 10:03:56 +00:00
parent 15a685e546
commit 041ec8042c
5 changed files with 380 additions and 100 deletions

View File

@ -395,7 +395,7 @@ $v = array_pop(\$stack);
{
if (!preg_match('/^((?:\w+\.)*\w+)(\s*=\s*(.*))?/is', $t, $m))
return NULL;
if ($m[3])
if (strlen($m[3]))
{
if ($kw != 'set')
{
@ -481,7 +481,7 @@ $iset";
// тоже legacy, но пока оставлю...
function compile_code_fragment_begin($st, $kw, $t)
{
if (preg_match('/^([a-z_][a-z0-9_]*)(?:\s+AT\s+(.+))?(?:\s+BY\s+(.+))?(?:\s+TO\s+(.+))?/is', $t, $m))
if (preg_match('/^([a-z_][a-z0-9_]*)(?:\s+AT\s+(.+))?(?:\s+BY\s+(.+))?(?:\s+TO\s+(.+))?\s*$/is', $t, $m))
{
$st->blocks[] = $m[1];
$t = implode('.', $st->blocks);
@ -898,6 +898,9 @@ $iset";
function function_hash_keys($a) { return "array_keys(is_array($a) ? $a : array())"; }
function function_array_keys($a) { return "array_keys(is_array($a) ? $a : array())"; }
/* пары id => ключ, name => значение для ассоциативного массива */
function function_each($a) { return "array_id_name(is_array($a) ? $a : array())"; }
// создание массива
function function_array()
{
@ -905,6 +908,12 @@ $iset";
return "array(" . join(",", $a) . ")";
}
// проверка, массив это или нет?
function function_is_array($a) { return "is_array($a)"; }
// диапазон от $a до $b
function function_range($a, $b) { return "range($a,$b)"; }
// подмассив по номерам элементов
function function_subarray() { $a = func_get_args(); return "array_slice(" . join(",", $a) . ")"; }
function function_array_slice() { $a = func_get_args(); return "array_slice(" . join(",", $a) . ")"; }

359
olap.php
View File

@ -36,11 +36,12 @@ class OLAP
'stddev' => array('name' => 'Дисперсия', 'sql' => 'STDDEV($)'),
'n_uniq' => array('name' => 'Количество уникальных', 'sql' => 'COUNT(DISTINCT $)'),
);
static $specf = array(
'v_field', 'v_func', 'h_field', 'h_func',
'cell_field', 'cell_aggr', 'cell_func', 'cell_group_field', 'cell_group_func',
'v_sort_dir', 'v_sort_field', 'v_sort_aggr', 'v_sort_func',
'h_sort_dir', 'h_sort_field', 'h_sort_aggr', 'h_sort_func',
static $specf = array('cell_field', 'cell_aggr', 'cell_func');
static $group_types = array(
'tables' => 'Несколько таблиц',
'tr' => 'По вертикали',
'td' => 'По горизонтали',
'cell' => 'В ячейке',
);
static function execute($request)
@ -49,6 +50,7 @@ class OLAP
set_time_limit(300);
$time_start = microtime(true);
$template->compiletime_functions['fformat'] = "OLAP::tpl_field_format";
foreach (self::$sources as $k => &$s)
$s['id'] = $k;
self::$current_srcid = $request['datasource'];
@ -60,21 +62,34 @@ class OLAP
print $template->parse('admin_olap.tpl');
exit;
}
global $db, $dbhost, $dbuser, $dbpwd;
if (self::$current_src['db'])
list($db, $dbhost, $dbuser, $dbpwd) = self::$current_src['db'];
foreach (self::$current_src['fielddescs'] as $k => &$v)
{
$v['id'] = $k;
if ($v['options'])
{
if (is_callable($v['options']))
$v['options'] = call_user_func($v['options']);
foreach ($v['options'] as &$o)
$v['options_hash'][$o['id']] = $o['name'];
}
}
foreach (self::$functions as $k => &$v)
$v['id'] = $k;
foreach (self::$aggregates as $k => &$v)
$v['id'] = $k;
$i = 0;
foreach (self::$group_types as $k => &$v)
$v = array('id' => $k, 'num' => $i++, 'name' => $v);
unset($v);
$wh = array();
$where = array();
$group_fields = array();
foreach ($request as $k => $v)
{
if (substr($k, 0, 6) == 'where-')
@ -99,10 +114,24 @@ class OLAP
else
unset($wh[$k]);
}
elseif (substr($k, 0, 11) == 'group-type-')
{
$i = substr($k, 11);
$gf = array();
foreach(array('type', 'field', 'func', 'sort_dir', 'sort_field', 'sort_aggr', 'sort_func') as $k)
$gf[$k] = $request["group-$k-$i"];
if (self::$group_types[$gf['type']] && strlen($gf['field']))
{
$gf['num'] = count($group_fields);
$group_fields[] = $gf;
}
}
}
usort($group_fields, 'OLAP::group_type_cmp');
$template->vars(array(
'wh' => $wh,
'group_fields' => $group_fields,
'srcid' => self::$current_srcid,
'src' => self::$current_src,
'fielddescs' => array_values(self::$current_src['fielddescs']),
@ -129,24 +158,13 @@ class OLAP
$fields = array();
$options = array('GROUP BY' => array());
if ($v_field !== '')
foreach ($group_fields as $i => &$v)
{
$v_sql = self::sql_trans_field($v_field, $v_func);
$fields[] = $v_sql.' v_field';
$options['GROUP BY'][] = $v_sql;
}
if ($h_field !== '')
{
$h_sql = self::sql_trans_field($h_field, $h_func);
$fields[] = $h_sql.' h_field';
$options['GROUP BY'][] = $h_sql;
}
if ($cell_group_field !== '')
{
$cg_sql = self::sql_trans_field($cell_group_field, $cell_group_func);
$fields[] = $cg_sql.' cell_group_field';
$options['GROUP BY'][] = $cg_sql;
$v['sql'] = self::sql_trans_field($v['field'], $v['func']);
$fields[] = $v['sql'].' gf_'.$i;
$options['GROUP BY'][] = $v['sql'];
}
$cell_sql = self::sql_trans_field($cell_field, $cell_func);
if (self::$aggregates[$cell_aggr]['sql'])
$fields[] = str_replace('$', $cell_sql, self::$aggregates[$cell_aggr]['sql']).' cell_field';
@ -158,67 +176,62 @@ class OLAP
}
$result = mysql_select(self::$current_src['tables'], $fields, array_merge(self::$current_src['where'], $where), $options, MS_RESULT);
$group = array();
$hkeys = array();
$data = array();
$tdkeys = array();
while ($r = mysql_fetch_assoc($result))
{
$v = &$group[$r['v_field']][$r['h_field']];
if ($cell_group_field !== '')
$v = &$v[$r['cell_group_field']];
$dv = &$data;
$tdv = &$tdkeys;
foreach ($group_fields as $i => &$v)
{
$v['keys'][$r["gf_$i"]] = true;
$dv = &$dv[$r["gf_$i"]];
if ($v['type'] == 'tables' || $v['type'] == 'td')
{
if (!$tdkeys[$r["gf_$i"]])
$tdkeys[$r["gf_$i"]] = array();
$tdv = &$tdv[$r["gf_$i"]];
}
}
if ($do_aggr)
call_user_func_array('self::aggr_update_'.$cell_aggr, array(&$v, &$row));
call_user_func_array('self::aggr_update_'.$cell_aggr, array(&$dv, &$r['cell_field']));
else
$v = $r['cell_field'];
$hkeys[$r['h_field']] = 1;
$dv = $r['cell_field'];
}
unset($v);
if ($do_aggr && is_callable('self::aggr_finish_'.$cell_aggr))
{
if ($cell_group_field !== '')
foreach ($group as $i => &$r)
foreach ($r as $j => &$c)
call_user_func_array('self::aggr_finish_'.$cell_aggr, array(&$c, $i, $j));
else
foreach ($group as $i => &$r)
foreach ($r as $j => &$c)
foreach ($c as $k => &$g)
call_user_func_array('self::aggr_finish_'.$cell_aggr, array(&$g, $i, $j, $k));
$code = '';
$upd = 'self::aggr_finish_'.$cell_aggr.'(&$v'.(count($group_fields)-1);
foreach ($group_fields as $i => &$v)
{
$code .= 'foreach('.($i ? '$v'.($i-1) : '$data').' as $k'.$i.' => $v'.$i.') ';
$upd .= ', $k'.$i;
}
$code .= $upd;
eval($code);
}
if ($v_field !== '' && $v_sort_aggr !== '' && $v_sort_field !== '')
foreach ($group_fields as &$v)
{
$fields = array($v_sql.' v_field');
$vs_sql = self::sql_trans_field($v_sort_field, $v_sort_func);
$fields[] = str_replace('$', $vs_sql, self::$aggregates[$v_sort_aggr]['sql']).' v_sort_field';
$result = mysql_select(self::$current_src['tables'], $fields, array_merge(self::$current_src['where'], $where), array('GROUP BY' => $v_sql), MS_RESULT);
while ($r = mysql_fetch_row($result))
$vsort[$r[0]] = $r[1];
if (strlen($v['sort_aggr']) && strlen($v['sort_field']))
{
$fields = array($v['sql'].' f');
$v['sort_sql'] = self::sql_trans_field($v['sort_field'], $v['sort_func']);
$fields[] = str_replace('$', $v['sort_sql'], self::$aggregates[$v['sort_aggr']]['sql']).' s';
$result = mysql_select(self::$current_src['tables'], $fields, array_merge(self::$current_src['where'], $where), array('GROUP BY' => $v['sql']), MS_RESULT);
while ($r = mysql_fetch_row($result))
$v['sort'][$r[0]] = $r[1];
}
$v['keys'] = array_keys($v['keys']);
self::do_sort($v['keys'], $v['sort'], $v['sort_dir']);
}
if ($h_field !== '' && $h_sort_aggr !== '' && $h_sort_field !== '')
{
$fields = array($h_sql.' v_field');
$hs_sql = self::sql_trans_field($h_sort_field, $h_sort_func);
$fields[] = str_replace('$', $hs_sql, self::$aggregates[$h_sort_aggr]['sql']).' h_sort_field';
$result = mysql_select(self::$current_src['tables'], $fields, array_merge(self::$current_src['where'], $where), array('GROUP BY' => $h_sql), MS_RESULT);
while ($r = mysql_fetch_row($result))
$hsort[$r[0]] = $r[1];
}
$hkeys = array_keys($hkeys);
self::do_sort($hkeys, $hsort, $h_sort_dir);
$vkeys = array_keys($group);
self::do_sort($vkeys, $vsort, $v_sort_dir);
$tables = self::build_tables($group_fields, $data, $tdkeys);
$vars = array(
'build' => 1,
'hkeys' => $hkeys,
'vkeys' => $vkeys,
'hsort' => $hsort,
'vsort' => $vsort,
'data' => $group,
'tables' => $tables,
'rpt_link' => "?".http_build_query($_GET+$_POST),
'csv_link' => "?csv=1&".http_build_query($_GET+$_POST),
'memory' => file_size_string(memory_get_usage()),
@ -241,6 +254,205 @@ class OLAP
print $template->parse('admin_olap.tpl');
}
static function recurse_tables($fields, &$bytype, &$data, &$tdkeys)
{
if ($fields && $fields[0]['type'] == 'tables')
{
$tables = array();
$my = array_shift($fields);
foreach ($my['keys'] as $k)
{
if (array_key_exists($k, $data))
{
$sub = self::recurse_tables($fields, $bytype, $data[$k], $tdkeys[$k]);
foreach ($sub as &$t)
{
array_unshift($t['desc'], array(
'field' => self::$current_src['fielddescs'][$my['field']]['name'],
'func' => self::$functions[$my['func']]['name'],
'value' => self::field_format($my['field'], '', '', $k)
));
$tables[] = $t;
}
}
}
return $tables;
}
$tdhead = self::recurse_head($bytype['td'], $tdkeys);
$trhead = self::recurse_head($bytype['tr'], $data);
$cells = self::recurse_cells($fields, $data, $tdkeys);
$rows = array();
foreach ($tdhead as $i => &$v)
{
foreach ($v as &$h)
$h['class'] = "h$i";
$vcopy = $v;
if ($trhead)
array_unshift($vcopy, array('class' => 'empty', 'colspan' => count($trhead)));
$rows[] = $vcopy;
}
foreach ($trhead as $i => &$v)
{
$rownum = count($tdhead);
foreach ($v as &$h)
{
$rows[$rownum][] = array('text' => $h['text'], 'rowspan' => $h['colspan'], 'class' => "v$i");
$rownum += $h['colspan'];
}
}
$i = count($tdhead);
foreach ($cells as &$v)
{
if (!$rows[$i])
$rows[$i] = array();
$rows[$i] = array_merge($rows[$i], $v);
$i++;
}
foreach ($tdhead as &$v)
{
if ($v[0]['sort'])
{
$r = array();
if ($trhead)
$r[] = array('class' => 'empty', 'colspan' => count($trhead));
foreach ($v as &$h)
$r[] = array('text' => $h['sort'], 'colspan' => $h['colspan'], 'class' => 'hsort');
$rows[] = $r;
}
}
foreach (array_reverse($trhead) as $v)
{
if ($v[0]['sort'])
{
for ($rownum = 0; $rownum < count($tdhead); $rownum++)
$rows[$rownum][] = array('class' => 'empty');
foreach ($v as &$h)
{
$rows[$rownum][] = array('text' => $h['sort'], 'rowspan' => $h['colspan'], 'class' => 'vsort');
$rownum += $h['colspan'];
}
for (; $rownum < count($rows); $rownum++)
$rows[$rownum][] = array('class' => 'empty');
}
}
ksort($rows);
$table = array(
'desc' => array(),
'rows' => $rows,
);
return array($table);
}
static function recurse_cells($fields, &$data, &$tdkeys)
{
if (!$fields)
return self::field_format(self::$current_spec['cell_field'], self::$current_spec['cell_func'], self::$current_spec['cell_aggr'], $data);
$my = array_shift($fields);
if ($my['type'] == 'tr')
{
$rows = array();
foreach ($my['keys'] as $k)
{
if (array_key_exists($k, $data))
{
$r = self::recurse_cells($fields, $data[$k], $tdkeys);
if (is_array($r))
$rows = array_merge($rows, $r);
else
$rows[] = array($r);
}
}
return $rows;
}
elseif ($my['type'] == 'td')
{
$cols = array();
foreach ($my['keys'] as $k)
{
if (array_key_exists($k, $tdkeys))
{
$r = self::recurse_cells($fields, $data[$k], $tdkeys[$k]);
if (is_array($r))
$cols = array_merge($cols, $r[0]);
else
$cols[] = $r;
}
}
return array($cols);
}
else // if ($my['type'] == 'cell')
{
$list = '<ul>';
foreach ($my['keys'] as $k)
{
if (array_key_exists($k, $data))
{
$list .= '<li>'.self::field_format($my['field'], $my['func'], '', $k).':&nbsp;';
if ($fields)
$list .= "\n";
$sub = self::recurse_cells($fields, $data[$k], $tdkeys);
$sub = str_replace('<li>', ' <li>', $sub);
$list .= $sub;
$list .= "</li>\n";
}
}
$list .= '</ul>';
return $list;
}
}
static function recurse_head($fields, &$data)
{
$rows = array();
if (!$fields)
return $rows;
$rows[] = array();
$my = array_shift($fields);
foreach ($my['keys'] as $k)
{
if (array_key_exists($k, $data))
{
$span = 0;
if ($fields)
{
$subrows = self::recurse_head($fields, $data[$k]);
foreach ($subrows[0] as $td)
$span += $td['colspan'];
foreach ($subrows as $i => &$row)
{
if (!$rows[$i+1])
$rows[$i+1] = array();
$rows[$i+1] = array_merge($rows[$i+1], $row);
}
}
else
$span = 1;
$c = array('text' => self::field_format($my['field'], $my['func'], '', $k), 'colspan' => $span);
if ($my['sort_field'])
$c['sort'] = self::field_format($my['sort_field'], $my['sort_func'], $my['sort_aggr'], $my['sort'][$k]);
$rows[0][] = $c;
}
}
return $rows;
}
static function array_refcopy(&$a)
{
$b = array();
foreach ($a as $k => &$v)
$b[$k] = &$v;
return $b;
}
static function build_tables($group_fields, &$data, &$tdkeys)
{
$bytype = array();
foreach ($group_fields as &$gf)
$bytype[$gf['type']][] = &$gf;
$tables = self::recurse_tables($group_fields, $bytype, $data, $tdkeys);
return $tables;
}
static function sql_trans_field($field, $func)
{
$fd = &self::$current_src['fielddescs'][$field];
@ -285,6 +497,13 @@ class OLAP
{
return self::$sort[$a] > self::$sort[$b] ? -1 : (self::$sort[$a] < self::$sort[$b] ? 1 : 0);
}
static function group_type_cmp($a, $b)
{
$r = self::$group_types[$a['type']]['num']-self::$group_types[$b['type']]['num'];
if ($r == 0)
$r = $a['num']-$b['num'];
return $r;
}
static $is_html_format = true;
static $decimal = '%.3f';
@ -294,7 +513,11 @@ class OLAP
if ($o && $o[$value])
return $o[$value];
elseif (preg_match('/^-?\d+\.\d+$/s', $value))
return sprintf(self::$decimal, $value);
{
$value = sprintf(self::$decimal, $value);
$value = preg_replace('/\.0+$|(\.\d*?)0+$/', '\1', $value);
return $value;
}
$fn = self::$is_html_format ? 'htmlspecialchars' : 'addslashes';
return $fn($value);
}

View File

@ -1,5 +1,10 @@
<?php
function v2tags_get_tags()
{
return mysql_select('tags', 'id, name', array('type' => 0));
}
OLAP::$sources = array(
'v2tags' => array(
'id' => 'v2tags',
@ -7,19 +12,49 @@ OLAP::$sources = array(
'tables' => array('t' => 'stats_tag_all'),
'fields' => 't.ts, t.tag',
'where' => array(),
'fetch_row' => true,
'fielddescs' => array(
'ts' => array(
'name' => 'Время',
'le_ge' => true,
'is_time' => true,
'format' => TS_UNIX,
'dbname' => 'ts',
),
'tag' => array(
'name' => 'Тег',
'dbname' => 'tag',
'options' => mysql_select('tags', 'id, name', array('type' => 0)),
'options' => 'v2tags_get_tags',
),
),
),
'chk' => array(
'id' => 'chk',
'db' => array('chk', 'localhost', 'chk', 'chk'),
'name' => 'Чеки', // date, price, seller, comment, count, each
'tables' => array('t' => 'chk'),
'fields' => 't.date, t.price, t.seller, t.comment, t.count, t.each',
'where' => array(),
'fielddescs' => array(
'date' => array(
'name' => 'Дата',
'le_ge' => true,
'is_time' => true,
'format' => TS_DB,
),
'price' => array(
'name' => 'Цена',
'le_ge' => true,
),
'seller' => array(
'name' => 'Где',
),
'comment' => array(
'name' => 'Комментарий',
),
'count' => array(
'name' => 'Количество',
),
'each' => array(
'name' => 'Цена за каждый',
'sql' => '`each`',
),
),
),

View File

@ -19,8 +19,20 @@ p { margin: 4px 0; }
.themes li { padding: 0; display: inline; margin-right: 8px; }
.simpletable, .simpletable td { border: 1px solid black; }
.simpletable { border-collapse: collapse; }
.simpletable th { padding: 3px 8px; }
.simpletable td { padding: 3px; }
.simpletable.center td { text-align: center; }
.simpletable td ul { padding: 0 6px 0 20px; }
.simpletable td li { text-align: left; }
.simpletable th { padding: 3px 8px; background-color: #a0a0a0; vertical-align: top; }
.simpletable th.empty { background-color: white; }
.simpletable th.h0, .simpletable th.v0 { background-color: #f0f0f0; }
.simpletable th.h1, .simpletable th.v1 { background-color: #e0e0e0; }
.simpletable th.h2, .simpletable th.v2 { background-color: #d0d0d0; }
.simpletable th.h3, .simpletable th.v3 { background-color: #c0c0c0; }
.simpletable th.h4, .simpletable th.v4 { background-color: #b0b0b0; }
.simpletable th.hsort, .simpletable th.vsort { background-color: white; color: gray; font-weight: normal; }
.simpletable th.h0, .simpletable th.h1, .simpletable th.h2, .simpletable th.h3, .simpletable th.h4, .simpletable th.hsort { border-right: 1px dashed gray; }
.simpletable th.v0, .simpletable th.v1, .simpletable th.v2, .simpletable th.v3, .simpletable th.v4, .simpletable th.vsort { border-bottom: 1px dashed gray; }
.simpletable td { padding: 3px; vertical-align: top; }
#middle ul.list div.right { float: left; }
-->
</style>

View File

@ -93,36 +93,37 @@
<!-- IF build -->
<h1>Отчёт</h1>
<p><a href="{s rpt_link}">Ссылка на данный отчёт</a> | <a href="{s csv_link}">В формате CSV</a></p>
<!-- IF NOT data -->
<!-- IF NOT tables -->
<p>Нет данных для показа.</p>
<!-- ELSE -->
<!-- SET hascg = strlen(cell_group_field) -->
<table class="simpletable">
<tr><th></th><!-- FOR k = hkeys --><th>{fformat(h_field,h_func,'',k)}</th><!-- END --><!-- IF vsort --><th></th><!-- END --></tr>
<!-- FOR v = vkeys -->
<tr>
<th>{fformat(v_field,v_func,'',v)}</th>
<!-- FOR k = hkeys -->
<td>
<!-- SET cv = get(get(data,v),k) -->
<!-- IF hascg -->
<!-- FOR ck = keys(cv) -->
{fformat(cell_group_field,cell_group_func,'',ck)}:&nbsp;{fformat(cell_field,cell_func,cell_aggr,get(cv,ck))}<br />
<!-- FOR table = tables -->
<!-- IF table.desc -->
<!-- SET o = 0 -->
<!-- FOR d = table.desc -->
<!-- SET o = or(o, not(table#), ne(get(get(get(get(tables,sub(table#,1)),'desc'),d#),'value'),d.value)) -->
<!-- IF o -->
<p style="margin-left: {mul(d#,20)}px"><!-- IF d# -->, <!-- END -->{d.field}<!-- IF d.func --> ({lc d.func})<!-- END -->: {d.value}</p>
<!-- END -->
<!-- END -->
<div style="margin-left: {mul(count(table.desc),20)}px">
<!-- END -->
<table class="simpletable center">
<!-- FOR row = table.rows -->
<tr>
<!-- FOR c = row -->
<!-- IF is_array(c) -->
<th<!-- IF c.colspan --> colspan="{c.colspan}"<!-- END --><!-- IF c.rowspan --> rowspan="{c.rowspan}"<!-- END --><!-- IF c.class --> class="{c.class}"<!-- END -->>{c.text}</th>
<!-- ELSE -->
<td>{c}</td>
<!-- END -->
<!-- ELSE -->
{fformat(cell_field,cell_func,cell_aggr,cv)}
<!-- END -->
</td>
<!-- END -->
<!-- IF vsort -->
<th>{get(vsort,v)}</th>
<!-- END -->
</tr>
<!-- END -->
<!-- IF hsort -->
<tr><th></th><!-- FOR k = hkeys --><th>{get(hsort,k)}</th><!-- END --><!-- IF vsort --><th></th><!-- END --></tr>
<!-- END -->
<!-- END -->
</table>
<!-- IF table.desc -->
</div>
<!-- END -->
<!-- END -->
<p>Отчёт занял {time_elapsed} сек. Использовано {memory} памяти для работы.</p>
<!-- END -->
<!-- END -->