From aa54c3273a18bdb0b53d2599312ed41c50580b6d Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 22 Dec 2022 09:03:13 -1000 Subject: [PATCH] dev: lib: clarify some amount parsers; describe Ledger lot notation amountpwithmultiplier -> amountp' amountpnolotpricesp -> amountnobasisp amountwithoutpricep -> simpleamountp priceamountp -> costp --- hledger-lib/Hledger/Read/Common.hs | 97 +++++++++++++++-------- hledger-lib/Hledger/Read/JournalReader.hs | 2 +- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/hledger-lib/Hledger/Read/Common.hs b/hledger-lib/Hledger/Read/Common.hs index ea9ce6987..ca7603193 100644 --- a/hledger-lib/Hledger/Read/Common.hs +++ b/hledger-lib/Hledger/Read/Common.hs @@ -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 '}' diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index a5cd09e4e..b55dc7178 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -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