arbtt-0.4.4: src/Stats.hs
{-# LANGUAGE RecordWildCards, NamedFieldPuns #-}
module Stats where
import Data.Time
import Data.Maybe
import Data.List
import Data.Ord
import Text.Printf
import qualified Data.Map as M
import qualified Data.Set as S
import Data
import Categorize
data Report = GeneralInfos | TotalTime | Category String | EachCategory
deriving (Show, Eq)
data Filter = Exclude Activity | Only Activity | AlsoInactive | GeneralCond String
deriving (Show, Eq)
data ReportOption = MinPercentage Double
deriving (Show, Eq)
-- Data format semantically representing the result of a report, including the
-- title
data ReportResults =
ListOfFields String [(String, String)]
| ListOfTimePercValues String [(String, String, Double)]
| PieChartOfTimePercValues String [(String, String, Double)]
applyFilters :: [Filter] -> TimeLog (Ctx, ActivityData) -> TimeLog (Ctx, ActivityData)
applyFilters filters tle =
foldr (\flag -> case flag of
Exclude act -> excludeTag act
Only act -> onlyTag act
AlsoInactive -> id
GeneralCond s-> applyCond s
) (if AlsoInactive `elem` filters then tle else defaultFilter tle) filters
excludeTag act = filter (notElem act . snd . tlData)
onlyTag act = filter (elem act . snd . tlData)
defaultFilter = excludeTag inactiveActivity
-- | to be used lazily, to re-use computation when generating more than one
-- report at a time
data Calculations = Calculations
{ firstDate :: UTCTime
, lastDate :: UTCTime
, timeDiff :: NominalDiffTime
, totalTimeRec :: NominalDiffTime
, totalTimeSel :: NominalDiffTime
, fractionRec :: Double
, fractionSel :: Double
, fractionSelRec :: Double
, sums :: M.Map Activity NominalDiffTime
, allTags :: TimeLog (Ctx, ActivityData)
, tags :: TimeLog (Ctx, ActivityData)
}
prepareCalculations :: TimeLog (Ctx, ActivityData) -> TimeLog (Ctx, ActivityData) -> Calculations
prepareCalculations allTags tags =
let c = Calculations
{ firstDate = tlTime (head allTags)
, lastDate = tlTime (last allTags)
, timeDiff = diffUTCTime (lastDate c) (firstDate c)
, totalTimeRec = fromInteger (sum (map tlRate allTags))/1000
, totalTimeSel = fromInteger (sum (map tlRate tags))/1000
, fractionRec = realToFrac (totalTimeRec c) / (realToFrac (timeDiff c))
, fractionSel = realToFrac (totalTimeSel c) / (realToFrac (timeDiff c))
, fractionSelRec = realToFrac (totalTimeSel c) / realToFrac (totalTimeRec c)
, sums = sumUp tags
, allTags
, tags
} in c
-- | Sums up each occurence of an 'Activity', weighted by the sampling rate
sumUp :: TimeLog (Ctx, ActivityData) -> M.Map Activity NominalDiffTime
sumUp = foldr go M.empty
where go tl m = foldr go' m (snd (tlData tl))
where go' act = M.insertWith (+) act (fromInteger (tlRate tl)/1000)
listCategories :: TimeLog (Ctx, ActivityData) -> [Category]
listCategories = S.toList . foldr go S.empty
where go tl m = foldr go' m (snd (tlData tl))
where go' (Activity (Just cat) _) = S.insert cat
go' _ = id
putReports :: [ReportOption] -> Calculations -> [Report] -> IO ()
putReports opts c = sequence_ . intersperse (putStrLn "") . map (putReport opts c)
putReport :: [ReportOption] -> Calculations -> Report -> IO ()
putReport opts c EachCategory = putReports opts c (map Category (listCategories (tags c)))
putReport opts c r = renderReport $ reportToTable opts c r
reportToTable :: [ReportOption] -> Calculations -> Report -> ReportResults
reportToTable opts (Calculations {..}) r = case r of
GeneralInfos -> ListOfFields "General Information" $
[ ("FirstRecord", show firstDate)
, ("LastRecord", show lastDate)
, ("Number of records", show (length allTags))
, ("Total time recorded", showTimeDiff totalTimeRec)
, ("Total time selected", showTimeDiff totalTimeSel)
, ("Fraction of total time recorded", printf "%3.0f%%" (fractionRec * 100))
, ("Fraction of total time selected", printf "%3.0f%%" (fractionSel * 100))
, ("Fraction of recorded time selected", printf "%3.0f%%" (fractionSelRec * 100))
]
TotalTime -> ListOfTimePercValues "Total time per tag" $
mapMaybe (\(tag,time) ->
let perc = realToFrac time/realToFrac totalTimeSel in
if perc*100 >= minPercentage
then Just $ ( show tag
, showTimeDiff time
, perc)
else Nothing
) $
reverse $
sortBy (comparing snd) $
M.toList sums
Category cat -> PieChartOfTimePercValues ("Statistics for category " ++ cat) $
let filteredSums = M.filterWithKey (\a _ -> isCategory cat a) sums
uncategorizedTime = totalTimeSel - M.fold (+) 0 filteredSums
tooSmallSums = M.filter (\t -> realToFrac t / realToFrac totalTimeSel * 100 < minPercentage) filteredSums
tooSmallTimes = M.fold (+) 0 tooSmallSums
in
mapMaybe (\(tag,time) ->
let perc = realToFrac time/realToFrac totalTimeSel in
if perc*100 >= minPercentage
then Just ( show tag
, showTimeDiff time
, perc)
else Nothing
)
(reverse $ sortBy (comparing snd) $ M.toList filteredSums)
++
(
if tooSmallTimes > 0
then [( printf "(%d entries omitted)" (M.size tooSmallSums)
, showTimeDiff tooSmallTimes
, realToFrac tooSmallTimes/realToFrac totalTimeSel
)]
else []
)
++
(if uncategorizedTime > 0
then [( "(unmatched time)"
, showTimeDiff uncategorizedTime
, realToFrac uncategorizedTime/realToFrac totalTimeSel
)]
else []
)
where minPercentage = last $ mapMaybe (\f -> case f of {MinPercentage m -> Just m {- ; _ -> Nothing -} }) opts
renderReport (ListOfFields title dats) = do
putStrLnUnderlined title
putStr $ tabulate False $ map (\(f,v) -> [f,v]) dats
renderReport (ListOfTimePercValues title dats) = do
putStrLnUnderlined title
putStr $ tabulate True $ ["Tag","Time","Percentage"] : map (\(f,t,p) -> [f,t,printf "%.2f" (p*100)]) dats
renderReport (PieChartOfTimePercValues title dats) = do
putStrLnUnderlined title
putStr $ tabulate True $ ["Tag","Time","Percentage"] : map (\(f,t,p) -> [f,t,printf "%.2f" (p*100)]) dats
tabulate :: Bool -> [[String]] -> String
tabulate titlerow rows = unlines $ addTitleRow $ map (intercalate " | " . zipWith (\l s -> take (l - length s) (repeat ' ') ++ s) colwidths) rows
where cols = transpose rows
colwidths = map (maximum . map length) cols
addTitleRow | titlerow = \(l:ls) -> (map (\c -> if c == ' ' then '_' else c) l ++ "_")
: ls
-- | titlerow = \(l:ls) -> l : (take (length l) (repeat '-')) : ls
| otherwise = id
showTimeDiff :: NominalDiffTime -> String
showTimeDiff t = go False $ zip [days,hours,mins,secs] ["d","h","m","s"]
where s = round t :: Integer
days = s `div` (24*60*60)
hours = (s `div` (60*60)) `mod` 24
mins = (s `div` 60) `mod` 60
secs = s `mod` 60
go False [] = "0s"
go True [] = ""
-- go True vs | all (==0) (map fst vs) = concat (replicate (length vs) " ")
go True ((a,u):vs) = printf "%02d%s" a u ++ go True vs
go False ((a,u):vs) | a > 0 = printf "%2d%s" a u ++ go True vs
| otherwise = go False vs
putStrLnUnderlined str = do
putStrLn str
putStrLn $ map (const '=') str