Skip to content

Instantly share code, notes, and snippets.

@Rapptz
Created April 18, 2015 20:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rapptz/500771f134756cfc558a to your computer and use it in GitHub Desktop.
Save Rapptz/500771f134756cfc558a to your computer and use it in GitHub Desktop.
Semantic Versioning Parsing -- http://semver.org/
0 out of 10 failed parsing
0 out of 31 tests incorrectly passed parsing
#include <string>
#include <cctype>
#include <exception>
struct invalid_semver : public std::exception {
const char* what() const noexcept {
return "invalid semantic version provided";
}
};
struct version {
public:
std::string tag;
std::string metadata;
unsigned major = 0;
unsigned minor = 0;
unsigned patch = 0;
version() = default;
version(unsigned major, unsigned minor, unsigned patch = 0): major(major), minor(minor), patch(patch) {}
version(const std::string& str) {
parse(str);
}
version(const char* str) {
parse(str);
}
void parse(const std::string& str) {
parse_str(str.c_str(), str.size());
}
void parse(const char* str) {
parse_str(str, std::char_traits<char>::length(str));
}
private:
bool is_valid_identifier(char c) const noexcept {
return std::isalnum(c) || c == '-';
}
void parse_str(const char* str, size_t length) {
// make sure it doesn't end in an invalid character
if(not is_valid_identifier(str[length - 1])) {
throw invalid_semver();
}
// <major>.<minor>.<patch>[-<dot separated>][+<dot separated>]
// <dot separated> = [a-zA-Z0-9-] . [a-zA-Z0-9-] ...
size_t index = 0;
// parse major.minor.patch
parse_number(str, length, index, major);
parse_number(str, length, index, minor);
parse_number(str, length, index, patch);
if(index >= length) {
return;
}
else if(str[index] == '-') {
++index;
parse_tag_and_meta(str, length, index);
}
else if(str[index] == '+') {
++index;
parse_meta(str, length, index);
}
}
void parse_number(const char* str, size_t length, size_t& index, unsigned& number) const {
bool first = true;
while(index < length) {
bool valid_digit = std::isdigit(str[index]);
// check for leading zero
if(first && str[index] == '0' && (index + 1) < length && std::isdigit(str[index + 1])) {
throw invalid_semver();
}
// empty number, e.g. negatives, periods, plus signs, etc.
if(first && not valid_digit) {
throw invalid_semver();
}
if(str[index] == '.') {
++index;
break;
}
if(str[index] == '-' || str[index] == '+') {
break;
}
if(not valid_digit) {
throw invalid_semver();
}
number = (number * 10) + (str[index++] - '0');
first = false;
}
}
void parse_tag_and_meta(const char* str, size_t length, size_t& index) {
size_t start_pos = index;
bool first = true;
while(index < length) {
if(str[index] == '+') {
// this formally ends the tag so process it
// make sure it doesn't end in a period
if(str[index - 1] == '.') {
throw invalid_semver();
}
tag.assign(str + start_pos, str + index);
++index;
return parse_meta(str, length, index);
}
if(str[index] == '.') {
++index;
first = true;
continue;
}
// leading zero
if(first && str[index] == '0' && (index + 1) < length && std::isdigit(str[index + 1])) {
throw invalid_semver();
}
if(not is_valid_identifier(str[index])) {
throw invalid_semver();
}
++index;
first = false;
}
// if we've reached the end of the string then process the tag
tag.assign(str + start_pos, str + length);
}
void parse_meta(const char* str, size_t length, size_t& index) {
size_t start_pos = index;
while(index < length) {
// just verify it's valid
if(!(is_valid_identifier(str[index]) || str[index] == '.')) {
throw invalid_semver();
}
++index;
}
// since we got this far just assign
metadata.assign(str + start_pos, str + length);
}
};
// returns < 0 if lhs < rhs
// returns > 0 if lhs > rhs
// returns 0 if lhs == rhs
// int compare(const version& lhs, const version& rhs) {
// }
#include <iostream>
void test_success() {
auto tests = { "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-0.3.7", "1.0.0-x.7.z.92",
"1.0.0-alpha+001", "1.0.0+20130313144700", "1.0.0-beta+exp.sha.5114f85",
"1.0.0", "12312.31232.1231", "2.0.0"};
size_t failed = 0;
for(auto&& test : tests) {
try {
version x(test);
}
catch(const std::exception&) {
// std::cerr << e.what() << '\n';
std::cout << test << " failed parsing\n";
++failed;
}
}
std::cout << failed << " out of " << tests.size() << " failed parsing\n";
}
void test_failure() {
auto tests = {
"",
".",
"1.",
".1",
"a.b.c",
"1.a.b",
"1.1.a",
"1.a.1",
"a.1.1",
"..",
"1..",
"1.1.",
"1..1",
"1.1.+123",
"1.1.-beta",
"-1.1.1",
"1.-1.1",
"1.1.-1",
// Leading zeroes
"01.1.1",
"001.1.1",
"1.01.1",
"1.001.1",
"1.1.01",
"1.1.001",
"1.1.1-01",
"1.1.1-001",
"1.1.1-beta.01",
"1.1.1-beta.001",
"0.1-2.3.4",
"-12312.122-12312",
"-1.2.3"
};
size_t success = 0;
for(auto&& test : tests) {
try {
version x(test);
++success;
std::cout << test << " incorrectly passed parsing\n";
}
catch(const std::exception&) {
// correctly failed
}
}
std::cout << success << " out of " << tests.size() << " tests incorrectly passed parsing\n";
}
int main() {
test_success();
test_failure();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment