mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-26 20:02:27 +03:00
bal: option to view one commodity per row
This adds the `--commodity-column` option that displays each commodity on a separate line and the commodities themselves as a separate column. The initial design considerations are at simonmichael.hledger.issues.1559 The single-period balance report with `--commodity-column` does not interoperate with custom formats.
This commit is contained in:
parent
ed7ee7a445
commit
f3c07144a8
@ -152,7 +152,7 @@ import Data.Foldable (toList)
|
||||
import Data.List (find, foldl', intercalate, intersperse, mapAccumL, partition)
|
||||
import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Maybe (fromMaybe, isNothing, isJust)
|
||||
import Data.Semigroup (Semigroup(..))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy.Builder as TB
|
||||
@ -175,6 +175,9 @@ data AmountDisplayOpts = AmountDisplayOpts
|
||||
, displayOneLine :: Bool -- ^ Whether to display on one line.
|
||||
, displayMinWidth :: Maybe Int -- ^ Minimum width to pad to
|
||||
, displayMaxWidth :: Maybe Int -- ^ Maximum width to clip to
|
||||
-- | Display amounts in this order (without the commodity symbol) and display
|
||||
-- a 0 in case a corresponding commodity does not exist
|
||||
, displayOrder :: Maybe [CommoditySymbol]
|
||||
} deriving (Show)
|
||||
|
||||
-- | Display Amount and MixedAmount with no colour.
|
||||
@ -186,8 +189,9 @@ noColour = AmountDisplayOpts { displayPrice = True
|
||||
, displayColour = False
|
||||
, displayZeroCommodity = False
|
||||
, displayOneLine = False
|
||||
, displayMinWidth = Nothing
|
||||
, displayMinWidth = Just 0
|
||||
, displayMaxWidth = Nothing
|
||||
, displayOrder = Nothing
|
||||
}
|
||||
|
||||
-- | Display Amount and MixedAmount with no prices.
|
||||
@ -429,14 +433,15 @@ showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder
|
||||
showAmountB _ Amount{acommodity="AUTO"} = mempty
|
||||
showAmountB opts a@Amount{astyle=style} =
|
||||
color $ case ascommodityside style of
|
||||
L -> c' <> space <> quantity' <> price
|
||||
R -> quantity' <> space <> c' <> price
|
||||
L -> showC c' space <> quantity' <> price
|
||||
R -> quantity' <> showC space c' <> price
|
||||
where
|
||||
quantity = showamountquantity a
|
||||
(quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"")
|
||||
| otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a)
|
||||
space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty
|
||||
c' = WideBuilder (TB.fromText c) (textWidth c)
|
||||
showC l r = if isJust (displayOrder opts) then mempty else l <> r
|
||||
price = if displayPrice opts then showAmountPrice a else mempty
|
||||
color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id
|
||||
|
||||
@ -820,13 +825,16 @@ showMixedAmountLinesB :: AmountDisplayOpts -> MixedAmount -> [WideBuilder]
|
||||
showMixedAmountLinesB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWidth=mmin} ma =
|
||||
map (adBuilder . pad) elided
|
||||
where
|
||||
astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . amounts $
|
||||
astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . orderedAmounts opts $
|
||||
if displayPrice opts then ma else mixedAmountStripPrices ma
|
||||
sep = WideBuilder (TB.singleton '\n') 0
|
||||
width = maximum $ fromMaybe 0 mmin : map (wbWidth . adBuilder) elided
|
||||
width = maximum $ map (wbWidth . adBuilder) elided
|
||||
|
||||
pad amt = amt{ adBuilder = WideBuilder (TB.fromText $ T.replicate w " ") w <> adBuilder amt }
|
||||
where w = width - wbWidth (adBuilder amt)
|
||||
pad amt
|
||||
| Just mw <- mmin =
|
||||
let w = (max width mw) - wbWidth (adBuilder amt)
|
||||
in amt{ adBuilder = WideBuilder (TB.fromText $ T.replicate w " ") w <> adBuilder amt }
|
||||
| otherwise = amt
|
||||
|
||||
elided = maybe id elideTo mmax astrs
|
||||
elideTo m xs = maybeAppend elisionStr short
|
||||
@ -843,7 +851,7 @@ showMixedAmountOneLineB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWi
|
||||
. max width $ fromMaybe 0 mmin
|
||||
where
|
||||
width = maybe 0 adTotal $ lastMay elided
|
||||
astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . amounts $
|
||||
astrs = amtDisplayList (wbWidth sep) (showAmountB opts) . orderedAmounts opts $
|
||||
if displayPrice opts then ma else mixedAmountStripPrices ma
|
||||
sep = WideBuilder (TB.fromString ", ") 2
|
||||
n = length astrs
|
||||
@ -866,6 +874,15 @@ showMixedAmountOneLineB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWi
|
||||
-- Add the elision strings (if any) to each amount
|
||||
withElided = zipWith (\num amt -> (amt, elisionDisplay Nothing (wbWidth sep) num amt)) [n-1,n-2..0]
|
||||
|
||||
orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount]
|
||||
orderedAmounts AmountDisplayOpts{displayOrder=ord} ma
|
||||
| Just cs <- ord = fmap pad cs
|
||||
| otherwise = as
|
||||
where
|
||||
as = amounts ma
|
||||
pad c = fromMaybe (amountWithCommodity c nullamt) . find ((==) c . acommodity) $ as
|
||||
|
||||
|
||||
data AmountDisplay = AmountDisplay
|
||||
{ adBuilder :: !WideBuilder -- ^ String representation of the Amount
|
||||
, adTotal :: !Int -- ^ Cumulative length of MixedAmount this Amount is part of,
|
||||
|
@ -136,7 +136,7 @@ formatfieldp = do
|
||||
char '('
|
||||
f <- fieldp
|
||||
char ')'
|
||||
return $ FormatField (isJust leftJustified) (parseDec minWidth) (parseDec maxWidth) f
|
||||
return $ FormatField (isJust leftJustified) (parseDec minWidth <|> Just 0) (parseDec maxWidth) f
|
||||
where
|
||||
parseDec s = case s of
|
||||
Just text -> Just m where ((m,_):_) = readDec text
|
||||
@ -175,20 +175,20 @@ tests_StringFormat = tests "StringFormat" [
|
||||
in tests "parseStringFormat" [
|
||||
"" `gives` (defaultStringFormatStyle [])
|
||||
, "D" `gives` (defaultStringFormatStyle [FormatLiteral "D"])
|
||||
, "%(date)" `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])
|
||||
, "%(total)" `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField])
|
||||
, "%(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 0) Nothing DescriptionField])
|
||||
, "%(total)" `gives` (defaultStringFormatStyle [FormatField False (Just 0) Nothing TotalField])
|
||||
-- TODO
|
||||
-- , "^%(total)" `gives` (TopAligned [FormatField False Nothing Nothing TotalField])
|
||||
-- , "_%(total)" `gives` (BottomAligned [FormatField False Nothing Nothing TotalField])
|
||||
-- , ",%(total)" `gives` (OneLine [FormatField False Nothing Nothing TotalField])
|
||||
, "Hello %(date)!" `gives` (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])
|
||||
, "%-(date)" `gives` (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])
|
||||
, "Hello %(date)!" `gives` (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False (Just 0) Nothing DescriptionField, FormatLiteral "!"])
|
||||
, "%-(date)" `gives` (defaultStringFormatStyle [FormatField True (Just 0) Nothing DescriptionField])
|
||||
, "%20(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])
|
||||
, "%.10(date)" `gives` (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])
|
||||
, "%.10(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 0) (Just 10) DescriptionField])
|
||||
, "%20.10(date)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])
|
||||
, "%20(account) %.10(total)" `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField
|
||||
,FormatLiteral " "
|
||||
,FormatField False Nothing (Just 10) TotalField
|
||||
,FormatField False (Just 0) (Just 10) TotalField
|
||||
])
|
||||
, test "newline not parsed" $ assertLeft $ parseStringFormat "\n"
|
||||
]
|
||||
|
@ -156,6 +156,7 @@ data ReportOpts = ReportOpts {
|
||||
-- whether stdout is an interactive terminal, and the value of
|
||||
-- TERM and existence of NO_COLOR environment variables.
|
||||
,transpose_ :: Bool
|
||||
,commodity_column_:: Bool
|
||||
} deriving (Show)
|
||||
|
||||
instance Default ReportOpts where def = defreportopts
|
||||
@ -193,6 +194,7 @@ defreportopts = ReportOpts
|
||||
, normalbalance_ = Nothing
|
||||
, color_ = False
|
||||
, transpose_ = False
|
||||
, commodity_column_ = False
|
||||
}
|
||||
|
||||
-- | Generate a ReportOpts from raw command-line input, given a day.
|
||||
@ -243,6 +245,7 @@ rawOptsToReportOpts d rawopts =
|
||||
,pretty_tables_ = boolopt "pretty-tables" rawopts
|
||||
,color_ = useColorOnStdout -- a lower-level helper
|
||||
,transpose_ = boolopt "transpose" rawopts
|
||||
,commodity_column_= boolopt "commodity-column" rawopts
|
||||
}
|
||||
|
||||
-- | The result of successfully parsing a ReportOpts on a particular
|
||||
|
@ -41,6 +41,7 @@ module Hledger.Utils.Text
|
||||
-- * wide-character-aware layout
|
||||
WideBuilder(..),
|
||||
wbToText,
|
||||
wbFromText,
|
||||
wbUnpack,
|
||||
textWidth,
|
||||
textTakeWidth,
|
||||
@ -61,7 +62,7 @@ import qualified Data.Text.Lazy.Builder as TB
|
||||
import Hledger.Utils.Test ((@?=), test, tests)
|
||||
import Text.Tabular.AsciiWide
|
||||
(Align(..), Header(..), Properties(..), TableOpts(..), renderRow, textCell)
|
||||
import Text.WideString (WideBuilder(..), wbToText, wbUnpack, charWidth, textWidth)
|
||||
import Text.WideString (WideBuilder(..), wbToText, wbFromText, wbUnpack, charWidth, textWidth)
|
||||
|
||||
|
||||
-- lowercase, uppercase :: String -> String
|
||||
|
@ -10,13 +10,16 @@ module Text.Tabular.AsciiWide
|
||||
, render
|
||||
, renderTable
|
||||
, renderTableB
|
||||
, renderTableByRowsB
|
||||
, renderRow
|
||||
, renderRowB
|
||||
, renderColumns
|
||||
|
||||
, Cell(..)
|
||||
, Align(..)
|
||||
, emptyCell
|
||||
, textCell
|
||||
, textsCell
|
||||
, cellWidth
|
||||
) where
|
||||
|
||||
@ -30,7 +33,7 @@ import qualified Data.Text.Lazy as TL
|
||||
import Data.Text.Lazy.Builder (Builder, fromString, fromText, singleton, toLazyText)
|
||||
import Safe (maximumMay)
|
||||
import Text.Tabular
|
||||
import Text.WideString (WideBuilder(..), textWidth)
|
||||
import Text.WideString (WideBuilder(..), wbFromText, textWidth)
|
||||
|
||||
|
||||
-- | The options to use for rendering a table.
|
||||
@ -60,6 +63,10 @@ emptyCell = Cell TopRight []
|
||||
textCell :: Align -> Text -> Cell
|
||||
textCell a x = Cell a . map (\x -> WideBuilder (fromText x) (textWidth x)) $ if T.null x then [""] else T.lines x
|
||||
|
||||
-- | Create a multi-line cell from the given contents with its natural width.
|
||||
textsCell :: Align -> [Text] -> Cell
|
||||
textsCell a = Cell a . fmap wbFromText
|
||||
|
||||
-- | Return the width of a Cell.
|
||||
cellWidth :: Cell -> Int
|
||||
cellWidth (Cell _ xs) = fromMaybe 0 . maximumMay $ map wbWidth xs
|
||||
@ -86,20 +93,31 @@ renderTableB :: TableOpts -- ^ Options controlling Table rendering
|
||||
-> (a -> Cell) -- ^ Function determining the string and width of a cell
|
||||
-> Table rh ch a
|
||||
-> Builder
|
||||
renderTableB topts@TableOpts{prettyTable=pretty, tableBorders=borders} fr fc f (Table rh ch cells) =
|
||||
renderTableB topts fr fc f = renderTableByRowsB topts (fmap fc) (\(rh, as) -> (fr rh, fmap f as))
|
||||
|
||||
-- | A version of renderTable that operates on rows (including the 'row' of
|
||||
-- column headers) and returns the underlying Builder.
|
||||
renderTableByRowsB :: TableOpts -- ^ Options controlling Table rendering
|
||||
-> ([ch] -> [Cell]) -- ^ Rendering function for column headers
|
||||
-> ((rh, [a]) -> (Cell, [Cell])) -- ^ Rendering function for row and row header
|
||||
-> Table rh ch a
|
||||
-> Builder
|
||||
renderTableByRowsB topts@TableOpts{prettyTable=pretty, tableBorders=borders} fc f (Table rh ch cells) =
|
||||
unlinesB . addBorders $
|
||||
renderColumns topts sizes ch2
|
||||
: bar VM DoubleLine -- +======================================+
|
||||
: renderRs (fmap renderR $ zipHeader [] cellContents rowHeaders)
|
||||
where
|
||||
renderR :: ([Cell], Cell) -> Builder
|
||||
renderR (cs,h) = renderColumns topts sizes $ Group DoubleLine
|
||||
[ Header h
|
||||
, fmap fst $ zipHeader emptyCell cs colHeaders
|
||||
]
|
||||
|
||||
rowHeaders = fmap fr rh
|
||||
colHeaders = fmap fc ch
|
||||
cellContents = map (map f) cells
|
||||
rows = unzip . fmap f $ zip (headerContents rh) cells
|
||||
rowHeaders = fmap fst $ zipHeader emptyCell (fst rows) rh
|
||||
colHeaders = fmap fst $ zipHeader emptyCell (fc $ headerContents ch) ch
|
||||
cellContents = snd rows
|
||||
|
||||
-- ch2 and cell2 include the row and column labels
|
||||
ch2 = Group DoubleLine [Header emptyCell, colHeaders]
|
||||
@ -162,6 +180,7 @@ renderColumns TableOpts{prettyTable=pretty, tableBorders=borders, borderSpaces=s
|
||||
padCell (w, Cell TopRight ls) = map (\x -> fromText (T.replicate (w - wbWidth x) " ") <> wbBuilder x) ls
|
||||
padCell (w, Cell BottomRight ls) = map (\x -> fromText (T.replicate (w - wbWidth x) " ") <> wbBuilder x) ls
|
||||
|
||||
|
||||
-- Pad each cell to have the same number of lines
|
||||
padRow (Cell TopLeft ls) = Cell TopLeft $ ls ++ replicate (nLines - length ls) mempty
|
||||
padRow (Cell TopRight ls) = Cell TopRight $ ls ++ replicate (nLines - length ls) mempty
|
||||
|
@ -8,7 +8,8 @@ module Text.WideString (
|
||||
-- * Text Builders which keep track of length
|
||||
WideBuilder(..),
|
||||
wbUnpack,
|
||||
wbToText
|
||||
wbToText,
|
||||
wbFromText
|
||||
) where
|
||||
|
||||
import Data.Text (Text)
|
||||
@ -33,6 +34,10 @@ instance Monoid WideBuilder where
|
||||
wbToText :: WideBuilder -> Text
|
||||
wbToText = TL.toStrict . TB.toLazyText . wbBuilder
|
||||
|
||||
-- | Convert a WideBuilder to a strict Text.
|
||||
wbFromText :: Text -> WideBuilder
|
||||
wbFromText t = WideBuilder (TB.fromText t) (textWidth t)
|
||||
|
||||
-- | Convert a WideBuilder to a String.
|
||||
wbUnpack :: WideBuilder -> String
|
||||
wbUnpack = TL.unpack . TB.toLazyText . wbBuilder
|
||||
|
@ -244,6 +244,7 @@ module Hledger.Cli.Commands.Balance (
|
||||
balancemode
|
||||
,balance
|
||||
,balanceReportAsText
|
||||
,balanceReportAsCsv
|
||||
,balanceReportItemAsText
|
||||
,multiBalanceReportAsText
|
||||
,multiBalanceReportAsCsv
|
||||
@ -255,14 +256,17 @@ module Hledger.Cli.Commands.Balance (
|
||||
) where
|
||||
|
||||
import Data.Default (def)
|
||||
import Data.List (intersperse, transpose)
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
import Data.List (intersperse, transpose, foldl', transpose)
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy as TL
|
||||
import qualified Data.Text.Lazy.Builder as TB
|
||||
import Data.Time (fromGregorian)
|
||||
import System.Console.CmdArgs.Explicit as C
|
||||
import Lucid as L
|
||||
import Safe (headMay, maximumMay)
|
||||
import Text.Tabular.AsciiWide as Tab
|
||||
|
||||
import Hledger
|
||||
@ -306,6 +310,8 @@ balancemode = hledgerCommandMode
|
||||
,flagNone ["percent", "%"] (setboolopt "percent") "express values in percentage of each column's total"
|
||||
,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign"
|
||||
,flagNone ["transpose"] (setboolopt "transpose") "transpose rows and columns"
|
||||
,flagNone ["commodity-column"] (setboolopt "commodity-column")
|
||||
"shows each commodity in its own automatically-generated subaccount, for tidier reports"
|
||||
,outputFormatFlag ["txt","html","csv","json"]
|
||||
,outputFileFlag
|
||||
]
|
||||
@ -385,31 +391,65 @@ balance opts@CliOpts{reportspec_=rspec} j = case balancecalc_ of
|
||||
-- | Render a single-column balance report as CSV.
|
||||
balanceReportAsCsv :: ReportOpts -> BalanceReport -> CSV
|
||||
balanceReportAsCsv opts (items, total) =
|
||||
["account","balance"] :
|
||||
[[accountNameDrop (drop_ opts) a, wbToText $ showMixedAmountB (balanceOpts False opts) b] | (a, _, _, b) <- items]
|
||||
++
|
||||
if no_total_ opts
|
||||
then []
|
||||
else [["total", wbToText $ showMixedAmountB (balanceOpts False opts) total]]
|
||||
("account" : ((if commodity_column_ opts then (:) "commodity" else id) $ "balance" : []))
|
||||
: (concatMap (\(a, _, _, b) -> rows a b) items)
|
||||
++ if no_total_ opts then [] else rows "total" total
|
||||
where
|
||||
rows :: AccountName -> MixedAmount -> [[T.Text]]
|
||||
rows name ma
|
||||
| commodity_column_ opts =
|
||||
fmap (\(k, a) -> [showName name, k, renderAmount . mixedAmount . amountStripPrices $ a])
|
||||
. M.toList . foldl' sumAmounts mempty . amounts $ ma
|
||||
| otherwise = [[showName name, renderAmount ma]]
|
||||
|
||||
showName = accountNameDrop (drop_ opts)
|
||||
renderAmount amt = wbToText $ showMixedAmountB bopts amt
|
||||
where bopts = (balanceOpts False opts){displayOrder = order}
|
||||
order = if commodity_column_ opts then Just (commodities [amt]) else Nothing
|
||||
sumAmounts mp am = M.insertWith (+) (acommodity am) am mp
|
||||
|
||||
-- | Render a single-column balance report as plain text.
|
||||
balanceReportAsText :: ReportOpts -> BalanceReport -> TB.Builder
|
||||
balanceReportAsText opts ((items, total)) =
|
||||
unlinesB lines
|
||||
<> unlinesB (if no_total_ opts then [] else [overline, totalLines])
|
||||
balanceReportAsText opts ((items, total))
|
||||
| not (commodity_column_ opts) =
|
||||
unlinesB lines
|
||||
<> unlinesB (if no_total_ opts then [] else [overline, totalLines])
|
||||
| iscustom = error' "Custom format not supported with --commodity-column" -- PARTIAL:
|
||||
| otherwise = balanceReportAsText' opts ((items, total))
|
||||
where
|
||||
(lines, sizes) = unzip $ map (balanceReportItemAsText opts) items
|
||||
-- abuse renderBalanceReportItem to render the total with similar format
|
||||
(totalLines, _) = renderBalanceReportItem opts ("",0,total)
|
||||
-- with a custom format, extend the line to the full report width;
|
||||
-- otherwise show the usual 20-char line for compatibility
|
||||
overlinewidth = case format_ opts of
|
||||
OneLine ((FormatField _ _ _ TotalField):_) -> 20
|
||||
TopAligned ((FormatField _ _ _ TotalField):_) -> 20
|
||||
BottomAligned ((FormatField _ _ _ TotalField):_) -> 20
|
||||
_ -> sum (map maximum' $ transpose sizes)
|
||||
iscustom = case format_ opts of
|
||||
OneLine ((FormatField _ _ _ TotalField):_) -> False
|
||||
TopAligned ((FormatField _ _ _ TotalField):_) -> False
|
||||
BottomAligned ((FormatField _ _ _ TotalField):_) -> False
|
||||
_ -> True
|
||||
overlinewidth = if iscustom then sum (map maximum' $ transpose sizes) else 20
|
||||
overline = TB.fromText $ T.replicate overlinewidth "-"
|
||||
|
||||
-- | Render a single-column balance report as plain text in commodity-column mode
|
||||
balanceReportAsText' :: ReportOpts -> BalanceReport -> TB.Builder
|
||||
balanceReportAsText' opts ((items, total)) =
|
||||
unlinesB . fmap (renderColumns def{tableBorders=False} sizes . Tab.Group NoLine . fmap Header) $
|
||||
lines ++ concat [[[overline], totalline] | not (no_total_ opts)]
|
||||
where
|
||||
render (_, acctname, depth, amt) =
|
||||
[ Cell TopRight damts
|
||||
, Cell TopLeft (fmap wbFromText cs)
|
||||
, Cell TopLeft (replicate (length damts - 1) mempty ++ [wbFromText dispname]) ]
|
||||
where dopts = (balanceOpts True opts){displayOrder=Just cs}
|
||||
cs = commodities [amt]
|
||||
dispname = T.replicate ((depth - 1) * 2) " " <> acctname
|
||||
damts = showMixedAmountLinesB dopts amt
|
||||
lines = fmap render items
|
||||
totalline = render ("", "", 0, total)
|
||||
sizes = fmap (fromMaybe 0 . maximumMay . map cellWidth) $
|
||||
transpose ([totalline | not (no_total_ opts)] ++ lines)
|
||||
overline = Cell TopLeft . pure . wbFromText . flip T.replicate "-" . fromMaybe 0 $ headMay sizes
|
||||
|
||||
{-
|
||||
:r
|
||||
This implementation turned out to be a bit convoluted but implements the following algorithm for formatting:
|
||||
@ -468,56 +508,64 @@ renderComponent topaligned opts (acctname, depth, total) (FormatField ljust mmin
|
||||
-- The CSV will always include the initial headings row,
|
||||
-- and will include the final totals row unless --no-total is set.
|
||||
multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV
|
||||
multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_}
|
||||
(PeriodicReport colspans items (PeriodicReportRow _ coltotals tot avg)) =
|
||||
maybetranspose $
|
||||
("account" : map showDateSpan colspans
|
||||
multiBalanceReportAsCsv opts@ReportOpts{..} =
|
||||
(if transpose_ then transpose else id) . uncurry (++) . multiBalanceReportAsCsv' opts
|
||||
|
||||
multiBalanceReportAsCsv' :: ReportOpts -> MultiBalanceReport -> (CSV, CSV)
|
||||
multiBalanceReportAsCsv' opts@ReportOpts{..}
|
||||
(PeriodicReport colspans items tr) =
|
||||
flip (,) totalrows $
|
||||
("account" : ["commodity" | commodity_column_] ++ map showDateSpan colspans
|
||||
++ ["total" | row_total_]
|
||||
++ ["average" | average_]
|
||||
) :
|
||||
[accountNameDrop (drop_ opts) (displayFull a) :
|
||||
map (wbToText . showMixedAmountB (balanceOpts False opts))
|
||||
(amts
|
||||
++ [rowtot | row_total_]
|
||||
++ [rowavg | average_])
|
||||
| PeriodicReportRow a amts rowtot rowavg <- items]
|
||||
++
|
||||
if no_total_ opts
|
||||
then []
|
||||
else ["total" :
|
||||
map (wbToText . showMixedAmountB (balanceOpts False opts)) (
|
||||
coltotals
|
||||
++ [tot | row_total_]
|
||||
++ [avg | average_]
|
||||
)]
|
||||
concatMap (rowAsTexts (accountNameDrop drop_ . prrFullName)) items
|
||||
where
|
||||
maybetranspose | transpose_ opts = transpose
|
||||
| otherwise = id
|
||||
rowAsTexts render row@(PeriodicReportRow _ as rowtot rowavg)
|
||||
| not commodity_column_ = [render row : fmap (wbToText . showMixedAmountB bopts) all]
|
||||
| otherwise =
|
||||
joinNames . zipWith (:) cs -- add symbols and names
|
||||
. transpose -- each row becomes a list of Text quantities
|
||||
. fmap (fmap wbToText . showMixedAmountLinesB bopts{displayOrder=Just cs, displayMinWidth=Nothing})
|
||||
$ all
|
||||
where
|
||||
bopts = balanceOpts False opts
|
||||
cs = commodities $ rowtot : rowavg : as
|
||||
all = as
|
||||
++ [rowtot | row_total_]
|
||||
++ [rowavg | average_]
|
||||
|
||||
joinNames = fmap ((:) (render row))
|
||||
|
||||
totalrows :: [[T.Text]]
|
||||
totalrows
|
||||
| no_total_ = mempty
|
||||
| otherwise = rowAsTexts (const "total") tr
|
||||
|
||||
-- | Render a multi-column balance report as HTML.
|
||||
multiBalanceReportAsHtml :: ReportOpts -> MultiBalanceReport -> Html ()
|
||||
multiBalanceReportAsHtml ropts mbr =
|
||||
let
|
||||
(headingsrow,bodyrows,mtotalsrow) = multiBalanceReportHtmlRows ropts mbr
|
||||
(headingsrow,bodyrows,mtotalsrows) = multiBalanceReportHtmlRows ropts mbr
|
||||
in
|
||||
table_ $ mconcat $
|
||||
[headingsrow]
|
||||
++ bodyrows
|
||||
++ maybeToList mtotalsrow
|
||||
++ mtotalsrows
|
||||
|
||||
-- | Render the HTML table rows for a MultiBalanceReport.
|
||||
-- Returns the heading row, 0 or more body rows, and the totals row if enabled.
|
||||
multiBalanceReportHtmlRows :: ReportOpts -> MultiBalanceReport -> (Html (), [Html ()], Maybe (Html ()))
|
||||
multiBalanceReportHtmlRows :: ReportOpts -> MultiBalanceReport -> (Html (), [Html ()], [Html ()])
|
||||
multiBalanceReportHtmlRows ropts mbr =
|
||||
let
|
||||
headingsrow:rest | transpose_ ropts = error' "Sorry, --transpose with HTML output is not yet supported" -- PARTIAL:
|
||||
| otherwise = multiBalanceReportAsCsv ropts mbr
|
||||
(bodyrows, mtotalsrow) | no_total_ ropts = (rest, Nothing)
|
||||
| otherwise = (init rest, Just $ last rest)
|
||||
-- TODO: should the commodity_column be displayed as a subaccount in this case as well?
|
||||
(headingsrow:bodyrows, mtotalsrows)
|
||||
| transpose_ ropts = error' "Sorry, --transpose with HTML output is not yet supported" -- PARTIAL:
|
||||
| otherwise = multiBalanceReportAsCsv' ropts mbr
|
||||
in
|
||||
(multiBalanceReportHtmlHeadRow ropts headingsrow
|
||||
,map (multiBalanceReportHtmlBodyRow ropts) bodyrows
|
||||
,multiBalanceReportHtmlFootRow ropts <$> mtotalsrow -- TODO pad totals row with zeros when there are
|
||||
,multiBalanceReportHtmlFootRow ropts <$> mtotalsrows -- TODO pad totals row with zeros when there are
|
||||
)
|
||||
|
||||
-- | Render one MultiBalanceReport heading row as a HTML table row.
|
||||
@ -627,7 +675,8 @@ balanceReportAsTable opts@ReportOpts{average_, row_total_, balanceaccum_}
|
||||
(map rowvals items)
|
||||
where
|
||||
totalscolumn = row_total_ && balanceaccum_ `notElem` [Cumulative, Historical]
|
||||
colheadings = map (reportPeriodName balanceaccum_ spans) spans
|
||||
colheadings = ["Commodity" | commodity_column_ opts]
|
||||
++ map (reportPeriodName balanceaccum_ spans) spans
|
||||
++ [" Total" | totalscolumn]
|
||||
++ ["Average" | average_]
|
||||
accts = map renderacct items
|
||||
@ -651,9 +700,28 @@ balanceReportAsTable opts@ReportOpts{average_, row_total_, balanceaccum_}
|
||||
-- unless --no-elide is used.
|
||||
balanceReportTableAsText :: ReportOpts -> Table T.Text T.Text MixedAmount -> TB.Builder
|
||||
balanceReportTableAsText ropts@ReportOpts{..} =
|
||||
Tab.renderTableB def{tableBorders=False, prettyTable=pretty_tables_}
|
||||
(Tab.textCell TopLeft) (Tab.textCell TopRight) $
|
||||
Cell TopRight . pure . showMixedAmountB (balanceOpts True ropts)
|
||||
Tab.renderTableByRowsB def{tableBorders=False, prettyTable=pretty_tables_} renderCh renderRow
|
||||
where
|
||||
renderCh
|
||||
| not commodity_column_ = fmap (Tab.textCell TopRight)
|
||||
| otherwise = zipWith ($) (Tab.textCell TopLeft : repeat (Tab.textCell TopRight))
|
||||
|
||||
renderRow :: (T.Text, [MixedAmount]) -> (Cell, [Cell])
|
||||
renderRow (rh, row)
|
||||
| not commodity_column_ =
|
||||
(Tab.textCell TopLeft rh, fmap (Cell TopRight . pure . showMixedAmountB bopts) row)
|
||||
| otherwise =
|
||||
( Tab.textsCell TopLeft (replicate (length cs) rh)
|
||||
, Tab.textsCell TopLeft cs
|
||||
: fmap (Cell TopRight . showMixedAmountLinesB bopts{displayOrder = Just cs}) row)
|
||||
where
|
||||
bopts = balanceOpts True ropts
|
||||
cs = commodities row
|
||||
|
||||
commodities :: [MixedAmount] -> [CommoditySymbol]
|
||||
commodities = filter (not . T.null) . S.toList
|
||||
. foldl' S.union mempty
|
||||
. fmap (S.fromList . fmap acommodity . amounts)
|
||||
|
||||
-- | Amount display options to use for balance reports
|
||||
balanceOpts :: Bool -> ReportOpts -> AmountDisplayOpts
|
||||
|
@ -297,11 +297,11 @@ compoundBalanceReportAsHtml ropts cbr =
|
||||
subreportrows :: (T.Text, MultiBalanceReport, Bool) -> [Html ()]
|
||||
subreportrows (subreporttitle, mbr, _increasestotal) =
|
||||
let
|
||||
(_,bodyrows,mtotalsrow) = multiBalanceReportHtmlRows ropts mbr
|
||||
(_,bodyrows,mtotalsrows) = multiBalanceReportHtmlRows ropts mbr
|
||||
in
|
||||
[tr_ $ th_ [colspanattr, leftattr] $ toHtml subreporttitle]
|
||||
++ bodyrows
|
||||
++ maybe [] (:[]) mtotalsrow
|
||||
++ mtotalsrows
|
||||
++ [blankrow]
|
||||
|
||||
totalrows | no_total_ ropts || length subreports == 1 = []
|
||||
|
Loading…
Reference in New Issue
Block a user