Explorar o código

Merge pull request #5317 from Laserlicht/search_inp

[1.6.4] improved text search
Ivan Savenko hai 9 meses
pai
achega
9a1c2a5800

+ 2 - 1
client/windows/CSpellWindow.cpp

@@ -41,6 +41,7 @@
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
 #include "../../lib/spells/Problem.h"
+#include "../../lib/texts/TextOperations.h"
 #include "../../lib/GameConstants.h"
 #include "../../lib/GameConstants.h"
 
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -242,7 +243,7 @@ void CSpellWindow::processSpells()
 	mySpells.reserve(CGI->spellh->objects.size());
 	mySpells.reserve(CGI->spellh->objects.size());
 	for(auto const & spell : CGI->spellh->objects)
 	for(auto const & spell : CGI->spellh->objects)
 	{
 	{
-		bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText()));
+		bool searchTextFound = !searchBox || TextOperations::textSearchSimilar(searchBox->getText(), spell->getNameTranslated());
 
 
 		if(onSpellSelect)
 		if(onSpellSelect)
 		{
 		{

+ 2 - 1
client/windows/GUIClasses.cpp

@@ -55,6 +55,7 @@
 #include "../lib/gameState/TavernHeroesPool.h"
 #include "../lib/gameState/TavernHeroesPool.h"
 #include "../lib/gameState/UpgradeInfo.h"
 #include "../lib/gameState/UpgradeInfo.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
+#include "../lib/texts/TextOperations.h"
 #include "../lib/IGameSettings.h"
 #include "../lib/IGameSettings.h"
 #include "ConditionalWait.h"
 #include "ConditionalWait.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/CConfigHandler.h"
@@ -1594,7 +1595,7 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 
 
 		itemsVisible.clear();
 		itemsVisible.clear();
 		for(auto & item : items)
 		for(auto & item : items)
-			if(boost::algorithm::contains(boost::algorithm::to_lower_copy(item.second), boost::algorithm::to_lower_copy(text)))
+			if(TextOperations::textSearchSimilar(text, item.second))
 				itemsVisible.push_back(item);
 				itemsVisible.push_back(item);
 
 
 		selected = 0;
 		selected = 0;

+ 2 - 52
lib/json/JsonValidator.cpp

@@ -17,61 +17,11 @@
 #include "../filesystem/Filesystem.h"
 #include "../filesystem/Filesystem.h"
 #include "../modding/ModScope.h"
 #include "../modding/ModScope.h"
 #include "../modding/CModHandler.h"
 #include "../modding/CModHandler.h"
+#include "../texts/TextOperations.h"
 #include "../ScopeGuard.h"
 #include "../ScopeGuard.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
-// Algorithm for detection of typos in words
-// Determines how 'different' two strings are - how many changes must be done to turn one string into another one
-// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
-static int getLevenshteinDistance(const std::string & s, const std::string & t)
-{
-	int n = t.size();
-	int m = s.size();
-
-	// create two work vectors of integer distances
-	std::vector<int> v0(n+1, 0);
-	std::vector<int> v1(n+1, 0);
-
-	// initialize v0 (the previous row of distances)
-	// this row is A[0][i]: edit distance from an empty s to t;
-	// that distance is the number of characters to append to  s to make t.
-	for (int i = 0; i < n; ++i)
-		v0[i] = i;
-
-	for (int i = 0; i < m; ++i)
-	{
-		// calculate v1 (current row distances) from the previous row v0
-
-		// first element of v1 is A[i + 1][0]
-		// edit distance is delete (i + 1) chars from s to match empty t
-		v1[0] = i + 1;
-
-		// use formula to fill in the rest of the row
-		for (int j = 0; j < n; ++j)
-		{
-			// 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])
-				substitutionCost = v0[j];
-			else
-				substitutionCost = v0[j] + 1;
-
-			v1[j + 1] = std::min({deletionCost, insertionCost, substitutionCost});
-		}
-
-		// copy v1 (current row) to v0 (previous row) for next iteration
-		// since data in v1 is always invalidated, a swap without copy could be more efficient
-		std::swap(v0, v1);
-	}
-
-	// after the last swap, the results of v1 are now in v0
-	return v0[n];
-}
-
 /// Searches for keys similar to 'target' in 'candidates' map
 /// Searches for keys similar to 'target' in 'candidates' map
 /// Returns closest match or empty string if no suitable candidates are found
 /// Returns closest match or empty string if no suitable candidates are found
 static std::string findClosestMatch(const JsonMap & candidates, const std::string & target)
 static std::string findClosestMatch(const JsonMap & candidates, const std::string & target)
@@ -84,7 +34,7 @@ static std::string findClosestMatch(const JsonMap & candidates, const std::strin
 
 
 	for (auto const & candidate : candidates)
 	for (auto const & candidate : candidates)
 	{
 	{
-		int newDistance = getLevenshteinDistance(candidate.first, target);
+		int newDistance = TextOperations::getLevenshteinDistance(candidate.first, target);
 
 
 		if (newDistance < bestDistance)
 		if (newDistance < bestDistance)
 		{
 		{

+ 70 - 0
lib/texts/TextOperations.cpp

@@ -236,5 +236,75 @@ std::string TextOperations::getCurrentFormattedDateTimeLocal(std::chrono::second
 	return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
 	return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
 }
 }
 
 
+int TextOperations::getLevenshteinDistance(const std::string & s, const std::string & t)
+{
+	int n = t.size();
+	int m = s.size();
+
+	// create two work vectors of integer distances
+	std::vector<int> v0(n+1, 0);
+	std::vector<int> v1(n+1, 0);
+
+	// initialize v0 (the previous row of distances)
+	// this row is A[0][i]: edit distance from an empty s to t;
+	// that distance is the number of characters to append to  s to make t.
+	for (int i = 0; i < n; ++i)
+		v0[i] = i;
+
+	for (int i = 0; i < m; ++i)
+	{
+		// calculate v1 (current row distances) from the previous row v0
+
+		// first element of v1 is A[i + 1][0]
+		// edit distance is delete (i + 1) chars from s to match empty t
+		v1[0] = i + 1;
+
+		// use formula to fill in the rest of the row
+		for (int j = 0; j < n; ++j)
+		{
+			// 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])
+				substitutionCost = v0[j];
+			else
+				substitutionCost = v0[j] + 1;
+
+			v1[j + 1] = std::min({deletionCost, insertionCost, substitutionCost});
+		}
+
+		// copy v1 (current row) to v0 (previous row) for next iteration
+		// since data in v1 is always invalidated, a swap without copy could be more efficient
+		std::swap(v0, v1);
+	}
+
+	// after the last swap, the results of v1 are now in v0
+	return v0[n];
+}
+
+bool TextOperations::textSearchSimilar(const std::string & s, const std::string & t)
+{
+	boost::locale::generator gen;
+	std::locale loc = gen("en_US.UTF-8"); // support for UTF8 lowercase
+	
+	auto haystack = boost::locale::to_lower(t, loc);
+	auto needle = boost::locale::to_lower(s, loc);
+
+	if(boost::algorithm::contains(haystack, needle))
+		return true;
+
+	for(int i = 0; i < haystack.size() - needle.size(); i++)
+	{
+		auto dist = getLevenshteinDistance(haystack.substr(i, needle.size()), needle);
+		if(needle.size() > 2 && dist <= 1)
+			return true;
+		else if(needle.size() > 4 && dist <= 2)
+			return true;
+	}
+	
+	return false;
+}
 
 
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 8 - 0
lib/texts/TextOperations.h

@@ -70,6 +70,14 @@ namespace TextOperations
 	/// get formatted time (without date)
 	/// get formatted time (without date)
 	/// timeOffset - optional parameter to modify current time by specified time in seconds
 	/// timeOffset - optional parameter to modify current time by specified time in seconds
 	DLL_LINKAGE std::string getCurrentFormattedTimeLocal(std::chrono::seconds timeOffset = {});
 	DLL_LINKAGE std::string getCurrentFormattedTimeLocal(std::chrono::seconds timeOffset = {});
+
+	/// Algorithm for detection of typos in words
+	/// Determines how 'different' two strings are - how many changes must be done to turn one string into another one
+	/// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
+	DLL_LINKAGE int getLevenshteinDistance(const std::string & s, const std::string & t);
+
+	/// Check if texts have similarity when typing into search boxes
+	DLL_LINKAGE bool textSearchSimilar(const std::string & s, const std::string & t);
 };
 };
 
 
 template<typename Arithmetic>
 template<typename Arithmetic>