dev: lib: clarify some amount parsers; describe Ledger lot notation

amountpwithmultiplier -> amountp'
amountpnolotpricesp   -> amountnobasisp
amountwithoutpricep   -> simpleamountp
priceamountp          -> costp
This commit is contained in:
Simon Michael 2022-12-22 09:03:13 -10:00
parent 04d5813a41
commit aa54c3273a
2 changed files with 66 additions and 33 deletions

View File

@ -76,9 +76,9 @@ module Hledger.Read.Common (
-- ** amounts -- ** amounts
spaceandamountormissingp, spaceandamountormissingp,
amountp, amountp,
amountpwithmultiplier, amountp',
commoditysymbolp, commoditysymbolp,
priceamountp, costp,
balanceassertionp, balanceassertionp,
lotpricep, lotpricep,
numberp, numberp,
@ -675,21 +675,42 @@ singlespacep = spacenonewline *> notFollowedBy spacenonewline
--- *** amounts --- *** amounts
-- | Parse whitespace then an amount, with an optional left or right -- | Parse whitespace then an amount, or return the special "missing" marker amount.
-- currency symbol and optional price, or return the special
-- "missing" marker amount.
spaceandamountormissingp :: JournalParser m MixedAmount spaceandamountormissingp :: JournalParser m MixedAmount
spaceandamountormissingp = spaceandamountormissingp =
option missingmixedamt $ try $ do option missingmixedamt $ try $ do
lift $ skipNonNewlineSpaces1 lift $ skipNonNewlineSpaces1
mixedAmount <$> amountp mixedAmount <$> amountp
-- | Parse a single-commodity amount, with optional symbol on the left -- | Parse a single-commodity amount, applying the default commodity if there is no commodity symbol;
-- or right, followed by, in any order: an optional transaction price, -- optionally followed by, in any order:
-- an optional ledger-style lot price, and/or an optional ledger-style -- a Ledger-style cost and/or one or more parts of a Ledger-style cost basis:
-- lot date. A lot price and lot date will be ignored. -- lot cost, lot date, and/or lot note (we loosely call this triple the lot's cost basis).
-- The cost basis makes it a lot rather than just an amount. The cost basis info is discarded for now.
-- The main amount's sign is significant; here are the possibilities and their interpretation:
-- @
-- --
-- To parse the amount's quantity (number) we need to know which character -- AMT -- acquiring an amount
-- AMT COST -- acquiring an amount at some cost
-- AMT COST COSTBASIS -- acquiring a lot at some cost, saving its cost basis
-- AMT COSTBASIS COST -- like the above
-- AMT COSTBASIS -- like the above with cost same as the cost basis
--
-- -AMT -- releasing an amount
-- -AMT SELLPRICE -- releasing an amount at some selling price
-- -AMT SELLPRICE COSTBASISSEL -- releasing a lot at some selling price, selecting it by its cost basis
-- -AMT COSTBASISSEL SELLPRICE -- like the above
-- -AMT COSTBASISSEL -- like the above with selling price same as the selected lot's cost basis amount
--
-- COST/SELLPRICE can be @ UNITAMT, @@ TOTALAMT, (@) UNITAMT, or (@@) TOTALAMT. The () are ignored.
-- COSTBASIS is one or more of {LOTCOST}, [LOTDATE], (LOTNOTE), in any order, with LOTCOST defaulting to COST.
-- COSTBASISSEL is one or more of {LOTCOST}, [LOTDATE], (LOTNOTE), in any order.
-- {LOTCOST} can be {UNITAMT}, {{TOTALAMT}}, {=UNITAMT}, or {{=TOTALAMT}}. The = is ignored.
-- Rule of thumb: curly braces, parentheses, and/or square brackets in an amount means a Ledger-style cost basis is involved.
--
-- @
--
-- To parse an amount's numeric quantity we need to know which character
-- represents a decimal mark. We find it in one of three ways: -- represents a decimal mark. We find it in one of three ways:
-- --
-- 1. If a decimal mark has been set explicitly in the journal parse state, -- 1. If a decimal mark has been set explicitly in the journal parse state,
@ -700,38 +721,45 @@ spaceandamountormissingp =
-- --
-- 3. Otherwise we will parse any valid decimal mark appearing in the -- 3. Otherwise we will parse any valid decimal mark appearing in the
-- number, as long as the number appears well formed. -- number, as long as the number appears well formed.
-- (This means we handle files with any supported decimal mark without configuration,
-- but it also allows different decimal marks in different amounts,
-- which is a bit too loose. There's an open issue.)
-- --
-- Note 3 is the default zero-config case; it means we automatically handle
-- files with any supported decimal mark, but it also allows different decimal marks
-- in different amounts, which is a bit too loose. There's an open issue.
amountp :: JournalParser m Amount amountp :: JournalParser m Amount
amountp = amountpwithmultiplier False amountp = amountp' False
amountpwithmultiplier :: Bool -> JournalParser m Amount -- An amount with optional cost and/or cost basis, as described above.
amountpwithmultiplier mult = label "amount" $ do -- A flag indicates whether we are parsing a multiplier amount;
-- if not, a commodity-less amount will have the default commodity applied to it.
amountp' :: Bool -> JournalParser m Amount
amountp' mult = label "amount" $ do
let spaces = lift $ skipNonNewlineSpaces let spaces = lift $ skipNonNewlineSpaces
amt <- amountwithoutpricep mult <* spaces amt <- simpleamountp mult <* spaces
(mprice, _elotprice, _elotdate) <- runPermutation $ (mprice, _elotprice, _elotdate) <- runPermutation $
(,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amt <* spaces) (,,) <$> toPermutationWithDefault Nothing (Just <$> costp amt <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)
pure $ amt { aprice = mprice } pure $ amt { aprice = mprice }
amountpnolotpricesp :: JournalParser m Amount -- An amount with optional cost, but no cost basis.
amountpnolotpricesp = label "amount" $ do amountnobasisp :: JournalParser m Amount
amountnobasisp = label "amount" $ do
let spaces = lift $ skipNonNewlineSpaces let spaces = lift $ skipNonNewlineSpaces
amt <- amountwithoutpricep False amt <- simpleamountp False
spaces spaces
mprice <- optional $ priceamountp amt <* spaces mprice <- optional $ costp amt <* spaces
pure $ amt { aprice = mprice } pure $ amt { aprice = mprice }
amountwithoutpricep :: Bool -> JournalParser m Amount -- An amount with no cost or cost basis.
amountwithoutpricep mult = do -- A flag indicates whether we are parsing a multiplier amount;
-- if not, a commodity-less amount will have the default commodity applied to it.
simpleamountp :: Bool -> JournalParser m Amount
simpleamountp mult = do
sign <- lift signp sign <- lift signp
leftsymbolamountp sign <|> rightornosymbolamountp sign leftsymbolamountp sign <|> rightornosymbolamountp sign
where where
-- An amount with commodity symbol on the left.
leftsymbolamountp :: (Decimal -> Decimal) -> JournalParser m Amount leftsymbolamountp :: (Decimal -> Decimal) -> JournalParser m Amount
leftsymbolamountp sign = label "amount" $ do leftsymbolamountp sign = label "amount" $ do
c <- lift commoditysymbolp c <- lift commoditysymbolp
@ -750,6 +778,9 @@ amountwithoutpricep mult = do
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps} let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
return nullamt{acommodity=c, aquantity=sign (sign2 q), astyle=s, aprice=Nothing} return nullamt{acommodity=c, aquantity=sign (sign2 q), astyle=s, aprice=Nothing}
-- An amount with commodity symbol on the right or no commodity symbol.
-- A no-symbol amount will have the default commodity applied to it
-- unless we are parsing a multiplier amount (*AMT).
rightornosymbolamountp :: (Decimal -> Decimal) -> JournalParser m Amount rightornosymbolamountp :: (Decimal -> Decimal) -> JournalParser m Amount
rightornosymbolamountp sign = label "amount" $ do rightornosymbolamountp sign = label "amount" $ do
offBeforeNum <- getOffset offBeforeNum <- getOffset
@ -837,8 +868,10 @@ quotedcommoditysymbolp =
simplecommoditysymbolp :: TextParser m CommoditySymbol simplecommoditysymbolp :: TextParser m CommoditySymbol
simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar) simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar)
priceamountp :: Amount -> JournalParser m AmountPrice -- | Ledger-style cost notation:
priceamountp baseAmt = label "transaction price" $ do -- @ UNITAMT, @@ TOTALAMT, (@) UNITAMT, or (@@) TOTALAMT. The () are ignored.
costp :: Amount -> JournalParser m AmountPrice
costp baseAmt = label "transaction price" $ do
-- https://www.ledger-cli.org/3.0/doc/ledger3.html#Virtual-posting-costs -- https://www.ledger-cli.org/3.0/doc/ledger3.html#Virtual-posting-costs
parenthesised <- option False $ char '(' >> pure True parenthesised <- option False $ char '(' >> pure True
char '@' char '@'
@ -846,7 +879,7 @@ priceamountp baseAmt = label "transaction price" $ do
when parenthesised $ void $ char ')' when parenthesised $ void $ char ')'
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
priceAmount <- amountwithoutpricep False -- <?> "unpriced amount (specifying a price)" priceAmount <- simpleamountp False -- <?> "unpriced amount (specifying a price)"
let amtsign' = signum $ aquantity baseAmt let amtsign' = signum $ aquantity baseAmt
amtsign = if amtsign' == 0 then 1 else amtsign' amtsign = if amtsign' == 0 then 1 else amtsign'
@ -863,9 +896,9 @@ balanceassertionp = do
istotal <- fmap isJust $ optional $ try $ char '=' istotal <- fmap isJust $ optional $ try $ char '='
isinclusive <- fmap isJust $ optional $ try $ char '*' isinclusive <- fmap isJust $ optional $ try $ char '*'
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
-- this amount can have a price; balance assertions ignore it, -- this amount can have a cost, but not a cost basis.
-- but balance assignments will use it -- balance assertions ignore it, but balance assignments will use it
a <- amountpnolotpricesp <?> "amount (for a balance assertion or assignment)" a <- amountnobasisp <?> "amount (for a balance assertion or assignment)"
return BalanceAssertion return BalanceAssertion
{ baamount = a { baamount = a
, batotal = istotal , batotal = istotal
@ -883,7 +916,7 @@ lotpricep = label "ledger-style lot price" $ do
doublebrace <- option False $ char '{' >> pure True doublebrace <- option False $ char '{' >> pure True
_fixed <- fmap isJust $ optional $ lift skipNonNewlineSpaces >> char '=' _fixed <- fmap isJust $ optional $ lift skipNonNewlineSpaces >> char '='
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
_a <- amountwithoutpricep False _a <- simpleamountp False
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
char '}' char '}'
when (doublebrace) $ void $ char '}' when (doublebrace) $ void $ char '}'

View File

@ -821,7 +821,7 @@ postingphelper isPostingRule mTransactionYear = do
let (ptype, account') = (accountNamePostingType account, textUnbracket account) let (ptype, account') = (accountNamePostingType account, textUnbracket account)
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
mult <- if isPostingRule then multiplierp else pure False mult <- if isPostingRule then multiplierp else pure False
amt <- optional $ amountpwithmultiplier mult amt <- optional $ amountp' mult
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
massertion <- optional balanceassertionp massertion <- optional balanceassertionp
lift skipNonNewlineSpaces lift skipNonNewlineSpaces