name.remal.gradle_plugins.dsl.Version.kt Maven / Gradle / Ivy
package name.remal.gradle_plugins.dsl
import name.remal.arrayEquals
import name.remal.default
import name.remal.escapeRegex
import java.io.Serializable
import java.time.Instant.ofEpochMilli
import java.time.LocalDateTime
import java.time.LocalDateTime.ofInstant
import java.time.ZoneId
import java.time.temporal.ChronoField.*
import java.util.*
class Version @JvmOverloads constructor(
vararg numbers: Int,
suffix: String? = null
) : Comparable, Serializable {
@JvmOverloads constructor(major: Int, suffix: String? = null) : this(*intArrayOf(major), suffix = suffix)
@JvmOverloads constructor(major: Int, minor: Int, suffix: String? = null) : this(*intArrayOf(major, minor), suffix = suffix)
@JvmOverloads constructor(major: Int, minor: Int, patch: Int, suffix: String? = null) : this(*intArrayOf(major, minor, patch), suffix = suffix)
@JvmOverloads constructor(major: Int, minor: Int, patch: Int, build: Int, suffix: String? = null) : this(*intArrayOf(major, minor, patch, build), suffix = suffix)
private val numbers: IntArray
val suffix: String
init {
if (numbers.isEmpty()) throw IllegalArgumentException("numbers is empty")
numbers.forEachIndexed { index, number ->
if (number < 0) {
throw IllegalArgumentException(buildString {
when (index) {
0 -> append("major")
1 -> append("minor")
2 -> append("patch")
3 -> append("build")
else -> append("numbers[").append(index).append("]")
}
append(" < 0")
})
}
}
this.numbers = numbers.copyOf()
this.suffix = suffix.default()
}
override fun toString() = buildString {
numbers.forEachIndexed { index, number ->
if (1 <= index) append(NUMBERS_DELIM)
append(number)
}
if (suffix.isNotEmpty()) append(SUFFIX_DELIM).append(suffix)
}
override fun compareTo(other: Version): Int {
val commonLength = Math.min(numbers.size, other.numbers.size)
(0 until commonLength).forEach { numbers[it].compareTo(other.numbers[it]).let { if (0 != it) return it } }
if (numbers.size < commonLength) return -1
if (other.numbers.size < commonLength) return 1
return suffix.compareTo(other.suffix)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (null == other || other !is Version) return false
return numbers.arrayEquals(other.numbers) && suffix == other.suffix
}
override fun hashCode(): Int {
var result = 1
numbers.forEach { result = 31 * result + Integer.hashCode(it) }
result = 31 * result + suffix.hashCode()
return result
}
val numbersCount get() = numbers.size
fun getNumberOrNull(index: Int) = numbers.getOrNull(index)
fun getNumberOr0(index: Int) = if (index < numbers.size) numbers[index] else 0
fun withNumber(index: Int, number: Int): Version {
if (index < numbers.size && numbers[index] == number) return this
return Version(
*numbers.copyOf(Math.max(numbers.size, index + 1)).apply { this[index] = number },
suffix = suffix
)
}
fun incrementNumber(index: Int, increment: Int = 1): Version {
if (index < numbers.size && 0 == increment) return this
return withNumber(index, getNumberOr0(index) + increment)
}
val major get() = numbers[0]
fun withMajor(major: Int) = withNumber(0, major)
fun incrementMajor(increment: Int = 1) = incrementNumber(0, increment)
val minor get() = getNumberOrNull(1)
val hasMinor get() = 2 <= numbers.size
fun withMinor(minor: Int) = withNumber(1, minor)
fun incrementMinor(increment: Int = 1) = incrementNumber(1, increment)
val patch get() = getNumberOrNull(2)
val hasPatch get() = 3 <= numbers.size
fun withPatch(patch: Int) = withNumber(2, patch)
fun incrementPatch(increment: Int = 1) = incrementNumber(2, increment)
val build get() = getNumberOrNull(3)
val hasBuild get() = 4 <= numbers.size
fun withBuild(build: Int) = withNumber(3, build)
fun incrementBuild(increment: Int = 1) = incrementNumber(3, increment)
val hasSuffix get() = suffix.isNotEmpty()
fun withSuffix(suffix: String?): Version {
if (suffix.default() == this.suffix) return this
return Version(*numbers, suffix = suffix)
}
fun appendSuffix(suffixPart: String, delimiter: String = "") = withSuffix(buildString {
append(suffix)
if (suffix.isNotEmpty()) append(delimiter)
append(suffixPart)
})
fun prependSuffix(suffixPart: String, delimiter: String = "") = withSuffix(buildString {
append(suffixPart)
if (suffix.isNotEmpty()) append(delimiter)
append(suffix)
})
companion object {
private const val NUMBERS_DELIM = "."
private const val SUFFIX_DELIM = "-"
private val PATTERN = Regex("\\d+(${escapeRegex(NUMBERS_DELIM)}\\d+)*(${escapeRegex(SUFFIX_DELIM)}.*)?")
@JvmStatic
fun parse(string: String): Version {
if (!string.matches(PATTERN)) throw ParseVersionException("'$string' doesn't match '$PATTERN'")
val stringNumbers = string.substringBefore(SUFFIX_DELIM).split(NUMBERS_DELIM)
val numbers = IntArray(stringNumbers.size)
stringNumbers.forEachIndexed { index, stringNumber -> numbers[index] = stringNumber.toInt() }
val suffix = string.substringAfter(SUFFIX_DELIM, "")
return Version(
*numbers,
suffix = suffix
)
}
@JvmStatic
fun parseOrNull(string: String): Version? {
if (!string.matches(PATTERN)) return null
try {
return parse(string)
} catch (ignored: Exception) {
return null
}
}
@JvmStatic
fun ofLocalDateTime(dateTime: LocalDateTime): Version {
return dateTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).let {
Version(
it.get(YEAR),
it.get(MONTH_OF_YEAR),
it.get(DAY_OF_MONTH),
"%02d%02d%02d".format(it.get(HOUR_OF_DAY), it.get(MINUTE_OF_HOUR), it.get(SECOND_OF_MINUTE))
)
}
}
@JvmStatic
fun ofTimestamp(timestamp: Long) = ofLocalDateTime(ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDateTime())
@JvmStatic
fun ofDate(date: Date) = ofLocalDateTime(ofInstant(date.toInstant(), ZoneId.systemDefault()))
}
}
class ParseVersionException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy