petname/lib.rs
1#![no_std]
2//!
3//! You can populate [`Petnames`] with your own word lists, but the word lists
4//! from upstream [petname](https://github.com/dustinkirkland/petname) are
5//! included with the `default-words` feature (enabled by default). See
6//! [`Petnames::small`], [`Petnames::medium`], and [`Petnames::large`] to select
7//! a particular built-in word list, or use [`Petnames::default`].
8//!
9//! The other thing you need is a random number generator from [rand][]:
10//!
11//! ```rust
12//! use petname::Generator; // Trait needs to be in scope for `generate`.
13//! # #[cfg(feature = "default-rng")]
14//! let mut rng = rand::thread_rng();
15//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
16//! let name = petname::Petnames::default().generate(&mut rng, 7, ":").expect("no names");
17//! ```
18//!
19//! It may be more convenient to use the default random number generator:
20//!
21//! ```rust
22//! use petname::Generator; // Trait needs to be in scope for `generate_one`.
23//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
24//! let name = petname::Petnames::default().generate_one(7, ":").expect("no names");
25//! ```
26//!
27//! There's a [convenience function][petname] that'll do all of this:
28//!
29//! ```rust
30//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
31//! let name = petname::petname(7, ":");
32//! ```
33//!
34//! But the most flexible approach is to create an [`Iterator`] with
35//! [`iter`][`Petnames::iter`]:
36//!
37//! ```rust
38//! use petname::Generator; // Trait needs to be in scope for `iter`.
39//! # #[cfg(feature = "default-rng")]
40//! let mut rng = rand::thread_rng();
41//! # #[cfg(feature = "default-words")]
42//! let petnames = petname::Petnames::default();
43//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
44//! let ten_thousand_names: Vec<String> =
45//! petnames.iter(&mut rng, 3, "_").take(10000).collect();
46//! ```
47//!
48//! You can modify the word lists to, for example, only use words beginning with
49//! the letter "b":
50//!
51//! ```rust
52//! # use petname::Generator;
53//! # #[cfg(feature = "default-words")]
54//! let mut petnames = petname::Petnames::default();
55//! # #[cfg(feature = "default-words")]
56//! petnames.retain(|s| s.starts_with("b"));
57//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
58//! let name = petnames.generate_one(3, ".").expect("no names");
59//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
60//! assert!(name.starts_with('b'));
61//! ```
62//!
63//! There's another way to generate alliterative petnames which is useful when
64//! you you don't need or want each name to be limited to using the same initial
65//! letter as the previous generated name. Create the `Petnames` as before, and
66//! then convert it into an [`Alliterations`]:
67//!
68//! ```rust
69//! # use petname::Generator;
70//! # #[cfg(feature = "default-words")]
71//! let mut petnames = petname::Petnames::default();
72//! # #[cfg(feature = "default-words")]
73//! let mut alliterations: petname::Alliterations = petnames.into();
74//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
75//! alliterations.generate_one(3, "/").expect("no names");
76//! ```
77//!
78//! Both [`Petnames`] and [`Alliterations`] implement [`Generator`]; this needs
79//! to be in scope in order to generate names. It's [object-safe] so you can use
80//! `Petnames` and `Alliterations` as trait objects:
81//!
82//! [object-safe]:
83//! https://doc.rust-lang.org/reference/items/traits.html#object-safety
84//!
85//! ```rust
86//! use petname::Generator;
87//! # #[cfg(feature = "default-words")]
88//! let generator: &dyn Generator = &petname::Petnames::default();
89//! # #[cfg(feature = "default-words")]
90//! let generator: &dyn Generator = &petname::Alliterations::default();
91//! ```
92//!
93
94extern crate alloc;
95
96use alloc::{
97 borrow::Cow,
98 boxed::Box,
99 collections::BTreeMap,
100 string::{String, ToString},
101 vec::Vec,
102};
103
104use rand::seq::{IteratorRandom, SliceRandom};
105
106/// Convenience function to generate a new petname from default word lists.
107#[allow(dead_code)]
108#[cfg(all(feature = "default-rng", feature = "default-words"))]
109pub fn petname(words: u8, separator: &str) -> Option<String> {
110 Petnames::default().generate_one(words, separator)
111}
112
113/// A word list.
114pub type Words<'a> = Cow<'a, [&'a str]>;
115
116#[cfg(feature = "default-words")]
117mod words {
118 include!(concat!(env!("OUT_DIR"), "/words.rs"));
119}
120
121/// Trait that defines a generator of petnames.
122///
123/// There are default implementations of `generate_one` and `iter`, i.e. only
124/// `generate` needs to be implemented.
125///
126pub trait Generator<'a> {
127 /// Generate a new petname.
128 ///
129 /// # Examples
130 ///
131 /// ```rust
132 /// # use petname::Generator;
133 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
134 /// let mut rng = rand::thread_rng();
135 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
136 /// petname::Petnames::default().generate(&mut rng, 7, ":");
137 /// ```
138 ///
139 /// # Notes
140 ///
141 /// This may return fewer words than you request if one or more of the word
142 /// lists are empty. For example, if there are no adverbs, requesting 3 or
143 /// more words may still yield only "doubtful-salmon".
144 ///
145 fn generate(&self, rng: &mut dyn rand::RngCore, words: u8, separator: &str) -> Option<String>;
146
147 /// Generate a single new petname.
148 ///
149 /// This is like `generate` but uses `rand::thread_rng` as the random
150 /// source. For efficiency use `generate` when creating multiple names, or
151 /// when you want to use a custom source of randomness.
152 #[cfg(feature = "default-rng")]
153 fn generate_one(&self, words: u8, separator: &str) -> Option<String> {
154 self.generate(&mut rand::thread_rng(), words, separator)
155 }
156
157 /// Iterator yielding petnames.
158 ///
159 /// # Examples
160 ///
161 /// ```rust
162 /// # use petname::Generator;
163 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
164 /// let mut rng = rand::thread_rng();
165 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
166 /// let petnames = petname::Petnames::default();
167 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
168 /// let mut iter = petnames.iter(&mut rng, 4, "_");
169 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
170 /// println!("name: {}", iter.next().unwrap());
171 /// ```
172 fn iter(
173 &'a self,
174 rng: &'a mut dyn rand::RngCore,
175 words: u8,
176 separator: &str,
177 ) -> Box<dyn Iterator<Item = String> + 'a>
178 where
179 Self: Sized,
180 {
181 let names = Names { generator: self, rng, words, separator: separator.to_string() };
182 Box::new(names)
183 }
184}
185
186/// Word lists and the logic to combine them into _petnames_.
187///
188/// A _petname_ with `n` words will contain, in order:
189///
190/// * `n - 2` adverbs when `n >= 2`, otherwise 0 adverbs.
191/// * 1 adjective when `n >= 2`, otherwise 0 adjectives.
192/// * 1 noun when `n >= 1`, otherwise 0 nouns.
193///
194#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct Petnames<'a> {
196 pub adjectives: Words<'a>,
197 pub adverbs: Words<'a>,
198 pub nouns: Words<'a>,
199}
200
201impl<'a> Petnames<'a> {
202 /// Constructs a new `Petnames` from the small word lists.
203 #[cfg(feature = "default-words")]
204 pub fn small() -> Self {
205 Self {
206 adjectives: Cow::from(&words::small::ADJECTIVES[..]),
207 adverbs: Cow::from(&words::small::ADVERBS[..]),
208 nouns: Cow::from(&words::small::NOUNS[..]),
209 }
210 }
211
212 /// Constructs a new `Petnames` from the medium word lists.
213 #[cfg(feature = "default-words")]
214 pub fn medium() -> Self {
215 Self {
216 adjectives: Cow::from(&words::medium::ADJECTIVES[..]),
217 adverbs: Cow::from(&words::medium::ADVERBS[..]),
218 nouns: Cow::from(&words::medium::NOUNS[..]),
219 }
220 }
221
222 /// Constructs a new `Petnames` from the large word lists.
223 #[cfg(feature = "default-words")]
224 pub fn large() -> Self {
225 Self {
226 adjectives: Cow::from(&words::large::ADJECTIVES[..]),
227 adverbs: Cow::from(&words::large::ADVERBS[..]),
228 nouns: Cow::from(&words::large::NOUNS[..]),
229 }
230 }
231
232 /// Constructs a new `Petnames` from the given word lists.
233 ///
234 /// The words are extracted from the given strings by splitting on whitespace.
235 pub fn new(adjectives: &'a str, adverbs: &'a str, nouns: &'a str) -> Self {
236 Self {
237 adjectives: Cow::Owned(adjectives.split_whitespace().collect()),
238 adverbs: Cow::Owned(adverbs.split_whitespace().collect()),
239 nouns: Cow::Owned(nouns.split_whitespace().collect()),
240 }
241 }
242
243 /// Keep words matching a predicate.
244 ///
245 /// # Examples
246 ///
247 /// ```rust
248 /// # use petname::Generator;
249 /// # #[cfg(feature = "default-words")]
250 /// let mut petnames = petname::Petnames::default();
251 /// # #[cfg(feature = "default-words")]
252 /// petnames.retain(|s| s.starts_with("h"));
253 /// # #[cfg(all(feature = "default-words", feature = "default-rng"))]
254 /// assert!(petnames.generate_one(2, ".").unwrap().starts_with('h'));
255 /// ```
256 ///
257 /// This is a convenience wrapper that applies the same predicate to the
258 /// adjectives, adverbs, and nouns lists.
259 ///
260 pub fn retain<F>(&mut self, mut predicate: F)
261 where
262 F: FnMut(&str) -> bool,
263 {
264 self.adjectives.to_mut().retain(|word| predicate(word));
265 self.adverbs.to_mut().retain(|word| predicate(word));
266 self.nouns.to_mut().retain(|word| predicate(word));
267 }
268
269 /// Calculate the cardinality of this `Petnames`.
270 ///
271 /// If this is low, names may be repeated by the generator with a higher
272 /// frequency than your use-case may allow.
273 ///
274 /// This can saturate. If the total possible combinations of words exceeds
275 /// `u128::MAX` then this will return `u128::MAX`.
276 pub fn cardinality(&self, words: u8) -> u128 {
277 Lists::new(words)
278 .map(|list| match list {
279 List::Adverb => self.adverbs.len() as u128,
280 List::Adjective => self.adjectives.len() as u128,
281 List::Noun => self.nouns.len() as u128,
282 })
283 .reduce(u128::saturating_mul)
284 .unwrap_or(0u128)
285 }
286}
287
288impl<'a> Generator<'a> for Petnames<'a> {
289 fn generate(&self, rng: &mut dyn rand::RngCore, words: u8, separator: &str) -> Option<String> {
290 let name = itertools::intersperse(
291 Lists::new(words).filter_map(|list| match list {
292 List::Adverb => self.adverbs.choose(rng).copied(),
293 List::Adjective => self.adjectives.choose(rng).copied(),
294 List::Noun => self.nouns.choose(rng).copied(),
295 }),
296 separator,
297 )
298 .collect::<String>();
299 if name.is_empty() {
300 None
301 } else {
302 Some(name)
303 }
304 }
305}
306
307#[cfg(feature = "default-words")]
308impl<'a> Default for Petnames<'a> {
309 /// Constructs a new [`Petnames`] from the default (medium) word lists.
310 fn default() -> Self {
311 Self::medium()
312 }
313}
314
315/// Word lists prepared for alliteration.
316#[derive(Clone, Debug, Eq, PartialEq)]
317pub struct Alliterations<'a> {
318 groups: BTreeMap<char, Petnames<'a>>,
319}
320
321impl<'a> Alliterations<'a> {
322 /// Keep only those groups that match a predicate.
323 pub fn retain<F>(&mut self, predicate: F)
324 where
325 F: FnMut(&char, &mut Petnames) -> bool,
326 {
327 self.groups.retain(predicate)
328 }
329
330 /// Calculate the cardinality of this `Alliterations`.
331 ///
332 /// This is the sum of the cardinality of all groups.
333 ///
334 /// This can saturate. If the total possible combinations of words exceeds
335 /// `u128::MAX` then this will return `u128::MAX`.
336 pub fn cardinality(&self, words: u8) -> u128 {
337 self.groups
338 .values()
339 .map(|petnames| petnames.cardinality(words))
340 .reduce(u128::saturating_add)
341 .unwrap_or(0u128)
342 }
343}
344
345impl<'a> From<Petnames<'a>> for Alliterations<'a> {
346 fn from(petnames: Petnames<'a>) -> Self {
347 let mut adjectives: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.adjectives);
348 let mut adverbs: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.adverbs);
349 let nouns: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.nouns);
350 // We find all adjectives and adverbs that start with the same letter as
351 // each group of nouns. We start from nouns because it's possible to
352 // have a petname with length of 1, i.e. a noun. This means that it's
353 // okay at this point for the adjectives and adverbs lists to be empty.
354 Alliterations {
355 groups: nouns.into_iter().fold(BTreeMap::default(), |mut acc, (first_letter, nouns)| {
356 acc.insert(
357 first_letter,
358 Petnames {
359 adjectives: adjectives.remove(&first_letter).unwrap_or_default().into(),
360 adverbs: adverbs.remove(&first_letter).unwrap_or_default().into(),
361 nouns: Cow::from(nouns),
362 },
363 );
364 acc
365 }),
366 }
367 }
368}
369
370impl<'a, GROUPS> From<GROUPS> for Alliterations<'a>
371where
372 GROUPS: IntoIterator<Item = (char, Petnames<'a>)>,
373{
374 fn from(groups: GROUPS) -> Self {
375 Self { groups: groups.into_iter().collect() }
376 }
377}
378
379fn group_words_by_first_letter(words: Words) -> BTreeMap<char, Vec<&str>> {
380 words.iter().fold(BTreeMap::default(), |mut acc, s| match s.chars().next() {
381 Some(first_letter) => {
382 acc.entry(first_letter).or_default().push(s);
383 acc
384 }
385 None => acc,
386 })
387}
388
389impl<'a> Generator<'a> for Alliterations<'a> {
390 /// Generate a new petname.
391 ///
392 /// # Examples
393 ///
394 /// ```rust
395 /// # use petname::Generator;
396 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
397 /// let mut rng = rand::thread_rng();
398 /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
399 /// petname::Petnames::default().generate(&mut rng, 7, ":");
400 /// ```
401 ///
402 /// # Notes
403 ///
404 /// This may return fewer words than you request if one or more of the word
405 /// lists are empty. For example, if there are no adverbs, requesting 3 or
406 /// more words may still yield only "doubtful-salmon".
407 ///
408 fn generate(&self, rng: &mut dyn rand::RngCore, words: u8, separator: &str) -> Option<String> {
409 self.groups.values().choose(rng).and_then(|group| group.generate(rng, words, separator))
410 }
411}
412
413#[cfg(feature = "default-words")]
414impl<'a> Default for Alliterations<'a> {
415 /// Constructs a new [`Alliterations`] from the default [`Petnames`].
416 fn default() -> Self {
417 Petnames::default().into()
418 }
419}
420
421/// Enum representing which word list to use.
422#[derive(Debug, PartialEq)]
423enum List {
424 Adverb,
425 Adjective,
426 Noun,
427}
428
429/// Iterator, yielding which word list to use next.
430///
431/// This yields the appropriate list – [adverbs][List::Adverb],
432/// [adjectives][List::Adjective]s, [nouns][List::Nouns] – from which to select
433/// a word when constructing a petname of `n` words. For example, if you want 4
434/// words in your petname, this will first yield [List::Adverb], then
435/// [List::Adverb] again, then [List::Adjective], and lastly [List::Noun].
436#[derive(Debug, PartialEq)]
437enum Lists {
438 Adverb(u8),
439 Adjective,
440 Noun,
441 Done,
442}
443
444impl Lists {
445 fn new(words: u8) -> Self {
446 match words {
447 0 => Self::Done,
448 1 => Self::Noun,
449 2 => Self::Adjective,
450 n => Self::Adverb(n - 3),
451 }
452 }
453
454 fn current(&self) -> Option<List> {
455 match self {
456 Self::Adjective => Some(List::Adjective),
457 Self::Adverb(_) => Some(List::Adverb),
458 Self::Noun => Some(List::Noun),
459 Self::Done => None,
460 }
461 }
462
463 fn advance(&mut self) {
464 *self = match self {
465 Self::Adverb(0) => Self::Adjective,
466 Self::Adverb(remaining) => Self::Adverb(*remaining - 1),
467 Self::Adjective => Self::Noun,
468 Self::Noun | Self::Done => Self::Done,
469 }
470 }
471
472 fn remaining(&self) -> usize {
473 match self {
474 Self::Adverb(n) => (n + 3) as usize,
475 Self::Adjective => 2,
476 Self::Noun => 1,
477 Self::Done => 0,
478 }
479 }
480}
481
482impl Iterator for Lists {
483 type Item = List;
484
485 fn next(&mut self) -> Option<Self::Item> {
486 let current = self.current();
487 self.advance();
488 current
489 }
490
491 fn size_hint(&self) -> (usize, Option<usize>) {
492 let remaining = self.remaining();
493 (remaining, Some(remaining))
494 }
495}
496
497/// Iterator yielding petnames.
498struct Names<'a, GENERATOR>
499where
500 GENERATOR: Generator<'a>,
501{
502 generator: &'a GENERATOR,
503 rng: &'a mut dyn rand::RngCore,
504 words: u8,
505 separator: String,
506}
507
508impl<'a, GENERATOR> Iterator for Names<'a, GENERATOR>
509where
510 GENERATOR: Generator<'a>,
511{
512 type Item = String;
513
514 fn next(&mut self) -> Option<Self::Item> {
515 self.generator.generate(self.rng, self.words, &self.separator)
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 #[test]
522 fn lists_sequences_adverbs_adjectives_then_names() {
523 let mut lists = super::Lists::new(4);
524 assert_eq!(super::Lists::Adverb(1), lists);
525 assert_eq!(Some(super::List::Adverb), lists.next());
526 assert_eq!(super::Lists::Adverb(0), lists);
527 assert_eq!(Some(super::List::Adverb), lists.next());
528 assert_eq!(super::Lists::Adjective, lists);
529 assert_eq!(Some(super::List::Adjective), lists.next());
530 assert_eq!(super::Lists::Noun, lists);
531 assert_eq!(Some(super::List::Noun), lists.next());
532 assert_eq!(super::Lists::Done, lists);
533 assert_eq!(None, lists.next());
534 }
535
536 #[test]
537 fn lists_size_hint() {
538 let mut lists = super::Lists::new(3);
539 assert_eq!((3, Some(3)), lists.size_hint());
540 assert!(lists.next().is_some());
541 assert_eq!((2, Some(2)), lists.size_hint());
542 assert!(lists.next().is_some());
543 assert_eq!((1, Some(1)), lists.size_hint());
544 assert!(lists.next().is_some());
545 assert_eq!((0, Some(0)), lists.size_hint());
546 assert_eq!(None, lists.next());
547 assert_eq!((0, Some(0)), lists.size_hint());
548 }
549}