hledger/hledger/Hledger/Cli/Commands/Stats.hs
Stephen Morgan b0aa70b27a lib!: Rename the fields of ReportSpec.
This is done to be more consistent with future field naming conventions,
and to make automatic generation of lenses simpler. See discussion in
\#1545.

rsOpts -> _rsReportOpts
rsToday -> _rsDay
rsQuery -> _rsQuery
rsQueryOpts -> _rsQueryOpts
2021-07-23 10:37:08 -10:00

112 lines
4.7 KiB
Haskell

{-|
Print some statistics for the journal.
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Hledger.Cli.Commands.Stats (
statsmode
,stats
)
where
import Data.Default (def)
import Data.List (nub, sortOn)
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe)
import Data.HashSet (size, fromList)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as TB
import Data.Time.Calendar (Day, addDays, diffDays)
import System.Console.CmdArgs.Explicit hiding (Group)
import Text.Printf (printf)
import qualified Data.Map as Map
import Hledger
import Hledger.Cli.CliOptions
import Hledger.Cli.Utils (writeOutputLazyText)
import Text.Tabular.AsciiWide
statsmode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Stats.txt")
[flagReq ["output-file","o"] (\s opts -> Right $ setopt "output-file" s opts) "FILE" "write output to FILE."
]
[generalflagsgroup1]
hiddenflags
([], Just $ argsFlag "[QUERY]")
-- like Register.summarisePostings
-- | Print various statistics for the journal.
stats :: CliOpts -> Journal -> IO ()
stats opts@CliOpts{reportspec_=rspec} j = do
d <- getCurrentDay
let q = _rsQuery rspec
l = ledgerFromJournal q j
reportspan = ledgerDateSpan l `spanDefaultsFrom` queryDateSpan False q
intervalspans = splitSpan (interval_ $ _rsReportOpts rspec) reportspan
showstats = showLedgerStats l d
s = unlinesB $ map showstats intervalspans
writeOutputLazyText opts $ TB.toLazyText s
showLedgerStats :: Ledger -> Day -> DateSpan -> TB.Builder
showLedgerStats l today span =
unlinesB $ map (renderRowB def{tableBorders=False, borderSpaces=False} . showRow) stats
where
showRow (label, value) = Group NoLine $ map (Header . textCell TopLeft)
[fitText (Just w1) (Just w1) False True label `T.append` ": ", T.pack value]
w1 = maximum $ map (T.length . fst) stats
stats = [
("Main file", path) -- ++ " (from " ++ source ++ ")")
,("Included files", unlines $ drop 1 $ journalFilePaths j)
,("Transactions span", printf "%s to %s (%d days)" (start span) (end span) days)
,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed)
,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)
,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30)
,("Transactions last 7 days", printf "%d (%0.1f per day)" tnum7 txnrate7)
,("Payees/descriptions", show $ size $ fromList $ map (tdescription) ts)
,("Accounts", printf "%d (depth %d)" acctnum acctdepth)
,("Commodities", printf "%s (%s)" (show $ length cs) (T.intercalate ", " cs))
,("Market prices", printf "%s (%s)" (show $ length mktprices) (T.intercalate ", " mktpricecommodities))
-- Transactions this month : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s)
-- Unmarked transactions : %(unmarked)s
-- Days since reconciliation : %(reconcileelapsed)s
-- Days since last transaction : %(recentelapsed)s
]
where
j = ljournal l
path = journalFilePath j
ts = sortOn tdate $ filter (spanContainsDate span . tdate) $ jtxns j
as = nub $ map paccount $ concatMap tpostings ts
cs = either error' Map.keys $ commodityStylesFromAmounts $ concatMap (amountsRaw . pamount) $ concatMap tpostings ts -- PARTIAL:
lastdate | null ts = Nothing
| otherwise = Just $ tdate $ last ts
lastelapsed = fmap (diffDays today) lastdate
showelapsed Nothing = ""
showelapsed (Just days) = printf " (%d %s)" days' direction
where days' = abs days
direction | days >= 0 = "days ago" :: String
| otherwise = "days from now"
tnum = length ts
start (DateSpan (Just d) _) = show d
start _ = ""
end (DateSpan _ (Just d)) = show d
end _ = ""
days = fromMaybe 0 $ daysInSpan span
txnrate | days==0 = 0
| otherwise = fromIntegral tnum / fromIntegral days :: Double
tnum30 = length $ filter withinlast30 ts
withinlast30 t = d >= addDays (-30) today && (d<=today) where d = tdate t
txnrate30 = fromIntegral tnum30 / 30 :: Double
tnum7 = length $ filter withinlast7 ts
withinlast7 t = d >= addDays (-7) today && (d<=today) where d = tdate t
txnrate7 = fromIntegral tnum7 / 7 :: Double
acctnum = length as
acctdepth | null as = 0
| otherwise = maximum $ map accountNameLevel as
mktprices = jpricedirectives j
mktpricecommodities = nubSort $ map pdcommodity mktprices