Refactor beast::SemanticVersion (RIPD-199)

This commit is contained in:
Nik Bougalis
2014-08-06 23:33:20 -07:00
parent bf403a6142
commit 0857c6350d
2 changed files with 300 additions and 227 deletions

View File

@@ -18,9 +18,131 @@
//==============================================================================
#include <beast/unit_test/suite.h>
#include <beast/module/core/text/LexicalCast.h>
#include <algorithm>
namespace beast {
std::string print_identifiers (SemanticVersion::identifier_list const& list)
{
std::string ret;
for (auto const& x : list)
{
if (!ret.empty ())
ret += ".";
ret += x;
}
return ret;
}
bool isNumeric (std::string const& s)
{
int n;
// Must be convertible to an integer
if (!lexicalCastChecked (n, s))
return false;
// Must not have leading zeroes
return std::to_string (n) == s;
}
bool chop (std::string const& what, std::string& input)
{
auto ret = input.find (what);
if (ret != 0)
return false;
input.erase (0, what.size ());
return true;
}
bool chopUInt (int& value, int limit, std::string& input)
{
// Must not be empty
if (input.empty ())
return false;
auto left_iter = std::find_if_not (input.begin (), input.end (),
[](std::string::value_type c)
{
return std::isdigit (c, std::locale::classic());
});
std::string item (input.begin (), left_iter);
// Must not be empty
if (item.empty ())
return false;
int n;
// Must be convertible to an integer
if (!lexicalCastChecked (n, item))
return false;
// Must not have leading zeroes
if (std::to_string (n) != item)
return false;
// Must not be out of range
if (n < 0 || n > limit)
return false;
input.erase (input.begin (), left_iter);
value = n;
return true;
}
bool extract_identifier (
std::string& value, bool allowLeadingZeroes, std::string& input)
{
// Must not be empty
if (input.empty ())
return false;
// Must not have a leading 0
if (!allowLeadingZeroes && input [0] == '0')
return false;
auto last = input.find_first_not_of (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-");
// Must not be empty
if (last == 0)
return false;
value = input.substr (0, last);
input.erase (0, last);
return true;
}
bool extract_identifiers (
SemanticVersion::identifier_list& identifiers,
bool allowLeadingZeroes,
std::string& input)
{
if (input.empty ())
return false;
do {
std::string s;
if (!extract_identifier (s, allowLeadingZeroes, input))
return false;
identifiers.push_back (s);
} while (chop (".", input));
return true;
}
//------------------------------------------------------------------------------
SemanticVersion::SemanticVersion ()
: majorVersion (0)
, minorVersion (0)
@@ -28,108 +150,138 @@ SemanticVersion::SemanticVersion ()
{
}
bool SemanticVersion::parse (String input)
SemanticVersion::SemanticVersion (std::string const& version)
: SemanticVersion ()
{
if (!parse (version))
throw std::invalid_argument ("invalid version string");
}
bool SemanticVersion::parse (std::string const& input, bool debug)
{
// May not have leading or trailing whitespace
if (input.trim () != input)
auto left_iter = std::find_if_not (input.begin (), input.end (),
[](std::string::value_type c)
{
return std::isspace (c, std::locale::classic());
});
auto right_iter = std::find_if_not (input.rbegin (), input.rend (),
[](std::string::value_type c)
{
return std::isspace (c, std::locale::classic());
}).base ();
// Must not be empty!
if (left_iter >= right_iter)
return false;
std::string version (left_iter, right_iter);
// May not have leading or trailing whitespace
if (version != input)
return false;
// Must have major version number
if (! chopUInt (&majorVersion, std::numeric_limits <int>::max (), input))
if (! chopUInt (majorVersion, std::numeric_limits <int>::max (), version))
return false;
if (! chop (".", input))
if (! chop (".", version))
return false;
// Must have minor version number
if (! chopUInt (&minorVersion, std::numeric_limits <int>::max (), input))
if (! chopUInt (minorVersion, std::numeric_limits <int>::max (), version))
return false;
if (! chop (".", input))
if (! chop (".", version))
return false;
// Must have patch version number
if (! chopUInt (&patchVersion, std::numeric_limits <int>::max (), input))
if (! chopUInt (patchVersion, std::numeric_limits <int>::max (), version))
return false;
// May have pre-release identifier list
if (chop ("-", input))
if (chop ("-", version))
{
chopIdentifiers (&preReleaseIdentifiers, false, input);
if (!extract_identifiers (preReleaseIdentifiers, false, version))
return false;
// Must not be empty
if (preReleaseIdentifiers.size () <= 0)
if (preReleaseIdentifiers.empty ())
return false;
}
// May have metadata identifier list
if (chop ("+", input))
if (chop ("+", version))
{
chopIdentifiers (&metaData, true, input);
if (!extract_identifiers (metaData, true, version))
return false;
// Must not be empty
if (metaData.size () <= 0)
if (metaData.empty ())
return false;
}
// May not have anything left
if (input.length () > 0)
return false;
return true;
return version.empty ();
}
String SemanticVersion::print () const
std::string SemanticVersion::print () const
{
String s;
std::string s;
s << String (majorVersion) << "." << String (minorVersion) << "." << String (patchVersion);
s = std::to_string (majorVersion) + "." +
std::to_string (minorVersion) + "." +
std::to_string (patchVersion);
if (preReleaseIdentifiers.size () > 0)
s << "-" << printIdentifiers (preReleaseIdentifiers);
if (!preReleaseIdentifiers.empty ())
{
s += "-";
s += print_identifiers (preReleaseIdentifiers);
}
if (metaData.size () > 0)
s << "+" << printIdentifiers (metaData);
if (!metaData.empty ())
{
s += "+";
s += print_identifiers (metaData);
}
return s;
}
int SemanticVersion::compare (SemanticVersion const& rhs) const noexcept
int compare (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
if (majorVersion > rhs.majorVersion)
if (lhs.majorVersion > rhs.majorVersion)
return 1;
else if (majorVersion < rhs.majorVersion)
else if (lhs.majorVersion < rhs.majorVersion)
return -1;
if (minorVersion > rhs.minorVersion)
if (lhs.minorVersion > rhs.minorVersion)
return 1;
else if (minorVersion < rhs.minorVersion)
else if (lhs.minorVersion < rhs.minorVersion)
return -1;
if (patchVersion > rhs.patchVersion)
if (lhs.patchVersion > rhs.patchVersion)
return 1;
else if (patchVersion < rhs.patchVersion)
else if (lhs.patchVersion < rhs.patchVersion)
return -1;
if (isPreRelease () || rhs.isPreRelease ())
if (lhs.isPreRelease () || rhs.isPreRelease ())
{
// Pre-releases have a lower precedence
if (isRelease () && rhs.isPreRelease ())
if (lhs.isRelease () && rhs.isPreRelease ())
return 1;
else if (isPreRelease () && rhs.isRelease ())
else if (lhs.isPreRelease () && rhs.isRelease ())
return -1;
// Compare pre-release identifiers
for (int i = 0; i < bmax (preReleaseIdentifiers.size (), rhs.preReleaseIdentifiers.size ()); ++i)
for (int i = 0; i < bmax (lhs.preReleaseIdentifiers.size (), rhs.preReleaseIdentifiers.size ()); ++i)
{
// A larger list of identifiers has a higher precedence
if (i >= rhs.preReleaseIdentifiers.size ())
return 1;
else if (i >= preReleaseIdentifiers.size ())
else if (i >= lhs.preReleaseIdentifiers.size ())
return -1;
String const& left (preReleaseIdentifiers [i]);
String const& right (rhs.preReleaseIdentifiers [i]);
std::string const& left (lhs.preReleaseIdentifiers [i]);
std::string const& right (rhs.preReleaseIdentifiers [i]);
// Numeric identifiers have lower precedence
if (! isNumeric (left) && isNumeric (right))
@@ -141,8 +293,8 @@ int SemanticVersion::compare (SemanticVersion const& rhs) const noexcept
{
bassert (isNumeric (right));
int const iLeft (left.getIntValue ());
int const iRight (right.getIntValue ());
int const iLeft (lexicalCastThrow <int> (left));
int const iRight (lexicalCastThrow <int> (right));
if (iLeft > iRight)
return 1;
@@ -153,7 +305,7 @@ int SemanticVersion::compare (SemanticVersion const& rhs) const noexcept
{
bassert (! isNumeric (right));
int result = left.compareLexicographically (right);
int result = left.compare (right);
if (result != 0)
return result;
@@ -166,133 +318,14 @@ int SemanticVersion::compare (SemanticVersion const& rhs) const noexcept
return 0;
}
bool SemanticVersion::isNumeric (String const& s)
{
return String (s.getIntValue ()) == s;
}
bool SemanticVersion::chop (String const& what, String& input)
{
if (input.startsWith (what))
{
input = input.substring (what.length ());
return true;
}
return false;
}
String SemanticVersion::printIdentifiers (StringArray const& list)
{
String s;
if (list.size () > 0)
{
s << list [0];
for (int i = 1; i < list.size (); ++i)
s << "." << list [i];
}
return s;
}
bool SemanticVersion::chopUInt (int* value, int limit, String& input)
{
// Must not be empty
if (input.length () <= 0)
return false;
int firstNonDigit = 0;
for (; firstNonDigit < input.length (); ++firstNonDigit)
{
if (! CharacterFunctions::isDigit (input [firstNonDigit]))
break;
}
String const s = input.substring (0, firstNonDigit);
// Must not be empty
if (s.length () <= 0)
return false;
int const n = s.getIntValue ();
// Must not have leading zeroes
if (String (n) != s)
return false;
// Must not be out of range
if (n < 0 || n > limit)
return false;
input = input.substring (s.length ());
*value = n;
return true;
}
bool SemanticVersion::chopIdentifier (String* value, bool allowLeadingZeroes, String& input)
{
// Must not be empty
if (input.length () <= 0)
return false;
// Must not have a leading 0
if (! allowLeadingZeroes && input [0] == '0')
return false;
// Find the first character that cannot be part of an identifier
int i;
for (i = 0; i < input.length (); ++i)
{
static char const* validSet =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
if (! String (validSet).contains (String::charToString (input [i])))
break;
}
// Must not be empty
if (i <= 0)
return false;
*value = input.substring (0, i);
input = input.substring (i);
return true;
}
bool SemanticVersion::chopIdentifiers (StringArray* value, bool allowLeadingZeroes, String& input)
{
if (input.length () <= 0)
return false;
for (;;)
{
String s;
if (! chopIdentifier (&s, allowLeadingZeroes, input))
return false;
value->add (s);
if (! chop (".", input))
break;
}
return true;
}
//------------------------------------------------------------------------------
class SemanticVersion_test: public unit_test::suite
{
typedef SemanticVersion::identifier_list identifier_list;
public:
void checkPass (String const& input, bool shouldPass = true)
void checkPass (std::string const& input, bool shouldPass = true)
{
SemanticVersion v;
@@ -307,13 +340,13 @@ public:
}
}
void checkFail (String const& input)
void checkFail (std::string const& input)
{
checkPass (input, false);
}
// check input and input with appended metadata
void checkMeta (String const& input, bool shouldPass)
void checkMeta (std::string const& input, bool shouldPass)
{
checkPass (input, shouldPass);
@@ -330,7 +363,7 @@ public:
checkFail (input + "+a.!");
}
void checkMetaFail (String const& input)
void checkMetaFail (std::string const& input)
{
checkMeta (input, false);
}
@@ -339,7 +372,7 @@ public:
// input with appended metadata, and input with both
// appended release data and appended metadata
//
void checkRelease (String const& input, bool shouldPass = true)
void checkRelease (std::string const& input, bool shouldPass = true)
{
checkMeta (input, shouldPass);
@@ -362,19 +395,19 @@ public:
// Checks the major.minor.version string alone and with all
// possible combinations of release identifiers and metadata.
//
void check (String const& input, bool shouldPass = true)
void check (std::string const& input, bool shouldPass = true)
{
checkRelease (input, shouldPass);
}
void negcheck (String const& input)
void negcheck (std::string const& input)
{
check (input, false);
}
void testParse ()
{
testcase ("parse");
testcase ("parsing");
check ("0.0.0");
check ("1.2.3");
@@ -405,42 +438,45 @@ public:
negcheck ("1.2.03");
}
static StringArray ids ()
static identifier_list ids ()
{
return StringArray ();
return identifier_list ();
}
static StringArray ids (String const& s1)
static identifier_list ids (
std::string const& s1)
{
StringArray v;
v.add (s1);
identifier_list v;
v.push_back (s1);
return v;
}
static StringArray ids (String const& s1, String const& s2)
static identifier_list ids (
std::string const& s1, std::string const& s2)
{
StringArray v;
v.add (s1);
v.add (s2);
identifier_list v;
v.push_back (s1);
v.push_back (s2);
return v;
}
static StringArray ids (String const& s1, String const& s2, String const& s3)
static identifier_list ids (
std::string const& s1, std::string const& s2, std::string const& s3)
{
StringArray v;
v.add (s1);
v.add (s2);
v.add (s3);
identifier_list v;
v.push_back (s1);
v.push_back (s2);
v.push_back (s3);
return v;
}
// Checks the decomposition of the input into appropriate values
void checkValues (String const& input,
int majorVersion,
int minorVersion,
int patchVersion,
StringArray const& preReleaseIdentifiers = StringArray (),
StringArray const& metaData = StringArray ())
void checkValues (std::string const& input,
int majorVersion,
int minorVersion,
int patchVersion,
identifier_list const& preReleaseIdentifiers = identifier_list (),
identifier_list const& metaData = identifier_list ())
{
SemanticVersion v;
@@ -456,6 +492,8 @@ public:
void testValues ()
{
testcase ("values");
checkValues ("0.1.2", 0, 1, 2);
checkValues ("1.2.3", 1, 2, 3);
checkValues ("1.2.3-rc1", 1, 2, 3, ids ("rc1"));
@@ -469,7 +507,7 @@ public:
}
// makes sure the left version is less than the right
void checkLessInternal (String const& lhs, String const& rhs)
void checkLessInternal (std::string const& lhs, std::string const& rhs)
{
SemanticVersion left;
SemanticVersion right;
@@ -477,10 +515,10 @@ public:
expect (left.parse (lhs));
expect (right.parse (rhs));
expect (left.compare (left) == 0);
expect (right.compare (right) == 0);
expect (left.compare (right) < 0);
expect (right.compare (left) > 0);
expect (compare (left, left) == 0);
expect (compare (right, right) == 0);
expect (compare (left, right) < 0);
expect (compare (right, left) > 0);
expect (left < right);
expect (right > left);
@@ -488,7 +526,7 @@ public:
expect (right == right);
}
void checkLess (String const& lhs, String const& rhs)
void checkLess (std::string const& lhs, std::string const& rhs)
{
checkLessInternal (lhs, rhs);
checkLessInternal (lhs + "+meta", rhs);
@@ -498,6 +536,8 @@ public:
void testCompare ()
{
testcase ("comparisons");
checkLess ("1.0.0-alpha", "1.0.0-alpha.1");
checkLess ("1.0.0-alpha.1", "1.0.0-alpha.beta");
checkLess ("1.0.0-alpha.beta", "1.0.0-beta");

View File

@@ -20,8 +20,9 @@
#ifndef BEAST_SEMANTICVERSION_H_INCLUDED
#define BEAST_SEMANTICVERSION_H_INCLUDED
#include <beast/strings/String.h>
#include <beast/module/core/text/StringArray.h>
#include <vector>
#include <string>
#include <beast/utility/noexcept.h>
namespace beast {
@@ -36,47 +37,79 @@ namespace beast {
class SemanticVersion
{
public:
typedef std::vector<std::string> identifier_list;
int majorVersion;
int minorVersion;
int patchVersion;
StringArray preReleaseIdentifiers;
StringArray metaData;
identifier_list preReleaseIdentifiers;
identifier_list metaData;
SemanticVersion ();
SemanticVersion (std::string const& version);
/** Parse a semantic version string.
The parsing is as strict as possible.
@return `true` if the string was parsed.
*/
bool parse (String input);
bool parse (std::string const& input, bool debug = false);
/** Produce a string from semantic version components. */
String print () const;
std::string print () const;
inline bool isRelease () const noexcept { return preReleaseIdentifiers.size () <= 0; }
inline bool isPreRelease () const noexcept { return ! isRelease (); }
/** Compare this against another version.
The comparison follows the rules as per the specification.
*/
int compare (SemanticVersion const& rhs) const noexcept;
inline bool operator== (SemanticVersion const& other) const noexcept { return compare (other) == 0; }
inline bool operator!= (SemanticVersion const& other) const noexcept { return compare (other) != 0; }
inline bool operator>= (SemanticVersion const& other) const noexcept { return compare (other) >= 0; }
inline bool operator<= (SemanticVersion const& other) const noexcept { return compare (other) <= 0; }
inline bool operator> (SemanticVersion const& other) const noexcept { return compare (other) > 0; }
inline bool operator< (SemanticVersion const& other) const noexcept { return compare (other) < 0; }
private:
static bool isNumeric (String const& s);
static String printIdentifiers (StringArray const& list);
static bool chop (String const& what, String& input);
static bool chopUInt (int* value, int limit, String& input);
static bool chopIdentifier (String* value, bool allowLeadingZeroes, String& input);
static bool chopIdentifiers (StringArray* value, bool preRelease, String& input);
inline bool isRelease () const noexcept
{
return preReleaseIdentifiers.empty();
}
inline bool isPreRelease () const noexcept
{
return !isRelease ();
}
};
/** Compare two SemanticVersions against each other.
The comparison follows the rules as per the specification.
*/
int compare (SemanticVersion const& lhs, SemanticVersion const& rhs);
inline bool
operator== (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) == 0;
}
inline bool
operator!= (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) != 0;
}
inline bool
operator>= (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) >= 0;
}
inline bool
operator<= (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) <= 0;
}
inline bool
operator> (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) > 0;
}
inline bool
operator< (SemanticVersion const& lhs, SemanticVersion const& rhs)
{
return compare (lhs, rhs) < 0;
}
} // beast
#endif