1<?php 2 3namespace dokuwiki\Utf8; 4 5use dokuwiki\Logger; 6 7/** 8 * DokuWiki sort functions 9 * 10 * When "intl" extension is available, all sorts are done using a collator. 11 * Otherwise, primitive PHP functions are called. 12 * 13 * The collator is created using the locale given in $conf['lang']. 14 * It always uses case insensitive "natural" ordering in its collation. 15 * The fallback solution uses the primitive PHP functions that return almost the same results 16 * when the input is text with only [A-Za-z0-9] characters. 17 * 18 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 19 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 20 * @author Andreas Gohr <andi@splitbrain.org> 21 */ 22class Sort 23{ 24 /** @var \Collator[] language specific collators, usually only one */ 25 protected static $collators = []; 26 27 /** @var bool should the intl extension be used if available? For testing only */ 28 protected static $useIntl = true; 29 30 /** 31 * Initialization of a collator using $conf['lang'] as the locale. 32 * The initialization is done only once. 33 * The collation takes "natural ordering" into account, that is, "page 2" is before "page 10". 34 * 35 * @return \Collator Returns a configured collator or null if the collator cannot be created. 36 * 37 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 38 */ 39 protected static function getCollator() 40 { 41 global $conf; 42 $lc = $conf['lang']; 43 44 // check if intl extension is available 45 if (!self::$useIntl || !class_exists('\Collator')) { 46 return null; 47 } 48 49 // load collator if not available yet 50 if (!isset(self::$collators[$lc])) { 51 $collator = \Collator::create($lc); 52 if (!isset($collator)) return null; // check needed as stated in the docs 53 $collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON); 54 Logger::getInstance(Logger::LOG_DEBUG)->log( 55 'Collator created with locale "' . $lc . '": numeric collation on, ' . 56 'valid locale "' . $collator->getLocale(\Locale::VALID_LOCALE) . '", ' . 57 'actual locale "' . $collator->getLocale(\Locale::ACTUAL_LOCALE) . '"', 58 null, 59 __FILE__, 60 __LINE__ 61 ); 62 self::$collators[$lc] = $collator; 63 } 64 65 return self::$collators[$lc]; 66 } 67 68 /** 69 * Enable or disable the use of the "intl" extension collator. 70 * This is used for testing and should not be used in normal code. 71 * 72 * @param bool $use 73 * 74 * @author Andreas Gohr <andi@splitbrain.org> 75 */ 76 public static function useIntl($use = true) 77 { 78 self::$useIntl = $use; 79 } 80 81 /** 82 * Drop-in replacement for strcmp(), strcasecmp(), strnatcmp() and strnatcasecmp(). 83 * It uses a collator-based comparison, or strnatcasecmp() as a fallback. 84 * 85 * @param string $str1 The first string. 86 * @param string $str2 The second string. 87 * @return int Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than $str2, and 0 if they are equal. 88 * 89 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 90 */ 91 public static function strcmp($str1, $str2) 92 { 93 $collator = self::getCollator(); 94 if (isset($collator)) { 95 return $collator->compare($str1, $str2); 96 } else { 97 return strnatcasecmp($str1, $str2); 98 } 99 } 100 101 /** 102 * Drop-in replacement for sort(). 103 * It uses a collator-based sort, or sort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 104 * 105 * @param array $array The input array. 106 * @return bool Returns true on success or false on failure. 107 * 108 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 109 */ 110 public static function sort(&$array) 111 { 112 $collator = self::getCollator(); 113 if (isset($collator)) { 114 return $collator->sort($array); 115 } else { 116 return sort($array, SORT_NATURAL | SORT_FLAG_CASE); 117 } 118 } 119 120 /** 121 * Drop-in replacement for ksort(). 122 * It uses a collator-based sort, or ksort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 123 * 124 * @param array $array The input array. 125 * @return bool Returns true on success or false on failure. 126 * 127 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 128 */ 129 public static function ksort(&$array) 130 { 131 $collator = self::getCollator(); 132 if (isset($collator)) { 133 return uksort($array, [$collator, 'compare']); 134 } else { 135 return ksort($array, SORT_NATURAL | SORT_FLAG_CASE); 136 } 137 } 138 139 /** 140 * Drop-in replacement for asort(), natsort() and natcasesort(). 141 * It uses a collator-based sort, or asort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 142 * 143 * @param array $array The input array. 144 * @return bool Returns true on success or false on failure. 145 * 146 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 147 */ 148 public static function asort(&$array) 149 { 150 $collator = self::getCollator(); 151 if (isset($collator)) { 152 return $collator->asort($array); 153 } else { 154 return asort($array, SORT_NATURAL | SORT_FLAG_CASE); 155 } 156 } 157 158 /** 159 * Drop-in replacement for asort(), natsort() and natcasesort() when the parameter is an array of filenames. 160 * Filenames may not be equal to page names, depending on the setting in $conf['fnencode'], 161 * so the correct behavior is to sort page names and reflect this sorting in the filename array. 162 * 163 * @param array $array The input array. 164 * @return bool Returns true on success or false on failure. 165 * 166 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 167 * @author Andreas Gohr <andi@splitbrain.org> 168 */ 169 public static function asortFN(&$array) 170 { 171 $collator = self::getCollator(); 172 return uasort($array, function ($fn1, $fn2) use ($collator) { 173 if (isset($collator)) { 174 return $collator->compare(utf8_decodeFN($fn1), utf8_decodeFN($fn2)); 175 } else { 176 return strnatcasecmp(utf8_decodeFN($fn1), utf8_decodeFN($fn2)); 177 } 178 }); 179 } 180} 181