Created
November 17, 2024 09:40
-
-
Save YannickFricke/1fa39a0c3dc289a2d005cfb43355767c to your computer and use it in GitHub Desktop.
Dart Semantic Version parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
final semanticVersionRegex = RegExp( | |
r'^' | |
r'(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)' | |
r'(?:-(?<preRelease>(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)))?' | |
r'(?:\+(?<build>(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)))?' | |
r'$', | |
); | |
/// Tries to parse the given [input] into a [SemanticVersion]. | |
/// | |
/// Returns null when the given [input] is not a valid semantic version. | |
SemanticVersion? parseSemanticVersion(String input) { | |
final match = semanticVersionRegex.firstMatch(input); | |
if (match == null) { | |
return null; | |
} | |
// Parse core version components | |
final major = int.parse(match.namedGroup('major')!); | |
final minor = int.parse(match.namedGroup('minor')!); | |
final patch = int.parse(match.namedGroup('patch')!); | |
// Parse pre-release identifiers if available | |
final preRelease = match | |
.namedGroup('preRelease') | |
?.split('.') | |
.where((id) => id.isNotEmpty) | |
.toList(); | |
// Parse build identifiers if available | |
final build = match | |
.namedGroup('build') | |
?.split('.') | |
.where((id) => id.isNotEmpty) | |
.toList(); | |
return SemanticVersion( | |
major: major, | |
minor: minor, | |
patch: patch, | |
preRelease: preRelease, | |
build: build, | |
); | |
} | |
class SemanticVersion implements Comparable<SemanticVersion> { | |
final int major; | |
final int minor; | |
final int patch; | |
final List<String>? preRelease; | |
final List<String>? build; | |
SemanticVersion({ | |
required this.major, | |
required this.minor, | |
required this.patch, | |
this.preRelease, | |
this.build, | |
}); | |
@override | |
String toString() { | |
final core = '$major.$minor.$patch'; | |
final preReleaseStr = preRelease != null ? '-${preRelease!.join('.')}' : ''; | |
final buildStr = build != null ? '+${build!.join('.')}' : ''; | |
return '$core$preReleaseStr$buildStr'; | |
} | |
@override | |
int compareTo(SemanticVersion other) { | |
if (major != other.major) { | |
return major.compareTo(other.major); | |
} | |
if (minor != other.minor) { | |
return minor.compareTo(other.minor); | |
} | |
if (patch != other.patch) { | |
return patch.compareTo(other.patch); | |
} | |
if (preRelease == null && other.preRelease == null) { | |
// No pre-release: equal | |
return 0; | |
} | |
if (preRelease == null) { | |
// No pre-release takes precedence over any pre-release | |
return 1; | |
} | |
if (other.preRelease == null) { | |
// Other has no pre-release: it takes precedence | |
return -1; | |
} | |
if (preRelease!.length < other.preRelease!.length) { | |
return -1; | |
} | |
if (preRelease!.length > other.preRelease!.length) { | |
return 1; | |
} | |
for (var i = 0; | |
i < preRelease!.length && i < other.preRelease!.length; | |
i++) { | |
final thisIdentifier = preRelease![i]; | |
final otherIdentifier = other.preRelease![i]; | |
final thisIsNumeric = int.tryParse(thisIdentifier); | |
final otherIsNumeric = int.tryParse(otherIdentifier); | |
// Numeric identifiers have lower precedence than alphanumeric identifiers | |
if (thisIsNumeric != null && otherIsNumeric == null) { | |
return -1; | |
} | |
if (thisIsNumeric == null && otherIsNumeric != null) { | |
return 1; | |
} | |
// If both are numeric, compare numerically | |
if (thisIsNumeric != null && otherIsNumeric != null) { | |
final numComparison = thisIsNumeric.compareTo(otherIsNumeric); | |
if (numComparison != 0) { | |
return numComparison; | |
} | |
} else { | |
// If both are alphanumeric, compare lexicographically | |
final strComparison = thisIdentifier.compareTo(otherIdentifier); | |
if (strComparison != 0) { | |
return strComparison; | |
} | |
} | |
} | |
// Step 4: If all compared identifiers are equal, shorter list has lower precedence | |
return preRelease!.length.compareTo(other.preRelease!.length); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'package:test/test.dart'; | |
import 'package:ying_shared/semantic_version.dart'; | |
void main() { | |
group('parseSemanticVersion', () { | |
test('it should parse a SemVer with major, minor and patch', () { | |
final result = parseSemanticVersion("0.0.0"); | |
expect(result?.major, 0); | |
expect(result?.minor, 0); | |
expect(result?.patch, 0); | |
expect(result?.preRelease, null); | |
expect(result?.build, null); | |
}); | |
test( | |
'it should parse a SemVer with major, minor, patch and pre-release identifier', | |
() { | |
final result = parseSemanticVersion("0.0.0-snapshot"); | |
expect(result?.major, 0); | |
expect(result?.minor, 0); | |
expect(result?.patch, 0); | |
expect(result?.preRelease, ["snapshot"]); | |
expect(result?.build, null); | |
}, | |
); | |
test( | |
'it should parse a SemVer with major, minor, patch and build identifier', | |
() { | |
final result = parseSemanticVersion("0.0.0+1"); | |
expect(result?.major, 0); | |
expect(result?.minor, 0); | |
expect(result?.patch, 0); | |
expect(result?.preRelease, null); | |
expect(result?.build, ["1"]); | |
}, | |
); | |
test( | |
'it should parse a SemVer with major, minor, patch, build and pre-release identifier', | |
() { | |
final result = parseSemanticVersion("0.0.0-snapshot+1"); | |
expect(result?.major, 0); | |
expect(result?.minor, 0); | |
expect(result?.patch, 0); | |
expect(result?.preRelease, ["snapshot"]); | |
expect(result?.build, ["1"]); | |
}, | |
); | |
test( | |
'it should return null when only the major version was given', | |
() { | |
final result = parseSemanticVersion("0"); | |
expect(result, isNull); | |
}, | |
); | |
test( | |
'it should return null when only the major and minor version was given', | |
() { | |
final result = parseSemanticVersion("0.0"); | |
expect(result, isNull); | |
}, | |
); | |
test('it should return null when the major version is negative', () { | |
final result = parseSemanticVersion("-1.0.0"); | |
expect(result, isNull); | |
}); | |
test('it should return null when the minor version is negative', () { | |
final result = parseSemanticVersion("1.-1.0"); | |
expect(result, isNull); | |
}); | |
test('it should return null when the patch version is negative', () { | |
final result = parseSemanticVersion("1.1.-1"); | |
expect(result, isNull); | |
}); | |
}); | |
group('SemanticVersion', () { | |
group('toString', () { | |
test( | |
'it should return the correct string for major, minor and patch', | |
() { | |
final result = SemanticVersion( | |
major: 0, | |
minor: 0, | |
patch: 0, | |
).toString(); | |
expect(result, "0.0.0"); | |
}, | |
); | |
test( | |
'it should return the correct string for major, minor, patch and pre-release', | |
() { | |
final result = SemanticVersion( | |
major: 0, | |
minor: 0, | |
patch: 0, | |
preRelease: ["snapshot"], | |
).toString(); | |
expect(result, "0.0.0-snapshot"); | |
}, | |
); | |
test( | |
'it should return the correct string for major, minor, patch, pre-release and build', | |
() { | |
final result = SemanticVersion( | |
major: 0, | |
minor: 0, | |
patch: 0, | |
preRelease: ["snapshot"], | |
build: ["1"], | |
).toString(); | |
expect(result, "0.0.0-snapshot+1"); | |
}, | |
); | |
test('it should return the correct string for major, minor and build', | |
() { | |
final result = SemanticVersion( | |
major: 0, | |
minor: 0, | |
patch: 0, | |
build: ["1"], | |
).toString(); | |
expect(result, "0.0.0+1"); | |
}); | |
}); | |
group('compareTo', () { | |
test('should compare major versions correctly', () { | |
final v1 = SemanticVersion(major: 2, minor: 0, patch: 0); | |
final v2 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
expect(v1.compareTo(v2), greaterThan(0)); | |
expect(v2.compareTo(v1), lessThan(0)); | |
}); | |
test('should compare minor versions correctly', () { | |
final v1 = SemanticVersion(major: 1, minor: 1, patch: 0); | |
final v2 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
expect(v1.compareTo(v2), greaterThan(0)); | |
expect(v2.compareTo(v1), lessThan(0)); | |
}); | |
test('should compare patch versions correctly', () { | |
final v1 = SemanticVersion(major: 1, minor: 0, patch: 1); | |
final v2 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
expect(v1.compareTo(v2), greaterThan(0)); | |
expect(v2.compareTo(v1), lessThan(0)); | |
}); | |
test( | |
'should treat versions with no pre-release as higher precedence than those with pre-release', | |
() { | |
final v1 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha'], | |
); | |
expect(v1.compareTo(v2), greaterThan(0)); | |
expect(v2.compareTo(v1), lessThan(0)); | |
}, | |
); | |
test('should compare numeric pre-release identifiers', () { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['1'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['2'], | |
); | |
expect(v1.compareTo(v2), lessThan(0)); | |
expect(v2.compareTo(v1), greaterThan(0)); | |
}); | |
test( | |
'should compare alphanumeric pre-release identifiers lexicographically', | |
() { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['beta'], | |
); | |
expect(v1.compareTo(v2), lessThan(0)); | |
expect(v2.compareTo(v1), greaterThan(0)); | |
}, | |
); | |
test( | |
'should compare numeric and alphanumeric pre-release identifiers', | |
() { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['1'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha'], | |
); | |
expect(v1.compareTo(v2), lessThan(0)); | |
expect(v2.compareTo(v1), greaterThan(0)); | |
}, | |
); | |
test('should compare multi-part pre-release identifiers', () { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '1'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '2'], | |
); | |
final v3 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '1', '1'], | |
); | |
expect(v1.compareTo(v2), lessThan(0)); | |
expect(v2.compareTo(v1), greaterThan(0)); | |
expect(v1.compareTo(v3), lessThan(0)); | |
expect(v3.compareTo(v1), greaterThan(0)); | |
}); | |
test( | |
'should treat shorter pre-release as having lower precedence when prefixes match', | |
() { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '1'], | |
); | |
expect(v1.compareTo(v2), lessThan(0)); | |
expect(v2.compareTo(v1), greaterThan(0)); | |
}, | |
); | |
test( | |
'should compare versions with build metadata (build metadata does not affect precedence)', | |
() { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
build: ['001'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
build: ['002'], | |
); | |
expect(v1.compareTo(v2), equals(0)); | |
}, | |
); | |
test('should handle equal versions', () { | |
final v1 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
final v2 = SemanticVersion(major: 1, minor: 0, patch: 0); | |
expect(v1.compareTo(v2), equals(0)); | |
}); | |
test('should handle equal versions with identical pre-release', () { | |
final v1 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '1'], | |
); | |
final v2 = SemanticVersion( | |
major: 1, | |
minor: 0, | |
patch: 0, | |
preRelease: ['alpha', '1'], | |
); | |
expect(v1.compareTo(v2), equals(0)); | |
}); | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment