Create a gist now

Instantly share code, notes, and snippets.

@ericksli /SemVer.kt
Last active Jun 18, 2017

What would you like to do?
Kotlin data object for Semantic Versioning (SemVer) 2.0.0 specification
/**
* 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+"""))
}
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