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, __FILE__, __LINE__ 59 ); 60 self::$collators[$lc] = $collator; 61 } 62 63 return self::$collators[$lc]; 64 } 65 66 /** 67 * Enable or disable the use of the "intl" extension collator. 68 * This is used for testing and should not be used in normal code. 69 * 70 * @param bool $use 71 * 72 * @author Andreas Gohr <andi@splitbrain.org> 73 */ 74 public static function useIntl($use = true) 75 { 76 self::$useIntl = $use; 77 } 78 79 /** 80 * Drop-in replacement for strcmp(), strcasecmp(), strnatcmp() and strnatcasecmp(). 81 * It uses a collator-based comparison, or strnatcasecmp() as a fallback. 82 * 83 * @param string $str1 The first string. 84 * @param string $str2 The second string. 85 * @return int Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than $str2, and 0 if they are equal. 86 * 87 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 88 */ 89 public static function strcmp($str1, $str2) 90 { 91 $collator = self::getCollator(); 92 if (isset($collator)) { 93 return $collator->compare($str1, $str2); 94 } else { 95 return strnatcasecmp($str1, $str2); 96 } 97 } 98 99 /** 100 * Drop-in replacement for sort(). 101 * It uses a collator-based sort, or sort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 102 * 103 * @param array $array The input array. 104 * @return bool Returns true on success or false on failure. 105 * 106 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 107 */ 108 public static function sort(&$array) 109 { 110 $collator = self::getCollator(); 111 if (isset($collator)) { 112 return $collator->sort($array); 113 } else { 114 return sort($array, SORT_NATURAL | SORT_FLAG_CASE); 115 } 116 } 117 118 /** 119 * Drop-in replacement for ksort(). 120 * It uses a collator-based sort, or ksort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 121 * 122 * @param array $array The input array. 123 * @return bool Returns true on success or false on failure. 124 * 125 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 126 */ 127 public static function ksort(&$array) 128 { 129 $collator = self::getCollator(); 130 if (isset($collator)) { 131 return uksort($array, array($collator, 'compare')); 132 } else { 133 return ksort($array, SORT_NATURAL | SORT_FLAG_CASE); 134 } 135 } 136 137 /** 138 * Drop-in replacement for asort(), natsort() and natcasesort(). 139 * It uses a collator-based sort, or asort() with flags SORT_NATURAL and SORT_FLAG_CASE as a fallback. 140 * 141 * @param array $array The input array. 142 * @return bool Returns true on success or false on failure. 143 * 144 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 145 */ 146 public static function asort(&$array) 147 { 148 $collator = self::getCollator(); 149 if (isset($collator)) { 150 return $collator->asort($array); 151 } else { 152 return asort($array, SORT_NATURAL | SORT_FLAG_CASE); 153 } 154 } 155 156 /** 157 * Drop-in replacement for asort(), natsort() and natcasesort() when the parameter is an array of filenames. 158 * Filenames may not be equal to page names, depending on the setting in $conf['fnencode'], 159 * so the correct behavior is to sort page names and reflect this sorting in the filename array. 160 * 161 * @param array $array The input array. 162 * @return bool Returns true on success or false on failure. 163 * 164 * @author Moisés Braga Ribeiro <moisesbr@gmail.com> 165 * @author Andreas Gohr <andi@splitbrain.org> 166 */ 167 public static function asortFN(&$array) 168 { 169 $collator = self::getCollator(); 170 return uasort($array, function ($fn1, $fn2) use ($collator) { 171 if (isset($collator)) { 172 return $collator->compare(utf8_decodeFN($fn1), utf8_decodeFN($fn2)); 173 } else { 174 return strnatcasecmp(utf8_decodeFN($fn1), utf8_decodeFN($fn2)); 175 } 176 }); 177 } 178} 179