Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Comparing two versions in a functional manner in Scala?

Tags:

scala

Is there an idiomatic, functional way in Scala to compare two dotted version strings, potentially of varying length?

For example:

1.0 == 1.0.0
1.2.4 > 1.2
1.10 > 1.2

(The Java solutions are typically quite imperative style).

like image 592
DNA Avatar asked Mar 19 '19 16:03

DNA


4 Answers

Kinda same thing, but without recursion:

 version1.split("\\.")
   .zipAll(version2.split("\\."), "0", "0")
   .find {case(a, b) => a != b }
   .fold(0) { case (a, b) => a.toInt - b.toInt }

Also, FWIW, I think, this is a duplicate, because the accepted answer in the linked question, answers this one fairly well too.

like image 98
Dima Avatar answered Nov 11 '22 05:11

Dima


Assuming the version strings have been obtained so they only contain dot-separated integers (i.e. "1.2.3" not "1.2.3-foo-beta-1234-etc"), this can be done by splitting on ".", zipping together the two sequences of numbers (zero-padding the least-significant positions using zipAll) then processing the pairs in turn recursively.

As soon as we find a number that is different from its counterpart in the other string, we have an answer; but if the digits are the same, we look further.

  def versionComp(a: String, b: String) = {
    def nums(s: String) = s.split("\\.").map(_.toInt) 
    val pairs = nums(a).zipAll(nums(b), 0, 0).toList
    def go(ps: List[(Int, Int)]): Int = ps match {
      case Nil => 0
      case (a, b) :: t => 
        if (a > b) 1 else if (a < b) -1 else go(t)
    }
    go(pairs)
  }

Examples:

  versionComp("2", "1.1")                     //>  1
  versionComp("1.2", "1.1")                   //>  1
  versionComp("1.2", "1.1.5")                 //>  1
  versionComp("1.2.0", "1.1")                 //>  1
  versionComp("1.2.0.1", "1.2.0.0")           //>  1

  versionComp("1.2.3", "1.2.3")               //>  0
  versionComp("1.2.0", "1.2")                 //>  0
  versionComp("1.2.0.0", "1.2")               //>  0

  versionComp("1.2", "1.5")                   //>  -1
  versionComp("1.2.0", "1.20")                //>  -1
  versionComp("2.20.345", "3.1")              //>  -1
like image 25
DNA Avatar answered Nov 11 '22 04:11

DNA


Well, if you are prepared to write some library code, you could create a Version type and use an Ordering type class on it.

class Version(major: Int, minor: Int, patchOpt: Option[Int], buildOpt: Option[Int], suffixOpt: Option[String])

with a parser and the Ordering in the companion object

object Version {
  // NOTE: This here may well not be complete. If this is what you are lokking for I'll update with a tested version
  // Also NOTE: This would be the place where you customize the ordering to your requirements
  implicit val ordering: Ordering[Version] = Ordering.tuple(v =>(v.major, v.minor, v.patchOpt, v.buildOpt, v.suffixOpt))
  def fromString(in: String): Option[Version] = {
    // ... extract all the parts into the proper fields of version or return None
    ???
  }
  def apply(in String): Version = fromVersion(in).getOrElse(throw new Exception())

  def unapply(...):...
}

Given that, you can use it like that

// This import enabels infix operations like '<, >, <=' on types 
// for which an Ordering type class exists
import Ordering.Implicits.infixOrderingOps

val v1 = Version("1.22.3")
val v2 = Version("0.33.")
val v3 = Version("1.3.5.6")

v1 < v2 // false
v2 <= v3 // true
... 

This may be overkill for your use case but it gives you a more expressive way to describe what you expect a version to be and how you want to order versions.

like image 2
Sascha Kolberg Avatar answered Nov 11 '22 04:11

Sascha Kolberg


You can use

val versionOrdering = 
  Ordering.by { (_: String).split("""\.""").map(_.toInt).toIterable }

def versionComp(a: String, b: String) = versionOrdering.compare(a, b)

You probably don't want to make it implicit because it would be used instead of normal String ordering as well! A work-around is using a value class:

case class Version(asString: String) extends AnyVal

object Version {
  implicit val versionOrdering: Ordering[Version] = 
    Ordering.by { _.asString.split("""\.""").map(_.toInt).toIterable }
}

as a lighter-weight alternative to Sascha Kolberg's answer. It'll let you write e.g.

listOfStrings.sortBy(Version(_))

For one-off comparisons, you can use

import scala.math.Ordering.Implicits._

a.split("""\.""").map(_.toInt).toSeq > b.split("""\.""").map(_.toInt).toSeq
like image 2
Alexey Romanov Avatar answered Nov 11 '22 06:11

Alexey Romanov