wrap sorting functions into their own class

This commit is contained in:
Andreas Gohr 2020-08-11 14:06:33 +02:00
parent 3649f74412
commit 2d85e84158
12 changed files with 196 additions and 225 deletions

View File

@ -1,5 +1,7 @@
<?php
use dokuwiki\Utf8\Sort;
/**
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
@ -7,19 +9,11 @@ class sort_with_collator_test extends DokuWikiTest {
private static $lang_before;
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
public function setUp() {
parent::setUp();
global $conf;
self::$lang_before = $conf['lang'];
$conf['lang'] = 'eo'; // Esperanto
lang_has_changed();
}
public static function tearDownAfterClass() {
global $conf;
$conf['lang'] = self::$lang_before;
lang_has_changed();
}
/**
@ -74,7 +68,7 @@ class sort_with_collator_test extends DokuWikiTest {
* @param $str2
*/
public function test_intl_strcmp($str1, $str2) {
$this->assertLessThan(0, intl_strcmp($str1, $str2));
$this->assertLessThan(0, Sort::strcmp($str1, $str2));
}
/**
@ -84,7 +78,7 @@ class sort_with_collator_test extends DokuWikiTest {
$sorted = explode(' ', $this->collation());
$random = explode(' ', $this->collation());
shuffle($random);
intl_sort($random);
Sort::sort($random);
$this->assertEquals(array_values($random), array_values($sorted));
}
@ -96,7 +90,7 @@ class sort_with_collator_test extends DokuWikiTest {
$random = explode(' ', $this->collation());
shuffle($random);
$random = array_flip($random);
intl_ksort($random);
Sort::ksort($random);
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -108,7 +102,7 @@ class sort_with_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asort($random);
Sort::asort($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -124,7 +118,7 @@ class sort_with_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -141,7 +135,7 @@ class sort_with_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -157,7 +151,7 @@ class sort_with_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}

View File

@ -1,5 +1,7 @@
<?php
use dokuwiki\Utf8\Sort;
/**
* Based on sort_with_collator.test.php.
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
@ -13,7 +15,7 @@ class sort_without_collator_test extends DokuWikiTest {
public function collation() {
// this would be the correct collation
// return 'a b c ĉ d e f g ĝ h ĥ i j ĵ k l m n o p r s ŝ t u ŭ v z';
// this collation is WRONG in practice!
// fallback sort doesn't recognize the Esperanto letters
return 'a b c d e f g h i j k l m n o p r s t u v z ĉ ĝ ĥ ĵ ŝ ŭ';
@ -70,7 +72,7 @@ class sort_without_collator_test extends DokuWikiTest {
* @param $str2
*/
public function test_intl_strcmp($str1, $str2) {
$this->assertLessThan(0, intl_strcmp($str1, $str2));
$this->assertLessThan(0, Sort::strcmp($str1, $str2));
}
/**
@ -80,7 +82,7 @@ class sort_without_collator_test extends DokuWikiTest {
$sorted = explode(' ', $this->collation());
$random = explode(' ', $this->collation());
shuffle($random);
intl_sort($random);
Sort::sort($random);
$this->assertEquals(array_values($random), array_values($sorted));
}
@ -92,7 +94,7 @@ class sort_without_collator_test extends DokuWikiTest {
$random = explode(' ', $this->collation());
shuffle($random);
$random = array_flip($random);
intl_ksort($random);
Sort::ksort($random);
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -104,7 +106,7 @@ class sort_without_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asort($random);
Sort::asort($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -120,7 +122,7 @@ class sort_without_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -137,7 +139,7 @@ class sort_without_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}
@ -153,7 +155,7 @@ class sort_without_collator_test extends DokuWikiTest {
$keys = array_keys($sorted);
shuffle($keys);
foreach($keys as $key) $random[$key] = $sorted[$key];
intl_asortFN($random);
Sort::asortFN($random);
$this->assertEquals(array_values($random), array_values($sorted));
$this->assertEquals(array_keys($random), array_keys($sorted));
}

View File

@ -2,6 +2,8 @@
namespace dokuwiki;
use dokuwiki\Utf8\Sort;
/**
* Manage all builtin AJAX calls
*
@ -98,7 +100,7 @@ class Ajax {
$data = array_map('trim', $data);
$data = array_map('noNS', $data);
$data = array_unique($data);
intl_sort($data);
Sort::sort($data);
/* now construct a json */
$suggestions = array(

View File

@ -6,6 +6,7 @@ use Doku_Renderer_xhtml;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Extension\Event;
use dokuwiki\Utf8\Sort;
define('DOKU_API_VERSION', 10);
@ -310,7 +311,7 @@ class ApiCore
$list = array();
$pages = idx_get_indexer()->getPages();
$pages = array_filter(array_filter($pages, 'isVisiblePage'), 'page_exists');
intl_ksort($pages);
Sort::ksort($pages);
foreach (array_keys($pages) as $idx) {
$perm = auth_quickaclcheck($pages[$idx]);

View File

@ -1,6 +1,8 @@
<?php
namespace dokuwiki\Ui;
use dokuwiki\Utf8\Sort;
/**
* Class Admin
*
@ -159,7 +161,7 @@ class Admin extends Ui {
* @return int
*/
protected function menuSort($a, $b) {
$strcmp = intl_strcmp($a['prompt'], $b['prompt']);
$strcmp = Sort::strcmp($a['prompt'], $b['prompt']);
if($strcmp != 0) return $strcmp;
if($a['sort'] === $b['sort']) return 0;
return ($a['sort'] < $b['sort']) ? -1 : 1;

View File

@ -4,6 +4,7 @@ namespace dokuwiki\Ui;
use dokuwiki\Extension\Event;
use dokuwiki\Form\Form;
use dokuwiki\Utf8\Sort;
class Search extends Ui
{
@ -384,7 +385,7 @@ class Search extends Ui
}
$namespaces[$subtopNS] += 1;
}
intl_ksort($namespaces);
Sort::ksort($namespaces);
arsort($namespaces);
return $namespaces;
}

154
inc/Utf8/Sort.php Normal file
View File

@ -0,0 +1,154 @@
<?php
namespace dokuwiki\Utf8;
/**
* DokuWiki sort functions
*
* When "intl" extension is available, all sorts are done using a collator.
* Otherwise, primitive PHP functions are called.
*
* The collator is created using the locale given in $conf['lang'].
* It always uses case insensitive "natural" ordering in its collation.
* The fallback solution uses the primitive PHP functions that return almost the same results
* when the input is text with only [A-Za-z0-9] characters.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
class Sort
{
/** @var \Collator[] language specific collators, usually only one */
protected static $collator;
/**
* Initialization of a collator using $conf['lang'] as the locale.
* The initialization is done only once, except when $reload is set to true.
* The collation takes "natural ordering" into account, that is, "page 2" is before "page 10".
*
* @param bool $reload Usually false; true forces collator re-creation
* @return \Collator Returns a configured collator or null if the collator cannot be created.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
protected static function getCollator($reload = false)
{
global $conf;
$lc = $conf['lang'];
// check if intl extension is available
if (!class_exists('\Collator')) {
return null;
}
// load collator if not available yet
if ($reload || !isset(self::$collator[$lc])) {
$collator = \Collator::create($lc);
if (!isset($collator)) return null;
$collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON);
self::$collator[$lc] = $collator;
}
return self::$collator[$lc];
}
/**
* Drop-in replacement for strcmp(), strcasecmp(), strnatcmp() and strnatcasecmp().
* It uses a collator-based comparison, or strnatcasecmp() as a fallback.
*
* @param string $str1 The first string.
* @param string $str2 The second string.
* @return int Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than $str2, and 0 if they are equal.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
public static function strcmp($str1, $str2)
{
$collator = self::getCollator();
if (isset($collator)) {
return $collator->compare($str1, $str2);
} else {
return strnatcasecmp($str1, $str2);
}
}
/**
* Drop-in replacement for sort().
* It uses a collator-based sort, or sort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
public static function sort(&$array)
{
$collator = self::getCollator();
if (isset($collator)) {
return $collator->sort($array);
} else {
return sort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for ksort().
* It uses a collator-based sort, or ksort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
public static function ksort(&$array)
{
$collator = self::getCollator();
if (isset($collator)) {
return uksort($array, array($collator, 'compare'));
} else {
return ksort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for asort(), natsort() and natcasesort().
* It uses a collator-based sort, or asort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
public static function asort(&$array)
{
$collator = self::getCollator();
if (isset($collator)) {
return $collator->asort($array);
} else {
return asort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for asort(), natsort() and natcasesort() when the parameter is an array of filenames.
* Filenames may not be equal to page names, depending on the setting in $conf['fnencode'],
* so the correct behavior is to sort page names and reflect this sorting in the filename array.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
public static function asortFN(&$array)
{
$collator = self::getCollator();
return uasort($array, function ($fn1, $fn2) use ($collator) {
if (isset($collator)) {
return $collator->compare(utf8_decodeFN($fn1), utf8_decodeFN($fn2));
} else {
return strnatcasecmp(utf8_decodeFN($fn1), utf8_decodeFN($fn2));
}
});
}
}

View File

@ -7,6 +7,7 @@
*/
use dokuwiki\Extension\Event;
use dokuwiki\Utf8\Sort;
/**
* create snippets for the first few results only
@ -181,7 +182,7 @@ function ft_backlinks($id, $ignore_perms = false){
}
}
intl_sort($result);
Sort::sort($result);
return $result;
}
@ -212,7 +213,7 @@ function ft_mediause($id, $ignore_perms = false){
}
}
intl_sort($result);
Sort::sort($result);
return $result;
}
@ -371,7 +372,7 @@ function ft_pagesorter($a, $b){
}elseif($ac > $bc){
return 1;
}
return intl_strcmp($a,$b);
return Sort::strcmp($a,$b);
}
/**

View File

@ -29,7 +29,6 @@ require_once(DOKU_INC.'inc/media.php');
require_once(DOKU_INC.'inc/pageutils.php');
require_once(DOKU_INC.'inc/parserutils.php');
require_once(DOKU_INC.'inc/search.php');
require_once(DOKU_INC.'inc/sort.php');
require_once(DOKU_INC.'inc/template.php');
require_once(DOKU_INC.'inc/toolbar.php');
require_once(DOKU_INC.'inc/utf8.php');

View File

@ -10,6 +10,7 @@ use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\HTTP\DokuHTTPClient;
use dokuwiki\Subscriptions\MediaSubscriptionSender;
use dokuwiki\Extension\Event;
use dokuwiki\Utf8\Sort;
/**
* Lists pages which currently use a media file selected for deletion
@ -1981,7 +1982,7 @@ function media_nstree($ns){
while ($data[$pos]['id'] != $tmp_ns) {
if (
$pos >= count($data) ||
($data[$pos]['level'] <= $level+1 && intl_strcmp($data[$pos]['id'], $tmp_ns) > 0)
($data[$pos]['level'] <= $level+1 && Sort::strcmp($data[$pos]['id'], $tmp_ns) > 0)
) {
array_splice($data, $pos, 0, array(array('level' => $level+1, 'id' => $tmp_ns, 'open' => 'true')));
break;

View File

@ -6,6 +6,8 @@
* @author Andreas Gohr <andi@splitbrain.org>
*/
use dokuwiki\Utf8\Sort;
/**
* Recurse directory
*
@ -49,9 +51,9 @@ function search(&$data,$base,$func,$opts,$dir='',$lvl=1,$sort='natural'){
if ($sort == 'date') {
@array_multisort(array_map('filemtime', $filepaths), SORT_NUMERIC, SORT_DESC, $files);
} else /* natural */ {
intl_asortFN($files);
Sort::asortFN($files);
}
intl_asortFN($dirs);
Sort::asortFN($dirs);
}
//give directories to userfunction then recurse
@ -368,7 +370,7 @@ function sort_search_fulltext($a,$b){
}elseif($a['count'] < $b['count']){
return 1;
}else{
return intl_strcmp($a['id'],$b['id']);
return Sort::strcmp($a['id'],$b['id']);
}
}

