Viktor Hedefalk
Viktor Hedefalk

Reputation: 3914

scala combinator parser to keep original input

I would like to compose a parser from another parser to have the consumed input as an argument to the ast construction.

Say I have

def ingredient = amount ~ nameOfIngredient ^^ {
  case amount ~ name => Ingredient(name, amount)
}

What I'm looking for is a way to have another parser to construct an element of:

case class RecipeRow(orginalText: String, ingredient: Ingredient)

So I'm looking for a way to retrieve the original consumed input to the parser in a composition. Maybe something like:

def recipeRow = ingredient withConsumedInput ^^ {
  case (amount ~ name, consumed) => RecipeRow(consumed, Ingredient(name, amount))
}

I guess the signature in this case would be:

def withConsumedInput [U](p: => Parser[U]): Parser[(U, String)]

Is there another simple way to get what I want or do I need to write that thing? It feels like it probably is a better way…

Upvotes: 2

Views: 317

Answers (1)

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297155

Not easy, actually.

Let's start with Parser: what can it give us? Well, a Parser extends Input => ParseResult, so we have to extract the information from either one.

The type Input is an alias, on RegexParsers anyway, for scala.util.parsing.input.Reader[Char]. There's very little there to help us, unless it happens to be a Reader of CharSequence, in which case we can use source and offset. Let's use that then.

Now, ParseResult has many subclasses, but we are only interested in Success, which has a next: Input field. Using that, we may try this:

def withConsumedInput [U](p: => Parser[U]): Parser[(U, String)] = new Parser[(U, String)] {
  def apply(in: Input) = p(in) match {
    case Success(result, next) =>
      val parsedString = in.source.subSequence(in.offset, next.offset).toString
      Success(result -> parsedString, next)
    case other: NoSuccess      => other
  }
}

It will capture any skipped whitespaces, though. You can adapt it to avoid that automatically:

def withConsumedInput [U](p: => Parser[U]): Parser[(U, String)] = new Parser[(U, String)] {
  def apply(in: Input) = p(in) match {
    case Success(result, next) =>
      val parsedString = in.source.subSequence(handleWhiteSpace(in.source, in.offset), next.offset).toString
      Success(result -> parsedString, next)
    case other: NoSuccess      => other
  }
}

Upvotes: 4

Related Questions