hledger/Models.hs

194 lines
6.8 KiB
Haskell
Raw Normal View History

2007-02-10 20:36:50 +03:00
2007-02-11 02:42:22 +03:00
module Models -- data types & behaviours
2007-02-10 20:36:50 +03:00
where
2007-02-09 04:23:12 +03:00
import Text.Printf
import Data.List
-- types
2007-02-09 04:23:12 +03:00
data Ledger = Ledger {
modifier_entries :: [ModifierEntry],
periodic_entries :: [PeriodicEntry],
entries :: [Entry]
2007-02-09 12:58:11 +03:00
} deriving (Eq)
2007-02-11 02:42:22 +03:00
data ModifierEntry = ModifierEntry { -- aka "automated entry"
2007-02-09 04:23:12 +03:00
valueexpr :: String,
m_transactions :: [Transaction]
} deriving (Eq)
data PeriodicEntry = PeriodicEntry {
periodexpr :: String,
p_transactions :: [Transaction]
} deriving (Eq)
data Entry = Entry {
date :: Date,
2007-02-11 02:42:22 +03:00
status :: Status,
2007-02-09 04:23:12 +03:00
code :: String,
description :: String,
transactions :: [Transaction]
} deriving (Eq)
data Transaction = Transaction {
account :: Account,
amount :: Amount
} deriving (Eq)
data Amount = Amount {
currency :: String,
quantity :: Double
2007-02-09 12:58:11 +03:00
} deriving (Eq)
2007-02-09 04:23:12 +03:00
type Date = String
2007-02-11 02:42:22 +03:00
type Status = Bool
2007-02-09 04:23:12 +03:00
type Account = String
-- Amount arithmetic - ignores currency conversion
instance Num Amount where
abs (Amount c q) = Amount c (abs q)
signum (Amount c q) = Amount c (signum q)
fromInteger i = Amount "$" (fromInteger i)
(+) = amountAdd
(-) = amountSub
(*) = amountMult
Amount ca qa `amountAdd` Amount cb qb = Amount ca (qa + qb)
Amount ca qa `amountSub` Amount cb qb = Amount ca (qa - qb)
Amount ca qa `amountMult` Amount cb qb = Amount ca (qa * qb)
-- show & display methods
2007-02-09 04:23:12 +03:00
2007-02-09 12:58:11 +03:00
instance Show Ledger where
show l = "Ledger with " ++ m ++ " modifier, " ++ p ++ " periodic, " ++ e ++ " normal entries:\n"
++ (concat $ map show (modifier_entries l))
++ (concat $ map show (periodic_entries l))
++ (concat $ map show (entries l))
where
m = show $ length $ modifier_entries l
p = show $ length $ periodic_entries l
e = show $ length $ entries l
2007-02-09 04:23:12 +03:00
instance Show ModifierEntry where
show e = "= " ++ (valueexpr e) ++ "\n" ++ unlines (map show (m_transactions e))
instance Show PeriodicEntry where
show e = "~ " ++ (periodexpr e) ++ "\n" ++ unlines (map show (p_transactions e))
instance Show Entry where show = showEntry
2007-02-09 12:58:11 +03:00
-- a register entry is displayed as two or more lines like this:
-- date description account amount balance
-- DDDDDDDDDD dddddddddddddddddddd aaaaaaaaaaaaaaaaaaaaaaaaa AAAAAAAAAA AAAAAAAAAA
-- aaaaaaaaaaaaaaaaaaaaaaaaa AAAAAAAAAA AAAAAAAAAA
-- ... ... ...
-- dateWidth = 10
-- descWidth = 20
-- acctWidth = 25
-- amtWidth = 10
-- balWidth = 10
2007-02-09 12:58:11 +03:00
showEntry :: Entry -> String
showEntry e = unlines $ map fst (entryLines e)
-- convert an Entry to entry lines (string, amount pairs)
entryLines :: Entry -> [(String,Amount)]
entryLines e =
[firstline] ++ otherlines
where
t:ts = transactions e
entrydesc = printf "%-10s %-20s " (date e) (take 20 $ description e)
firstline = (entrydesc ++ (show t), amount t)
otherlines = map (\t -> (prependSpace $ show t, amount t)) ts
prependSpace = (replicate 32 ' ' ++)
2007-02-09 12:58:11 +03:00
2007-02-09 04:23:12 +03:00
instance Show Transaction where
show t = printf "%-25s %10s" (take 25 $ account t) (show $ amount t)
instance Show Amount where
show (Amount cur qty) =
let roundedqty = printf "%.2f" qty in
case roundedqty of
"0.00" -> "0"
otherwise -> cur ++ roundedqty
-- in the register report we show entries plus a running balance
showEntriesWithBalances :: [Entry] -> Amount -> String
showEntriesWithBalances [] _ = ""
showEntriesWithBalances (e:es) b =
showEntryWithBalances e b ++ (showEntriesWithBalances es b')
where b' = b + (entryBalance e)
entryBalance :: Entry -> Amount
entryBalance = sumTransactions . transactions
showEntryWithBalances :: Entry -> Amount -> String
showEntryWithBalances e b =
unlines [s | (s,a,b) <- entryLinesWithBalances (entryLines e) b]
entryLinesWithBalances :: [(String,Amount)] -> Amount -> [(String,Amount,Amount)]
entryLinesWithBalances [] _ = []
entryLinesWithBalances ((str,amt):els) bal =
[(str',amt,bal')] ++ entryLinesWithBalances els bal'
where
bal' = bal + amt
str' = str ++ (printf " %10.2s" (show bal'))
2007-02-09 12:58:11 +03:00
-- misc
autofillEntry :: Entry -> Entry
autofillEntry e =
Entry (date e) (status e) (code e) (description e)
(autofillTransactions (transactions e))
autofillTransactions :: [Transaction] -> [Transaction]
autofillTransactions ts =
let (ns, as) = normalAndAutoTransactions ts in
case (length as) of
0 -> ns
1 -> ns ++ [balanceTransaction $ head as]
where balanceTransaction t = t{amount = -(sumTransactions ns)}
otherwise -> error "too many blank transactions in this entry"
normalAndAutoTransactions :: [Transaction] -> ([Transaction], [Transaction])
normalAndAutoTransactions ts =
partition isNormal ts
where isNormal t = (currency $ amount t) /= "AUTO"
sumTransactions :: [Transaction] -> Amount
sumTransactions ts = sum [amount t | t <- ts]
transactionsFromEntries :: [Entry] -> [Transaction]
transactionsFromEntries es = concat $ map transactions es
accountsFromTransactions :: [Transaction] -> [Account]
accountsFromTransactions ts = nub $ map account ts
2007-02-10 20:36:50 +03:00
accountsUsed :: Ledger -> [Account]
accountsUsed l = accountsFromTransactions $ transactionsFromEntries $ entries l
-- ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"]
expandAccounts :: [Account] -> [Account]
expandAccounts l = nub $ concat $ map expand l
where
expand l' = map (concat . intersperse ":") (tail $ inits $ splitAtElement ':' l')
splitAtElement :: Eq a => a -> [a] -> [[a]]
splitAtElement e l =
case dropWhile (e==) l of
[] -> []
l' -> first : splitAtElement e rest
where
(first,rest) = break (e==) l'
accountTree :: Ledger -> [Account]
accountTree = sort . expandAccounts . accountsUsed
entriesMatching :: String -> Ledger -> [Entry]
entriesMatching s l = filterEntriesByAccount s (entries l)
filterEntriesByAccount :: String -> [Entry] -> [Entry]
filterEntriesByAccount s es = filter (matchEntryAccount s) es
matchEntryAccount :: String -> Entry -> Bool
matchEntryAccount s e = any (matchTransactionAccount s) (transactions e)
2007-02-10 20:36:50 +03:00
matchTransactionAccount :: String -> Transaction -> Bool
matchTransactionAccount s t = s `isInfixOf` (account t)