mirror of
https://github.com/vcmi/vcmi.git
synced 2025-11-25 22:42:04 +02:00
Fixes to locale/unicode handling
- Fixes crash on opening spellbook (recent regression) - Fix crash if `haystack` string is shorter than `needle` - Levenstein distance computation is now case-insensitive - Text similarity computation now assumes utf-8 strings, not ascii - Fix corruption of utf-8 string on life drain
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
|
||||
#include <vstd/DateUtils.h>
|
||||
|
||||
#include <boost/locale.hpp>
|
||||
#include <boost/locale/encoding.hpp>
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
@@ -97,7 +97,7 @@ bool TextOperations::isValidASCII(const char * data, size_t size)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextOperations::isValidUnicodeString(const std::string & text)
|
||||
bool TextOperations::isValidUnicodeString(std::string_view text)
|
||||
{
|
||||
for (size_t i=0; i<text.size(); i += getUnicodeCharacterSize(text[i]))
|
||||
{
|
||||
@@ -210,7 +210,7 @@ void TextOperations::trimRightUnicode(std::string & text, const size_t amount)
|
||||
}
|
||||
}
|
||||
|
||||
size_t TextOperations::getUnicodeCharactersCount(const std::string & text)
|
||||
size_t TextOperations::getUnicodeCharactersCount(std::string_view text)
|
||||
{
|
||||
size_t charactersCount = 0;
|
||||
|
||||
@@ -253,10 +253,65 @@ std::string TextOperations::getCurrentFormattedDateTimeLocal(std::chrono::second
|
||||
return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
|
||||
}
|
||||
|
||||
static const std::locale & getLocale()
|
||||
{
|
||||
auto getLocale = []() -> std::locale
|
||||
{
|
||||
const std::string & baseLocaleName = Languages::getLanguageOptions(LIBRARY->generaltexth->getPreferredLanguage()).localeName;
|
||||
const std::string fallbackLocale = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).localeName;
|
||||
|
||||
for (const auto & localeName : { baseLocaleName + ".UTF-8", baseLocaleName, fallbackLocale + ".UTF-8", fallbackLocale })
|
||||
{
|
||||
try
|
||||
{
|
||||
// Locale generation may fail (and throw an exception) in two cases:
|
||||
// - if the corresponding locale is not installed on the system
|
||||
// - on Android named locales are not supported at all and always throw an exception
|
||||
return std::locale(localeName);
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
logGlobal->warn("Failed to set locale '%s'", localeName);
|
||||
}
|
||||
}
|
||||
return std::locale();
|
||||
};
|
||||
|
||||
static const std::locale locale = getLocale();
|
||||
return locale;
|
||||
}
|
||||
|
||||
int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view t)
|
||||
{
|
||||
int n = t.size();
|
||||
int m = s.size();
|
||||
assert(isValidUnicodeString(s));
|
||||
assert(isValidUnicodeString(t));
|
||||
|
||||
auto charactersEqual = [&s, &t](int sPoint, int tPoint)
|
||||
{
|
||||
uint32_t sUTF32 = getUnicodeCodepoint(s.data() + sPoint, s.size() - sPoint);
|
||||
uint32_t tUTF32 = getUnicodeCodepoint(t.data() + tPoint, t.size() - tPoint);
|
||||
|
||||
if (sUTF32 == tUTF32)
|
||||
return true;
|
||||
|
||||
// Windows - wchar_t represents UTF-16 symbol that does not cover entire Unicode
|
||||
// In UTF-16 such characters can only be represented as 2 wchar_t's, but toupper can only operate on single wchar
|
||||
// Assume symbols are different if one of them cannot be represented as a single UTF-16 symbol
|
||||
if constexpr (sizeof(wchar_t) == 2)
|
||||
{
|
||||
if (sUTF32 > 0xFFFF || (sUTF32 >= 0xD800 && sUTF32 <= 0xDFFF ))
|
||||
return false;
|
||||
|
||||
if (tUTF32 > 0xFFFF || (tUTF32 >= 0xD800 && tUTF32 <= 0xDFFF ))
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto & facet = std::use_facet<std::ctype<wchar_t>>(getLocale());
|
||||
return facet.toupper(sUTF32) == facet.toupper(tUTF32);
|
||||
};
|
||||
|
||||
int n = getUnicodeCharactersCount(t);
|
||||
int m = getUnicodeCharactersCount(s);
|
||||
|
||||
// create two work vectors of integer distances
|
||||
std::vector<int> v0(n+1, 0);
|
||||
@@ -268,8 +323,9 @@ int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view
|
||||
for (int i = 0; i < n; ++i)
|
||||
v0[i] = i;
|
||||
|
||||
for (int i = 0; i < m; ++i)
|
||||
for (int i = 0, iPoint = 0; i < m; ++i, iPoint += getUnicodeCharacterSize(s[iPoint]))
|
||||
{
|
||||
|
||||
// calculate v1 (current row distances) from the previous row v0
|
||||
|
||||
// first element of v1 is A[i + 1][0]
|
||||
@@ -277,14 +333,14 @@ int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view
|
||||
v1[0] = i + 1;
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for (int j = 0; j < n; ++j)
|
||||
for (int j = 0, jPoint = 0; j < n; ++j, jPoint += getUnicodeCharacterSize(t[jPoint]))
|
||||
{
|
||||
// calculating costs for A[i + 1][j + 1]
|
||||
int deletionCost = v0[j + 1] + 1;
|
||||
int insertionCost = v1[j] + 1;
|
||||
int substitutionCost;
|
||||
|
||||
if (s[i] == t[j])
|
||||
if (charactersEqual(iPoint, jPoint))
|
||||
substitutionCost = v0[j];
|
||||
else
|
||||
substitutionCost = v0[j] + 1;
|
||||
@@ -301,45 +357,17 @@ int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view
|
||||
return v0[n];
|
||||
}
|
||||
|
||||
DLL_LINKAGE const std::locale & TextOperations::getLocale()
|
||||
{
|
||||
static std::locale loc;
|
||||
|
||||
const std::string & localeName = Languages::getLanguageOptions(LIBRARY->generaltexth->getPreferredLanguage()).localeName;
|
||||
try
|
||||
{
|
||||
loc = std::locale(localeName); // might fail on Android
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
logGlobal->warn("Failed to set locale '%s'. Falling back to 'en_US.UTF-8'", localeName);
|
||||
try
|
||||
{
|
||||
loc = std::locale("en_US.UTF-8");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
logGlobal->warn("Fallback locale 'en_US.UTF-8' failed. Using default 'C' locale.");
|
||||
loc = std::locale::classic();
|
||||
}
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
DLL_LINKAGE bool TextOperations::compareLocalizedStrings(std::string_view str1, std::string_view str2)
|
||||
{
|
||||
static const std::collate<char> & col = std::use_facet<std::collate<char>>(getLocale());
|
||||
return col.compare(str1.data(), str1.data() + str1.size(),
|
||||
str2.data(), str2.data() + str2.size()) < 0;
|
||||
const std::collate<char> & col = std::use_facet<std::collate<char>>(getLocale());
|
||||
return col.compare(
|
||||
str1.data(), str1.data() + str1.size(),
|
||||
str2.data(), str2.data() + str2.size()
|
||||
) < 0;
|
||||
}
|
||||
|
||||
std::optional<int> TextOperations::textSearchSimilarityScore(const std::string & s, const std::string & t)
|
||||
std::optional<int> TextOperations::textSearchSimilarityScore(const std::string & needle, const std::string & haystack)
|
||||
{
|
||||
static const std::collate<char> & col = std::use_facet<std::collate<char>>(getLocale());
|
||||
|
||||
auto haystack = col.transform(t.data(), t.data() + t.size());
|
||||
auto needle = col.transform(s.data(), s.data() + s.size());
|
||||
|
||||
// 0 - Best possible match: text starts with the search string
|
||||
if(haystack.rfind(needle, 0) == 0)
|
||||
return 0;
|
||||
@@ -349,13 +377,24 @@ std::optional<int> TextOperations::textSearchSimilarityScore(const std::string &
|
||||
return 1;
|
||||
|
||||
// Dynamic threshold: Reject if too many typos based on word length
|
||||
int maxAllowedDistance = std::max(2, static_cast<int>(needle.size() / 2));
|
||||
int haystackCodepoints = getUnicodeCharactersCount(haystack);
|
||||
int needleCodepoints = getUnicodeCharactersCount(needle);
|
||||
int maxAllowedDistance = needleCodepoints / 2;
|
||||
|
||||
// Compute Levenshtein distance for fuzzy similarity
|
||||
int minDist = std::numeric_limits<int>::max();
|
||||
for(size_t i = 0; i <= haystack.size() - needle.size(); i++)
|
||||
|
||||
for(int i = 0; i <= haystackCodepoints - needleCodepoints; ++i)
|
||||
{
|
||||
int dist = getLevenshteinDistance(haystack.substr(i, needle.size()), needle);
|
||||
int haystackBegin = 0;
|
||||
for(int j = 0; j < i; ++j)
|
||||
haystackBegin += getUnicodeCharacterSize(haystack[haystackBegin]);
|
||||
|
||||
int haystackEnd = haystackBegin;
|
||||
for(int j = 0; j < needleCodepoints; ++j)
|
||||
haystackEnd += getUnicodeCharacterSize(haystack[haystackEnd]);
|
||||
|
||||
int dist = getLevenshteinDistance(haystack.substr(haystackBegin, haystackEnd - haystackBegin), needle);
|
||||
minDist = std::min(minDist, dist);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user