Skip to main content

words_count/
lib.rs

1/*!
2# Words Count
3
4Count the words and characters, with or without whitespaces.
5
6The algorithm is roughly aligned with the way LibreOffice is counting words. This means that it does not exactly match the [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries) standard.
7
8## Examples
9
10```rust
11use words_count::WordsCount;
12
13assert_eq!(WordsCount {
14    words: 20,
15    characters: 31,
16    whitespaces: 2,
17    cjk: 18,
18}, words_count::count("Rust是由 Mozilla 主導開發的通用、編譯型程式語言。"));
19```
20
21```rust
22let result = words_count::count_separately("apple banana apple");
23
24assert_eq!(2, result.len());
25assert_eq!(Some(&2), result.get("apple"));
26```
27*/
28
29#![no_std]
30
31extern crate alloc;
32
33use alloc::collections::BTreeMap;
34use core::{
35    ops::{Add, AddAssign},
36    str::from_utf8_unchecked,
37};
38
39#[derive(Debug, Clone, Default, Eq, PartialEq)]
40pub struct WordsCount {
41    pub words:       usize,
42    pub characters:  usize,
43    pub whitespaces: usize,
44    pub cjk:         usize,
45}
46
47/// A WordsCount equivalent to words_count::count("\n").
48///
49/// It is useful when processing files a line at a time.
50///
51/// ## Example
52///
53/// ```rust
54/// use words_count::{count, WordsCount, NEWLINE};
55///
56/// let mut total = WordsCount::default();
57/// for ln in std::io::stdin().lines() {
58///     total += count(ln.unwrap()) + NEWLINE;
59/// }
60/// println!("{total:?}");
61/// ```
62pub const NEWLINE: WordsCount =
63    WordsCount {
64        words: 0, characters: 1, whitespaces: 1, cjk: 0
65    };
66
67impl AddAssign for WordsCount {
68    #[inline]
69    fn add_assign(&mut self, other: Self) {
70        *self = Self {
71            words:       self.words + other.words,
72            characters:  self.characters + other.characters,
73            whitespaces: self.whitespaces + other.whitespaces,
74            cjk:         self.cjk + other.cjk,
75        }
76    }
77}
78
79impl Add for WordsCount {
80    type Output = Self;
81
82    #[inline]
83    fn add(mut self, other: Self) -> Self {
84        self += other;
85        self
86    }
87}
88
89/// Count the words in the given string. In general, every non-CJK string of characters between two whitespaces is a word. Dashes (at least two of them) also act as a word delimiter. A CJK character is considered to be an independent word.
90pub fn count<S: AsRef<str>>(s: S) -> WordsCount {
91    let mut in_word = false;
92    let mut consecutive_dashes = 0usize;
93    let mut word_len = 0usize; // the number of characters accumulated in the current word
94
95    let mut count = WordsCount::default();
96
97    for c in s.as_ref().chars() {
98        count.characters += 1;
99
100        if c.is_whitespace() {
101            consecutive_dashes = 0;
102
103            count.whitespaces += 1;
104
105            if in_word {
106                if word_len > 0 {
107                    count.words += 1;
108                }
109
110                in_word = false;
111            }
112
113            word_len = 0;
114        } else {
115            match c {
116                '-' => {
117                    consecutive_dashes += 1;
118
119                    if consecutive_dashes > 1 {
120                        if in_word {
121                            // Two dashes end a word, but only when there is real content before them.
122                            if consecutive_dashes == 2 && word_len >= 2 {
123                                count.words += 1;
124                            }
125
126                            in_word = false;
127                            word_len = 0;
128
129                            continue;
130                        } else {
131                            // A third or later dash starts a new word without being part of it.
132                            in_word = true;
133                            word_len = 0;
134
135                            continue;
136                        }
137                    }
138                },
139                _ => {
140                    consecutive_dashes = 0;
141
142                    if unicode_blocks::is_cjk(c) {
143                        count.words += 1;
144                        count.cjk += 1;
145
146                        if in_word {
147                            if word_len > 0 {
148                                count.words += 1;
149                            }
150
151                            in_word = false;
152                        }
153
154                        word_len = 0;
155
156                        continue;
157                    }
158                },
159            }
160
161            in_word = true;
162            word_len += 1;
163        }
164    }
165
166    if in_word && word_len > 0 {
167        count.words += 1;
168    }
169
170    count
171}
172
173/// Count the words separately in the given string. In general, every non-CJK string of characters between two whitespaces is a word. Dashes (at least two of them) also act as a word delimiter. A CJK character is considered to be an independent word. Punctuations are not handled.
174pub fn count_separately<S: ?Sized + AsRef<str>>(s: &S) -> BTreeMap<&str, usize> {
175    let mut in_word = false;
176    let mut consecutive_dashes = 0usize;
177
178    let mut count = BTreeMap::new();
179
180    let mut p = 0;
181    let mut pp = 0;
182
183    let s = s.as_ref();
184    let bytes = s.as_bytes();
185
186    for c in s.chars() {
187        let cl = c.len_utf8();
188
189        if c.is_whitespace() {
190            if in_word {
191                inc_or_insert(&mut count, unsafe { from_utf8_unchecked(&bytes[p..pp]) });
192
193                in_word = false;
194            }
195
196            p = pp + cl;
197
198            consecutive_dashes = 0;
199        } else {
200            match c {
201                '-' => {
202                    consecutive_dashes += 1;
203
204                    if consecutive_dashes > 1 {
205                        if in_word {
206                            if consecutive_dashes == 2 {
207                                inc_or_insert(&mut count, unsafe {
208                                    from_utf8_unchecked(&bytes[p..(pp - 1)])
209                                });
210                            }
211
212                            in_word = false;
213
214                            pp += cl;
215                            p = pp;
216                            continue;
217                        } else {
218                            p = pp + cl;
219                        }
220                    }
221                },
222                _ => {
223                    if unicode_blocks::is_cjk(c) {
224                        inc_or_insert(&mut count, unsafe {
225                            from_utf8_unchecked(&bytes[pp..(pp + cl)])
226                        });
227
228                        if in_word {
229                            inc_or_insert(&mut count, unsafe {
230                                from_utf8_unchecked(&bytes[p..pp])
231                            });
232
233                            in_word = false;
234                        }
235
236                        consecutive_dashes = 0;
237                        pp += cl;
238                        p = pp;
239                        continue;
240                    }
241
242                    consecutive_dashes = 0;
243                },
244            }
245
246            in_word = true;
247        }
248
249        pp += cl;
250    }
251
252    if in_word {
253        inc_or_insert(&mut count, unsafe { from_utf8_unchecked(&bytes[p..pp]) });
254    }
255
256    count
257}
258
259#[inline]
260fn inc_or_insert<'a>(map: &mut BTreeMap<&'a str, usize>, s: &'a str) {
261    if s.is_empty() {
262        return;
263    }
264
265    *map.entry(s).or_insert(0) += 1;
266}