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}