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}