// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "multi_rename.h" #include using namespace zen; using namespace fff; namespace { std::wstring_view findLongestSubstring(const std::vector& strings) { if (strings.empty()) return {}; const std::wstring_view strMin = *std::min_element(strings.begin(), strings.end(), /**/[](const std::wstring_view lhs, const std::wstring_view rhs) { return lhs.size() < rhs.size(); }); for (size_t sz = strMin.size(); sz > 0; --sz) //iterate over size, descending for (size_t i = 0; i + sz <= strMin.size(); ++i) { const std::wstring_view substr(strMin.data() + i, sz); //perf: duplicate substrings, especially für size = 1? const bool isCommon = [&] { for (const std::wstring_view str : strings) if (str.data() != strMin.data()) //sufficient check: an extension of strMin necessarily contains "substr" anyway if (!contains(str, substr)) return false; return true; }(); if (isCommon) return substr; //*first* occuring substring of maximum size } return {}; } struct StringPart { std::vector diff; //may be empty, but only at beginning (or between filename/extension parts) std::wstring_view common; //may be empty, but only at end (dito) }; std::vector getStringParts(std::vector&& strings) { std::wstring_view substr = findLongestSubstring(strings); if (!substr.empty()) { std::vector head; std::vector tail; for (const std::wstring_view str : strings) { head.push_back(beforeFirst(str, substr, IfNotFoundReturn::none)); tail.push_back(afterFirst (str, substr, IfNotFoundReturn::none)); } std::vector np = getStringParts(std::move(head)); assert(np.empty() || np.back().common.empty()); //otherwise we could construct an even longer substring! if (np.empty()) np.push_back({{}, substr}); else np.back().common = substr; const std::vector npTail = getStringParts(std::move(tail)); assert(npTail.empty() || !npTail.front().diff.empty()); //otherwise we could construct an even longer substring! append(np, npTail); return np; } else { if (std::all_of(strings.begin(), strings.end(), [](const std::wstring_view str) { return str.empty(); })) /**/return {}; return {{std::move(strings), {}}}; } } constexpr wchar_t placeholders[] = //http://xahlee.info/comp/unicode_circled_numbers.html { //L'\u24FF', //⓿ <- rendered bigger than the rest (same for ⓫) on Centos Linux L'\u2776', //❶ L'\u2777', //❷ L'\u2778', //❸ L'\u2779', //❹ L'\u277A', //❺ L'\u277B', //❻ L'\u277C', //❼ L'\u277D', //❽ L'\u277E', //❾ L'\u277F', //❿ -> last one is special: represents "all the rest" }; inline size_t getPlaceholderIndex(wchar_t c) { static_assert(std::size(placeholders) == 10); if (placeholders[0] <= c && c <= placeholders[9]) return static_cast(c - placeholders[0]); return static_cast(-1); } } bool fff::isRenamePlaceholderChar(wchar_t c) { return getPlaceholderIndex(c) < std::size(placeholders); } struct fff::RenameBuf { explicit RenameBuf(const std::vector& s) : strings(s) { //file extensions deserve special treatment: https://freefilesync.org/forum/viewtopic.php?t=11943#p46453 std::vector names; std::vector extensions; //including "." for (const std::wstring& fileName : strings) { auto it = findLast(fileName.begin(), fileName.end(), L'.'); names. push_back(makeStringView(fileName.begin(), it)); extensions.push_back(makeStringView(it, fileName.end())); } parts = getStringParts(std::move(names)); append(parts, getStringParts(std::move(extensions))); } std::vector strings; std::vector parts; }; //e.g. "Season ❶, Episode ❷ - ❸.avi" std::pair> fff::getPlaceholderPhrase(const std::vector& strings) { auto renameBuf = makeSharedRef(strings); std::wstring phrase; size_t placeIdx = 0; for (const StringPart& p : renameBuf.ref().parts) { if (!p.diff.empty()) { phrase += placeholders[placeIdx++]; if (placeIdx >= std::size(placeholders)) break; //represent "all the rest" with last placeholder } phrase += p.common; //TODO: what if common part incidentally contains placeholder character!? } return {phrase, renameBuf}; } const std::vector fff::resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf) { std::vector*> diffByIdx; for (const StringPart& p : buf.parts) if (!p.diff.empty()) diffByIdx.push_back(&p.diff), assert(p.diff.size() == buf.strings.size()); std::vector output; for (size_t i = 0; i < buf.strings.size(); ++i) { std::wstring resolved; for (const wchar_t c : phrase) if (const size_t placeIdx = getPlaceholderIndex(c); placeIdx < diffByIdx.size()) { if (placeIdx == std::size(placeholders) - 1) //last placeholder represents "all the rest" resolved.append((*diffByIdx[placeIdx])[i].data(), buf.strings[i].data() + buf.strings[i].size()); else resolved += (*diffByIdx[placeIdx])[i]; } else resolved += c; output.push_back(std::move(resolved)); } return output; }