All Downloads are FREE. Search and download functionalities are using the official Maven repository.

coursier.core.Version.scala Maven / Gradle / Ivy

The newest version!
package coursier.core

import scala.annotation.tailrec
import coursier.core.compatibility._

/**
 *  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 items = Version.items(repr)
  lazy val rawItems: Seq[Version.Item] = {
    val (first, tokens) = Version.Tokenizer(repr)
    first +: tokens.toVector.map { case (_, item) => item }
  }
  def compare(other: Version) = Version.listCompare(items, other.items)
  def isEmpty = items.forall(_.isEmpty)
}

object Version {

  sealed abstract class 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 abstract class Numeric extends Item {
    def repr: String
    def next: Numeric
  }
  final case class Number(value: Int) extends Numeric {
    val order = 0
    def next: Number = Number(value + 1)
    def repr: String = value.toString
    override def compareToEmpty = value.compare(0)
  }
  final case class BigNumber(value: BigInt) extends Numeric {
    val order = 0
    def next: BigNumber = BigNumber(value + 1)
    def repr: String = value.toString
    override def compareToEmpty = value.compare(0)
  }
  final case class Qualifier(value: String, level: Int) extends Item {
    val order = -2
    override def compareToEmpty = level.compare(0)
  }
  final 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 abstract class 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
      }
    }

    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
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy