{-# 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)
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 ((>=>))


type BudgetGoal    = Change
type BudgetTotal   = Total
type BudgetAverage = Average

-- | A budget report tracks expected and actual changes per account and subperiod.
type BudgetCell = (Maybe Change, Maybe BudgetGoal)
type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell
type BudgetReport    = PeriodicReport    DisplayName BudgetCell

type BudgetDisplayCell = (WideBuilder, Maybe (WideBuilder, Maybe WideBuilder))
type BudgetDisplayRow  = [BudgetDisplayCell]
type BudgetShowMixed   = MixedAmount -> [WideBuilder]
type BudgetPercBudget  = Change -> BudgetGoal -> [Maybe Percentage]

-- | 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 = 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_ :: AccountListMode
accountlistmode_ = AccountListMode
ALTree }
    showunbudgeted :: Bool
showunbudgeted = ReportOpts -> Bool
empty_ ReportOpts
ropts
    budgetedaccts :: Set Text
budgetedaccts =
      forall a. Show a => String -> a -> a
dbg3 String
"budgetedacctsinperiod" forall a b. (a -> b) -> a -> b
$
      forall a. Ord a => [a] -> Set a
S.fromList forall a b. (a -> b) -> a -> b
$
      [Text] -> [Text]
expandAccountNames forall a b. (a -> b) -> a -> b
$
      [Posting] -> [Text]
accountNamesFromPostings forall a b. (a -> b) -> a -> b
$
      forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
tpostings forall a b. (a -> b) -> a -> b
$
      forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (PeriodicTransaction -> DateSpan -> [Transaction]
`runPeriodicTransaction` DateSpan
reportspan) 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) =
        forall a. Show a => String -> a -> a
dbg5 String
"budgetgoalreport" forall a b. (a -> b) -> a -> b
$ ReportSpec
-> Journal -> PriceOracle -> Set Text -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec{_rsReportOpts :: ReportOpts
_rsReportOpts=ReportOpts
ropts{empty_ :: Bool
empty_=Bool
True}} Journal
budgetj PriceOracle
priceoracle forall a. Monoid a => a
mempty
    budgetedacctsseen :: Set Text
budgetedacctsseen = forall a. Ord a => [a] -> Set a
S.fromList forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map forall a. PeriodicReportRow DisplayName a -> Text
prrFullName [PeriodicReportRow DisplayName Change]
budgetgoalitems
    actualreport :: MultiBalanceReport
actualreport@(PeriodicReport [DateSpan]
actualspans [PeriodicReportRow DisplayName Change]
_ PeriodicReportRow () Change
_) =
        forall a. Show a => String -> a -> a
dbg5 String
"actualreport"     forall a b. (a -> b) -> a -> b
$ ReportSpec
-> Journal -> PriceOracle -> Set Text -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec{_rsReportOpts :: ReportOpts
_rsReportOpts=ReportOpts
ropts{empty_ :: Bool
empty_=Bool
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 forall a. Eq a => a -> a -> Bool
== Interval
NoInterval = 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 =
  forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either forall a. String -> a
error' forall a. a -> a
id forall a b. (a -> b) -> a -> b
$  -- PARTIAL:
    (Journal -> Either String Journal
journalApplyCommodityStyles 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 :: [Transaction]
jtxns = [Transaction]
budgetts }
  where
    budgetspan :: DateSpan
budgetspan = forall a. Show a => String -> a -> a
dbg3 String
"budget span" forall a b. (a -> b) -> a -> b
$ Maybe Day -> Maybe Day -> DateSpan
DateSpan Maybe Day
mbudgetgoalsstartdate (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 forall a. Ord a => a -> [a] -> a
minimumDef forall a. Maybe a
Nothing forall a b. (a -> b) -> a -> b
$ forall a. (a -> Bool) -> [a] -> [a]
filter 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 -> forall a. Maybe a
Nothing
            Just Day
d  -> 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 = forall (t :: * -> *) a.
Foldable t =>
(a -> a -> Ordering) -> t a -> a
maximumBy (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
intervalStartBefore 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' = forall a. Ord a => a -> a -> a
min Day
d forall a b. (a -> b) -> a -> b
$ forall b a. b -> (a -> b) -> Maybe a -> b
maybe Day
intervalstart (forall a. Ord a => a -> a -> a
max Day
intervalstart) 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 = forall a. a -> Maybe a -> a
fromMaybe Text
"" forall a b. (a -> b) -> a -> b
$ forall a. Show a => String -> a -> a
dbg3 String
"budget pattern" forall a b. (a -> b) -> a -> b
$ Text -> Text
T.toLower 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 =
      forall a. Show a => String -> a -> a
dbg5 String
"budget goal txns" forall a b. (a -> b) -> a -> b
$
      [Transaction -> Transaction
makeBudgetTxn Transaction
t
      | PeriodicTransaction
pt <- [PeriodicTransaction]
budgetpts
      , Transaction
t <- PeriodicTransaction -> DateSpan -> [Transaction]
runPeriodicTransaction PeriodicTransaction
pt DateSpan
budgetspan
      ]
    makeBudgetTxn :: Transaction -> Transaction
makeBudgetTxn Transaction
t = Transaction -> Transaction
txnTieKnot forall a b. (a -> b) -> a -> b
$ Transaction
t { tdescription :: Text
tdescription = String -> Text
T.pack String
"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 =
  forall a. Show a => (a -> String) -> a -> a
dbg5With ((String
"budget account names: "forall a. [a] -> [a] -> [a]
++)forall b c a. (b -> c) -> (a -> b) -> a -> c
.forall a. Show a => a -> String
pshowforall b c a. (b -> c) -> (a -> b) -> a -> c
.Journal -> [Text]
journalAccountNamesUsed) forall a b. (a -> b) -> a -> b
$
  Journal
j { jtxns :: [Transaction]
jtxns = Transaction -> Transaction
remapTxn forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Journal -> [Transaction]
jtxns Journal
j }
  where
    remapTxn :: Transaction -> Transaction
remapTxn = Transaction -> Transaction
txnTieKnot 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 :: Text
paccount = Text -> Text
remapAccount forall a b. (a -> b) -> a -> b
$ Posting -> Text
paccount Posting
p, poriginal :: Maybe Posting
poriginal = Posting -> Maybe Posting
poriginal Posting
p forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall a. a -> Maybe a
Just Posting
p }
    remapAccount :: Text -> Text
remapAccount Text
a
      | Text
a 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 forall a. Semigroup a => a -> a -> a
<> Text
acctsep forall a. Semigroup a => a -> a -> a
<> Text
a else Text
u
      where
        budgetedparent :: Maybe Text
budgetedparent = forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (forall a. Ord a => a -> Set a -> Bool
`S.member` Set Text
budgetedaccts) 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)) =
    forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
periods [BudgetReportRow]
sortedrows PeriodicReportRow () (Maybe Change, Maybe Change)
totalrow
  where
    periods :: [DateSpan]
periods = forall a. Ord a => [a] -> [a]
nubSort forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. (a -> Bool) -> [a] -> [a]
filter (forall a. Eq a => a -> a -> Bool
/= DateSpan
nulldatespan) forall a b. (a -> b) -> a -> b
$ [DateSpan]
budgetperiods forall a. [a] -> [a] -> [a]
++ [DateSpan]
actualperiods

    -- first, combine any corresponding budget goals with actual changes
    rows1 :: [BudgetReportRow]
rows1 =
      [ 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       = 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        = forall b a. b -> (a -> b) -> Maybe a -> b
maybe (forall a. Maybe a
Nothing forall (f :: * -> *) a b. Functor f => a -> f b -> f a
<$ [DateSpan]
periods) (forall a b. (a -> b) -> [a] -> [b]
map forall a. a -> Maybe a
Just forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall {a} {b} {c}. (a, b, c) -> a
first3) Maybe ([Change], Change, Change)
mbudgetgoals :: [Maybe BudgetGoal]
      , let mbudgettot :: Maybe Change
mbudgettot         = forall {a} {b} {c}. (a, b, c) -> b
second3 forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([Change], Change, Change)
mbudgetgoals :: Maybe BudgetTotal
      , let mbudgetavg :: Maybe Change
mbudgetavg         = forall {a} {b} {c}. (a, b, c) -> c
third3 forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([Change], Change, Change)
mbudgetgoals  :: Maybe BudgetAverage
      , let acctBudgetByPeriod :: Map DateSpan Change
acctBudgetByPeriod = forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,Change
budgetamt) | (DateSpan
p, Just Change
budgetamt) <- forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Maybe Change]
budgetmamts ] :: Map DateSpan BudgetGoal
      , let acctActualByPeriod :: Map DateSpan Change
acctActualByPeriod = forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,Change
actualamt) | (DateSpan
p, Just Change
actualamt) <- forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
actualperiods (forall a b. (a -> b) -> [a] -> [b]
map forall a. a -> Maybe a
Just [Change]
actualamts) ] :: Map DateSpan Change
      , let amtandgoals :: [(Maybe Change, Maybe Change)]
amtandgoals        = [ (forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
acctActualByPeriod, forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
acctBudgetByPeriod) | DateSpan
p <- [DateSpan]
periods ] :: [BudgetCell]
      , let totamtandgoal :: (Maybe Change, Maybe Change)
totamtandgoal      = (forall a. a -> Maybe a
Just Change
actualtot, Maybe Change
mbudgettot)
      , let avgamtandgoal :: (Maybe Change, Maybe Change)
avgamtandgoal      = (forall a. a -> Maybe a
Just Change
actualavg, Maybe Change
mbudgetavg)
      ]
      where
        HashMap Text ([Change], Change, Change)
budgetGoalsByAcct :: HashMap AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) =
          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 <- [PeriodicReportRow DisplayName Change]
budgetrows ]

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

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

    totalrow :: PeriodicReportRow () (Maybe Change, Maybe Change)
totalrow = forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow ()
        [ (forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
totActualByPeriod, forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan Change
totBudgetByPeriod) | DateSpan
p <- [DateSpan]
periods ]
        ( forall a. a -> Maybe a
Just Change
actualgrandtot, Change -> Maybe Change
budget Change
budgetgrandtot )
        ( forall a. a -> Maybe a
Just Change
actualgrandavg, Change -> Maybe Change
budget Change
budgetgrandavg )
      where
        totBudgetByPeriod :: Map DateSpan Change
totBudgetByPeriod = forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList forall a b. (a -> b) -> a -> b
$ forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Change]
budgettots :: Map DateSpan BudgetTotal
        totActualByPeriod :: Map DateSpan Change
totActualByPeriod = forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList forall a b. (a -> b) -> a -> b
$ 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 forall a. Maybe a
Nothing else 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
layout_ :: ReportOpts -> Layout
transpose_ :: ReportOpts -> Bool
color_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
invert_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
declared_ :: ReportOpts -> Bool
drop_ :: ReportOpts -> Int
balanceaccum_ :: ReportOpts -> BalanceAccumulation
balancecalc_ :: ReportOpts -> BalanceCalculation
txn_dates_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
average_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
pretty_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
real_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
date2_ :: ReportOpts -> Bool
depth_ :: ReportOpts -> Maybe Int
value_ :: ReportOpts -> Maybe ValuationType
conversionop_ :: ReportOpts -> Maybe ConversionOp
statuses_ :: ReportOpts -> [Status]
period_ :: ReportOpts -> Period
layout_ :: Layout
transpose_ :: Bool
color_ :: Bool
normalbalance_ :: Maybe NormalSign
invert_ :: Bool
percent_ :: Bool
sort_amount_ :: Bool
show_costs_ :: Bool
no_total_ :: Bool
row_total_ :: Bool
declared_ :: Bool
drop_ :: Int
accountlistmode_ :: AccountListMode
budgetpat_ :: Maybe Text
balanceaccum_ :: BalanceAccumulation
balancecalc_ :: BalanceCalculation
txn_dates_ :: Bool
related_ :: Bool
average_ :: Bool
querystring_ :: [Text]
pretty_ :: Bool
format_ :: StringFormat
real_ :: Bool
no_elide_ :: Bool
empty_ :: Bool
date2_ :: Bool
depth_ :: Maybe Int
infer_prices_ :: Bool
value_ :: Maybe ValuationType
conversionop_ :: Maybe ConversionOp
statuses_ :: [Status]
interval_ :: Interval
period_ :: Period
budgetpat_ :: ReportOpts -> Maybe Text
interval_ :: ReportOpts -> Interval
infer_prices_ :: ReportOpts -> Bool
empty_ :: ReportOpts -> Bool
accountlistmode_ :: ReportOpts -> AccountListMode
..} BudgetReport
budgetr = Builder -> Text
TB.toLazyText forall a b. (a -> b) -> a -> b
$
    Text -> Builder
TB.fromText Text
title forall a. Semigroup a => a -> a -> a
<> Text -> Builder
TB.fromText Text
"\n\n" 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 " forall a. Semigroup a => a -> a -> a
<> DateSpan -> Text
showDateSpan (forall a b. PeriodicReport a b -> DateSpan
periodicReportSpan BudgetReport
budgetr)
           forall a. Semigroup a => a -> a -> a
