Skip to content
Extraits de code Groupes Projets
Valider e2c4a994 rédigé par Thomas Steur's avatar Thomas Steur Validation de sgiehl
Parcourir les fichiers

support for secondary sort and faster sort

parent a2bd2fbe
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -98,6 +98,9 @@ class DataTableGenericFilter ...@@ -98,6 +98,9 @@ class DataTableGenericFilter
array( array(
'filter_sort_column' => array('string'), 'filter_sort_column' => array('string'),
'filter_sort_order' => array('string', 'desc'), 'filter_sort_order' => array('string', 'desc'),
$naturalSort = true,
$recursiveSort = false,
$doSortBySecondaryColumn = true
)), )),
array('Truncate', array('Truncate',
array( array(
...@@ -159,22 +162,27 @@ class DataTableGenericFilter ...@@ -159,22 +162,27 @@ class DataTableGenericFilter
} }
foreach ($filterParams as $name => $info) { foreach ($filterParams as $name => $info) {
// parameter type to cast to if (!is_array($info)) {
$type = $info[0]; // hard coded value that cannot be changed via API, see eg $naturalSort = true in 'Sort'
$filterParameters[] = $info;
// default value if specified, when the parameter doesn't have a value } else {
$defaultValue = null; // parameter type to cast to
if (isset($info[1])) { $type = $info[0];
$defaultValue = $info[1];
} // default value if specified, when the parameter doesn't have a value
$defaultValue = null;
try { if (isset($info[1])) {
$value = Common::getRequestVar($name, $defaultValue, $type, $this->request); $defaultValue = $info[1];
settype($value, $type); }
$filterParameters[] = $value;
} catch (Exception $e) { try {
$exceptionRaised = true; $value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
break; settype($value, $type);
$filterParameters[] = $value;
} catch (Exception $e) {
$exceptionRaised = true;
break;
}
} }
} }
......
...@@ -877,6 +877,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess ...@@ -877,6 +877,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
return $this->rows; return $this->rows;
} }
/**
* @ignore
*/
public function getRowsCountWithoutSummaryRow()
{
return count($this->rows);
}
/** /**
* Returns an array containing all column values for the requested column. * Returns an array containing all column values for the requested column.
* *
......
...@@ -13,6 +13,7 @@ use Piwik\DataTable\Row; ...@@ -13,6 +13,7 @@ use Piwik\DataTable\Row;
use Piwik\DataTable\Simple; use Piwik\DataTable\Simple;
use Piwik\DataTable; use Piwik\DataTable;
use Piwik\Metrics; use Piwik\Metrics;
use Piwik\Metrics\Sorter;
/** /**
* Sorts a {@link DataTable} based on the value of a specific column. * Sorts a {@link DataTable} based on the value of a specific column.
...@@ -25,7 +26,8 @@ class Sort extends BaseFilter ...@@ -25,7 +26,8 @@ class Sort extends BaseFilter
{ {
protected $columnToSort; protected $columnToSort;
protected $order; protected $order;
protected $sign; protected $naturalSort;
protected $isSecondaryColumnSortEnabled;
const ORDER_DESC = 'desc'; const ORDER_DESC = 'desc';
const ORDER_ASC = 'asc'; const ORDER_ASC = 'asc';
...@@ -38,8 +40,10 @@ class Sort extends BaseFilter ...@@ -38,8 +40,10 @@ class Sort extends BaseFilter
* @param string $order order `'asc'` or `'desc'`. * @param string $order order `'asc'` or `'desc'`.
* @param bool $naturalSort Whether to use a natural sort or not (see {@link http://php.net/natsort}). * @param bool $naturalSort Whether to use a natural sort or not (see {@link http://php.net/natsort}).
* @param bool $recursiveSort Whether to sort all subtables or not. * @param bool $recursiveSort Whether to sort all subtables or not.
* @param bool $doSortBySecondaryColumn If true will sort by a secondary column. The column is automatically
* detected and will be either nb_visits or label, if possible.
*/ */
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true) public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true, $doSortBySecondaryColumn = false)
{ {
parent::__construct($table); parent::__construct($table);
...@@ -48,144 +52,9 @@ class Sort extends BaseFilter ...@@ -48,144 +52,9 @@ class Sort extends BaseFilter
} }
$this->columnToSort = $columnToSort; $this->columnToSort = $columnToSort;
$this->naturalSort = $naturalSort; $this->naturalSort = $naturalSort;
$this->setOrder($order); $this->order = strtolower($order);
} $this->isSecondaryColumnSortEnabled = $doSortBySecondaryColumn;
/**
* Updates the order
*
* @param string $order asc|desc
*/
public function setOrder($order)
{
if ($order == 'asc') {
$this->order = 'asc';
$this->sign = 1;
} else {
$this->order = 'desc';
$this->sign = -1;
}
}
/**
* Sorting method used for sorting numbers
*
* @param array $rowA array[0 => value of column to sort, 1 => label]
* @param array $rowB array[0 => value of column to sort, 1 => label]
* @return int
*/
public function numberSort($rowA, $rowB)
{
if (isset($rowA[0]) && isset($rowB[0])) {
if ($rowA[0] != $rowB[0] || !isset($rowA[1])) {
return $this->sign * ($rowA[0] < $rowB[0] ? -1 : 1);
} else {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
}
} elseif (!isset($rowB[0]) && !isset($rowA[0])) {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
} elseif (!isset($rowA[0])) {
return 1;
}
return -1;
}
/**
* Sorting method used for sorting values natural
*
* @param mixed $valA
* @param mixed $valB
* @return int
*/
public function naturalSort($valA, $valB)
{
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($valA)
? 1
: (!isset($valB)
? -1
: $this->sign * strnatcasecmp(
$valA,
$valB
)
)
);
}
/**
* Sorting method used for sorting values
*
* @param mixed $valA
* @param mixed $valB
* @return int
*/
public function sortString($valA, $valB)
{
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($valA)
? 1
: (!isset($valB)
? -1
: $this->sign *
strcasecmp($valA,
$valB
)
)
);
}
protected function getColumnValue(Row $row)
{
$value = $row->getColumn($this->columnToSort);
if ($value === false || is_array($value)) {
return null;
}
return $value;
}
/**
* Sets the column to be used for sorting
*
* @param Row $row
* @return int
*/
protected function selectColumnToSort($row)
{
$value = $row->hasColumn($this->columnToSort);
if ($value) {
return $this->columnToSort;
}
$columnIdToName = Metrics::getMappingFromNameToId();
// sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table
if (isset($columnIdToName[$this->columnToSort])) {
$column = $columnIdToName[$this->columnToSort];
$value = $row->hasColumn($column);
if ($value) {
return $column;
}
}
// eg. was previously sorted by revenue_per_visit, but this table
// doesn't have this column; defaults with nb_visits
$column = Metrics::INDEX_NB_VISITS;
$value = $row->hasColumn($column);
if ($value) {
return $column;
}
// even though this column is not set properly in the table,
// we select it for the sort, so that the table's internal state is set properly
return $this->columnToSort;
} }
/** /**
...@@ -204,87 +73,48 @@ class Sort extends BaseFilter ...@@ -204,87 +73,48 @@ class Sort extends BaseFilter
return; return;
} }
if (!$table->getRowsCount()) { if (!$table->getRowsCountWithoutSummaryRow()) {
return; return;
} }
$row = $table->getFirstRow(); $row = $table->getFirstRow();
if ($row === false) { if ($row === false) {
return; return;
} }
$this->columnToSort = $this->selectColumnToSort($row); $config = new Sorter\Config();
$sorter = new Sorter($config);
$value = $this->getFirstValueFromDataTable($table); $config->naturalSort = $this->naturalSort;
$config->primaryColumnToSort = $sorter->getPrimaryColumnToSort($table, $this->columnToSort);
$config->primarySortOrder = $sorter->getPrimarySortOrder($this->order);
$config->primarySortFlags = $sorter->getBestSortFlags($table, $config->primaryColumnToSort);
$config->secondaryColumnToSort = $sorter->getSecondaryColumnToSort($row, $config->primaryColumnToSort);
$config->secondarySortOrder = $sorter->getSecondarySortOrder($this->order, $config->secondaryColumnToSort);
$config->secondarySortFlags = $sorter->getBestSortFlags($table, $config->secondaryColumnToSort);
if (is_numeric($value) && $this->columnToSort !== 'label') { // secondary sort should not be needed for all other sort flags (eg string/natural sort) as label is unique and would make it slower
$methodToUse = "numberSort"; $isSecondaryColumnSortNeeded = $config->primarySortFlags === SORT_NUMERIC;
} else { $config->isSecondaryColumnSortEnabled = $this->isSecondaryColumnSortEnabled && $isSecondaryColumnSortNeeded;
if ($this->naturalSort) {
$methodToUse = "naturalSort";
} else {
$methodToUse = "sortString";
}
}
$this->sort($table, $methodToUse); $this->sort($sorter, $table);
} }
private function getFirstValueFromDataTable($table) private function sort(Sorter $sorter, DataTable $table)
{ {
foreach ($table->getRowsWithoutSummaryRow() as $row) { $sorter->sort($table);
$value = $this->getColumnValue($row);
if (!is_null($value)) {
return $value;
}
}
}
/**
* Sorts the DataTable rows using the supplied callback function.
*
* @param string $functionCallback A comparison callback compatible with {@link usort}.
* @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
* so we can determine how the DataTable was sorted in the future.
*/
private function sort(DataTable $table, $functionCallback)
{
$table->setTableSortedBy($this->columnToSort);
$rows = $table->getRowsWithoutSummaryRow();
// get column value and label only once for performance tweak
$values = array();
if ($functionCallback === 'numberSort') {
foreach ($rows as $key => $row) {
$values[$key] = array($this->getColumnValue($row), $row->getColumn('label'));
}
} else {
foreach ($rows as $key => $row) {
$values[$key] = $this->getColumnValue($row);
}
}
uasort($values, array($this, $functionCallback));
$sortedRows = array();
foreach ($values as $key => $value) {
$sortedRows[] = $rows[$key];
}
$table->setRows($sortedRows);
unset($rows);
unset($sortedRows);
if ($table->isSortRecursiveEnabled()) { if ($table->isSortRecursiveEnabled()) {
foreach ($table->getRowsWithoutSummaryRow() as $row) { foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable(); $subTable = $row->getSubtable();
if ($subTable) { if ($subTable) {
$subTable->enableRecursiveSort(); $subTable->enableRecursiveSort();
$this->sort($subTable, $functionCallback); $this->sort($sorter, $subTable);
} }
} }
} }
} }
}
}
\ No newline at end of file
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics;
use Piwik\Plugin\Metric;
use Piwik\Plugin\Report;
class Sorter
{
/**
* @var Sorter\Config
*/
private $config;
public function __construct(Sorter\Config $config)
{
$this->config = $config;
}
/**
* Sorts the DataTable rows using the supplied callback function.
*
* @param DataTable $table The table to sort.
*/
public function sort(DataTable $table)
{
// all that code is in here and not in separate methods for best performance. It does make a difference once
// php has to copy many (eg 50k) rows otherwise.
$table->setTableSortedBy($this->config->primaryColumnToSort);
$rows = $table->getRowsWithoutSummaryRow();
// we need to sort rows that have a value separately from rows that do not have a value since we always want
// to append rows that do not have a value at the end.
$rowsWithValues = array();
$rowsWithoutValues = array();
$valuesToSort = array();
foreach ($rows as $key => $row) {
$value = $this->getColumnValue($row);
if (isset($value)) {
$valuesToSort[] = $value;
$rowsWithValues[] = $row;
} else {
$rowsWithoutValues[] = $row;
}
}
unset($rows);
if ($this->config->isSecondaryColumnSortEnabled && $this->config->secondaryColumnToSort) {
$secondaryValues = array();
foreach ($rowsWithValues as $key => $row) {
$secondaryValues[$key] = $row->getColumn($this->config->secondaryColumnToSort);
}
array_multisort($valuesToSort, $this->config->primarySortOrder, $this->config->primarySortFlags, $secondaryValues, $this->config->secondarySortOrder, $this->config->secondarySortFlags, $rowsWithValues);
} else {
array_multisort($valuesToSort, $this->config->primarySortOrder, $this->config->primarySortFlags, $rowsWithValues);
}
if (!empty($rowsWithoutValues) && $this->config->secondaryColumnToSort) {
$secondaryValues = array();
foreach ($rowsWithoutValues as $key => $row) {
$secondaryValues[$key] = $row->getColumn($this->config->secondaryColumnToSort);
}
array_multisort($secondaryValues, $this->config->secondarySortOrder, $this->config->secondarySortFlags, $rowsWithoutValues);
}
unset($secondaryValues);
foreach ($rowsWithoutValues as $row) {
$rowsWithValues[] = $row;
}
$table->setRows(array_values($rowsWithValues));
}
private function getColumnValue(Row $row)
{
$value = $row->getColumn($this->config->primaryColumnToSort);
if ($value === false || is_array($value)) {
return null;
}
return $value;
}
/**
* @param string $order 'asc' or 'desc'
* @return int
*/
public function getPrimarySortOrder($order)
{
if ($order === 'asc') {
return SORT_ASC;
}
return SORT_DESC;
}
/**
* @param string $order 'asc' or 'desc'
* @param string|int $secondarySortColumn column name or column id
* @return int
*/
public function getSecondarySortOrder($order, $secondarySortColumn)
{
if ($secondarySortColumn === 'label') {
$secondaryOrder = SORT_ASC;
if ($order === 'asc') {
$secondaryOrder = SORT_DESC;
}
return $secondaryOrder;
}
return $this->getPrimarySortOrder($order);
}
/**
* Detect the column to be used for sorting
*
* @param DataTable $table
* @param string|int $columnToSort column name or column id
* @return int
*/
public function getPrimaryColumnToSort(DataTable $table, $columnToSort)
{
// we fallback to nb_visits in case columnToSort does not exist
$columnsToCheck = array($columnToSort, 'nb_visits');
$row = $table->getFirstRow();
foreach ($columnsToCheck as $column) {
$column = Metric::getActualMetricColumn($table, $column);
if ($row->hasColumn($column)) {
// since getActualMetricColumn() returns a default value, we need to make sure it actually has that column
return $column;
}
}
return $columnToSort;
}
/**
* Detect the secondary sort column to be used for sorting
*
* @param Row $row
* @param int|string $primaryColumnToSort
* @return int
*/
public function getSecondaryColumnToSort(Row $row, $primaryColumnToSort)
{
$defaultSecondaryColumn = array(Metrics::INDEX_NB_VISITS, 'nb_visits');
if (in_array($primaryColumnToSort, $defaultSecondaryColumn)) {
// if sorted by visits, then sort by label as a secondary column
$column = 'label';
$value = $row->hasColumn($column);
if ($value !== false) {
return $column;
}
return null;
}
if ($primaryColumnToSort !== 'label') {
// we do not add this by default to make sure we do not sort by label as a first and secondary column
$defaultSecondaryColumn[] = 'label';
}
foreach ($defaultSecondaryColumn as $column) {
$value = $row->hasColumn($column);
if ($value !== false) {
return $column;
}
}
}
/**
* @param DataTable $table
* @param string|int $columnToSort A column name or column id. Make sure that column actually exists in the row.
* You might want to get a valid column via {@link getPrimaryColumnToSort()} or
* {@link getSecondaryColumnToSort()}
* @return int
*/
public function getBestSortFlags(DataTable $table, $columnToSort)
{
// when column is label we always to sort by string or natural
if ($columnToSort !== 'label') {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$value = $row->getColumn($columnToSort);
if ($value !== false && $value !== null && !is_array($value)) {
if (is_numeric($value)) {
$sortFlags = SORT_NUMERIC;
} else {
$sortFlags = $this->getStringSortFlags();
}
return $sortFlags;
}
}
}
return $this->getStringSortFlags();
}
private function getStringSortFlags()
{
if ($this->config->naturalSort) {
$sortFlags = SORT_NATURAL | SORT_FLAG_CASE;
} else {
$sortFlags = SORT_STRING | SORT_FLAG_CASE;
}
return $sortFlags;
}
}
\ No newline at end of file
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Metrics\Sorter;
class Config
{
public $naturalSort = false;
public $primaryColumnToSort;
public $primarySortFlags;
public $primarySortOrder;
public $secondaryColumnToSort;
public $secondarySortOrder;
public $secondarySortFlags;
public $isSecondaryColumnSortEnabled = true;
}
\ No newline at end of file
...@@ -174,16 +174,19 @@ abstract class Metric ...@@ -174,16 +174,19 @@ abstract class Metric
*/ */
public static function getActualMetricColumn(DataTable $table, $columnName, $mappingNameToId = null) public static function getActualMetricColumn(DataTable $table, $columnName, $mappingNameToId = null)
{ {
if (empty($mappingIdToName)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
$firstRow = $table->getFirstRow(); $firstRow = $table->getFirstRow();
if (!empty($firstRow)
&& $firstRow->getColumn($columnName) === false if (!empty($firstRow) && $firstRow->hasColumn($columnName) === false) {
) {
$columnName = $mappingNameToId[$columnName]; if (empty($mappingIdToName)) {
$mappingNameToId = Metrics::getMappingFromNameToId();
}
if (array_key_exists($columnName, $mappingNameToId)) {
$columnName = $mappingNameToId[$columnName];
}
} }
return $columnName; return $columnName;
} }
} }
...@@ -14,8 +14,10 @@ use Piwik\DataTable\Row; ...@@ -14,8 +14,10 @@ use Piwik\DataTable\Row;
/** /**
* @group DataTableTest * @group DataTableTest
* @group Core
* @group sort
*/ */
class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase class SortTest extends \PHPUnit_Framework_TestCase
{ {
public function testNormalSortDescending() public function testNormalSortDescending()
...@@ -47,7 +49,6 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase ...@@ -47,7 +49,6 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedOrder, $table->getColumn('label')); $this->assertEquals($expectedOrder, $table->getColumn('label'));
} }
public function testMissingColumnValuesShouldAppearLastAfterSortAsc() public function testMissingColumnValuesShouldAppearLastAfterSortAsc()
{ {
$table = new DataTable(); $table = new DataTable();
...@@ -83,8 +84,6 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase ...@@ -83,8 +84,6 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase
/** /**
* Test to sort by label * Test to sort by label
*
* @group Core
*/ */
public function testFilterSortString() public function testFilterSortString()
{ {
...@@ -125,36 +124,31 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase ...@@ -125,36 +124,31 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase
/** /**
* Test to sort by visit * Test to sort by visit
*
* @group Core
*/ */
public function testFilterSortNumeric() public function testFilterSortNumeric()
{ {
$idcol = Row::COLUMNS; $table = $this->createDataTable(array(
$table = new DataTable(); array('label' => 'google', 'nb_visits' => 897), //0
$rows = array( array('label' => 'ask', 'nb_visits' => -152), //1
array($idcol => array('label' => 'google', 'nb_visits' => 897)), //0 array('label' => 'piwik', 'nb_visits' => 1.5), //2
array($idcol => array('label' => 'ask', 'nb_visits' => -152)), //1 array('label' => 'yahoo', 'nb_visits' => 154), //3
array($idcol => array('label' => 'piwik', 'nb_visits' => 1.5)), //2 array('label' => 'amazon', 'nb_visits' => 30), //4
array($idcol => array('label' => 'yahoo', 'nb_visits' => 154)), //3 array('label' => '238949', 'nb_visits' => 0), //5
array($idcol => array('label' => 'amazon', 'nb_visits' => 30)), //4 array('label' => 'Q*(%&*', 'nb_visits' => 1) //6
array($idcol => array('label' => '238949', 'nb_visits' => 0)), //5 ));
array($idcol => array('label' => 'Q*(%&*', 'nb_visits' => 1)) //6
);
$table->addRowsFromArray($rows);
$expectedtable = new DataTable();
$rows = array( $rows = array(
array($idcol => array('label' => 'ask', 'nb_visits' => -152)), //1 array('label' => 'ask', 'nb_visits' => -152), //1
array($idcol => array('label' => '238949', 'nb_visits' => 0)), //5 array('label' => '238949', 'nb_visits' => 0), //5
array($idcol => array('label' => 'Q*(%&*', 'nb_visits' => 1)), //6 array('label' => 'Q*(%&*', 'nb_visits' => 1), //6
array($idcol => array('label' => 'piwik', 'nb_visits' => 1.5)), //2 array('label' => 'piwik', 'nb_visits' => 1.5), //2
array($idcol => array('label' => 'amazon', 'nb_visits' => 30)), //4 array('label' => 'amazon', 'nb_visits' => 30), //4
array($idcol => array('label' => 'yahoo', 'nb_visits' => 154)), //3 array('label' => 'yahoo', 'nb_visits' => 154), //3
array($idcol => array('label' => 'google', 'nb_visits' => 897)) //0 array('label' => 'google', 'nb_visits' => 897) //0
); );
$expectedtable->addRowsFromArray($rows);
$expectedtableReverse = new DataTable(); $expectedtable = $this->createDataTable($rows);
$expectedtableReverse->addRowsFromArray(array_reverse($rows)); $expectedtableReverse = $this->createDataTable(array_reverse($rows));
$filter = new Sort($table, 'nb_visits', 'asc'); $filter = new Sort($table, 'nb_visits', 'asc');
$filter->filter($table); $filter->filter($table);
...@@ -165,6 +159,47 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase ...@@ -165,6 +159,47 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase
$this->assertTrue(DataTable::isEqual($table, $expectedtableReverse)); $this->assertTrue(DataTable::isEqual($table, $expectedtableReverse));
} }
/**
* Test to sort by visit
*/
public function testFilterSortNumeric_withSecondaryColumnSortLabel()
{
$rows = array(
array('label' => 'google', 'nb_visits' => array()),
array('label' => 'ask', 'nb_visits' => false),
array('label' => 'piwik', 'nb_visits' => 143),
array('label' => 'yahoo', 'nb_visits' => 154),
array('label' => 'zzzzz', 'nb_visits' => false),
array('label' => 'amazon', 'nb_visits' => 154),
array('label' => '238949', 'nb_visits' => 154),
array('label' => 'Q*(%&*', 'nb_visits' => 1)
);
$table = $this->createDataTable($rows);
$rows = array(
array('label' => '238949', 'nb_visits' => 154),
array('label' => 'amazon', 'nb_visits' => 154),
array('label' => 'yahoo', 'nb_visits' => 154),
array('label' => 'piwik', 'nb_visits' => 143),
array('label' => 'Q*(%&*', 'nb_visits' => 1),
array('label' => 'ask', 'nb_visits' => false),
array('label' => 'google', 'nb_visits' => array()),
array('label' => 'zzzzz', 'nb_visits' => false)
);
$expectedtable = $this->createDataTable($rows);
$expectedtableReverse = $this->createDataTable(array_reverse($rows));
$filter = new Sort($table, 'nb_visits', 'desc', $natural = true, $reverse = false, $secondaryColumn = true);
$filter->filter($table);
$this->assertTrue(DataTable::isEqual($table, $expectedtable));
$filter = new Sort($table, 'nb_visits', 'asc', $natural = true, $reverse = false, $secondaryColumn = true);
$filter->filter($table);
$this->assertTrue(DataTable::isEqual($table, $expectedtableReverse));
}
public function test_sortingArrayValues_doesNotError() public function test_sortingArrayValues_doesNotError()
{ {
$table = new DataTable(); $table = new DataTable();
...@@ -180,4 +215,56 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase ...@@ -180,4 +215,56 @@ class DataTable_Filter_SortTest extends \PHPUnit_Framework_TestCase
$filter->filter($table); $filter->filter($table);
$this->assertTrue(DataTable::isEqual($tableOriginal, $table)); $this->assertTrue(DataTable::isEqual($tableOriginal, $table));
} }
/**
* Test to sort by label
*/
public function testFilter_shouldPickStringSearchEvenIfFirstLabelIsNumeric()
{
$table = $this->createDataTable(array(
array('label' => '238975247578949'),
array('label' => 'google'),
array('label' => '013494'),
array('label' => '9'),
array('label' => '999yahoo'),
array('label' => '494'),
array('label' => 'Q*(%&*("$&%*(&"$*")"))')
));
$rows = array(
array('label' => '9'),
array('label' => '494'),
array('label' => '999yahoo'),
array('label' => '013494'),
array('label' => '238975247578949'),
array('label' => 'google'),
array('label' => 'Q*(%&*("$&%*(&"$*")"))')
);
$expectedtable = $this->createDataTable($rows);
$expectedtableReverse = $this->createDataTable(array_reverse($rows));
$filter = new Sort($table, 'label', 'asc', $natural = true);
$filter->filter($table);
$this->assertTrue(DataTable::isEqual($expectedtable, $table));
$filter = new Sort($table, 'label', 'desc');
$filter->filter($table);
$this->assertTrue(DataTable::isEqual($table, $expectedtableReverse));
}
private function createDataTable($rows)
{
$table = new DataTable();
foreach ($rows as $columns) {
$table->addRow($this->createRow($columns));
}
return $table;
}
private function createRow($columns)
{
return new Row(array(Row::COLUMNS => $columns));
}
} }
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Tests\Unit\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics;
use Piwik\Metrics\Sorter;
use Piwik\Metrics\Sorter\Config;
use Piwik\Tests\Framework\TestCase\UnitTestCase;
/**
* @group Core
* @group sort
*/
class SorterTest extends UnitTestCase
{
/**
* @var Sorter
*/
private $sorter;
/**
* @var Config
*/
private $config;
public function setUp()
{
parent::setUp();
$this->config = new Config();
$this->config->primaryColumnToSort = 'nb_visits';
$this->config->primarySortOrder = SORT_DESC;
$this->config->primarySortFlags = SORT_NUMERIC;
$this->sorter = new Sorter($this->config);
}
public function test_getPrimarySortOrder_shouldReturnDescByDefault()
{
$this->assertSame(SORT_DESC, $this->sorter->getPrimarySortOrder(null));
$this->assertSame(SORT_DESC, $this->sorter->getPrimarySortOrder('whatever'));
$this->assertSame(SORT_DESC, $this->sorter->getPrimarySortOrder('desc'));
}
public function test_getPrimarySortOrder_shouldReturnAscIfRequestedLowerCase()
{
$this->assertSame(SORT_ASC, $this->sorter->getPrimarySortOrder('asc'));
$this->assertSame(SORT_DESC, $this->sorter->getPrimarySortOrder('AsC')); // we require 'asc' to be lowercase
}
public function test_getSecondarySortOrder_shouldReturnInvertedOrder_IfColumnIsLabel()
{
$this->assertSame(SORT_DESC, $this->sorter->getSecondarySortOrder('asc', 'label'));
$this->assertSame(SORT_ASC, $this->sorter->getSecondarySortOrder('whatever', 'label'));
$this->assertSame(SORT_ASC, $this->sorter->getSecondarySortOrder('desc', 'label'));
$this->assertSame(SORT_ASC, $this->sorter->getSecondarySortOrder('AsC', 'label'));
}
public function test_getPrimarySortOrder_shouldReturnDescByDefault_IfNotLabelColumnIsRequested()
{
$this->assertSame(SORT_DESC, $this->sorter->getSecondarySortOrder(null, 'nb_visits'));
$this->assertSame(SORT_DESC, $this->sorter->getSecondarySortOrder('whatever', 'nb_visits'));
$this->assertSame(SORT_DESC, $this->sorter->getSecondarySortOrder('desc', 'nb_visits'));
}
public function test_getSecondarySortOrder_shouldReturnAscIfRequestedLowerCase_IfNotLabelColumnIsRequested()
{
$this->assertSame(SORT_ASC, $this->sorter->getSecondarySortOrder('asc', 'nb_visits'));
$this->assertSame(SORT_DESC, $this->sorter->getSecondarySortOrder('AsC', 'nb_visits')); // we require 'asc' to be lowercase
}
/**
* @dataProvider getPrimaryColumnsToSort
*/
public function test_getPrimaryColumnToSort_shouldPickCorrectPrimaryColumnAndMapMetricNameToIdIfNeededAndReverse($expectedUsedColumn, $columnToSortBy)
{
$table = $this->createDataTable(array(
array('label' => 'nintendo', 'nb_visits' => false, 'nb_hits' => 0, Metrics::INDEX_NB_VISITS_CONVERTED => false, Metrics::INDEX_BOUNCE_COUNT => 5)
));
$this->assertSame($expectedUsedColumn, $this->sorter->getPrimaryColumnToSort($table, $columnToSortBy));
}
public function getPrimaryColumnsToSort()
{
return array(
array('nb_visits', 'nb_visits'), // it is present in the row and should be used even though the value is false
array('nb_hits', 'nb_hits'), // it is present in the row and should be used even though the value is zero
array(Metrics::INDEX_NB_VISITS_CONVERTED, 'nb_visits_converted'), // the column name is not present but it should find the column id even though the value is false
array(Metrics::INDEX_NB_VISITS_CONVERTED, Metrics::INDEX_NB_VISITS_CONVERTED), // if a column is present as id it should still be able to find it
array(Metrics::INDEX_BOUNCE_COUNT, 'bounce_count'), // should resolve column name to id, column has a value
array(Metrics::INDEX_BOUNCE_COUNT, Metrics::INDEX_BOUNCE_COUNT), // should find a column with a value
);
}
public function test_getPrimaryColumnToSort_shouldFallbackToNbVisitsIfPossible()
{
$table = $this->createDataTable(array(
array('label' => 'nintendo', 'nb_visits' => false)
));
$this->assertSame('nb_visits', $this->sorter->getPrimaryColumnToSort($table, 'any_random_column_that_doesnt_exist'));
}
public function test_getPrimaryColumnToSort_shouldFallbackToThePassedColumnNameIfColumnCannotBeFoundAndNbVisitsDoesNotExist()
{
$table = $this->createDataTable(array(array('label' => 'nintendo')));
$this->assertSame('any_random_column_that_doesnt_exist', $this->sorter->getPrimaryColumnToSort($table, 'any_random_column_that_doesnt_exist'));
}
public function test_getSecondaryColumnToSort_shouldNotFindASecondaryColumnToSort_IfSortedByLabelButNoVisitsColumnPresent()
{
$row = $this->createRow(array('label' => 'nintendo'));
$this->assertNull($this->sorter->getSecondaryColumnToSort($row, 'label'));
}
public function test_getSecondaryColumnToSort_shouldPreferVisitsColumn_IfColumnIsPresent_EvenIfValueIsFalse()
{
$row = $this->createRow(array('label' => 'nintendo', 'nb_visits' => false, 'nb_hits' => 10));
$this->assertSame('nb_visits', $this->sorter->getSecondaryColumnToSort($row, 'nb_hits'));
$this->assertSame('nb_visits', $this->sorter->getSecondaryColumnToSort($row, 'label'));
}
public function test_getSecondaryColumnToSort_shouldPreferVisitsColumn_IfColumnIsPresent_EvenIfVisitsColumnIsId()
{
$row = $this->createRow(array('label' => 'nintendo', Metrics::INDEX_NB_VISITS => false, 'nb_hits' => 10));
$this->assertSame(Metrics::INDEX_NB_VISITS, $this->sorter->getSecondaryColumnToSort($row, 'nb_hits'));
$this->assertSame(Metrics::INDEX_NB_VISITS, $this->sorter->getSecondaryColumnToSort($row, 'label'));
}
public function test_getSecondaryColumnToSort_shouldUseLabelColumn_IfColumnIsPresentButNotNbVisitsColumn()
{
$row = $this->createRow(array('label' => 'nintendo', 'nb_hits' => 10));
$this->assertSame('label', $this->sorter->getSecondaryColumnToSort($row, 'nb_hits'));
}
public function test_getSecondaryColumnToSort_shouldUseLabelColumn_IfPrimaryColumnIsNbVisitsColumn()
{
$row = $this->createRow(array('label' => 'nintendo', 'nb_visits' => 10));
$this->assertSame('label', $this->sorter->getSecondaryColumnToSort($row, 'nb_visits'));
$this->assertSame('label', $this->sorter->getSecondaryColumnToSort($row, Metrics::INDEX_NB_VISITS));
}
public function test_getSecondaryColumnToSort_shouldNotBeAbleToFallback_IfVisitsColumnIsUsedButThereIsNoLabelColumn()
{
$row = $this->createRow(array('nb_visits' => 10, 'nb_hits' => 10));
$this->assertNull($this->sorter->getSecondaryColumnToSort($row, 'nb_visits'));
}
public function test_getSecondaryColumnToSort_shouldUseVisitsAsSecondaryColumn_IfLabelIsUsedAsPrimaryColumn()
{
$row = $this->createRow(array('label' => 'nintendo', 'nb_visits' => false));
$this->assertSame('nb_visits', $this->sorter->getSecondaryColumnToSort($row, 'label'));
}
/**
* @dataProvider getLabelsForNaturalSortTest
*/
public function test_getBestSortFlags_shouldAlwaysPickStringOrNaturalSortCaseInsensitive($label)
{
$table = $this->createDataTable(array(array('label' => $label)));
$this->config->naturalSort = false; // even if natural sort is not preferred it should be still used
$this->assertSame(SORT_STRING | SORT_FLAG_CASE, $this->sorter->getBestSortFlags($table, 'label'));
$this->config->naturalSort = true;
$this->assertSame(SORT_NATURAL | SORT_FLAG_CASE, $this->sorter->getBestSortFlags($table, 'label'));
}
public function getLabelsForNaturalSortTest()
{
return array(array('nintendo'), array('2015'), array('240.4'), array(2015), array('/test'));
}
/**
* @dataProvider getColumnsForBestSortFlagsTest
*/
public function test_getBestSortFlags($expectedSortFlags, $columnToReadFrom, $naturalSort = false)
{
$this->config->naturalSort = $naturalSort;
$table = $this->createDataTable(array(
array('label' => 'nintendo1', 'nb_visits' => false, 'nb_hits' => 0, Metrics::INDEX_NB_VISITS_CONVERTED => false, Metrics::INDEX_BOUNCE_COUNT => 5),
array('label' => 'nintendo2', 'nb_visits' => 100, 'nb_pageviews' => 100, Metrics::INDEX_NB_VISITS_CONVERTED => null, 'sum_visit_length' => '5.5s'),
array('label' => 'nintendo2', Metrics::INDEX_NB_VISITS_CONVERTED => array(), 'min_time_generation' => '5.5')
));
$this->assertSame($expectedSortFlags, $this->sorter->getBestSortFlags($table, $columnToReadFrom));
}
public function getColumnsForBestSortFlagsTest()
{
return array(
array(SORT_NUMERIC, 'nb_visits'), // should find a numeric value in the first row
array(SORT_NUMERIC, 'nb_pageviews'), // should find a numeric value in the second row
array(SORT_STRING | SORT_FLAG_CASE, Metrics::INDEX_NB_VISITS_CONVERTED), // should not find any value in any row and use default value
array(SORT_NATURAL | SORT_FLAG_CASE, Metrics::INDEX_NB_VISITS_CONVERTED, true), // should not find any value in any row and use default value, natural preferred
array(SORT_STRING | SORT_FLAG_CASE, 'sum_visit_length'), // it is not numeric so should use string as natural is disabled
array(SORT_NATURAL | SORT_FLAG_CASE, 'sum_visit_length', true), // it is not numeric but natural is preferred so should use natural sort
array(SORT_NUMERIC, 'min_time_generation') // value is a string but numeric so should use numeric
);
}
public function test_sort_shouldNotFailIfNoRowsAreSet()
{
$table = $this->createDataTable(array());
$this->sorter->sort($table);
$this->assertSame(0, $table->getRowsCount());
}
public function test_sort_shouldSetTheSortedColumnNameOnTheTable()
{
$table = $this->createDataTable(array(array('nb_test' => 5)));
$this->config->primaryColumnToSort = 'nb_test';
$this->sorter->sort($table);
$this->assertSame('nb_test', $table->getSortedByColumnName());
}
public function test_sort_shouldKeepTheAmountOfColumns()
{
$table = $this->createDataTableFromValues(array(5, null));
$table->addSummaryRow($this->createRow(array('nb_test' => 10)));
$this->sorter->sort($table);
$this->assertSame(3, $table->getRowsCount());
$this->assertSame(2, $table->getRowsCountWithoutSummaryRow());
}
public function test_sort_shouldNotSortOrChangeTheSummaryRow()
{
$table = $this->createDataTableFromValues(array(5, null));
$table->addSummaryRow($this->createRow(array('nb_test' => 10)));
$this->sorter->sort($table);
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
$this->assertSame(10, $summaryRow->getColumn('nb_test'));
}
public function test_sort_shouldSortNumeric_AndShouldAddEmptyValuesAlwaysAtTheEnd()
{
$table = $this->createDataTableFromValues(array(5, null, 61, array(), 10, false, 20, 15));
$this->config->primarySortFlags = SORT_NUMERIC;
$this->config->primarySortOrder = SORT_ASC;
$this->sorter->sort($table);
$expected = array(5, 10, 15, 20, 61, false, array(), false);
$this->assertExpectedRowsOrder($expected, $table);
$this->config->primarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$expected = array(61, 20, 15, 10, 5, false, array(), false);
$this->assertExpectedRowsOrder($expected, $table);
}
public function test_sort_sortNatural_ShoudAddEmptyValuesAlwaysAtTheEnd()
{
$table = $this->createDataTableFromValues(array('nintendo', null, 'abc', array(), 'DeF', 'def', false, '1210', 'piwik'));
$this->config->primarySortFlags = SORT_NATURAL;
$this->config->primarySortOrder = SORT_ASC;
$this->sorter->sort($table);
$expected = array('1210', 'DeF', 'abc', 'def', 'nintendo', 'piwik', false, array(), false);
$this->assertExpectedRowsOrder($expected, $table);
$this->config->primarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$expected = array('piwik', 'nintendo', 'def', 'abc', 'DeF', '1210', false, array(), false);
$this->assertExpectedRowsOrder($expected, $table);
}
public function test_sort_ShoudIgnoreASecondColumnSort_IfDisabled()
{
$table = $this->createDataTableFromValues(array('abc', 'abc', 'abc', 'abc', 'abc'));
$this->config->primarySortFlags = SORT_NATURAL;
$this->config->isSecondaryColumnSortEnabled = false;
$this->sorter->sort($table);
// we make sure the labels order did not change neither when ASC nor DESC
$expected = array('My Label 0', 'My Label 1', 'My Label 2', 'My Label 3', 'My Label 4');
$this->assertExpectedRowsOrder($expected, $table, 'label');
$this->config->secondarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$this->assertExpectedRowsOrder($expected, $table, 'label');
}
public function test_sort_ShoudIgnoreASecondColumnSort_IfSortIsNumericButNoSecondaryColumnIsSet()
{
$table = $this->createDataTableFromValues(array('abc', 'abc', 'abc', 'abc', 'abc'));
$this->config->primarySortFlags = SORT_NUMERIC;
$this->sorter->sort($table);
// we make sure the labels order did not change neither when ASC nor DESC
$expected = array('My Label 0', 'My Label 1', 'My Label 2', 'My Label 3', 'My Label 4');
$this->assertExpectedRowsOrder($expected, $table, 'label');
$this->config->secondarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$this->assertExpectedRowsOrder($expected, $table, 'label');
}
public function test_sort_ShoudSortBySecondColumn_IfSortedNumeric()
{
$table = $this->createDataTableFromValues(array('abc', 'abc', 'abc', 'abc', 'abc'));
$this->config->primarySortFlags = SORT_NUMERIC;
$this->config->secondaryColumnToSort = 'label';
$this->config->secondarySortOrder = SORT_ASC;
$this->config->secondarySortFlags = SORT_NATURAL;
$this->sorter->sort($table);
// we make sure the labels order did not change neither when ASC nor DESC
$expected = array('My Label 0', 'My Label 1', 'My Label 2', 'My Label 3', 'My Label 4');
$this->assertExpectedRowsOrder($expected, $table, 'label');
$this->config->secondarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$expected = array_reverse($expected);
$this->assertExpectedRowsOrder($expected, $table, 'label');
}
public function test_sort_ShoudSortEmptyValues_BySecondColumn_IfSortedNumeric()
{
$table = $this->createDataTableFromValues(array(null, null, null, null, null));
$this->config->primarySortFlags = SORT_NUMERIC;
$this->config->secondaryColumnToSort = 'label';
$this->config->secondarySortOrder = SORT_ASC;
$this->config->secondarySortFlags = SORT_NATURAL;
$this->sorter->sort($table);
// we make sure the labels order did not change neither when ASC nor DESC
$expected = array('My Label 0', 'My Label 1', 'My Label 2', 'My Label 3', 'My Label 4');
$this->assertExpectedRowsOrder($expected, $table, 'label');
$this->config->secondarySortOrder = SORT_DESC;
$this->sorter->sort($table);
$expected = array_reverse($expected);
$this->assertExpectedRowsOrder($expected, $table, 'label');
}
private function assertExpectedRowsOrder($expectedValuesOrder, $table, $column = 'nb_visits')
{
foreach ($table->getRows() as $index => $row) {
$this->assertSame($expectedValuesOrder[$index], $row->getColumn($column));
}
}
private function createDataTableFromValues($values)
{
$rows = array();
foreach ($values as $index => $value) {
$rows[] = array('nb_visits' => $value, 'label' => 'My Label ' . $index);
}
return $this->createDataTable($rows);
}
private function createDataTable($rows)
{
$table = new DataTable();
foreach ($rows as $columns) {
$table->addRow($this->createRow($columns));
}
return $table;
}
private function createRow($columns)
{
return new Row(array(Row::COLUMNS => $columns));
}
}
\ No newline at end of file
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter