Skip to main content

petname/
lib.rs

1#![no_std]
2//!
3//! [`petname()`] will generate a single name with a default random number
4//! generator:
5//!
6//! ```rust
7//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
8//! let name: Option<String> = petname::petname(3, "-");
9//! // e.g. deftly-apt-swiftlet
10//! ```
11//!
12//! You can bring your own random number generator from [rand][]:
13//!
14//! ```rust
15//! # #[cfg(feature = "default-rng")] {
16//! let mut rng = rand::rngs::ThreadRng::default();
17//! # #[cfg(feature = "default-words")] {
18//! let petnames = petname::Petnames::default();
19//! let name = petnames.namer(7, ":").iter(&mut rng).next().expect("no names");
20//! # } }
21//! ```
22//!
23//! See that call to [`namer`][`Petnames::namer`] above? It returned a
24//! [`Namer`]. Calling [`iter`][`Namer::iter`] on that gives a standard
25//! [`Iterator`]. This is more efficient than calling [`petname()`] repeatedly,
26//! plus you get all the features of Rust iterators:
27//!
28//! ```rust
29//! # #[cfg(feature = "default-rng")]
30//! let mut rng = rand::rngs::ThreadRng::default();
31//! # #[cfg(feature = "default-words")]
32//! let petnames = petname::Petnames::default();
33//! # #[cfg(all(feature = "default-rng", feature = "default-words"))]
34//! let ten_thousand_names: Vec<String> =
35//!   petnames.namer(3, "_").iter(&mut rng).take(10000).collect();
36//! ```
37//!
38//! 💡 Even more efficient but slightly less convenient is
39//! [`Namer::generate_into`].
40//!
41//! # Word lists
42//!
43//! You can populate [`Petnames`] with your own word lists at runtime, but the
44//! word lists from upstream [petname][] are included with the `default-words`
45//! feature (which is enabled by default). See [`Petnames::small`],
46//! [`Petnames::medium`], and [`Petnames::large`] to select a particular
47//! built-in word list, or use [`Petnames::default`].
48//!
49//! ## Embedding your own word lists
50//!
51//! The [`petnames!`] macro will statically embed your own word lists at
52//! compile-time. This is available with the `macros` feature (enabled by
53//! default). This same mechanism is used to embed the default word lists.
54//!
55//! [petname]: https://github.com/dustinkirkland/petname
56//!
57//! ## Basic filtering
58//!
59//! You can modify the word lists to, for example, only use words beginning with
60//! the letter "b":
61//!
62//! ```rust
63//! # #[cfg(feature = "default-words")] {
64//! let mut petnames = petname::Petnames::default();
65//! petnames.retain(|s| s.starts_with("b"));
66//! # #[cfg(feature = "default-rng")] {
67//! let name = petnames.namer(3, ".").iter(&mut rand::rng()).next().expect("no names");
68//! assert!(name.starts_with('b'));
69//! # } }
70//! ```
71//!
72//! ## Alliterating
73//!
74//! There is another way to generate alliterative petnames, useful in particular
75//! when you don't need or want each name to be limited to using the same
76//! initial letter as the previous generated name. Create the `Petnames` as
77//! before, and then convert it into an [`Alliterations`]:
78//!
79//! ```rust
80//! # #[cfg(feature = "default-words")] {
81//! let mut petnames = petname::Petnames::default();
82//! let mut alliterations: petname::Alliterations = petnames.into();
83//! # #[cfg(feature = "default-rng")]
84//! alliterations.namer(3, "/").iter(&mut rand::rng()).next().expect("no names");
85//! # }
86//! ```
87//!
88//! # The [`Generator`] trait
89//!
90//! Both [`Petnames`] and [`Alliterations`] implement [`Generator`]. It's
91//! [object-safe] so you can use them as trait objects:
92//!
93//! [object-safe]:
94//!     https://doc.rust-lang.org/reference/items/traits.html#object-safety
95//!
96//! ```rust
97//! use petname::Generator;
98//! let mut buf = String::new();
99//! # #[cfg(all(feature = "default-words", feature = "default-rng"))] {
100//! let petnames: &dyn Generator = &petname::Petnames::default();
101//! petnames.generate_into(&mut buf, &mut rand::rng(), 3, "/");
102//! let alliterations: &dyn Generator = &petname::Alliterations::default();
103//! alliterations.generate_into(&mut buf, &mut rand::rng(), 3, "/");
104//! # }
105//! ```
106//!
107
108extern crate alloc;
109
110#[cfg(feature = "macros")]
111extern crate self as petname;
112
113use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec::Vec};
114
115use rand::seq::{IndexedRandom, IteratorRandom};
116
117/// Convenience function to generate a new petname from default word lists.
118#[allow(dead_code)]
119#[cfg(all(feature = "default-rng", feature = "default-words"))]
120pub fn petname(words: u8, separator: &str) -> Option<String> {
121    Petnames::default().namer(words, separator).iter(&mut rand::rng()).next()
122}
123
124/// A word list.
125pub type Words<'a> = Cow<'a, [&'a str]>;
126
127// Re-export proc macro.
128#[cfg(feature = "macros")]
129pub use petname_macros::petnames;
130
131/// Trait that defines a generator of petnames, as consumed by [`Namer`].
132///
133/// The sole required method is [`generate_into`][`Self::generate_into`].
134///
135/// This trait is [object-safe] so you can use implementors as trait objects.
136///
137/// [object-safe]:
138///     https://doc.rust-lang.org/reference/items/traits.html#object-safety
139///
140pub trait Generator {
141    /// Generate a petname into a given [`String`] buffer.
142    ///
143    /// This method does not clear the buffer. The generated name is pushed at
144    /// the end of the string. The name _may_ contain fewer words than requested
145    /// if one or more of the word lists are empty.
146    ///
147    fn generate_into(&self, buf: &mut String, rng: &mut dyn rand::Rng, words: u8, separator: &str);
148}
149
150/// A configured petname generator.
151///
152/// Created by [`Petnames::namer`] or [`Alliterations::namer`]. Holds a
153/// reference to a word list, a word count, and a separator. Call
154/// [`iter`][`Self::iter`] to get an [`Iterator`] over generated names, or
155/// [`generate_into`][`Self::generate_into`] to write into a buffer directly.
156///
157/// # Examples
158///
159/// ```rust
160/// # #[cfg(all(feature = "default-rng", feature = "default-words"))] {
161/// let petnames = petname::Petnames::default();
162/// let namer = petnames.namer(3, "-");
163///
164/// // As an iterator:
165/// let names: Vec<String> = namer.iter(&mut rand::rng()).take(10).collect();
166///
167/// // Or writing into a buffer:
168/// let mut buf = String::new();
169/// namer.generate_into(&mut buf, &mut rand::rng());
170/// # }
171/// ```
172///
173pub struct Namer<'a, G: ?Sized> {
174    generator: &'a G,
175    words: u8,
176    separator: &'a str,
177}
178
179impl<'a, G: Generator + ?Sized> Namer<'a, G> {
180    /// Generate a petname into a given [`String`] buffer.
181    ///
182    /// This can be more efficient than [`iter`][`Self::iter`] when generating
183    /// many names because the buffer can be reused; each name yielded by
184    /// [`iter`][`Self::iter`] allocates a new `String`.
185    ///
186    /// This method does not clear the buffer. The generated name is pushed at
187    /// the end of the string.
188    ///
189    /// # Examples
190    ///
191    /// ```rust
192    /// # #[cfg(all(feature = "default-rng", feature = "default-words"))] {
193    /// let petnames = petname::Petnames::default();
194    /// let namer = petnames.namer(7, "::");
195    /// let mut buf = String::new();
196    /// namer.generate_into(&mut buf, &mut rand::rng());
197    /// assert_eq!(7, buf.split("::").count());
198    /// # }
199    /// ```
200    ///
201    /// When looping you might want to check if the buffer has been modified or
202    /// not. An unmodified buffer might mean that the source of names or
203    /// randomness has been exhausted.
204    ///
205    /// ```rust
206    /// # #[cfg(all(feature = "default-rng", feature = "default-words"))] {
207    /// let petnames = petname::Petnames::default();
208    /// let namer = petnames.namer(3, "+");
209    /// let mut buf = String::new();
210    /// loop {
211    ///     namer.generate_into(&mut buf, &mut rand::rng());
212    ///     if buf.is_empty() {
213    ///         break;  // Source exhausted?
214    ///     } else {
215    ///         println!("Petname: {buf}");
216    ///         buf.clear();  // Reset before next iteration.
217    ///         # break;
218    ///     }
219    /// }
220    /// # }
221    /// ```
222    ///
223    pub fn generate_into(&self, buf: &mut String, rng: &mut dyn rand::Rng) {
224        self.generator.generate_into(buf, rng, self.words, self.separator);
225    }
226
227    /// Iterator yielding petnames.
228    ///
229    /// Note that a new [`String`] is allocated for each name yielded. If this
230    /// is a problem, consider [`generate_into`][`Self::generate_into`] instead.
231    ///
232    /// # Examples
233    ///
234    /// ```rust
235    /// # #[cfg(all(feature = "default-rng", feature = "default-words"))] {
236    /// let petnames = petname::Petnames::default();
237    /// let mut rng = rand::rngs::ThreadRng::default();
238    /// let mut namer = petnames.namer(4, "_");
239    /// println!("name: {}", namer.iter(&mut rng).next().unwrap());
240    /// # }
241    /// ```
242    pub fn iter<'b>(&'b self, rng: &'b mut dyn rand::Rng) -> impl Iterator<Item = String> + 'b {
243        core::iter::from_fn(move || {
244            let mut buf = String::new();
245            self.generate_into(&mut buf, rng);
246            (!buf.is_empty()).then_some(buf)
247        })
248    }
249}
250
251/// Word lists and the logic to combine them into _petnames_.
252///
253/// A _petname_ with `n` words will contain, in order:
254///
255///   * `n - 2` adverbs when `n >= 2`, otherwise 0 adverbs.
256///   * 1 adjective when `n >= 2`, otherwise 0 adjectives.
257///   * 1 noun when `n >= 1`, otherwise 0 nouns.
258///
259#[derive(Clone, Debug, Eq, PartialEq)]
260pub struct Petnames<'a> {
261    pub adjectives: Words<'a>,
262    pub adverbs: Words<'a>,
263    pub nouns: Words<'a>,
264}
265
266impl<'a> Petnames<'a> {
267    /// Constructs a new `Petnames` from the small word lists.
268    #[cfg(feature = "default-words")]
269    pub fn small() -> Self {
270        petnames!("words/small")
271    }
272
273    /// Constructs a new `Petnames` from the medium word lists.
274    #[cfg(feature = "default-words")]
275    pub fn medium() -> Self {
276        petnames!("words/medium")
277    }
278
279    /// Constructs a new `Petnames` from the large word lists.
280    #[cfg(feature = "default-words")]
281    pub fn large() -> Self {
282        petnames!("words/large")
283    }
284
285    /// Constructs a new `Petnames` from the given word lists.
286    ///
287    /// The words are extracted from the given strings by splitting on whitespace.
288    pub fn new(adjectives: &'a str, adverbs: &'a str, nouns: &'a str) -> Self {
289        Self {
290            adjectives: Cow::Owned(adjectives.split_whitespace().collect()),
291            adverbs: Cow::Owned(adverbs.split_whitespace().collect()),
292            nouns: Cow::Owned(nouns.split_whitespace().collect()),
293        }
294    }
295
296    /// Keep words matching a predicate.
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// # #[cfg(feature = "default-words")] {
302    /// let mut petnames = petname::Petnames::default();
303    /// petnames.retain(|s| s.starts_with("h"));
304    /// # #[cfg(feature = "default-rng")]
305    /// assert!(petnames.namer(2, ".").iter(&mut rand::rng()).next().unwrap().starts_with('h'));
306    /// # }
307    /// ```
308    ///
309    /// This is a convenience wrapper that applies the same predicate to the
310    /// adjectives, adverbs, and nouns lists.
311    ///
312    pub fn retain<F>(&mut self, mut predicate: F)
313    where
314        F: FnMut(&str) -> bool,
315    {
316        self.adjectives.to_mut().retain(|word| predicate(word));
317        self.adverbs.to_mut().retain(|word| predicate(word));
318        self.nouns.to_mut().retain(|word| predicate(word));
319    }
320
321    /// Calculate the cardinality of this `Petnames`.
322    ///
323    /// If this is low, names may be repeated by the generator with a higher
324    /// frequency than your use-case may allow.
325    ///
326    /// This can saturate. If the total possible combinations of words exceeds
327    /// `u128::MAX` then this will return `u128::MAX`.
328    pub fn cardinality(&self, words: u8) -> u128 {
329        Lists::new(words)
330            .map(|list| match list {
331                List::Adverb => self.adverbs.len() as u128,
332                List::Adjective => self.adjectives.len() as u128,
333                List::Noun => self.nouns.len() as u128,
334            })
335            .reduce(u128::saturating_mul)
336            .unwrap_or(0u128)
337    }
338
339    /// Create a [`Namer`] that generates petnames from these word lists.
340    ///
341    /// # Examples
342    ///
343    /// ```rust
344    /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
345    /// let name = petname::Petnames::default()
346    ///     .namer(3, "-")
347    ///     .iter(&mut rand::rng())
348    ///     .next()
349    ///     .expect("no names");
350    /// ```
351    pub fn namer<'b>(&'b self, words: u8, separator: &'b str) -> Namer<'b, Self> {
352        Namer { generator: self, words, separator }
353    }
354}
355
356impl Generator for Petnames<'_> {
357    fn generate_into(&self, buf: &mut String, rng: &mut dyn rand::Rng, words: u8, separator: &str) {
358        for list in Lists::new(words) {
359            match list {
360                List::Adverb => {
361                    if let Some(word) = self.adverbs.choose(rng).copied() {
362                        buf.push_str(word);
363                        buf.push_str(separator);
364                    }
365                }
366                List::Adjective => {
367                    if let Some(word) = self.adjectives.choose(rng).copied() {
368                        buf.push_str(word);
369                        buf.push_str(separator);
370                    }
371                }
372                List::Noun => {
373                    if let Some(word) = self.nouns.choose(rng).copied() {
374                        buf.push_str(word);
375                    }
376                }
377            };
378        }
379    }
380}
381
382#[cfg(feature = "default-words")]
383impl Default for Petnames<'_> {
384    /// Constructs a new [`Petnames`] from the default (medium) word lists.
385    fn default() -> Self {
386        Self::medium()
387    }
388}
389
390/// Word lists prepared for alliteration.
391///
392/// Construct from a [`Petnames`] with [`Alliterations::from`]. This takes that
393/// instance and splits it into several _groups_. In each, all of the nouns,
394/// adverbs, and adjectives will start with the same letter. A name generated
395/// from any of them will naturally produce an alliterative petname.
396///
397/// You can also create one of these from an iterable of `(char, Petnames)`.
398/// This might be useful for testing, or for repurposing this to generate names
399/// with assonance, say.
400///
401#[derive(Clone, Debug, Eq, PartialEq)]
402pub struct Alliterations<'a> {
403    groups: BTreeMap<char, Petnames<'a>>,
404}
405
406impl Alliterations<'_> {
407    /// Keep only those groups that match a predicate.
408    ///
409    /// A _group_ is defined by a [`char`] and a corresponding [`Petnames`]
410    /// instance.
411    ///
412    /// The given predicate can return `true` to keep the group or `false` to
413    /// evict it. It can also mutate each `Petnames` instance. The notional
414    /// invariant is that every noun, adverb, and adjective in that `Petnames`
415    /// instance should start with that `char`, but it's okay to break that.
416    ///
417    pub fn retain<F>(&mut self, predicate: F)
418    where
419        F: FnMut(&char, &mut Petnames) -> bool,
420    {
421        self.groups.retain(predicate)
422    }
423
424    /// Calculate the cardinality of this `Alliterations`.
425    ///
426    /// This is the sum of the cardinality of all groups.
427    ///
428    /// This can saturate. If the total possible combinations of words exceeds
429    /// `u128::MAX` then this will return `u128::MAX`.
430    pub fn cardinality(&self, words: u8) -> u128 {
431        self.groups
432            .values()
433            .map(|petnames| petnames.cardinality(words))
434            .reduce(u128::saturating_add)
435            .unwrap_or(0u128)
436    }
437
438    /// Create a [`Namer`] that generates alliterative petnames from these word
439    /// lists.
440    ///
441    /// # Examples
442    ///
443    /// ```rust
444    /// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
445    /// let name = petname::Alliterations::default()
446    ///     .namer(3, "-")
447    ///     .iter(&mut rand::rng())
448    ///     .next()
449    ///     .expect("no names");
450    /// ```
451    pub fn namer<'b>(&'b self, words: u8, separator: &'b str) -> Namer<'b, Self> {
452        Namer { generator: self, words, separator }
453    }
454}
455
456impl<'a> From<Petnames<'a>> for Alliterations<'a> {
457    fn from(petnames: Petnames<'a>) -> Self {
458        let mut adjectives: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.adjectives);
459        let mut adverbs: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.adverbs);
460        let nouns: BTreeMap<char, Vec<&str>> = group_words_by_first_letter(petnames.nouns);
461        // We find all adjectives and adverbs that start with the same letter as
462        // each group of nouns. We start from nouns because it's possible to
463        // have a petname with length of 1, i.e. a noun. This means that it's
464        // okay at this point for the adjectives and adverbs lists to be empty.
465        Alliterations {
466            groups: nouns.into_iter().fold(BTreeMap::default(), |mut acc, (first_letter, nouns)| {
467                acc.insert(
468                    first_letter,
469                    Petnames {
470                        adjectives: adjectives.remove(&first_letter).unwrap_or_default().into(),
471                        adverbs: adverbs.remove(&first_letter).unwrap_or_default().into(),
472                        nouns: Cow::from(nouns),
473                    },
474                );
475                acc
476            }),
477        }
478    }
479}
480
481impl<'a, GROUPS> From<GROUPS> for Alliterations<'a>
482where
483    GROUPS: IntoIterator<Item = (char, Petnames<'a>)>,
484{
485    fn from(groups: GROUPS) -> Self {
486        Self { groups: groups.into_iter().collect() }
487    }
488}
489
490fn group_words_by_first_letter(words: Words<'_>) -> BTreeMap<char, Vec<&str>> {
491    words.iter().fold(BTreeMap::default(), |mut acc, s| match s.chars().next() {
492        Some(first_letter) => {
493            acc.entry(first_letter).or_default().push(s);
494            acc
495        }
496        None => acc,
497    })
498}
499
500impl Generator for Alliterations<'_> {
501    fn generate_into(&self, buf: &mut String, rng: &mut dyn rand::Rng, words: u8, separator: &str) {
502        if let Some(group) = self.groups.values().choose(rng) {
503            group.generate_into(buf, rng, words, separator);
504        }
505    }
506}
507
508#[cfg(feature = "default-words")]
509impl Default for Alliterations<'_> {
510    /// Constructs a new [`Alliterations`] from the default [`Petnames`].
511    fn default() -> Self {
512        Petnames::default().into()
513    }
514}
515
516/// Enum representing which word list to use.
517#[derive(Debug, PartialEq)]
518enum List {
519    Adverb,
520    Adjective,
521    Noun,
522}
523
524/// Iterator, yielding which word list to use next.
525///
526/// This yields the appropriate list – [adverbs][List::Adverb],
527/// [adjectives][List::Adjective], [nouns][List::Noun] – from which to select a
528/// word when constructing a petname of `n` words. For example, if you want 4
529/// words in your petname, this will first yield [List::Adverb], then
530/// [List::Adverb] again, then [List::Adjective], and lastly [List::Noun].
531#[derive(Debug, PartialEq)]
532enum Lists {
533    Adverb(u8),
534    Adjective,
535    Noun,
536    Done,
537}
538
539impl Lists {
540    fn new(words: u8) -> Self {
541        match words {
542            0 => Self::Done,
543            1 => Self::Noun,
544            2 => Self::Adjective,
545            n => Self::Adverb(n - 3),
546        }
547    }
548
549    fn current(&self) -> Option<List> {
550        match self {
551            Self::Adjective => Some(List::Adjective),
552            Self::Adverb(_) => Some(List::Adverb),
553            Self::Noun => Some(List::Noun),
554            Self::Done => None,
555        }
556    }
557
558    fn advance(&mut self) {
559        *self = match self {
560            Self::Adverb(0) => Self::Adjective,
561            Self::Adverb(remaining) => Self::Adverb(*remaining - 1),
562            Self::Adjective => Self::Noun,
563            Self::Noun | Self::Done => Self::Done,
564        }
565    }
566
567    fn remaining(&self) -> usize {
568        match self {
569            Self::Adverb(n) => (n + 3) as usize,
570            Self::Adjective => 2,
571            Self::Noun => 1,
572            Self::Done => 0,
573        }
574    }
575}
576
577impl Iterator for Lists {
578    type Item = List;
579
580    fn next(&mut self) -> Option<Self::Item> {
581        let current = self.current();
582        self.advance();
583        current
584    }
585
586    fn size_hint(&self) -> (usize, Option<usize>) {
587        let remaining = self.remaining();
588        (remaining, Some(remaining))
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    #[test]
595    fn lists_sequences_adverbs_adjectives_then_names() {
596        let mut lists = super::Lists::new(4);
597        assert_eq!(super::Lists::Adverb(1), lists);
598        assert_eq!(Some(super::List::Adverb), lists.next());
599        assert_eq!(super::Lists::Adverb(0), lists);
600        assert_eq!(Some(super::List::Adverb), lists.next());
601        assert_eq!(super::Lists::Adjective, lists);
602        assert_eq!(Some(super::List::Adjective), lists.next());
603        assert_eq!(super::Lists::Noun, lists);
604        assert_eq!(Some(super::List::Noun), lists.next());
605        assert_eq!(super::Lists::Done, lists);
606        assert_eq!(None, lists.next());
607    }
608
609    #[test]
610    fn lists_size_hint() {
611        let mut lists = super::Lists::new(3);
612        assert_eq!((3, Some(3)), lists.size_hint());
613        assert!(lists.next().is_some());
614        assert_eq!((2, Some(2)), lists.size_hint());
615        assert!(lists.next().is_some());
616        assert_eq!((1, Some(1)), lists.size_hint());
617        assert!(lists.next().is_some());
618        assert_eq!((0, Some(0)), lists.size_hint());
619        assert_eq!(None, lists.next());
620        assert_eq!((0, Some(0)), lists.size_hint());
621    }
622}