Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions framework/global/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ target_sources(muse_global PRIVATE
functional/inplace_function_mv.h
dataformatter.cpp
dataformatter.h
stringsearch.cpp
stringsearch.h
stringutils.cpp
stringutils.h
ptrutils.h
Expand Down
183 changes: 183 additions & 0 deletions framework/global/stringsearch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2026 MuseScore Limited and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "stringsearch.h"

#include <algorithm>

bool muse::operator==(const FuzzyMatch& left, const FuzzyMatch& right)
{
return left.beginPos == right.beginPos
&& left.endPos == right.endPos
&& left.editDistance == right.editDistance;
}

bool muse::operator!=(const FuzzyMatch& left, const FuzzyMatch& right)
{
return !(left == right);
}

namespace muse {
FuzzyMatcher& FuzzyMatcher::operator()(const std::u32string_view text, const std::u32string_view pattern, const std::size_t maxDistance)
{
return match(text, pattern, maxDistance);
}

FuzzyMatcher& FuzzyMatcher::match(const std::u32string_view text, const std::u32string_view pattern, const std::size_t maxDistance)
{
m_textSize = text.size();
m_patternSize = pattern.size();
m_suffixTable.clear();
m_suffixTable.resize((m_patternSize + 1) * (m_textSize + 1));
m_maxDistance = maxDistance;

for (std::size_t textSuffixSize = 0; textSuffixSize <= m_textSize; ++textSuffixSize) {
const std::size_t endPos = text.size() - textSuffixSize;
// 0 insertions to transform empty pattern to text suffix.
// This allows the match to be at any position in text.
m_suffixTable[index(0, textSuffixSize)] = SuffixMatch{ endPos, 0 };
}

for (std::size_t patternSuffixSize = 1; patternSuffixSize <= m_patternSize; ++patternSuffixSize) {
// patternSuffixSize deletions to transform pattern suffix to empty text
m_suffixTable[index(patternSuffixSize, 0)] = SuffixMatch{ 0, patternSuffixSize };

const std::size_t patternIdx = pattern.size() - patternSuffixSize;
for (std::size_t textSuffixSize = 1; textSuffixSize <= m_textSize; ++textSuffixSize) {
const std::size_t textIdx = text.size() - textSuffixSize;

if (pattern[patternIdx] == text[textIdx]) {
m_suffixTable[index(patternSuffixSize, textSuffixSize)] = m_suffixTable[index(patternSuffixSize - 1, textSuffixSize - 1)];
continue;
}

const SuffixMatch insertMatch = m_suffixTable[index(patternSuffixSize, textSuffixSize - 1)];
const SuffixMatch replaceMatch = m_suffixTable[index(patternSuffixSize - 1, textSuffixSize - 1)];
const SuffixMatch deleteMatch = m_suffixTable[index(patternSuffixSize - 1, textSuffixSize)];
SuffixMatch minMatch = std::min(insertMatch, std::min(replaceMatch, deleteMatch));

if (patternSuffixSize > 1 && textSuffixSize > 1
&& pattern[patternIdx] == text[textIdx + 1]
&& pattern[patternIdx + 1] == text[textIdx]) {
const SuffixMatch transpositionMatch = m_suffixTable[index(patternSuffixSize - 2, textSuffixSize - 2)];
minMatch = std::min(transpositionMatch, minMatch);
}

m_suffixTable[index(patternSuffixSize, textSuffixSize)] = SuffixMatch{ minMatch.endPos, minMatch.editDistance + 1 };
}
}

return *this;
}

std::optional<FuzzyMatch> FuzzyMatcher::findMatch(const std::size_t textBeginPos) const
{
if (m_suffixTable.empty() || textBeginPos > m_textSize) {
return std::nullopt;
}

for (std::size_t beginPos = textBeginPos; beginPos <= m_textSize; ++beginPos) {
const std::size_t textSuffixSize = m_textSize - beginPos;
const SuffixMatch match = m_suffixTable[index(m_patternSize, textSuffixSize)];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (match.editDistance <= m_maxDistance) {
return FuzzyMatch{ beginPos, match.endPos, match.editDistance };
}
}

return std::nullopt;
}

bool FuzzyMatcher::empty() const
{
return !findMatch(0).has_value();
}

void FuzzyMatcher::clear()
{
m_textSize = 0;
m_patternSize = 0;
m_suffixTable.clear();
}

FuzzyMatcher::const_iterator FuzzyMatcher::begin() const
{
return Iterator(*this);
}

FuzzyMatcher::const_iterator FuzzyMatcher::end() const
{
return Iterator();
}

std::size_t FuzzyMatcher::index(const std::size_t patternSuffixSize, const std::size_t textSuffixSize) const
{
return (m_textSize + 1) * patternSuffixSize + textSuffixSize;
}

FuzzyMatcher::Iterator::Iterator(const FuzzyMatcher& matcher)
: m_matcher{&matcher}
{
next(0);
}

FuzzyMatcher::Iterator::reference FuzzyMatcher::Iterator::operator*() const
{
return *m_match;
}

FuzzyMatcher::Iterator::pointer FuzzyMatcher::Iterator::operator->() const
{
return m_match.operator->();
}

FuzzyMatcher::Iterator& FuzzyMatcher::Iterator::operator++()
{
next(m_match->beginPos + 1);
return *this;
}

FuzzyMatcher::Iterator FuzzyMatcher::Iterator::operator++(int)
{
Iterator prev = *this;
operator++();

return prev;
}

bool FuzzyMatcher::Iterator::operator==(const Iterator& right) const
{
return m_matcher == right.m_matcher && m_match == right.m_match;
}

bool FuzzyMatcher::Iterator::operator!=(const Iterator& right) const
{
return !(*this == right);
}

void FuzzyMatcher::Iterator::next(const std::size_t textBeginPos)
{
m_match = m_matcher->findMatch(textBeginPos);
if (!m_match) {
m_matcher = nullptr;
}
}
}
109 changes: 109 additions & 0 deletions framework/global/stringsearch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2026 MuseScore Limited and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#pragma once

#include <cstddef>
#include <iterator>
#include <optional>
#include <string_view>
#include <vector>

namespace muse {
struct FuzzyMatch {
std::size_t beginPos = 0;
std::size_t endPos = 0;
std::size_t editDistance = 0;
};

bool operator==(const FuzzyMatch&, const FuzzyMatch&);
bool operator!=(const FuzzyMatch&, const FuzzyMatch&);

class FuzzyMatcher
{
public:
using value_type = FuzzyMatch;

static constexpr auto UNLIMITED_DISTANCE = static_cast<std::size_t>(-1);

FuzzyMatcher() = default;

FuzzyMatcher& operator()(std::u32string_view text, std::u32string_view pattern, std::size_t maxDistance = UNLIMITED_DISTANCE);
FuzzyMatcher& match(std::u32string_view text, std::u32string_view pattern, std::size_t maxDistance = UNLIMITED_DISTANCE);

bool empty() const;
std::optional<FuzzyMatch> findMatch(std::size_t textBeginPos) const;

void clear();

class Iterator
{
public:
using difference_type = std::ptrdiff_t;
using value_type = FuzzyMatcher::value_type;
using pointer = const value_type*;
using reference = const value_type&;
using iterator_category = std::input_iterator_tag;

Iterator() = default;
explicit Iterator(const FuzzyMatcher&);

reference operator*() const;
pointer operator->() const;

Iterator& operator++();
Iterator operator++(int);

bool operator==(const Iterator&) const;
bool operator!=(const Iterator&) const;

private:
void next(std::size_t textBeginPos);

const FuzzyMatcher* m_matcher = nullptr;
std::optional<FuzzyMatch> m_match;
};

using const_iterator = Iterator;

const_iterator begin() const;
const_iterator end() const;

private:
struct SuffixMatch {
std::size_t endPos = 0;
std::size_t editDistance = 0;

friend bool operator<(const SuffixMatch& left, const SuffixMatch& right)
{
return left.editDistance < right.editDistance;
}
};

std::size_t index(std::size_t patternSuffixSize, std::size_t textSuffixSize) const;

std::size_t m_textSize = 0;
std::size_t m_patternSize = 0;
std::size_t m_maxDistance = 0;
std::vector<SuffixMatch> m_suffixTable;
};
}
69 changes: 2 additions & 67 deletions framework/global/stringutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "stringutils.h"

#include <cctype>
#include <algorithm>
#include <cctype>

bool muse::strings::replace(std::string& str, const std::string& from, const std::string& to)
{
Expand All @@ -34,72 +35,6 @@ bool muse::strings::replace(std::string& str, const std::string& from, const std
return true;
}

namespace {
void split_simple(const std::string& str, std::vector<std::string>& out, const std::string& delim)
{
if (delim.empty()) {
out.push_back(str);
return;
}

std::size_t current, previous = 0;
current = str.find(delim);
std::size_t delimLen = delim.length();

while (current != std::string::npos) {
out.push_back(str.substr(previous, current - previous));
previous = current + delimLen;
current = str.find(delim, previous);
}
out.push_back(str.substr(previous, current - previous));
}

//! Split that handles an escape character, which causes the following escape or separator to be emitted verbatim.
void split_handle_escape(const std::string& str, std::vector<std::string>& out, const std::string& delim, const std::string& esc)
{
const size_t delimLen = delim.size();
const size_t escLen = esc.size();
std::string current;

for (size_t i = 0; i < str.size();) {
if (escLen > 0 && str.compare(i, escLen, esc) == 0) {
// Escaped sequence: drop the escape, then emit the following escape
// or separator token literally (mirrors what join() produced). Any
// other (or dangling) escape consumes a single following character.
i += escLen;
if (str.compare(i, escLen, esc) == 0) {
current += esc;
i += escLen;
} else if (str.compare(i, delimLen, delim) == 0) {
current += delim;
i += delimLen;
} else if (i < str.size()) {
current += str[i];
++i;
}
} else if (delimLen > 0 && str.compare(i, delimLen, delim) == 0) {
out.push_back(current);
current.clear();
i += delimLen;
} else {
current += str[i];
++i;
}
}
out.push_back(current);
}
}

void muse::strings::split(const std::string& str, std::vector<std::string>& out, const std::string& delim,
const std::string& esc)
{
if (esc.empty()) {
split_simple(str, out, delim);
} else {
split_handle_escape(str, out, delim, esc);
}
}

namespace {
std::string join_simple(const std::vector<std::string>& strs, const std::string& sep)
{
Expand Down
Loading