From d609d962619a9477f31d62f1ec227c439ed8edd2 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Wed, 10 Aug 2011 12:56:04 +0800 Subject: [PATCH] MDL-28599 textlib Separated collator to collatorlib with static methods, and added support for locale aware sorting of objects --- admin/blocks.php | 2 +- admin/localplugins.php | 2 +- admin/registration/forms.php | 2 +- .../block_activity_modules.php | 2 +- blocks/community/forms.php | 2 +- course/edit_form.php | 4 +- course/editcategory_form.php | 4 +- course/lib.php | 4 +- course/publish/forms.php | 2 +- lib/blocklib.php | 2 +- lib/filterlib.php | 2 +- lib/gradelib.php | 2 +- lib/moodlelib.php | 10 +- lib/outputlib.php | 9 + lib/questionlib.php | 2 +- lib/simpletest/testtextlib.php | 148 +++++++++-- lib/textlib.class.php | 243 +++++++++++++++++- question/engine/bank.php | 2 +- user/editlib.php | 2 +- 19 files changed, 394 insertions(+), 52 deletions(-) diff --git a/admin/blocks.php b/admin/blocks.php index 55170d28dfa..8f4fdc904b2 100644 --- a/admin/blocks.php +++ b/admin/blocks.php @@ -250,7 +250,7 @@ $tablerows[] = array(strip_tags($strblockname), $row); // first element will be used for sorting } - textlib_get_instance()->asort($tablerows); + collatorlib::asort($tablerows); foreach ($tablerows as $row) { $table->add_data($row[1]); } diff --git a/admin/localplugins.php b/admin/localplugins.php index ea95388bd64..674138b438c 100644 --- a/admin/localplugins.php +++ b/admin/localplugins.php @@ -89,7 +89,7 @@ foreach (get_plugin_list('local') as $plugin => $plugindir) { } $plugins[$plugin] = $strpluginname; } -textlib_get_instance()->asort($plugins); +collatorlib::asort($plugins); foreach ($plugins as $plugin => $name) { $delete = new moodle_url($PAGE->url, array('delete' => $plugin, 'sesskey' => sesskey())); diff --git a/admin/registration/forms.php b/admin/registration/forms.php index 23634f59b30..30ec47c26bc 100644 --- a/admin/registration/forms.php +++ b/admin/registration/forms.php @@ -283,7 +283,7 @@ class site_registration_form extends moodleform { $mform->addHelpButton('urlstring', 'siteurl', 'hub'); $languages = get_string_manager()->get_list_of_languages(); - textlib_get_instance()->asort($languages); + collatorlib::asort($languages); $mform->addElement('select', 'language', get_string('sitelang', 'hub'), $languages); $mform->setType('language', PARAM_ALPHANUMEXT); diff --git a/blocks/activity_modules/block_activity_modules.php b/blocks/activity_modules/block_activity_modules.php index c44ceb40f9b..c290b2444f4 100644 --- a/blocks/activity_modules/block_activity_modules.php +++ b/blocks/activity_modules/block_activity_modules.php @@ -46,7 +46,7 @@ class block_activity_modules extends block_list { } } - textlib_get_instance()->asort($modfullnames); + collatorlib::asort($modfullnames); foreach ($modfullnames as $modname => $modfullname) { if ($modname === 'resources') { diff --git a/blocks/community/forms.php b/blocks/community/forms.php index 141bf663165..fafab8a6035 100644 --- a/blocks/community/forms.php +++ b/blocks/community/forms.php @@ -259,7 +259,7 @@ class community_hub_search_form extends moodleform { $mform->setDefault('licence', $licence); $languages = get_string_manager()->get_list_of_languages(); - textlib_get_instance()->asort($languages); + collatorlib::asort($languages); $languages = array_merge(array('all' => get_string('any')), $languages); $mform->addElement('select', 'language', get_string('language'), $languages); $mform->setDefault('language', $language); diff --git a/course/edit_form.php b/course/edit_form.php index 888e0c3274c..a5bad505cb8 100644 --- a/course/edit_form.php +++ b/course/edit_form.php @@ -176,7 +176,9 @@ class course_edit_form extends moodleform { $themes=array(); $themes[''] = get_string('forceno'); foreach ($themeobjects as $key=>$theme) { - $themes[$key] = $theme->name; + if (empty($theme->hidefromselector)) { + $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); + } } $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); } diff --git a/course/editcategory_form.php b/course/editcategory_form.php index 63181468408..a708a021496 100644 --- a/course/editcategory_form.php +++ b/course/editcategory_form.php @@ -41,7 +41,9 @@ class editcategory_form extends moodleform { $themes = array(''=>get_string('forceno')); $allthemes = get_list_of_themes(); foreach ($allthemes as $key=>$theme) { - $themes[$key] = $theme->name; + if (empty($theme->hidefromselector)) { + $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); + } } $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); } diff --git a/course/lib.php b/course/lib.php index a5628871397..8cace618ad7 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1206,7 +1206,7 @@ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modname $modnamesplural[$mod->name] = get_string("modulenameplural", "$mod->name"); } } - textlib_get_instance()->asort($modnames); + collatorlib::asort($modnames); } else { print_error("nomodules", 'debug'); } @@ -1231,7 +1231,7 @@ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modname $modnamesused[$mod->modname] = $modnames[$mod->modname]; } if ($modnamesused) { - textlib_get_instance()->asort($modnamesused); + collatorlib::asort($modnamesused); } } } diff --git a/course/publish/forms.php b/course/publish/forms.php index 0ee89998e1b..84a9852f01d 100644 --- a/course/publish/forms.php +++ b/course/publish/forms.php @@ -239,7 +239,7 @@ class course_publication_form extends moodleform { $mform->addHelpButton('description', 'description', 'hub'); $languages = get_string_manager()->get_list_of_languages(); - textlib_get_instance()->asort($languages); + collatorlib::asort($languages); $mform->addElement('select', 'language', get_string('language'), $languages); $mform->setDefault('language', $defaultlanguage); $mform->addHelpButton('language', 'language', 'hub'); diff --git a/lib/blocklib.php b/lib/blocklib.php index 86fd35decd3..5586b3cb2a8 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1754,7 +1754,7 @@ function block_add_block_ui($page, $output) { $menu[$block->name] = $blockobject->get_title(); } } - textlib_get_instance()->asort($menu); + collatorlib::asort($menu); $actionurl = new moodle_url($page->url, array('sesskey'=>sesskey())); $select = new single_select($actionurl, 'bui_addblock', $menu, null, array(''=>get_string('adddots')), 'add_block'); diff --git a/lib/filterlib.php b/lib/filterlib.php index 0b77f6d3548..125f43a0059 100644 --- a/lib/filterlib.php +++ b/lib/filterlib.php @@ -519,7 +519,7 @@ function filter_get_all_installed() { } } } - textlib_get_instance()->asort($filternames); + collatorlib::asort($filternames); return $filternames; } diff --git a/lib/gradelib.php b/lib/gradelib.php index c9793f1c71e..b78fde9eda4 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -798,7 +798,7 @@ function grade_get_categories_menu($courseid, $includenew=false) { foreach ($categories as $category) { $cats[$category->id] = $category->get_name(); } - textlib_get_instance()->asort($cats); + collatorlib::asort($cats); return ($result+$cats); } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 51abbdc333f..e7dee692911 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -6143,7 +6143,7 @@ class core_string_manager implements string_manager { } $countries = $this->load_component_strings('core_countries', $lang); - textlib_get_instance()->asort($countries); + collatorlib::asort($countries); if (!$returnall and !empty($CFG->allcountrycodes)) { $enabled = explode(',', $CFG->allcountrycodes); $return = array(); @@ -6315,12 +6315,12 @@ class core_string_manager implements string_manager { if (!empty($CFG->langcache) and !empty($this->menucache)) { // cache the list so that it can be used next time - textlib_get_instance()->asort($languages); + collatorlib::asort($languages); file_put_contents($this->menucache, json_encode($languages)); } } - textlib_get_instance()->asort($languages); + collatorlib::asort($languages); return $languages; } @@ -6739,7 +6739,6 @@ function get_list_of_charsets() { /** * Returns a list of valid and compatible themes * - * @global object * @return array */ function get_list_of_themes() { @@ -6757,7 +6756,8 @@ function get_list_of_themes() { $theme = theme_config::load($themename); $themes[$themename] = $theme; } - asort($themes); + + collatorlib::asort_objects_by_method($themes, 'get_theme_name'); return $themes; } diff --git a/lib/outputlib.php b/lib/outputlib.php index 304b7443822..8712d5ac903 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -1179,6 +1179,15 @@ class theme_config { } return $regions; } + + /** + * Returns the human readable name of the theme + * + * @return string + */ + public function get_theme_name() { + return get_string('pluginname', 'theme_'.$this->name); + } } diff --git a/lib/questionlib.php b/lib/questionlib.php index 446fa95783b..f0a44360f80 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1228,7 +1228,7 @@ function get_import_export_formats($type) { } } - textlib_get_instance()->asort($fileformatnames); + collatorlib::asort($fileformatnames); return $fileformatnames; } diff --git a/lib/simpletest/testtextlib.php b/lib/simpletest/testtextlib.php index e093db38054..09f1458f8f8 100644 --- a/lib/simpletest/testtextlib.php +++ b/lib/simpletest/testtextlib.php @@ -272,28 +272,6 @@ class textlib_test extends UnitTestCase { $this->assertIdentical(textlib::strtotitle($str), "Žluťoučký Koníček"); } - public function test_asort() { - global $SESSION; - $SESSION->lang = 'en'; // make sure we test en language to get consistent results, hopefully all systems have this locale - - $arr = array('b'=>'ab', 1=>'aa', 0=>'cc'); - textlib::asort($arr); - $this->assertIdentical(array_keys($arr), array(1, 'b', 0)); - $this->assertIdentical(array_values($arr), array('aa', 'ab', 'cc')); - - if (extension_loaded('intl')) { - $error = 'Collation aware sorting not supported'; - } else { - $error = 'Collation aware sorting not supported, PHP extension "intl" is not available.'; - } - - $arr = array('a'=>'áb', 'b'=>'ab', 1=>'aa', 0=>'cc'); - textlib::asort($arr); - $this->assertIdentical(array_keys($arr), array(1, 'b', 'a', 0), $error); - - unset($SESSION->lang); - } - public function test_deprecated_textlib_get_instance() { $textlib = textlib_get_instance(); $this->assertIdentical($textlib->substr('abc', 1, 1), 'b'); @@ -307,3 +285,129 @@ class textlib_test extends UnitTestCase { $this->assertIdentical($textlib->strtotitle('abc ABC'), 'Abc Abc'); } } + +/** + * Unit tests for our utf-8 aware collator. + * + * Used for sorting. + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collatorlib_test extends UnitTestCase { + + protected $initiallang = null; + protected $error = null; + + public function setUp() { + global $SESSION; + if (isset($SESSION->lang)) { + $this->initiallang = $SESSION->lang; + } + $SESSION->lang = 'en'; // make sure we test en language to get consistent results, hopefully all systems have this locale + if (extension_loaded('intl')) { + $this->error = 'Collation aware sorting not supported'; + } else { + $this->error = 'Collation aware sorting not supported, PHP extension "intl" is not available.'; + } + parent::setUp(); + } + public function tearDown() { + global $SESSION; + parent::tearDown(); + if ($this->initiallang !== null) { + $SESSION->lang = $this->initiallang; + $this->initiallang = null; + } else { + unset($SESSION->lang); + } + } + function test_asort() { + $arr = array('b' => 'ab', 1 => 'aa', 0 => 'cc'); + collatorlib::asort($arr); + $this->assertIdentical(array_keys($arr), array(1, 'b', 0)); + $this->assertIdentical(array_values($arr), array('aa', 'ab', 'cc')); + + $arr = array('a' => 'áb', 'b' => 'ab', 1 => 'aa', 0=>'cc'); + collatorlib::asort($arr); + $this->assertIdentical(array_keys($arr), array(1, 'b', 'a', 0), $this->error); + $this->assertIdentical(array_values($arr), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + function test_asort_objects_by_method() { + $objects = array( + 'b' => new string_test_class('ab'), + 1 => new string_test_class('aa'), + 0 => new string_test_class('cc') + ); + collatorlib::asort_objects_by_method($objects, 'get_protected_name'); + $this->assertIdentical(array_keys($objects), array(1, 'b', 0)); + $this->assertIdentical($this->get_ordered_names($objects, 'get_protected_name'), array('aa', 'ab', 'cc')); + + $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') + ); + collatorlib::asort_objects_by_method($objects, 'get_private_name'); + $this->assertIdentical(array_keys($objects), array(1, 'b', 'a', 0), $this->error); + $this->assertIdentical($this->get_ordered_names($objects, 'get_private_name'), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + 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'); + $this->assertIdentical(array_keys($objects), array(1, 'b', 0)); + $this->assertIdentical($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'cc')); + + $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') + ); + collatorlib::asort_objects_by_property($objects, 'publicname'); + $this->assertIdentical(array_keys($objects), array(1, 'b', 'a', 0), $this->error); + $this->assertIdentical($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + protected function get_ordered_names($objects, $methodproperty = 'get_protected_name') { + $return = array(); + foreach ($objects as $object) { + if ($methodproperty == 'publicname') { + $return[] = $object->publicname; + } else { + $return[] = $object->$methodproperty(); + } + } + return $return; + } +} +/** + * Simple class used to work with the unit test. + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class string_test_class extends stdClass { + public $publicname; + protected $protectedname; + private $privatename; + public function __construct($name) { + $this->publicname = $name; + $this->protectedname = $name; + $this->privatename = $name; + } + public function get_protected_name() { + return $this->protectedname; + } + public function get_private_name() { + return $this->publicname; + } +} \ No newline at end of file diff --git a/lib/textlib.class.php b/lib/textlib.class.php index 4dff46b5038..25eb53bb967 100644 --- a/lib/textlib.class.php +++ b/lib/textlib.class.php @@ -550,18 +550,243 @@ class textlib { /** * Locale aware sorting, the key associations are kept, values are sorted alphabetically. * - * Note: this function is using current moodle locale. - * - * @param array $arr array to be sorted - * @return void, modifies parameter + * @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 */ - public static function asort(array &$arr) { - if (function_exists('collator_asort')) { - if ($coll = collator_create(get_string('locale', 'langconfig'))) { - collator_asort($coll, $arr); - return; + public static function asort(array &$arr, $sortflag = null) { + debugging('textlib::asort has been superseeded by collatorlib::asort please upgrade your code to use that', DEBUG_DEVELOPER); + collatorlib::asort($arr, $sortflag); + } +} + +/** + * A collator class with static methods that can be used for sorting. + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class collatorlib { + + /** @var Collator|false|null **/ + protected static $collator = null; + + /** @var string|null The locale that was used in instantiating the current collator **/ + protected static $locale = null; + + /** + * 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; + self::$locale = $locale; + if (class_exists('Collator', false)) { + $collator = new Collator($locale); + if (!empty($collator) && $collator instanceof Collator) { + // Check for non fatal error messages. This has to be done immediately + // after instantiation as any futher calls to collation will cause + // it to reset to 0 again (or another error code if one occured) + $errorcode = $collator->getErrorCode(); + // Check for an error code, 0 means no error occured + if ($errorcode !== 0) { + // Get the actual locale being used, e.g. en, he, zh + $localeinuse = $collator->getLocale(Locale::ACTUAL_LOCALE); + // Check for the common fallback wardning error code. If this occured + // there is normally little to worry about. (U_USING_FALLBACK_WARNING) + if ($errorcode === -128) { + // Check if the local in use is anything like the locale we asked for + if (strpos($locale, $localeinuse) !== 0) { + // The locale we asked for is completely different to the locale + // we have recieved, let the user know via debugging + debugging('Invalid locale, falling back to the system default locale "'.$collator->getLocale(Locale::VALID_LOCALE).'"'); + } else { + // Nothing to do here, this is expected! + // The Moodle locale setting isn't what the collator expected but + // it is smart enough to match the first characters of our locale + // to find the correct locale + // debugging('Invalid locale, falling back to closest match "'.$localeinuse.'" which may not be the exact locale'); + } + } else { + // We've recieved some other sort of non fatal warning - let the + // user know about it via debugging. + debugging('Locale collator generated warnings (not fatal) "'.$collator->getErrorMessage().'" falling back to '.$collator->getLocale(Locale::VALID_LOCALE)); + } + } + // Store the collator object now that we can be sure it is in a workable condition. + self::$collator = $collator; + } else { + // Fatal error while trying to instantiate the collator... who know what went wrong. + debugging('Error instantiating collator: ['.collator_get_error_code($collator).']'.collator_get_error_message($collator)); + } } } + return (self::$collator instanceof Collator); + } + + /** + * 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 + */ + 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; + } 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 + * + * @return int + */ + public static function compare($str1, $str2) { + if (self::ensure_collator_available()) { + return self::$collator->compare($str1, $str2); + } + return strcmp($str1, $str2); + } + + /** + * 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 + */ + public static function asort_objects_by_property(array &$objects, $property) { + $comparison = new collatorlib_property_comparison($property); + return uasort($objects, array($comparison, 'compare')); + } + + /** + * Locale aware sort of objects by a method in common to all objects + * + * @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 + * @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')); + } } + +/** + * Abstract class to aid the sorting of objects with respect to proper language + * comparison using collator + * + * @package core + * @subpackage lib + * @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); +} + +/** + * A comparison helper for comparing properties of two objects + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collatorlib_property_comparison extends collatorlib_comparison { + + /** @var string The property to sort by **/ + protected $property; + + /** + * @param string $property + */ + public function __construct($property) { + $this->property = $property; + } + + /** + * 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); + } +} + +/** + * A comparison helper for comparing the result of a method on two objects + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collatorlib_method_comparison extends collatorlib_comparison { + + /** @var string The method to use for comparison **/ + protected $method; + + /** + * @param string $method The method to call against each object + */ + public function __construct($method) { + $this->method = $method; + } + + /** + * 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->method}(); + $resultb = $objb->{$this->method}(); + return collatorlib::compare($resulta, $resultb); + } +} \ No newline at end of file diff --git a/question/engine/bank.php b/question/engine/bank.php index 656531510df..5ccc2c19d20 100644 --- a/question/engine/bank.php +++ b/question/engine/bank.php @@ -182,7 +182,7 @@ abstract class question_bank { } ksort($sortorder); - textlib_get_instance()->asort($otherqtypes); + collatorlib::asort($otherqtypes); $sortedqtypes = array(); foreach ($sortorder as $name) { diff --git a/user/editlib.php b/user/editlib.php index e7ea1c7c78f..af8e537e067 100644 --- a/user/editlib.php +++ b/user/editlib.php @@ -224,7 +224,7 @@ function useredit_shared_definition(&$mform, $editoroptions = null) { $themes = get_list_of_themes(); foreach ($themes as $key=>$theme) { if (empty($theme->hidefromselector)) { - $choices[$key] = $theme->name; + $choices[$key] = get_string('pluginname', 'theme_'.$theme->name); } } $mform->addElement('select', 'theme', get_string('preferredtheme'), $choices);