<> (case Maybe ConversionOp
conversionop_ of
                 Just ConversionOp
ToCost -> Text
", converted to cost"
                 Maybe ConversionOp
_           -> 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 " forall a. Semigroup a => a -> a -> a
<> Day -> Text
showDate Day
d
                 Maybe ValuationType
Nothing             -> 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
layout_ :: Layout
transpose_ :: Bool
color_ :: Bool
normalbalance_ :: Maybe NormalSign
invert_ :: Bool
percent_ :: Bool
sort_amount_ :: Bool
show_costs_ :: Bool
no_total_ :: Bool
row_total_ :: Bool
declared_ :: Bool
drop_ :: Int
accountlistmode_ :: AccountListMode
budgetpat_ :: Maybe Text
balanceaccum_ :: BalanceAccumulation
balancecalc_ :: BalanceCalculation
txn_dates_ :: Bool
related_ :: Bool
average_ :: Bool
querystring_ :: [Text]
pretty_ :: Bool
format_ :: StringFormat
real_ :: Bool
no_elide_ :: Bool
empty_ :: Bool
date2_ :: Bool
depth_ :: Maybe Int
infer_prices_ :: Bool
value_ :: Maybe ValuationType
conversionop_ :: Maybe ConversionOp
statuses_ :: [Status]
interval_ :: Interval
period_ :: Period
layout_ :: ReportOpts -> Layout
transpose_ :: ReportOpts -> Bool
color_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
invert_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
declared_ :: ReportOpts -> Bool
drop_ :: ReportOpts -> Int
balanceaccum_ :: ReportOpts -> BalanceAccumulation
balancecalc_ :: ReportOpts -> BalanceCalculation
txn_dates_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
average_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
pretty_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
real_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
date2_ :: ReportOpts -> Bool
depth_ :: ReportOpts -> Maybe Int
value_ :: ReportOpts -> Maybe ValuationType
conversionop_ :: ReportOpts -> Maybe ConversionOp
statuses_ :: ReportOpts -> [Status]
period_ :: ReportOpts -> Period
budgetpat_ :: ReportOpts -> Maybe Text
interval_ :: ReportOpts -> Interval
infer_prices_ :: ReportOpts -> Bool
empty_ :: ReportOpts -> Bool
accountlistmode_ :: ReportOpts -> AccountListMode
..}
  (PeriodicReport [DateSpan]
spans [BudgetReportRow]
items PeriodicReportRow () (Maybe Change, Maybe Change)
tr) =
    forall {rh} {a}. Table rh rh a -> Table rh rh a
maybetransposetable forall a b. (a -> b) -> a -> b
$
    forall {ch}. Table Text ch WideBuilder -> Table Text ch WideBuilder
addtotalrow forall a b. (a -> b) -> a -> b
$
    forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Tab.Table
      (forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map forall h. h -> Header h
Tab.Header [Text]
accts)
      (forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map forall h. h -> Header h
Tab.Header [Text]
colheadings)
      [[WideBuilder]]
rows
  where
    colheadings :: [Text]
colheadings = [Text
"Commodity" | Layout
layout_ forall a. Eq a => a -> a -> Bool
== Layout
LayoutBare]
                  forall a. [a] -> [a] -> [a]
++ forall a b. (a -> b) -> [a] -> [b]
map (BalanceAccumulation -> [DateSpan] -> DateSpan -> Text
reportPeriodName BalanceAccumulation
balanceaccum_ [DateSpan]
spans) [DateSpan]
spans
                  forall a. [a] -> [a] -> [a]
++ [Text
"  Total" | Bool
row_total_]
                  forall a. [a] -> [a] -> [a]
++ [Text
"Average" | Bool
average_]

    -- 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 ((forall a. PeriodicReportRow DisplayName a -> Int
prrDepth PeriodicReportRow DisplayName a
row forall a. Num a => a -> a -> a
- Int
1)forall a. Num a => a -> a -> a
*Int
2) Text
" " forall a. Semigroup a => a -> a -> a
<> forall a. PeriodicReportRow DisplayName a -> Text
prrDisplayName PeriodicReportRow DisplayName a
row
        AccountListMode
ALFlat -> Int -> Text -> Text
accountNameDrop (Int
drop_) forall a b. (a -> b) -> a -> b
$ forall a. PeriodicReportRow DisplayName a -> Text
prrFullName PeriodicReportRow DisplayName a
row

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

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

    maybetransposetable :: Table rh rh a -> Table rh rh a
maybetransposetable
      | Bool
transpose_ = \(Tab.Table Header rh
rh Header rh
ch [[a]]
vals) -> forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Tab.Table Header rh
ch Header rh
rh (forall a. [[a]] -> [[a]]
transpose [[a]]
vals)
      | Bool
otherwise  = forall a. a -> a
id

    ([Text]
accts, [[WideBuilder]]
rows, [[WideBuilder]]
totalrows) = ([Text]
accts', forall {a}. [a] -> [[a]] -> [[a]]
prependcs [WideBuilder]
itemscs ([[BudgetDisplayCell]] -> [[WideBuilder]]
padcells [[BudgetDisplayCell]]
texts), forall {a}. [a] -> [[a]] -> [[a]]
prependcs [WideBuilder]
trcs ([[BudgetDisplayCell]] -> [[WideBuilder]]
padtr [[BudgetDisplayCell]]
trtexts))
      where
        shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]]
        shownitems :: [[(Text, WideBuilder, [BudgetDisplayCell])]]
shownitems = (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (\BudgetReportRow
i -> forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (\(WideBuilder
cs, [BudgetDisplayCell]
cvals) -> (forall a. PeriodicReportRow DisplayName a -> Text
renderacct BudgetReportRow
i, WideBuilder
cs, [BudgetDisplayCell]
cvals)) forall b c a. (b -> c) -> (a -> b) -> a -> c
. [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow forall a b. (a -> b) -> a -> b
$ forall {a} {a}. PeriodicReportRow a a -> [a]
rowToBudgetCells BudgetReportRow
i) [BudgetReportRow]
items)
        ([Text]
accts', [WideBuilder]
itemscs, [[BudgetDisplayCell]]
texts) = forall a b c. [(a, b, c)] -> ([a], [b], [c])
unzip3 forall a b. (a -> b) -> a -> b
$ forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[(Text, WideBuilder, [BudgetDisplayCell])]]
shownitems

        showntr    :: [[(WideBuilder, BudgetDisplayRow)]]
        showntr :: [[(WideBuilder, [BudgetDisplayCell])]]
