Comparing string numbers in Scala

Viewed 77

I have a usage case like;

val fieldType = "int" // we know what is the type of field
val value1 = "1" // can be double/int/long/float etc.
val value2 = "2" // can be double/int/long/float etc.

val operator = "=" // can be >, >=, <, <= etc.

def compare(operator, value1, value2): Boolean = {}

Couldn't find a good way to do this. Is there any way to do this efficiently?

EDIT

I've just made this one but not sure this is the most efficient way;


  private def apply(fieldType: String, operator: String, value1: String, value2: String): Boolean =
    operator match {
      case ">" =>
        compare(fieldType, value1, value2) > 0
      case ">=" =>
        compare(fieldType, value1, value2) >= 0
      case "<" =>
        compare(fieldType, value1, value2) < 0
      case "<=" =>
        compare(fieldType, value1, value2) <= 0
      case "==" =>
        compare(fieldType, value1, value2) == 0
    }

  private def compare(fieldType: String, value1: String, value2: String): Int =
    fieldType match {
      case "string"     => value1.compareTo(value2)
      case "int"        => value1.toInt.compareTo(value2.toInt)
      case "long"       => value1.toLong.compareTo(value2.toLong)
      case "float"      => value1.toFloat.compareTo(value2.toFloat)
     
    }
1 Answers

The advantage of this solution is that it's generic, so it will work for all numeric types.

I'm using the implicit ordering that Scala provides for every numeric type. And I provide a generic compare method that works for very numeric type. But I still had to parse fieldType to figure out what types I should compare.

T : Ordering means a context bound - this avoids the explicit use of an implicit parameter in the method, and lets Scala provide one of type Ordering[T] automatically wherever I need it in the body of the method.

implicitly[Ordering[T] is a standard library method that tells Scala to look for an implicit definition of type Ordering[T], call it, and return the object right back, then use the methods defined in trait Ordering for comparation purposes.

These 2 ideas fuse together to eliminate the need to include an implicit parameter for the method, because we are not interested in an explicit ordering parameter:

  def compareBy(fieldType: String)(
      op: String,
      a: String,
      b: String
  ): Boolean = {
    def compare[T: Ordering](op: String, a: T, b: T): Boolean =
      op match {
        case "==" => implicitly[Ordering[T]].eq(a, b)
        case "<"  => implicitly[Ordering[T]].lt(a, b)
        case "<=" => implicitly[Ordering[T]].lteq(a, b)
        case ">"  => implicitly[Ordering[T]].gt(a, b)
        case ">=" => implicitly[Ordering[T]].gteq(a, b)
        case _    => throw new UnsupportedOperationException("op not supported!")
      }

    fieldType match {
      case "byte"   => compare(op, a.toByte, b.toByte)
      case "short"  => compare(op, a.toShort, b.toShort)
      case "int"    => compare(op, a.toInt, b.toInt)
      case "long"   => compare(op, a.toLong, b.toLong)
      case "float"  => compare(op, a.toFloat, b.toFloat)
      case "double" => compare(op, a.toDouble, b.toDouble)
      case _ =>
        throw new UnsupportedOperationException("fieldType not supported!")
    }
  }

  val a = "10"
  val b = "2"
  val op = ">"
  val intFieldType = "int"
  val compareInts = compareBy(intFieldType) _

  println(compareInts(op, a, b))     // true

The solution seems over-engineered. The following observations can lead to a more compact one:

  • we can treat any number as a Double, because no precision is lost from converting the smaller type Float to Double or from converting integral types to Double.

  • in the corner-case where we want to compare 2 numbers given as floating-point string literals using an integral type, we drop the precision using longValue, then convert them back to Double, to remove the need of a generic method. By using toDoubleOption first, we also remove the NumberFormatException when parsing a floating-point string literal as an integral type.

    def compareBy(
        fieldType: String
    )(op: String, a: String, b: String): Boolean = {
      def compare(op: String, a: Double, b: Double): Boolean = {
        op match {
          case "==" => a == b
          case "<"  => a < b
          case "<=" => a <= b
          case ">"  => a > b
          case ">=" => a >= b
          case _    => throw new UnsupportedOperationException("op not supported!")
        }
      }
    
      val x: Double = a.toDoubleOption.getOrElse(
        throw new NumberFormatException(s"given $a is not parsable as $fieldType")
      )
      val y: Double = b.toDoubleOption.getOrElse(
        throw new NumberFormatException(s"given $b is not parsable as $fieldType")
      )
    
      fieldType match {
        case "byte" | "short" | "int" | "long" =>
          compare(op, x.longValue.toDouble, y.longValue.toDouble)
        case "float" | "double" => compare(op, x, y)
        case _ =>
          throw new UnsupportedOperationException("fieldType not supported!")
      }
    }
    
    val a = "10.1"
    val b = "10.2"
    val op = "=="
    val intFieldType = "int"
    val compareInts = compareBy(intFieldType) _  
    
    println(compareInts(op, a, b))    // true
    
Related