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}