showntr    = [[(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow forall a b. (a -> b) -> a -> b
$ forall {a} {a}. PeriodicReportRow a a -> [a]
rowToBudgetCells PeriodicReportRow () (Maybe Change, Maybe Change)
tr]
        ([WideBuilder]
trcs, [[BudgetDisplayCell]]
trtexts)         = forall a b. [(a, b)] -> ([a], [b])
unzip  forall a b. (a -> b) -> a -> b
$ forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[(WideBuilder, [BudgetDisplayCell])]]
showntr
        trwidths :: [(Int, Int, Int)]
trwidths
          | Bool
transpose_ = forall a. Int -> [a] -> [a]
drop (forall (t :: * -> *) a. Foldable t => t a -> Int
length [[BudgetDisplayCell]]
texts) [(Int, Int, Int)]
widths
          | Bool
otherwise = [(Int, Int, Int)]
widths

        padcells :: [[BudgetDisplayCell]] -> [[WideBuilder]]
padcells = forall a. [[a]] -> [[a]]
maybetranspose forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
paddisplaycell) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. [a] -> [b] -> [(a, b)]
zip [(Int, Int, Int)]
widths)   forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. [[a]] -> [[a]]
maybetranspose
        padtr :: [[BudgetDisplayCell]] -> [[WideBuilder]]
padtr    = forall a. [[a]] -> [[a]]
maybetranspose forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
paddisplaycell) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. [a] -> [b] -> [(a, b)]
zip [(Int, Int, Int)]
trwidths) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. [[a]] -> [[a]]
maybetranspose

        -- commodities are shown with the amounts without `layout_ == LayoutBare`
        prependcs :: [a] -> [[a]] -> [[a]]
prependcs [a]
cs
          | Layout
layout_ forall a. Eq a => a -> a -> Bool
/= Layout
LayoutBare = forall a. a -> a
id
          | Bool
otherwise = forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (:) [a]
cs

    rowToBudgetCells :: PeriodicReportRow a a -> [a]
rowToBudgetCells (PeriodicReportRow a
_ [a]
as a
rowtot a
rowavg) = [a]
as
        forall a. [a] -> [a] -> [a]
++ [a
rowtot | Bool
row_total_ Bool -> Bool -> Bool
&& Bool -> Bool
not (forall (t :: * -> *) a. Foldable t => t a -> Bool
null [a]
as)]
        forall a. [a] -> [a] -> [a]
++ [a
rowavg | Bool
average_   Bool -> Bool -> Bool
&& Bool -> Bool
not (forall (t :: * -> *) a. Foldable t => t a -> Bool
null [a]
as)]

    -- functions for displaying budget cells depending on `commodity-layout_` option
    rowfuncs :: [CommoditySymbol] -> (BudgetShowMixed, BudgetPercBudget)
    rowfuncs :: [Text] -> (BudgetShowMixed, BudgetPercBudget)
rowfuncs [Text]
cs = case Layout
layout_ of
      LayoutWide Maybe Int
width ->
           ( forall (f :: * -> *) a. Applicative f => a -> f a
pure forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountDisplayOpts -> Change -> WideBuilder
showMixedAmountB AmountDisplayOpts
oneLine{displayColour :: Bool
displayColour=Bool
color_, displayMaxWidth :: Maybe Int
displayMaxWidth=Maybe Int
width}
           , \Change
a -> forall (f :: * -> *) a. Applicative f => a -> f a
pure forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> Change -> Maybe Percentage
percentage Change
a)
      Layout
_ -> ( AmountDisplayOpts -> BudgetShowMixed
showMixedAmountLinesB AmountDisplayOpts
noPrice{displayOrder :: Maybe [Text]
displayOrder=forall a. a -> Maybe a
Just [Text]
cs, displayMinWidth :: Maybe Int
displayMinWidth=forall a. Maybe a
Nothing, displayColour :: Bool
displayColour=Bool
color_}
           , \Change
a Change
b -> forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Change -> Change -> Text -> Maybe Percentage
percentage' Change
a Change
b) [Text]
cs)

    showrow :: [BudgetCell] -> [(WideBuilder, BudgetDisplayRow)]
    showrow :: [(Maybe Change, Maybe Change)]
-> [(WideBuilder, [BudgetDisplayCell])]
showrow [(Maybe Change, Maybe Change)]
row =
      let cs :: [Text]
cs = [(Maybe Change, Maybe Change)] -> [Text]
budgetCellsCommodities [(Maybe Change, Maybe Change)]
row
          (BudgetShowMixed
showmixed, BudgetPercBudget
percbudget) = [Text] -> (BudgetShowMixed, BudgetPercBudget)
rowfuncs [Text]
cs
       in   forall a b. [a] -> [b] -> [(a, b)]
zip (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Text -> WideBuilder
wbFromText [Text]
cs)
          forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. [[a]] -> [[a]]
transpose
          forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (BudgetShowMixed
-> BudgetPercBudget
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowMixed
showmixed BudgetPercBudget
percbudget)
          forall a b. (a -> b) -> a -> b
$ [(Maybe Change, Maybe Change)]
row

    budgetCellsCommodities :: [(Maybe Change, Maybe Change)] -> [Text]
budgetCellsCommodities = forall a. Set a -> [a]
S.toList forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' forall a. Ord a => Set a -> Set a -> Set a
S.union forall a. Monoid a => a
mempty forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Maybe Change, Maybe Change) -> Set Text
budgetCellCommodities
    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 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 = forall b a. b -> (a -> b) -> Maybe a -> b
maybe forall a. Monoid a => a
mempty Change -> Set Text
maCommodities

    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
          (BudgetShowMixed
showmixed, BudgetPercBudget
percbudget) = [Text] -> (BudgetShowMixed, BudgetPercBudget)
rowfuncs [Text]
cs
          disp :: (Maybe Change, Maybe Change) -> [BudgetDisplayCell]
disp = BudgetShowMixed
-> BudgetPercBudget
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowMixed
showmixed BudgetPercBudget
percbudget
          budgetpercwidth :: (WideBuilder, Maybe WideBuilder) -> (Int, Int)
budgetpercwidth = WideBuilder -> Int
wbWidth forall (a :: * -> * -> *) b c b' c'.
Arrow a =>
a b c -> a b' c' -> a (b, b') (c, c')
*** 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) = 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 forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap BudgetDisplayCell -> (Int, Int, Int)
cellwidth forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Maybe Change, Maybe Change) -> [BudgetDisplayCell]
disp) [(Maybe Change, Maybe Change)]
row

    -- build a list of widths for each column. In the case of transposed budget
    -- reports, the total 'row' must be included in this list
    widths :: [(Int, Int, Int)]
widths = forall a b c. [a] -> [b] -> [c] -> [(a, b, c)]
zip3 [Int]
actualwidths [Int]
budgetwidths [Int]
percentwidths
      where
        actualwidths :: [Int]
actualwidths  = forall a b. (a -> b) -> [a] -> [b]
map (forall a. Integral a => [a] -> a
maximum' forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map forall {a} {b} {c}. (a, b, c) -> a
first3 ) forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
        budgetwidths :: [Int]
budgetwidths  = forall a b. (a -> b) -> [a] -> [b]
map (forall a. Integral a => [a] -> a
maximum' forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map forall {a} {b} {c}. (a, b, c) -> b
second3) forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
        percentwidths :: [Int]
percentwidths = forall a b. (a -> b) -> [a] -> [b]
map (forall a. Integral a => [a] -> a
maximum' forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map forall {a} {b} {c}. (a, b, c) -> c
third3 ) forall a b. (a -> b) -> a -> b
$ [[(Int, Int, Int)]]
cols
        catcolumnwidths :: [[[a]]] -> [[a]]
catcolumnwidths = forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith forall a. [a] -> [a] -> [a]
(++)) forall a b. (a -> b) -> a -> b
$ forall a. a -> [a]
repeat []
        cols :: [[(Int, Int, Int)]]
