    {-|

    A 'Posting' represents a change (by some 'MixedAmount') of the balance in
    some 'Account'.  Each 'Transaction' contains two or more postings which
    should add up to 0. Postings reference their parent transaction, so we can
    look up the date or description there.

    -}

    {-# LANGUAGE CPP #-}
    {-# LANGUAGE NamedFieldPuns #-}
    {-# LANGUAGE OverloadedStrings #-}

    module Hledger.Data.Posting (
      -- * Posting
      nullposting,
      posting,
      post,
      vpost,
      post',
      vpost',
      nullassertion,
      balassert,
      balassertTot,
      balassertParInc,
      balassertTotInc,
      -- * operations
      originalPosting,
      postingStatus,
      isReal,
      isVirtual,
      isBalancedVirtual,
      isEmptyPosting,
      hasBalanceAssignment,
      hasAmount,
      postingAllTags,
      transactionAllTags,
      relatedPostings,
      postingStripCosts,
      postingApplyAliases,
      postingApplyCommodityStyles,
      postingStyleAmounts,
      postingAddTags,
      postingAddHiddenAndMaybeVisibleTag,
      -- * date operations
      postingDate,
      postingDate2,
      postingDateOrDate2,
      -- * account name operations
      accountNamesFromPostings,
      -- * comment/tag operations
      commentJoin,
      commentAddTag,
      commentAddTagUnspaced,
      commentAddTagNextLine,
      generatedTransactionTagName,
      modifiedTransactionTagName,
      generatedPostingTagName,
      costPostingTagName,
      conversionPostingTagName,

      -- * arithmetic
      sumPostings,
      postingNegate,
      postingNegateMainAmount,
      -- * rendering
      showPosting,
      showPostingLines,
      postingAsLines,
      postingsAsLines,
      postingIndent,
      showAccountName,
      renderCommentLines,
      showBalanceAssertion,
      -- * misc.
      postingTransformAmount,
      postingApplyValuation,
      postingToCost,
      postingAddInferredEquityPostings,
      postingPriceDirectivesFromCost,
      tests_Posting
    )
    where

    import Data.Default (def)
    import Data.Foldable (asum)
    import Data.Function ((&))
    import Data.Map qualified as M
    import Data.Maybe (fromMaybe, isJust, mapMaybe)
    import Data.List (sort, union)
    #if !MIN_VERSION_base(4,20,0)
    import Data.List (foldl')
    #endif
    import Data.Set qualified as S
    import Data.Text (Text)
    import Data.Text qualified as T
    import Data.Text.Lazy qualified as TL
    import Data.Text.Lazy.Builder qualified as TB
    import Data.Time.Calendar (Day)
    import Safe (maximumBound)
    import Text.DocLayout (realLength)
    import Text.Tabular.AsciiWide hiding (render)

    import Hledger.Utils
    import Hledger.Data.Types
    import Hledger.Data.Amount
    import Hledger.Data.AccountName
    import Hledger.Data.Dates (nulldate)
    import Hledger.Data.Valuation

    -- | Special tags hledger sometimes adds to mark various things.
    -- These should be hidden tag names, beginning with _.
    -- With --verbose-tags, the equivalent visible tags will also be added.
    -- These tag names are mentioned in docs and can be matched by user queries, so consider the impact before changing them.
    generatedTransactionTagName, modifiedTransactionTagName, costPostingTagName, conversionPostingTagName, generatedPostingTagName :: TagName
    generatedTransactionTagName :: Text
    generatedTransactionTagName = Text
    "_generated-transaction"  -- transactions generated by a periodic txn rule
    modifiedTransactionTagName :: Text
    modifiedTransactionTagName  = Text
    "_modified-transaction"   -- transactions modified by an auto posting rule
    generatedPostingTagName :: Text
    generatedPostingTagName     = Text
    "_generated-posting"      -- postings generated by hledger for one reason or another
    costPostingTagName :: Text
    costPostingTagName          = Text
    "_cost-posting"           -- postings which have or could have a cost that's equivalent to nearby conversion postings
    conversionPostingTagName :: Text
    conversionPostingTagName    = Text
    "_conversion-posting"     -- postings to an equity account of Conversion type which have an amount that's equivalent to a nearby costful or potentially costful posting

    instance HasAmounts BalanceAssertion where
      styleAmounts :: Map Text AmountStyle -> BalanceAssertion -> BalanceAssertion
    styleAmounts Map Text AmountStyle
    styles ba :: BalanceAssertion
    ba@BalanceAssertion{Amount
    baamount :: Amount
    baamount :: BalanceAssertion -> Amount
    baamount} = BalanceAssertion
    ba{baamount=styleAmounts styles baamount}

    instance HasAmounts Posting where
      styleAmounts :: Map Text AmountStyle -> Posting -> Posting
    styleAmounts Map Text AmountStyle
    styles p :: Posting
    p@Posting{MixedAmount
    pamount :: MixedAmount
    pamount :: Posting -> MixedAmount
    pamount, Maybe BalanceAssertion
    pbalanceassertion :: Maybe BalanceAssertion
    pbalanceassertion :: Posting -> Maybe BalanceAssertion
    pbalanceassertion} =
        Posting
    p{ pamount=styleAmounts styles pamount
          ,pbalanceassertion=styleAmounts styles pbalanceassertion
          }

    {-# DEPRECATED postingApplyCommodityStyles "please use styleAmounts instead" #-}
    -- | Find and apply the appropriate display style to the posting amounts
    -- in each commodity (see journalCommodityStyles).
    -- Main amount precisions may be set or not according to the styles, but cost precisions are not set.
    postingApplyCommodityStyles :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
    postingApplyCommodityStyles :: Map Text AmountStyle -> Posting -> Posting
    postingApplyCommodityStyles = Map Text AmountStyle -> Posting -> Posting
    forall a. HasAmounts a => Map Text AmountStyle -> a -> a
    styleAmounts

    {-# DEPRECATED postingStyleAmounts "please use styleAmounts instead" #-}
    -- | Like postingApplyCommodityStyles, but neither
    -- main amount precisions or cost precisions are set.
    postingStyleAmounts :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
    postingStyleAmounts :: Map Text AmountStyle -> Posting -> Posting
    postingStyleAmounts = Map Text AmountStyle -> Posting -> Posting
    forall a. HasAmounts a => Map Text AmountStyle -> a -> a
    styleAmounts

    nullposting, posting :: Posting
    nullposting :: Posting
    nullposting = Posting
                    {pdate :: Maybe Day
    pdate=Maybe Day
    forall a. Maybe a
    Nothing
                    ,pdate2 :: Maybe Day
    pdate2=Maybe Day
    forall a. Maybe a
    Nothing
                    ,pstatus :: Status
    pstatus=Status
    Unmarked
                    ,paccount :: Text
    paccount=Text
    ""
                    ,pamount :: MixedAmount
    pamount=MixedAmount
    nullmixedamt
                    ,pcomment :: Text
    pcomment=Text
    ""
                    ,ptype :: PostingType
    ptype=PostingType
    RegularPosting
                    ,ptags :: [Tag]
    ptags=[]
                    ,pbalanceassertion :: Maybe BalanceAssertion
    pbalanceassertion=Maybe BalanceAssertion
    forall a. Maybe a
    Nothing
                    ,ptransaction :: Maybe Transaction
    ptransaction=Maybe Transaction
    forall a. Maybe a
    Nothing
                    ,poriginal :: Maybe Posting
    poriginal=Maybe Posting
    forall a. Maybe a
    Nothing
                    }
    posting :: Posting
    posting = Posting
    nullposting

    -- constructors

    -- | Make a posting to an account.
    post :: AccountName -> Amount -> Posting
    post :: Text -> Amount -> Posting
    post Text
    acc Amount
    amt = Posting
    posting {paccount=acc, pamount=mixedAmount amt}

    -- | Make a virtual (unbalanced) posting to an account.
    vpost :: AccountName -> Amount -> Posting
    vpost :: Text -> Amount -> Posting
    vpost Text
    acc Amount
    amt = (Text -> Amount -> Posting
    post Text
    acc Amount
    amt){ptype=VirtualPosting}

    -- | Make a posting to an account, maybe with a balance assertion.
    post' :: AccountName -> Amount -> Maybe BalanceAssertion -> Posting
    post' :: Text -> Amount -> Maybe BalanceAssertion -> Posting
    post' Text
    acc Amount
    amt Maybe BalanceAssertion
    ass = Posting
    posting {paccount=acc, pamount=mixedAmount amt, pbalanceassertion=ass}

    -- | Make a virtual (unbalanced) posting to an account, maybe with a balance assertion.
    vpost' :: AccountName -> Amount -> Maybe BalanceAssertion -> Posting
    vpost' :: Text -> Amount -> Maybe BalanceAssertion -> Posting
    vpost' Text
    acc Amount
    amt Maybe BalanceAssertion
    ass = (Text -> Amount -> Maybe BalanceAssertion -> Posting
    post' Text
    acc Amount
    amt Maybe BalanceAssertion
    ass){ptype=VirtualPosting, pbalanceassertion=ass}

    nullassertion :: BalanceAssertion
    nullassertion :: BalanceAssertion
    nullassertion = BalanceAssertion
                      {baamount :: Amount
    baamount=Amount
    nullamt
                      ,batotal :: Bool
    batotal=Bool
    False
                      ,bainclusive :: Bool
    bainclusive=Bool
    False
                      ,baposition :: SourcePos
    baposition=String -> SourcePos
    initialPos String
    ""
                      }

    -- | Make a partial, exclusive balance assertion.
    balassert :: Amount -> Maybe BalanceAssertion
    balassert :: Amount -> Maybe BalanceAssertion
    balassert Amount
    amt = BalanceAssertion -> Maybe BalanceAssertion
    forall a. a -> Maybe a
    Just (BalanceAssertion -> Maybe BalanceAssertion)
    -> BalanceAssertion -> Maybe BalanceAssertion
    forall a b. (a -> b) -> a -> b
    $ BalanceAssertion
    nullassertion{baamount=amt}

    -- | Make a total, exclusive balance assertion.
    balassertTot :: Amount -> Maybe BalanceAssertion
    balassertTot :: Amount -> Maybe BalanceAssertion
    balassertTot Amount
    amt = BalanceAssertion -> Maybe BalanceAssertion
    forall a. a -> Maybe a
    Just (BalanceAssertion -> Maybe BalanceAssertion)
    -> BalanceAssertion -> Maybe BalanceAssertion
    forall a b. (a -> b) -> a -> b
    $ BalanceAssertion
    nullassertion{baamount=amt, batotal=True}

    -- | Make a partial, inclusive balance assertion.
    balassertParInc :: Amount -> Maybe BalanceAssertion
    balassertParInc :: Amount -> Maybe BalanceAssertion
    balassertParInc Amount
    amt = BalanceAssertion -> Maybe BalanceAssertion
    forall a. a -> Maybe a
    Just (BalanceAssertion -> Maybe BalanceAssertion)
    -> BalanceAssertion -> Maybe BalanceAssertion
    forall a b. (a -> b) -> a -> b
    $ BalanceAssertion
    nullassertion{baamount=amt, bainclusive=True}

    -- | Make a total, inclusive balance assertion.
    balassertTotInc :: Amount -> Maybe BalanceAssertion
    balassertTotInc :: Amount -> Maybe BalanceAssertion
    balassertTotInc Amount
    amt = BalanceAssertion -> Maybe BalanceAssertion
    forall a. a -> Maybe a
    Just (BalanceAssertion -> Maybe BalanceAssertion)
    -> BalanceAssertion -> Maybe BalanceAssertion
    forall a b. (a -> b) -> a -> b
    $ BalanceAssertion
    nullassertion{baamount=amt, batotal=True, bainclusive=True}

    -- | Render a balance assertion, as the =[=][*] symbol and expected amount.
    showBalanceAssertion :: BalanceAssertion -> WideBuilder
    showBalanceAssertion :: BalanceAssertion -> WideBuilder
    showBalanceAssertion BalanceAssertion
    ba =
        Char -> WideBuilder
    singleton Char
    '=' WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <> WideBuilder
    eq WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <> WideBuilder
    ast WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <> Char -> WideBuilder
    singleton Char
    ' ' WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <> AmountFormat -> Amount -> WideBuilder
    showAmountB AmountFormat
    forall a. Default a => a
    def{displayZeroCommodity=True, displayForceDecimalMark=True} (BalanceAssertion -> Amount
    baamount BalanceAssertion
    ba)
      where
        eq :: WideBuilder
    eq  = if BalanceAssertion -> Bool
    batotal BalanceAssertion
    ba     then Char -> WideBuilder
    singleton Char
    '=' else WideBuilder
    forall a. Monoid a => a
    mempty
        ast :: WideBuilder
    ast = if BalanceAssertion -> Bool
    bainclusive BalanceAssertion
    ba then Char -> WideBuilder
    singleton Char
    '*' else WideBuilder
    forall a. Monoid a => a
    mempty
        singleton :: Char -> WideBuilder
    singleton Char
    c = Builder -> Int -> WideBuilder
    WideBuilder (Char -> Builder
    TB.singleton Char
    c) Int
    1

    -- Get the original posting, if any.
    originalPosting :: Posting -> Posting
    originalPosting :: Posting -> Posting
    originalPosting Posting
    p = Posting -> Maybe Posting -> Posting
    forall a. a -> Maybe a -> a
    fromMaybe Posting
    p (Maybe Posting -> Posting) -> Maybe Posting -> Posting
    forall a b. (a -> b) -> a -> b
    $ Posting -> Maybe Posting
    poriginal Posting
    p

    showPosting :: Posting -> String
    showPosting :: Posting -> String
    showPosting Posting
    p = Text -> String
    T.unpack (Text -> String) -> ([Text] -> Text) -> [Text] -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Text] -> Text
    T.unlines ([Text] -> String) -> [Text] -> String
    forall a b. (a -> b) -> a -> b
    $ Bool -> [Posting] -> [Text]
    postingsAsLines Bool
    False [Posting
    p]

    -- | Render a posting, at the appropriate width for aligning with
    -- its siblings if any. Used by the rewrite command.
    showPostingLines :: Posting -> [Text]
    showPostingLines :: Posting -> [Text]
    showPostingLines Posting
    p = ([Text], Int, Int) -> [Text]
    forall {a} {b} {c}. (a, b, c) -> a
    first3 (([Text], Int, Int) -> [Text]) -> ([Text], Int, Int) -> [Text]
    forall a b. (a -> b) -> a -> b
    $ Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
    postingAsLines Bool
    False Bool
    False Int
    maxacctwidth Int
    maxamtwidth Posting
    p
      where
        linesWithWidths :: [([Text], Int, Int)]
    linesWithWidths = (Posting -> ([Text], Int, Int))
    -> [Posting] -> [([Text], Int, Int)]
    forall a b. (a -> b) -> [a] -> [b]
    map (Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
    postingAsLines Bool
    False Bool
    False Int
    maxacctwidth Int
    maxamtwidth) ([Posting] -> [([Text], Int, Int)])
    -> (Maybe Transaction -> [Posting])
    -> Maybe Transaction
    -> [([Text], Int, Int)]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Posting]
    -> (Transaction -> [Posting]) -> Maybe Transaction -> [Posting]
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe [Posting
    p] Transaction -> [Posting]
    tpostings (Maybe Transaction -> [([Text], Int, Int)])
    -> Maybe Transaction -> [([Text], Int, Int)]
    forall a b. (a -> b) -> a -> b
    $ Posting -> Maybe Transaction
    ptransaction Posting
    p
        maxacctwidth :: Int
    maxacctwidth = Int -> [Int] -> Int
    forall a. Ord a => a -> [a] -> a
    maximumBound Int
    0 ([Int] -> Int) -> [Int] -> Int
    forall a b. (a -> b) -> a -> b
    $ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
    forall a b. (a -> b) -> [a] -> [b]
    map ([Text], Int, Int) -> Int
    forall {a} {b} {c}. (a, b, c) -> b
    second3 [([Text], Int, Int)]
    linesWithWidths
        maxamtwidth :: Int
    maxamtwidth  = Int -> [Int] -> Int
    forall a. Ord a => a -> [a] -> a
    maximumBound Int
    0 ([Int] -> Int) -> [Int] -> Int
    forall a b. (a -> b) -> a -> b
    $ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
    forall a b. (a -> b) -> [a] -> [b]
    map ([Text], Int, Int) -> Int
    forall {a} {b} {c}. (a, b, c) -> c
    third3 [([Text], Int, Int)]
    linesWithWidths

    -- | Render a transaction's postings as indented lines, suitable for `print` output.
    --
    -- Normally these will be in valid journal syntax which hledger can reparse
    -- (though they may include no-longer-valid balance assertions).
    -- Explicit amounts are shown, implicit amounts are not.
    --
    -- Postings with multicommodity explicit amounts are handled as follows:
    -- if onelineamounts is true, these amounts are shown on one line,
    -- comma-separated, and the output will not be valid journal syntax.
    -- Otherwise, they are shown as several similar postings, one per commodity.
    -- When the posting has a balance assertion, it is attached to the last of these postings.
    --
    -- Posting amounts will be aligned with each other, starting about 4 columns
    -- beyond the widest account name (see postingAsLines for details).
    -- The postings will appear balanced (amounts summing to zero).
    -- Amounts' display precisions, which may have been limited by commodity directives,
    -- will be increased if necessary to ensure this.
    --
    postingsAsLines :: Bool -> [Posting] -> [Text]
    postingsAsLines :: Bool -> [Posting] -> [Text]
    postingsAsLines Bool
    onelineamounts [Posting]
    ps = (([Text], Int, Int) -> [Text]) -> [([Text], Int, Int)] -> [Text]
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap ([Text], Int, Int) -> [Text]
    forall {a} {b} {c}. (a, b, c) -> a
    first3 [([Text], Int, Int)]
    linesWithWidths
      where
        linesWithWidths :: [([Text], Int, Int)]
    linesWithWidths = (Posting -> ([Text], Int, Int))
    -> [Posting] -> [([Text], Int, Int)]
    forall a b. (a -> b) -> [a] -> [b]
    map (Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
    postingAsLines Bool
    False Bool
    onelineamounts Int
    maxacctwidth Int
    maxamtwidth) [Posting]
    ps
        maxacctwidth :: Int
    maxacctwidth = Int -> [Int] -> Int
    forall a. Ord a => a -> [a] -> a
    maximumBound Int
    0 ([Int] -> Int) -> [Int] -> Int
    forall a b. (a -> b) -> a -> b
    $ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
    forall a b. (a -> b) -> [a] -> [b]
    map ([Text], Int, Int) -> Int
    forall {a} {b} {c}. (a, b, c) -> b
    second3 [([Text], Int, Int)]
    linesWithWidths
        maxamtwidth :: Int
    maxamtwidth  = Int -> [Int] -> Int
    forall a. Ord a => a -> [a] -> a
    maximumBound Int
    0 ([Int] -> Int) -> [Int] -> Int
    forall a b. (a -> b) -> a -> b
    $ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
    forall a b. (a -> b) -> [a] -> [b]
    map ([Text], Int, Int) -> Int
    forall {a} {b} {c}. (a, b, c) -> c
    third3 [([Text], Int, Int)]
    linesWithWidths

    -- | Render one posting, on one or more lines, suitable for `print` output.
    -- Also returns the widths calculated for the account and amount fields.
    --
    -- There will be an indented account name, plus one or more of status flag,
    -- posting amount, balance assertion, same-line comment, next-line comments.
    --
    -- If the posting's amount is implicit or if elideamount is true, no amount is shown.
    -- If the posting's amount is explicit and multi-commodity, multiple similar
    -- postings are shown, one for each commodity, to help produce parseable journal syntax.
    -- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
    -- (and the output will not be valid journal syntax).
    --
    -- If an amount is zero, any commodity symbol attached to it is shown
    -- (and the corresponding commodity display style is used).
    --
    -- By default, 4 spaces (2 if there's a status flag) are shown between
    -- account name and start of amount area, which is typically 12 chars wide
    -- and contains a right-aligned amount (so 10-12 visible spaces between
    -- account name and amount is typical).
    -- When given a list of postings to be aligned with, the whitespace will be
    -- increased if needed to match the posting with the longest account name.
    -- This is used to align the amounts of a transaction's postings.
    --
    postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
    postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
    postingAsLines Bool
    elideamount Bool
    onelineamounts Int
    acctwidth Int
    amtwidth Posting
    p =
        (([Text] -> [Text]) -> [[Text]] -> [Text]
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap ([Text] -> [Text] -> [Text]
    forall a. [a] -> [a] -> [a]
    ++ [Text]
    newlinecomments) [[Text]]
    postingblocks, Int
    thisacctwidth, Int
    thisamtwidth)
      where
        -- This needs to be converted to strict Text in order to strip trailing
        -- spaces. This adds a small amount of inefficiency, and the only difference
        -- is whether there are trailing spaces in print (and related) reports. This
        -- could be removed and we could just keep everything as a Text Builder, but
        -- would require adding trailing spaces to 42 failing tests.
        postingblocks :: [[Text]]
    postingblocks = [(Text -> Text) -> [Text] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Text -> Text
    T.stripEnd ([Text] -> [Text]) -> (Text -> [Text]) -> Text -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> [Text]
    T.lines (Text -> [Text]) -> (Text -> Text) -> Text -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Text
    TL.toStrict (Text -> [Text]) -> Text -> [Text]
    forall a b. (a -> b) -> a -> b
    $
                           [Cell] -> Text
    render [ Align -> Text -> Cell
    textCell Align
    BottomLeft Text
    statusandaccount
                                  , Align -> Text -> Cell
    textCell Align
    BottomLeft Text
    "  "
                                  , Align -> [WideBuilder] -> Cell
    Cell Align
    BottomLeft [WideBuilder -> WideBuilder
    pad WideBuilder
    amt]
                                  , Align -> [WideBuilder] -> Cell
    Cell Align
    BottomLeft [WideBuilder
    assertion]
                                  , Align -> Text -> Cell
    textCell Align
    BottomLeft Text
    samelinecomment
                                  ]
                        | (WideBuilder
    amt,WideBuilder
    assertion) <- [(WideBuilder, WideBuilder)]
    shownAmountsAssertions]
        render :: [Cell] -> Text
    render = TableOpts -> Header Cell -> Text
    renderRow TableOpts
    forall a. Default a => a
    def{tableBorders=False, borderSpaces=False} (Header Cell -> Text) -> ([Cell] -> Header Cell) -> [Cell] -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Properties -> [Header Cell] -> Header Cell
    forall h. Properties -> [Header h] -> Header h
    Group Properties
    NoLine ([Header Cell] -> Header Cell)
    -> ([Cell] -> [Header Cell]) -> [Cell] -> Header Cell
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Cell -> Header Cell) -> [Cell] -> [Header Cell]
    forall a b. (a -> b) -> [a] -> [b]
    map Cell -> Header Cell
    forall h. h -> Header h
    Header
        pad :: WideBuilder -> WideBuilder
    pad WideBuilder
    amt = Builder -> Int -> WideBuilder
    WideBuilder (Text -> Builder
    TB.fromText (Text -> Builder) -> Text -> Builder
    forall a b. (a -> b) -> a -> b
    $ Int -> Text -> Text
    T.replicate Int
    w Text
    " ") Int
    w WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <> WideBuilder
    amt
          where w :: Int
    w = Int -> Int -> Int
    forall a. Ord a => a -> a -> a
    max Int
    12 Int
    amtwidth Int -> Int -> Int
    forall a. Num a => a -> a -> a
    - WideBuilder -> Int
    wbWidth WideBuilder
    amt  -- min. 12 for backwards compatibility

        pacctstr :: Posting -> Text
    pacctstr Posting
    p' = Maybe Int -> PostingType -> Text -> Text
    showAccountName Maybe Int
    forall a. Maybe a
    Nothing (Posting -> PostingType
    ptype Posting
    p') (Posting -> Text
    paccount Posting
    p')
        pstatusandacct :: Posting -> Text
    pstatusandacct Posting
    p' = Posting -> Text
    pstatusprefix Posting
    p' Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Posting -> Text
    pacctstr Posting
    p'
        pstatusprefix :: Posting -> Text
    pstatusprefix Posting
    p' = case Posting -> Status
    pstatus Posting
    p' of
            Status
    Unmarked -> Text
    ""
            Status
    s        -> String -> Text
    T.pack (Status -> String
    forall a. Show a => a -> String
    show Status
    s) Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    " "

        -- currently prices are considered part of the amount string when right-aligning amounts
        -- Since we will usually be calling this function with the knot tied between
        -- amtwidth and thisamtwidth, make sure thisamtwidth does not depend on
        -- amtwidth at all.
        shownAmounts :: [WideBuilder]
    shownAmounts
          | Bool
    elideamount = [WideBuilder
    forall a. Monoid a => a
    mempty]
          | Bool
    otherwise   = AmountFormat -> MixedAmount -> [WideBuilder]
    showMixedAmountLinesB AmountFormat
    displayopts (MixedAmount -> [WideBuilder]) -> MixedAmount -> [WideBuilder]
    forall a b. (a -> b) -> a -> b
    $ Posting -> MixedAmount
    pamount Posting
    p
            where displayopts :: AmountFormat
    displayopts = AmountFormat
    defaultFmt{
              displayZeroCommodity=True, displayForceDecimalMark=True, displayOneLine=onelineamounts
              }
        thisamtwidth :: Int
    thisamtwidth = Int -> [Int] -> Int
    forall a. Ord a => a -> [a] -> a
    maximumBound Int
    0 ([Int] -> Int) -> [Int] -> Int
    forall a b. (a -> b) -> a -> b
    $ (WideBuilder -> Int) -> [WideBuilder] -> [Int]
    forall a b. (a -> b) -> [a] -> [b]
    map WideBuilder -> Int
    wbWidth [WideBuilder]
    shownAmounts

        -- when there is a balance assertion, show it only on the last posting line
        shownAmountsAssertions :: [(WideBuilder, WideBuilder)]
    shownAmountsAssertions = [WideBuilder] -> [WideBuilder] -> [(WideBuilder, WideBuilder)]
    forall a b. [a] -> [b] -> [(a, b)]
    zip [WideBuilder]
    shownAmounts [WideBuilder]
    shownAssertions
          where
            shownAssertions :: [WideBuilder]
    shownAssertions = Int -> WideBuilder -> [WideBuilder]
    forall a. Int -> a -> [a]
    replicate ([WideBuilder] -> Int
    forall a. [a] -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length [WideBuilder]
    shownAmounts Int -> Int -> Int
    forall a. Num a => a -> a -> a
    - Int
    1) WideBuilder
    forall a. Monoid a => a
    mempty [WideBuilder] -> [WideBuilder] -> [WideBuilder]
    forall a. [a] -> [a] -> [a]
    ++ [WideBuilder
    assertion]
              where
                assertion :: WideBuilder
    assertion = WideBuilder
    -> (BalanceAssertion -> WideBuilder)
    -> Maybe BalanceAssertion
    -> WideBuilder
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe WideBuilder
    forall a. Monoid a => a
    mempty ((Builder -> Int -> WideBuilder
    WideBuilder (Char -> Builder
    TB.singleton Char
    ' ') Int
    1 WideBuilder -> WideBuilder -> WideBuilder
    forall a. Semigroup a => a -> a -> a
    <>)(WideBuilder -> WideBuilder)
    -> (BalanceAssertion -> WideBuilder)
    -> BalanceAssertion
    -> WideBuilder
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .BalanceAssertion -> WideBuilder
    showBalanceAssertion) (Maybe BalanceAssertion -> WideBuilder)
    -> Maybe BalanceAssertion -> WideBuilder
    forall a b. (a -> b) -> a -> b
    $ Posting -> Maybe BalanceAssertion
    pbalanceassertion Posting
    p

        -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
        statusandaccount :: Text
    statusandaccount = Text -> Text
    postingIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
    fitText (Int -> Maybe Int
    forall a. a -> Maybe a
    Just (Int -> Maybe Int) -> Int -> Maybe Int
    forall a b. (a -> b) -> a -> b
    $ Int
    2 Int -> Int -> Int
    forall a. Num a => a -> a -> a
    + Int
    acctwidth) Maybe Int
    forall a. Maybe a
    Nothing Bool
    False Bool
    True (Text -> Text) -> Text -> Text
    forall a b. (a -> b) -> a -> b
    $ Posting -> Text
    pstatusandacct Posting
    p
        thisacctwidth :: Int
    thisacctwidth = Text -> Int
    forall a. HasChars a => a -> Int
    realLength (Text -> Int) -> Text -> Int
    forall a b. (a -> b) -> a -> b
    $ Posting -> Text
    pacctstr Posting
    p

        (Text
    samelinecomment, [Text]
    newlinecomments) =
          case Text -> [Text]
    renderCommentLines (Posting -> Text
    pcomment Posting
    p) of []   -> (Text
    "",[])
                                                  Text
    c:[Text]
    cs -> (Text
    c,[Text]
    cs)

    -- | Show an account name, clipped to the given width if any, and
    -- appropriately bracketed/parenthesised for the given posting type.
    showAccountName :: Maybe Int -> PostingType -> AccountName -> Text
    showAccountName :: Maybe Int -> PostingType -> Text -> Text
    showAccountName Maybe Int
    w = PostingType -> Text -> Text
    fmt
      where
        fmt :: PostingType -> Text -> Text
    fmt PostingType
    RegularPosting         = (Text -> Text)
    -> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe Text -> Text
    forall a. a -> a
    id Int -> Text -> Text
    T.take Maybe Int
    w
        fmt PostingType
    VirtualPosting         = Text -> Text -> Text -> Text
    wrap Text
    "(" Text
    ")" (Text -> Text) -> (Text -> Text) -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Text -> Text)
    -> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe Text -> Text
    forall a. a -> a
    id (Int -> Text -> Text
    T.takeEnd (Int -> Text -> Text) -> (Int -> Int) -> Int -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int -> Int -> Int
    forall a. Num a => a -> a -> a
    subtract Int
    2) Maybe Int
    w
        fmt PostingType
    BalancedVirtualPosting = Text -> Text -> Text -> Text
    wrap Text
    "[" Text
    "]" (Text -> Text) -> (Text -> Text) -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Text -> Text)
    -> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe Text -> Text
    forall a. a -> a
    id (Int -> Text -> Text
    T.takeEnd (Int -> Text -> Text) -> (Int -> Int) -> Int -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int -> Int -> Int
    forall a. Num a => a -> a -> a
    subtract Int
    2) Maybe Int
    w

    -- | Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
    -- The first line (unless empty) will have leading space, subsequent lines will have a larger indent.
    renderCommentLines :: Text -> [Text]
    renderCommentLines :: Text -> [Text]
    renderCommentLines Text
    t =
      case Text -> [Text]
    T.lines Text
    t of
        []      -> []
        [Text
    l]     -> [Text -> Text
    commentSpace (Text -> Text) -> Text -> Text
    forall a b. (a -> b) -> a -> b
    $ Text -> Text
    comment Text
    l]        -- single-line comment
        (Text
    "":[Text]
    ls) -> Text
    "" Text -> [Text] -> [Text]
    forall a. a -> [a] -> [a]
    : (Text -> Text) -> [Text] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map (Text -> Text
    postingIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Text
    comment) [Text]
    ls  -- multi-line comment with empty first line
        (Text
    l:[Text]
    ls)  -> Text -> Text
    commentSpace (Text -> Text
    comment Text
    l) Text -> [Text] -> [Text]
    forall a. a -> [a] -> [a]
    : (Text -> Text) -> [Text] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map (Text -> Text
    postingIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Text
    comment) [Text]
    ls
      where
        comment :: Text -> Text
    comment = (Text
    "; "Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <>)

    -- | Prepend a suitable indent for a posting (or transaction/posting comment) line.
    postingIndent :: Text -> Text
    postingIndent :: Text -> Text
    postingIndent = (Text
    "    "Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <>)

    -- | Prepend the space required before a same-line comment.
    commentSpace :: Text -> Text
    commentSpace :: Text -> Text
    commentSpace = (Text
    "  "Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <>)

    isReal :: Posting -> Bool
    isReal :: Posting -> Bool
    isReal Posting
    p = Posting -> PostingType
    ptype Posting
    p PostingType -> PostingType -> Bool
    forall a. Eq a => a -> a -> Bool
    == PostingType
    RegularPosting

    isVirtual :: Posting -> Bool
    isVirtual :: Posting -> Bool
    isVirtual Posting
    p = Posting -> PostingType
    ptype Posting
    p PostingType -> PostingType -> Bool
    forall a. Eq a => a -> a -> Bool
    == PostingType
    VirtualPosting

    isBalancedVirtual :: Posting -> Bool
    isBalancedVirtual :: Posting -> Bool
    isBalancedVirtual Posting
    p = Posting -> PostingType
    ptype Posting
    p PostingType -> PostingType -> Bool
    forall a. Eq a => a -> a -> Bool
    == PostingType
    BalancedVirtualPosting

    hasAmount :: Posting -> Bool
    hasAmount :: Posting -> Bool
    hasAmount = Bool -> Bool
    not (Bool -> Bool) -> (Posting -> Bool) -> Posting -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . MixedAmount -> Bool
    isMissingMixedAmount (MixedAmount -> Bool)
    -> (Posting -> MixedAmount) -> Posting -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Posting -> MixedAmount
    pamount

    hasBalanceAssignment :: Posting -> Bool
    hasBalanceAssignment :: Posting -> Bool
    hasBalanceAssignment Posting
    p = Bool -> Bool
    not (Posting -> Bool
    hasAmount Posting
    p) Bool -> Bool -> Bool
    && Maybe BalanceAssertion -> Bool
    forall a. Maybe a -> Bool
    isJust (Posting -> Maybe BalanceAssertion
    pbalanceassertion Posting
    p)

    -- | Sorted unique account names referenced by these postings.
    accountNamesFromPostings :: [Posting] -> [AccountName]
    accountNamesFromPostings :: [Posting] -> [Text]
    accountNamesFromPostings = Set Text -> [Text]
    forall a. Set a -> [a]
    S.toList (Set Text -> [Text])
    -> ([Posting] -> Set Text) -> [Posting] -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Text] -> Set Text
    forall a. Ord a => [a] -> Set a
    S.fromList ([Text] -> Set Text)
    -> ([Posting] -> [Text]) -> [Posting] -> Set Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Posting -> Text) -> [Posting] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Posting -> Text
    paccount

    -- | Sum all amounts from a list of postings.
    sumPostings :: [Posting] -> MixedAmount
    sumPostings :: [Posting] -> MixedAmount
    sumPostings = (MixedAmount -> Posting -> MixedAmount)
    -> MixedAmount -> [Posting] -> MixedAmount
    forall b a. (b -> a -> b) -> b -> [a] -> b
    forall (t :: * -> *) b a.
    Foldable t =>
    (b -> a -> b) -> b -> t a -> b
    foldl' (\MixedAmount
    amt Posting
    p -> MixedAmount -> MixedAmount -> MixedAmount
    maPlus MixedAmount
    amt (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ Posting -> MixedAmount
    pamount Posting
    p) MixedAmount
    nullmixedamt

    -- | Negate the posting's main amount and balance assertion amount if any.
    postingNegate :: Posting -> Posting
    postingNegate :: Posting -> Posting
    postingNegate p :: Posting
    p@Posting{pamount :: Posting -> MixedAmount
    pamount=MixedAmount
    a, pbalanceassertion :: Posting -> Maybe BalanceAssertion
    pbalanceassertion=Maybe BalanceAssertion
    mb} =
      Posting
    p{pamount=negate a, pbalanceassertion=fmap balanceAssertionNegate mb}
      where
        balanceAssertionNegate :: BalanceAssertion -> BalanceAssertion
    balanceAssertionNegate b :: BalanceAssertion
    b@BalanceAssertion{baamount :: BalanceAssertion -> Amount
    baamount=Amount
    ba} = BalanceAssertion
    b{baamount=negate ba}

    -- | Negate the posting's main amount but not the balance assertion amount.
    postingNegateMainAmount :: Posting -> Posting
    postingNegateMainAmount :: Posting -> Posting
    postingNegateMainAmount p :: Posting
    p@Posting{pamount :: Posting -> MixedAmount
    pamount=MixedAmount
    a} = Posting
    p{pamount=negate a}

    -- | Strip all prices from a Posting.
    postingStripCosts :: Posting -> Posting
    postingStripCosts :: Posting -> Posting
    postingStripCosts = (MixedAmount -> MixedAmount) -> Posting -> Posting
    postingTransformAmount MixedAmount -> MixedAmount
    mixedAmountStripCosts

    -- | Get a posting's (primary) date - it's own primary date if specified,
    -- otherwise the parent transaction's primary date, or the null date if
    -- there is no parent transaction.
    postingDate :: Posting -> Day
    postingDate :: Posting -> Day
    postingDate Posting
    p = Day -> Maybe Day -> Day
    forall a. a -> Maybe a -> a
    fromMaybe Day
    nulldate (Maybe Day -> Day) -> Maybe Day -> Day
    forall a b. (a -> b) -> a -> b
    $ [Maybe Day] -> Maybe Day
    forall (t :: * -> *) (f :: * -> *) a.
    (Foldable t, Alternative f) =>
    t (f a) -> f a
    asum [Maybe Day]
    dates
        where dates :: [Maybe Day]
    dates = [ Posting -> Maybe Day
    pdate Posting
    p, Transaction -> Day
    tdate (Transaction -> Day) -> Maybe Transaction -> Maybe Day
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> Posting -> Maybe Transaction
    ptransaction Posting
    p ]

    -- | Get a posting's secondary (secondary) date, which is the first of:
    -- posting's secondary date, transaction's secondary date, posting's
    -- primary date, transaction's primary date, or the null date if there is
    -- no parent transaction.
    postingDate2 :: Posting -> Day
    postingDate2 :: Posting -> Day
    postingDate2 Posting
    p = Day -> Maybe Day -> Day
    forall a. a -> Maybe a -> a
    fromMaybe Day
    nulldate (Maybe Day -> Day) -> Maybe Day -> Day
    forall a b. (a -> b) -> a -> b
    $ [Maybe Day] -> Maybe Day
    forall (t :: * -> *) (f :: * -> *) a.
    (Foldable t, Alternative f) =>
    t (f a) -> f a
    asum [Maybe Day]
    dates
      where dates :: [Maybe Day]
    dates = [ Posting -> Maybe Day
    pdate2 Posting
    p
                    , Transaction -> Maybe Day
    tdate2 (Transaction -> Maybe Day) -> Maybe Transaction -> Maybe Day
    forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
    =<< Posting -> Maybe Transaction
    ptransaction Posting
    p
                    , Posting -> Maybe Day
    pdate Posting
    p
                    , Transaction -> Day
    tdate (Transaction -> Day) -> Maybe Transaction -> Maybe Day
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> Posting -> Maybe Transaction
    ptransaction Posting
    p
                    ]

    -- | Get a posting's primary or secondary date, as specified.
    postingDateOrDate2 :: WhichDate -> Posting -> Day
    postingDateOrDate2 :: WhichDate -> Posting -> Day
    postingDateOrDate2 WhichDate
    PrimaryDate   = Posting -> Day
    postingDate
    postingDateOrDate2 WhichDate
    SecondaryDate = Posting -> Day
    postingDate2

    -- | Get a posting's status. This is cleared or pending if those are
    -- explicitly set on the posting, otherwise the status of its parent
    -- transaction, or unmarked if there is no parent transaction. (Note
    -- the ambiguity, unmarked can mean "posting and transaction are both
    -- unmarked" or "posting is unmarked and don't know about the transaction".
    postingStatus :: Posting -> Status
    postingStatus :: Posting -> Status
    postingStatus Posting{pstatus :: Posting -> Status
    pstatus=Status
    s, ptransaction :: Posting -> Maybe Transaction
    ptransaction=Maybe Transaction
    mt} = case Status
    s of
        Status
    Unmarked -> Status -> (Transaction -> Status) -> Maybe Transaction -> Status
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe Status
    Unmarked Transaction -> Status
    tstatus Maybe Transaction
    mt
        Status
    _ -> Status
    s

    -- | Tags for this posting including any inherited from its parent transaction.
    postingAllTags :: Posting -> [Tag]
    postingAllTags :: Posting -> [Tag]
    postingAllTags Posting
    p = Posting -> [Tag]
    ptags Posting
    p [Tag] -> [Tag] -> [Tag]
    forall a. [a] -> [a] -> [a]
    ++ [Tag] -> (Transaction -> [Tag]) -> Maybe Transaction -> [Tag]
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe [] Transaction -> [Tag]
    ttags (Posting -> Maybe Transaction
    ptransaction Posting
    p)

    -- | Tags for this transaction including any from its postings (which includes any from the postings' accounts).
    transactionAllTags :: Transaction -> [Tag]
    transactionAllTags :: Transaction -> [Tag]
    transactionAllTags Transaction
    t = Transaction -> [Tag]
    ttags Transaction
    t [Tag] -> [Tag] -> [Tag]
    forall a. [a] -> [a] -> [a]
    ++ (Posting -> [Tag]) -> [Posting] -> [Tag]
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap Posting -> [Tag]
    ptags (Transaction -> [Posting]
    tpostings Transaction
    t)

    -- Get the other postings from this posting's transaction.
    relatedPostings :: Posting -> [Posting]
    relatedPostings :: Posting -> [Posting]
    relatedPostings p :: Posting
    p@Posting{ptransaction :: Posting -> Maybe Transaction
    ptransaction=Just Transaction
    t} = (Posting -> Bool) -> [Posting] -> [Posting]
    forall a. (a -> Bool) -> [a] -> [a]
    filter (Posting -> Posting -> Bool
    forall a. Eq a => a -> a -> Bool
    /= Posting
    p) ([Posting] -> [Posting]) -> [Posting] -> [Posting]
    forall a b. (a -> b) -> a -> b
    $ Transaction -> [Posting]
    tpostings Transaction
    t
    relatedPostings Posting
    _ = []

    isEmptyPosting :: Posting -> Bool
    isEmptyPosting :: Posting -> Bool
    isEmptyPosting = MixedAmount -> Bool
    mixedAmountLooksZero (MixedAmount -> Bool)
    -> (Posting -> MixedAmount) -> Posting -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Posting -> MixedAmount
    pamount

    -- | Apply some account aliases to the posting's account name, as described by accountNameApplyAliases.
    -- This can fail due to a bad replacement pattern in a regular expression alias.
    postingApplyAliases :: [AccountAlias] -> Posting -> Either RegexError Posting
    postingApplyAliases :: [AccountAlias] -> Posting -> Either String Posting
    postingApplyAliases [AccountAlias]
    aliases p :: Posting
    p@Posting{Text
    paccount :: Posting -> Text
    paccount :: Text
    paccount} =
      case [AccountAlias] -> Text -> Either String Text
    accountNameApplyAliases [AccountAlias]
    aliases Text
    paccount of
        Right Text
    a -> Posting -> Either String Posting
    forall a b. b -> Either a b
    Right Posting
    p{paccount=a}
        Left String
    e  -> String -> Either String Posting
    forall a b. a -> Either a b
    Left String
    err
          where
            err :: String
    err = String
    "problem while applying account aliases:\n" String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ [AccountAlias] -> String
    forall a. Show a => a -> String
    pshow [AccountAlias]
    aliases
              String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    "\n to account name: "String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++Text -> String
    T.unpack Text
    paccountString -> String -> String
    forall a. [a] -> [a] -> [a]
    ++String
    "\n "String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++String
    e

    -- | Add tags to a posting, discarding any for which the posting already has a value.
    -- Note this does not add tags to the posting's comment.
    postingAddTags :: Posting -> [Tag] -> Posting
    postingAddTags :: Posting -> [Tag] -> Posting
    postingAddTags p :: Posting
    p@Posting{[Tag]
    ptags :: Posting -> [Tag]
    ptags :: [Tag]
    ptags} [Tag]
    tags = Posting
    p{ptags=ptags `union` tags}

    -- | Add the given hidden tag to a posting; and with a true argument,
    -- also add the equivalent visible tag to the posting's tags and comment fields.
    -- If the posting already has these tags (with any value), do nothing.
    postingAddHiddenAndMaybeVisibleTag :: Bool -> HiddenTag -> Posting -> Posting
    postingAddHiddenAndMaybeVisibleTag :: Bool -> Tag -> Posting -> Posting
    postingAddHiddenAndMaybeVisibleTag Bool
    verbosetags Tag
    ht p :: Posting
    p@Posting{pcomment :: Posting -> Text
    pcomment=Text
    c, [Tag]
    ptags :: Posting -> [Tag]
    ptags :: [Tag]
    ptags} =
      (Posting
    p Posting -> [Tag] -> Posting
    `postingAddTags` ([Tag
    ht] [Tag] -> [Tag] -> [Tag]
    forall a. Semigroup a => a -> a -> a
    <> [Tag
    vt|Bool
    verbosetags]))
      {pcomment=if verbosetags && not hadtag then c `commentAddTag` vt else c}
      where
        vt :: Tag
    vt@(Text
    vname,Text
    _) = Tag -> Tag
    toVisibleTag Tag
    ht
        hadtag :: Bool
    hadtag = (Tag -> Bool) -> [Tag] -> Bool
    forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
    any ((Text -> Text -> Bool
    forall a. Eq a => a -> a -> Bool
    == (Text -> Text
    T.toLower Text
    vname)) (Text -> Bool) -> (Tag -> Text) -> Tag -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Text
    T.toLower (Text -> Text) -> (Tag -> Text) -> Tag -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Tag -> Text
    forall a b. (a, b) -> a
    fst) [Tag]
    ptags  -- XXX should regex-quote vname

    -- | Apply a specified valuation to this posting's amount, using the
    -- provided price oracle, commodity styles, and reference dates.
    -- See amountApplyValuation.
    postingApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Posting -> Posting
    postingApplyValuation :: PriceOracle
    -> Map Text AmountStyle
    -> Day
    -> Day
    -> ValuationType
    -> Posting
    -> Posting
    postingApplyValuation PriceOracle
    priceoracle Map Text AmountStyle
    styles Day
    periodlast Day
    today ValuationType
    v Posting
    p =
        (MixedAmount -> MixedAmount) -> Posting -> Posting
    postingTransformAmount (PriceOracle
    -> Map Text AmountStyle
    -> Day
    -> Day
    -> Day
    -> ValuationType
    -> MixedAmount
    -> MixedAmount
    mixedAmountApplyValuation PriceOracle
    priceoracle Map Text AmountStyle
    styles Day
    periodlast Day
    today (Posting -> Day
    postingDate Posting
    p) ValuationType
    v) Posting
    p

    -- | Maybe convert this 'Posting's amount to cost.
    postingToCost :: ConversionOp -> Posting -> Maybe Posting
    postingToCost :: ConversionOp -> Posting -> Maybe Posting
    postingToCost ConversionOp
    NoConversionOp Posting
    p = Posting -> Maybe Posting
    forall a. a -> Maybe a
    Just Posting
    p
    postingToCost ConversionOp
    ToCost         Posting
    p
      -- If this is an equity conversion posting with an associated cost nearby, ignore it
      | Text
    conversionPostingTagName Text -> [Text] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` (Tag -> Text) -> [Tag] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Tag -> Text
    forall a b. (a, b) -> a
    fst (Posting -> [Tag]
    ptags Posting
    p) Bool -> Bool -> Bool
    && Bool
    nocosts = Maybe Posting
    forall a. Maybe a
    Nothing
      | Bool
    otherwise = Posting -> Maybe Posting
    forall a. a -> Maybe a
    Just (Posting -> Maybe Posting) -> Posting -> Maybe Posting
    forall a b. (a -> b) -> a -> b
    $ (MixedAmount -> MixedAmount) -> Posting -> Posting
    postingTransformAmount MixedAmount -> MixedAmount
    mixedAmountCost Posting
    p
      where
        nocosts :: Bool
    nocosts = (Bool -> Bool
    not (Bool -> Bool) -> (MixedAmount -> Bool) -> MixedAmount -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Amount -> Bool) -> [Amount] -> Bool
    forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
    any (Maybe AmountCost -> Bool
    forall a. Maybe a -> Bool
    isJust (Maybe AmountCost -> Bool)
    -> (Amount -> Maybe AmountCost) -> Amount -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Amount -> Maybe AmountCost
    acost) ([Amount] -> Bool)
    -> (MixedAmount -> [Amount]) -> MixedAmount -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . MixedAmount -> [Amount]
    amountsRaw) (MixedAmount -> Bool) -> MixedAmount -> Bool
    forall a b. (a -> b) -> a -> b
    $ Posting -> MixedAmount
    pamount Posting
    p

    -- | Generate equity conversion postings corresponding to a 'Posting''s cost(s)
    -- (one pair of conversion postings per cost), wherever they don't already exist.
    postingAddInferredEquityPostings :: Bool -> Text -> Posting -> [Posting]
    postingAddInferredEquityPostings :: Bool -> Text -> Posting -> [Posting]
    postingAddInferredEquityPostings Bool
    verbosetags Text
    equityAcct Posting
    p
      -- this posting has no costs
      | [Amount] -> Bool
    forall a. [a] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [Amount]
    costs = [Posting
    p]
      -- this posting is already tagged as having associated conversion postings
      | Text
    costPostingTagName Text -> [Text] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` (Tag -> Text) -> [Tag] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Tag -> Text
    forall a b. (a, b) -> a
    fst (Posting -> [Tag]
    ptags Posting
    p) = [Posting
    p]
      -- tag the posting, and for each of its costs, add an equivalent pair of conversion postings after it
      | Bool
    otherwise =
        Bool -> Tag -> Posting -> Posting
    postingAddHiddenAndMaybeVisibleTag Bool
    verbosetags (Text
    costPostingTagName,Text
    "") Posting
    p Posting -> [Posting] -> [Posting]
    forall a. a -> [a] -> [a]
    :
        (Amount -> [Posting]) -> [Amount] -> [Posting]
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap Amount -> [Posting]
    makeConversionPostings [Amount]
    costs
      where
        costs :: [Amount]
    costs = (Amount -> Bool) -> [Amount] -> [Amount]
    forall a. (a -> Bool) -> [a] -> [a]
    filter (Maybe AmountCost -> Bool
    forall a. Maybe a -> Bool
    isJust (Maybe AmountCost -> Bool)
    -> (Amount -> Maybe AmountCost) -> Amount -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Amount -> Maybe AmountCost
    acost) ([Amount] -> [Amount])
    -> (MixedAmount -> [Amount]) -> MixedAmount -> [Amount]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . MixedAmount -> [Amount]
    amountsRaw (MixedAmount -> [Amount]) -> MixedAmount -> [Amount]
    forall a b. (a -> b) -> a -> b
    $ Posting -> MixedAmount
    pamount Posting
    p
        makeConversionPostings :: Amount -> [Posting]
    makeConversionPostings Amount
    amt = case Amount -> Maybe AmountCost
    acost Amount
    amt of
          Maybe AmountCost
    Nothing -> []
          Just AmountCost
    _  -> [ Posting
    convp{ paccount = accountPrefix <> amtCommodity
                            , pamount = mixedAmount . negate $ amountStripCost amt
                            }
                     , Posting
    convp{ paccount = accountPrefix <> costCommodity
                            , pamount = mixedAmount cost
                            }
                     ]
          where
            cost :: Amount
    cost = Amount -> Amount
    amountCost Amount
    amt
            amtCommodity :: Text
    amtCommodity  = Amount -> Text
    commodity Amount
    amt
            costCommodity :: Text
    costCommodity = Amount -> Text
    commodity Amount
    cost
            convp :: Posting
    convp = Posting
    p{pbalanceassertion=Nothing, poriginal=Nothing}
              Posting -> (Posting -> Posting) -> Posting
    forall a b. a -> (a -> b) -> b
    & Bool -> Tag -> Posting -> Posting
    postingAddHiddenAndMaybeVisibleTag Bool
    verbosetags (Text
    conversionPostingTagName,Text
    "")
              Posting -> (Posting -> Posting) -> Posting
    forall a b. a -> (a -> b) -> b
    & Bool -> Tag -> Posting -> Posting
    postingAddHiddenAndMaybeVisibleTag Bool
    verbosetags (Text
    generatedPostingTagName, Text
    "")
            accountPrefix :: Text
    accountPrefix = [Text] -> Text
    forall a. Monoid a => [a] -> a
    mconcat [ Text
    equityAcct, Text
    ":", Text -> [Text] -> Text
    T.intercalate Text
    "-" ([Text] -> Text) -> [Text] -> Text
    forall a b. (a -> b) -> a -> b
    $ [Text] -> [Text]
    forall a. Ord a => [a] -> [a]
    sort [Text
    amtCommodity, Text
    costCommodity], Text
    ":"]
            -- Take the commodity of an amount and collapse consecutive spaces to a single space
            commodity :: Amount -> Text
    commodity = [Text] -> Text
    T.unwords ([Text] -> Text) -> (Amount -> [Text]) -> Amount -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Text -> Bool) -> [Text] -> [Text]
    forall a. (a -> Bool) -> [a] -> [a]
    filter (Bool -> Bool
    not (Bool -> Bool) -> (Text -> Bool) -> Text -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> Bool
    T.null) ([Text] -> [Text]) -> (Amount -> [Text]) -> Amount -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Text -> [Text]
    T.words (Text -> [Text]) -> (Amount -> Text) -> Amount -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Amount -> Text
    acommodity

    -- | Make a market price equivalent to this posting's amount's unit
    -- price, if any.
    postingPriceDirectivesFromCost :: Posting -> [PriceDirective]
    postingPriceDirectivesFromCost :: Posting -> [PriceDirective]
    postingPriceDirectivesFromCost p :: Posting
    p@Posting{MixedAmount
    pamount :: Posting -> MixedAmount
    pamount :: MixedAmount
    pamount} =
        (Amount -> Maybe PriceDirective) -> [Amount] -> [PriceDirective]
    forall a b. (a -> Maybe b) -> [a] -> [b]
    mapMaybe (Day -> Amount -> Maybe PriceDirective
    amountPriceDirectiveFromCost (Day -> Amount -> Maybe PriceDirective)
    -> Day -> Amount -> Maybe PriceDirective
    forall a b. (a -> b) -> a -> b
    $ Posting -> Day
    postingDate Posting
    p) ([Amount] -> [PriceDirective]) -> [Amount] -> [PriceDirective]
    forall a b. (a -> b) -> a -> b
    $ MixedAmount -> [Amount]
    amountsRaw MixedAmount
    pamount

    -- | Apply a transform function to this posting's main amount (but not its balance assertion amount).
    postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
    postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
    postingTransformAmount MixedAmount -> MixedAmount
    f p :: Posting
    p@Posting{pamount :: Posting -> MixedAmount
    pamount=MixedAmount
    a} = Posting
    p{pamount=f a}

    -- | Join two parts of a comment, eg a tag and another tag, or a tag
    -- and a non-tag, on a single line. Interpolates a comma and space
    -- unless one of the parts is empty.
    commentJoin :: Text -> Text -> Text
    commentJoin :: Text -> Text -> Text
    commentJoin Text
    c1 Text
    c2
      | Text -> Bool
    T.null Text
    c1 = Text
    c2
      | Text -> Bool
    T.null Text
    c2 = Text
    c1
      | Bool
    otherwise = Text
    c1 Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    ", " Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    c2

    -- | Add a tag to a comment, comma-separated from any prior content.
    -- A space is inserted following the colon, before the value.
    commentAddTag :: Text -> Tag -> Text
    commentAddTag :: Text -> Tag -> Text
    commentAddTag Text
    c (Text
    t,Text
    v)
      | Text -> Bool
    T.null Text
    c' = Text
    tag
      | Bool
    otherwise = Text
    c' Text -> Text -> Text
    `commentJoin` Text
    tag
      where
        c' :: Text
    c'  = Text -> Text
    T.stripEnd Text
    c
        tag :: Text
    tag = Text
    t Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    ": " Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    v

    -- | Like commentAddTag, but omits the space after the colon.
    commentAddTagUnspaced :: Text -> Tag -> Text
    commentAddTagUnspaced :: Text -> Tag -> Text
    commentAddTagUnspaced Text
    c (Text
    t,Text
    v)
      | Text -> Bool
    T.null Text
    c' = Text
    tag
      | Bool
    otherwise = Text
    c' Text -> Text -> Text
    `commentJoin` Text
    tag
      where
        c' :: Text
    c'  = Text -> Text
    T.stripEnd Text
    c
        tag :: Text
    tag = Text
    t Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    ":" Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    v

    -- | Add a tag on its own line to a comment, preserving any prior content.
    -- A space is inserted following the colon, before the value.
    commentAddTagNextLine :: Text -> Tag -> Text
    commentAddTagNextLine :: Text -> Tag -> Text
    commentAddTagNextLine Text
    cmt (Text
    t,Text
    v) =
      Text
    cmt Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> (if Text
    "\n" Text -> Text -> Bool
    `T.isSuffixOf` Text
    cmt then Text
    "" else Text
    "\n") Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    t Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    ": " Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    v

    -- tests

    tests_Posting :: TestTree
    tests_Posting = String -> [TestTree] -> TestTree
    testGroup String
    "Posting" [

      String -> Assertion -> TestTree
    testCase String
    "accountNamePostingType" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        Text -> PostingType
    accountNamePostingType Text
    "a" PostingType -> PostingType -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= PostingType
    RegularPosting
        Text -> PostingType
    accountNamePostingType Text
    "(a)" PostingType -> PostingType -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= PostingType
    VirtualPosting
        Text -> PostingType
    accountNamePostingType Text
    "[a]" PostingType -> PostingType -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= PostingType
    BalancedVirtualPosting

     ,String -> Assertion -> TestTree
    testCase String
    "accountNameWithoutPostingType" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        Text -> Text
    accountNameWithoutPostingType Text
    "(a)" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "a"

     ,String -> Assertion -> TestTree
    testCase String
    "accountNameWithPostingType" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        PostingType -> Text -> Text
    accountNameWithPostingType PostingType
    VirtualPosting Text
    "[a]" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "(a)"

     ,String -> Assertion -> TestTree
    testCase String
    "joinAccountNames" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        Text
    "a" Text -> Text -> Text
    `joinAccountNames` Text
    "b:c" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "a:b:c"
        Text
    "a" Text -> Text -> Text
    `joinAccountNames` Text
    "(b:c)" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "(a:b:c)"
        Text
    "[a]" Text -> Text -> Text
    `joinAccountNames` Text
    "(b:c)" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "[a:b:c]"
        Text
    "" Text -> Text -> Text
    `joinAccountNames` Text
    "a" Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "a"

     ,String -> Assertion -> TestTree
    testCase String
    "concatAccountNames" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        [Text] -> Text
    concatAccountNames [] Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    ""
        [Text] -> Text
    concatAccountNames [Text
    "a",Text
    "(b)",Text
    "[c:d]"] Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "(a:b:c:d)"

     ,String -> Assertion -> TestTree
    testCase String
    "commentAddTag" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        Text -> Tag -> Text
    commentAddTag Text
    "" (Text
    "a",Text
    "") Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "a: "
        Text -> Tag -> Text
    commentAddTag Text
    "[1/2]" (Text
    "a",Text
    "") Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "[1/2], a: "

     ,String -> Assertion -> TestTree
    testCase String
    "commentAddTagNextLine" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        Text -> Tag -> Text
    commentAddTagNextLine Text
    "" (Text
    "a",Text
    "") Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "\na: "
        Text -> Tag -> Text
    commentAddTagNextLine Text
    "[1/2]" (Text
    "a",Text
    "") Text -> Text -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Text
    "[1/2]\na: "

     ]