Merge Version and ComparableVersion

This commit is contained in:
Alexandre Archambault 2015-06-21 18:04:57 +01:00
parent a70ed0c292
commit bda358ba9b
6 changed files with 298 additions and 304 deletions

View File

@ -1,205 +0,0 @@
package coursier.core
import scala.annotation.tailrec
import coursier.core.compatibility._
/** Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java */
object ComparableVersion {
sealed trait Item extends Ordered[Item] {
def compare(other: Item): Int =
(this, other) match {
case (Number(a), Number(b)) => a.compare(b)
case (BigNumber(a), BigNumber(b)) => a.compare(b)
case (Number(a), BigNumber(b)) => -b.compare(a)
case (BigNumber(a), Number(b)) => a.compare(b)
case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b)
case (Literal(a), Literal(b)) => a.compareToIgnoreCase(b)
case _ =>
val rel0 = compareToEmpty
val rel1 = other.compareToEmpty
if (rel0 == rel1) order.compare(other.order)
else rel0.compare(rel1)
}
def order: Int
def isEmpty: Boolean = compareToEmpty == 0
def compareToEmpty: Int = 1
}
sealed trait Numeric extends Item
case class Number(value: Int) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class BigNumber(value: BigInt) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class Qualifier(value: String, level: Int) extends Item {
val order = -2
override def compareToEmpty = level.compare(0)
}
case class Literal(value: String) extends Item {
val order = -1
override def compareToEmpty = if (value.isEmpty) 0 else 1
}
case object Min extends Item {
val order = -8
override def compareToEmpty = -1
}
case object Max extends Item {
val order = 8
}
val empty = Number(0)
val qualifiers = Seq[Qualifier](
Qualifier("alpha", -5),
Qualifier("beta", -4),
Qualifier("milestone", -3),
Qualifier("cr", -2),
Qualifier("rc", -2),
Qualifier("snapshot", -1),
Qualifier("ga", 0),
Qualifier("final", 0),
Qualifier("sp", 1)
)
val qualifiersMap = qualifiers.map(q => q.value -> q).toMap
object Tokenizer {
sealed trait Separator
case object Dot extends Separator
case object Hyphen extends Separator
case object Underscore extends Separator
case object None extends Separator
def apply(s: String): (Item, Stream[(Separator, Item)]) = {
def parseItem(s: Stream[Char]): (Item, Stream[Char]) = {
if (s.isEmpty || !s.head.letterOrDigit) (empty, s)
else if (s.head.isDigit) {
def digits(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.isDigit) (b.result(), s)
else digits(b + s.head, s.tail)
val (digits0, rem) = digits(new StringBuilder, s)
val item =
if (digits0.length >= 10) BigNumber(BigInt(digits0))
else Number(digits0.toInt)
(item, rem)
} else {
assert(s.head.letter)
def letters(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.letter) (b.result().toLowerCase, s)
else letters(b + s.head, s.tail)
val (letters0, rem) = letters(new StringBuilder, s)
val item =
qualifiersMap.getOrElse(letters0, Literal(letters0))
(item, rem)
}
}
def parseSeparator(s: Stream[Char]): (Separator, Stream[Char]) = {
assert(s.nonEmpty)
s.head match {
case '.' => (Dot, s.tail)
case '-' => (Hyphen, s.tail)
case '_' => (Underscore, s.tail)
case _ => (None, s)
}
}
def helper(s: Stream[Char]): Stream[(Separator, Item)] = {
if (s.isEmpty) Stream()
else {
val (sep, rem0) = parseSeparator(s)
val (item, rem) = parseItem(rem0)
(sep, item) #:: helper(rem)
}
}
val (first, rem) = parseItem(s.toStream)
(first, helper(rem))
}
}
def parse(s: String): ComparableVersion = {
val (first, tokens) = Tokenizer(s)
def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
def postProcess(prevIsNumeric: Option[Boolean], item: Item, tokens0: Stream[(Tokenizer.Separator, Item)]): Stream[Item] = {
val tokens = {
var _tokens = tokens0
if (isNumeric(item)) {
val nextNonDotZero = _tokens.dropWhile{case (Tokenizer.Dot, n: Numeric) => n.isEmpty; case _ => false }
if (nextNonDotZero.forall(t => t._1 == Tokenizer.Hyphen || ((t._1 == Tokenizer.Dot || t._1 == Tokenizer.None) && !isNumeric(t._2)))) { // Dot && isNumeric(t._2)
_tokens = nextNonDotZero
}
}
_tokens
}
def ifFollowedByNumberElse(ifFollowedByNumber: Item, default: Item) = {
val followedByNumber = tokens.headOption
.exists{ case (Tokenizer.None, num: Numeric) if !num.isEmpty => true; case _ => false }
if (followedByNumber) ifFollowedByNumber
else default
}
def next =
if (tokens.isEmpty) Stream()
else postProcess(Some(isNumeric(item)), tokens.head._2, tokens.tail)
item match {
case Literal("min") => Min #:: next
case Literal("max") => Max #:: next
case Literal("a") =>
ifFollowedByNumberElse(qualifiersMap("alpha"), item) #:: next
case Literal("b") =>
ifFollowedByNumberElse(qualifiersMap("beta"), item) #:: next
case Literal("m") =>
ifFollowedByNumberElse(qualifiersMap("milestone"), item) #:: next
case _ =>
item #:: next
}
}
ComparableVersion(postProcess(None, first, tokens).toList)
}
@tailrec
def listCompare(first: List[Item], second: List[Item]): Int = {
if (first.isEmpty && second.isEmpty) 0
else if (first.isEmpty) {
assert(second.nonEmpty)
-second.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else if (second.isEmpty) {
assert(first.nonEmpty)
first.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else {
val rel = first.head.compare(second.head)
if (rel == 0) listCompare(first.tail, second.tail)
else rel
}
}
}
case class ComparableVersion(items: List[ComparableVersion.Item]) extends Ordered[ComparableVersion] {
def compare(other: ComparableVersion) = ComparableVersion.listCompare(items, other.items)
def isEmpty = items.forall(_.isEmpty)
}

View File

@ -29,7 +29,7 @@ object Parse {
strTo = s0.drop(commaIdx + 1)
from <- if (strFrom.isEmpty) Some(None) else version(strFrom).map(Some(_))
to <- if (strTo.isEmpty) Some(None) else version(strTo).map(Some(_))
} yield VersionInterval(from.filterNot(_.cmp.isEmpty), to.filterNot(_.cmp.isEmpty), fromIncluded, toIncluded)
} yield VersionInterval(from.filterNot(_.isEmpty), to.filterNot(_.isEmpty), fromIncluded, toIncluded)
}
def versionConstraint(s: String): Option[VersionConstraint] = {

View File

@ -1,108 +1,210 @@
package coursier.core
case class Versions(latest: String,
release: String,
available: List[String],
lastUpdated: Option[Version.DateTime])
import scala.annotation.tailrec
import coursier.core.compatibility._
/** Used internally by Resolver */
/**
* Used internally by Resolver.
*
* Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
*/
case class Version(repr: String) extends Ordered[Version] {
lazy val cmp = ComparableVersion.parse(repr)
def compare(other: Version): Int = {
cmp.compare(other.cmp)
}
lazy val items = Version.items(repr)
def compare(other: Version) = Version.listCompare(items, other.items)
def isEmpty = items.forall(_.isEmpty)
}
object Version {
case class DateTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int)
sealed trait Item extends Ordered[Item] {
def compare(other: Item): Int =
(this, other) match {
case (Number(a), Number(b)) => a.compare(b)
case (BigNumber(a), BigNumber(b)) => a.compare(b)
case (Number(a), BigNumber(b)) => -b.compare(a)
case (BigNumber(a), Number(b)) => a.compare(b)
case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b)
case (Literal(a), Literal(b)) => a.compareToIgnoreCase(b)
}
case _ =>
val rel0 = compareToEmpty
val rel1 = other.compareToEmpty
case class VersionInterval(from: Option[Version],
to: Option[Version],
fromIncluded: Boolean,
toIncluded: Boolean) {
def isValid: Boolean = {
val fromToOrder =
for {
f <- from
t <- to
cmd = f.compare(t)
} yield cmd < 0 || (cmd == 0 && fromIncluded && toIncluded)
fromToOrder.forall(x => x) && (from.nonEmpty || !fromIncluded) && (to.nonEmpty || !toIncluded)
}
def merge(other: VersionInterval): Option[VersionInterval] = {
val (newFrom, newFromIncluded) =
(from, other.from) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(b), other.fromIncluded)
else if (cmp > 0) (Some(a), fromIncluded)
else (Some(a), fromIncluded && other.fromIncluded)
case (Some(a), None) => (Some(a), fromIncluded)
case (None, Some(b)) => (Some(b), other.fromIncluded)
case (None, None) => (None, false)
if (rel0 == rel1) order.compare(other.order)
else rel0.compare(rel1)
}
val (newTo, newToIncluded) =
(to, other.to) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(a), toIncluded)
else if (cmp > 0) (Some(b), other.toIncluded)
else (Some(a), toIncluded && other.toIncluded)
case (Some(a), None) => (Some(a), toIncluded)
case (None, Some(b)) => (Some(b), other.toIncluded)
case (None, None) => (None, false)
}
Some(VersionInterval(newFrom, newTo, newFromIncluded, newToIncluded))
.filter(_.isValid)
def order: Int
def isEmpty: Boolean = compareToEmpty == 0
def compareToEmpty: Int = 1
}
def constraint: VersionConstraint =
this match {
case VersionInterval.zero => VersionConstraint.None
case VersionInterval(Some(version), None, true, false) => VersionConstraint.Preferred(version)
case itv => VersionConstraint.Interval(itv)
sealed trait Numeric extends Item
case class Number(value: Int) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class BigNumber(value: BigInt) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class Qualifier(value: String, level: Int) extends Item {
val order = -2
override def compareToEmpty = level.compare(0)
}
case class Literal(value: String) extends Item {
val order = -1
override def compareToEmpty = if (value.isEmpty) 0 else 1
}
case object Min extends Item {
val order = -8
override def compareToEmpty = -1
}
case object Max extends Item {
val order = 8
}
val empty = Number(0)
val qualifiers = Seq[Qualifier](
Qualifier("alpha", -5),
Qualifier("beta", -4),
Qualifier("milestone", -3),
Qualifier("cr", -2),
Qualifier("rc", -2),
Qualifier("snapshot", -1),
Qualifier("ga", 0),
Qualifier("final", 0),
Qualifier("sp", 1)
)
val qualifiersMap = qualifiers.map(q => q.value -> q).toMap
object Tokenizer {
sealed trait Separator
case object Dot extends Separator
case object Hyphen extends Separator
case object Underscore extends Separator
case object None extends Separator
def apply(s: String): (Item, Stream[(Separator, Item)]) = {
def parseItem(s: Stream[Char]): (Item, Stream[Char]) = {
if (s.isEmpty || !s.head.letterOrDigit) (empty, s)
else if (s.head.isDigit) {
def digits(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.isDigit) (b.result(), s)
else digits(b + s.head, s.tail)
val (digits0, rem) = digits(new StringBuilder, s)
val item =
if (digits0.length >= 10) BigNumber(BigInt(digits0))
else Number(digits0.toInt)
(item, rem)
} else {
assert(s.head.letter)
def letters(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.letter) (b.result().toLowerCase, s)
else letters(b + s.head, s.tail)
val (letters0, rem) = letters(new StringBuilder, s)
val item =
qualifiersMap.getOrElse(letters0, Literal(letters0))
(item, rem)
}
}
def parseSeparator(s: Stream[Char]): (Separator, Stream[Char]) = {
assert(s.nonEmpty)
s.head match {
case '.' => (Dot, s.tail)
case '-' => (Hyphen, s.tail)
case '_' => (Underscore, s.tail)
case _ => (None, s)
}
}
def helper(s: Stream[Char]): Stream[(Separator, Item)] = {
if (s.isEmpty) Stream()
else {
val (sep, rem0) = parseSeparator(s)
val (item, rem) = parseItem(rem0)
(sep, item) #:: helper(rem)
}
}
val (first, rem) = parseItem(s.toStream)
(first, helper(rem))
}
}
def items(repr: String): List[Item] = {
val (first, tokens) = Tokenizer(repr)
def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
def postProcess(prevIsNumeric: Option[Boolean], item: Item, tokens0: Stream[(Tokenizer.Separator, Item)]): Stream[Item] = {
val tokens = {
var _tokens = tokens0
if (isNumeric(item)) {
val nextNonDotZero = _tokens.dropWhile{case (Tokenizer.Dot, n: Numeric) => n.isEmpty; case _ => false }
if (nextNonDotZero.forall(t => t._1 == Tokenizer.Hyphen || ((t._1 == Tokenizer.Dot || t._1 == Tokenizer.None) && !isNumeric(t._2)))) { // Dot && isNumeric(t._2)
_tokens = nextNonDotZero
}
}
_tokens
}
def ifFollowedByNumberElse(ifFollowedByNumber: Item, default: Item) = {
val followedByNumber = tokens.headOption
.exists{ case (Tokenizer.None, num: Numeric) if !num.isEmpty => true; case _ => false }
if (followedByNumber) ifFollowedByNumber
else default
}
def next =
if (tokens.isEmpty) Stream()
else postProcess(Some(isNumeric(item)), tokens.head._2, tokens.tail)
item match {
case Literal("min") => Min #:: next
case Literal("max") => Max #:: next
case Literal("a") =>
ifFollowedByNumberElse(qualifiersMap("alpha"), item) #:: next
case Literal("b") =>
ifFollowedByNumberElse(qualifiersMap("beta"), item) #:: next
case Literal("m") =>
ifFollowedByNumberElse(qualifiersMap("milestone"), item) #:: next
case _ =>
item #:: next
}
}
def repr: String = Seq(
if (fromIncluded) "[" else "(",
from.map(_.repr).mkString,
",",
to.map(_.repr).mkString,
if (toIncluded) "]" else ")"
).mkString
}
postProcess(None, first, tokens).toList
}
object VersionInterval {
val zero = VersionInterval(None, None, fromIncluded = false, toIncluded = false)
}
@tailrec
def listCompare(first: List[Item], second: List[Item]): Int = {
if (first.isEmpty && second.isEmpty) 0
else if (first.isEmpty) {
assert(second.nonEmpty)
-second.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else if (second.isEmpty) {
assert(first.nonEmpty)
first.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else {
val rel = first.head.compare(second.head)
if (rel == 0) listCompare(first.tail, second.tail)
else rel
}
}
sealed trait VersionConstraint {
def interval: VersionInterval
def repr: String
}
object VersionConstraint {
/** Currently treated as minimum... */
case class Preferred(version: Version) extends VersionConstraint {
def interval: VersionInterval = VersionInterval(Some(version), Option.empty, fromIncluded = true, toIncluded = false)
def repr: String = version.repr
}
case class Interval(interval: VersionInterval) extends VersionConstraint {
def repr: String = interval.repr
}
case object None extends VersionConstraint {
val interval = VersionInterval.zero
def repr: String = "" // Once parsed, "(,)" becomes "" because of this
}
}

View File

@ -0,0 +1,98 @@
package coursier.core
case class Versions(latest: String,
release: String,
available: List[String],
lastUpdated: Option[Versions.DateTime])
object Versions {
case class DateTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int)
}
case class VersionInterval(from: Option[Version],
to: Option[Version],
fromIncluded: Boolean,
toIncluded: Boolean) {
def isValid: Boolean = {
val fromToOrder =
for {
f <- from
t <- to
cmd = f.compare(t)
} yield cmd < 0 || (cmd == 0 && fromIncluded && toIncluded)
fromToOrder.forall(x => x) && (from.nonEmpty || !fromIncluded) && (to.nonEmpty || !toIncluded)
}
def merge(other: VersionInterval): Option[VersionInterval] = {
val (newFrom, newFromIncluded) =
(from, other.from) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(b), other.fromIncluded)
else if (cmp > 0) (Some(a), fromIncluded)
else (Some(a), fromIncluded && other.fromIncluded)
case (Some(a), None) => (Some(a), fromIncluded)
case (None, Some(b)) => (Some(b), other.fromIncluded)
case (None, None) => (None, false)
}
val (newTo, newToIncluded) =
(to, other.to) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(a), toIncluded)
else if (cmp > 0) (Some(b), other.toIncluded)
else (Some(a), toIncluded && other.toIncluded)
case (Some(a), None) => (Some(a), toIncluded)
case (None, Some(b)) => (Some(b), other.toIncluded)
case (None, None) => (None, false)
}
Some(VersionInterval(newFrom, newTo, newFromIncluded, newToIncluded))
.filter(_.isValid)
}
def constraint: VersionConstraint =
this match {
case VersionInterval.zero => VersionConstraint.None
case VersionInterval(Some(version), None, true, false) => VersionConstraint.Preferred(version)
case itv => VersionConstraint.Interval(itv)
}
def repr: String = Seq(
if (fromIncluded) "[" else "(",
from.map(_.repr).mkString,
",",
to.map(_.repr).mkString,
if (toIncluded) "]" else ")"
).mkString
}
object VersionInterval {
val zero = VersionInterval(None, None, fromIncluded = false, toIncluded = false)
}
sealed trait VersionConstraint {
def interval: VersionInterval
def repr: String
}
object VersionConstraint {
/** Currently treated as minimum... */
case class Preferred(version: Version) extends VersionConstraint {
def interval: VersionInterval = VersionInterval(Some(version), Option.empty, fromIncluded = true, toIncluded = false)
def repr: String = version.repr
}
case class Interval(interval: VersionInterval) extends VersionConstraint {
def repr: String = interval.repr
}
case object None extends VersionConstraint {
val interval = VersionInterval.zero
def repr: String = "" // Once parsed, "(,)" becomes "" because of this
}
}