cols = forall a. [[a]] -> [[a]]
maybetranspose forall a b. (a -> b) -> a -> b
$ forall {a}. [[[a]]] -> [[a]]
catcolumnwidths forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map ([(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
cellswidth forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall {a} {a}. PeriodicReportRow a a -> [a]
rowToBudgetCells) [BudgetReportRow]
items forall a. [a] -> [a] -> [a]
++ [[(Maybe Change, Maybe Change)] -> [[(Int, Int, Int)]]
cellswidth forall a b. (a -> b) -> a -> b
$ forall {a} {a}. PeriodicReportRow a a -> [a]
rowToBudgetCells PeriodicReportRow () (Maybe Change, Maybe Change)
tr]

    -- split a BudgetCell into BudgetDisplayCell's (one per commodity when applicable)
    showcell :: BudgetShowMixed -> BudgetPercBudget -> BudgetCell -> BudgetDisplayRow
    showcell :: BudgetShowMixed
-> BudgetPercBudget
-> (Maybe Change, Maybe Change)
-> [BudgetDisplayCell]
showcell BudgetShowMixed
showmixed BudgetPercBudget
percbudget (Maybe Change
actual, Maybe Change
mbudget) = forall a b. [a] -> [b] -> [(a, b)]
zip (BudgetShowMixed
showmixed Change
actual') [Maybe (WideBuilder, Maybe WideBuilder)]
full
      where
        actual' :: Change
actual' = forall a. a -> Maybe a -> a
fromMaybe Change
nullmixedamt Maybe Change
actual

        budgetAndPerc :: Change -> [(WideBuilder, Maybe WideBuilder)]
budgetAndPerc Change
b = 
          forall a b. [a] -> [b] -> [(a, b)]
zip (BudgetShowMixed
showmixed Change
b) (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Text -> WideBuilder
wbFromText forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
T.pack forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Show a => a -> String
show forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo Word8
0) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> BudgetPercBudget
percbudget Change
actual' Change
b)

        full :: [Maybe (WideBuilder, Maybe WideBuilder)]
full
          | Just Change
b <- Maybe Change
mbudget = forall a. a -> Maybe a
Just forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Change -> [(WideBuilder, Maybe WideBuilder)]
budgetAndPerc Change
b
          | Bool
otherwise         = forall a. a -> [a]
repeat forall a. Maybe a
Nothing

    paddisplaycell :: (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
    paddisplaycell :: (Int, Int, Int) -> BudgetDisplayCell -> WideBuilder
paddisplaycell (Int
actualwidth, Int
budgetwidth, Int
percentwidth) (WideBuilder
actual, Maybe (WideBuilder, Maybe WideBuilder)
mbudget) = WideBuilder
full
      where
        toPadded :: WideBuilder -> Builder
toPadded (WideBuilder Builder
b Int
w) =
            (Text -> Builder
TB.fromText forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b c. (a -> b -> c) -> b -> a -> c
flip Int -> Text -> Text
T.replicate Text
" " forall a b. (a -> b) -> a -> b
$ Int
actualwidth forall a. Num a => a -> a -> a
- Int
w) forall a. Semigroup a => a -> a -> a
<> Builder
b

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

        -- | Display a padded budget string
        budgetb :: (WideBuilder, Maybe WideBuilder) -> Builder
budgetb (WideBuilder
budget, 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 forall a. Num a => a -> a -> a
- WideBuilder -> Int
wbWidth WideBuilder
pct) Text
" " forall a. Semigroup a => a -> a -> a
<> WideBuilder -> Text
wbToText WideBuilder
pct forall a. Semigroup a => a -> a -> a
<> Text
"% of "
           in Text -> Builder
TB.fromText forall a b. (a -> b) -> a -> b
$ Text
" [" forall a. Semigroup a => a -> a -> a
<> Text
perct forall a. Semigroup a => a -> a -> a
<> Int -> Text -> Text
T.replicate (Int
budgetwidth forall a. Num a => a -> a -> a
- WideBuilder -> Int
wbWidth WideBuilder
budget) Text
" " forall a. Semigroup a => a -> a -> a
<> WideBuilder -> Text
wbToText WideBuilder
budget forall a. Semigroup a => a -> a -> a
<> Text
"]"

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

        full :: WideBuilder
full = forall a b c. (a -> b -> c) -> b -> a -> c
flip Builder -> Int -> WideBuilder
WideBuilder (Int
actualwidth forall a. Num a => a -> a -> a
+ Int
totalbudgetwidth) forall a b. (a -> b) -> a -> b
$
            WideBuilder -> Builder
toPadded WideBuilder
actual forall a. Semigroup a => a -> a -> a
<> forall b a. b -> (a -> b) -> Maybe a -> b
maybe Builder
emptyBudget (WideBuilder, Maybe WideBuilder) -> Builder
budgetb Maybe (WideBuilder, Maybe WideBuilder)
mbudget

    -- | 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 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)
            -> forall a. a -> Maybe a
Just forall a b. (a -> b) -> a -> b
$ Percentage
100 forall a. Num a => a -> a -> a
* Amount -> Percentage
aquantity Amount
a forall a. Fractional a => a -> a -> a
/ Amount -> Percentage
aquantity Amount
b
        ([Amount], [Amount])
_   -> -- trace (pshow $ (maybecost actual, maybecost budget))  -- debug missing percentage
               forall a. Maybe a
Nothing
      where
        costedAmounts :: Change -> [Amount]
costedAmounts = case Maybe ConversionOp
conversionop_ of
            Just ConversionOp
ToCost -> Change -> [Amount]
amounts forall b c a. (b -> c) -> (a -> b) -> a -> c
. Change -> Change
mixedAmountCost
            Maybe ConversionOp
_           -> Change -> [Amount]
amounts

    -- | Calculate the percentage of actual change to budget goal for a particular commodity
    percentage' :: Change -> BudgetGoal -> CommoditySymbol -> Maybe Percentage
    percentage' :: Change -> Change -> Text -> Maybe Percentage
percentage' Change
am Change
bm Text
c = case ((,) forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
`on` forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (forall a. Eq a => a -> a -> Bool
(==) Text
c forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Text
acommodity) 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)
_                -> 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
layout_ :: Layout
transpose_ :: Bool
color_ :: Bool
normalbalance_ :: Maybe NormalSign
invert_ :: Bool
percent_ :: Bool
sort_amount_ :: Bool
show_costs_ :: Bool
no_total_ :: Bool
row_total_ :: Bool
declared_ :: Bool
drop_ :: Int
accountlistmode_ :: AccountListMode
budgetpat_ :: Maybe Text
balanceaccum_ :: BalanceAccumulation
balancecalc_ :: BalanceCalculation
txn_dates_ :: Bool
related_ :: Bool
average_ :: Bool
querystring_ :: [Text]
pretty_ :: Bool
format_ :: StringFormat
real_ :: Bool
no_elide_ :: Bool
empty_ :: Bool
date2_ :: Bool
depth_ :: Maybe Int
infer_prices_ :: Bool
value_ :: Maybe ValuationType
conversionop_ :: Maybe ConversionOp
statuses_ :: [Status]
interval_ :: Interval
period_ :: Period
layout_ :: ReportOpts -> Layout
transpose_ :: ReportOpts -> Bool
color_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
invert_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
declared_ :: ReportOpts -> Bool
drop_ :: ReportOpts -> Int
balanceaccum_ :: ReportOpts -> BalanceAccumulation
balancecalc_ :: ReportOpts -> BalanceCalculation
txn_dates_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
average_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
pretty_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
real_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
date2_ :: ReportOpts -> Bool
depth_ :: ReportOpts -> Maybe Int
value_ :: ReportOpts -> Maybe ValuationType
conversionop_ :: ReportOpts -> Maybe ConversionOp
statuses_ :: ReportOpts -> [Status]
period_ :: ReportOpts -> Period
budgetpat_ :: ReportOpts -> Maybe Text
interval_ :: ReportOpts -> Interval
infer_prices_ :: ReportOpts -> Bool
empty_ :: ReportOpts -> Bool
accountlistmode_ :: ReportOpts -> AccountListMode
..}
  (PeriodicReport [DateSpan]
colspans [BudgetReportRow]
items PeriodicReportRow () (Maybe Change, Maybe Change)
tr)
  = (if Bool
transpose_ then forall a. [[a]] -> [[a]]
transpose else forall a. a -> a
id) forall a b. (a -> b) -> a -> b
$

  -- heading row
  

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

  -- account rows
  

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

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

  where
    flattentuples :: [(a, a)] -> [a]
flattentuples [(a, a)]
tups = 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 = forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
"" (WideBuilder -> Text
wbToText forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountDisplayOpts -> Change -> WideBuilder
showMixedAmountB AmountDisplayOpts
oneLine)

    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_ forall a. Eq a => a -> a -> Bool
/= Layout
LayoutBare = [PeriodicReportRow a (Maybe Change, Maybe Change) -> Text
render PeriodicReportRow a (Maybe Change, Maybe Change)
row forall a. a -> [a] -> [a]
: forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Maybe Change -> Text
showNorm [Maybe Change]
vals]
      | Bool
otherwise =
            [[Text]] -> [[Text]]
joinNames forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (:) [Text]
cs  -- add symbols and names
          forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. [[a]] -> [[a]]
transpose                   -- each row becomes a list of Text quantities
          forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap WideBuilder -> Text
wbToText forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountDisplayOpts -> BudgetShowMixed
showMixedAmountLinesB AmountDisplayOpts
oneLine{displayOrder :: Maybe [Text]
displayOrder=forall a. a -> Maybe a
Just [Text]
cs, displayMinWidth :: Maybe Int
displayMinWidth=forall a. Maybe a
Nothing}
                 forall b c a. (b -> c) -> (a -> b) -> a -> c
.forall a. a -> Maybe a -> a
fromMaybe Change
nullmixedamt)
          forall a b. (a -> b) -> a -> b
$ [Maybe Change]
vals
      where
        cs :: [Text]
cs = forall a. Set a -> [a]
S.toList forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' forall a. Ord a => Set a -> Set a -> Set a
S.union forall a. Monoid a => a
mempty forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Change -> Set Text
maCommodities forall a b. (a -> b) -> a -> b
$ forall a. [Maybe a] -> [a]
catMaybes [Maybe Change]
vals
        vals :: [Maybe Change]
vals = forall {a}. [(a, a)] -> [a]
flattentuples [(Maybe Change, Maybe Change)]
as
            forall a. [a] -> [a] -> [a]
++ forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe Change
rowtot, Maybe Change
budgettot] | Bool
row_total_]
            forall a. [a] -> [a] -> [a]
++ forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe Change
rowavg, Maybe Change
budgetavg] | Bool
average_]

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

-- tests

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