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

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

The newest version!
package coursier.version

import dataclass.data
import coursier.version.internal.Compatibility._

import scala.annotation.tailrec

/**
 *  Used internally by Resolver.
 *
 *  Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
 */
@data class Version(repr: String) extends Ordered[Version] {
  lazy val items: Vector[Version.Item] = Version.items(repr)
  def compare(other: Version) = Version.listCompare(items, other.items)
  def isEmpty = items.forall(_.isEmpty)

  lazy val isStable: Boolean =
    !repr.endsWith("SNAPSHOT") &&
    !repr.exists(_.isLetter) &&
    repr
      .split(Array('.', '-'))
      .forall(_.lengthCompare(5) <= 0)
}

object Version {

  private[version] val zero = Version("0")

  sealed abstract class Item extends Ordered[Item] {
    def compare(other: Item): Int =
      (this, other) match {
        case (a: Number, b: Number) => a.value.compare(b.value)
        case (a: BigNumber, b: BigNumber) => a.value.compare(b.value)
        case (a: Number, b: BigNumber) => -b.value.compare(a.value)
        case (a: BigNumber, b: Number) => a.value.compare(b.value)
        case (a: Tag, b: Tag) => a.compareTag(b)
        case _ =>
          val rel0 = compareToEmpty
          val rel1 = other.compareToEmpty

          if (rel0 == rel1) order.compare(other.order)
          else rel0.compare(rel1)
      }

    final def isNumber: Boolean =
      this match {
        case _: Numeric => true
        case _ => false
      }

    def order: Int
    def isEmpty: Boolean = compareToEmpty == 0
    def compareToEmpty: Int = 1
  }

  sealed abstract class Numeric extends Item {
    def repr: String
    def next: Numeric
  }
  @data 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)
  }
  @data 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)
  }

  /**
   * Tags represent prerelease tags, typically appearing after - for SemVer compatible versions.
   */
  @data class Tag(value: String) extends Item {
    val order = -1
    private val otherLevel = -5
    lazy val level: Int =
      value match {
        case "ga" | "final" | "" => 0 // 1.0.0 equivalent
        case "snapshot"          => -1
        case "rc" | "cr"         => -2
        case "beta" | "b"        => -3
        case "alpha" | "a"       => -4
        case "dev"               => -6
        case "sp" | "bin"        => 1
        case _                   => otherLevel
      }

    override def compareToEmpty = level.compare(0)
    def isPreRelease: Boolean = level < 0
    def compareTag(other: Tag): Int = {
      val levelComp = level.compare(other.level)
      if (levelComp == 0 && level == otherLevel) value.compareToIgnoreCase(other.value)
      else levelComp
    }
  }
  @data class BuildMetadata(value: String) extends Item {
    val order = 1
    override def compareToEmpty = 0
  }

  case object Min extends Item {
    val order = -8
    override def compareToEmpty = -1
  }
  case object Max extends Item {
    val order = 8
  }

  val empty = Number(0)

  object Tokenizer {
    sealed abstract class Separator
    case object Dot extends Separator
    case object Hyphen extends Separator
    case object Underscore extends Separator
    case object Plus extends Separator
    case object None extends Separator

    def apply(str: String): (Item, Stream[(Separator, Item)]) = {
      def parseItem(s: Stream[Char], prev: Option[Separator]): (Item, Stream[Char]) = {
        if (s.isEmpty) (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 if (s.head.letter) {
          def letters(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
            if (s.isEmpty || !s.head.letter)
              (b.result().toLowerCase, s) // not specifying a Locale (error with scala js)
            else
              letters(b += s.head, s.tail)

          val (letters0, rem) = letters(new StringBuilder, s)
          val item = letters0 match {
            case "x" if prev == Some(Dot) => Max
            case "min" => Min
            case "max" => Max
            case _     => Tag(letters0)
          }
          (item, rem)
        } else {
          val (sep, _) = parseSeparator(s)
          (prev, sep) match {
            case (_, None) =>
              def other(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
                if (s.isEmpty || s.head.isLetterOrDigit || parseSeparator(s)._1 != None)
                  (b.result().toLowerCase, s)  // not specifying a Locale (error with scala js)
                else
                  other(b += s.head, s.tail)

              val (item, rem0) = other(new StringBuilder, s)
              // treat .* as .max
              if (prev == Some(Dot) && item == "*") (Max, rem0)
              else (Tag(item), rem0)
            // treat .+ as .max
            case (Some(Dot), Plus) => (Max, s)
            case _                 => (empty, s)
          }
        }
      }

      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 '+' => (Plus, s.tail)
          case _ => (None, s)
        }
      }

      def helper(s: Stream[Char]): Stream[(Separator, Item)] = {
        if (s.isEmpty) Stream()
        else {
          val (sep, rem0) = parseSeparator(s)
          sep match {
            case Plus =>
              Stream((sep, BuildMetadata(rem0.mkString)))
            case _ =>
              val (item, rem) = parseItem(rem0, Some(sep))
              (sep, item) #:: helper(rem)
          }
        }
      }

      val (first, rem) = parseItem(str.toStream, scala.None)
      (first, helper(rem))
    }
  }

  def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
  private def isNumericOrMinMax(item: Item): Boolean =
    item match {
      case _: Numeric | Min | Max => true
      case _ => false
    }
  def isBuildMetadata(item: Item) = item match { case _: BuildMetadata => true; case _ => false }

  def items(repr: String): Vector[Item] = {
    val (first, tokens) = Tokenizer(repr)
    first +: tokens.toVector.map(_._2)
  }

  // before comparing two versions pad the number parts to the equal number of digits
  // for example, 1-ga, and 1.0.0 comparison will be adjusted first to 1.0.0-ga and 1.0.0.
  def listCompare(first0: Vector[Item], second0: Vector[Item]): Int = {
    // Semver § 10: two versions that differ only in the build metadata, have the same precedence.
    val first = first0.filterNot(isBuildMetadata)
    val second = second0.filterNot(isBuildMetadata)

    def padNum(xs: Vector[Item], original: Int, next: Int): Vector[Item] = {
      val (before, after) = xs.splitAt(original)
      before ++ Vector.fill(next - original)(empty) ++ after
    }
    val num1 = first.takeWhile(isNumericOrMinMax)
    val num2 = second.takeWhile(isNumericOrMinMax)
    (num1.size, num2.size) match {
      case (x, y) if x == y =>
        listCompare0(first, second)
      case (x, y) if x > y  =>
        listCompare0(first, padNum(second, y, x))
      case (x, y) if x < y  =>
        listCompare0(padNum(first, x, y), second)
    }
  }

  @tailrec
  private def listCompare0(first: Vector[Item], second: Vector[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) listCompare0(first.tail, second.tail)
      else rel
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy