Advertisement
Guest User

Untitled

a guest
Oct 16th, 2019
132
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 4.82 KB | None | 0 0
  1. #!/usr/bin/env stack
  2. {- stack
  3. script
  4. --nix --no-nix-pure
  5. --resolver lts-14.7
  6. --package boxes,containers,Decimal,hledger-lib,text,time
  7. -}
  8.  
  9. {-# OPTIONS_GHC -Weverything -Wno-implicit-prelude -Wno-missing-export-lists -Wno-unsafe #-}
  10.  
  11. {-# LANGUAGE OverloadedStrings #-}
  12.  
  13. import Data.Decimal (Decimal)
  14. import Data.List (inits)
  15. import qualified Data.Map as M
  16. import Data.Maybe (fromMaybe, maybeToList)
  17. import qualified Data.Text as T
  18. import Data.Time.Calendar (Day, diffDays)
  19. import Data.Time.Clock (UTCTime(utctDay), getCurrentTime)
  20. import qualified Hledger.Data.Types as H
  21. import qualified Hledger.Read as H
  22. import qualified Text.PrettyPrint.Boxes as B
  23. import Text.Printf (printf)
  24.  
  25. main :: IO ()
  26. main = do
  27. journal <- H.defaultJournal
  28. today <- utctDay <$> getCurrentTime
  29. printAccountStats (calcAccountStats journal today)
  30.  
  31. -------------------------------------------------------------------------------
  32.  
  33. data AccountStats = AccountStats
  34. { aAgeOfOldestGBP :: Integer
  35. , aAgeOfAverageGBP :: Decimal
  36. , aAverageLifespanOfSpentGBP :: Maybe Decimal
  37. }
  38.  
  39. calcAccountStats :: H.Journal -> Day -> M.Map H.AccountName AccountStats
  40. calcAccountStats journal today =
  41. doAccountStats today <$>
  42. M.filter (not . null . snd) (foldl doTransaction M.empty (H.jtxns journal))
  43.  
  44. printAccountStats :: M.Map H.AccountName AccountStats -> IO ()
  45. printAccountStats stats = B.printBox $ B.hsep 1 B.top
  46. [ col B.left "account" T.unpack (M.keys stats)
  47. , col B.right "age of oldest £" (show . aAgeOfOldestGBP) vals
  48. , col B.right "age of average £" (roundDecimal . aAgeOfAverageGBP) vals
  49. , col B.right "average livespan of spent £" (maybe "-" roundDecimal . aAverageLifespanOfSpentGBP) vals
  50. ]
  51. where
  52. vals = M.elems stats
  53.  
  54. col :: B.Alignment -> String -> (a -> String) -> [a] -> B.Box
  55. col a hdr f bs = B.vcat a (map B.text (hdr:"---":map f bs))
  56.  
  57. -------------------------------------------------------------------------------
  58.  
  59. type Pots = ([(Integer, Decimal)], M.Map Day Decimal)
  60.  
  61. doTransaction :: M.Map H.AccountName Pots -> H.Transaction -> M.Map H.AccountName Pots
  62. doTransaction ages0 txn = foldl (doPosting day) ages0 expandedPostings where
  63. day = H.tdate txn
  64. expandedPostings =
  65. [ (H.paccount posting, amount)
  66. | posting <- concatMap explode (H.tpostings txn)
  67. , check (H.paccount posting)
  68. , amount <- maybeToList (toGBP (H.pamount posting))
  69. ]
  70.  
  71. check account =
  72. ("assets:cash:" `T.isPrefixOf` account) && not ("assets:cash:petty" `T.isPrefixOf` account)
  73.  
  74. doPosting :: Day -> M.Map H.AccountName Pots -> (H.AccountName, Decimal) -> M.Map H.AccountName Pots
  75. doPosting day ages (account, amount)
  76. | amount > 0 = M.alter (Just . addMoneyToPot) account ages
  77. | amount < 0 = M.alter (Just . delMoneyFromPot) account ages
  78. | otherwise = ages
  79. where
  80. addMoneyToPot :: Maybe Pots -> Pots
  81. addMoneyToPot (Just (ls, pots)) = (ls, M.alter (\a -> Just (amount + fromMaybe 0 a)) day pots)
  82. addMoneyToPot Nothing = ([], M.fromList [(day, amount)])
  83.  
  84. delMoneyFromPot :: Maybe Pots -> Pots
  85. delMoneyFromPot (Just (ls, pots)) = delMoneyFromPot' (abs amount) ls (M.toList pots)
  86. delMoneyFromPot Nothing = debt amount
  87.  
  88. delMoneyFromPot' :: Decimal -> [(Integer, Decimal)] -> [(Day, Decimal)] -> Pots
  89. delMoneyFromPot' q ls ((d,p):ps)
  90. | q > p = delMoneyFromPot' (q - p) ((diffDays day d, p):ls) ps
  91. | q < p = ((diffDays day d, p - q):ls, M.fromList ((d,p - q):ps))
  92. | otherwise = ((diffDays day d, p):ls, M.fromList ps)
  93. delMoneyFromPot' q _ [] = debt q
  94.  
  95. debt :: Decimal -> a
  96. debt q = error ("[" ++ T.unpack account ++ ", " ++ show day ++ "] tried to go into debt by " ++ show (abs q))
  97.  
  98. doAccountStats :: Day -> Pots -> AccountStats
  99. doAccountStats today (ls, pots) = AccountStats
  100. { aAgeOfOldestGBP = diffDays today (fst (head ds))
  101. , aAgeOfAverageGBP = weightedAvg [(fromIntegral (diffDays today d), w) | (d, w) <- ds]
  102. , aAverageLifespanOfSpentGBP =
  103. let ls' = filter ((/=0) . fst) ls
  104. in if null ls'
  105. then Nothing
  106. else Just (weightedAvg [(fromInteger d, w) | (d, w) <- ls'])
  107. }
  108. where
  109. ds = M.toList pots -- ideally 'pots' would be some non-empty map type
  110.  
  111. -------------------------------------------------------------------------------
  112.  
  113. weightedAvg :: Fractional a => [(a,a)] -> a
  114. weightedAvg xws = sum [x * w | (x, w) <- xws] / sum [w | (_, w) <- xws]
  115.  
  116. roundDecimal :: Decimal -> String
  117. roundDecimal = printf "%0.3f" . (realToFrac :: Decimal -> Double)
  118.  
  119. explode :: H.Posting -> [H.Posting]
  120. explode p =
  121. [ p { H.paccount = a }
  122. | a <- tail . map (T.intercalate ":") . inits . T.splitOn ":" $ H.paccount p
  123. ]
  124.  
  125. toGBP :: H.MixedAmount -> Maybe Decimal
  126. toGBP (H.Mixed [H.Amount "£" q _ _ _]) = Just q
  127. toGBP _ = Nothing
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement