Last active
June 18, 2017 13:29
-
-
Save ericksli/7f2e7457234b6edc144fc8b69c825efd to your computer and use it in GitHub Desktop.
Kotlin data object for Semantic Versioning (SemVer) 2.0.0 specification
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
/** | |
* Version number in [Semantic Versioning 2.0.0](http://semver.org/spec/v2.0.0.html) specification (SemVer). | |
* | |
* @property major major version, increment it when you make incompatible API changes. | |
* @property minor minor version, increment it when you add functionality in a backwards-compatible manner. | |
* @property patch patch version, increment it when you make backwards-compatible bug fixes. | |
* @property preRelease pre-release version. | |
* @property buildMetadata build metadata. | |
*/ | |
data class SemVer( | |
val major: Int = 0, | |
val minor: Int = 0, | |
val patch: Int = 0, | |
val preRelease: String? = null, | |
val buildMetadata: String? = null | |
) : Comparable<SemVer> { | |
companion object { | |
/** | |
* Parse the version string to [SemVer] data object. | |
* @param version version string. | |
* @throws IllegalArgumentException if the version is not valid. | |
*/ | |
fun parse(version: String): SemVer { | |
val pattern = Regex("""(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\dA-z\-]+(?:\.[\dA-z\-]+)*))?(?:\+([\dA-z\-]+(?:\.[\dA-z\-]+)*))?""") | |
val result = pattern.matchEntire(version) ?: throw IllegalArgumentException("Invalid version string [$version]") | |
return SemVer( | |
major = result.groupValues[1].toInt(), | |
minor = result.groupValues[2].toInt(), | |
patch = result.groupValues[3].toInt(), | |
preRelease = if (result.groupValues[4].isEmpty()) null else result.groupValues[4], | |
buildMetadata = if (result.groupValues[5].isEmpty()) null else result.groupValues[5] | |
) | |
} | |
} | |
init { | |
require(major >= 0) { "Major version must be a positive number" } | |
require(minor >= 0) { "Minor version must be a positive number" } | |
require(patch >= 0) { "Patch version must be a positive number" } | |
if (preRelease != null) require(preRelease.matches(Regex("""[\dA-z\-]+(?:\.[\dA-z\-]+)*"""))) { "Pre-release version is not valid" } | |
if (buildMetadata != null) require(buildMetadata.matches(Regex("""[\dA-z\-]+(?:\.[\dA-z\-]+)*"""))) { "Build metadata is not valid" } | |
} | |
override fun toString(): String = buildString { | |
append("$major.$minor.$patch") | |
if (preRelease != null) { | |
append('-') | |
append(preRelease) | |
} | |
if (buildMetadata != null) { | |
append('+') | |
append(buildMetadata) | |
} | |
} | |
/** | |
* Check the version number is in initial development. | |
* @return true if it is in initial development. | |
*/ | |
fun isInitialDevelopmentPhase(): Boolean = major == 0 | |
/** | |
* Compare two SemVer objects using major, minor, patch and pre-release version as specified in SemVer specification. | |
* | |
* For comparing the whole SemVer object including build metadata, use [equals] instead. | |
* | |
* @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. | |
*/ | |
override fun compareTo(other: SemVer): Int { | |
if (major > other.major) return 1 | |
if (major < other.major) return -1 | |
if (minor > other.minor) return 1 | |
if (minor < other.minor) return -1 | |
if (patch > other.patch) return 1 | |
if (patch < other.patch) return -1 | |
if (preRelease == null && other.preRelease == null) return 0 | |
if (preRelease != null && other.preRelease == null) return -1 | |
if (preRelease == null && other.preRelease != null) return 1 | |
val parts = preRelease.orEmpty().split(".") | |
val otherParts = other.preRelease.orEmpty().split(".") | |
val endIndex = Math.min(parts.size, otherParts.size) - 1 | |
for (i in 0..endIndex) { | |
val part = parts[i] | |
val otherPart = otherParts[i] | |
if (part == otherPart) continue | |
val partIsNumeric = part.isNumeric() | |
val otherPartIsNumeric = otherPart.isNumeric() | |
when { | |
partIsNumeric && !otherPartIsNumeric -> { | |
// lower priority | |
return -1 | |
} | |
!partIsNumeric && otherPartIsNumeric -> { | |
// higher priority | |
return 1 | |
} | |
!partIsNumeric && !otherPartIsNumeric -> { | |
if (part > otherPart) return 1 | |
if (part < otherPart) return -1 | |
} | |
else -> { | |
val partInt = part.toInt() | |
val otherPartInt = otherPart.toInt() | |
if (partInt > otherPartInt) return 1 | |
if (partInt < otherPartInt) return -1 | |
} | |
} | |
} | |
if (parts.size == endIndex + 1 && otherParts.size > endIndex + 1) { | |
// parts is ended and otherParts is not ended | |
return -1 | |
} else if (parts.size > endIndex + 1 && otherParts.size == endIndex + 1) { | |
// parts is not ended and otherParts is ended | |
return 1 | |
} else { | |
return 0 | |
} | |
} | |
private fun String.isNumeric(): Boolean = this.matches(Regex("""\d+""")) | |
} |
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 org.junit.Test | |
import kotlin.test.assertEquals | |
import kotlin.test.assertFails | |
import kotlin.test.assertFalse | |
import kotlin.test.assertTrue | |
class SemVerTest { | |
@Test | |
fun init_valid() { | |
SemVer(12, 23, 34, "alpha.12", "test.34") | |
} | |
@Test | |
fun init_invalid_major() { | |
assertFails { SemVer(-1, 23, 34, "alpha.12", "test.34") } | |
} | |
@Test | |
fun init_invalid_minor() { | |
assertFails { SemVer(12, -1, 34, "alpha.12", "test.34") } | |
} | |
@Test | |
fun init_invalid_patch() { | |
assertFails { SemVer(12, 23, -1, "alpha.12", "test.34") } | |
} | |
@Test | |
fun init_invalid_preRelease() { | |
assertFails { SemVer(12, 23, 34, "alpha.12#", "test.34") } | |
} | |
@Test | |
fun init_invalid_metadata() { | |
assertFails { SemVer(12, 23, 34, "alpha.12", "test.34#") } | |
} | |
@Test | |
fun parse_numeric() { | |
val actual = SemVer.parse("1.0.45") | |
val expected = SemVer(1, 0, 45) | |
assertEquals(expected, actual) | |
} | |
@Test | |
fun parse_preRelease() { | |
val actual = SemVer.parse("1.0.0-alpha.beta-a.12") | |
val expected = SemVer(1, 0, 0, preRelease = "alpha.beta-a.12") | |
assertEquals(expected, actual) | |
} | |
@Test | |
fun parse_metadata() { | |
val actual = SemVer.parse("1.0.0+exp.sha-part.5114f85") | |
val expected = SemVer(1, 0, 0, buildMetadata = "exp.sha-part.5114f85") | |
assertEquals(expected, actual) | |
} | |
@Test | |
fun parse_all() { | |
val actual = SemVer.parse("1.0.0-beta+exp.sha.5114f85") | |
val expected = SemVer(1, 0, 0, preRelease = "beta", buildMetadata = "exp.sha.5114f85") | |
assertEquals(expected, actual) | |
} | |
@Test | |
fun parse_invalid() { | |
assertFails { SemVer.parse("1.0.1.4-beta+exp.sha.5114f85") } | |
} | |
@Test | |
fun isInitialDevelopmentPhase_true() { | |
assertTrue { SemVer(0, 23, 34, "alpha.123", "testing.123").isInitialDevelopmentPhase() } | |
} | |
@Test | |
fun isInitialDevelopmentPhase_false() { | |
assertFalse { SemVer(1, 23, 34, "alpha.123", "testing.123").isInitialDevelopmentPhase() } | |
} | |
@Test | |
fun toString_numeric() { | |
val semVer = SemVer(1, 0, 45) | |
assertEquals("1.0.45", semVer.toString()) | |
} | |
@Test | |
fun toString_preRelease() { | |
val semVer = SemVer(1, 0, 0, preRelease = "alpha.beta-a.12") | |
assertEquals("1.0.0-alpha.beta-a.12", semVer.toString()) | |
} | |
@Test | |
fun toString_metadata() { | |
val semVer = SemVer(1, 0, 0, buildMetadata = "exp.sha-part.5114f85") | |
assertEquals("1.0.0+exp.sha-part.5114f85", semVer.toString()) | |
} | |
@Test | |
fun toString_all() { | |
val semVer = SemVer(1, 0, 0, preRelease = "beta", buildMetadata = "exp.sha.5114f85") | |
assertEquals("1.0.0-beta+exp.sha.5114f85", semVer.toString()) | |
} | |
@Test | |
fun compareTo_numeric1() { | |
val semVer1 = SemVer(1, 0, 0) | |
val semVer2 = SemVer(1, 0, 0) | |
assertEquals(0, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric2() { | |
val semVer1 = SemVer(1, 0, 0) | |
val semVer2 = SemVer(2, 0, 0) | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric3() { | |
val semVer1 = SemVer(2, 0, 0) | |
val semVer2 = SemVer(2, 1, 0) | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric4() { | |
val semVer1 = SemVer(2, 1, 4) | |
val semVer2 = SemVer(2, 1, 0) | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric5() { | |
val semVer1 = SemVer(2, 0, 0) | |
val semVer2 = SemVer(1, 0, 0) | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric6() { | |
val semVer1 = SemVer(1, 2, 0) | |
val semVer2 = SemVer(1, 0, 0) | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_numeric7() { | |
val semVer1 = SemVer(1, 0, 0) | |
val semVer2 = SemVer(1, 0, 2) | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease1() { | |
val semVer1 = SemVer(1, 0, 0) | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha") | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease2() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha") | |
val semVer2 = SemVer(1, 0, 0) | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease3() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease4() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha") | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease5() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.beta") | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease6() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.beta") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease7() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "beta") | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease8() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "beta") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease9() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.2") | |
assertEquals(-1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease10() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.2") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
assertEquals(1, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease11() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha.1") | |
assertEquals(0, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease12() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha") | |
assertEquals(0, semVer1.compareTo(semVer2)) | |
} | |
@Test | |
fun compareTo_preRelease_metadata() { | |
val semVer1 = SemVer(1, 0, 0, preRelease = "alpha", buildMetadata = "xyz") | |
val semVer2 = SemVer(1, 0, 0, preRelease = "alpha", buildMetadata = "abc") | |
assertEquals(0, semVer1.compareTo(semVer2)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment