Search code examples
scalaparsingexceptionparser-combinators

How can I make my parser fail gracefully if an exception is thrown?


Here is my attempt at writing a small parser for positive Ints:

import scala.util.parsing.combinator.RegexParsers

object PositiveIntParser extends RegexParsers {

  private def positiveInt: Parser[Int] = """0*[1-9]\d*""".r ^^ { _.toInt }

  def apply(input: String): Option[Int] = parseAll(positiveInt, input) match {
    case Success(result, _) => Some(result)
    case _ => None
  }

}

The problem is that, if the input string is too long, toInt throws an NumberFormatException, which makes my parser blow up:

scala> :load PositiveIntParser.scala
Loading PositiveIntParser.scala...
import scala.util.parsing.combinator.RegexParsers
defined object PositiveIntParser

scala> PositiveIntParser("12")
res0: Option[Int] = Some(12)

scala> PositiveIntParser("-12")
res1: Option[Int] = None

scala> PositiveIntParser("123123123123123123")
java.lang.NumberFormatException: For input string: "123123123123123123"
  at ...

Instead, I would like my positiveInt parser to fail gracefully (by returning a Failure) when toInt throws an exception. How can I do that?

An easy fix that comes to mind consists in limiting the length of the strings accepted by my regex, but that's unsatisfactory.

I'm guessing that a parser combinator for this use case is already provided by the scala.util.parsing.combinator library, but I've been unable to find one...


Solution

  • You can use a combinator accepting a partial function (inspired by how to make scala parser fail):

    private def positiveInt: Parser[Int] = """0*[1-9]\d*""".r ^? {
      case x if Try(x.toInt).isSuccess => x.toInt
    }
    

    If you want to avoid the double conversion, you can create an extractor to perform the matching and conversion:

    object ParsedInt {
      def unapply(str: String): Option[Int] = Try(str.toInt).toOption
    }
    
    private def positiveInt: Parser[Int] = """0*[1-9]\d*""".r ^? { case ParsedInt(x) => x }
    

    It is also possible to move the positiveness test into the case condition, which I find more readable than a bit intricate regex:

    private def positiveInt: Parser[Int] = """\d+""".r ^? { case ParsedInt(x) if x > 0 => x }
    

    As per your comment the extraction can also be performed in a separate ^^ step, as follows:

    private def positiveInt: Parser[Int] = """\d+""".r ^^
      { str => Try(str.toInt)} ^? { case util.Success(x) if x > 0 => x }