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}