mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-08 07:09:28 +03:00
225 lines
7.7 KiB
Haskell
225 lines
7.7 KiB
Haskell
|
|
module Models -- data types & behaviours
|
|
where
|
|
|
|
import Text.Printf
|
|
import Data.List
|
|
|
|
-- types
|
|
|
|
data Ledger = Ledger {
|
|
modifier_entries :: [ModifierEntry],
|
|
periodic_entries :: [PeriodicEntry],
|
|
entries :: [Entry]
|
|
} deriving (Eq)
|
|
data ModifierEntry = ModifierEntry { -- aka "automated entry"
|
|
valueexpr :: String,
|
|
m_transactions :: [Transaction]
|
|
} deriving (Eq)
|
|
data PeriodicEntry = PeriodicEntry {
|
|
periodexpr :: String,
|
|
p_transactions :: [Transaction]
|
|
} deriving (Eq)
|
|
data Entry = Entry {
|
|
date :: Date,
|
|
status :: Status,
|
|
code :: String,
|
|
description :: String,
|
|
transactions :: [Transaction]
|
|
} deriving (Eq)
|
|
data Transaction = Transaction {
|
|
account :: Account,
|
|
amount :: Amount
|
|
} deriving (Eq)
|
|
data Amount = Amount {
|
|
currency :: String,
|
|
quantity :: Double
|
|
} deriving (Eq)
|
|
type Date = String
|
|
type Status = Bool
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
-- 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
|
|
|
|
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
|
|
firstline = (entrydesc e ++ (show t), amount t)
|
|
otherlines = map (\t -> (prependSpace $ show t, amount t)) ts
|
|
prependSpace = (replicate 32 ' ' ++)
|
|
|
|
entrydesc e = printf "%-10s %-20s " (date e) (take 20 $ description e)
|
|
|
|
|
|
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 ++ (showBalance bal')
|
|
|
|
showBalance b = printf " %10.2s" (show b)
|
|
|
|
-- 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"
|
|
|
|
-- transactions
|
|
|
|
sumTransactions :: [Transaction] -> Amount
|
|
sumTransactions ts = sum [amount t | t <- ts]
|
|
|
|
transactionsFromEntries :: [Entry] -> [Transaction]
|
|
transactionsFromEntries es = concat $ map transactions es
|
|
|
|
matchTransactionAccount :: String -> Transaction -> Bool
|
|
matchTransactionAccount s t = s `isInfixOf` (account t)
|
|
|
|
transactionsWithEntries :: [Entry] -> [(Transaction,Entry)]
|
|
transactionsWithEntries es = [(t,e) | e <- es, t <- transactions e]
|
|
|
|
showTransactionsWithBalances :: [(Transaction,Entry)] -> Amount -> String
|
|
showTransactionsWithBalances [] _ = []
|
|
showTransactionsWithBalances tes b =
|
|
unlines $ showTransactionsWithBalances' tes b
|
|
where
|
|
showTransactionsWithBalances' [] _ = []
|
|
showTransactionsWithBalances' ((t,e):rest) b =
|
|
[showTransactionWithBalance t e b'] ++ (showTransactionsWithBalances' rest b')
|
|
where b' = b + (amount t)
|
|
|
|
showTransactionWithBalance :: Transaction -> Entry -> Amount -> String
|
|
showTransactionWithBalance t e b =
|
|
(entrydesc e) ++ (show t) ++ (showBalance b)
|
|
|
|
transactionsMatching :: String -> Ledger -> [(Transaction,Entry)]
|
|
transactionsMatching s l = filter (\(t,e) -> matchTransactionAccount s t) (transactionsWithEntries $ entries l)
|
|
|
|
-- entries
|
|
|
|
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)
|
|
|
|
-- accounts
|
|
|
|
accountsFromTransactions :: [Transaction] -> [Account]
|
|
accountsFromTransactions ts = nub $ map account ts
|
|
|
|
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
|
|
|