Skip to main content

diff_match_patch/
match_.rs

1// Bitap fuzzy match: locate a pattern in text near an expected location.
2
3use crate::engine;
4use crate::types::{max, min, min1, Dmp};
5use std::collections::HashMap;
6
7// The historic public API takes &Vec/&mut Vec/&String; frozen by the
8// drop-in compatibility contract.
9#[allow(clippy::ptr_arg)]
10impl Dmp {
11    /// Locate the best instance of 'pattern' in 'text' near 'loc'.
12    ///
13    /// Args:
14    /// text: The text to search.
15    /// pattern: The pattern to search for.
16    /// loc: The location to search around.
17    ///
18    /// Returns:
19    /// Best match index or -1.
20    pub fn match_main(&mut self, text1: &str, patern1: &str, mut loc: i32) -> i32 {
21        // Historical contract: loc clamps on the BYTE length of text1 here
22        // even though everything below counts chars; preserved for drop-in
23        // compatibility (the internal char-space entry clamps on scalars).
24        loc = max(0, min(loc, text1.len() as i32));
25        if patern1.is_empty() {
26            return loc;
27        }
28        if text1.is_empty() {
29            return -1;
30        }
31        let text: Vec<char> = text1.chars().collect();
32        let patern: Vec<char> = patern1.chars().collect();
33        match_clamped(self, &text, &patern, loc)
34    }
35
36    /// Locate the best instance of 'pattern' in 'text' near 'loc' using the
37    /// Bitap algorithm.
38    ///
39    /// Args:
40    /// text: The text to search.
41    /// pattern: The pattern to search for.
42    /// loc: The location to search around.
43    ///
44    /// Returns:
45    /// Best match index or -1.
46    pub fn match_bitap(&mut self, text: &Vec<char>, patern: &Vec<char>, loc: i32) -> i32 {
47        bitap(self, text, patern, loc)
48    }
49
50    /// Compute and return the score for a match with e errors and x location.
51    /// Accesses loc and pattern through being a closure.
52    ///
53    /// Args:
54    /// e: Number of errors in match.
55    /// x: Location of match.
56    ///
57    /// Returns:
58    /// Overall score for match (0.0 = good, 1.0 = bad).
59    pub fn match_bitap_score(&mut self, e: i32, x: i32, loc: i32, patern: &Vec<char>) -> f32 {
60        bitap_score(self, e, x, loc, patern.len())
61    }
62    /// Initialise the alphabet for the Bitap algorithm.
63    ///
64    /// Args:
65    /// pattern: The text to encode.
66    ///
67    /// Returns:
68    /// Hash of character locations. Public i32 view of the internal u64
69    /// masks; bit 31 lands in the sign bit exactly as the pre-rewrite
70    /// release builds computed it.
71    pub fn match_alphabet(&mut self, patern: &Vec<char>) -> HashMap<char, i32> {
72        alphabet(patern)
73            .into_iter()
74            .map(|(ch, mask)| (ch, mask as u32 as i32))
75            .collect()
76    }
77}
78
79/// `match_main` over char slices with the reference's scalar clamp — the
80/// entry patch_apply uses, so locating a pattern never materializes text.
81pub(crate) fn match_chars(dmp: &mut Dmp, text: &[char], patern: &[char], loc: i32) -> i32 {
82    let loc = max(0, min(loc, text.len() as i32));
83    if patern.is_empty() {
84        return loc;
85    }
86    if text.is_empty() {
87        return -1;
88    }
89    match_clamped(dmp, text, patern, loc)
90}
91
92/// Shared tail of the match entries; `loc` is already clamped by the caller.
93fn match_clamped(dmp: &mut Dmp, text: &[char], patern: &[char], loc: i32) -> i32 {
94    if text == patern {
95        // Shortcut (potentially not guaranteed by the algorithm)
96        return 0;
97    } else if loc as usize + patern.len() <= text.len()
98        && text[(loc as usize)..(loc as usize + patern.len())] == *patern
99    {
100        // Perfect match at the perfect spot!  (Includes case of null pattern)
101        return loc;
102    }
103    bitap(dmp, text, patern, loc)
104}
105
106/// DMP match_bitap over token slices.
107fn bitap(dmp: &mut Dmp, text: &[char], patern: &[char], loc: i32) -> i32 {
108    // check for maxbits limit.
109    if !(dmp.match_maxbits == 0 || patern.len() as i32 <= dmp.match_maxbits) {
110        panic!("patern too long for this application");
111    }
112    // With match_maxbits = 0 ("no limit") the u64 bit vectors still cap the
113    // pattern at 64 tokens; fail clearly instead of overflowing the shifts.
114    if patern.len() > 64 {
115        panic!("patern too long for this application");
116    }
117    // Initialise the alphabet.
118    let s: HashMap<char, u64> = alphabet(patern);
119
120    // Highest score beyond which we give up.
121    let mut score_threshold: f32 = dmp.match_threshold;
122    // Is there a nearby exact match? (speedup)
123    let mut best_loc = match engine::find_sub(text, patern, loc as usize) {
124        Some(i) => i as i32,
125        None => -1,
126    };
127    if best_loc != -1 {
128        score_threshold = min1(
129            bitap_score(dmp, 0, best_loc, loc, patern.len()),
130            score_threshold,
131        );
132        // What about in the other direction? (speedup)
133        best_loc = match engine::rfind_sub(text, patern, loc as usize + patern.len()) {
134            Some(i) => i as i32,
135            None => -1,
136        };
137        if best_loc != -1 {
138            score_threshold = min1(
139                score_threshold,
140                bitap_score(dmp, 0, best_loc, loc, patern.len()),
141            );
142        }
143    }
144    // Initialise the bit arrays.
145    let matchmask: u64 = 1 << (patern.len() - 1);
146    best_loc = -1;
147    let mut bin_min: i32;
148    let mut bin_mid: i32;
149    let mut bin_max: i32 = (patern.len() + text.len()) as i32;
150    // Empty initialization added to appease pychecker.
151    let mut last_rd: Vec<u64> = vec![];
152    for d in 0..patern.len() {
153        /*
154        Scan for the best match each iteration allows for one more error.
155        Run a binary search to determine how far from 'loc' we can stray at
156        this error level.
157        */
158        let mut rd: Vec<u64> = vec![];
159        bin_min = 0;
160        bin_mid = bin_max;
161        // Use the result from this iteration as the maximum for the next.
162        while bin_min < bin_mid {
163            if bitap_score(dmp, d as i32, loc + bin_mid, loc, patern.len()) <= score_threshold {
164                bin_min = bin_mid;
165            } else {
166                bin_max = bin_mid;
167            }
168            bin_mid = bin_min + (bin_max - bin_min) / 2;
169        }
170        bin_max = bin_mid;
171        let mut start = max(1, loc - bin_mid + 1);
172        let finish = min(loc + bin_mid, text.len() as i32) + patern.len() as i32;
173        rd.resize((finish + 2) as usize, 0);
174        rd[(finish + 1) as usize] = (1u64 << d) - 1;
175        let mut j = finish;
176        while j >= start {
177            let char_match: u64;
178            if text.len() < j as usize {
179                // Out of range.
180                char_match = 0;
181            } else {
182                // Subsequent passes: fuzzy match.
183                match s.get(&(text[j as usize - 1])) {
184                    Some(num) => {
185                        char_match = *num;
186                    }
187                    None => {
188                        char_match = 0;
189                    }
190                }
191            }
192            if d == 0 {
193                // First pass: exact match.
194                rd[j as usize] = ((rd[j as usize + 1] << 1) | 1) & char_match;
195            } else {
196                rd[j as usize] = (((rd[j as usize + 1] << 1) | 1) & char_match)
197                    | (((last_rd[j as usize + 1] | last_rd[j as usize]) << 1) | 1)
198                    | last_rd[j as usize + 1];
199            }
200            if (rd[j as usize] & matchmask) != 0 {
201                let score: f32 = bitap_score(dmp, d as i32, j - 1, loc, patern.len());
202                // This match will almost certainly be better than any existing match.
203                // But check anyway.
204                if score <= score_threshold {
205                    // Told you so.
206                    score_threshold = score;
207                    best_loc = j - 1;
208                    if best_loc > loc {
209                        // When passing loc, don't exceed our current distance from loc.
210                        start = max(1, 2 * loc - best_loc);
211                    } else {
212                        // Already passed loc, downhill from here on in.
213                        break;
214                    }
215                }
216            }
217            j -= 1;
218        }
219        // No hope for a (better) match at greater error levels.
220        if bitap_score(dmp, d as i32 + 1, loc, loc, patern.len()) > score_threshold {
221            break;
222        }
223        last_rd = rd;
224    }
225    best_loc
226}
227
228/// Bitap match quality (0.0 = perfect) from error count and distance; only
229/// the pattern LENGTH matters, so the hot loops pass it directly.
230fn bitap_score(dmp: &Dmp, e: i32, x: i32, loc: i32, patern_len: usize) -> f32 {
231    let accuracy: f32 = (e as f32) / (patern_len as f32);
232    let proximity: i32 = (loc - x).abs();
233    if dmp.match_distance == 0 {
234        // Dodge divide by zero error.
235        if proximity == 0 {
236            return accuracy;
237        } else {
238            return 1.0;
239        }
240    }
241    accuracy + ((proximity as f32) / (dmp.match_distance as f32))
242}
243
244/// Bitap alphabet over u64 masks (safe for patterns up to match_maxbits = 32,
245/// and beyond up to 64 tokens).
246fn alphabet(patern: &[char]) -> HashMap<char, u64> {
247    let mut s: HashMap<char, u64> = HashMap::new();
248    for &ch in patern {
249        s.insert(ch, 0);
250    }
251    for (i, &ch) in patern.iter().enumerate() {
252        let mask = s[&ch] | (1u64 << (patern.len() - i - 1));
253        s.insert(ch, mask);
254    }
255    s
256}