Skip to content

Commit bd82537

Browse files
committed
cli: Commands.Balance.multiBalanceReportAsSpreadsheetHelper: vertically merge cells showing account names and Total
lib: Write.Spreadsheet: add support for cell spans
1 parent 37705f3 commit bd82537

File tree

5 files changed

+123
-34
lines changed

5 files changed

+123
-34
lines changed

hledger-lib/Hledger/Write/Html.hs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,30 @@ formatCell cell =
5454
let class_ =
5555
map Lucid.class_ $
5656
filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in
57+
let span_ makeCell attrs cont =
58+
case Spr.cellSpan cell of
59+
Spr.NoSpan -> makeCell attrs cont
60+
Spr.Covered -> pure ()
61+
Spr.SpanHorizontal n ->
62+
makeCell (Lucid.colspan_ (Text.pack $ show n) : attrs) cont
63+
Spr.SpanVertical n ->
64+
makeCell (Lucid.rowspan_ (Text.pack $ show n) : attrs) cont
65+
in
5766
case cellStyle cell of
58-
Head -> Lucid.th_ (style++class_) content
67+
Head -> span_ Lucid.th_ (style++class_) content
5968
Body emph ->
6069
let align =
6170
case cellType cell of
6271
TypeString -> []
6372
TypeDate -> []
6473
_ -> [LucidBase.makeAttribute "align" "right"]
74+
valign = [LucidBase.makeAttribute "valign" "top"]
6575
withEmph =
6676
case emph of
6777
Item -> id
6878
Total -> Lucid.b_
69-
in Lucid.td_ (style++align++class_) $ withEmph content
79+
in span_ Lucid.td_ (style++align++valign++class_) $
80+
withEmph content
7081

7182

7283
class (Spr.Lines border) => Lines border where

hledger-lib/Hledger/Write/Ods.hs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,24 +239,32 @@ data DataStyle =
239239

240240
cellConfig :: ((Spr.Border Spr.NumLines, Style), DataStyle) -> [String]
241241
cellConfig ((border, cstyle), dataStyle) =
242-
let moreStyles =
242+
let boldStyle = " <style:text-properties fo:font-weight='bold'/>"
243+
alignTop =
244+
" <style:table-cell-properties style:vertical-align='top'/>"
245+
alignParagraph =
246+
printf " <style:paragraph-properties fo:text-align='%s'/>"
247+
moreStyles =
243248
borderStyle border
244249
++
245250
(
246251
case cstyle of
247-
Body Item -> []
252+
Body Item ->
253+
alignTop :
254+
[]
248255
Body Total ->
249-
[" <style:text-properties fo:font-weight='bold'/>"]
256+
alignTop :
257+
boldStyle :
258+
[]
250259
Head ->
251-
" <style:paragraph-properties fo:text-align='center'/>" :
252-
" <style:text-properties fo:font-weight='bold'/>" :
260+
alignParagraph "center" :
261+
boldStyle :
253262
[]
254263
)
255264
++
256265
(
257266
case dataStyle of
258-
DataMixedAmount ->
259-
[" <style:paragraph-properties fo:text-align='end'/>"]
267+
DataMixedAmount -> [alignParagraph "end"]
260268
_ -> []
261269
)
262270
cstyleName = cellStyleName cstyle
@@ -314,17 +322,30 @@ formatCell cell =
314322
(cellContent cell)
315323
_ -> "office:value-type='string'"
316324

325+
covered =
326+
case cellSpan cell of
327+
Spr.Covered -> "covered-"
328+
_ -> ""
329+
330+
span_ =
331+
case cellSpan cell of
332+
Spr.SpanHorizontal n | n>1 ->
333+
printf " table:number-columns-spanned='%d'" n
334+
Spr.SpanVertical n | n>1 ->
335+
printf " table:number-rows-spanned='%d'" n
336+
_ -> ""
337+
317338
anchor text =
318339
if T.null $ Spr.cellAnchor cell
319340
then text
320341
else printf "<text:a xlink:href='%s'>%s</text:a>"
321342
(escape $ T.unpack $ Spr.cellAnchor cell) text
322343

323344
in
324-
printf "<table:table-cell%s %s>" style valueType :
345+
printf "<table:%stable-cell%s%s %s>" covered style span_ valueType :
325346
printf "<text:p>%s</text:p>"
326347
(anchor $ escape $ T.unpack $ cellContent cell) :
327-
"</table:table-cell>" :
348+
printf "</table:%stable-cell>" covered :
328349
[]
329350

330351
escape :: String -> String

