{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Hledger.Reports.BudgetReport (
  BudgetGoal,
  BudgetTotal,
  BudgetAverage,
  BudgetCell,
  BudgetReportRow,
  BudgetReport,
  budgetReport,
  budgetReportAsTable,
  budgetReportAsText,
  budgetReportAsCsv,
  -- * Helpers
  combineBudgetAndActual,
  -- * Tests
  tests_BudgetReport
)
where

import Control.Applicative ((<|>))
import Control.Arrow ((***))
import Data.Decimal (roundTo)
import Data.Function (on)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.List (find, partition, transpose, foldl', maximumBy, intercalate)
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe, catMaybes, isJust)
import Data.Map (Map)
import qualified Data.Map as Map
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 Safe (minimumDef)
--import System.Console.CmdArgs.Explicit as C
--import Lucid as L
import qualified Text.Tabular.AsciiWide as Tab

import Hledger.Data
import Hledger.Utils
import Hledger.Reports.ReportOptions
import Hledger.Reports.ReportTypes
import Hledger.Reports.MultiBalanceReport
import Data.Ord (comparing)
import Control.Monad ((>=>))

-- All MixedAmounts:
type BudgetGoal    = Change
type BudgetTotal   = Total
type BudgetAverage = Average

-- | A budget report tracks expected and actual changes per account and subperiod.
-- Each table cell has an actual change amount and/or a budget goal amount.
type BudgetCell = (Maybe Change, Maybe BudgetGoal)
-- | A row in a budget report table - account name and data cells.
type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell
-- | A full budget report table.
type BudgetReport    = PeriodicReport    DisplayName BudgetCell

-- A BudgetCell's data values rendered for display - the actual change amount,
-- the budget goal amount if any, and the corresponding goal percentage if possible.
type BudgetDisplayCell = (WideBuilder, Maybe (WideBuilder, Maybe WideBuilder))
-- | A row of rendered budget data cells.
type BudgetDisplayRow  = [BudgetDisplayCell]

-- | An amount render helper for the budget report. Renders each commodity separately.
type BudgetShowAmountsFn   = MixedAmount -> [WideBuilder]
-- | A goal percentage calculating helper for the budget report.
type BudgetCalcPercentagesFn  = Change -> BudgetGoal -> [Maybe Percentage]

_brrShowDebug :: BudgetReportRow -> String
_brrShowDebug :: BudgetReportRow -> String
_brrShowDebug (PeriodicReportRow DisplayName
dname [(Maybe Change, Maybe Change)]
budgetpairs (Maybe Change, Maybe Change)
_tot (Maybe Change, Maybe Change)
_avg) =
  [String] -> String
unwords [
    Text -> String
T.unpack (Text -> String) -> Text -> String
forall a b. (a -> b) -> a -> b
$ DisplayName -> Text
displayFull DisplayName
dname,
    String
"",
    String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
intercalate String
" | "
      [ String -> (Change -> String) -> Maybe Change -> String
forall b a. b -> (a -> b) -> Maybe a -> b
maybe String
"-" Change -> String
showMixedAmount Maybe Change
mactual String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
" [" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String -> (Change -> String) -> Maybe Change -> String
forall b a. b -> (a -> b) -> Maybe a -> b
maybe String
"-" Change -> String
showMixedAmount Maybe Change
mgoal String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"]"
      | (Maybe Change
mactual,Maybe Change
mgoal) <- [(Maybe Change, Maybe Change)]
budgetpairs ]
    ]

-- | Calculate per-account, per-period budget (balance change) goals
-- from all periodic transactions, calculate actual balance changes
-- from the regular transactions, and compare these to get a 'BudgetReport'.
-- Unbudgeted accounts may be hidden or renamed (see journalWithBudgetAccountNames).
budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport
budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport
budgetReport ReportSpec
rspec BalancingOpts
bopts DateSpan
reportspan Journal
j = String -> BudgetReport -> BudgetReport
forall a. Show a => String -> a -> a
dbg4 String
"sortedbudgetreport" BudgetReport
budgetreport
  where
    -- Budget report demands ALTree mode to ensure subaccounts and subaccount budgets are properly handled
    -- and that reports with and without --empty make sense when compared side by side
    ropts :: ReportOpts
ropts = (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec){ accountlistmode_ = ALTree }
    showunbudgeted :: Bool
showunbudgeted = ReportOpts -> Bool
empty_ ReportOpts
ropts
    budgetedaccts :: Set Text
budgetedaccts =
      String -> Set Text -> Set Text
forall a. Show a => String -> a -> a
dbg3 String
"budgetedacctsinperiod" (Set Text -> Set Text) -> Set Text -> Set Text
forall a b. (a -> b) -> a -> b
$
      [Text] -> Set Text
forall a. Ord a => [a] -> Set a
S.fromList ([Text] -> Set Text) -> [Text] -> Set Text
forall a b. (a -> b) -> a -> b
$
      [Text] -> [Text]
expandAccountNames ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$
      [Posting] -> [Text]
accountNamesFromPostings ([Posting] -> [Text]) -> [Posting] -> [Text]
forall a b. (a -> b) -> a -> b
$
      (Transaction -> [Posting]) -> [Transaction] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
tpostings ([Transaction] -> [Posting]) -> [Transaction] -> [Posting]
forall a b. (a -> b) -> a -> b
$
      (PeriodicTransaction -> [Transaction])
-> [PeriodicTransaction] -> [Transaction]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\PeriodicTransaction
pt -> Bool -> PeriodicTransaction -> DateSpan -> [Transaction]
runPeriodicTransaction Bool
False PeriodicTransaction
pt DateSpan
reportspan) ([PeriodicTransaction] -> [Transaction])
-> [PeriodicTransaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$
      Journal -> [PeriodicTransaction]
jperiodictxns Journal
j
    actualj :: Journal
actualj = Set Text -> Bool -> Journal -> Journal
journalWithBudgetAccountNames Set Text
budgetedaccts Bool
showunbudgeted Journal
j
    budgetj :: Journal
budgetj = BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions BalancingOpts
bopts ReportOpts
ropts DateSpan
reportspan Journal
j
    priceoracle :: PriceOracle
priceoracle = Bool -> Journal -> PriceOracle
journalPriceOracle (ReportOpts -> Bool
infer_prices_ ReportOpts
ropts) Journal
j
    budgetgoalreport :: MultiBalanceReport
budgetgoalreport@(PeriodicReport [DateSpan]
_ [PeriodicReportRow DisplayName Change]
budgetgoalitems PeriodicReportRow () Change
budgetgoaltotals) =
        String -> MultiBalanceReport -> MultiBalanceReport
forall a. Show a => String -> a -> a
dbg5 String
"budgetgoalreport" (MultiBalanceReport -> MultiBalanceReport)
-> MultiBalanceReport -> MultiBalanceReport
forall a b. (a -> b) -> a -> b
$ ReportSpec
-> Journal -> PriceOracle -> Set Text -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec{_rsReportOpts=ropts{empty_=True}} Journal
budgetj PriceOracle
priceoracle Set Text
forall a. Monoid a => a
mempty
    budgetedacctsseen :: Set Text
budgetedacctsseen = [Text] -> Set Text
forall a. Ord a => [a] -> Set a
S.fromList ([Text] -> Set Text) -> [Text] -> Set Text
forall a b. (a -> b) -> a -> b
$ (PeriodicReportRow DisplayName Change -> Text)
-> [PeriodicReportRow DisplayName Change] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName Change -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName [PeriodicReportRow DisplayName Change]
budgetgoalitems
    actualreport :: MultiBalanceReport
actualreport@(PeriodicReport [DateSpan]
actualspans [PeriodicReportRow DisplayName Change]
_ PeriodicReportRow () Change
_) =
        String -> MultiBalanceReport -> MultiBalanceReport
forall a. Show a => String -> a -> a
dbg5 String
"actualreport"     (MultiBalanceReport -> MultiBalanceReport)
-> MultiBalanceReport -> MultiBalanceReport
forall a b. (a -> b) -> a -> b
$ ReportSpec
-> Journal -> PriceOracle -> Set Text -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec{_rsReportOpts=ropts{empty_=True}} Journal
actualj PriceOracle
priceoracle Set Text
budgetedacctsseen
    budgetgoalreport' :: MultiBalanceReport
budgetgoalreport'
      -- If no interval is specified:
      -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;
      -- it should be safe to replace it with the latter, so they combine well.
      | ReportOpts -> Interval
interval_ ReportOpts
ropts Interval -> Interval -> Bool
forall a. Eq a => a -> a -> Bool
== Interval
NoInterval = [DateSpan]
-> [PeriodicReportRow DisplayName Change]
-> PeriodicReportRow () Change
-> MultiBalanceReport
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
actualspans [PeriodicReportRow DisplayName Change]
budgetgoalitems PeriodicReportRow () Change
budgetgoaltotals
      | Bool
otherwise = MultiBalanceReport
budgetgoalreport
    budgetreport :: BudgetReport
budgetreport = ReportOpts
-> Journal
-> MultiBalanceReport
-> MultiBalanceReport
-> BudgetReport
combineBudgetAndActual ReportOpts
ropts Journal
j MultiBalanceReport
budgetgoalreport' MultiBalanceReport
actualreport

-- | Use all (or all matched by --budget's argument) periodic transactions in the journal 
-- to generate budget goal transactions in the specified date span (and before, to support
-- --historical. The precise start date is the natural start date of the largest interval
-- of the active periodic transaction rules that is on or before the earlier of journal start date,
-- report start date.)
-- Budget goal transactions are similar to forecast transactions except their purpose 
-- and effect is to define balance change goals, per account and period, for BudgetReport.
--
journalAddBudgetGoalTransactions :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions BalancingOpts
bopts ReportOpts
ropts DateSpan
reportspan Journal
j =
  (String -> Journal)
-> (Journal -> Journal) -> Either String Journal -> Journal
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> Journal
forall a. String -> a
error' Journal -> Journal
forall a. a -> a
id (Either String Journal -> Journal)
-> Either String Journal -> Journal
forall a b. (a -> b) -> a -> b
$  -- PARTIAL:
    (Journal -> Either String Journal
journalStyleAmounts (Journal -> Either String Journal)
-> (Journal -> Either String Journal)
-> Journal
-> Either String Journal
forall (m :: * -> *) a b c.
Monad m =>
(a -> m b) -> (b -> m c) -> a -> m c
>=> BalancingOpts -> Journal -> Either String Journal
journalBalanceTransactions BalancingOpts
bopts) Journal
j{ jtxns = budgetts }
  where
    budgetspan :: DateSpan
budgetspan = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg3 String
"budget span" (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ Maybe EFDay -> Maybe EFDay -> DateSpan
DateSpan (Day -> EFDay
Exact (Day -> EFDay) -> Maybe Day -> Maybe EFDay
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Day
mbudgetgoalsstartdate) (Day -> EFDay
Exact (Day -> EFDay) -> Maybe Day -> Maybe EFDay
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> DateSpan -> Maybe Day
spanEnd DateSpan
reportspan)
      where
        mbudgetgoalsstartdate :: Maybe Day
mbudgetgoalsstartdate =
          -- We want to also generate budget goal txns before the report start date, in case -H is used.
          -- What should the actual starting date for goal txns be ? This gets tricky. 
          -- Consider a journal with a "~ monthly" periodic transaction rule, where the first transaction is on 1/5.
          -- Users will certainly expect a budget goal for january, but "~ monthly" generates transactions
          -- on the first of month, and starting from 1/5 would exclude 1/1.
          -- Secondly, consider a rule like "~ every february 2nd from 2020/01"; we should not start that
          -- before 2020-02-02.
          -- Hopefully the following algorithm produces intuitive behaviour in general:
          -- from the earlier of the journal start date and the report start date,
          -- move backward to the nearest natural start date of the largest period seen among the
          -- active periodic transactions, unless that is disallowed by a start date in the periodic rule.
          -- (Do we need to pay attention to an end date in the rule ? Don't think so.)
          -- (So with "~ monthly", the journal start date 1/5 is adjusted to 1/1.)
          case Maybe Day -> [Maybe Day] -> Maybe Day
forall a. Ord a => a -> [a] -> a
minimumDef Maybe Day
forall a. Maybe a
Nothing ([Maybe Day] -> Maybe Day) -> [Maybe Day] -> Maybe Day
forall a b. (a -> b) -> a -> b
$ (Maybe Day -> Bool) -> [Maybe Day] -> [Maybe Day]
forall a. (a -> Bool) -> [a] -> [a]
filter Maybe Day -> Bool
forall a. Maybe a -> Bool
isJust [Bool -> Journal -> Maybe Day
journalStartDate Bool
False Journal
j, DateSpan -> Maybe Day
spanStart DateSpan
reportspan] of
            Maybe Day
Nothing -> Maybe Day
forall a. Maybe a
Nothing
            Just Day
d  -> Day -> Maybe Day
forall a. a -> Maybe a
Just Day
d'
              where
                -- the interval and any date span of the periodic transaction with longest period
                (Interval
intervl, DateSpan
spn) =
                  case [PeriodicTransaction]
budgetpts of
                    []  -> (Int -> Interval
Days Int
1, DateSpan
nulldatespan)
                    [PeriodicTransaction]
pts -> (PeriodicTransaction -> Interval
ptinterval PeriodicTransaction
pt, PeriodicTransaction -> DateSpan
ptspan PeriodicTransaction
pt)
                      where pt :: PeriodicTransaction
pt = (PeriodicTransaction -> PeriodicTransaction -> Ordering)
-> [PeriodicTransaction] -> PeriodicTransaction
forall (t :: * -> *) a.
Foldable t =>
(a -> a -> Ordering) -> t a -> a
maximumBy ((PeriodicTransaction -> Interval)
-> PeriodicTransaction -> PeriodicTransaction -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing PeriodicTransaction -> Interval
ptinterval) [PeriodicTransaction]
pts  -- PARTIAL: maximumBy won't fail
                -- the natural start of this interval on or before the journal/report start
                intervalstart :: Day
intervalstart = Interval -> Day -> Day
intervalBoundaryBefore Interval
intervl Day
d
                -- the natural interval start before the journal/report start,
                -- or the rule-specified start if later,
                -- but no later than the journal/report start.
                d' :: Day
d' = Day -> Day -> Day
forall a. Ord a => a -> a -> a
min Day
d (Day -> Day) -> Day -> Day
forall a b. (a -> b) -> a -> b
$ Day -> (Day -> Day) -> Maybe Day -> Day
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Day
intervalstart (Day -> Day -> Day
forall a. Ord a => a -> a -> a
max Day
intervalstart) (Maybe Day -> Day) -> Maybe Day -> Day
forall a b. (a -> b) -> a -> b
$ DateSpan -> Maybe Day
spanStart DateSpan
spn

    -- select periodic transactions matching a pattern
    -- (the argument of the (final) --budget option).
    -- XXX two limitations/wishes, requiring more extensive type changes:
    -- - give an error if pat is non-null and matches no periodic txns
    -- - allow a regexp or a full hledger query, not just a substring
    pat :: Text
pat = Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
"" (Maybe Text -> Text) -> Maybe Text -> Text
forall a b. (a -> b) -> a -> b
$ String -> Maybe Text -> Maybe Text
forall a. Show a => String -> a -> a
dbg3 String
"budget pattern" (Maybe Text -> Maybe Text) -> Maybe Text -> Maybe Text
forall a b. (a -> b) -> a -> b
$ Text -> Text
T.toLower (Text -> Text) -> Maybe Text -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ReportOpts -> Maybe Text
budgetpat_ ReportOpts
ropts
    budgetpts :: [PeriodicTransaction]
budgetpts = [PeriodicTransaction
pt | PeriodicTransaction
pt <- Journal -> [PeriodicTransaction]
jperiodictxns Journal
j, Text
pat Text -> Text -> Bool
`T.isInfixOf` Text -> Text
T.toLower (PeriodicTransaction -> Text
ptdescription PeriodicTransaction
pt)]
    budgetts :: [Transaction]
budgetts =
      String -> [Transaction] -> [Transaction]
forall a. Show a => String -> a -> a
dbg5 String
"budget goal txns" ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$
      [Transaction -> Transaction
makeBudgetTxn Transaction
t
      | PeriodicTransaction
pt <- [PeriodicTransaction]
budgetpts
      , Transaction
t <- Bool -> PeriodicTransaction -> DateSpan -> [Transaction]
runPeriodicTransaction Bool
False PeriodicTransaction
pt DateSpan
budgetspan
      ]
    makeBudgetTxn :: Transaction -> Transaction
makeBudgetTxn Transaction
t = Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$ Transaction
t { tdescription = T.pack "Budget transaction" }

-- | Adjust a journal's account names for budget reporting, in two ways:
--
-- 1. accounts with no budget goal anywhere in their ancestry are moved
--    under the "unbudgeted" top level account.
--
-- 2. subaccounts with no budget goal are merged with their closest parent account
--    with a budget goal, so that only budgeted accounts are shown.
--    This can be disabled by -E/--empty.
--
journalWithBudgetAccountNames :: S.Set AccountName -> Bool -> Journal -> Journal
journalWithBudgetAccountNames :: Set Text -> Bool -> Journal -> Journal
journalWithBudgetAccountNames Set Text
budgetedaccts Bool
showunbudgeted Journal
j =
  (Journal -> String) -> Journal -> Journal
forall a. Show a => (a -> String) -> a -> a
dbg5With ((String
"budget account names: "String -> String -> String
forall a. [a] -> [a] -> [a]
++)(String -> String) -> (Journal -> String) -> Journal -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
.[Text] -> String
forall a. Show a => a -> String
pshow([Text] -> String) -> (Journal -> [Text]) -> Journal -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Journal -> [Text]
journalAccountNamesUsed) (Journal -> Journal) -> Journal -> Journal
forall a b. (a -> b) -> a -> b
$
  Journal
j { jtxns = remapTxn <$> jtxns j }
  where
    remapTxn :: Transaction -> Transaction
remapTxn = Transaction -> Transaction
txnTieKnot (Transaction -> Transaction)
-> (Transaction -> Transaction) -> Transaction -> Transaction
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings Posting -> Posting
remapPosting
    remapPosting :: Posting -> Posting
remapPosting Posting
p = Posting
p { paccount = remapAccount $ paccount p, poriginal = poriginal p <|> Just p }
    remapAccount :: Text -> Text
remapAccount Text
a
      | Text
a Text -> Set Text -> Bool
forall a. Ord a => a -> Set a -> Bool
`S.member` Set Text
budgetedaccts = Text
a
      | Just Text
p <- Maybe Text
budgetedparent   = if Bool
showunbudgeted then Text
a else Text
p
      | Bool
otherwise                  = if Bool
showunbudgeted then Text
u Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
acctsep Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
a else Text
u
      where
        budgetedparent :: Maybe Text
budgetedparent = (Text -> Bool) -> [Text] -> Maybe Text
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (Text -> Set Text -> Bool
forall a. Ord a => a -> Set a -> Bool
`S.member` Set Text
budgetedaccts) ([Text] -> Maybe Text) -> [Text] -> Maybe Text
forall a b. (a -> b) -> a -> b
$ Text -> [Text]
parentAccountNames Text
a
        u :: Text
u = Text
unbudgetedAccountName

-- | Combine a per-account-and-subperiod report of budget goals, and one
-- of actual change amounts, into a budget performance report.
-- The two reports should have the same report interval, but need not
-- have exactly the same account rows or date columns.
-- (Cells in the combined budget report can be missing a budget goal,
-- an actual amount, or both.) The combined report will include:
--
-- - consecutive subperiods at the same interval as the two reports,
--   spanning the period of both reports
--
-- - all accounts mentioned in either report, sorted by account code or
--   account name or amount as appropriate.
--
combineBudgetAndActual :: ReportOpts -> Journal -> MultiBalanceReport -> MultiBalanceReport -> BudgetReport
combineBudgetAndActual :: ReportOpts
-> Journal
-> MultiBalanceReport
-> MultiBalanceReport
-> BudgetReport
combineBudgetAndActual ReportOpts
ropts Journal
j
      (PeriodicReport [DateSpan]
budgetperiods [PeriodicReportRow DisplayName Change]
budgetrows (PeriodicReportRow ()
_ [Change]
budgettots Change
budgetgrandtot Change
budgetgrandavg))
      (PeriodicReport [DateSpan]
actualperiods [PeriodicReportRow DisplayName Change]
actualrows (PeriodicReportRow ()
_ [Change]
actualtots Change
actualgrandtot Change
actualgrandavg)) =
    [DateSpan]
-> [BudgetReportRow]
-> PeriodicReportRow () (Maybe Change, Maybe Change)
-> BudgetReport
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
periods [BudgetReportRow]
combinedrows PeriodicReportRow () (Maybe Change, Maybe Change)
totalrow
  where
    periods :: [DateSpan]
periods = [DateSpan] -> [DateSpan]
forall a. Ord a => [a] -> [a]
nubSort ([DateSpan] -> [DateSpan])
-> ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (DateSpan -> Bool) -> [DateSpan] -> [DateSpan]
forall a. (a -> Bool) -> [a] -> [a]
filter (DateSpan -> DateSpan -> Bool
forall a. Eq a => a -> a -> Bool
/= DateSpan
nulldatespan) ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall a b. (a -> b) -> a -> b
$ [DateSpan]
budgetperiods [DateSpan] -> [DateSpan] -> [DateSpan]
forall a. [a] -> [a] -> [a]
++ [DateSpan]
actualperiods

    -- first, combine any corresponding budget goals with actual changes
    actualsplusgoals :: [BudgetReportRow]
actualsplusgoals = [
        -- dbg0With (("actualsplusgoals: "<>)._brrShowDebug) $
        DisplayName
-> [(Maybe Change, Maybe Change)]
-> (Maybe Change, Maybe Change)
-> (Maybe Change, Maybe Change)
-> BudgetReportRow
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow DisplayName
acct [(Maybe Change, Maybe Change)]
amtandgoals (Maybe Change, Maybe Change)
totamtandgoal (Maybe Change, Maybe Change)
avgamtandgoal
      | PeriodicReportRow DisplayName
acct [Change]
actualamts Change
actualtot Change
actualavg <- [PeriodicReportRow DisplayName Change]
actualrows

      , let mbudgetgoals :: Maybe ([Change], Change, Change)
mbudgetgoals       = Text
-> HashMap Text ([Change], Change, Change)
-> Maybe ([Change], Change, Change)
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HM.lookup (DisplayName -> Text
displayFull DisplayName
acct) HashMap Text ([Change], Change, Change)
budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage)
      , let budgetmamts :: [Maybe Change]
budgetmamts        = [Maybe Change]
-> (([Change], Change, Change) -> [Maybe Change])
-> Maybe ([Change], Change, Change)
-> [Maybe Change]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (Maybe Change
forall a. Maybe a
Nothing Maybe Change -> [DateSpan] -> [Maybe Change]
forall a b. a -> [b] -> [a]
forall (f :: * -> *) a b. Functor f => a -> f b -> f a
<$ [DateSpan]
periods) ((Change -> Maybe Change) -> [Change] -> [Maybe Change]
forall a b. (a -> b) -> [a] -> [b]
map Change -> Maybe Change
forall a. a -> Maybe a
Just ([Change] -> [Maybe Change])
-> (([Change], Change, Change) -> [Change])
-> ([Change], Change, Change)
-> [Maybe Change]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([Change], Change, Change) -> [Change]
forall {a} {b} {c}. (a, b, c) -> a
first3) Maybe ([Change], Change, Change)
mbudgetgoals :: [Maybe BudgetGoal]
      , let mbudgettot :: Maybe Change
mbudgettot         = ([Change], Change, Change) -> Change
forall {a} {b} {c}. (a, b, c) -> b
second3 (([Change], Change, Change) -> Change)
-> Maybe ([Change], Change, Change) -> Maybe Change
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([Change], Change, Change)
mbudgetgoals :: Maybe BudgetTotal
      , let mbudgetavg :: Maybe Change
mbudgetavg         = ([Change], Change, Change) -> Change
forall {a} {b} {c}. (a, b, c) -> c
third3 (([Change], Change, Change) -> Change)
-> Maybe ([Change], Change, Change) -> Maybe Change
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([Change], Change, Change)
mbudgetgoals  :: Maybe BudgetAverage
      , let acctGoalByPeriod :: Map DateSpan Change
acctGoalByPeriod   = [(DateSpan, Change)] -> Map DateSpan Change
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,Change
budgetamt) | (DateSpan
p, Just Change
budgetamt) <- [DateSpan] -> [Maybe Change] -> [(DateSpan, Maybe Change)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Maybe Change]
budgetmamts ] :: Map DateSpan BudgetGoal
      , let acctActualByPeriod :: Map DateSpan Change
acctActualByPeriod = [(DateSpan, Change)] -> Map DateSpan Change
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,Change
actualamt) | (DateSpan
p, Just Change
actualamt) <- [DateSpan] -> [Maybe Change] -> [(DateSpan, Maybe Change)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
actualperiods ((Change -> Maybe Change) -> [Change] -> [Maybe Change]
forall a b. (a -> b) -> [a] -> [b]
map Change -> Maybe Change
forall a. a -> Maybe a
Just [Change]
actualamts) ] :: Map DateSpan Change
      , let amtandgoals :: [(Maybe Change, Maybe Change)]
amtandgoals        = [ (DateSpan -> Map DateSpan Change -> Maybe Change
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
acctActualByPeriod, DateSpan -> Map DateSpan Change -> Maybe Change
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
acctGoalByPeriod) | DateSpan
p <- [DateSpan]
periods ] :: [BudgetCell]
      , let totamtandgoal :: (Maybe Change, Maybe Change)
totamtandgoal      = (Change -> Maybe Change
forall a. a -> Maybe a
Just Change
actualtot, Maybe Change
mbudgettot)
      , let avgamtandgoal :: (Maybe Change, Maybe Change)
avgamtandgoal      = (Change -> Maybe Change
forall a. a -> Maybe a
Just Change
actualavg, Maybe Change
mbudgetavg)
      ]
      where
        HashMap Text ([Change], Change, Change)
