Skip to main content

better_profanity/
words.rs

1use rustc_hash::FxHashMap;
2
3/// The words to censor, answering `string in CENSOR_WORDSET` for all of them at once.
4///
5/// better_profanity stores each word as a `VaryingString` and compares a string with every
6/// word in turn. When every variant in `CHARS_MAPPING` is a single character, as in the
7/// default mapping, a string equals a `VaryingString` exactly when it has one character per
8/// position of the word and each character is one of the variants allowed at its position.
9/// A word is therefore a sequence of character sets, and the words form a trie whose edges
10/// are those sets. A character can lie in the sets of several edges (`*` stands for any
11/// vowel), so a lookup follows every matching edge.
12#[derive(Clone, Debug)]
13pub struct WordSet {
14    nodes: Vec<Node>,
15}
16
17#[derive(Clone, Debug, Default)]
18struct Node {
19    terminal: bool,
20    /// One entry per character of each child's set, sorted by character.
21    edges: Box<[(char, u32)]>,
22}
23
24/// A character set interned by a [`WordSetBuilder`].
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct SetId(u32);
27
28/// Builds a [`WordSet`] from words given as sequences of character sets.
29#[derive(Debug)]
30pub struct WordSetBuilder {
31    sets: Vec<Box<[char]>>,
32    set_ids: FxHashMap<Box<[char]>, SetId>,
33    /// (node, set) to child node.
34    children: FxHashMap<(u32, SetId), u32>,
35    terminal: Vec<bool>,
36    scratch: Vec<char>,
37    ids: Vec<SetId>,
38}
39
40impl Default for WordSetBuilder {
41    fn default() -> Self {
42        WordSetBuilder {
43            sets: Vec::new(),
44            set_ids: FxHashMap::default(),
45            children: FxHashMap::default(),
46            terminal: vec![false],
47            scratch: Vec::new(),
48            ids: Vec::new(),
49        }
50    }
51}
52
53impl WordSetBuilder {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// The id of a set of characters; equal sets get the same id.
59    pub fn intern(&mut self, chars: impl IntoIterator<Item = char>) -> SetId {
60        self.scratch.clear();
61        self.scratch.extend(chars);
62        self.scratch.sort_unstable();
63        self.scratch.dedup();
64        if let Some(&id) = self.set_ids.get(self.scratch.as_slice()) {
65            return id;
66        }
67        let id = SetId(self.sets.len() as u32);
68        let set: Box<[char]> = self.scratch.as_slice().into();
69        self.sets.push(set.clone());
70        self.set_ids.insert(set, id);
71        id
72    }
73
74    /// Whether an interned set has no characters, so that no word containing it can match.
75    pub fn is_empty_set(&self, id: SetId) -> bool {
76        self.sets[id.0 as usize].is_empty()
77    }
78
79    /// Adds a word given as interned sets, one per position.
80    pub fn insert_sets(&mut self, positions: &[SetId]) {
81        let mut node = 0u32;
82        for &set in positions {
83            let next = self.terminal.len() as u32;
84            node = *self.children.entry((node, set)).or_insert(next);
85            if node == next {
86                self.terminal.push(false);
87            }
88        }
89        self.terminal[node as usize] = true;
90    }
91
92    /// Adds a word: for each position, the characters that match there.
93    pub fn insert<P, C>(&mut self, positions: P)
94    where
95        P: IntoIterator<Item = C>,
96        C: IntoIterator<Item = char>,
97    {
98        let mut ids = std::mem::take(&mut self.ids);
99        ids.clear();
100        ids.extend(positions.into_iter().map(|p| self.intern(p)));
101        self.insert_sets(&ids);
102        self.ids = ids;
103    }
104
105    pub fn build(self) -> WordSet {
106        let mut edges: Vec<Vec<(char, u32)>> = vec![Vec::new(); self.terminal.len()];
107        for (&(node, set), &child) in &self.children {
108            let set = &self.sets[set.0 as usize];
109            edges[node as usize].extend(set.iter().map(|&c| (c, child)));
110        }
111        let nodes = edges
112            .into_iter()
113            .zip(self.terminal)
114            .map(|(mut edges, terminal)| {
115                edges.sort_unstable();
116                Node {
117                    terminal,
118                    edges: edges.into_boxed_slice(),
119                }
120            })
121            .collect();
122        WordSet { nodes }
123    }
124}
125
126/// The trie nodes reached by the characters fed so far.
127#[derive(Clone, Debug, Default)]
128pub struct Walk {
129    states: Vec<u32>,
130    next: Vec<u32>,
131}
132
133impl Walk {
134    /// No word starts with the characters fed so far, so no longer string matches either.
135    pub fn is_dead(&self) -> bool {
136        self.states.is_empty()
137    }
138
139    pub(crate) fn copy_from(&mut self, other: &Walk) {
140        self.states.clear();
141        self.states.extend_from_slice(&other.states);
142    }
143}
144
145impl WordSet {
146    pub fn builder() -> WordSetBuilder {
147        WordSetBuilder::new()
148    }
149
150    /// Whether the set has no words.
151    pub fn is_empty(&self) -> bool {
152        !self.nodes[0].terminal && self.nodes[0].edges.is_empty()
153    }
154
155    /// Starts `walk` at the empty string.
156    pub fn reset(&self, walk: &mut Walk) {
157        walk.states.clear();
158        walk.states.push(0);
159    }
160
161    pub fn step(&self, walk: &mut Walk, c: char) {
162        walk.next.clear();
163        for &state in &walk.states {
164            let edges = &self.nodes[state as usize].edges;
165            let start = edges.partition_point(|&(e, _)| e < c);
166            for &(e, child) in &edges[start..] {
167                if e != c {
168                    break;
169                }
170                walk.next.push(child);
171            }
172        }
173        std::mem::swap(&mut walk.states, &mut walk.next);
174    }
175
176    /// Whether the characters fed to `walk` form one of the words.
177    pub fn accepts(&self, walk: &Walk) -> bool {
178        walk.states.iter().any(|&s| self.nodes[s as usize].terminal)
179    }
180
181    /// `s in CENSOR_WORDSET`.
182    pub fn contains(&self, s: impl IntoIterator<Item = char>) -> bool {
183        let mut walk = Walk::default();
184        self.reset(&mut walk);
185        for c in s {
186            self.step(&mut walk, c);
187            if walk.is_dead() {
188                return false;
189            }
190        }
191        self.accepts(&walk)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn set(words: &[&[&str]]) -> WordSet {
200        let mut b = WordSet::builder();
201        for w in words {
202            b.insert(w.iter().map(|p| p.chars()));
203        }
204        b.build()
205    }
206
207    #[test]
208    fn matches_each_position() {
209        let s = set(&[&["a@*", "s$", "s$"], &["o*0", "k"], &["ab"]]);
210        assert!(s.contains("ass".chars()));
211        assert!(s.contains("*$s".chars()));
212        assert!(s.contains("*k".chars()));
213        assert!(s.contains("b".chars()));
214        assert!(!s.contains("as".chars()));
215        assert!(!s.contains("asss".chars()));
216        assert!(!s.contains("".chars()));
217        assert!(!s.is_empty());
218        assert!(set(&[]).is_empty());
219        assert!(set(&[&[]]).contains("".chars()));
220    }
221}