hledger-lib/Hledger/Write/Spreadsheet.hs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Hledger.Write.Spreadsheet (
88
Emphasis(..),
99
Cell(..),
1010
Class(Class), textFromClass,
11+
Span(..),
1112
Border(..),
1213
Lines(..),
1314
NumLines(..),
@@ -23,6 +24,8 @@ import Hledger.Data.Types (Amount)
2324
import qualified Data.List as List
2425
import Data.Text (Text)
2526

27+
import Prelude hiding (span)
28+
2629

2730
data Type =
2831
TypeString
@@ -82,26 +85,67 @@ newtype Class = Class Text
8285
textFromClass :: Class -> Text
8386
textFromClass (Class cls) = cls
8487

88+
89+
{- |
90+
* 'NoSpan' means a single unmerged cell.
91+
92+
* 'Covered' is a cell if it is part of a horizontally or vertically merged cell.
93+
We maintain these cells although they are ignored in HTML output.
94+
In contrast to that, FODS can store covered cells
95+
and allows to access the hidden cell content via formulas.
96+
CSV does not support merged cells
97+
and thus simply writes the content of covered cells.
98+
Maintaining 'Covered' cells also simplifies transposing.
99+
100+
* @'SpanHorizontal' n@ denotes the first cell in a row
101+
that is part of a merged cell.
102+
The merged cell contains @n@ atomic cells, including the first one.
103+
That is @SpanHorizontal 1@ is actually like @NoSpan@.
104+
The content of this cell is shown as content of the merged cell.
105+
106+
* @'SpanVertical' n@ starts a vertically merged cell.
107+
108+
The writer functions expect consistent data,
109+
that is, 'Covered' cells must actually be part of a merged cell
110+
and merged cells must only cover 'Covered' cells.
111+
-}
112+
data Span =
113+
NoSpan
114+
| Covered
115+
| SpanHorizontal Int
116+
| SpanVertical Int
117+
deriving (Eq)
118+
119+
transposeSpan :: Span -> Span
120+
transposeSpan span =
121+
case span of
122+
NoSpan -> NoSpan
123+
Covered -> Covered
124+
SpanHorizontal n -> SpanVertical n
125+
SpanVertical n -> SpanHorizontal n
126+
85127
data Cell border text =
86128
Cell {
87129
cellType :: Type,
88130
cellBorder :: Border border,
89131
cellStyle :: Style,
132+
cellSpan :: Span,
90133
cellAnchor :: Text,
91134
cellClass :: Class,
92135
cellContent :: text
93136
}
94137

95138
instance Functor (Cell border) where
96-
fmap f (Cell typ border style anchor class_ content) =
97-
Cell typ border style anchor class_ $ f content
139+
fmap f (Cell typ border style span anchor class_ content) =
140+
Cell typ border style span anchor class_ $ f content
98141

99142
defaultCell :: (Lines border) => text -> Cell border text
100143
defaultCell text =
101144
Cell {
102145
cellType = TypeString,
103146
cellBorder = noBorder,
104147
cellStyle = Body Item,
148+
cellSpan = NoSpan,
105149
cellAnchor = mempty,
106150
cellClass = Class mempty,
107151
cellContent = text
@@ -112,7 +156,10 @@ emptyCell = defaultCell mempty
112156

113157
transposeCell :: Cell border text -> Cell border text
114158
transposeCell cell =
115-
cell {cellBorder = transposeBorder $ cellBorder cell}
159+
cell {
160+
cellBorder = transposeBorder $ cellBorder cell,
161+
cellSpan = transposeSpan $ cellSpan cell
162+
}
116163

117164
transpose :: [[Cell border text]] -> [[Cell border text]]
118165
transpose = List.transpose . map (map transposeCell)

hledger/Hledger/Cli/Commands/Balance.hs

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ module Hledger.Cli.Commands.Balance (
260260
,multiBalanceReportTableAsText
261261
,multiBalanceReportAsSpreadsheet
262262
,addTotalBorders
263+
,addRowSpanHeader
263264
,simpleDateSpanCell
264265
,RowClass(..)
265266
-- ** Tests
@@ -454,12 +455,11 @@ budgetAverageClass rc =
454455
case rc of Value -> "budget rowaverage"; Total -> "budget colaverage"
455456

456457
-- What to show as heading for the totals row in balance reports ?
457-
-- Currently nothing in terminal, Total: in html and xSV output.
458-
totalRowHeadingText = ""
459-
totalRowHeadingBudgetText = ""
460-
totalRowHeadingHtml = "Total:"
461-
totalRowHeadingCsv = "Total:"
462-
totalRowHeadingBudgetCsv = "Total:"
458+
-- Currently nothing in terminal, Total: in HTML, FODS and xSV output.
459+
totalRowHeadingText = ""
460+
totalRowHeadingSpreadsheet = "Total:"
461+
totalRowHeadingBudgetText = ""
462+
totalRowHeadingBudgetCsv = "Total:"
463463

464464
-- Single-column balance reports
465465

@@ -659,6 +659,19 @@ addTotalBorders =
659659
rawTableContent :: [[Ods.Cell border text]] -> [[text]]
660660
rawTableContent = map (map Ods.cellContent)
661661

662+
addRowSpanHeader ::
663+
Ods.Cell border text ->
664+
[[Ods.Cell border text]] -> [[Ods.Cell border text]]
665+
addRowSpanHeader header rows =
666+
case rows of
667+
[] -> []
668+
[row] -> [header:row]
669+
_ ->
670+
zipWith (:)
671+
(header{Ods.cellSpan = Ods.SpanVertical (length rows)} :
672+
repeat header{Ods.cellSpan = Ods.Covered})
673+
rows
674+
662675
setAccountAnchor ::
663676
Maybe Text -> [Text] -> Text -> Ods.Cell border text -> Ods.Cell border text
664677
setAccountAnchor base query acct cell =
@@ -673,7 +686,7 @@ balanceReportAsSpreadsheet opts (items, total) =
673686
headers :
674687
concatMap (\(a, _, _, b) -> rows Value a b) items ++
675688
if no_total_ opts then []
676-
else addTotalBorders $ rows Total totalRowHeadingCsv total
689+
else addTotalBorders $ rows Total totalRowHeadingSpreadsheet total
677690
where
678691
cell = Ods.defaultCell
679692
headers =
@@ -690,14 +703,12 @@ balanceReportAsSpreadsheet opts (items, total) =
690703
(guard (rc==Value) >> balance_base_url_ opts)
691704
(querystring_ opts) name $
692705
cell $ accountNameDrop (drop_ opts) name in
706+
addRowSpanHeader accountCell $
693707
case layout_ opts of
694708
LayoutBare ->
695-
map (\a ->
696-
[accountCell,
697-
cell $ acommodity a,
698-
renderAmount rc $ mixedAmount a])
709+
map (\a -> [cell $ acommodity a, renderAmount rc $ mixedAmount a])
699710
. amounts $ mixedAmountStripCosts ma
700-
_ -> [[accountCell, renderAmount rc ma]]
711+
_ -> [[renderAmount rc ma]]
701712

702713
renderAmount rc mixedAmt =
703714
wbToText <$> cellFromMixedAmount bopts (amountClass rc, mixedAmt)
@@ -783,18 +794,17 @@ multiBalanceReportAsSpreadsheetHelper ishtml opts@ReportOpts{..} (PeriodicReport
783794
[hCell "rowtotal" "total" | row_total_] ++
784795
[hCell "rowaverage" "average" | average_]
785796
fullRowAsTexts row =
786-
map (anchorCell:) $
797+
addRowSpanHeader anchorCell $
787798
rowAsText Value (dateSpanCell balance_base_url_ querystring_ acctName) row
788799
where acctName = prrFullName row
789800
anchorCell =
790801
setAccountAnchor balance_base_url_ querystring_ acctName $
791802
accountCell $ accountNameDrop drop_ acctName
792-
totalrows
793-
| no_total_ = []
794-
| ishtml = zipWith (:) (accountCell totalRowHeadingHtml : repeat Ods.emptyCell) $
795-
rowAsText Total simpleDateSpanCell tr
796-
| otherwise = map (accountCell totalRowHeadingCsv :) $
797-
rowAsText Total simpleDateSpanCell tr
803+
totalrows =
804+
if no_total_
805+
then []
806+
else addRowSpanHeader (accountCell totalRowHeadingSpreadsheet) $
807+
rowAsText Total simpleDateSpanCell tr
798808
rowAsText rc dsCell =
799809
let fmt = if ishtml then oneLineNoCostFmt else machineFmt
800810
in map (map (fmap wbToText)) .

hledger/Hledger/Cli/CompoundBalanceCommand.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ compoundBalanceReportAsHtml ropts cbr =
370370
Total simpleDateSpanCell totalrow
371371
-- make a table of rendered lines of the report totals row
372372
& map (map (fmap wbToText))
373-
& zipWith (:) (Spr.defaultCell "Net:" : repeat Spr.emptyCell)
373+
& addRowSpanHeader (Spr.defaultCell "Net:")
374374
-- insert a headings column, with Net: on the first line only
375375
& addTotalBorders -- marking the first for special styling
376376
& map (Html.formatRow . map (fmap L.toHtml))

0 commit comments

Comments
 (0)