Skip to content

Instantly share code, notes, and snippets.

@YannickFricke
Created November 17, 2024 09:40
Show Gist options
  • Save YannickFricke/1fa39a0c3dc289a2d005cfb43355767c to your computer and use it in GitHub Desktop.
Save YannickFricke/1fa39a0c3dc289a2d005cfb43355767c to your computer and use it in GitHub Desktop.
Dart Semantic Version parser
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);
}
}
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