budgetGoalsByAcct :: HashMap AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) =
          [(Text, ([Change], Change, Change))]
-> HashMap Text ([Change], Change, Change)
forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
HM.fromList [ (DisplayName -> Text
displayFull DisplayName
acct, ([Change]
amts, Change
tot, Change
avg))
                      | PeriodicReportRow DisplayName
acct [Change]
amts Change
tot Change
avg <-
                          -- dbg0With (unlines.map (("budgetgoals: "<>).prrShowDebug)) $
                          [PeriodicReportRow DisplayName Change]
budgetrows
                      ]

    -- next, make rows for budget goals with no actual changes
    othergoals :: [BudgetReportRow]
othergoals = [
        -- dbg0With (("othergoals: "<>)._brrShowDebug) $
        DisplayName
-> [(Maybe Change, Maybe Change)]
-> (Maybe Change, Maybe Change)
-> (Maybe Change, Maybe Change)
-> BudgetReportRow
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow DisplayName
acct [(Maybe Change, Maybe Change)]
amtandgoals (Maybe Change, Maybe Change)
totamtandgoal (Maybe Change, Maybe Change)
avgamtandgoal
      | PeriodicReportRow DisplayName
acct [Change]
budgetgoals Change
budgettot Change
budgetavg <- [PeriodicReportRow DisplayName Change]
budgetrows
      , DisplayName -> Text
displayFull DisplayName
acct Text -> [Text] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` (BudgetReportRow -> Text) -> [BudgetReportRow] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map BudgetReportRow -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName [BudgetReportRow]
actualsplusgoals
      , let acctGoalByPeriod :: Map DateSpan Change
acctGoalByPeriod   = [(DateSpan, Change)] -> Map DateSpan Change
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, Change)] -> Map DateSpan Change)
-> [(DateSpan, Change)] -> Map DateSpan Change
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [Change] -> [(DateSpan, Change)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Change]
budgetgoals :: Map DateSpan BudgetGoal
      , let amtandgoals :: [(Maybe Change, Maybe Change)]
amtandgoals        = [ (Change -> Maybe Change
forall a. a -> Maybe a
Just Change
0, DateSpan -> Map DateSpan Change -> Maybe Change
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
acctGoalByPeriod) | DateSpan
p <- [DateSpan]
periods ] :: [BudgetCell]
      , let totamtandgoal :: (Maybe Change, Maybe Change)
totamtandgoal      = (Change -> Maybe Change
forall a. a -> Maybe a
Just Change
0, Change -> Maybe Change
forall a. a -> Maybe a
Just Change
budgettot)
      , let avgamtandgoal :: (Maybe Change, Maybe Change)
avgamtandgoal      = (Change -> Maybe Change
forall a. a -> Maybe a
Just Change
0, Change -> Maybe Change
forall a. a -> Maybe a
Just Change
budgetavg)
      ]

    -- combine and re-sort rows
    -- TODO: add --sort-budget to sort by budget goal amount
    [BudgetReportRow]
combinedrows :: [BudgetReportRow] =
      -- map (dbg0With (("combinedrows: "<>)._brrShowDebug)) $
      [Text] -> [BudgetReportRow] -> [BudgetReportRow]
forall b.
[Text]
-> [PeriodicReportRow DisplayName b]
-> [PeriodicReportRow DisplayName b]
sortRowsLike ([BudgetReportRow] -> [Text]
forall {b}.
[PeriodicReportRow DisplayName (Maybe Change, b)] -> [Text]
mbrsorted [BudgetReportRow]
unbudgetedrows [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [BudgetReportRow] -> [Text]
forall {b}.
[PeriodicReportRow DisplayName (Maybe Change, b)] -> [Text]
mbrsorted [BudgetReportRow]
rows') [BudgetReportRow]
rows
      where
        ([BudgetReportRow]
unbudgetedrows, [BudgetReportRow]
rows') = (BudgetReportRow -> Bool)
-> [BudgetReportRow] -> ([BudgetReportRow], [BudgetReportRow])
forall a. (a -> Bool) -> [a] -> ([a], [a])
partition ((Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
==Text
unbudgetedAccountName) (Text -> Bool)
-> (BudgetReportRow -> Text) -> BudgetReportRow -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BudgetReportRow -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName) [BudgetReportRow]
rows
        mbrsorted :: [PeriodicReportRow DisplayName (Maybe Change, b)] -> [Text]
mbrsorted = (PeriodicReportRow DisplayName Change -> Text)
-> [PeriodicReportRow DisplayName Change] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName Change -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName ([PeriodicReportRow DisplayName Change] -> [Text])
-> ([PeriodicReportRow DisplayName (Maybe Change, b)]
    -> [PeriodicReportRow DisplayName Change])
-> [PeriodicReportRow DisplayName (Maybe Change, b)]
-> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReportOpts
-> Journal
-> [PeriodicReportRow DisplayName Change]
-> [PeriodicReportRow DisplayName Change]
sortRows ReportOpts
ropts Journal
j ([PeriodicReportRow DisplayName Change]
 -> [PeriodicReportRow DisplayName Change])
-> ([PeriodicReportRow DisplayName (Maybe Change, b)]
    -> [PeriodicReportRow DisplayName Change])
-> [PeriodicReportRow DisplayName (Maybe Change, b)]
-> [PeriodicReportRow DisplayName Change]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (PeriodicReportRow DisplayName (Maybe Change, b)
 -> PeriodicReportRow DisplayName Change)
-> [PeriodicReportRow DisplayName (Maybe Change, b)]
-> [PeriodicReportRow DisplayName Change]
forall a b. (a -> b) -> [a] -> [b]
map (((Maybe Change, b) -> Change)
-> PeriodicReportRow DisplayName (Maybe Change, b)
-> PeriodicReportRow DisplayName Change
forall a b.
(a -> b)
-> PeriodicReportRow DisplayName a
-> PeriodicReportRow DisplayName b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (((Maybe Change, b) -> Change)
 -> PeriodicReportRow DisplayName (Maybe Change, b)
 -> PeriodicReportRow DisplayName Change)
-> ((Maybe Change, b) -> Change)
-> PeriodicReportRow DisplayName (Maybe Change, b)
-> PeriodicReportRow DisplayName Change
forall a b. (a -> b) -> a -> b
$ Change -> Maybe Change -> Change
forall a. a -> Maybe a -> a
fromMaybe Change
nullmixedamt (Maybe Change -> Change)
-> ((Maybe Change, b) -> Maybe Change)
-> (Maybe Change, b)
-> Change
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Maybe Change, b) -> Maybe Change
forall a b. (a, b) -> a
fst)
        rows :: [BudgetReportRow]
rows = [BudgetReportRow]
actualsplusgoals [BudgetReportRow] -> [BudgetReportRow] -> [BudgetReportRow]
forall a. [a] -> [a] -> [a]
++ [BudgetReportRow]
othergoals

    totalrow :: PeriodicReportRow () (Maybe Change, Maybe Change)
totalrow = ()
-> [(Maybe Change, Maybe Change)]
-> (Maybe Change, Maybe Change)
-> (Maybe Change, Maybe Change)
-> PeriodicReportRow () (Maybe Change, Maybe Change)
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow ()
        [ (DateSpan -> Map DateSpan Change -> Maybe Change
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
totActualByPeriod, DateSpan -> Map DateSpan Change -> Maybe Change
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
totGoalByPeriod) | DateSpan
p <- [DateSpan]
periods ]
        ( Change -> Maybe Change
forall a. a -> Maybe a
Just Change
actualgrandtot, Change -> Maybe Change
budget Change
budgetgrandtot )
        ( Change -> Maybe Change
forall a. a -> Maybe a
Just Change
actualgrandavg, Change -> Maybe Change
budget Change
budgetgrandavg )
      where
        totGoalByPeriod :: Map DateSpan Change
totGoalByPeriod = [(DateSpan, Change)] -> Map DateSpan Change
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, Change)] -> Map DateSpan Change)
-> [(DateSpan, Change)] -> Map DateSpan Change
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [Change] -> [(DateSpan, Change)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Change]
budgettots :: Map DateSpan BudgetTotal
        totActualByPeriod :: Map DateSpan Change
totActualByPeriod = [(DateSpan, Change)] -> Map DateSpan Change
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, Change)] -> Map DateSpan Change)
-> [(DateSpan, Change)] -> Map DateSpan Change
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [Change] -> [(DateSpan, Change)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
actualperiods [Change]
actualtots :: Map DateSpan Change
        budget :: Change -> Maybe Change
budget Change
b = if Change -> Bool
mixedAmountLooksZero Change
b then Maybe Change
forall a. Maybe a
Nothing else Change -> Maybe Change
forall a. a -> Maybe a
Just Change
b

-- | Render a budget report as plain text suitable for console output.
budgetReportAsText :: ReportOpts -> BudgetReport -> TL.Text
budgetReportAsText :: ReportOpts -> BudgetReport -> Text
budgetReportAsText ropts :: ReportOpts
ropts@ReportOpts{Bool
Int
[Text]
[Status]
Maybe Int
Maybe Text
Maybe NormalSign
Maybe ValuationType
Maybe ConversionOp
Interval
Period
StringFormat
Layout
AccountListMode
BalanceAccumulation
BalanceCalculation
accountlistmode_ :: ReportOpts -> AccountListMode
empty_ :: ReportOpts -> Bool
infer_prices_ :: ReportOpts -> Bool
interval_ :: ReportOpts -> Interval
budgetpat_ :: ReportOpts -> Maybe Text
period_ :: Period
interval_ :: Interval
statuses_ :: [Status]
conversionop_ :: Maybe ConversionOp
value_ :: Maybe ValuationType
infer_prices_ :: Bool
depth_ :: Maybe Int
date2_ :: Bool
empty_ :: Bool
no_elide_ :: Bool
real_ :: Bool
format_ :: StringFormat
pretty_ :: Bool
querystring_ :: [Text]
average_ :: Bool
related_ :: Bool
txn_dates_ :: Bool
balancecalc_ :: BalanceCalculation
balanceaccum_ :: BalanceAccumulation
budgetpat_ :: Maybe Text
accountlistmode_ :: AccountListMode
drop_ :: Int
declared_ :: Bool
row_total_ :: Bool
no_total_ :: Bool
summary_only_ :: Bool
show_costs_ :: Bool
sort_amount_ :: Bool
percent_ :: Bool
invert_ :: Bool
normalbalance_ :: Maybe NormalSign
color_ :: Bool
transpose_ :: Bool
layout_ :: Layout
period_ :: ReportOpts -> Period
statuses_ :: ReportOpts -> [Status]
conversionop_ :: ReportOpts -> Maybe ConversionOp
value_ :: ReportOpts -> Maybe ValuationType
depth_ :: ReportOpts -> Maybe Int
date2_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
real_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
pretty_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
average_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
txn_dates_ :: ReportOpts -> Bool
balancecalc_ :: ReportOpts -> BalanceCalculation
balanceaccum_ :: ReportOpts -> BalanceAccumulation
drop_ :: ReportOpts -> Int
declared_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
summary_only_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
invert_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
color_ :: ReportOpts -> Bool
transpose_ :: ReportOpts -> Bool
layout_ :: ReportOpts -> Layout
..} BudgetReport
budgetr = Builder -> Text
TB.toLazyText (Builder -> Text) -> Builder -> Text
forall a b. (a -> b) -> a -> b
$
    Text -> Builder
TB.fromText Text
title Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Text -> Builder
TB.fromText Text
"\n\n" Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<>
      ReportOpts -> Table Text Text WideBuilder -> Builder
balanceReportTableAsText ReportOpts
ropts (ReportOpts -> BudgetReport -> Table Text Text WideBuilder
budgetReportAsTable ReportOpts
ropts BudgetReport
budgetr)
  where
    title :: Text
title = Text
"Budget performance in " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> DateSpan -> Text
showDateSpan (BudgetReport -> DateSpan
forall a b. PeriodicReport a b -> DateSpan
periodicReportSpan BudgetReport
budgetr)
           Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (case Maybe ConversionOp
conversionop_ of
                 Just ConversionOp
ToCost -> Text
", converted to cost"
                 Maybe ConversionOp
_           -> Text
"")
           Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (case Maybe ValuationType
value_ of
                 Just (AtThen Maybe Text
_mc)   -> Text
", valued at posting date"
                 Just (AtEnd Maybe Text
_mc)    -> Text
", valued at period ends"
                 Just (AtNow Maybe Text
_mc)    -> Text
", current value"
                 Just (AtDate Day
d Maybe Text
_mc) -> Text
", valued at " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Day -> Text
showDate Day
d
                 Maybe ValuationType
Nothing             -> Text
"")
           Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
":"

-- | Build a 'Table' from a multi-column balance report.
budgetReportAsTable :: ReportOpts -> BudgetReport -> Tab.Table Text Text WideBuilder
budgetReportAsTable :: ReportOpts -> BudgetReport -> Table Text Text WideBuilder
budgetReportAsTable ReportOpts{Bool
Int
[Text]
[Status]
Maybe Int
Maybe Text
Maybe NormalSign
Maybe ValuationType
Maybe ConversionOp
Interval
Period
StringFormat
Layout
AccountListMode
BalanceAccumulation
BalanceCalculation
accountlistmode_ :: ReportOpts -> AccountListMode
empty_ :: ReportOpts -> Bool
infer_prices_ :: ReportOpts -> Bool
interval_ :: ReportOpts -> Interval
budgetpat_ :: ReportOpts -> Maybe Text
period_ :: ReportOpts -> Period
statuses_ :: ReportOpts -> [Status]
conversionop_ :: ReportOpts -> Maybe ConversionOp
value_ :: ReportOpts -> Maybe ValuationType
depth_ :: ReportOpts -> Maybe Int
date2_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
real_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
pretty_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
average_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
txn_dates_ :: ReportOpts -> Bool
balancecalc_ :: ReportOpts -> BalanceCalculation
balanceaccum_ :: ReportOpts -> BalanceAccumulation
drop_ :: ReportOpts -> Int
declared_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
summary_only_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
invert_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
color_ :: ReportOpts -> Bool
transpose_ :: ReportOpts -> Bool
layout_ :: ReportOpts -> Layout
period_ :: Period
interval_ :: Interval
statuses_ :: [Status]
conversionop_ :: Maybe ConversionOp
value_ :: Maybe ValuationType
infer_prices_ :: Bool
depth_ :: Maybe Int
date2_ :: Bool
empty_ :: Bool
no_elide_ :: Bool
real_ :: Bool
format_ :: StringFormat
pretty_ :: Bool
querystring_ :: [Text]
average_ :: Bool
related_ :: Bool
txn_dates_ :: Bool
balancecalc_ :: BalanceCalculation
balanceaccum_ :: BalanceAccumulation
budgetpat_ :: Maybe Text
accountlistmode_ :: AccountListMode
drop_ :: Int
declared_ :: Bool
row_total_ :: Bool
no_total_ :: Bool
summary_only_ :: Bool
show_costs_ :: Bool
sort_amount_ :: Bool
percent_ :: Bool
invert_ :: Bool
normalbalance_ :: Maybe NormalSign
color_ :: Bool
transpose_ :: Bool
layout_ :: Layout
..} (PeriodicReport [DateSpan]
spans [BudgetReportRow]
items PeriodicReportRow () (Maybe Change, Maybe Change)
totrow) =
  Table Text Text WideBuilder -> Table Text Text WideBuilder
forall {rh} {a}. Table rh rh a -> Table rh rh a
maybetransposetable (Table Text Text WideBuilder -> Table Text Text WideBuilder)
-> Table Text Text WideBuilder -> Table Text Text WideBuilder
forall a b. (a -> b) -> a -> b
$
  Table Text Text WideBuilder -> Table Text Text WideBuilder
forall {ch}. Table Text ch WideBuilder -> Table Text ch WideBuilder
addtotalrow (Table Text Text WideBuilder -> Table Text Text WideBuilder)
-> Table Text Text WideBuilder -> Table Text Text WideBuilder
forall a b. (a -> b) -> a -> b
$
    Header Text
-> Header Text -> [[WideBuilder]] -> Table Text Text WideBuilder
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Tab.Table
      (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine ([Header Text] -> Header Text) -> [Header Text] -> Header Text
forall a b. (a -> b) -> a -> b
$ (Text -> Header Text) -> [Text] -> [Header Text]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Header Text
forall h. h -> Header h
Tab.Header [Text]
accts)
      (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine ([Header Text] -> Header Text) -> [Header Text] -> Header Text
forall a b. (a -> b) -> a -> b
$ (Text -> Header Text) -> [Text] -> [Header Text]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Header Text
forall h. h -> Header h
Tab.Header [Text]
colheadings)
      [[WideBuilder]]
rows
  where
    maybetransposetable :: Table rh rh a -> Table rh rh a
maybetransposetable
      | Bool
transpose_ = \(Tab.Table Header rh
rh Header rh
ch [[a]]
vals) -> Header rh -> Header rh -> [[a]] -> Table rh rh a
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Tab.Table Header rh
ch Header rh
rh ([[a]] -> [[a]]
forall a. [[a]] -> [[a]]
transpose [[a]]
vals)
      | Bool
otherwise  = Table rh rh a -> Table rh rh a
forall a. a -> a
id

    addtotalrow :: Table Text ch WideBuilder -> Table Text ch WideBuilder
addtotalrow
      | Bool
no_total_ = Table Text ch WideBuilder -> Table Text ch WideBuilder
forall a. a -> a
id
      | Bool
otherwise = let rh :: Header Text
rh = Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine ([Header Text] -> Header Text)
-> (Header Text -> [Header Text]) -> Header Text -> Header Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Header Text -> [Header Text]
forall a. Int -> a -> [a]
replicate ([[WideBuilder]] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [[WideBuilder]]
totalrows) (Header Text -> Header Text) -> Header Text -> Header Text
forall a b. (a -> b) -> a -> b
$ Text -> Header Text
forall h. h -> Header h
Tab.Header Text
""
                        ch :: Header [a]
ch = [a] -> Header [a]
forall h. h -> Header h
Tab.Header [] -- ignored
                     in ((Table Text ch WideBuilder
 -> Table Text [Any] WideBuilder -> Table Text ch WideBuilder)
-> Table Text [Any] WideBuilder
-> Table Text ch WideBuilder
-> Table Text ch WideBuilder
forall a b c. (a -> b -> c) -> b -> a -> c
flip (Properties
-> Table Text ch WideBuilder
-> Table Text [Any] WideBuilder
-> Table Text ch WideBuilder
forall rh ch a ch2.
Properties -> Table rh ch a -> Table rh ch2 a -> Table rh ch a
Tab.concatTables Properties
Tab.SingleLine) (Table Text [Any] WideBuilder
 -> Table Text ch WideBuilder -> Table Text ch WideBuilder)
-> Table Text [Any] WideBuilder
-> Table Text ch WideBuilder
-> Table Text ch WideBuilder
forall a b. (a -> b) -> a -> b
$ Header Text
-> Header [Any] -> [[WideBuilder]] -> Table Text [Any] WideBuilder
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Tab.Table Header Text
rh Header [Any]
forall {a}. Header [a]
ch [[WideBuilder]]
totalrows)

    colheadings :: [Text]
colheadings = [Text
"Commodity" | Layout
layout_ Layout -> Layout -> Bool
forall a. Eq a => a -> a -> Bool
== Layout
LayoutBare]
                  [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ (DateSpan -> Text) -> [DateSpan] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (BalanceAccumulation -> [DateSpan] -> DateSpan -> Text
reportPeriodName BalanceAccumulation
balanceaccum_ [DateSpan]
spans) [DateSpan]
spans
                  [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [Text
"  Total" | Bool
row_total_]
                  [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [Text
"Average" | Bool
average_]

    ([Text]
accts, [[WideBuilder]]
rows, [[WideBuilder]]
totalrows) =
      ([Text]
accts'
      ,[WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]
maybecommcol [WideBuilder]
itemscs  ([[WideBuilder]] -> [[WideBuilder]])
-> [[WideBuilder]] -> [[WideBuilder]]
forall a b. (a -> b) -> a -> b
$ [[BudgetDisplayCell]] -> [[WideBuilder]]
showcells  [[BudgetDisplayCell]]
texts
      ,[WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]
maybecommcol [WideBuilder]
totrowcs ([[WideBuilder]] -> [[WideBuilder]])
-> [[WideBuilder]] -> [[WideBuilder]]
forall a b. (a -> b) -> a -> b
$ [[BudgetDisplayCell]] -> [[WideBuilder]]
showtotrow [[BudgetDisplayCell]]
totrowtexts)
      where
        -- If --layout=bare, prepend a commodities column.
        maybecommcol :: [WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]
        maybecommcol :: [WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]
maybecommcol [WideBuilder]
cs
          | Layout
layout_ Layout -> Layout -> Bool
forall a. Eq a => a -> a -> Bool
== Layout
LayoutBare = (WideBuilder -> [WideBuilder] -> [WideBuilder])
-> [WideBuilder] -> [[WideBuilder]] -> [[WideBuilder]]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (:) [WideBuilder]
cs
          | Bool
otherwise             = [[WideBuilder]] -> [[WideBuilder]]
forall a. a -> a
id

        showcells, showtotrow :: [[BudgetDisplayCell]] -> [[WideBuilder]]
        ([[BudgetDisplayCell]] -> [[WideBuilder]]
showcells, [[BudgetDisplayCell]] -> [[WideBuilder]]
showtotrow) =
          ([[WideBuilder]] -> [[WideBuilder]]
forall a. [[a]] -> [[a]]
maybetranspose ([[WideBuilder]] -> [[WideBuilder]])
-> ([[BudgetDisplayCell]] -> [[WideBuilder]])
-> [[BudgetDisplayCell]]
-> [[WideBuilder]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([BudgetDisplayCell] -> [WideBuilder])
-> [[BudgetDisplayCell]] -> [[WideBuilder]]
forall a b. (a -> b) -> [a] -> [b]
map (((Int, Int, Int) -> BudgetDisplayCell -> WideBuilder)
-> [(Int, Int, Int)] -> [BudgetDisplayCell] -> [WideBuilder]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
showBudgetDisplayCell [(Int, Int, Int)]
widths)       ([[BudgetDisplayCell]] -> [[WideBuilder]])
-> ([[BudgetDisplayCell]] -> [[BudgetDisplayCell]])
-> [[BudgetDisplayCell]]
-> [[WideBuilder]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [[BudgetDisplayCell]] -> [[BudgetDisplayCell]]
forall a. [[a]] -> [[a]]
maybetranspose
          ,[[WideBuilder]] -> [[WideBuilder]]
forall a. [[a]] -> [[a]]
maybetranspose ([[WideBuilder]] -> [[WideBuilder]])
-> ([[BudgetDisplayCell]] -> [[WideBuilder]])
-> [[BudgetDisplayCell]]
-> [[WideBuilder]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([BudgetDisplayCell] -> [WideBuilder])
-> [[BudgetDisplayCell]] -> [[WideBuilder]]
forall a b. (a -> b) -> [a] -> [b]
map (((Int, Int, Int) -> BudgetDisplayCell -> WideBuilder)
-> [(Int, Int, Int)] -> [BudgetDisplayCell] -> [WideBuilder]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
showBudgetDisplayCell [(Int, Int, Int)]
totrowwidths) ([[BudgetDisplayCell]] -> [[WideBuilder]])
-> ([[BudgetDisplayCell]] -> [[BudgetDisplayCell]])
-> [[BudgetDisplayCell]]
-> [[WideBuilder]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [[BudgetDisplayCell]] -> [[BudgetDisplayCell]]
forall a. [[a]] -> [[a]]
maybetranspose)
          where
            -- | Combine a BudgetDisplayCell's rendered values into a "[PERCENT of GOAL]" rendering,
            -- respecting the given widths.
            showBudgetDisplayCell :: (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
            showBudgetDisplayCell :: (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
showBudgetDisplayCell (Int
actualwidth, Int
budgetwidth, Int
percentwidth) (WideBuilder
actual, Maybe (WideBuilder, Maybe WideBuilder)
mbudget) =
              (Builder -> Int -> WideBuilder) -> Int -> Builder -> WideBuilder
forall a b c. (a -> b -> c) -> b -> a -> c
flip Builder -> Int -> WideBuilder
WideBuilder (Int
actualwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
totalbudgetwidth) (Builder -> WideBuilder) -> Builder -> WideBuilder
forall a b. (a -> b) -> a -> b
$
                WideBuilder -> Builder
toPadded WideBuilder
actual Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
-> ((WideBuilder, Maybe WideBuilder) -> Builder)
-> Maybe (WideBuilder, Maybe WideBuilder)
-> Builder
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Builder
emptycell (WideBuilder, Maybe WideBuilder) -> Builder
showBudgetGoalAndPercentage Maybe (WideBuilder, Maybe WideBuilder)
mbudget

              where
                toPadded :: WideBuilder -> Builder
toPadded (WideBuilder Builder
b Int
w) = (Text -> Builder
TB.fromText (Text -> Builder) -> (Int -> Text) -> Int -> Builder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Int -> Text -> Text) -> Text -> Int -> Text
forall a b c. (a -> b -> c) -> b -> a -> c
flip Int -> Text -> Text
T.replicate Text
" " (Int -> Builder) -> Int -> Builder
forall a b. (a -> b) -> a -> b
$ Int
actualwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
w) Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
b

                (Int
totalpercentwidth, Int
totalbudgetwidth) =
                  let totalpercentwidth' :: Int
totalpercentwidth' = if Int
percentwidth Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 then Int
0 else Int
percentwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
5
                   in ( Int
totalpercentwidth'
                      , if Int
budgetwidth Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 then Int
0 else Int
budgetwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
totalpercentwidth' Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
3
                      )

                emptycell :: TB.Builder
                emptycell :: Builder
emptycell = Text -> Builder
TB.fromText (Text -> Builder) -> Text -> Builder
forall a b. (a -> b) -> a -> b
$ Int -> Text -> Text
T.replicate Int
totalbudgetwidth Text
" "

                showBudgetGoalAndPercentage :: (WideBuilder, Maybe WideBuilder) -> TB.Builder
                showBudgetGoalAndPercentage :: (WideBuilder, Maybe WideBuilder) -> Builder
showBudgetGoalAndPercentage (WideBuilder
goal, Maybe WideBuilder
perc) =
                  let perct :: Text
perct = case Maybe WideBuilder
perc of
                        Maybe WideBuilder
Nothing  -> Int -> Text -> Text
T.replicate Int
totalpercentwidth Text
" "
                        Just WideBuilder
pct -> Int -> Text -> Text
T.replicate (Int
percentwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- WideBuilder -> Int
wbWidth WideBuilder
pct) Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> WideBuilder -> Text
wbToText WideBuilder
pct Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"% of "
                   in Text -> Builder
TB.fromText (Text -> Builder) -> Text -> Builder
forall a b. (a -> b) -> a -> b
$ Text
" [" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
perct Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text -> Text
T.replicate (Int
budgetwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- WideBuilder -> Int
wbWidth WideBuilder
goal) Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> WideBuilder -> Text
wbToText WideBuilder
goal Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"]"

            -- | Build a list of widths for each column.
            -- When --transpose is used, the totals row must be included in this list.
            widths :: [(Int, Int, Int)]
            widths :: [(Int, Int, Int)]
widths = [Int] -> [Int] -> [Int] -> [(Int, Int, Int)]
forall a b c. [a] -> [b] -> [c] -> [(a, b, c)]
zip3 [Int]
actualwidths [Int]
budgetwidths [Int]
percentwidths
              where
                actualwidths :: [Int]
actualwidths  = ([(Int, Int, Int)] -> Int) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([(Int, Int, Int)] -> [Int]) -> [(Int, Int, Int)] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Int, Int, Int) -> Int) -> [(Int, Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map (Int, Int, Int) -> Int
forall {a} {b} {c}. (a, b, c) -> a
first3 ) ([[(Int, Int, Int)]] -> [Int]) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
                budgetwidths :: [Int]
budgetwidths  = ([(Int, Int, Int)] -> Int) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([(Int, Int, Int)] -> [Int]) -> [(Int, Int, Int)] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Int, Int, Int) -> Int) -> [(Int, Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map (Int, Int, Int) -> Int
forall {a} {b} {c}. (a, b, c) -> b
second3) ([[(Int, Int, Int)]] -> [Int]) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
                percentwidths :: [Int]
percentwidths = ([(Int, Int, Int)] -> Int) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([(Int, Int, Int)] -> [Int]) -> [(Int, Int, Int)] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Int, Int, Int) -> Int) -> [(Int, Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map (Int, Int, Int) -> Int
forall {a} {b} {c}. (a, b, c) -> c
third3 ) ([[(Int, Int, Int)]] -> [Int]) -> [[(Int, Int, Int)]] -> [Int]
forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
                catcolumnwidths :: [[[a]]] -> [[a]]
catcolumnwidths = ([[a]] -> [[a]] -> [[a]]) -> [[a]] -> [[[a]]] -> [[a]]
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (([a] -> [a] -> [a]) -> [[a]] -> [[a]] -> [[a]]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith [a] -> [a] -> [a]
forall a. [a] -> [a] -> [a]
(++)) ([[a]] -> [[[a]]] -> [[a]]) -> [[a]] -> [[[a]]] -> [[a]]
forall a b. (a -> b) -> a -> b
$ [a] -> [[a]]
forall a. a -> [a]
repeat []
                cols :: [[(Int, Int, Int)]]
cols = [[(Int, Int, Int)]] -> [[(Int, Int, Int)]]
forall a. [[a]] -> [[a]]
maybetranspose ([[(Int, Int, Int)]] -> [[(Int, Int, Int)]])
-> [[(Int, Int, Int)]] -> [[(Int, Int, Int)]]
forall a b. (a -> b) -> a -> b
$ [[[(Int, Int, Int)]]] -> [[(Int, Int, Int)]]
forall {a}. [[[a]]] -> [[a]]
catcolumnwidths ([[[(Int, Int, Int)]]] -> [[(Int, Int, Int)]])
-> [[[(Int, Int, Int)]]] -> [[(Int, Int, Int)]]
forall a b. (a -> b) -> a -> b
$ (BudgetReportRow -> [[(Int, Int, Int)]])
-> [BudgetReportRow] -> [[[(Int, Int, Int)]]]
forall a b. (a -> b) -> [a] -> [b]
map ([(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
cellswidth ([(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]])
-> (BudgetReportRow -> [(Maybe Change, Maybe Change)])
-> BudgetReportRow
-> [[(Int, Int, Int)]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BudgetReportRow -> [(Maybe Change, Maybe Change)]
forall a.
PeriodicReportRow a (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
rowToBudgetCells) [BudgetReportRow]
items [[[(Int, Int, Int)]]]
-> [[[(Int, Int, Int)]]] -> [[[(Int, Int, Int)]]]
forall a. [a] -> [a] -> [a]
++ [[(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
cellswidth ([(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]])
-> [(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow () (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
forall a.
PeriodicReportRow a (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
rowToBudgetCells PeriodicReportRow () (Maybe Change, Maybe Change)
totrow]

                cellswidth :: [BudgetCell] -> [[(Int, Int, Int)]]
                cellswidth :: [(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
cellswidth [(Maybe Change, Maybe Change)]
row =
                  let cs :: [Text]
cs = [(Maybe Change, Maybe Change)] -> [Text]
budgetCellsCommodities [(Maybe Change, Maybe Change)]
row
                      (BudgetShowAmountsFn
showmixed, BudgetCalcPercentagesFn
percbudget) = [Text] -> (BudgetShowAmountsFn, BudgetCalcPercentagesFn)
mkBudgetDisplayFns [Text]
cs
                      disp :: (Maybe Change, Maybe Change) -> [BudgetDisplayCell]
disp = BudgetShowAmountsFn
-> BudgetCalcPercentagesFn
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowAmountsFn
showmixed BudgetCalcPercentagesFn
percbudget
                      budgetpercwidth :: (WideBuilder, Maybe WideBuilder) -> (Int, Int)
budgetpercwidth = WideBuilder -> Int
wbWidth (WideBuilder -> Int)
-> (Maybe WideBuilder -> Int)
-> (WideBuilder, Maybe WideBuilder)
-> (Int, Int)
forall b c b' c'. (b -> c) -> (b' -> c') -> (b, b') -> (c, c')
forall (a :: * -> * -> *) b c b' c'.
Arrow a =>
a b c -> a b' c' -> a (b, b') (c, c')
*** Int -> (WideBuilder -> Int) -> Maybe WideBuilder -> Int
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Int
0 WideBuilder -> Int
wbWidth
                      cellwidth :: BudgetDisplayCell -> (Int, Int, Int)
cellwidth (WideBuilder
am, Maybe (WideBuilder, Maybe WideBuilder)
bm) = let (Int
bw, Int
pw) = (Int, Int)
-> ((WideBuilder, Maybe WideBuilder) -> (Int, Int))
-> Maybe (WideBuilder, Maybe WideBuilder)
-> (Int, Int)
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (Int
0, Int
0) (WideBuilder, Maybe WideBuilder) -> (Int, Int)
budgetpercwidth Maybe (WideBuilder, Maybe WideBuilder)
bm in (WideBuilder -> Int
wbWidth WideBuilder
am, Int
bw, Int
pw)
                   in ((Maybe Change, Maybe Change) -> [(Int, Int, Int)])
-> [(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
forall a b. (a -> b) -> [a] -> [b]
map ((BudgetDisplayCell -> (Int, Int, Int))
-> [BudgetDisplayCell] -> [(Int, Int, Int)]
forall a b. (a -> b) -> [a] -> [b]
map BudgetDisplayCell -> (Int, Int, Int)
cellwidth ([BudgetDisplayCell] -> [(Int, Int, Int)])
-> ((Maybe Change, Maybe Change) -> [BudgetDisplayCell])
-> (Maybe Change, Maybe Change)
-> [(Int, Int, Int)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Maybe Change, Maybe Change) -> [BudgetDisplayCell]
disp) [(Maybe Change, Maybe Change)]
row

            totrowwidths :: [(Int, Int, Int)]
            totrowwidths :: [(Int, Int, Int)]
totrowwidths
              | Bool
transpose_ = Int -> [(Int, Int, Int)] -> [(Int, Int, Int)]
forall a. Int -> [a] -> [a]
drop ([[BudgetDisplayCell]] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [[BudgetDisplayCell]]
texts) [(Int, Int, Int)]
widths
              | Bool
otherwise = [(Int, Int, Int)]
widths

            maybetranspose :: [[a]] -> [[a]]
maybetranspose
              | Bool
transpose_ = [[a]] -> [[a]]
forall a. [[a]] -> [[a]]
transpose
              | Bool
otherwise  = [[a]] -> [[a]]
forall a. a -> a
id

        ([Text]
accts', [WideBuilder]
itemscs, [[BudgetDisplayCell]]
texts) = [(Text, WideBuilder, [BudgetDisplayCell])]
-> ([Text], [WideBuilder], [[BudgetDisplayCell]])
forall a b c. [(a, b, c)] -> ([a], [b], [c])
unzip3 ([(Text, WideBuilder, [BudgetDisplayCell])]
 -> ([Text], [WideBuilder], [[BudgetDisplayCell]]))
-> [(Text, WideBuilder, [BudgetDisplayCell])]
-> ([Text], [WideBuilder], [[BudgetDisplayCell]])
forall a b. (a -> b) -> a -> b
$ [[(Text, WideBuilder, [BudgetDisplayCell])]]
-> [(Text, WideBuilder, [BudgetDisplayCell])]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[(Text, WideBuilder, [BudgetDisplayCell])]]
shownitems
          where
            shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]]
            shownitems :: [[(Text, WideBuilder, [BudgetDisplayCell])]]
shownitems =
              (BudgetReportRow -> [(Text, WideBuilder, [BudgetDisplayCell])])
-> [BudgetReportRow]
-> [[(Text, WideBuilder, [BudgetDisplayCell])]]
forall a b. (a -> b) -> [a] -> [b]
map (\BudgetReportRow
i ->
                let
                  addacctcolumn :: [(b, c)] -> [(Text, b, c)]
addacctcolumn = ((b, c) -> (Text, b, c)) -> [(b, c)] -> [(Text, b, c)]
forall a b. (a -> b) -> [a] -> [b]
map (\(b
cs, c
cvals) -> (BudgetReportRow -> Text
forall a. PeriodicReportRow DisplayName a -> Text
renderacct BudgetReportRow
i, b
cs, c
cvals))
                  isunbudgetedrow :: Bool
isunbudgetedrow = DisplayName -> Text
displayFull (BudgetReportRow -> DisplayName
forall a b. PeriodicReportRow a b -> a
prrName BudgetReportRow
i) Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
unbudgetedAccountName
                in [(WideBuilder, [BudgetDisplayCell])]
-> [(Text, WideBuilder, [BudgetDisplayCell])]
forall {b} {c}. [(b, c)] -> [(Text, b, c)]
addacctcolumn ([(WideBuilder, [BudgetDisplayCell])]
 -> [(Text, WideBuilder, [BudgetDisplayCell])])
-> [(WideBuilder, [BudgetDisplayCell])]
-> [(Text, WideBuilder, [BudgetDisplayCell])]
forall a b. (a -> b) -> a -> b
$ Bool
-> [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow Bool
isunbudgetedrow ([(Maybe Change, Maybe Change)]
 -> [(WideBuilder, [BudgetDisplayCell])])
-> [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
forall a b. (a -> b) -> a -> b
$ BudgetReportRow -> [(Maybe Change, Maybe Change)]
forall a.
PeriodicReportRow a (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
rowToBudgetCells BudgetReportRow
i)
              [BudgetReportRow]
items
              where
                -- FIXME. Have to check explicitly for which to render here, since
                -- budgetReport sets accountlistmode to ALTree. Find a principled way to do
                -- this.
                renderacct :: PeriodicReportRow DisplayName a -> Text
renderacct PeriodicReportRow DisplayName a
row = case AccountListMode
accountlistmode_ of
                  AccountListMode
ALTree -> Int -> Text -> Text
T.replicate ((PeriodicReportRow DisplayName a -> Int
forall a. PeriodicReportRow DisplayName a -> Int
prrDepth PeriodicReportRow DisplayName a
row Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)Int -> Int -> Int
forall a. Num a => a -> a -> a
*Int
2) Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PeriodicReportRow DisplayName a -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrDisplayName PeriodicReportRow DisplayName a
row
                  AccountListMode
ALFlat -> Int -> Text -> Text
accountNameDrop (Int
drop_) (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow DisplayName a -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName PeriodicReportRow DisplayName a
row

        ([WideBuilder]
totrowcs, [[BudgetDisplayCell]]
totrowtexts)  = [(WideBuilder, [BudgetDisplayCell])]
-> ([WideBuilder], [[BudgetDisplayCell]])
forall a b. [(a, b)] -> ([a], [b])
unzip  ([(WideBuilder, [BudgetDisplayCell])]
 -> ([WideBuilder], [[BudgetDisplayCell]]))
-> [(WideBuilder, [BudgetDisplayCell])]
-> ([WideBuilder], [[BudgetDisplayCell]])
forall a b. (a -> b) -> a -> b
$ [[(WideBuilder, [BudgetDisplayCell])]]
-> [(WideBuilder, [BudgetDisplayCell])]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[(WideBuilder, [BudgetDisplayCell])]]
showntotrow
          where
            showntotrow :: [[(WideBuilder, BudgetDisplayRow)]]
            showntotrow :: [[(WideBuilder, [BudgetDisplayCell])]]
showntotrow = [Bool
-> [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow Bool
False ([(Maybe Change, Maybe Change)]
 -> [(WideBuilder, [BudgetDisplayCell])])
-> [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow () (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
forall a.
PeriodicReportRow a (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
rowToBudgetCells PeriodicReportRow () (Maybe Change, Maybe Change)
totrow]

        -- | Get the data cells from a row or totals row, maybe adding 
        -- the row total and/or row average depending on options.
        rowToBudgetCells :: PeriodicReportRow a BudgetCell -> [BudgetCell]
        rowToBudgetCells :: forall a.
PeriodicReportRow a (Maybe Change, Maybe Change)
-> [(Maybe Change, Maybe Change)]
rowToBudgetCells (PeriodicReportRow a
_ [(Maybe Change, Maybe Change)]
as (Maybe Change, Maybe Change)
rowtot (Maybe Change, Maybe Change)
rowavg) = [(Maybe Change, Maybe Change)]
as
            [(Maybe Change, Maybe Change)]
-> [(Maybe Change, Maybe Change)] -> [(Maybe Change, Maybe Change)]
forall a. [a] -> [a] -> [a]
++ [(Maybe Change, Maybe Change)
rowtot | Bool
row_total_ Bool -> Bool -> Bool
&& Bool -> Bool
not ([(Maybe Change, Maybe Change)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(Maybe Change, Maybe Change)]
as)]
            [(Maybe Change, Maybe Change)]
-> [(Maybe Change, Maybe Change)] -> [(Maybe Change, Maybe Change)]
forall a. [a] -> [a] -> [a]
++ [(Maybe Change, Maybe Change)
rowavg | Bool
average_   Bool -> Bool -> Bool
&& Bool -> Bool
not ([(Maybe Change, Maybe Change)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(Maybe Change, Maybe Change)]
as)]

        -- | Render a row's data cells as "BudgetDisplayCell"s, and a rendered list of commodity symbols.
        -- Also requires a flag indicating whether this is the special <unbudgeted> row.
        -- (The types make that hard to check here.)
        showrow :: Bool -> [BudgetCell] -> [(WideBuilder, BudgetDisplayRow)]
        showrow :: Bool
-> [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow Bool
isunbudgetedrow [(Maybe Change, Maybe Change)]
cells =
          let
            cs :: [Text]
cs = [(Maybe Change, Maybe Change)] -> [Text]
budgetCellsCommodities [(Maybe Change, Maybe Change)]
cells
            -- #2071 If there are no commodities - because there are no actual or goal amounts -
            -- the zipped list would be empty, causing this row not to be shown.
            -- But rows like this sometimes need to be shown to preserve the account tree structure.
            -- So, ensure 0 will be shown as actual amount(s).
            -- Unfortunately this disables boring parent eliding, as if --no-elide had been used.
            -- (Just turning on --no-elide higher up doesn't work right.)
            -- Note, no goal amount will be shown for these rows,
            -- whereas --no-elide is likely to show a goal amount aggregated from children.
            cs1 :: [Text]
cs1 = if [Text] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text]
cs Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
isunbudgetedrow then [Text
""] else [Text]
cs
            (BudgetShowAmountsFn
showmixed, BudgetCalcPercentagesFn
percbudget) = [Text] -> (BudgetShowAmountsFn, BudgetCalcPercentagesFn)
mkBudgetDisplayFns [Text]
cs1
          in
            [WideBuilder]
-> [[BudgetDisplayCell]] -> [(WideBuilder, [BudgetDisplayCell])]
forall a b. [a] -> [b] -> [(a, b)]
zip ((Text -> WideBuilder) -> [Text] -> [WideBuilder]
forall a b. (a -> b) -> [a] -> [b]
map Text -> WideBuilder
wbFromText [Text]
cs1) ([[BudgetDisplayCell]] -> [(WideBuilder, [BudgetDisplayCell])])
-> [[BudgetDisplayCell]] -> [(WideBuilder, [BudgetDisplayCell])]
forall a b. (a -> b) -> a -> b
$
            [[BudgetDisplayCell]] -> [[BudgetDisplayCell]]
forall a. [[a]] -> [[a]]
transpose ([[BudgetDisplayCell]] -> [[BudgetDisplayCell]])
-> [[BudgetDisplayCell]] -> [[BudgetDisplayCell]]
forall a b. (a -> b) -> a -> b
$
            ((Maybe Change, Maybe Change) -> [BudgetDisplayCell])
-> [(Maybe Change, Maybe Change)] -> [[BudgetDisplayCell]]
forall a b. (a -> b) -> [a] -> [b]
map (BudgetShowAmountsFn
-> BudgetCalcPercentagesFn
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowAmountsFn
showmixed BudgetCalcPercentagesFn
percbudget)
            [(Maybe Change, Maybe Change)]
cells

        budgetCellsCommodities :: [BudgetCell] -> [CommoditySymbol]
        budgetCellsCommodities :: [(Maybe Change, Maybe Change)] -> [Text]
budgetCellsCommodities = Set Text -> [Text]
forall a. Set a -> [a]
S.toList (Set Text -> [Text])
-> ([(Maybe Change, Maybe Change)] -> Set Text)
-> [(Maybe Change, Maybe Change)]
-> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Set Text -> Set Text -> Set Text)
-> Set Text -> [Set Text] -> Set Text
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' Set Text -> Set Text -> Set Text
forall a. Ord a => Set a -> Set a -> Set a
S.union Set Text
forall a. Monoid a => a
mempty ([Set Text] -> Set Text)
-> ([(Maybe Change, Maybe Change)] -> [Set Text])
-> [(Maybe Change, Maybe Change)]
-> Set Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Maybe Change, Maybe Change) -> Set Text)
-> [(Maybe Change, Maybe Change)] -> [Set Text]
forall a b. (a -> b) -> [a] -> [b]
map (Maybe Change, Maybe Change) -> Set Text
budgetCellCommodities
          where
            budgetCellCommodities :: BudgetCell -> S.Set CommoditySymbol
            budgetCellCommodities :: (Maybe Change, Maybe Change) -> Set Text
budgetCellCommodities (Maybe Change
am, Maybe Change
bm) = Maybe Change -> Set Text
f Maybe Change
am Set Text -> Set Text -> Set Text
forall a. Ord a => Set a -> Set a -> Set a
`S.union` Maybe Change -> Set Text
f Maybe Change
bm
              where f :: Maybe Change -> Set Text
f = Set Text -> (Change -> Set Text) -> Maybe Change -> Set Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Set Text
forall a. Monoid a => a
mempty Change -> Set Text
maCommodities

        -- | Render a "BudgetCell"'s amounts as "BudgetDisplayCell"s (one per commodity).
        showcell :: BudgetShowAmountsFn -> BudgetCalcPercentagesFn -> BudgetCell -> BudgetDisplayRow
        showcell :: BudgetShowAmountsFn
-> BudgetCalcPercentagesFn
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowAmountsFn
showCommodityAmounts BudgetCalcPercentagesFn
calcCommodityPercentages (Maybe Change
mactual, Maybe Change
mbudget) =
          [WideBuilder]
-> [Maybe (WideBuilder, Maybe WideBuilder)] -> [BudgetDisplayCell]
forall a b. [a] -> [b] -> [(a, b)]
zip [WideBuilder]
actualamts [Maybe (WideBuilder, Maybe WideBuilder)]
budgetinfos
          where
            actual :: Change
actual = Change -> Maybe Change -> Change
forall a. a -> Maybe a -> a
fromMaybe Change
nullmixedamt Maybe Change
mactual
            actualamts :: [WideBuilder]
actualamts = BudgetShowAmountsFn
showCommodityAmounts Change
actual
            budgetinfos :: [Maybe (WideBuilder, Maybe WideBuilder)]
budgetinfos =
              case Maybe Change
mbudget of
                Maybe Change
Nothing   -> Maybe (WideBuilder, Maybe WideBuilder)
-> [Maybe (WideBuilder, Maybe WideBuilder)]
forall a. a -> [a]
repeat Maybe (WideBuilder, Maybe WideBuilder)
forall a. Maybe a
Nothing
                Just Change
goal -> ((WideBuilder, Maybe WideBuilder)
 -> Maybe (WideBuilder, Maybe WideBuilder))
-> [(WideBuilder, Maybe WideBuilder)]
-> [Maybe (WideBuilder, Maybe WideBuilder)]
forall a b. (a -> b) -> [a] -> [b]
map (WideBuilder, Maybe WideBuilder)
-> Maybe (WideBuilder, Maybe WideBuilder)
forall a. a -> Maybe a
Just ([(WideBuilder, Maybe WideBuilder)]
 -> [Maybe (WideBuilder, Maybe WideBuilder)])
-> [(WideBuilder, Maybe WideBuilder)]
-> [Maybe (WideBuilder, Maybe WideBuilder)]
forall a b. (a -> b) -> a -> b
$ Change -> [(WideBuilder, Maybe WideBuilder)]
showGoalAmountsAndPercentages Change
goal
                where
                  showGoalAmountsAndPercentages :: MixedAmount -> [(WideBuilder, Maybe WideBuilder)]
                  showGoalAmountsAndPercentages :: Change -> [(WideBuilder, Maybe WideBuilder)]
showGoalAmountsAndPercentages Change
goal = [WideBuilder]
-> [Maybe WideBuilder] -> [(WideBuilder, Maybe WideBuilder)]
forall a b. [a] -> [b] -> [(a, b)]
zip [WideBuilder]
amts [Maybe WideBuilder]
mpcts
                    where
                      amts :: [WideBuilder]
amts  = BudgetShowAmountsFn
showCommodityAmounts Change
goal
                      mpcts :: [Maybe WideBuilder]
mpcts = (Maybe Percentage -> Maybe WideBuilder)
-> [Maybe Percentage] -> [Maybe WideBuilder]
forall a b. (a -> b) -> [a] -> [b]
map (Percentage -> WideBuilder
showrounded (Percentage -> WideBuilder)
-> Maybe Percentage -> Maybe WideBuilder
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$>) ([Maybe Percentage] -> [Maybe WideBuilder])
-> [Maybe Percentage] -> [Maybe WideBuilder]
forall a b. (a -> b) -> a -> b
$ BudgetCalcPercentagesFn
calcCommodityPercentages Change
actual Change
goal
                        where showrounded :: Percentage -> WideBuilder
showrounded = Text -> WideBuilder
wbFromText (Text -> WideBuilder)
-> (Percentage -> Text) -> Percentage -> WideBuilder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
T.pack (String -> Text) -> (Percentage -> String) -> Percentage -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Percentage -> String
forall a. Show a => a -> String
show (Percentage -> String)
-> (Percentage -> Percentage) -> Percentage -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Word8 -> Percentage -> Percentage
forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo Word8
0

        -- | Make budget info display helpers that adapt to --layout=wide.
        mkBudgetDisplayFns :: [CommoditySymbol] -> (BudgetShowAmountsFn, BudgetCalcPercentagesFn)
        mkBudgetDisplayFns :: [Text] -> (BudgetShowAmountsFn, BudgetCalcPercentagesFn)
mkBudgetDisplayFns [Text]
cs = case Layout
layout_ of
          LayoutWide Maybe Int
width ->
               ( WideBuilder -> [WideBuilder]
forall a. a -> [a]
forall (f :: * -> *) a. Applicative f => a -> f a
pure (WideBuilder -> [WideBuilder])
-> (Change -> WideBuilder) -> BudgetShowAmountsFn
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountFormat -> Change -> WideBuilder
showMixedAmountB AmountFormat
oneLineNoCostFmt{displayMaxWidth=width, displayColour=color_}
               , \Change
a -> Maybe Percentage -> [Maybe Percentage]
forall a. a -> [a]
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Maybe Percentage -> [Maybe Percentage])
-> (Change -> Maybe Percentage) -> Change -> [Maybe Percentage]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> Change -> Maybe Percentage
percentage Change
a)
          Layout
_ -> ( AmountFormat -> BudgetShowAmountsFn
showMixedAmountLinesB AmountFormat
noCostFmt{displayCommodity=layout_/=LayoutBare, displayCommodityOrder=Just cs, displayMinWidth=Nothing, displayColour=color_}
               , \Change
a Change
b -> (Text -> Maybe Percentage) -> [Text] -> [Maybe Percentage]
forall a b. (a -> b) -> [a] -> [b]
map (Change -> Change -> Text -> Maybe Percentage
percentage' Change
a Change
b) [Text]
cs)
          where
            -- | Calculate the percentage of actual change to budget goal to show, if any.
            -- If valuing at cost, both amounts are converted to cost before comparing.
            -- A percentage will not be shown if:
            --
            -- - actual or goal are not the same, single, commodity
            --
            -- - the goal is zero
            --
            percentage :: Change -> BudgetGoal -> Maybe Percentage
            percentage :: Change -> Change -> Maybe Percentage
percentage Change
actual Change
budget =
              case (Change -> [Amount]
costedAmounts Change
actual, Change -> [Amount]
costedAmounts Change
budget) of
                ([Amount
a], [Amount
b]) | (Amount -> Text
acommodity Amount
a Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> Text
acommodity Amount
b Bool -> Bool -> Bool
|| Amount -> Bool
amountLooksZero Amount
a) Bool -> Bool -> Bool
&& Bool -> Bool
not (Amount -> Bool
amountLooksZero Amount
b)
                    -> Percentage -> Maybe Percentage
forall a. a -> Maybe a
Just (Percentage -> Maybe Percentage) -> Percentage -> Maybe Percentage
forall a b. (a -> b) -> a -> b
$ Percentage
100 Percentage -> Percentage -> Percentage
forall a. Num a => a -> a -> a
* Amount -> Percentage
aquantity Amount
a Percentage -> Percentage -> Percentage
forall a. Fractional a => a -> a -> a
/ Amount -> Percentage
aquantity Amount
b
                ([Amount], [Amount])
_   -> Maybe Percentage
forall a. Maybe a
Nothing
              where
                costedAmounts :: Change -> [Amount]
costedAmounts = case Maybe ConversionOp
conversionop_ of
                    Just ConversionOp
ToCost -> Change -> [Amount]
amounts (Change -> [Amount]) -> (Change -> Change) -> Change -> [Amount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> Change
mixedAmountCost
                    Maybe ConversionOp
_           -> Change -> [Amount]
amounts

            -- | Like percentage, but accept multicommodity actual and budget amounts,
            -- and extract the specified commodity from both.
            percentage' :: Change -> BudgetGoal -> CommoditySymbol -> Maybe Percentage
            percentage' :: Change -> Change -> Text -> Maybe Percentage
percentage' Change
am Change
bm Text
c = case ((,) (Maybe Amount -> Maybe Amount -> (Maybe Amount, Maybe Amount))
-> (Change -> Maybe Amount)
-> Change
-> Change
-> (Maybe Amount, Maybe Amount)
forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
`on` (Amount -> Bool) -> [Amount] -> Maybe Amount
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
(==) Text
c (Text -> Bool) -> (Amount -> Text) -> Amount -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Text
acommodity) ([Amount] -> Maybe Amount)
-> (Change -> [Amount]) -> Change -> Maybe Amount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> [Amount]
amounts) Change
am Change
bm of
                (Just Amount
a, Just Amount
b) -> Change -> Change -> Maybe Percentage
percentage (Amount -> Change
mixedAmount Amount
a) (Amount -> Change
mixedAmount Amount
b)
                (Maybe Amount, Maybe Amount)
_                -> Maybe Percentage
forall a. Maybe a
Nothing

-- XXX generalise this with multiBalanceReportAsCsv ?
-- | Render a budget report as CSV. Like multiBalanceReportAsCsv,
-- but includes alternating actual and budget amount columns.
budgetReportAsCsv :: ReportOpts -> BudgetReport -> [[Text]]
budgetReportAsCsv :: ReportOpts -> BudgetReport -> [[Text]]
budgetReportAsCsv
  ReportOpts{Bool
Int
[Text]
[Status]
Maybe Int
Maybe Text
Maybe NormalSign
Maybe ValuationType
Maybe ConversionOp
Interval
Period
StringFormat
Layout
AccountListMode
BalanceAccumulation
BalanceCalculation
accountlistmode_ :: ReportOpts -> AccountListMode
empty_ :: ReportOpts -> Bool
infer_prices_ :: ReportOpts -> Bool
interval_ :: ReportOpts -> Interval
budgetpat_ :: ReportOpts -> Maybe Text
period_ :: ReportOpts -> Period
statuses_ :: ReportOpts -> [Status]
conversionop_ :: ReportOpts -> Maybe ConversionOp
value_ :: ReportOpts -> Maybe ValuationType
depth_ :: ReportOpts -> Maybe Int
date2_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
real_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
pretty_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
average_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
txn_dates_ :: ReportOpts -> Bool
balancecalc_ :: ReportOpts -> BalanceCalculation
balanceaccum_ :: ReportOpts -> BalanceAccumulation
drop_ :: ReportOpts -> Int
declared_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
summary_only_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
invert_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
color_ :: ReportOpts -> Bool
transpose_ :: ReportOpts -> Bool
layout_ :: ReportOpts -> Layout
period_ :: Period
interval_ :: Interval
statuses_ :: [Status]
conversionop_ :: Maybe ConversionOp
value_ :: Maybe ValuationType
infer_prices_ :: Bool
depth_ :: Maybe Int
date2_ :: Bool
empty_ :: Bool
no_elide_ :: Bool
real_ :: Bool
format_ :: StringFormat
pretty_ :: Bool
querystring_ :: [Text]
average_ :: Bool
related_ :: Bool
txn_dates_ :: Bool
balancecalc_ :: BalanceCalculation
balanceaccum_ :: BalanceAccumulation
budgetpat_ :: Maybe Text
accountlistmode_ :: AccountListMode
drop_ :: Int
declared_ :: Bool
row_total_ :: Bool
no_total_ :: Bool
summary_only_ :: Bool
show_costs_ :: Bool
sort_amount_ :: Bool
percent_ :: Bool
invert_ :: Bool
normalbalance_ :: Maybe NormalSign
color_ :: Bool
transpose_ :: Bool
layout_ :: Layout
..}
  (PeriodicReport [DateSpan]
colspans [BudgetReportRow]
items PeriodicReportRow () (Maybe Change, Maybe Change)
totrow)
  = (if Bool
transpose_ then [[Text]] -> [[Text]]
forall a. [[a]] -> [[a]]
transpose else [[Text]] -> [[Text]]
forall a. a -> a
id) ([[Text]] -> [[Text]]) -> [[Text]] -> [[Text]]
forall a b. (a -> b) -> a -> b
$

  -- heading row
  (Text
"Account" Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
:
  [Text
"Commodity" | Layout
layout_ Layout -> Layout -> Bool
forall a. Eq a => a -> a -> Bool
== Layout
LayoutBare ]
   [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ (DateSpan -> [Text]) -> [DateSpan] -> [Text]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\DateSpan
spn -> [DateSpan -> Text
showDateSpan DateSpan
spn, Text
"budget"]) [DateSpan]
colspans
   [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [[Text]] -> [Text]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Text
"Total"  ,Text
"budget"] | Bool
row_total_]
   [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [[Text]] -> [Text]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Text
"Average",Text
"budget"] | Bool
average_]
  ) [Text] -> [[Text]] -> [[Text]]
forall a. a -> [a] -> [a]
:

  -- account rows
  (BudgetReportRow -> [[Text]]) -> [BudgetReportRow] -> [[Text]]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap ((BudgetReportRow -> Text) -> BudgetReportRow -> [[Text]]
forall a.
(PeriodicReportRow a (Maybe Change, Maybe Change) -> Text)
-> PeriodicReportRow a (Maybe Change, Maybe Change) -> [[Text]]
rowAsTexts BudgetReportRow -> Text
forall a. PeriodicReportRow DisplayName a -> Text
prrFullName) [BudgetReportRow]
items

  -- totals row
  [[Text]] -> [[Text]] -> [[Text]]
forall a. [a] -> [a] -> [a]
++ [[[Text]]] -> [[Text]]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [ (PeriodicReportRow () (Maybe Change, Maybe Change) -> Text)
-> PeriodicReportRow () (Maybe Change, Maybe Change) -> [[Text]]
forall a.
(PeriodicReportRow a (Maybe Change, Maybe Change) -> Text)
-> PeriodicReportRow a (Maybe Change, Maybe Change) -> [[Text]]
rowAsTexts (Text -> PeriodicReportRow () (Maybe Change, Maybe Change) -> Text
forall a b. a -> b -> a
const Text
"Total:") PeriodicReportRow () (Maybe Change, Maybe Change)
totrow | Bool -> Bool
not Bool
no_total_ ]

  where
    flattentuples :: [(a, a)] -> [a]
flattentuples [(a, a)]
tups = [[a]] -> [a]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[a
a,a
b] | (a
a,a
b) <- [(a, a)]
tups]
    showNorm :: Maybe Change -> Text
showNorm = Text -> (Change -> Text) -> Maybe Change -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
"" (WideBuilder -> Text
wbToText (WideBuilder -> Text) -> (Change -> WideBuilder) -> Change -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountFormat -> Change -> WideBuilder
showMixedAmountB AmountFormat
oneLineNoCostFmt)

    rowAsTexts :: (PeriodicReportRow a BudgetCell -> Text)
               -> PeriodicReportRow a BudgetCell
               -> [[Text]]
    rowAsTexts :: forall a.
(PeriodicReportRow a (Maybe Change, Maybe Change) -> Text)
-> PeriodicReportRow a (Maybe Change, Maybe Change) -> [[Text]]
rowAsTexts PeriodicReportRow a (Maybe Change, Maybe Change) -> Text
render row :: PeriodicReportRow a (Maybe Change, Maybe Change)
row@(PeriodicReportRow a
_ [(Maybe Change, Maybe Change)]
as (Maybe Change
rowtot,Maybe Change
budgettot) (Maybe Change
rowavg, Maybe Change
budgetavg))
      | Layout
layout_ Layout -> Layout -> Bool
forall a. Eq a => a -> a -> Bool
/= Layout
LayoutBare = [PeriodicReportRow a (Maybe Change, Maybe Change) -> Text
render PeriodicReportRow a (Maybe Change, Maybe Change)
row Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: (Maybe Change -> Text) -> [Maybe Change] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Maybe Change -> Text
showNorm [Maybe Change]
vals]
      | Bool
otherwise =
            [[Text]] -> [[Text]]
joinNames ([[Text]] -> [[Text]])
-> ([Maybe Change] -> [[Text]]) -> [Maybe Change] -> [[Text]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> [Text] -> [Text]) -> [Text] -> [[Text]] -> [[Text]]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (:) [Text]
cs  -- add symbols and names
          ([[Text]] -> [[Text]])
-> ([Maybe Change] -> [[Text]]) -> [Maybe Change] -> [[Text]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [[Text]] -> [[Text]]
forall a. [[a]] -> [[a]]
transpose                   -- each row becomes a list of Text quantities
          ([[Text]] -> [[Text]])
-> ([Maybe Change] -> [[Text]]) -> [Maybe Change] -> [[Text]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Maybe Change -> [Text]) -> [Maybe Change] -> [[Text]]
forall a b. (a -> b) -> [a] -> [b]
map ((WideBuilder -> Text) -> [WideBuilder] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map WideBuilder -> Text
wbToText ([WideBuilder] -> [Text])
-> (Maybe Change -> [WideBuilder]) -> Maybe Change -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountFormat -> BudgetShowAmountsFn
showMixedAmountLinesB AmountFormat
dopts BudgetShowAmountsFn
-> (Maybe Change -> Change) -> Maybe Change -> [WideBuilder]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> Maybe Change -> Change
forall a. a -> Maybe a -> a
fromMaybe Change
nullmixedamt)
          ([Maybe Change] -> [[Text]]) -> [Maybe Change] -> [[Text]]
forall a b. (a -> b) -> a -> b
$ [Maybe Change]
vals
      where
        cs :: [Text]
cs = Set Text -> [Text]
forall a. Set a -> [a]
S.toList (Set Text -> [Text])
-> ([Change] -> Set Text) -> [Change] -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Set Text -> Set Text -> Set Text)
-> Set Text -> [Set Text] -> Set Text
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' Set Text -> Set Text -> Set Text
forall a. Ord a => Set a -> Set a -> Set a
S.union Set Text
forall a. Monoid a => a
mempty ([Set Text] -> Set Text)
-> ([Change] -> [Set Text]) -> [Change] -> Set Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Change -> Set Text) -> [Change] -> [Set Text]
forall a b. (a -> b) -> [a] -> [b]
map Change -> Set Text
maCommodities ([Change] -> [Text]) -> [Change] -> [Text]
forall a b. (a -> b) -> a -> b
$ [Maybe Change] -> [Change]
forall a. [Maybe a] -> [a]
catMaybes [Maybe Change]
vals
        dopts :: AmountFormat
dopts = AmountFormat
oneLineNoCostFmt{displayCommodity=layout_ /= LayoutBare, displayCommodityOrder=Just cs, displayMinWidth=Nothing}
        vals :: [Maybe Change]
vals = [(Maybe Change, Maybe Change)] -> [Maybe Change]
forall {a}. [(a, a)] -> [a]
flattentuples [(Maybe Change, Maybe Change)]
as
            [Maybe Change] -> [Maybe Change] -> [Maybe Change]
forall a. [a] -> [a] -> [a]
++ [[Maybe Change]] -> [Maybe Change]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe Change
rowtot, Maybe Change
budgettot] | Bool
row_total_]
            [Maybe Change] -> [Maybe Change] -> [Maybe Change]
forall a. [a] -> [a] -> [a]
++ [[Maybe Change]] -> [Maybe Change]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe Change
rowavg, Maybe Change
budgetavg] | Bool
average_]

        joinNames :: [[Text]] -> [[Text]]
joinNames = ([Text] -> [Text]) -> [[Text]] -> [[Text]]
forall a b. (a -> b) -> [a] -> [b]
map (PeriodicReportRow a (Maybe Change, Maybe Change) -> Text
render PeriodicReportRow a (Maybe Change, Maybe Change)
row Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
:)

-- tests

tests_BudgetReport :: TestTree
tests_BudgetReport = String -> [TestTree] -> TestTree
testGroup String
"BudgetReport" [
 ]