Search code examples
scalaparboiled

Parboiled - how to parse a real number?


I took this from a project that claims to parse real numbers, but it somehow eats the pre-decimal part:

object Main extends App {
  import org.parboiled.scala._

  val res = TestParser.parseDouble("2.3")
  println(s"RESULT: ${res.result}")

  object TestParser extends Parser {
    def RealNumber = rule { 
      oneOrMore(Digit) ~ optional( "." ~ oneOrMore(Digit) ) ~> { s =>
        println(s"CAPTURED '$s'")
        s.toDouble
      }
    }
    def Digit = rule { "0" - "9" }

    def parseDouble(input: String): ParsingResult[Double] =
      ReportingParseRunner(RealNumber).run(input)
  }
}

This prints:

CAPTURED '.3'
RESULT: Some(0.3)

What is wrong here? Note that currently I cannot go from Parboiled-1 to Parboiled-2, because I have a larger grammar that would have to be rewritten.


Solution

  • As stated in parboiled documentation, action rules like ~> take the match of the immediately preceding peer rule. In the rule sequence oneOrMore(Digit) ~ optional( "." ~ oneOrMore(Digit) ) the immediately preceding rule is optional( "." ~ oneOrMore(Digit) ), so you get only its match ".3" in the action rule.

    To fix that you may, for example, extract the first two elements into a separate rule:

    def RealNumberString = rule { 
      oneOrMore(Digit) ~ optional( "." ~ oneOrMore(Digit) ) 
    }
    
    def RealNumber = rule {
      RealNumberString ~> { s =>
        println(s"CAPTURED '$s'")
        s.toDouble
      }
    }
    

    Or push both parts onto the stack and then combine them:

    def RealNumber = rule {
      oneOrMore(Digit) ~> identity ~ 
      optional( "." ~ oneOrMore(Digit) ) ~> identity ~~> { (s1, s2) =>
        val s = s1 + s2
        println(s"CAPTURED '$s'")
        s.toDouble
      }
    }