scalaparsingparser-combinatorsalternation

Scala parser combinators: parse either integer or float


EDIT: Solved, see "FIX" below

I am trying to setup a scala parser combinator to parse either a float or an integer depending on the complexity of the number. Here is what I have currently:

import scala.util.parsing.combinator.JavaTokenParsers

trait NumberLiteral
case class IntegerLiteral(i:Int) extends NumberLiteral
case class FloatLiteral(f:Float) extends NumberLiteral

class Parser extends JavaTokenParsers {

  def integer:Parser[IntegerLiteral] = wholeNumber ^^ {i => new IntegerLiteral(i.toInt)}
  def float:Parser[FloatLiteral] = floatingPointNumber ^^ {f => new FloatLiteral(f.toFloat)}
  //FIX: def float:Parser[FloatLiteral] = """[+-]?[0-9]*((\.[0-9]+([eE][+-]?[0-9]+)?[fF]?)|([fF])|([eE][+-]?[0-9]+))\b""".r ^^ {f => new FloatLiteral(f.toFloat)} 

  def number:Parser[NumberLiteral] = integer | float;
  //FIX: def number:Parser[NumberLiteral] = float | integer;

}

I set up scalatest to test both the integer and float parsers and they both work. Here is what my testing class looks like:

import org.scalatest._

class ParserSpec extends FlatSpec with Matchers {

  val parser = new Parser()

  "Parser" should "parse IntegerLiteral" in {
    parser.parseAll(parser.integer, "0").get should equal (new IntegerLiteral(0))
    parser.parseAll(parser.integer, "4").get should equal (new IntegerLiteral(4))
    parser.parseAll(parser.integer, "4448338").get should equal (new IntegerLiteral(4448338))
    parser.parseAll(parser.integer, "-33").get should equal (new IntegerLiteral(-33))
    parser.parseAll(parser.integer, "-10101010").get should equal (new IntegerLiteral(-10101010))
    parser.parseAll(parser.integer, "004").get should equal (new IntegerLiteral(4))
  }
  it should "parse FloatLiteral" in {
    parser.parseAll(parser.float, "1.0").get should equal (new FloatLiteral(1.0f))
    parser.parseAll(parser.float, "0").get should equal (new FloatLiteral(0))
    parser.parseAll(parser.float, "32.3").get should equal (new FloatLiteral(32.3f))
    parser.parseAll(parser.float, "3.4e3").get should equal (new FloatLiteral(3400))
    parser.parseAll(parser.float, "-10").get should equal (new FloatLiteral(-10))
    parser.parseAll(parser.float, "-4e-4").get should equal (new FloatLiteral(-0.0004f))
    parser.parseAll(parser.float, "003.4").get should equal (new FloatLiteral(3.4f))
    parser.parseAll(parser.float, "4f").get should equal (new FloatLiteral(4))
  }
  it should "parse NumberLiteral" in {
    parser.parseAll(parser.number, "32").get should equal (new IntegerLiteral(32))
    parser.parseAll(parser.number, "32.3").get should equal (new FloatLiteral(32.3f))
    parser.parseAll(parser.number, "32f").get should equal (new FloatLiteral(32))
    parser.parseAll(parser.number, "0.33").get should equal (new FloatLiteral(0.33f))
    parser.parseAll(parser.number, "32e2").get should equal (new IntegerLiteral(3200))
    parser.parseAll(parser.number, "0").get should equal (new IntegerLiteral(32))
    parser.parseAll(parser.number, "32.3e1").get should equal (new IntegerLiteral(323))
  }

}

Both the IntegerLiteral and FloatLiteral tests work perfectly. As you can see, I want to parse the number as either an IntegerLiteral or FloatLiteral depending on whether it can be parsed as an int or a float. The first line in the NumberLiteral test works however I get the following error on the second line: java.lang.RuntimeException: no result when parsing failed. I can't figure out why the parser is throwing this error as the float parser can parse 32.3. Am I doing something wrong in the number parser with the integer | float?


Solution

  • Just swap them:

    ...
    def number:Parser[NumberLiteral] = float | integer //float first
    ...
    

    Example:

    scala> parser.parseAll(parser.number, "32.3").get
    res0: NumberLiteral = FloatLiteral(32.3)
    

    The reason why it didn't work in first place is that parser did parse "32" from "32.3" as integer - and unparsed tail ".3" did cause an error. You can easily see it with parse:

    ...
    def number:Parser[NumberLiteral] = integer | float //integer first
    ...
    
    scala> parser.parse(parser.number, "32.3")
    res3: parser.ParseResult[NumberLiteral] = [1.3] parsed: IntegerLiteral(32)
    
    //And here is how to get unparsed tail (".3"): 
    
    scala> val pointer = parser.parse(parser.number, "32.3").next
    pointer: parser.Input = scala.util.parsing.input.CharSequenceReader@34a2d29d
    
    scala> pointer.source.toString.drop(pointer.pos.column - 1)
    res15: String = .3