View File

@ -1,188 +0,0 @@
<?php
/**
* DokuWiki sort functions
*
* When "intl" extension is available, all sorts are done using a collator.
* Otherwise, primitive PHP functions are called.
*
* The collator is created using the locale given in $conf['lang'].
* It always uses case insensitive "natural" ordering in its collation.
* The fallback solution uses the primitive PHP functions that return almost the same results
* when the input is text with only [A-Za-z0-9] characters.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
/**
* Initialization of a collator using $conf['lang'] as the locale.
* The initialization is done only once, except when $reload is set to true.
* The collation takes "natural ordering" into account, that is, "page 2" is before "page 10".
*
* @param bool $reload Usually false; true forces collator re-creation after a change in $conf['lang']
* @return Collator Returns a configured collator or null if the collator cannot be created.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function _get_collator($reload = false)
{
global $conf;
static $intl_extension_available = null;
static $collator = null;
if (is_null($intl_extension_available)) {
$intl_extension_available = class_exists('Collator');
}
if (!$intl_extension_available) {
return null;
}
if ($reload || !isset($collator)) {
$collator = Collator::create($conf['lang']);
if (isset($collator)) {
$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
dbglog('Collator created with locale "' . $conf['lang'] . '": numeric collation on, ' .
'valid locale "' . $collator->getLocale(Locale::VALID_LOCALE) . '", ' .
'actual locale "' . $collator->getLocale(Locale::ACTUAL_LOCALE) . '"');
}
}
return $collator;
}
/**
* When $conf['lang'] is changed, this function should be called to re-create the collator
* using the new value.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function lang_has_changed()
{
_get_collator(true);
}
/**
* Drop-in replacement for strcmp(), strcasecmp(), strnatcmp() and strnatcasecmp().
* It uses a collator-based comparison, or strnatcasecmp() as a fallback.
*
* @param string $str1 The first string.
* @param string $str2 The second string.
* @return int Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than $str2, and 0 if they are equal.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function intl_strcmp($str1, $str2)
{
$collator = _get_collator();
if (isset($collator)) {
return $collator->compare($str1, $str2);
} else {
return strnatcasecmp($str1, $str2);
}
}
/**
* Drop-in replacement for sort().
* It uses a collator-based sort, or sort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function intl_sort(&$array)
{
$collator = _get_collator();
if (isset($collator)) {
return $collator->sort($array);
} else {
return sort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for ksort().
* It uses a collator-based sort, or ksort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function intl_ksort(&$array)
{
$collator = _get_collator();
if (isset($collator)) {
return uksort($array, array($collator, 'compare'));
} else {
return ksort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for asort(), natsort() and natcasesort().
* It uses a collator-based sort, or asort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function intl_asort(&$array)
{
$collator = _get_collator();
if (isset($collator)) {
return $collator->asort($array);
} else {
return asort($array, SORT_NATURAL | SORT_FLAG_CASE);
}
}
/**
* Drop-in replacement for asort(), natsort() and natcasesort() when the parameter is an array of filenames.
* Filenames may not be equal to page names, depending on the setting in $conf['fnencode'],
* so the correct behavior is to sort page names and reflect this sorting in the filename array.
*
* @param array $array The input array.
* @return bool Returns true on success or false on failure.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function intl_asortFN(&$array)
{
$collator = _get_collator();
if (isset($collator)) {
return uasort($array, '_sort_filenames_with_collator');
} else {
return uasort($array, '_sort_filenames_without_collator');
}
}
/**
* Collator-based string comparison for filenames.
* The filenames are converted to page names with utf8_decodeFN() before the comparison.
*
* @param string $fn1 The first filename.
* @param string $fn2 The second filename.
* @return int Returns < 0 if $fn1 is less than $fn2; > 0 if $fn1 is greater than $fn2, and 0 if they are equal.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function _sort_filenames_with_collator($fn1, $fn2)
{
$collator = _get_collator();
return $collator->compare(utf8_decodeFN($fn1), utf8_decodeFN($fn2));
}
/**
* Fallback string comparison for filenames, using strnatcasecmp().
* The filenames are converted to page names with utf8_decodeFN() before the comparison.
*
* @param string $fn1 The first filename.
* @param string $fn2 The second filename.
* @return int Returns < 0 if $fn1 is less than $fn2; > 0 if $fn1 is greater than $fn2, and 0 if they are equal.
*
* @author Moisés Braga Ribeiro <moisesbr@gmail.com>
*/
function _sort_filenames_without_collator($fn1, $fn2)
{
return strnatcasecmp(utf8_decodeFN($fn1), utf8_decodeFN($fn2));
}