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
spaceandamountormissingp,
amountp,
amountpwithmultiplier,
amountp',
commoditysymbolp,
priceamountp,
costp,
balanceassertionp,
lotpricep,
numberp,
@ -675,21 +675,42 @@ singlespacep = spacenonewline *> notFollowedBy spacenonewline
--- *** amounts
-- | Parse whitespace then an amount, with an optional left or right
-- currency symbol and optional price, or return the special
-- "missing" marker amount.
-- | Parse whitespace then an amount, or return the special "missing" marker amount.
spaceandamountormissingp :: JournalParser m MixedAmount
spaceandamountormissingp =
option missingmixedamt $ try $ do
lift $ skipNonNewlineSpaces1
mixedAmount <$> amountp
-- | Parse a single-commodity amount, with optional symbol on the left
-- or right, followed by, in any order: an optional transaction price,
-- an optional ledger-style lot price, and/or an optional ledger-style
-- lot date. A lot price and lot date will be ignored.
-- | Parse a single-commodity amount, applying the default commodity if there is no commodity symbol;
-- optionally followed by, in any order:
-- a Ledger-style cost and/or one or more parts of a Ledger-style cost basis:
-- 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:
--
-- 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
-- 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 = amountpwithmultiplier False
amountp = amountp' False
amountpwithmultiplier :: Bool -> JournalParser m Amount
amountpwithmultiplier mult = label "amount" $ do
-- An amount with optional cost and/or cost basis, as described above.
-- 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
amt <- amountwithoutpricep mult <* spaces
amt <- simpleamountp mult <* spaces
(mprice, _elotprice, _elotdate) <- runPermutation $
(,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amt <* spaces)
(,,) <$> toPermutationWithDefault Nothing (Just <$> costp amt <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)
pure $ amt { aprice = mprice }
amountpnolotpricesp :: JournalParser m Amount
amountpnolotpricesp = label "amount" $ do
-- An amount with optional cost, but no cost basis.
amountnobasisp :: JournalParser m Amount
amountnobasisp = label "amount" $ do
let spaces = lift $ skipNonNewlineSpaces
amt <- amountwithoutpricep False
amt <- simpleamountp False
spaces
mprice <- optional $ priceamountp amt <* spaces
mprice <- optional $ costp amt <* spaces
pure $ amt { aprice = mprice }
amountwithoutpricep :: Bool -> JournalParser m Amount
amountwithoutpricep mult = do
-- An amount with no cost or cost basis.
-- 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
leftsymbolamountp sign <|> rightornosymbolamountp sign
where
-- An amount with commodity symbol on the left.
leftsymbolamountp :: (Decimal -> Decimal) -> JournalParser m Amount
leftsymbolamountp sign = label "amount" $ do
c <- lift commoditysymbolp
@ -750,6 +778,9 @@ amountwithoutpricep mult = do
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}
-- 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 sign = label "amount" $ do
offBeforeNum <- getOffset
@ -837,8 +868,10 @@ quotedcommoditysymbolp =
simplecommoditysymbolp :: TextParser m CommoditySymbol
simplecommoditysymbolp = takeWhile1P Nothing (not . isNonsimpleCommodityChar)
priceamountp :: Amount -> JournalParser m AmountPrice
priceamountp baseAmt = label "transaction price" $ do
-- | Ledger-style cost notation:
-- @ 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
parenthesised <- option False $ char '(' >> pure True
char '@'
@ -846,7 +879,7 @@ priceamountp baseAmt = label "transaction price" $ do
when parenthesised $ void $ char ')'
lift skipNonNewlineSpaces
priceAmount <- amountwithoutpricep False -- <?> "unpriced amount (specifying a price)"
priceAmount <- simpleamountp False -- <?> "unpriced amount (specifying a price)"
let amtsign' = signum $ aquantity baseAmt
amtsign = if amtsign' == 0 then 1 else amtsign'
@ -863,9 +896,9 @@ balanceassertionp = do
istotal <- fmap isJust $ optional $ try $ char '='
isinclusive <- fmap isJust $ optional $ try $ char '*'
lift skipNonNewlineSpaces
-- this amount can have a price; balance assertions ignore it,
-- but balance assignments will use it
a <- amountpnolotpricesp <?> "amount (for a balance assertion or assignment)"
-- this amount can have a cost, but not a cost basis.
-- balance assertions ignore it, but balance assignments will use it
a <- amountnobasisp <?> "amount (for a balance assertion or assignment)"
return BalanceAssertion
{ baamount = a
, batotal = istotal
@ -883,7 +916,7 @@ lotpricep = label "ledger-style lot price" $ do
doublebrace <- option False $ char '{' >> pure True
_fixed <- fmap isJust $ optional $ lift skipNonNewlineSpaces >> char '='
lift skipNonNewlineSpaces
_a <- amountwithoutpricep False
_a <- simpleamountp False
lift skipNonNewlineSpaces
char '}'
when (doublebrace) $ void $ char '}'

View File

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