{-# LANGUAGE NamedFieldPuns #-}
{-|

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 OverloadedStrings #-}

module Hledger.Data.Posting (
  -- * Posting
  nullposting,
  posting,
  post,
  vpost,
  post',
  vpost',
  nullsourcepos,
  nullassertion,
  balassert,
  balassertTot,
  balassertParInc,
  balassertTotInc,
  -- * operations
  originalPosting,
  postingStatus,
  isReal,
  isVirtual,
  isBalancedVirtual,
  isEmptyPosting,
  hasBalanceAssignment,
  hasAmount,
  postingAllTags,
  transactionAllTags,
  relatedPostings,
  postingStripCosts,
  postingApplyAliases,
  postingApplyCommodityStyles,
  postingStyleAmounts,
  postingAddTags,
  -- * date operations
  postingDate,
  postingDate2,
  postingDateOrDate2,
  isPostingInDateSpan,
  isPostingInDateSpan',
  -- * account name operations
  accountNamesFromPostings,
  -- * comment/tag operations
  commentJoin,
  commentAddTag,
  commentAddTagUnspaced,
  commentAddTagNextLine,
  -- * arithmetic
  sumPostings,
  -- * rendering
  showPosting,
  showPostingLines,
  postingAsLines,
  postingsAsLines,
  postingsAsLinesBeancount,
  postingAsLinesBeancount,
  showAccountName,
  showAccountNameBeancount,
  renderCommentLines,
  showBalanceAssertion,
  -- * misc.
  postingTransformAmount,
  postingApplyValuation,
  postingToCost,
  postingAddInferredEquityPostings,
  postingPriceDirectivesFromCost,
  tests_Posting
)
where

import Data.Default (def)
import Data.Foldable (asum)
import Data.Function ((&))
import qualified Data.Map as M
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.List (foldl', sort, union)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder 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, spanContainsDate)
import Hledger.Data.Valuation


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}

nullsourcepos :: (SourcePos, SourcePos)
nullsourcepos :: (SourcePos, SourcePos)
nullsourcepos = (FilePath -> Pos -> Pos -> SourcePos
SourcePos FilePath
"" (Int -> Pos
mkPos Int
1) (Int -> Pos
mkPos Int
1), FilePath -> Pos -> Pos -> SourcePos
SourcePos FilePath
"" (Int -> Pos
mkPos Int
2) (Int -> Pos
mkPos Int
1))

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

-- | 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 -> FilePath
showPosting Posting
p = Text -> FilePath
T.unpack (Text -> FilePath) -> ([Text] -> Text) -> [Text] -> FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Text] -> Text
T.unlines ([Text] -> FilePath) -> [Text] -> FilePath
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

-- | Given a transaction and its postings, render the postings, suitable
-- for `print` output. Normally this output will be valid journal syntax which
-- hledger can reparse (though it may include no-longer-valid balance assertions).
--
-- Explicit amounts are shown, any 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.
--
-- The output will appear to be a balanced transaction.
-- Amounts' display precisions, which may have been limited by commodity
-- directives, will be increased if necessary to ensure this.
--
-- Posting amounts will be aligned with each other, starting about 4 columns
-- beyond the widest account name (see postingAsLines for details).
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.
-- 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.
--
-- Also returns the account width and amount width used.
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        -> FilePath -> Text
T.pack (Status -> FilePath
forall a. Show a => a -> FilePath
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
lineIndent (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

-- | Like postingsAsLines but generates Beancount journal format.
postingsAsLinesBeancount :: [Posting] -> [Text]
postingsAsLinesBeancount :: [Posting] -> [Text]
postingsAsLinesBeancount [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 -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLinesBeancount Bool
False 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

-- | Like postingAsLines but generates Beancount journal format.
postingAsLinesBeancount  :: Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLinesBeancount :: Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLinesBeancount Bool
elideamount 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 -> 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

    pacct :: Text
pacct = Maybe Int -> Text -> Text
showAccountNameBeancount Maybe Int
forall a. Maybe a
Nothing (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Posting -> Text
paccount Posting
p
    pstatusandacct :: Posting -> Text
pstatusandacct Posting
p' = if Posting -> Status
pstatus Posting
p' Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Pending then Text
"! " else Text
"" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
pacct

    -- 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
a'
        where
          displayopts :: AmountFormat
displayopts = AmountFormat
defaultFmt{ displayZeroCommodity=True, displayForceDecimalMark=True }
          a' :: MixedAmount
a' = (Amount -> Amount) -> MixedAmount -> MixedAmount
mapMixedAmount Amount -> Amount
amountToBeancount (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Posting -> MixedAmount
pamount Posting
p
    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
lineIndent (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
pacct

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

type BeancountAmount = Amount

-- | Do some best effort adjustments to make an amount that renders
-- in a way that Beancount can read: forces the commodity symbol to the right,
-- converts a few currency symbols to names, capitalises all letters.
amountToBeancount :: Amount -> BeancountAmount
amountToBeancount :: Amount -> Amount
amountToBeancount a :: Amount
a@Amount{acommodity :: Amount -> Text
acommodity=Text
c,astyle :: Amount -> AmountStyle
astyle=AmountStyle
s,acost :: Amount -> Maybe AmountCost
acost=Maybe AmountCost
mp} = Amount
a{acommodity=c', astyle=s', acost=mp'}
  -- https://beancount.github.io/docs/beancount_language_syntax.html#commodities-currencies
  where
    c' :: Text
c' = Text -> Text
T.toUpper (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$
      HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"$" Text
"USD" (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$
      HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"€" Text
"EUR" (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$
      HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"¥" Text
"JPY" (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$
      HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"£" Text
"GBP" (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$
      Text
c
    s' :: AmountStyle
s' = AmountStyle
s{ascommodityside=R, ascommodityspaced=True}
    mp' :: Maybe AmountCost
mp' = AmountCost -> AmountCost
costToBeancount (AmountCost -> AmountCost) -> Maybe AmountCost -> Maybe AmountCost
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe AmountCost
mp
      where
        costToBeancount :: AmountCost -> AmountCost
costToBeancount (TotalCost Amount
amt) = Amount -> AmountCost
TotalCost (Amount -> AmountCost) -> Amount -> AmountCost
forall a b. (a -> b) -> a -> b
$ Amount -> Amount
amountToBeancount Amount
amt
        costToBeancount (UnitCost  Amount
amt) = Amount -> AmountCost
UnitCost  (Amount -> AmountCost) -> Amount -> AmountCost
forall a b. (a -> b) -> a -> b
$ Amount -> Amount
amountToBeancount Amount
amt

-- | Like showAccountName for Beancount journal format.
-- Calls accountNameToBeancount first.
showAccountNameBeancount :: Maybe Int -> AccountName -> Text
showAccountNameBeancount :: Maybe Int -> Text -> Text
showAccountNameBeancount Maybe Int
w = (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 (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
accountNameToBeancount

-- | 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
lineIndent (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
lineIndent (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.
lineIndent :: Text -> Text
lineIndent :: Text -> Text
lineIndent = (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

-- | 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.
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
_ = []

-- | Does this posting fall within the given date span ?
isPostingInDateSpan :: DateSpan -> Posting -> Bool
isPostingInDateSpan :: DateSpan -> Posting -> Bool
isPostingInDateSpan = WhichDate -> DateSpan -> Posting -> Bool
isPostingInDateSpan' WhichDate
PrimaryDate

-- --date2-sensitive version, separate for now to avoid disturbing multiBalanceReport.
isPostingInDateSpan' :: WhichDate -> DateSpan -> Posting -> Bool
isPostingInDateSpan' :: WhichDate -> DateSpan -> Posting -> Bool
isPostingInDateSpan' WhichDate
PrimaryDate   DateSpan
s = DateSpan -> Day -> Bool
spanContainsDate DateSpan
s (Day -> Bool) -> (Posting -> Day) -> Posting -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Posting -> Day
postingDate
isPostingInDateSpan' WhichDate
SecondaryDate DateSpan
s = DateSpan -> Day -> Bool
spanContainsDate DateSpan
s (Day -> Bool) -> (Posting -> Day) -> Posting -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Posting -> Day
postingDate2

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 FilePath Posting
postingApplyAliases [AccountAlias]
aliases p :: Posting
p@Posting{Text
paccount :: Posting -> Text
paccount :: Text
paccount} =
  case [AccountAlias] -> Text -> Either FilePath Text
accountNameApplyAliases [AccountAlias]
aliases Text
paccount of
    Right Text
a -> Posting -> Either FilePath Posting
forall a b. b -> Either a b
Right Posting
p{paccount=a}
    Left FilePath
e  -> FilePath -> Either FilePath Posting
forall a b. a -> Either a b
Left FilePath
err
      where
        err :: FilePath
err = FilePath
"problem while applying account aliases:\n" FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ [AccountAlias] -> FilePath
forall a. Show a => a -> FilePath
pshow [AccountAlias]
aliases
          FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
"\n to account name: "FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++Text -> FilePath
T.unpack Text
paccountFilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++FilePath
"\n "FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++FilePath
e

-- | Add tags to a posting, discarding any for which the posting already has a value.
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}

-- | 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 a conversion posting with a matched transaction price posting, ignore it
  | Text
"_conversion-matched" 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 inferred equity postings from a 'Posting''s costs.
-- Make sure not to duplicate them when matching ones exist already.
postingAddInferredEquityPostings :: Bool -> Text -> Posting -> [Posting]
postingAddInferredEquityPostings :: Bool -> Text -> Posting -> [Posting]
postingAddInferredEquityPostings Bool
verbosetags Text
equityAcct Posting
p
    | Text
"_price-matched" 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]
    | Bool
otherwise = Posting
taggedPosting 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]
conversionPostings [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
    taggedPosting :: Posting
taggedPosting
      | [Amount] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Amount]
costs = Posting
p
      | Bool
otherwise  = Posting
p{ ptags = ("_price-matched","") : ptags p }
    conversionPostings :: Amount -> [Posting]
conversionPostings Amount
amt = case Amount -> Maybe AmountCost
acost Amount
amt of
        Maybe AmountCost
Nothing -> []
        Just AmountCost
_  -> [ Posting
cp{ paccount = accountPrefix <> amtCommodity
                       , pamount = mixedAmount . negate $ amountStripCost amt
                       }
                   , Posting
cp{ 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
        cp :: Posting
cp = Posting
p{ pcomment = pcomment p & (if verbosetags then (`commentAddTag` ("generated-posting","conversion")) else id)
              , ptags    =
                   ("_conversion-matched","") : -- implementation-specific internal tag, not for users
                   ("_generated-posting","conversion") :
                   (if verbosetags then [("generated-posting", "conversion")] else [])
              , pbalanceassertion = Nothing
              , poriginal = Nothing
              }
        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 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 = FilePath -> [TestTree] -> TestTree
testGroup FilePath
"Posting" [

  FilePath -> Assertion -> TestTree
testCase FilePath
"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

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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"

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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)"

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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"

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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)"

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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: "

 ,FilePath -> Assertion -> TestTree
testCase FilePath
"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: "

 ]