1#![feature(test)]
2extern crate test;
3
4use hashbrown::HashMap;
5use unidecode::unidecode;
6use std::cell::RefCell;
7
8pub struct ThreeSetCompare {
9 alphabet: Vec<char>,
10 minimum_word_len: i32,
11 delta_word_len_ignore: usize,
12 min_word_similarity: f64,
13 left_chars: RefCell<CharMap>,
14 right_chars: RefCell<CharMap>
15}
16
17type CharMap = HashMap<char, i32>;
18
19enum Word {
20 Left,
21 Right
22}
23
24impl ThreeSetCompare {
25 pub fn new() -> ThreeSetCompare {
26 let alphabet = (b'a'..=b'z')
27 .chain(b'0'..=b'9')
28 .map(|c| c as char)
29 .collect::<Vec<_>>();
30
31 let minimum_word_len = 2_i32;
32 let delta_word_len_ignore = 3_usize;
33 let min_word_similarity = 0.707_f64;
34 let average_word_length = 20;
35
36 ThreeSetCompare {
37 alphabet,
38 minimum_word_len,
39 delta_word_len_ignore,
40 min_word_similarity,
41
42 left_chars: RefCell::new(CharMap::with_capacity(average_word_length)),
43 right_chars: RefCell::new(CharMap::with_capacity(average_word_length))
44 }
45 }
46
47 #[inline(always)]
48 fn count_chars(&self, data: &str, pos: Word) {
49 let mut result = match pos {
50 Word::Left => self.left_chars.borrow_mut(),
51 Word::Right => self.right_chars.borrow_mut()
52 };
53
54 result.clear();
55
56 for letter in data.chars() {
57 *result.entry(letter).or_insert(0) += 1;
58 }
59 }
60
61 #[inline(always)]
62 fn preprocess(&self, data: &str) -> Vec<String> {
63 unidecode(data)
64 .to_lowercase()
65 .split_whitespace()
66 .map(|word| word.to_string())
67 .collect::<Vec<String>>()
68 }
69
70 fn logic(&self, first: &Vec<String>, second: &Vec<String>) -> f64 {
71 let mut equality = 0;
72
73 for first_word in first {
74 for second_word in second {
75 let first_len = first_word.chars().count() as i32;
76 let second_len = second_word.chars().count() as i32;
77 let delta_len = (first_len - second_len).abs() as usize;
78
79 if first_len < self.minimum_word_len || second_len < self.minimum_word_len {
80 continue;
81 }
82
83 if first_word.find(second_word).is_some() || second_word.find(first_word).is_some()
84 {
85 if delta_len <= self.delta_word_len_ignore {
86 equality += 1;
87 }
88 } else {
89 self.count_chars(first_word, Word::Left);
90 self.count_chars(second_word, Word::Right);
91
92 let first_map = self.left_chars.borrow();
93 let second_map = self.right_chars.borrow();
94
95 let total_length = first_map
96 .iter()
97 .chain(second_map.iter())
98 .fold(0, |acc, (_, val)| acc + val);
99
100 let zero_count = 0;
101 let mut errors_sum = 0;
102
103 for alpha in &self.alphabet {
104 let count_first = first_map.get(&alpha).unwrap_or(&zero_count);
105 let count_second = second_map.get(&alpha).unwrap_or(&zero_count);
106
107 errors_sum += (count_first - count_second).abs();
108 }
109
110 let local_possibility = 1_f64 - (errors_sum as f64 / total_length as f64);
111 if local_possibility > self.min_word_similarity {
112 equality += 1;
113 }
114 }
115 }
116 }
117
118 let first_count_filtered = first
119 .into_iter()
120 .filter(|word| word.chars().count() >= self.minimum_word_len as usize)
121 .count();
122
123 let second_count_filtered = second
124 .into_iter()
125 .filter(|word| word.chars().count() >= self.minimum_word_len as usize)
126 .count();
127
128 let sum_count = (first_count_filtered + second_count_filtered) as f64 / 2_f64;
129 f64::min(equality as f64 / sum_count as f64, 1.0)
130 }
131
132 pub fn similarity(&self, first: &str, second: &str) -> f64 {
135 let first_p = self.preprocess(first);
136 let second_p = self.preprocess(second);
137
138 return self.logic(&first_p, &second_p);
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use test::Bencher;
145
146 #[bench]
147 fn bench_similarity(b: &mut Bencher) {
148 let comparator = ThreeSetCompare::new();
149 b.iter(|| {
150 comparator.similarity(
151 "Сравнеие двух строк с помощью инвариантной метрики",
152 "Сравнеие двух строк с помощью метрики, инвариантной к перестановке слов"
153 );
154 });
155 }
156
157 use crate::ThreeSetCompare;
158 use assert_approx_eq::assert_approx_eq;
159
160 #[test]
161 fn differences() {
162 let comparator = ThreeSetCompare::new();
163
164 assert_approx_eq!(comparator.similarity(
165 "Сравнение трех строк с помощью инвариантной метрики",
166 "Сравнение двух строк с помощью инвариантной метрики"
167 ), 0.8333333_f64);
168 assert_approx_eq!(comparator.similarity(
169 "Сравнение трех строк помощью инвариантной метрики",
170 "Сравнение двух строк с помощью инвариантной метрики"
171 ), 0.8333333_f64);
172 assert_approx_eq!(comparator.similarity(
173 "Сравнеие двух строк с помощью инвариантной метрики",
174 "Сравнеие двух строк с помощью метрики, инвариантной к перестановке слов"
175 ), 0.8571428_f64);
176 }
177
178 #[test]
179 fn equal() {
180 let comparator = ThreeSetCompare::new();
181
182 assert_approx_eq!(comparator.similarity(
183 "Сравнение двух строк с помощью инвариантной метрики",
184 "Сравнение двух строк с помощью инвариантной метрики"
185 ), 1_f64);
186 assert_approx_eq!(comparator.similarity(
187 "Сравнение двух строк с помощью инвариантной метрики!",
188 "Сравнение двух строк с помощью инвариантной метрики?"
189 ), 1_f64);
190 assert_approx_eq!(comparator.similarity(
191 "Сравнение двху строк с пмоощью инвариатнной метркии",
192 "Сравнение двух строк с помощью инвариантной метрики"
193 ), 1_f64);
194 assert_approx_eq!(comparator.similarity(
195 "Сравнение строк двух с помощью метрики инвариантной",
196 "Сравнение двух строк с помощью инвариантной метрики"
197 ), 1_f64);
198 }
199
200 #[test]
201 fn not_equal() {
202 let comparator = ThreeSetCompare::new();
203
204 assert_approx_eq!(
205 comparator.similarity("Первая строка", "Вторая фраза"),
206 0.5_f64
207 );
208
209 assert_approx_eq!(comparator.similarity("АБВ", "ГДЕ"), 0_f64);
210 }
211}