Search code examples
parsinghaskellparsecparser-combinatorstrifecta

How to combine parsers up to n times in Haskell?


I'm trying to combine parsers in Haskell in such a way that I could parse certain patterns up to n times. To illustrate, imagine I want to parse up to eight digits from the input. I know I can use count from Text.Parser.Combinators to parse exactly n occurrences, e.g.:

import Text.Parser.Char        (digit)
import Text.Parser.Combinators (count)

eightDigits :: Parser [Char]
eightDigits = count 8 digit

This, however, fails if it doesn't find exactly 8 digits. I could also use some to parse one or more digits:

import Text.Parser.Char        (digit)
import Text.Parser.Combinators (some)

someDigits :: Parser [Char]
someDigits = some digit

The problem with the above is that it may consume more digits than I want. Finally, I could use try, which combine parsers that may consume input and, on failure, go back to where it started:

import Text.Parser.Char        (digit)
import Text.Parser.Combinators (count, try)
import Control.Applicative     ((<|>))

twoOrThreeDigits :: Parser [Char]
twoOrThreeDigits = try (count 3 digit) <|> count 2 digit

While this could be extended to up to 8 repetitions, it's not scalable nor elegant, so the question is how can I combine parsers to parse a pattern anywhere between 1 and up to n times?


Solution

  • You could construct a many-like combinator with an upper limit:

    upto :: Int -> Parser a -> Parser [a]
    upto n p | n > 0 = (:) <$> try p <*> upto (n-1) p <|> return []
    upto _ _ = return []
    

    And for 1 up to n, a many1-like combinator:

    upto1 :: Int -> Parser a -> Parser [a]
    upto1 n p | n > 0 = (:) <$> p <*> upto (n-1) p
    upto1 _ _ = return []
    

    A short demo:

    > map (parse (upto 8 digitChar) "") ["", "123", "1234567890"]
    [Right "",Right "123",Right "12345678"]