diff --git a/core/src/main/scala/coursier/core/ComparableVersion.scala b/core/src/main/scala/coursier/core/ComparableVersion.scala deleted file mode 100644 index fcea0a119..000000000 --- a/core/src/main/scala/coursier/core/ComparableVersion.scala +++ /dev/null @@ -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) -} diff --git a/core/src/main/scala/coursier/core/Parse.scala b/core/src/main/scala/coursier/core/Parse.scala index 083d9010e..4d89c35ea 100644 --- a/core/src/main/scala/coursier/core/Parse.scala +++ b/core/src/main/scala/coursier/core/Parse.scala @@ -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] = { diff --git a/core/src/main/scala/coursier/core/Version.scala b/core/src/main/scala/coursier/core/Version.scala index a5737bc9e..e01ebfb77 100644 --- a/core/src/main/scala/coursier/core/Version.scala +++ b/core/src/main/scala/coursier/core/Version.scala @@ -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 - } -} \ No newline at end of file diff --git a/core/src/main/scala/coursier/core/Versions.scala b/core/src/main/scala/coursier/core/Versions.scala new file mode 100644 index 000000000..024d953a0 --- /dev/null +++ b/core/src/main/scala/coursier/core/Versions.scala @@ -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 + } +} \ No newline at end of file diff --git a/core/src/main/scala/coursier/core/Xml.scala b/core/src/main/scala/coursier/core/Xml.scala index b6cb4ae4e..9c7d0426d 100644 --- a/core/src/main/scala/coursier/core/Xml.scala +++ b/core/src/main/scala/coursier/core/Xml.scala @@ -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, diff --git a/core/src/test/scala/coursier/test/ComparableVersionTests.scala b/core/src/test/scala/coursier/test/VersionTests.scala similarity index 97% rename from core/src/test/scala/coursier/test/ComparableVersionTests.scala rename to core/src/test/scala/coursier/test/VersionTests.scala index d86dd6450..41bf57b44 100644 --- a/core/src/test/scala/coursier/test/ComparableVersionTests.scala +++ b/core/src/test/scala/coursier/test/VersionTests.scala @@ -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" )) ) {