From 212826853b6e15f9ac839182e8cd81dc8f9cd87a Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sun, 10 Jun 2012 16:28:31 +0200 Subject: [PATCH] MDL-33635 improve collatorlib 1/ the sort flag can not use Intl extension constants because they may not be available 2/ add sort flag to all methods 3/ use private constructor instead of abstract - more Java like API to match the textlib 4/ add natural sorting support 5/ consistent bool return type 6/ better non-intl fallback 7/ more tests --- lib/tests/textlib_test.php | 90 +++++++++++---- lib/textlib.class.php | 281 +++++++++++++++++++++++---------------------- 2 files changed, 211 insertions(+), 160 deletions(-) diff --git a/lib/tests/textlib_test.php b/lib/tests/textlib_test.php index 68f45139c02..4bf7bf26371 100644 --- a/lib/tests/textlib_test.php +++ b/lib/tests/textlib_test.php @@ -424,14 +424,58 @@ class collatorlib_testcase extends basic_testcase { */ public function test_asort() { $arr = array('b' => 'ab', 1 => 'aa', 0 => 'cc'); - collatorlib::asort($arr); + $result = collatorlib::asort($arr); + $this->assertSame(array_values($arr), array('aa', 'ab', 'cc')); $this->assertSame(array_keys($arr), array(1, 'b', 0)); + $this->assertTrue($result); + + $arr = array('b' => 'ab', 1 => 'aa', 0 => 'cc'); + $result = collatorlib::asort($arr, collatorlib::SORT_STRING); $this->assertSame(array_values($arr), array('aa', 'ab', 'cc')); + $this->assertSame(array_keys($arr), array(1, 'b', 0)); + $this->assertTrue($result); - $arr = array('a' => 'áb', 'b' => 'ab', 1 => 'aa', 0=>'cc'); - collatorlib::asort($arr); - $this->assertSame(array_keys($arr), array(1, 'b', 'a', 0), $this->error); - $this->assertSame(array_values($arr), array('aa', 'ab', 'áb', 'cc'), $this->error); + $arr = array('b' => 'aac', 1 => 'Aac', 0 => 'cc'); + $result = collatorlib::asort($arr, (collatorlib::SORT_STRING | collatorlib::CASE_SENSITIVE)); + $this->assertSame(array_values($arr), array('Aac', 'aac', 'cc')); + $this->assertSame(array_keys($arr), array(1, 'b', 0)); + $this->assertTrue($result); + + $arr = array('b' => 'a1', 1 => 'a10', 0 => 'a3b'); + $result = collatorlib::asort($arr); + $this->assertSame(array_values($arr), array('a1', 'a10', 'a3b')); + $this->assertSame(array_keys($arr), array('b', 1, 0)); + $this->assertTrue($result); + + $arr = array('b' => 'a1', 1 => 'a10', 0 => 'a3b'); + $result = collatorlib::asort($arr, collatorlib::SORT_NATURAL); + $this->assertSame(array_values($arr), array('a1', 'a3b', 'a10')); + $this->assertSame(array_keys($arr), array('b', 0, 1)); + $this->assertTrue($result); + + $arr = array('b' => '1.1.1', 1 => '1.2', 0 => '1.20.2'); + $result = collatorlib::asort($arr, collatorlib::SORT_NATURAL); + $this->assertSame(array_values($arr), array('1.1.1', '1.2', '1.20.2')); + $this->assertSame(array_keys($arr), array('b', 1, 0)); + $this->assertTrue($result); + + $arr = array('b' => '-1', 1 => 1000, 0 => -1.2, 3 => 1, 4 => false); + $result = collatorlib::asort($arr, collatorlib::SORT_NUMERIC); + $this->assertSame(array_values($arr), array(-1.2, '-1', false, 1, 1000)); + $this->assertSame(array_keys($arr), array(0, 'b', 4, 3, 1)); + $this->assertTrue($result); + + $arr = array('b' => array(1), 1 => array(2, 3), 0 => 1); + $result = collatorlib::asort($arr, collatorlib::SORT_REGULAR); + $this->assertSame(array_values($arr), array(1, array(1), array(2, 3))); + $this->assertSame(array_keys($arr), array(0, 'b', 1)); + $this->assertTrue($result); + + $arr = array('a' => 'áb', 'b' => 'ab', 1 => 'aa', 0=>'cc', 'x' => 'Áb',); + $result = collatorlib::asort($arr); + $this->assertSame(array_values($arr), array('aa', 'ab', 'áb', 'Áb', 'cc'), $this->error); + $this->assertSame(array_keys($arr), array(1, 'b', 'a', 'x', 0), $this->error); + $this->assertTrue($result); } /** @@ -444,44 +488,46 @@ class collatorlib_testcase extends basic_testcase { 1 => new string_test_class('aa'), 0 => new string_test_class('cc') ); - collatorlib::asort_objects_by_method($objects, 'get_protected_name'); + $result = collatorlib::asort_objects_by_method($objects, 'get_protected_name'); $this->assertSame(array_keys($objects), array(1, 'b', 0)); $this->assertSame($this->get_ordered_names($objects, 'get_protected_name'), array('aa', 'ab', 'cc')); + $this->assertTrue($result); $objects = array( - 'a' => new string_test_class('áb'), - 'b' => new string_test_class('ab'), - 1 => new string_test_class('aa'), - 0 => new string_test_class('cc') + 'b' => new string_test_class('a20'), + 1 => new string_test_class('a1'), + 0 => new string_test_class('a100') ); - collatorlib::asort_objects_by_method($objects, 'get_private_name'); - $this->assertSame(array_keys($objects), array(1, 'b', 'a', 0), $this->error); - $this->assertSame($this->get_ordered_names($objects, 'get_private_name'), array('aa', 'ab', 'áb', 'cc'), $this->error); + $result = collatorlib::asort_objects_by_method($objects, 'get_protected_name', collatorlib::SORT_NATURAL); + $this->assertSame(array_keys($objects), array(1, 'b', 0)); + $this->assertSame($this->get_ordered_names($objects, 'get_protected_name'), array('a1', 'a20', 'a100')); + $this->assertTrue($result); } /** * Tests the static asort_objects_by_method method * @return void */ - public function asort_objects_by_property() { + public function test_asort_objects_by_property() { $objects = array( 'b' => new string_test_class('ab'), 1 => new string_test_class('aa'), 0 => new string_test_class('cc') ); - collatorlib::asort_objects_by_property($objects, 'publicname'); + $result = collatorlib::asort_objects_by_property($objects, 'publicname'); $this->assertSame(array_keys($objects), array(1, 'b', 0)); $this->assertSame($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'cc')); + $this->assertTrue($result); $objects = array( - 'a' => new string_test_class('áb'), - 'b' => new string_test_class('ab'), - 1 => new string_test_class('aa'), - 0 => new string_test_class('cc') + 'b' => new string_test_class('a20'), + 1 => new string_test_class('a1'), + 0 => new string_test_class('a100') ); - collatorlib::asort_objects_by_property($objects, 'publicname'); - $this->assertSame(array_keys($objects), array(1, 'b', 'a', 0), $this->error); - $this->assertSame($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'áb', 'cc'), $this->error); + $result = collatorlib::asort_objects_by_property($objects, 'publicname', collatorlib::SORT_NATURAL); + $this->assertSame(array_keys($objects), array(1, 'b', 0)); + $this->assertSame($this->get_ordered_names($objects, 'publicname'), array('a1', 'a20', 'a100')); + $this->assertTrue($result); } /** diff --git a/lib/textlib.class.php b/lib/textlib.class.php index 91cf609fffb..6b7d7b567f5 100644 --- a/lib/textlib.class.php +++ b/lib/textlib.class.php @@ -606,14 +606,30 @@ class textlib { } } + /** * A collator class with static methods that can be used for sorting. * * @package core * @copyright 2011 Sam Hemelryk + * 2012 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class collatorlib { +class collatorlib { + /** @const compare items as strings, equivalent to Collator::SORT_REGULAR */ + const SORT_REGULAR = 0; + + /** @const compare items as strings, equivalent to Collator::SORT_STRING */ + const SORT_STRING = 1; + + /** @const compare items as numbers, equivalent to Collator::SORT_NUMERIC */ + const SORT_NUMERIC = 2; + + /** @const compare items like natsort(), equivalent to SORT_NATURAL */ + const SORT_NATURAL = 6; + + /** @const do not ignore case when sorting, use bitwise "|" with SORT_NATURAL or SORT_STRING, equivalent to Collator::UPPER_FIRST */ + const CASE_SENSITIVE = 64; /** @var Collator|false|null **/ protected static $collator = null; @@ -622,13 +638,17 @@ abstract class collatorlib { protected static $locale = null; /** + * Prevent class instances, all methods are static. + */ + private function __construct() { + } + + /** * Ensures that a collator is available and created * * @return bool Returns true if collation is available and ready */ protected static function ensure_collator_available() { - global $CFG; - $locale = get_string('locale', 'langconfig'); if (is_null(self::$collator) || $locale != self::$locale) { self::$collator = false; @@ -665,7 +685,7 @@ abstract class collatorlib { // to find the correct locale or to use UCA collation } } else { - // We've recieved some other sort of non fatal warning - let the + // We've received some other sort of non fatal warning - let the // user know about it via debugging. debugging('Problem with locale: "' . $locale . '", with message "' . $errormessage . '", falling back to "' . $collator->getLocale(Locale::VALID_LOCALE) . '"'); @@ -684,171 +704,156 @@ abstract class collatorlib { } /** - * Locale aware sorting, the key associations are kept, values are sorted alphabetically. - * - * @param array $arr array to be sorted (reference) - * @param int $sortflag One of Collator::SORT_REGULAR, Collator::SORT_NUMERIC, Collator::SORT_STRING - * @return void modifies parameter + * Restore array contents keeping new keys. + * @static + * @param array $arr + * @param array $original + * @return void modifies $arr */ - public static function asort(array &$arr, $sortflag = null) { - if (self::ensure_collator_available()) { - if (!isset($sortflag)) { - $sortflag = Collator::SORT_REGULAR; - } - self::$collator->asort($arr, $sortflag); - return; + protected static function restore_array(array &$arr, array &$original) { + foreach ($arr as $key => $ignored) { + $arr[$key] = $original[$key]; } - asort($arr, SORT_LOCALE_STRING); } /** - * Locale aware comparison of two strings. - * - * Returns: - * 1 if str1 is greater than str2 - * 0 if str1 is equal to str2 - * -1 if str1 is less than str2 - * - * @param string $str1 first string to compare - * @param string $str2 second string to compare - * @return int + * Normalise numbers in strings for natural sorting comparisons. + * @static + * @param string $string + * @return string string with normalised numbers */ - public static function compare($str1, $str2) { - if (self::ensure_collator_available()) { - return self::$collator->compare($str1, $str2); - } - return strcmp($str1, $str2); + protected static function naturalise($string) { + return preg_replace_callback('/[0-9]+/', array('collatorlib', 'callback_naturalise'), $string); } /** - * Locale aware sort of objects by a property in common to all objects - * - * @param array $objects An array of objects to sort (handled by reference) - * @param string $property The property to use for comparison - * @return bool True on success + * @internal + * @static + * @param array $matches + * @return string */ - public static function asort_objects_by_property(array &$objects, $property) { - $comparison = new collatorlib_property_comparison($property); - return uasort($objects, array($comparison, 'compare')); + public static function callback_naturalise($matches) { + return str_pad($matches[0], 20, '0', STR_PAD_LEFT); } /** - * Locale aware sort of objects by a method in common to all objects + * Locale aware sorting, the key associations are kept, values are sorted alphabetically. * - * @param array $objects An array of objects to sort (handled by reference) - * @param string $method The method to call to generate a value for comparison + * @param array $arr array to be sorted (reference) + * @param int $sortflag One of collatorlib::SORT_NUMERIC, collatorlib::SORT_STRING, collatorlib::SORT_NATURAL, collatorlib::SORT_REGULAR + * optionally "|" collatorlib::CASE_SENSITIVE * @return bool True on success */ - public static function asort_objects_by_method(array &$objects, $method) { - $comparison = new collatorlib_method_comparison($method); - return uasort($objects, array($comparison, 'compare')); - } -} + public static function asort(array &$arr, $sortflag = collatorlib::SORT_STRING) { + if (empty($arr)) { + // nothing to do + return true; + } -/** - * Object comparison using collator - * - * Abstract class to aid the sorting of objects with respect to proper language - * comparison using collator - * - * @package core - * @copyright 2011 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class collatorlib_comparison { - /** - * This function will perform the actual comparison of values - * It must be overridden by the deriving class. - * - * Returns: - * 1 if str1 is greater than str2 - * 0 if str1 is equal to str2 - * -1 if str1 is less than str2 - * - * @param mixed $a The first something to compare - * @param mixed $b The second something to compare - * @return int - */ - public abstract function compare($a, $b); -} + $original = null; -/** - * Compare properties of two objects - * - * A comparison helper for comparing properties of two objects - * - * @package core - * @category string - * @copyright 2011 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class collatorlib_property_comparison extends collatorlib_comparison { + $casesensitive = (bool)($sortflag & collatorlib::CASE_SENSITIVE); + $sortflag = ($sortflag & ~collatorlib::CASE_SENSITIVE); + if ($sortflag != collatorlib::SORT_NATURAL and $sortflag != collatorlib::SORT_STRING) { + $casesensitive = false; + } - /** @var string The property to sort by **/ - protected $property; + if (self::ensure_collator_available()) { + if ($sortflag == collatorlib::SORT_NUMERIC) { + $flag = Collator::SORT_NUMERIC; - /** - * Constructor - * - * @param string $property - */ - public function __construct($property) { - $this->property = $property; - } + } else if ($sortflag == collatorlib::SORT_REGULAR) { + $flag = Collator::SORT_REGULAR; - /** - * Returns: - * 1 if str1 is greater than str2 - * 0 if str1 is equal to str2 - * -1 if str1 is less than str2 - * - * @param mixed $obja The first object to compare - * @param mixed $objb The second object to compare - * @return int - */ - public function compare($obja, $objb) { - $resulta = $obja->{$this->property}; - $resultb = $objb->{$this->property}; - return collatorlib::compare($resulta, $resultb); - } -} + } else { + $flag = Collator::SORT_STRING; + } -/** - * Compare method of two objects - * - * A comparison helper for comparing the result of a method on two objects - * - * @package core - * @copyright 2011 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class collatorlib_method_comparison extends collatorlib_comparison { + if ($sortflag == collatorlib::SORT_NATURAL) { + $original = $arr; + if ($sortflag == collatorlib::SORT_NATURAL) { + foreach ($arr as $key => $value) { + $arr[$key] = self::naturalise((string)$value); + } + } + } + if ($casesensitive) { + self::$collator->setAttribute(Collator::CASE_FIRST, Collator::UPPER_FIRST); + } else { + self::$collator->setAttribute(Collator::CASE_FIRST, Collator::OFF); + } + $result = self::$collator->asort($arr, $flag); + if ($original) { + self::restore_array($arr, $original); + } + return $result; + } + + // try some fallback that works at least for English + + if ($sortflag == collatorlib::SORT_NUMERIC) { + return asort($arr, SORT_NUMERIC); + + } else if ($sortflag == collatorlib::SORT_REGULAR) { + return asort($arr, SORT_REGULAR); + } + + if (!$casesensitive) { + $original = $arr; + foreach ($arr as $key => $value) { + $arr[$key] = textlib::strtolower($value); + } + } + + if ($sortflag == collatorlib::SORT_NATURAL) { + $result = natsort($arr); + + } else { + $result = asort($arr, SORT_LOCALE_STRING); + } + + if ($original) { + self::restore_array($arr, $original); + } - /** @var string The method to use for comparison **/ - protected $method; + return $result; + } /** - * Constructor + * Locale aware sort of objects by a property in common to all objects * - * @param string $method The method to call against each object + * @param array $objects An array of objects to sort (handled by reference) + * @param string $property The property to use for comparison + * @param int $sortflag One of collatorlib::SORT_NUMERIC, collatorlib::SORT_STRING, collatorlib::SORT_NATURAL, collatorlib::SORT_REGULAR + * optionally "|" collatorlib::CASE_SENSITIVE + * @return bool True on success */ - public function __construct($method) { - $this->method = $method; + public static function asort_objects_by_property(array &$objects, $property, $sortflag = collatorlib::SORT_STRING) { + $original = $objects; + foreach ($objects as $key => $object) { + $objects[$key] = $object->$property; + } + $result = self::asort($objects, $sortflag); + self::restore_array($objects, $original); + return $result; } /** - * Returns: - * 1 if str1 is greater than str2 - * 0 if str1 is equal to str2 - * -1 if str1 is less than str2 + * Locale aware sort of objects by a method in common to all objects * - * @param mixed $obja The first object to compare - * @param mixed $objb The second object to compare - * @return int + * @param array $objects An array of objects to sort (handled by reference) + * @param string $method The method to call to generate a value for comparison + * @param int $sortflag One of collatorlib::SORT_NUMERIC, collatorlib::SORT_STRING, collatorlib::SORT_NATURAL, collatorlib::SORT_REGULAR + * optionally "|" collatorlib::CASE_SENSITIVE + * @return bool True on success */ - public function compare($obja, $objb) { - $resulta = $obja->{$this->method}(); - $resultb = $objb->{$this->method}(); - return collatorlib::compare($resulta, $resultb); + public static function asort_objects_by_method(array &$objects, $method, $sortflag = collatorlib::SORT_STRING) { + $original = $objects; + foreach ($objects as $key => $object) { + $objects[$key] = $object->{$method}(); + } + $result = self::asort($objects, $sortflag); + self::restore_array($objects, $original); + return $result; } } -- 2.11.4.GIT