three_set_compare/
lib.rs

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    /// Compare two strings for equality. Don't use this method with strings longer than 255 characters.
133    /// You can use any language, the data is unidecoded before comparing.
134    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}