penny-lib-0.4.0.0: Penny/Cabin/Posts.hs
-- | The Penny Postings report
--
-- The Postings report displays postings in a tabular format designed
-- to be read by humans. Some terminology used in the Postings report:
--
-- [@row@] The smallest unit that spans from left to right. A row,
-- however, might consist of more than one screen line. For example,
-- the running balance is shown on the far right side of the Postings
-- report. The running balance might consist of more than one
-- commodity. Each commodity is displayed on its own screen
-- line. However, all these lines put together are displayed in a
-- single row.
--
-- [@column@] The smallest unit that spans from top to bottom.
--
-- [@tranche@] Each posting is displayed in several rows. The group of
-- rows that is displayed for a single posting is called a tranche.
--
-- [@tranche row@] Each tranche has a particular number of rows
-- (currently four); each of these rows is known as a tranche row.
--
-- [@field@] Corresponds to a particular element of the posting, such
-- as whether it is a debit or credit or its payee. The user can
-- select which fields to see.
--
-- [@allocation@] The width of the Payee and Account fields is
-- variable. Generally their width will adjust to fill the entire
-- width of the screen. The allocations of the Payee and Account
-- fields determine how much of the remaining space each field will
-- receive.
--
-- The Postings report is easily customized from the command line to
-- show various fields. However, the order of the fields is not
-- configurable without editing the source code (sorry).
module Penny.Cabin.Posts (
postsReport
, parseReport
, makeReport
, defaultOptions
, ZincOpts(..)
, ymd
, qtyAsIs
, balanceAsIs
, defaultWidth
, columnsVarToWidth
, widthFromRuntime
, defaultFields
, defaultSpacerWidth
) where
import qualified Control.Monad.Exception.Synchronous as Ex
import qualified Data.Text as X
import qualified Data.Text.Lazy as XL
import qualified Penny.Cabin.Chunk as CC
import qualified Penny.Cabin.Colors as PC
import qualified Penny.Cabin.Colors.DarkBackground as Dark
import qualified Penny.Cabin.Interface as I
import qualified Penny.Cabin.Options as CO
import qualified Penny.Cabin.Posts.Allocated as A
import qualified Penny.Cabin.Posts.Allocate as Alc
import qualified Penny.Cabin.Posts.Chunk as C
import qualified Penny.Cabin.Posts.Fields as F
import qualified Penny.Cabin.Posts.Help as H
import qualified Penny.Cabin.Posts.Meta as M
import Penny.Cabin.Posts.Meta (Box)
import qualified Penny.Cabin.Posts.Parser as P
import qualified Penny.Cabin.Posts.Spacers as S
import qualified Penny.Cabin.Posts.Types as T
import qualified Penny.Copper as Cop
import qualified Penny.Lincoln as L
import qualified Penny.Lincoln.Balance as Bal
import qualified Penny.Lincoln.Queries as Q
import qualified Penny.Liberty as Ly
import qualified Penny.Shield as Sh
import Data.Time as Time
import System.Console.MultiArg.Prim (Parser)
import System.Locale (defaultTimeLocale)
import Text.Matchers.Text (CaseSensitive)
-- | All information needed to make a Posts report. This function
-- never fails.
postsReport ::
CC.Colors
-- ^ How many colors to show.
-> CO.ShowZeroBalances
-> (L.Box Ly.LibertyMeta -> Bool)
-- ^ Removes posts from the report if applying this function to the
-- post returns False. Posts removed still affect the running
-- balance.
-> [Ly.PostFilterFn]
-- ^ Applies these post-filters to the list of posts that results
-- from applying the predicate above. Might remove more
-- postings. Postings removed still affect the running balance.
-> C.ChunkOpts
-> [L.Box Ly.LibertyMeta]
-> XL.Text
postsReport col szb pdct pff co =
CC.chunksToText col
. C.makeChunk co
. M.toBoxList szb pdct pff
parseReport ::
(Sh.Runtime -> ZincOpts)
-> Parser I.ReportFunc
parseReport frt = do
getState <- P.parseOptions
let rf rt cs fty ps _ = do
let zo = frt rt
maySt' = getState rt dtz rg st
where
dtz = defaultTimeZone zo
rg = radGroup zo
st = newParseState cs fty zo
st' <- Ex.mapException showParserError maySt'
pdct <- getPredicate . P.tokens $ st'
return $ postsReport (P.colorPref st')
(P.showZeroBalances st') pdct
(P.postFilter st') (chunkOpts st' zo) ps
return rf
makeReport ::
(Sh.Runtime -> ZincOpts)
-> I.Report
makeReport frt = I.Report {
I.help = H.help
, I.name = "postings"
, I.parseReport = parseReport frt }
defaultOptions ::
Cop.DefaultTimeZone
-> Cop.RadGroup
-> Sh.Runtime
-> ZincOpts
defaultOptions dtz rg rt = ZincOpts {
defaultTimeZone = dtz
, radGroup = rg
, fields = defaultFields
, colorPref = CO.maxCapableColors rt
, drCrColors = Dark.drCrColors
, baseColors = Dark.baseColors
, width = widthFromRuntime rt
, showZeroBalances = CO.ShowZeroBalances False
, dateFormat = ymd
, qtyFormat = qtyAsIs
, balanceFormat = balanceAsIs
, subAccountLength = A.SubAccountLength 2
, payeeAllocation = Alc.allocation 60
, accountAllocation = Alc.allocation 40
, spacers = defaultSpacerWidth }
showParserError :: P.Error -> X.Text
showParserError = X.pack . show
getPredicate ::
[Ly.Token (L.Box Ly.LibertyMeta -> Bool)]
-> Ex.Exceptional X.Text (L.Box Ly.LibertyMeta -> Bool)
getPredicate ts =
case ts of
[] -> return $ const True
ls ->
Ex.fromMaybe
(X.pack "posts report: bad posting filter expression")
(Ly.parseTokenList ls)
-- | All the information to configure the postings report if the
-- options will be parsed in from the command line.
data ZincOpts = ZincOpts {
defaultTimeZone :: Cop.DefaultTimeZone
-- ^ The postings report takes options to determine which posts will
-- be visible. This determines the time zone to use when parsing
-- these options. Does not affect how the resulting report is
-- formatted. For that, see the dateFormat field.
, radGroup :: Cop.RadGroup
-- ^ The postings report takes options to determine which posts
-- will be visible. This determines the radix point and grouping
-- character to use when parsing these options. Does not affect
-- how the resulting report is formatted. For that, see the
-- qtyFormat field.
, fields :: F.Fields Bool
-- ^ Default fields to show in the report.
, colorPref :: CC.Colors
-- ^ How many colors you want to see, or do it
-- automatically.
, drCrColors :: PC.DrCrColors
-- ^ Colors to use when displaying debits, credits, and
-- when displaying balance totals
, baseColors :: PC.BaseColors
-- ^ Colors to use when displaying everything else
, width :: T.ReportWidth
-- ^ Gives the default report width. This can be
-- overridden on the command line. You can use the
-- information from the Runtime to make this as wide as
-- the current terminal.
, showZeroBalances :: CO.ShowZeroBalances
-- ^ Are commodities that have no balance shown in the Total fields
-- of the report?
, dateFormat :: Box -> X.Text
-- ^ How to display dates. This function is applied to the
-- a PostingInfo so it has lots of information, but it
-- should return a date for use in the Date field.
, qtyFormat :: Box -> X.Text
-- ^ How to display the quantity of the posting. This
-- function is applied to a Box so it has lots of
-- information, but it should return a formatted string of
-- the quantity. Allows you to format digit grouping,
-- radix points, perform rounding, etc.
, balanceFormat :: L.Commodity -> L.BottomLine -> X.Text
-- ^ How to display balance totals. Similar to
-- balanceFormat.
, subAccountLength :: A.SubAccountLength
-- ^ When shortening the names of sub accounts to make
-- them fit, they will be this long.
, payeeAllocation :: Alc.Allocation
-- ^ This and accountAllocation determine how much space
-- payees and accounts receive. They divide up the
-- remaining space after everything else is displayed. For
-- instance if payeeAllocation is 60 and accountAllocation
-- is 40, the payee takes about 60 percent of the
-- remaining space and the account takes about 40 percent.
, accountAllocation :: Alc.Allocation
-- ^ See payeeAllocation above
, spacers :: S.Spacers Int
-- ^ Default width for spacer fields. If any of these Ints are
-- less than or equal to zero, there will be no spacer. There is
-- never a spacer for fields that do not appear in the report.
}
chunkOpts ::
P.State
-> ZincOpts
-> C.ChunkOpts
chunkOpts s z = C.ChunkOpts {
C.baseColors = P.baseColors s
, C.drCrColors = P.drCrColors s
, C.dateFormat = dateFormat z
, C.qtyFormat = qtyFormat z
, C.balanceFormat = balanceFormat z
, C.fields = P.fields s
, C.subAccountLength = subAccountLength z
, C.payeeAllocation = payeeAllocation z
, C.accountAllocation = accountAllocation z
, C.spacers = spacers z
, C.reportWidth = P.width s
}
newParseState ::
CaseSensitive
-> L.Factory
-> ZincOpts
-> P.State
newParseState cs fty o = P.State {
P.sensitive = cs
, P.factory = fty
, P.tokens = []
, P.postFilter = []
, P.fields = fields o
, P.colorPref = colorPref o
, P.drCrColors = drCrColors o
, P.baseColors = baseColors o
, P.width = width o
, P.showZeroBalances = showZeroBalances o
}
-- | Shows the date of a posting in YYYY-MM-DD format.
ymd :: Box -> X.Text
ymd p = X.pack (Time.formatTime defaultTimeLocale fmt d) where
d = Time.localDay
. L.localTime
. Q.dateTime
. L.boxPostFam
$ p
fmt = "%Y-%m-%d"
-- | Shows the quantity of a posting. Does no rounding or
-- prettification; simply uses show on the underlying Decimal.
qtyAsIs :: Box -> X.Text
qtyAsIs p = X.pack . show . L.unQty . Q.qty . L.boxPostFam $ p
-- | Shows the quantity of a balance. If there is no quantity, shows
-- two dashes.
balanceAsIs :: a -> L.BottomLine -> X.Text
balanceAsIs _ n = case n of
L.Zero -> X.pack "--"
L.NonZero c -> X.pack . show . L.unQty . Bal.qty $ c
-- | The default width for the report.
defaultWidth :: T.ReportWidth
defaultWidth = T.ReportWidth 80
-- | Applied to the value of the COLUMNS environment variable, returns
-- an appropriate ReportWidth.
columnsVarToWidth :: Maybe String -> T.ReportWidth
columnsVarToWidth ms = case ms of
Nothing -> defaultWidth
Just str -> case reads str of
[] -> defaultWidth
(i, []):[] -> if i > 0 then T.ReportWidth i else defaultWidth
_ -> defaultWidth
-- | Given the Runtime, use the defaultWidth given above to calculate
-- the report's width if COLUMNS does not yield a value. Otherwise,
-- use what is in COLUMNS.
widthFromRuntime :: Sh.Runtime -> T.ReportWidth
widthFromRuntime rt = case Sh.screenWidth rt of
Nothing -> defaultWidth
Just w -> T.ReportWidth . Sh.unScreenWidth $ w
-- | Default fields to show in the Postings report.
defaultFields :: F.Fields Bool
defaultFields =
F.Fields { F.globalTransaction = False
, F.revGlobalTransaction = False
, F.globalPosting = False
, F.revGlobalPosting = False
, F.fileTransaction = False
, F.revFileTransaction = False
, F.filePosting = False
, F.revFilePosting = False
, F.filtered = False
, F.revFiltered = False
, F.sorted = False
, F.revSorted = False
, F.visible = False
, F.revVisible = False
, F.lineNum = False
, F.date = True
, F.flag = False
, F.number = False
, F.payee = True
, F.account = True
, F.postingDrCr = True
, F.postingCmdty = True
, F.postingQty = True
, F.totalDrCr = True
, F.totalCmdty = True
, F.totalQty = True
, F.tags = False
, F.memo = False
, F.filename = False }
-- | Default width of spacers; most are one character wide, but the
-- spacer after payee is 4 characters wide.
defaultSpacerWidth :: S.Spacers Int
defaultSpacerWidth =
S.Spacers { S.globalTransaction = 1
, S.revGlobalTransaction = 1
, S.globalPosting = 1
, S.revGlobalPosting = 1
, S.fileTransaction = 1
, S.revFileTransaction = 1
, S.filePosting = 1
, S.revFilePosting = 1
, S.filtered = 1
, S.revFiltered = 1
, S.sorted = 1
, S.revSorted = 1
, S.visible = 1
, S.revVisible = 1
, S.lineNum = 1
, S.date = 1
, S.flag = 1
, S.number = 1
, S.payee = 4
, S.account = 1
, S.postingDrCr = 1
, S.postingCmdty = 1
, S.postingQty = 1
, S.totalDrCr = 1
, S.totalCmdty = 1 }