Skip to content

Commit

Permalink
Remove the non-repeating iterator
Browse files Browse the repository at this point in the history
Fixes #61. This issue also documents the problems that led to this, thoughts
about how it could be resurrected, and how to work around it if this is
something you were using.
  • Loading branch information
allenap committed Sep 12, 2023
1 parent 3c9ffc1 commit 1cde0a1
Show file tree
Hide file tree
Showing 5 changed files with 1 addition and 225 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Options:
-d, --dir <DIR> Use custom word lists by specifying a directory containing `adjectives.txt`, `adverbs.txt`, and `nouns.txt`
--count <COUNT> Generate multiple names; or use --stream to generate continuously [default: 1]
--stream Stream names continuously
--non-repeating Do not generate the same name more than once
-l, --letters <LETTERS> Maximum number of letters in each word; 0 for unlimited [default: 0]
-a, --alliterate Generate names where each word begins with the same letter
-A, --alliterate-with <LETTER> Generate names where each word begins with the given letter
Expand Down
4 changes: 0 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ pub struct Cli {
#[arg(long, conflicts_with = "count")]
pub stream: bool,

/// Do not generate the same name more than once
#[arg(long)]
pub non_repeating: bool,

/// Maximum number of letters in each word; 0 for unlimited
#[arg(short, long, value_name = "LETTERS", default_value_t = 0)]
pub letters: usize,
Expand Down
152 changes: 0 additions & 152 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ extern crate alloc;
use alloc::{
borrow::Cow,
string::{String, ToString},
vec::Vec,
};

use itertools::Itertools;
Expand Down Expand Up @@ -252,40 +251,6 @@ impl<'a> Petnames<'a> {
{
Names { petnames: self, rng, words, separator: separator.to_string() }
}

/// Iterator yielding unique – i.e. non-repeating – petnames.
///
/// # Examples
///
/// ```rust
/// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
/// let mut rng = rand::thread_rng();
/// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
/// let petnames = petname::Petnames::default();
/// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
/// let mut iter = petnames.iter_non_repeating(&mut rng, 4, "_");
/// # #[cfg(all(feature = "default-rng", feature = "default-words"))]
/// println!("name: {}", iter.next().unwrap());
/// ```
///
pub fn iter_non_repeating<RNG>(
&'a self,
rng: &'a mut RNG,
words: u8,
separator: &str,
) -> impl Iterator<Item = String> + 'a
where
RNG: rand::Rng,
{
let lists: Vec<&'a Words<'a>> = Lists::new(words)
.map(|list| match list {
List::Adverb => &self.adverbs,
List::Adjective => &self.adjectives,
List::Noun => &self.nouns,
})
.collect();
NamesProduct::shuffled(&lists, rng, separator)
}
}

#[cfg(feature = "default-words")]
Expand Down Expand Up @@ -394,123 +359,6 @@ where
}
}

