{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} module Hcompta.CLI.Command.Stats where import Control.Monad (liftM, forM_) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (runExceptT) import qualified Data.Either import qualified Data.Foldable import qualified Data.Map.Strict as Data.Map import Data.Monoid ((<>)) import System.Console.GetOpt ( ArgDescr(..) , OptDescr(..) , usageInfo ) import System.Environment as Env (getProgName) import System.Exit (exitSuccess) import qualified System.IO as IO import qualified Hcompta.CLI.Args as Args import qualified Hcompta.CLI.Context as Context import Hcompta.CLI.Context (Context) import qualified Hcompta.CLI.Format.Ledger as CLI.Ledger import qualified Hcompta.CLI.Lang as Lang import qualified Hcompta.CLI.Write as Write -- import qualified Hcompta.Date as Date import qualified Hcompta.Amount.Unit as Amount.Unit import qualified Hcompta.Filter as Filter import qualified Hcompta.Filter.Read as Filter.Read -- import qualified Hcompta.Filter.Reduce as Filter.Reduce import qualified Hcompta.Format.Ledger as Ledger import qualified Hcompta.Format.Ledger.Journal as Ledger.Journal import qualified Hcompta.Format.Ledger.Read as Ledger.Read -- import qualified Hcompta.Format.Ledger.Write as Ledger.Write import qualified Hcompta.Lib.Interval as Interval import qualified Hcompta.Lib.Leijen as W import qualified Hcompta.Stats as Stats data Ctx = Ctx { ctx_input :: [FilePath] , ctx_filter_transaction :: Filter.Simplified (Filter.Filter_Bool (Filter.Filter_Transaction Ledger.Transaction)) } deriving (Show) nil :: Ctx nil = Ctx { ctx_input = [] , ctx_filter_transaction = mempty } usage :: IO String usage = do bin <- Env.getProgName let pad = replicate (length bin) ' ' return $unlines $ [ "SYNTAX " , " "++bin++" stats [-i JOURNAL_FILE]" , " "++pad++" [-t TRANSACTION_FILTER]" , " "++pad++" [JOURNAL_FILE] [...]" , "" , usageInfo "OPTIONS" options ] options :: Args.Options Ctx options = [ Option "h" ["help"] (NoArg (\_context _ctx -> do usage >>= IO.hPutStr IO.stderr exitSuccess)) "show this help" , Option "i" ["input"] (ReqArg (\s _context ctx -> do return $ ctx{ctx_input=s:ctx_input ctx}) "FILE") "read data from given file, multiple uses merge the data as would a concatenation do" , Option "t" ["transaction-filter"] (ReqArg (\s context ctx -> do ctx_filter_transaction <- liftM ((ctx_filter_transaction ctx <>) . Filter.simplify) $ liftIO $ Filter.Read.read Filter.Read.filter_transaction s >>= \f -> case f of Left ko -> Write.fatal context $ ko Right ok -> do Write.debug context $ "filter: transaction: " ++ show ok return ok return $ ctx{ctx_filter_transaction}) "FILTER") "filter at transaction level, multiple uses are merged with a logical AND" ] run :: Context.Context -> [String] -> IO () run context args = do (ctx, inputs) <- Args.parse context usage options (nil, args) read_journals <- liftM Data.Either.partitionEithers $ do CLI.Ledger.paths context $ ctx_input ctx ++ inputs >>= do mapM $ \path -> do liftIO $ runExceptT $ Ledger.Read.file (Ledger.Read.context (ctx_filter_transaction ctx) Ledger.journal) path >>= \x -> case x of Left ko -> return $ Left (path, ko) Right ok -> return $ Right ok case read_journals of (errs@(_:_), _journals) -> forM_ errs $ \(_path, err) -> do Write.fatal context $ err ([], journals) -> do Write.debug context $ "filter: transaction: " ++ show (ctx_filter_transaction ctx) let (files, stats) = ledger_stats ctx journals style_color <- Write.with_color context IO.stdout W.displayIO IO.stdout $ do W.renderPretty style_color 1.0 maxBound $ do doc_stats context ctx (files, stats) ledger_stats :: Ctx -> [ Ledger.Journal (Stats.Stats Ledger.Transaction) ] -> ([FilePath], Stats.Stats Ledger.Transaction) ledger_stats _ctx = Data.Foldable.foldl' (flip $ Ledger.Journal.fold (\Ledger.Journal { Ledger.journal_transactions=s , Ledger.journal_file=f } -> mappend ([f], s))) mempty doc_stats :: Context -> Ctx -> ([FilePath], Stats.Stats Ledger.Transaction) -> W.Doc doc_stats context _ctx (files, stats) = let lang = Context.lang context in W.toDoc lang Lang.Message_Accounts <> " (" <> (W.toDoc () $ Data.Map.size $ Stats.stats_accounts stats) <> ")" <> W.nest 2 ( let depth = Stats.stats_accounts_depths stats in W.line <> W.toDoc lang Lang.Message_Depths <> " (" <> W.toDoc () (Interval.limit $ Interval.low depth) <> ".." <> W.toDoc () (Interval.limit $ Interval.high depth) <> ")" ) <> W.line <> W.toDoc lang Lang.Message_Transactions <> " (" <> (W.toDoc () $ Stats.stats_transactions stats) <> ")" <> W.nest 2 ( case Stats.stats_transactions_span stats of Nothing -> W.empty Just date -> W.line <> "Dates" <> " (" <> W.toDoc () (Interval.limit $ Interval.low date) <> ".." <> W.toDoc () (Interval.limit $ Interval.high date) <> ")" ) <> W.line <> W.toDoc lang Lang.Message_Units <> " (" <> (W.toDoc () (Data.Map.size $ Data.Map.delete Amount.Unit.nil $ Stats.stats_units stats)) <> ")" <> W.line <> W.toDoc lang Lang.Message_Journals <> " (" <> (W.toDoc () (length $ files)) <> ")" <> W.line <> W.toDoc lang Lang.Message_Tags <> " (" <> (W.toDoc () (foldr (flip $ foldr (+)) 0 $ Stats.stats_tags stats)) <> ")" <> W.nest 2 ( W.line <> "Distincts" <> " (" <> (W.toDoc () (Data.Map.size $ Stats.stats_tags stats)) <> ")" ) <> W.line