Merge "Fixed Bug 40464 Placeholder attribute of searchInput element"
[lhc/web/wiklou.git] / includes / Collation.php
index 2734871..215659e 100644 (file)
@@ -62,7 +62,7 @@ abstract class Collation {
                                }
 
                                // If all else fails...
-                               throw new MWException( __METHOD__.": unknown collation type \"$collationName\"" );
+                               throw new MWException( __METHOD__ . ": unknown collation type \"$collationName\"" );
                }
        }
 
@@ -149,6 +149,8 @@ class IdentityCollation extends Collation {
 }
 
 class IcuCollation extends Collation {
+       const FIRST_LETTER_VERSION = 1;
+
        var $primaryCollator, $mainCollator, $locale;
        var $firstLetterData;
 
@@ -211,7 +213,9 @@ class IcuCollation extends Collation {
                'pl' => array( "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ź", "Ż" ),
                'pt' => array(),
                'ru' => array(),
+               'sv' => array( "Å", "Ä", "Ö" ),
                'uk' => array( "Ґ", "Ь" ),
+               'vi' => array( "Ă", "Â", "Đ", "Ê", "Ô", "Ơ", "Ư" ),
                // Not verified, but likely correct
                'af' => array(),
                'ast' => array( "Ch", "Ll", "Ñ" ),
@@ -264,14 +268,11 @@ class IcuCollation extends Collation {
                'smn' => array( "Á", "Č", "Đ", "Ŋ", "Š", "Ŧ", "Ž", "Æ", "Ø", "Å", "Ä", "Ö" ),
                'sq' => array( "Ç", "Dh", "Ë", "Gj", "Ll", "Nj", "Rr", "Sh", "Th", "Xh", "Zh" ),
                'sr' => array(),
-               'sv' => array( "Å", "Ä", "Ö" ),
-               '-sv' => array( "Þ" ), // sorted as "th" in Swedish, causing unexpected output - bug 45446
                'tk' => array( "Ç", "Ä", "Ž", "Ň", "Ö", "Ş", "Ü", "Ý" ),
                'tl' => array( "Ñ", "Ng" ),
                'tr' => array( "Ç", "Ğ", "İ", "Ö", "Ş", "Ü" ),
                'tt' => array( "Ә", "Ө", "Ү", "Җ", "Ң", "Һ" ),
                'uz' => array( "Ch", "G'", "Ng", "O'", "Sh" ),
-               'vi' => array( "Ă", "Â", "Đ", "Ê", "Ô", "Ơ", "Ư" ),
        );
 
        const RECORD_LENGTH = 14;
@@ -347,7 +348,9 @@ class IcuCollation extends Collation {
                $cacheKey = wfMemcKey( 'first-letters', $this->locale );
                $cacheEntry = $cache->get( $cacheKey );
 
-               if ( $cacheEntry ) {
+               if ( $cacheEntry && isset( $cacheEntry['version'] )
+                       && $cacheEntry['version'] == self::FIRST_LETTER_VERSION ) 
+               {
                        $this->firstLetterData = $cacheEntry;
                        return $this->firstLetterData;
                }
@@ -359,8 +362,8 @@ class IcuCollation extends Collation {
                        // Append additional characters
                        $letters = array_merge( $letters, self::$tailoringFirstLetters[$this->locale] );
                        // Remove unnecessary ones, if any
-                       if ( isset( self::$tailoringFirstLetters[ '-' . $this->locale ] ) ) {
-                               $letters = array_diff( $letters, self::$tailoringFirstLetters[ '-' . $this->locale ] );
+                       if ( isset( self::$tailoringFirstLetters['-' . $this->locale] ) ) {
+                               $letters = array_diff( $letters, self::$tailoringFirstLetters['-' . $this->locale] );
                        }
                } else {
                        $letters = wfGetPrecompiledData( "first-letters-{$this->locale}.ser" );
@@ -392,9 +395,76 @@ class IcuCollation extends Collation {
                        }
                }
                ksort( $letterMap, SORT_STRING );
+               // Remove duplicate prefixes. Basically if something has a sortkey
+               // which is a prefix of some other sortkey, then it is an
+               // expansion and probably should not be considered a section
+               // header.
+               //
+               // For example 'þ' is sometimes sorted as if it is the letters
+               // 'th'. Other times it is its own primary element. Another
+               // example is '₨'. Sometimes its a currency symbol. Sometimes it
+               // is an 'R' followed by an 's'.
+               //
+               // Additionally an expanded element should always sort directly
+               // after its first element due to they way sortkeys work.
+               //
+               // UCA sortkey elements are of variable length but no collation
+               // element should be a prefix of some other element, so I think
+               // this is safe. See:
+               // * https://ssl.icu-project.org/repos/icu/icuhtml/trunk/design/collation/ICU_collation_design.htm
+               // * http://site.icu-project.org/design/collation/uca-weight-allocation
+               //
+               // Additionally, there is something called primary compression to
+               // worry about. Basically, if you have two primary elements that
+               // are more than one byte and both start with the same byte then
+               // the first byte is dropped on the second primary. Additionally
+               // either \x03 or \xFF may be added to mean that the next primary
+               // does not start with the first byte of the first primary.
+               //
+               // This shouldn't matter much, as the first primary is not
+               // changed, and that is what we are comparing against.
+               //
+               // tl;dr: This makes some assumptions about how icu implements
+               // collations. It seems incredibly unlikely these assumptions
+               // will change, but nonetheless they are assumptions.
+
+               $prev = false;
+               $duplicatePrefixes = array();
+               foreach ( $letterMap as $key => $value ) {
+                       // Remove terminator byte. Otherwise the prefix
+                       // comparison will get hung up on that.
+                       $trimmedKey = rtrim( $key, "\0" );
+                       if ( $prev === false || $prev === '' ) {
+                               $prev = $trimmedKey;
+                               // We don't yet have a collation element
+                               // to compare against, so continue.
+                               continue;
+                       }
+
+                       // Due to the fact the array is sorted, we only have
+                       // to compare with the element directly previous
+                       // to the current element (skipping expansions).
+                       // An element "X" will always sort directly
+                       // before "XZ" (Unless we have "XY", but we
+                       // do not update $prev in that case).
+                       if ( substr( $trimmedKey, 0, strlen( $prev ) ) === $prev ) {
+                               $duplicatePrefixes[] = $key;
+                               // If this is an expansion, we don't want to
+                               // compare the next element to this element,
+                               // but to what is currently $prev
+                               continue;
+                       }
+                       $prev = $trimmedKey;
+               }
+               foreach ( $duplicatePrefixes as $badKey ) {
+                       wfDebug( "Removing '{$letterMap[$badKey]}' from first letters." );
+                       unset( $letterMap[$badKey] );
+                       // This code assumes that unsetting does not change sort order.
+               }
                $data = array(
                        'chars' => array_values( $letterMap ),
-                       'keys' => array_keys( $letterMap )
+                       'keys' => array_keys( $letterMap ),
+                       'version' => self::FIRST_LETTER_VERSION,
                );
 
                // Reduce memory usage before caching