From f1aec275a6d535e06aced84b62842ce270a08faf Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Mon, 7 Dec 2015 18:54:36 +0000 Subject: [PATCH] Improve accuracy of version extraction In particular, where multiple versions exist in a string, prefer those prefixed by "version" over others. --- src/backend/helpers/version.cpp | 76 ++++++++++++------------ src/backend/helpers/version.h | 2 +- src/tests/backend/helpers/test_version.h | 20 +++++++ 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/backend/helpers/version.cpp b/src/backend/helpers/version.cpp index 47803d7a..411ee6be 100644 --- a/src/backend/helpers/version.cpp +++ b/src/backend/helpers/version.cpp @@ -43,53 +43,51 @@ namespace loot { using namespace std; - const regex Version::versionRegex( - // The R"()" raw string literal format means the outermost brackets in - // the C strings below are not part of the regex. - // There are three separate version string matchers below: - // 1. Timestamp with forwardslash date separators. - // 2. Subset of Pseudosem-supported versions. - // 3. Single-number versions that are at the start of the search string + /* The string below matches timestamps that use forwardslashes for date + separators. However, Pseudosem v1.0.1 will only compare the first + two digits as it does not recognise forwardslashes as separators. */ + const std::string dateRegex = R"((\d{1,2}/\d{1,2}/\d{1,4} \d{1,2}:\d{1,2}:\d{1,2}))"; - R"((?:)" - // The string below matches timestamps that use forwardslashes for date - // separators. However, Pseudosem v1.0.1 will only compare the first - // two digits as it does not recognise forwardslashes as separators. - R"((\d{1,2}/\d{1,2}/\d{1,4} \d{1,2}:\d{1,2}:\d{1,2}))" - R"(|)" - // The string below only allows matches that have one of the following - // conditions: - // * are at the start of the search string - // * are preceded by a "v" - // * are preceded by a whitespace character - R"((?:^|v|\s))" - // The string below matches the range of version strings supported by - // Pseudosem v1.0.1, excluding space separators, as they make version - // extraction from inside sentences very tricky and have not been - // seen "in the wild". + /* The string below matches the range of version strings supported by + Pseudosem v1.0.1, excluding space separators, as they make version + extraction from inside sentences very tricky and have not been + seen "in the wild". */ + const std::string pseudosemVersionRegex = R"((\d+(?:\.\d+)+(?:[-._:]?[A-Za-z0-9]+)*))" - // The string below prevents numbers followed by a comma from matching. - R"((?!,))" - R"(|)" - // The string below matches a number containing one or more digits - // found at the start of the search string. - R"(^(\d+))" - R"())" - , - regex::ECMAScript | regex::icase); + // The string below prevents version numbers followed by a comma from + // matching. + R"((?!,))"; + + /* There are a few different version formats that can appear in strings + together, and in order to extract the correct one, they must be searched + for in order of priority. */ + const vector Version::versionRegexes({ + regex(dateRegex, regex::ECMAScript | regex::icase), + regex(R"(version:?\s)" + + pseudosemVersionRegex, regex::ECMAScript | regex::icase), + regex(R"((?:^|v|\s))" + + pseudosemVersionRegex, regex::ECMAScript | regex::icase), + regex( + /* The string below matches a number containing one or more digits + found at the start of the search string or preceded by 'v'. */ + R"((?:^|v)(\d+))", regex::ECMAScript | regex::icase), + }); Version::Version() {} Version::Version(const std::string& ver) { smatch what; - if (regex_search(ver, what, versionRegex)) { - for (auto it = next(begin(what)); it != end(what); ++it) { - if (it->str().empty()) - continue; + for (const auto& versionRegex : versionRegexes) { + if (regex_search(ver, what, versionRegex)) { + for (auto it = next(begin(what)); it != end(what); ++it) { + if (it->str().empty()) + continue; - //Use the first non-empty sub-match. - verString = *it; - boost::trim(verString); + //Use the first non-empty sub-match. + verString = *it; + boost::trim(verString); + return; + } } } } diff --git a/src/backend/helpers/version.h b/src/backend/helpers/version.h index 21301543..37728bfd 100644 --- a/src/backend/helpers/version.h +++ b/src/backend/helpers/version.h @@ -47,7 +47,7 @@ namespace loot { bool operator != (const Version&) const; private: std::string verString; - static const std::regex versionRegex; + static const std::vector versionRegexes; }; } diff --git a/src/tests/backend/helpers/test_version.h b/src/tests/backend/helpers/test_version.h index 6b6c20b4..ee687dc8 100644 --- a/src/tests/backend/helpers/test_version.h +++ b/src/tests/backend/helpers/test_version.h @@ -144,6 +144,26 @@ namespace loot { EXPECT_EQ("", Version(testText).AsString()); } + TEST(Version, shouldPreferVersionPrefixedNumbersOverVersionsInSentence) { + // Found in + std::string testText("Requires Skyrim patch 1.9.32.0.8 or greater.\n" + "Requires Unofficial Skyrim Legendary Edition Patch 3.0.0 or greater.\n" + "Version 2.0.0"); + EXPECT_EQ("2.0.0", Version(testText).AsString()); + } + + TEST(Version, shouldExtractSingleDigitVersionPrecededByV) { + // Found in + std::string testText("Immersive Armors v8 Main Plugin"); + EXPECT_EQ("8", Version(testText).AsString()); + } + + TEST(Version, shouldPreferVersionPrefixedNumbersOverVPrefixedNumber) { + // Found in + std::string testText("Compatibility patch for AOS v2.5 and True Storms v1.5 (or later),\nPatch Version: 1.0"); + EXPECT_EQ("1.0", Version(testText).AsString()); + } + TEST(Version, GreaterThan) { Version version1, version2; EXPECT_FALSE(version1 > version2);