Scala - String to Int/Double Conversion

Scala's `String` class provides `toInt` and `toDouble` methods for direct conversion. These methods throw `NumberFormatException` if the string cannot be parsed.

Key Insights

  • Scala provides multiple conversion methods including toInt, toDouble, and the safer Try wrapper to handle parsing exceptions gracefully
  • The Option pattern combined with toIntOption/toDoubleOption (Scala 2.13+) offers idiomatic null-safe conversions without exception handling overhead
  • For production code, prefer explicit error handling with Either or validation libraries like Cats to provide meaningful failure context rather than swallowing exceptions

Basic String to Int Conversion

Scala’s String class provides toInt and toDouble methods for direct conversion. These methods throw NumberFormatException if the string cannot be parsed.

val numStr = "42"
val num: Int = numStr.toInt
println(num) // 42

val priceStr = "99.99"
val price: Double = priceStr.toDouble
println(price) // 99.99

This approach works for valid numeric strings but crashes on invalid input:

val invalid = "abc"
val result = invalid.toInt // throws NumberFormatException

Use this method only when you’re certain the input is valid, such as when parsing hardcoded configuration values or after validation.

Safe Conversion with Try

The scala.util.Try wrapper catches exceptions and converts them into Success or Failure instances. This provides explicit error handling without try-catch blocks.

import scala.util.{Try, Success, Failure}

def parseIntSafe(s: String): Try[Int] = Try(s.toInt)

parseIntSafe("123") match {
  case Success(value) => println(s"Parsed: $value")
  case Failure(exception) => println(s"Failed: ${exception.getMessage}")
}

// Using map and getOrElse for default values
val result = Try("456".toInt).getOrElse(0)
println(result) // 456

val resultInvalid = Try("xyz".toInt).getOrElse(0)
println(resultInvalid) // 0

Try is particularly useful when chaining operations:

def calculateTotal(priceStr: String, quantityStr: String): Try[Double] = {
  for {
    price <- Try(priceStr.toDouble)
    quantity <- Try(quantityStr.toInt)
  } yield price * quantity
}

calculateTotal("29.99", "3") match {
  case Success(total) => println(f"Total: $$${total}%.2f")
  case Failure(_) => println("Invalid input")
}
// Output: Total: $89.97

Option-Based Conversion (Scala 2.13+)

Scala 2.13 introduced toIntOption and toDoubleOption methods that return Option[Int] and Option[Double] respectively. These methods provide cleaner syntax for optional values without exception overhead.

val maybeInt: Option[Int] = "42".toIntOption
println(maybeInt) // Some(42)

val maybeInvalid: Option[Int] = "not-a-number".toIntOption
println(maybeInvalid) // None

// Pattern matching
"123".toIntOption match {
  case Some(value) => println(s"Got $value")
  case None => println("Invalid number")
}

// Using map and getOrElse
val doubled = "10".toIntOption.map(_ * 2).getOrElse(0)
println(doubled) // 20

For Double conversions with Option:

def calculateDiscount(priceStr: String, discountStr: String): Option[Double] = {
  for {
    price <- priceStr.toDoubleOption
    discount <- discountStr.toDoubleOption
    if discount >= 0 && discount <= 100
  } yield price * (1 - discount / 100)
}

println(calculateDiscount("100.0", "20")) // Some(80.0)
println(calculateDiscount("100.0", "150")) // None (invalid discount)

Handling Multiple Conversions with Either

For applications requiring detailed error messages, Either provides more context than Option or Try. By convention, Left contains errors and Right contains successful values.

def parseIntEither(s: String): Either[String, Int] = {
  try {
    Right(s.toInt)
  } catch {
    case _: NumberFormatException => Left(s"'$s' is not a valid integer")
  }
}

def parseDoubleEither(s: String): Either[String, Double] = {
  try {
    Right(s.toDouble)
  } catch {
    case _: NumberFormatException => Left(s"'$s' is not a valid double")
  }
}

parseIntEither("42") match {
  case Right(value) => println(s"Success: $value")
  case Left(error) => println(s"Error: $error")
}
// Output: Success: 42

parseIntEither("abc") match {
  case Right(value) => println(s"Success: $value")
  case Left(error) => println(s"Error: $error")
}
// Output: Error: 'abc' is not a valid integer

Combining multiple Either operations:

def processOrder(idStr: String, amountStr: String): Either[String, (Int, Double)] = {
  for {
    id <- parseIntEither(idStr)
    amount <- parseDoubleEither(amountStr)
    validAmount <- if (amount > 0) Right(amount) 
                   else Left("Amount must be positive")
  } yield (id, validAmount)
}

processOrder("1001", "250.50") match {
  case Right((id, amount)) => println(s"Order $id: $$${amount}")
  case Left(error) => println(s"Invalid order: $error")
}
// Output: Order 1001: $250.5

Custom Conversion with Validation

For complex validation requirements, create custom parsers with business logic:

sealed trait ParseError
case class InvalidFormat(input: String) extends ParseError
case class OutOfRange(value: Int, min: Int, max: Int) extends ParseError

def parseAge(s: String): Either[ParseError, Int] = {
  Try(s.toInt) match {
    case Success(age) if age >= 0 && age <= 150 => Right(age)
    case Success(age) => Left(OutOfRange(age, 0, 150))
    case Failure(_) => Left(InvalidFormat(s))
  }
}

parseAge("25") match {
  case Right(age) => println(s"Valid age: $age")
  case Left(InvalidFormat(input)) => println(s"'$input' is not a number")
  case Left(OutOfRange(value, min, max)) => 
    println(s"$value is outside valid range $min-$max")
}

Performance Considerations

When parsing large datasets, consider using Java’s parsing methods directly for better performance:

import java.lang.{Integer, Double => JDouble}

def fastParseInt(s: String): Option[Int] = {
  try {
    Some(Integer.parseInt(s))
  } catch {
    case _: NumberFormatException => None
  }
}

def fastParseDouble(s: String): Option[Double] = {
  try {
    Some(JDouble.parseDouble(s))
  } catch {
    case _: NumberFormatException => None
  }
}

// Benchmark-friendly batch processing
def parseIntList(strings: List[String]): List[Int] = {
  strings.flatMap(s => Try(s.toInt).toOption)
}

val numbers = List("1", "2", "invalid", "4", "5")
println(parseIntList(numbers)) // List(1, 2, 4, 5)

Working with Different Number Bases

Parse hexadecimal, octal, or binary strings using Java’s Integer.parseInt with radix:

def parseHex(s: String): Option[Int] = {
  try {
    Some(Integer.parseInt(s, 16))
  } catch {
    case _: NumberFormatException => None
  }
}

println(parseHex("FF")) // Some(255)
println(parseHex("1A3")) // Some(419)

def parseBinary(s: String): Option[Int] = {
  try {
    Some(Integer.parseInt(s, 2))
  } catch {
    case _: NumberFormatException => None
  }
}

println(parseBinary("1010")) // Some(10)
println(parseBinary("11111111")) // Some(255)

Choose conversion methods based on your error handling requirements: toInt/toDouble for guaranteed valid input, Option for simple presence/absence semantics, Try for exception wrapping, and Either for detailed error reporting. In production systems, prefer explicit error handling over exception throwing to maintain predictable control flow.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.