Files
create/src/Base/UniqueNameManager.h
Kevin Martin eb3b0b9d87 Base: UniqueNameManager support for very long numbers in name (#19943)
* Add unit tests for large digit count in unique names

* Updated to use arbitrary-precision unsigneds

Passes the new unit tests, all diagnostics, and resolves Issue 19881

* Place UnlimitedUnsigned at top level and add unit tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-31 10:45:58 -05:00

248 lines
11 KiB
C++

/***************************************************************************
* Copyright (c) 2024 Kevin Martin <kpmartin@papertrail.ca> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef SRC_BASE_UNIQUENAMEMANAGER_H_
#define SRC_BASE_UNIQUENAMEMANAGER_H_
#ifndef FC_GLOBAL_H
#include <FCGlobal.h>
#endif
#include <vector>
#include <string>
#include <set>
#include <map>
#include <utility> // Forward declares std::tuple
#include <stdexcept>
#include <algorithm>
#include <tuple>
#include "UnlimitedUnsigned.h"
// ----------------------------------------------------------------------------
namespace Base
{
/// @brief
/// This class acts as a multiset of strings with the capability of generating
/// other names that are not in the collection. Unique names are generated
/// by inserting digits into a base name. Normally these are appended to the
/// base name, but a derived class can override getNameSuffixStartPosition
/// to place the digits elsewhere in the base name.
// This class does not support copy-/move-construction
// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
class BaseExport UniqueNameManager
{
protected:
// This method returns a reverse const iterator on name that references the position
// of the start of the suffix (or name.crbegin() if no suffix).
// It must return the same suffix length (name.crbegin() - returnValue) for both
// unique names (one containing digits) and the corresponding base name (with no digits).
virtual std::string::const_reverse_iterator
getNameSuffixStartPosition(const std::string& name) const
{
return name.crbegin();
}
private:
template<typename IT>
class PiecewiseSparseIntegerSet
{
public:
PiecewiseSparseIntegerSet() = default;
private:
// Each pair being <lowest, count> represents the span of integers from lowest to
// (lowest+count-1) inclusive
using SpanType = std::pair<IT, IT>;
// This span Comparer class is analogous to std::less and treats overlapping spans as being
// neither greater nor less than each other
class Comparer
{
public:
bool operator()(SpanType lhs, SpanType rhs) const
{
// The equality case here is when lhs is below and directly adjacent to rhs.
return lhs.second + lhs.first <= rhs.first;
}
};
// spans is the set of spans. Adjacent spans are coalesced so there are always gaps between
// the entries.
std::set<SpanType, Comparer> spans;
using SpanSetIterator = typename std::set<SpanType, Comparer>::iterator;
public:
void add(IT value)
{
SpanType newSpan(value, 1);
// Look for the smallest entry not less than newSpan.
// Bear in mind that overlapping spans are neither less than nor greater than ech other.
auto above = spans.lower_bound(newSpan);
if (above != spans.end() && above->first <= value) {
// A span was found that includes value so there is nothing to do as it is already
// in the set.
return;
}
// Set below to the next span below 'after', if any, otherwise to spans.end().
// Logically, we want spans.begin()-1 so 'below' is always the entry before 'after',
// but that is not an allowed reference so spans.end() is used
SpanSetIterator below;
if (above == spans.begin()) {
// No spans are less than newSpan
// (possibly spans is empty, in which case above == spans.end() too)
below = spans.end();
}
else {
// At least one span is less than newSpan,
// and 'above' is the next span above that
// (or above == spans.end() if all spans are below newSpan)
below = above;
--below;
}
// Determine whether the span above (if any) and/or the span below (if any)
// are adjacent to newSpan and if so, merge them appropriately and remove the
// original span(s) that was/were merged, updating newSpan to be the new merged
// span.
if (above != spans.end() && below != spans.end()
&& above->first - below->first + 1 == below->second) {
// below and above have a gap of exactly one between them, and this must be value
// so we coalesce the two spans (and the gap) into one.
newSpan = SpanType(below->first, below->second + above->second + 1);
spans.erase(above);
above = spans.erase(below);
}
else if (below != spans.end() && value - below->first == below->second) {
// value is adjacent to the end of below, so just expand below by one
newSpan = SpanType(below->first, below->second + 1);
above = spans.erase(below);
}
else if (above != spans.end() && above->first - value == 1) {
// value is adjacent to the start of above, so just expand above down by one
newSpan = SpanType(above->first - 1, above->second + 1);
above = spans.erase(above);
}
// else value is not adjacent to any existing span, so just make anew span for it
spans.insert(above, newSpan);
}
void remove(IT value)
{
SpanType newSpan(value, 1);
auto at = spans.lower_bound(newSpan);
if (at == spans.end() || at->first > value) {
// The found span does not include value so there is nothing to do, as it is already
// not in the set.
return;
}
if (at->second == 1) {
// value is the only in this span, just remove the span
spans.erase(at);
}
else if (at->first == value) {
// value is the first in this span, trim the lower end
SpanType replacement(at->first + 1, at->second - 1);
spans.insert(spans.erase(at), replacement);
}
else if (value - at->first == at->second - 1) {
// value is the last in this span, trim the upper end
SpanType replacement(at->first, at->second - 1);
spans.insert(spans.erase(at), replacement);
}
else {
// value is in the moddle of the span, so we must split it.
SpanType firstReplacement(at->first, value - at->first);
SpanType secondReplacement(value + 1, at->second - ((value + 1) - at->first));
// Because erase returns the iterator after the erased element, and insert returns
// the iterator for the inserted item, we want to insert secondReplacement first.
spans.insert(spans.insert(spans.erase(at), secondReplacement), firstReplacement);
}
}
bool contains(IT value) const
{
auto at = spans.lower_bound(SpanType(value, IT(1)));
return at != spans.end() && at->first <= value;
}
bool empty() const
{
return spans.empty();
}
void clear()
{
spans.clear();
}
IT next() const
{
if (spans.empty()) {
return IT(0);
}
auto last = spans.end();
--last;
return last->first + last->second;
}
};
// Keyed as uniqueSeeds[baseName][digitCount][digitValue] iff that seed is taken.
// We need the double-indexing so that Name01 and Name001 can both be indexed, although we only
// ever allocate off the longest for each name i.e. uniqueSeeds[baseName].size()-1 digits.
std::map<std::string, std::vector<PiecewiseSparseIntegerSet<UnlimitedUnsigned>>> uniqueSeeds;
// Counts of inserted strings that have duplicates, i.e. more than one instance in the
// collection. This does not contain entries for singleton names.
std::map<std::string, unsigned int> duplicateCounts;
/// @brief Break a uniquified name into its parts
/// @param name The name to break up
/// @return a tuple(basePrefix, nameSuffix, uniqueDigitCount, uniqueDigitsValue);
/// The two latter values will be (0,0) if name is a base name without uniquifying digits.
std::tuple<std::string, std::string, unsigned int, UnlimitedUnsigned>
decomposeName(const std::string& name) const;
public:
UniqueNameManager() = default;
virtual ~UniqueNameManager() = default;
/// Check if two names are unique forms of the same base name
bool haveSameBaseName(const std::string& first, const std::string& second) const;
/// Register a name in the collection.
/// This collection acts as a multiset, so multiple registrations of the same
/// name are counted, and an equal number of removals is necessary to fully
/// remove the name, allowing it to be returned by makeUniqueName
void addExactName(const std::string& name);
/// Using the given modelName as a model, generate a name that is not already
/// in the collection of names. The model name may already contain uniquifying digits
/// which will be stripped and replaced with other digits as needed.
std::string makeUniqueName(const std::string& modelName, std::size_t minDigits = 0) const;
/// Remove a registered name so it can be generated again. If the name was added
/// several times this decrements the count, and the name is only fully removed when
/// the count of removes equals or exceeds the count of adds.
void removeExactName(const std::string& name);
/// Test if the given name is already in the collection
bool containsName(const std::string& name) const;
/// @brief Empty (clear) out the contents from this collection
void clear()
{
uniqueSeeds.clear();
duplicateCounts.clear();
}
};
} // namespace Base
#endif // SRC_BASE_UNIQUENAMEMANAGER_H_