View File

@ -236,7 +236,7 @@ object Xml {
lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time")
.toOption
.filter(s => s.length == 14 && s.forall(_.isDigit))
.map(s => Version.DateTime(
.map(s => Versions.DateTime(
s.substring(0, 4).toInt,
s.substring(4, 6).toInt,
s.substring(6, 8).toInt,

View File

@ -1,14 +1,12 @@
package coursier
package test
import java.util.Locale
import utest._
object ComparableVersionTests extends TestSuite {
import core.ComparableVersion.parse
object VersionTests extends TestSuite {
import core.Version
def compare(first: String, second: String) = parse(first).compare(parse(second))
def compare(first: String, second: String) = Version(first).compare(Version(second))
def increasing(versions: String*): Boolean =
versions.iterator.sliding(2).withPartial(false).forall{case Seq(a, b) => compare(a, b) < 0 }
@ -16,13 +14,13 @@ object ComparableVersionTests extends TestSuite {
val tests = TestSuite {
'stackOverflow{
val s = "." * 100000
val v = parse(s)
val v = Version(s)
assert(v.isEmpty)
}
'empty{
val v0 = parse("0")
val v = parse("")
val v0 = Version("0")
val v = Version("")
assert(v0.isEmpty)
assert(v.isEmpty)
@ -328,6 +326,7 @@ object ComparableVersionTests extends TestSuite {
// 'CaseInsensitiveOrderingOfQualifiersIsLocaleIndependent
// {
// import java.util.Locale
// val orig = Locale.getDefault
// try {
// for ( locale <- Seq(Locale.ENGLISH, new Locale( "tr" )) ) {