Merge pull request #819 from awjchen/moreParseErrors
Improving parse errors
This commit is contained in:
commit
c26674466a
@ -328,9 +328,13 @@ statusp =
|
|||||||
]
|
]
|
||||||
|
|
||||||
codep :: TextParser m Text
|
codep :: TextParser m Text
|
||||||
codep = option "" $ try $ do
|
codep = option "" $ do
|
||||||
|
try $ do
|
||||||
skipSome spacenonewline
|
skipSome spacenonewline
|
||||||
between (char '(') (char ')') $ takeWhileP Nothing (/= ')')
|
char '('
|
||||||
|
code <- takeWhileP Nothing $ \c -> c /= ')' && c /= '\n'
|
||||||
|
char ')' <?> "closing bracket ')' for transaction code"
|
||||||
|
pure code
|
||||||
|
|
||||||
descriptionp :: TextParser m Text
|
descriptionp :: TextParser m Text
|
||||||
descriptionp = takeWhileP Nothing (not . semicolonOrNewline)
|
descriptionp = takeWhileP Nothing (not . semicolonOrNewline)
|
||||||
@ -399,31 +403,55 @@ datep' mYear = do
|
|||||||
-- Leading zeroes may be omitted (except in a timezone).
|
-- Leading zeroes may be omitted (except in a timezone).
|
||||||
datetimep :: JournalParser m LocalTime
|
datetimep :: JournalParser m LocalTime
|
||||||
datetimep = do
|
datetimep = do
|
||||||
day <- datep
|
mYear <- getYear
|
||||||
lift $ skipSome spacenonewline
|
lift $ datetimep' mYear
|
||||||
h <- some digitChar
|
|
||||||
let h' = read h
|
datetimep' :: Maybe Year -> TextParser m LocalTime
|
||||||
guard $ h' >= 0 && h' <= 23
|
datetimep' mYear = do
|
||||||
char ':'
|
day <- datep' mYear
|
||||||
m <- some digitChar
|
skipSome spacenonewline
|
||||||
let m' = read m
|
time <- timeOfDay
|
||||||
guard $ m' >= 0 && m' <= 59
|
optional timeZone -- ignoring time zones
|
||||||
s <- optional $ char ':' >> some digitChar
|
pure $ LocalTime day time
|
||||||
let s' = case s of Just sstr -> read sstr
|
|
||||||
Nothing -> 0
|
where
|
||||||
guard $ s' >= 0 && s' <= 59
|
timeOfDay :: TextParser m TimeOfDay
|
||||||
{- tz <- -}
|
timeOfDay = do
|
||||||
optional $ do
|
pos1 <- getPosition
|
||||||
plusminus <- oneOf ("-+" :: [Char])
|
h' <- twoDigitDecimal <?> "hour"
|
||||||
d1 <- digitChar
|
pos2 <- getPosition
|
||||||
d2 <- digitChar
|
unless (h' >= 0 && h' <= 23) $ parseErrorAtRegion pos1 pos2
|
||||||
d3 <- digitChar
|
"invalid time (bad hour)"
|
||||||
d4 <- digitChar
|
|
||||||
return $ plusminus:d1:d2:d3:d4:""
|
char ':' <?> "':' (hour-minute separator)"
|
||||||
-- ltz <- liftIO $ getCurrentTimeZone
|
pos3 <- getPosition
|
||||||
-- let tz' = maybe ltz (fromMaybe ltz . parseTime defaultTimeLocale "%z") tz
|
m' <- twoDigitDecimal <?> "minute"
|
||||||
-- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
|
pos4 <- getPosition
|
||||||
return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
|
unless (m' >= 0 && m' <= 59) $ parseErrorAtRegion pos3 pos4
|
||||||
|
"invalid time (bad minute)"
|
||||||
|
|
||||||
|
s' <- option 0 $ do
|
||||||
|
char ':' <?> "':' (minute-second separator)"
|
||||||
|
pos5 <- getPosition
|
||||||
|
s' <- twoDigitDecimal <?> "second"
|
||||||
|
pos6 <- getPosition
|
||||||
|
unless (s' >= 0 && s' <= 59) $ parseErrorAtRegion pos5 pos6
|
||||||
|
"invalid time (bad second)" -- we do not support leap seconds
|
||||||
|
pure s'
|
||||||
|
|
||||||
|
pure $ TimeOfDay h' m' (fromIntegral s')
|
||||||
|
|
||||||
|
twoDigitDecimal :: TextParser m Int
|
||||||
|
twoDigitDecimal = do
|
||||||
|
d1 <- digitToInt <$> digitChar
|
||||||
|
d2 <- digitToInt <$> (digitChar <?> "a second digit")
|
||||||
|
pure $ d1*10 + d2
|
||||||
|
|
||||||
|
timeZone :: TextParser m String
|
||||||
|
timeZone = do
|
||||||
|
plusminus <- satisfy $ \c -> c == '-' || c == '+'
|
||||||
|
fourDigits <- count 4 (digitChar <?> "a digit (for a time zone)")
|
||||||
|
pure $ plusminus:fourDigits
|
||||||
|
|
||||||
secondarydatep :: Day -> TextParser m Day
|
secondarydatep :: Day -> TextParser m Day
|
||||||
secondarydatep primaryDate = char '=' *> datep' (Just primaryYear)
|
secondarydatep primaryDate = char '=' *> datep' (Just primaryYear)
|
||||||
@ -493,14 +521,85 @@ test_spaceandamountormissingp = do
|
|||||||
-- right, optional unit or total price, and optional (ignored)
|
-- right, optional unit or total price, and optional (ignored)
|
||||||
-- ledger-style balance assertion or fixed lot price declaration.
|
-- ledger-style balance assertion or fixed lot price declaration.
|
||||||
amountp :: JournalParser m Amount
|
amountp :: JournalParser m Amount
|
||||||
amountp = do
|
amountp = label "amount" $ do
|
||||||
amount <- amountwithoutpricep
|
amount <- amountwithoutpricep
|
||||||
|
lift $ skipMany spacenonewline
|
||||||
price <- priceamountp
|
price <- priceamountp
|
||||||
pure $ amount { aprice = price }
|
pure $ amount { aprice = price }
|
||||||
|
|
||||||
amountwithoutpricep :: JournalParser m Amount
|
amountwithoutpricep :: JournalParser m Amount
|
||||||
amountwithoutpricep =
|
amountwithoutpricep = do
|
||||||
try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp
|
(mult, sign) <- lift $ (,) <$> multiplierp <*> signp
|
||||||
|
leftsymbolamountp mult sign <|> rightornosymbolamountp mult sign
|
||||||
|
|
||||||
|
where
|
||||||
|
|
||||||
|
leftsymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount
|
||||||
|
leftsymbolamountp mult sign = label "amount" $ do
|
||||||
|
c <- lift commoditysymbolp
|
||||||
|
suggestedStyle <- getAmountStyle c
|
||||||
|
|
||||||
|
commodityspaced <- lift $ skipMany' spacenonewline
|
||||||
|
|
||||||
|
sign2 <- lift $ signp
|
||||||
|
posBeforeNum <- getPosition
|
||||||
|
ambiguousRawNum <- lift rawnumberp
|
||||||
|
mExponent <- lift $ optional $ try exponentp
|
||||||
|
posAfterNum <- getPosition
|
||||||
|
let numRegion = (posBeforeNum, posAfterNum)
|
||||||
|
|
||||||
|
(q,prec,mdec,mgrps) <- lift $
|
||||||
|
interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
|
||||||
|
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
|
||||||
|
return $ Amount c (sign (sign2 q)) NoPrice s mult
|
||||||
|
|
||||||
|
rightornosymbolamountp
|
||||||
|
:: Bool -> (Decimal -> Decimal) -> JournalParser m Amount
|
||||||
|
rightornosymbolamountp mult sign = label "amount" $ do
|
||||||
|
posBeforeNum <- getPosition
|
||||||
|
ambiguousRawNum <- lift rawnumberp
|
||||||
|
mExponent <- lift $ optional $ try exponentp
|
||||||
|
posAfterNum <- getPosition
|
||||||
|
let numRegion = (posBeforeNum, posAfterNum)
|
||||||
|
|
||||||
|
mSpaceAndCommodity <- lift $ optional $ try $
|
||||||
|
(,) <$> skipMany' spacenonewline <*> commoditysymbolp
|
||||||
|
|
||||||
|
case mSpaceAndCommodity of
|
||||||
|
Just (commodityspaced, c) -> do
|
||||||
|
suggestedStyle <- getAmountStyle c
|
||||||
|
(q,prec,mdec,mgrps) <- lift $
|
||||||
|
interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
|
||||||
|
|
||||||
|
let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
|
||||||
|
return $ Amount c (sign q) NoPrice s mult
|
||||||
|
|
||||||
|
Nothing -> do
|
||||||
|
suggestedStyle <- getDefaultAmountStyle
|
||||||
|
(q,prec,mdec,mgrps) <- lift $
|
||||||
|
interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
|
||||||
|
|
||||||
|
-- apply the most recently seen default commodity and style to this commodityless amount
|
||||||
|
defcs <- getDefaultCommodityAndStyle
|
||||||
|
let (c,s) = case defcs of
|
||||||
|
Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})
|
||||||
|
Nothing -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})
|
||||||
|
return $ Amount c (sign q) NoPrice s mult
|
||||||
|
|
||||||
|
-- For reducing code duplication. Doesn't parse anything. Has the type
|
||||||
|
-- of a parser only in order to throw parse errors (for convenience).
|
||||||
|
interpretNumber
|
||||||
|
:: (SourcePos, SourcePos)
|
||||||
|
-> Maybe AmountStyle
|
||||||
|
-> Either AmbiguousNumber RawNumber
|
||||||
|
-> Maybe Int
|
||||||
|
-> TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
|
||||||
|
interpretNumber posRegion suggestedStyle ambiguousNum mExp =
|
||||||
|
let rawNum = either (disambiguateNumber suggestedStyle) id ambiguousNum
|
||||||
|
in case fromRawNumber rawNum mExp of
|
||||||
|
Left errMsg -> uncurry parseErrorAtRegion posRegion errMsg
|
||||||
|
Right res -> pure res
|
||||||
|
|
||||||
|
|
||||||
#ifdef TESTS
|
#ifdef TESTS
|
||||||
test_amountp = do
|
test_amountp = do
|
||||||
@ -545,50 +644,6 @@ skipMany' p = go False
|
|||||||
then go True
|
then go True
|
||||||
else pure isNull
|
else pure isNull
|
||||||
|
|
||||||
leftsymbolamountp :: JournalParser m Amount
|
|
||||||
leftsymbolamountp = do
|
|
||||||
sign <- lift signp
|
|
||||||
m <- lift multiplierp
|
|
||||||
c <- lift commoditysymbolp
|
|
||||||
suggestedStyle <- getAmountStyle c
|
|
||||||
commodityspaced <- lift $ skipMany' spacenonewline
|
|
||||||
(q,prec,mdec,mgrps) <- lift $ numberp suggestedStyle
|
|
||||||
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
|
|
||||||
return $ Amount c (sign q) NoPrice s m
|
|
||||||
<?> "left-symbol amount"
|
|
||||||
|
|
||||||
rightsymbolamountp :: JournalParser m Amount
|
|
||||||
rightsymbolamountp = do
|
|
||||||
m <- lift multiplierp
|
|
||||||
sign <- lift signp
|
|
||||||
ambiguousRawNum <- lift rawnumberp
|
|
||||||
mExponent <- lift $ optional $ try exponentp
|
|
||||||
commodityspaced <- lift $ skipMany' spacenonewline
|
|
||||||
c <- lift commoditysymbolp
|
|
||||||
suggestedStyle <- getAmountStyle c
|
|
||||||
|
|
||||||
let rawNum = either (disambiguateNumber suggestedStyle) id ambiguousRawNum
|
|
||||||
(q, prec, mdec, mgrps) <- case fromRawNumber rawNum mExponent of
|
|
||||||
Left errMsg -> fail errMsg
|
|
||||||
Right res -> pure res
|
|
||||||
|
|
||||||
let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
|
|
||||||
return $ Amount c (sign q) NoPrice s m
|
|
||||||
<?> "right-symbol amount"
|
|
||||||
|
|
||||||
nosymbolamountp :: JournalParser m Amount
|
|
||||||
nosymbolamountp = do
|
|
||||||
m <- lift multiplierp
|
|
||||||
suggestedStyle <- getDefaultAmountStyle
|
|
||||||
(q,prec,mdec,mgrps) <- lift $ numberp suggestedStyle
|
|
||||||
-- apply the most recently seen default commodity and style to this commodityless amount
|
|
||||||
defcs <- getDefaultCommodityAndStyle
|
|
||||||
let (c,s) = case defcs of
|
|
||||||
Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})
|
|
||||||
Nothing -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})
|
|
||||||
return $ Amount c q NoPrice s m
|
|
||||||
<?> "no-symbol amount"
|
|
||||||
|
|
||||||
commoditysymbolp :: TextParser m CommoditySymbol
|
commoditysymbolp :: TextParser m CommoditySymbol
|
||||||
commoditysymbolp =
|
commoditysymbolp =
|
||||||
quotedcommoditysymbolp <|> simplecommoditysymbolp <?> "commodity symbol"
|
quotedcommoditysymbolp <|> simplecommoditysymbolp <?> "commodity symbol"
|
||||||
@ -602,23 +657,24 @@ simplecommoditysymbolp :: TextParser m CommoditySymbol
|
|||||||
simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar)
|
simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar)
|
||||||
|
|
||||||
priceamountp :: JournalParser m Price
|
priceamountp :: JournalParser m Price
|
||||||
priceamountp = option NoPrice $ try $ do
|
priceamountp = option NoPrice $ do
|
||||||
lift (skipMany spacenonewline)
|
|
||||||
char '@'
|
char '@'
|
||||||
priceConstructor <- char '@' *> pure TotalPrice <|> pure UnitPrice
|
priceConstructor <- char '@' *> pure TotalPrice <|> pure UnitPrice
|
||||||
|
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
priceAmount <- amountwithoutpricep
|
priceAmount <- amountwithoutpricep <?> "amount (as a price)"
|
||||||
|
|
||||||
pure $ priceConstructor priceAmount
|
pure $ priceConstructor priceAmount
|
||||||
|
|
||||||
partialbalanceassertionp :: JournalParser m BalanceAssertion
|
partialbalanceassertionp :: JournalParser m BalanceAssertion
|
||||||
partialbalanceassertionp = optional $ try $ do
|
partialbalanceassertionp = optional $ do
|
||||||
|
sourcepos <- try $ do
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
sourcepos <- genericSourcePos <$> lift getPosition
|
sourcepos <- genericSourcePos <$> lift getPosition
|
||||||
char '='
|
char '='
|
||||||
|
pure sourcepos
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
a <- amountp -- XXX should restrict to a simple amount
|
a <- amountp <?> "amount (for a balance assertion or assignment)" -- XXX should restrict to a simple amount
|
||||||
return (a, sourcepos)
|
return (a, sourcepos)
|
||||||
|
|
||||||
-- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount)
|
-- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount)
|
||||||
@ -633,7 +689,8 @@ partialbalanceassertionp = optional $ try $ do
|
|||||||
|
|
||||||
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
|
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
|
||||||
fixedlotpricep :: JournalParser m (Maybe Amount)
|
fixedlotpricep :: JournalParser m (Maybe Amount)
|
||||||
fixedlotpricep = optional $ try $ do
|
fixedlotpricep = optional $ do
|
||||||
|
try $ do
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
char '{'
|
char '{'
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
@ -657,7 +714,7 @@ fixedlotpricep = optional $ try $ do
|
|||||||
-- and the digit group style if any.
|
-- and the digit group style if any.
|
||||||
--
|
--
|
||||||
numberp :: Maybe AmountStyle -> TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
|
numberp :: Maybe AmountStyle -> TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
|
||||||
numberp suggestedStyle = do
|
numberp suggestedStyle = label "number" $ do
|
||||||
-- a number is an optional sign followed by a sequence of digits possibly
|
-- a number is an optional sign followed by a sequence of digits possibly
|
||||||
-- interspersed with periods, commas, or both
|
-- interspersed with periods, commas, or both
|
||||||
-- ptrace "numberp"
|
-- ptrace "numberp"
|
||||||
@ -669,10 +726,9 @@ numberp suggestedStyle = do
|
|||||||
$ fromRawNumber rawNum mExp of
|
$ fromRawNumber rawNum mExp of
|
||||||
Left errMsg -> fail errMsg
|
Left errMsg -> fail errMsg
|
||||||
Right (q, p, d, g) -> pure (sign q, p, d, g)
|
Right (q, p, d, g) -> pure (sign q, p, d, g)
|
||||||
<?> "numberp"
|
|
||||||
|
|
||||||
exponentp :: TextParser m Int
|
exponentp :: TextParser m Int
|
||||||
exponentp = char' 'e' *> signp <*> decimal <?> "exponentp"
|
exponentp = char' 'e' *> signp <*> decimal <?> "exponent"
|
||||||
|
|
||||||
-- | Interpret a raw number as a decimal number.
|
-- | Interpret a raw number as a decimal number.
|
||||||
--
|
--
|
||||||
@ -706,8 +762,8 @@ fromRawNumber raw mExp = case raw of
|
|||||||
(quantity, precision) = toQuantity (mconcat digitGrps) decimalGrp
|
(quantity, precision) = toQuantity (mconcat digitGrps) decimalGrp
|
||||||
|
|
||||||
in Right (quantity, precision, mDecPt, Just digitGroupStyle)
|
in Right (quantity, precision, mDecPt, Just digitGroupStyle)
|
||||||
Just _ ->
|
Just _ -> Left
|
||||||
Left "mixing digit separators with exponents is not allowed"
|
"invalid number: mixing digit separators with exponents is not allowed"
|
||||||
|
|
||||||
where
|
where
|
||||||
-- Outputs digit group sizes from least significant to most significant
|
-- Outputs digit group sizes from least significant to most significant
|
||||||
@ -764,10 +820,20 @@ disambiguateNumber suggestedStyle (AmbiguousNumber grp1 sep grp2) =
|
|||||||
-- Right (WithSeparators ' ' ["1","000"] Nothing)
|
-- Right (WithSeparators ' ' ["1","000"] Nothing)
|
||||||
--
|
--
|
||||||
rawnumberp :: TextParser m (Either AmbiguousNumber RawNumber)
|
rawnumberp :: TextParser m (Either AmbiguousNumber RawNumber)
|
||||||
rawnumberp = label "rawnumberp" $ do
|
rawnumberp = label "number" $ do
|
||||||
rawNumber <- fmap Right leadingDecimalPt <|> leadingDigits
|
rawNumber <- fmap Right leadingDecimalPt <|> leadingDigits
|
||||||
|
|
||||||
-- Guard against mistyped numbers
|
-- Guard against mistyped numbers
|
||||||
notFollowedBy $ satisfy isDecimalPointChar <|> char ' ' *> digitChar
|
mExtraDecimalSep <- optional $ lookAhead $ satisfy isDecimalPointChar
|
||||||
|
when (isJust mExtraDecimalSep) $
|
||||||
|
fail "invalid number (invalid use of separator)"
|
||||||
|
|
||||||
|
mExtraFragment <- optional $ lookAhead $ try $
|
||||||
|
char ' ' *> getPosition <* digitChar
|
||||||
|
case mExtraFragment of
|
||||||
|
Just pos -> parseErrorAt pos "invalid number (excessive trailing digits)"
|
||||||
|
Nothing -> pure ()
|
||||||
|
|
||||||
return $ dbg8 "rawnumberp" rawNumber
|
return $ dbg8 "rawnumberp" rawNumber
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -843,7 +909,7 @@ instance Monoid DigitGrp where
|
|||||||
mappend = (Sem.<>)
|
mappend = (Sem.<>)
|
||||||
|
|
||||||
digitgroupp :: TextParser m DigitGrp
|
digitgroupp :: TextParser m DigitGrp
|
||||||
digitgroupp = label "digit group"
|
digitgroupp = label "digits"
|
||||||
$ makeGroup <$> takeWhile1P (Just "digit") isDigit
|
$ makeGroup <$> takeWhile1P (Just "digit") isDigit
|
||||||
where
|
where
|
||||||
makeGroup = uncurry DigitGrp . foldl' step (0, 0) . T.unpack
|
makeGroup = uncurry DigitGrp . foldl' step (0, 0) . T.unpack
|
||||||
|
|||||||
@ -636,7 +636,8 @@ postingp mTransactionYear = do
|
|||||||
account <- modifiedaccountnamep
|
account <- modifiedaccountnamep
|
||||||
return (status, account)
|
return (status, account)
|
||||||
let (ptype, account') = (accountNamePostingType account, textUnbracket account)
|
let (ptype, account') = (accountNamePostingType account, textUnbracket account)
|
||||||
amount <- spaceandamountormissingp
|
lift (skipMany spacenonewline)
|
||||||
|
amount <- option missingmixedamt $ Mixed . (:[]) <$> amountp
|
||||||
massertion <- partialbalanceassertionp
|
massertion <- partialbalanceassertionp
|
||||||
_ <- fixedlotpricep
|
_ <- fixedlotpricep
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user