packages feed

typed-range-0.1.0.0: Data/Range/Typed/Parser.hs

{-# LANGUAGE FlexibleContexts #-}

-- | This package provides a simple range parser.
--
-- This range parser was designed to be a useful tool for CLI programs. For example, by
-- default, this example depicts how the parser works:
--
-- >>> parseRanges "-5,8-10,13-15,20-" :: Either ParseError [AnyRange Integer]
-- Right [UpperBoundRange 5,SpanRange 8 10,SpanRange 13 15,LowerBoundRange 20]
-- (0.01 secs, 681,792 bytes)
--
-- And the * character translates to an infinite range. This is very useful for accepting
-- ranges as input in CLI programs, but not as useful for parsing .cabal or package.json files.
--
-- To handle more complex parsing cases it is recommended that you use the ranges library
-- in conjunction with parsec or Alex/Happy and convert the versions that you find into
-- ranges.
module Data.Range.Typed.Parser
  ( parseRanges,
    customParseRanges,
    RangeParserArgs (..),
    defaultArgs,
    ranges,
    ParseError,
  )
where

import Data.Range.Typed
import Text.Parsec
import Text.Parsec.String

-- | These are the arguments that will be used when parsing a string as a range.
data RangeParserArgs = Args
  { -- | A separator that represents a union.
    unionSeparator :: String,
    -- | A separator that separates the two halves of a range.
    rangeSeparator :: String,
    -- | A separator that implies an unbounded range.
    wildcardSymbol :: String
  }
  deriving (Show)

-- | These are the default arguments that are used by the parser. Please feel free to use
-- the default arguments for you own parser and modify it from the defaults at will.
defaultArgs :: RangeParserArgs
defaultArgs =
  Args
    { unionSeparator = ",",
      rangeSeparator = "-",
      wildcardSymbol = "*"
    }

-- | Given a string, this function will either return a parse error back to the user or the
-- list of ranges that are represented by the parsed string. Very useful for CLI programs
-- that need to load ranges from a single-line string.
parseRanges :: (Read a) => String -> Either ParseError [AnyRange a]
parseRanges = parse (ranges defaultArgs) "(range parser)"

-- | If you disagree with the default characters for separating ranges then this function can
-- be used to customise them, up to a point.
customParseRanges :: (Read a) => RangeParserArgs -> String -> Either ParseError [AnyRange a]
customParseRanges args = parse (ranges args) "(range parser)"

string_ :: (Stream s m Char) => String -> ParsecT s u m ()
string_ x = string x >> return ()

-- | Given the parser arguments this returns a parsec parser that is capable of parsing a list of
-- ranges.
ranges :: (Read a) => RangeParserArgs -> Parser [AnyRange a]
ranges args = range `sepBy` (string $ unionSeparator args)
  where
    range :: (Read a) => Parser (AnyRange a)
    range =
      choice
        [ infiniteRange,
          spanRange,
          singletonRange
        ]

    infiniteRange :: (Read a) => Parser (AnyRange a)
    infiniteRange = do
      string_ $ wildcardSymbol args
      return $ anyRange InfiniteRange

    spanRange :: (Read a) => Parser (AnyRange a)
    spanRange = try $ do
      first <- readSection
      string_ $ rangeSeparator args
      second <- readSection
      case (first, second) of
        (Just x, Just y) -> return $ anyRange $ SpanRange (InclusiveBound x) (InclusiveBound y)
        (Just x, _) -> return $ anyRange $ LowerBoundRange (InclusiveBound x)
        (_, Just y) -> return $ anyRange $ UpperBoundRange (InclusiveBound y)
        _ -> parserFail ("Range should have a number on one end: " ++ rangeSeparator args)

    singletonRange :: (Read a) => Parser (AnyRange a)
    singletonRange = fmap (anyRange . SingletonRange . read) $ many1 digit

readSection :: (Read a) => Parser (Maybe a)
readSection = fmap (fmap read) $ optionMaybe (many1 digit)