Skip to main content

petname/
lib.rs

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