/// Iterator yielding petnames from the product of given word lists.
///
/// This can be used to ensure that only unique names are produced.
struct NamesProduct<'a, ITERATOR>
where
ITERATOR: Iterator<Item = Option<&'a str>>,
{
iters: Vec<(ITERATOR, Option<&'a str>)>,
separator: String,
capacity: usize,
size: Option<usize>,
}

impl<'a> NamesProduct<'a, core::iter::Cycle<alloc::vec::IntoIter<Option<&'a str>>>> {
/// Shuffles each of the given `lists` with `rng`, then cycles through the
/// product of the lists, joining with `separator`. The leftmost list will
/// cycle most rapidly.
fn shuffled<RNG>(lists: &[&'a Words<'a>], rng: &'a mut RNG, separator: &str) -> Self
where
RNG: rand::Rng,
{
NamesProduct {
iters: lists
.iter()
.map(|words| {
let mut list: Vec<Option<&'a str>> = Vec::with_capacity(words.len().saturating_add(1));
list.extend(words.iter().map(|word| Some(*word)));
list.shuffle(rng); // Could be expensive.
list.push(None); // Cycle marker.
(list.into_iter().cycle(), None)
})
.collect(),
separator: separator.to_string(),
capacity: Self::capacity(lists, separator),
size: match lists {
[] => Some(0),
ls => ls.iter().try_fold(1usize, |acc, list| acc.checked_mul(list.len())),
},
}
}

fn capacity(lists: &[&'a Words<'a>], separator: &str) -> usize {
(
// Sum of the length of the longest possible word in each word list.
lists
.iter()
.filter_map(|words| words.iter().map(|word| word.len()).max())
.fold(0usize, |sum, len| sum.saturating_add(len))
// The total length of all separators. Careful not to wrap usize.
+ (separator.len().saturating_mul(lists.len().saturating_sub(1)))
)
// Things run _much_ quicker when the capacity is a power of 2. Memory
// alignment? If so it may be enough to align at, say, 8 bytes, but this
// works for now.
.checked_next_power_of_two()
// In case there are no lists, or they're all empty... or we have
// calculated that we need more than usize::MAX capacity.
.unwrap_or(0)
}
}

impl<'a, ITERATOR> Iterator for NamesProduct<'a, ITERATOR>
where
ITERATOR: Iterator<Item = Option<&'a str>>,
{
type Item = String;

fn size_hint(&self) -> (usize, Option<usize>) {
(self.size.unwrap_or(0), self.size)
}

fn next(&mut self) -> Option<Self::Item> {
let mut bump = true; // Request advance of next iterator.
for (iter, word) in self.iters.iter_mut() {
if bump || word.is_none() {
match iter.next() {
None => {
// This shouldn't happen because we expect the iterators
// to cycle. However, if it does, we're definitely done.
return None;
}
Some(None) => {
// This is the cycle end marker. We want to get another
// new word from this iterator, and advance the *next*
// iterator too.
match iter.next() {
None => return None,
Some(None) => return None,
Some(s) => *word = s,
}
bump = true
}
Some(s) => {
// We have a new word from this iterator, so we do not
// yet need to advance the next iterator.
*word = s;
bump = false
}
}
}
}
if bump {
// We reached the end of the last iterator, hence we're done.
None
} else {
// Keep track of the number of names remaining.
self.size = self.size.map(|s| s.saturating_sub(1));
// We may be able to construct a name!
self.iters.iter().try_fold(String::with_capacity(self.capacity), |acc, (_, w)| match (acc, *w) {
(s, Some(w)) if s.is_empty() => Some(s + w),
(s, Some(w)) => Some(s + &self.separator + w),
_ => None,
})
}
}
}

#[cfg(test)]
mod tests {
#[test]
Expand Down
6 changes: 1 addition & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,7 @@ fn run(cli: Cli) -> Result<(), Error> {
let count = if cli.stream { None } else { Some(cli.count) };

// Get an iterator for the names we want to print out.
if cli.non_repeating {
printer(&mut writer, petnames.iter_non_repeating(&mut rng, cli.words, &cli.separator), count)
} else {
printer(&mut writer, petnames.iter(&mut rng, cli.words, &cli.separator), count)
}
printer(&mut writer, petnames.iter(&mut rng, cli.words, &cli.separator), count)
}

fn printer<OUT, NAMES>(writer: &mut OUT, names: NAMES, count: Option<usize>) -> Result<(), Error>
Expand Down
63 changes: 0 additions & 63 deletions tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,66 +77,3 @@ fn petnames_iter_yields_names() {
let mut iter: Box<dyn Iterator<Item = _>> = Box::new(names);
assert_eq!(Some("bar.foo.baz".to_string()), iter.next());
}

#[test]
fn petnames_iter_non_repeating_yields_unique_names() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "b1 b2 b3", "c1 c2");
let names: Vec<String> = petnames.iter_non_repeating(&mut rng, 3, ".").collect();
assert_eq!(
vec![
"b2.a2.c2", "b3.a2.c2", "b1.a2.c2", "b2.a1.c2", "b3.a1.c2", "b1.a1.c2", "b2.a2.c1", "b3.a2.c1",
"b1.a2.c1", "b2.a1.c1", "b3.a1.c1", "b1.a1.c1"
],
names
)
}

#[test]
fn petnames_iter_non_repeating_provides_size_hint() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "b1 b2 b3", "c1 c2");
let iter = petnames.iter_non_repeating(&mut rng, 3, ".");
assert_eq!((12, Some(12)), iter.size_hint());
assert_eq!((9, Some(9)), iter.skip(3).size_hint());
}

#[test]
fn petnames_iter_non_repeating_provides_size_hint_that_saturates() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "b1 b2", "c1 c2");
let iter = petnames.iter_non_repeating(&mut rng, 3, ".");
assert_eq!((0, Some(0)), iter.skip(10).size_hint());
}

#[test]
fn petnames_iter_non_repeating_provides_size_hint_that_is_zero_when_any_list_is_empty() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("", "b1 b2", "c1 c2");
let iter = petnames.iter_non_repeating(&mut rng, 3, ".");
assert_eq!((0, Some(0)), iter.size_hint());
}

#[test]
fn petnames_iter_non_repeating_provides_size_hint_that_is_zero_when_no_word_lists_are_given() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "b1 b2", "c1 c2");
let iter = petnames.iter_non_repeating(&mut rng, 0, ".");
assert_eq!((0, Some(0)), iter.size_hint());
}

#[test]
fn petnames_iter_non_repeating_yields_nothing_when_any_word_list_is_empty() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "", "c1 c2");
let names: Vec<String> = petnames.iter_non_repeating(&mut rng, 3, ".").collect();
assert_eq!(Vec::<String>::new(), names);
}

#[test]
fn petnames_iter_non_repeating_yields_nothing_when_no_word_lists_are_given() {
let mut rng = StepRng::new(0, 1);
let petnames = Petnames::new("a1 a2", "b1 b2", "c1 c2");
let names: Vec<String> = petnames.iter_non_repeating(&mut rng, 0, ".").collect();
assert_eq!(Vec::<String>::new(), names);
}

0 comments on commit 1cde0a1

Please sign in to comment.