Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
NearMatchPicker
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 4
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pickBest
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 checkAllMatches
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 checkOneMatch
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch;
4
5use MediaWiki\Language\Language;
6use MediaWiki\Logger\LoggerFactory;
7use MediaWiki\Title\Title;
8
9/**
10 * Picks the best "near match" title.
11 *
12 * @license GPL-2.0-or-later
13 */
14class NearMatchPicker {
15    /**
16     * @var Language language to use during normalization process
17     */
18    private $language;
19    /**
20     * @var string the search term
21     */
22    private $term;
23    /**
24     * @var array[] Potential near matches
25     */
26    private $titles;
27
28    /**
29     * @param Language $language to use during normalization process
30     * @param string $term the search term
31     * @param array[] $titles Array of arrays, each with optional keys:
32     *   titleMatch => a title if the title matched
33     *   redirectMatches => an array of redirect matches, one per matched redirect
34     */
35    public function __construct( $language, $term, $titles ) {
36        $this->language = $language;
37        $this->term = $term;
38        $this->titles = $titles;
39    }
40
41    /**
42     * Pick the best near match if possible.
43     *
44     * @return Title|null title if there is a near match and null otherwise
45     */
46    public function pickBest() {
47        if ( !$this->titles ) {
48            return null;
49        }
50        if ( !$this->term ) {
51            return null;
52        }
53        if ( count( $this->titles ) === 1 ) {
54            if ( isset( $this->titles[ 0 ][ 'titleMatch' ] ) ) {
55                return $this->titles[ 0 ][ 'titleMatch' ];
56            }
57            if ( isset( $this->titles[ 0 ][ 'redirectMatches' ][ 0 ] ) ) {
58                return $this->titles[ 0 ][ 'redirectMatches' ][ 0 ];
59            }
60            LoggerFactory::getInstance( LogChannel::DEFAULT )->info(
61                'NearMatchPicker built with busted matches.  Assuming no near match' );
62            return null;
63        }
64
65        $transformers = [
66            static fn ( $term ) => $term,
67            $this->language->lc( ... ),
68            $this->language->ucwords( ... ),
69        ];
70
71        foreach ( $transformers as $transformer ) {
72            $transformedTerm = $transformer( $this->term );
73            $found = null;
74            foreach ( $this->titles as $title ) {
75                $match = $this->checkAllMatches( $transformer, $transformedTerm, $title );
76                if ( $match ) {
77                    // @phan-suppress-next-line PhanRedundantValueComparisonInLoop
78                    if ( $found === null ) {
79                        $found = $match;
80                    } else {
81                        // Found more than one result so we try another transformer
82                        $found = null;
83                        break;
84                    }
85                }
86
87            }
88            if ( $found ) {
89                return $found;
90            }
91        }
92
93        // Didn't find anything
94        return null;
95    }
96
97    /**
98     * Check a single title's worth of matches.  The big thing here is that titles cannot compete with themselves.
99     * @param callable $transformer
100     * @param string $transformedTerm
101     * @param array $allMatchedTitles
102     * @return null|Title null if no title matches and the actual title (either of the page or of a redirect to the
103     *       page) if one did match
104     */
105    private function checkAllMatches( $transformer, $transformedTerm, $allMatchedTitles ) {
106        if ( isset( $allMatchedTitles[ 'titleMatch' ] ) &&
107                $this->checkOneMatch( $transformer, $transformedTerm, $allMatchedTitles[ 'titleMatch' ] ) ) {
108            return $allMatchedTitles[ 'titleMatch' ];
109        }
110        if ( isset( $allMatchedTitles[ 'redirectMatches' ] ) ) {
111            foreach ( $allMatchedTitles[ 'redirectMatches' ] as $redirectMatch ) {
112                if ( $this->checkOneMatch( $transformer, $transformedTerm, $redirectMatch ) ) {
113                    return $redirectMatch;
114                }
115            }
116        }
117        return null;
118    }
119
120    /**
121     * @param callable $transformer
122     * @param string $transformedTerm
123     * @param Title $matchedTitle
124     * @return bool
125     */
126    private function checkOneMatch( $transformer, $transformedTerm, $matchedTitle ) {
127        $transformedTitle = $transformer( $matchedTitle->getText() );
128        return $transformedTerm === $transformedTitle;
129    }
130}