pytools_rs/
unique_name_generator.rs

1// Copyright (c) 2022 Kaushik Kulkarni
2// Copyright (c) 2009-2013 Andreas Kloeckner (for original code in pytools)
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22use lazy_static::lazy_static;
23use regex::Regex;
24use std::collections::{HashMap, HashSet};
25use std::iter::IntoIterator;
26use std::str::FromStr;
27use std::string::ToString;
28
29lazy_static! {
30    static ref RE_COUNTER_MATCH: Regex =
31        Regex::new(r"^(?P<based_on>\w+)_(?P<counter>\d+)$").unwrap();
32}
33
34/// Generates names that are based on a prefix and avoids collisions with names
35/// already taken.
36pub struct UniqueNameGenerator {
37    /// Names that are already taken i.e. would never be generated again.
38    pub existing_names: HashSet<String>,
39    prefix_to_counter: HashMap<String, u32>,
40}
41
42impl UniqueNameGenerator {
43    /// Returns `true` only if `name` is present in
44    /// [`UniqueNameGenerator::existing_names`].
45    pub fn is_name_conflicting<T: ToString>(&self, name: T) -> bool {
46        return self.existing_names.contains(&name.to_string());
47    }
48    /// Records `name` into the set of existing names.
49    pub fn add_name<T: ToString>(&mut self, name: T) {
50        if self.is_name_conflicting(name.to_string()) {
51            panic!("Name '{}' conflicts with existing names.", name.to_string())
52        }
53        self.existing_names.insert(name.to_string());
54    }
55    /// Calls [`UniqueNameGenerator::add_name`] on each name in `names`.
56    pub fn add_names<I: ToString, T: IntoIterator<Item = I>>(&mut self, names: T) {
57        for name in names.into_iter() {
58            self.add_name(name.to_string());
59        }
60    }
61    /// Returns a name prefixed with `based_on` that is not present in
62    /// ['UniqueNameGenerator::existing_names`].
63    pub fn get<T: ToString>(&mut self, based_on: T) -> String {
64        let new_based_on_x_counter: Option<(String, u32)> = match self.prefix_to_counter
65                                                                      .get(&based_on.to_string())
66        {
67            Some(x) => Some((based_on.to_string(), *x)),
68            None => {
69                if !RE_COUNTER_MATCH.is_match(&(based_on.to_string()[..])) {
70                    None
71                } else {
72                    let based_on_string = based_on.to_string();
73                    let captures_iter = RE_COUNTER_MATCH.captures_iter(based_on_string.as_str());
74                    let captures_iter_vec: Vec<_> = captures_iter.collect();
75                    assert_eq!(captures_iter_vec.len(), 1); // this should be a single match
76                    let capture = &captures_iter_vec[0];
77                    Some((capture.name("based_on").unwrap().as_str().to_string(),
78                          u32::from_str(capture.name("counter").unwrap().as_str()).unwrap()))
79                }
80            }
81        };
82
83        let unique_name = match new_based_on_x_counter {
84            None => {
85                self.prefix_to_counter.insert(based_on.to_string(), 0);
86                format!("{}", based_on.to_string())
87            }
88            Some((based_on, counter)) => {
89                let mut icounter = counter;
90                while self.is_name_conflicting(format!("{}_{}", based_on.to_string(), icounter)) {
91                    icounter += 1;
92                }
93                self.prefix_to_counter
94                    .insert(based_on.to_string(), icounter + 1);
95                format!("{}_{}", based_on.to_string(), icounter)
96            }
97        };
98
99        self.add_name(unique_name.clone());
100        return unique_name;
101    }
102}
103
104/// Returns a new [`UniqueNameGenerator`].
105///
106/// # Arguments
107/// * existing_names - An iterator of names that have already been taken and
108///   illegal
109/// for [`UniqueNameGenerator`] to generate.
110pub fn make_unique_name_gen<T: IntoIterator<Item = String>>(existing_names: T)
111                                                            -> UniqueNameGenerator {
112    UniqueNameGenerator { existing_names: existing_names.into_iter().collect(),
113                          prefix_to_counter: HashMap::new() }
114}