mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-07 21:15:19 +03:00
much refactoring, get quickcheck working, beginnings of account matching
This commit is contained in:
parent
080d567f15
commit
6bf13fb262
115
Models.hs
115
Models.hs
@ -3,7 +3,9 @@ module Models -- data types & behaviours
|
||||
where
|
||||
|
||||
import Text.Printf
|
||||
import List
|
||||
import Data.List
|
||||
|
||||
-- types
|
||||
|
||||
data Ledger = Ledger {
|
||||
modifier_entries :: [ModifierEntry],
|
||||
@ -36,8 +38,8 @@ data Amount = Amount {
|
||||
type Date = String
|
||||
type Account = String
|
||||
|
||||
-- Amount arithmetic
|
||||
-- ignores currency conversion
|
||||
-- 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)
|
||||
@ -69,16 +71,8 @@ instance Show PeriodicEntry where
|
||||
|
||||
instance Show Entry where show = showEntry
|
||||
|
||||
showEntryOld :: Entry -> String
|
||||
showEntryOld e = date e ++ " " ++ s ++ c ++ d ++ "\n" ++ unlines (map show (transactions e))
|
||||
where
|
||||
d = description e
|
||||
s = case (status e) of {True -> "* "; False -> ""}
|
||||
c = case (length(code e) > 0) of {True -> (code e ++ " "); False -> ""}
|
||||
|
||||
-- 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
|
||||
-- ... ... ...
|
||||
@ -88,18 +82,22 @@ showEntryOld e = date e ++ " " ++ s ++ c ++ d ++ "\n" ++ unlines (map show (tran
|
||||
-- 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 =
|
||||
[(entrydesc ++ (show t), amount t)]
|
||||
++ map (\t -> (prependSpace $ show t, amount t)) ts
|
||||
[firstline] ++ otherlines
|
||||
where
|
||||
t:ts = transactions e
|
||||
entrydesc = printf "%-10s %-20s " (date e) (take 20 $ description e)
|
||||
prependSpace = (printf (take 32 (repeat ' ')) ++)
|
||||
firstline = (entrydesc ++ (show t), amount t)
|
||||
otherlines = map (\t -> (prependSpace $ show t, amount t)) ts
|
||||
prependSpace = (replicate 32 ' ' ++)
|
||||
|
||||
instance Show Transaction where
|
||||
show t = printf "%-25s %10s " (take 25 $ account t) (show $ amount t)
|
||||
show t = printf "%-25s %10s" (take 25 $ account t) (show $ amount t)
|
||||
|
||||
instance Show Amount where
|
||||
show (Amount cur qty) =
|
||||
@ -108,58 +106,87 @@ instance Show Amount where
|
||||
"0.00" -> "0"
|
||||
otherwise -> cur ++ roundedqty
|
||||
|
||||
showEntry :: Entry -> String
|
||||
showEntry e = unlines $ map fst (entryLines e)
|
||||
-- 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]
|
||||
|
||||
-- add balances to entry lines, given a starting balance
|
||||
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'))
|
||||
|
||||
showEntryWithBalances :: Entry -> Amount -> String
|
||||
showEntryWithBalances e b = unlines $
|
||||
[s | (s,a,b) <- entryLinesWithBalances (entryLines e) b]
|
||||
|
||||
-- show register entries, keeping a running balance
|
||||
showRegisterEntries :: [Entry] -> Amount -> String
|
||||
showRegisterEntries [] _ = ""
|
||||
showRegisterEntries (e:es) b =
|
||||
showEntryWithBalances e b ++ (showRegisterEntries es b')
|
||||
where b' = b + (sumTransactions (transactions e))
|
||||
str' = str ++ (printf " %10.2s" (show bal'))
|
||||
|
||||
-- misc
|
||||
|
||||
-- fill in missing amounts etc., as far as possible
|
||||
autofill :: Entry -> Entry
|
||||
autofill e = Entry (date e) (status e) (code e) (description e)
|
||||
(autofillTransactions (transactions e))
|
||||
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
|
||||
let (ns, as) = normalAndAutoTransactions ts in
|
||||
case (length as) of
|
||||
0 -> ns
|
||||
1 -> ns ++ [Transaction (account (head as)) (-(sumTransactions 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 =
|
||||
([t | t <- ts, (currency $ amount t) /= "AUTO"],
|
||||
[t | t <- ts, (currency $ amount t) == "AUTO"])
|
||||
normalAndAutoTransactions ts =
|
||||
partition isNormal ts
|
||||
where isNormal t = (currency $ amount t) /= "AUTO"
|
||||
|
||||
sumTransactions :: [Transaction] -> Amount
|
||||
sumTransactions ts = sum [amount t | t <- ts]
|
||||
|
||||
transactionsFrom :: [Entry] -> [Transaction]
|
||||
transactionsFrom es = concat $ map transactions es
|
||||
transactionsFromEntries :: [Entry] -> [Transaction]
|
||||
transactionsFromEntries es = concat $ map transactions es
|
||||
|
||||
accountsFrom :: [Transaction] -> [Account]
|
||||
accountsFrom ts = nub $ map account ts
|
||||
accountsFromTransactions :: [Transaction] -> [Account]
|
||||
accountsFromTransactions ts = nub $ map account ts
|
||||
|
||||
accountsUsed :: Ledger -> [Account]
|
||||
accountsUsed l = accountsFrom $ transactionsFrom $ entries l
|
||||
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)
|
||||
|
||||
matchTransactionAccount :: String -> Transaction -> Bool
|
||||
matchTransactionAccount s t = s `isInfixOf` (account t)
|
||||
|
@ -31,6 +31,6 @@ get_content (File s) = Just s
|
||||
--defaultLedgerFile = tildeExpand "~/ledger.dat"
|
||||
defaultLedgerFile = "ledger.dat"
|
||||
|
||||
ledgerFilePath :: IO String
|
||||
ledgerFilePath = do
|
||||
getLedgerFilePath :: IO String
|
||||
getLedgerFilePath = do
|
||||
getEnv "LEDGER" `catch` \_ -> return defaultLedgerFile >>= return
|
||||
|
16
Parse.hs
16
Parse.hs
@ -182,7 +182,7 @@ ledgerentry = do
|
||||
transactions <- ledgertransactions
|
||||
ledgernondatalines
|
||||
let entry = Entry date status code description transactions
|
||||
return $ autofill entry
|
||||
return $ autofillEntry entry
|
||||
|
||||
ledgerdate :: Parser String
|
||||
ledgerdate = do date <- many1 (digit <|> char '/'); many1 spacenonewline; return date
|
||||
@ -235,11 +235,15 @@ whiteSpace1 :: Parser ()
|
||||
whiteSpace1 = do space; whiteSpace
|
||||
|
||||
|
||||
-- ok, what can we do with it ?
|
||||
|
||||
printParseResult r = case r of
|
||||
Left e -> parseError e
|
||||
Right v -> print v
|
||||
-- utils
|
||||
|
||||
parseError :: (Show a) => a -> IO ()
|
||||
parseError e = do putStr "ledger parse error at "; print e
|
||||
|
||||
printParseResult :: Show v => Either ParseError v -> IO ()
|
||||
printParseResult r = case r of Left e -> parseError e
|
||||
Right v -> print v
|
||||
|
||||
parseLedgerFile :: String -> IO (Either ParseError Ledger)
|
||||
parseLedgerFile f = parseFromFile ledger f
|
||||
|
||||
|
25
TODO
25
TODO
@ -1,9 +1,24 @@
|
||||
features
|
||||
register
|
||||
account matching
|
||||
match transactions, not entries
|
||||
|
||||
$ ledger reg equi
|
||||
2007/01/01 opening balance equity:opening balan.. $-4.82 $-4.82
|
||||
2007/01/25 balance adjustment equity $91.15 $86.33
|
||||
$ hledger reg equi
|
||||
2007/01/01 opening balance assets:cash $4.82 $4.82
|
||||
equity:opening balances $-4.82 0
|
||||
2007/01/25 balance adjustment equity $91.15 $91.15
|
||||
assets:cash $-91.15 0
|
||||
description matching
|
||||
regexp matching
|
||||
|
||||
balance
|
||||
show top-level acct balance
|
||||
show per-account balances
|
||||
show top-level acct balances
|
||||
show all account balances
|
||||
|
||||
print
|
||||
matching by account/description regexp
|
||||
more directives, eg include
|
||||
read timelog files
|
||||
-p period expressions
|
||||
@ -14,13 +29,11 @@ features
|
||||
read gnucash files
|
||||
|
||||
testing
|
||||
get quickcheck working
|
||||
consider hunit dsl
|
||||
ledger regression/compatibility tests
|
||||
|
||||
environment
|
||||
cleaner option processing
|
||||
smart ledger file finding
|
||||
robust ledger file finding
|
||||
|
||||
documentation
|
||||
literate docs
|
||||
|
73
Tests.hs
73
Tests.hs
@ -202,15 +202,21 @@ ledger7 = Ledger [] []
|
||||
|
||||
-- utils
|
||||
|
||||
assertEqual' e a = assertEqual "" e a
|
||||
|
||||
parse' p ts = parse p "" ts
|
||||
|
||||
assertParseEqual :: (Show a, Eq a) => a -> (Either ParseError a) -> Assertion
|
||||
assertParseEqual expected parsed =
|
||||
case parsed of
|
||||
Left e -> parseError e
|
||||
Right v -> assertEqual " " expected v
|
||||
Right v -> assertEqual " " expected v
|
||||
|
||||
assertEqual' e a = assertEqual "" e a
|
||||
|
||||
parse' p ts = parse p "" ts
|
||||
parseEquals :: Eq a => (Either ParseError a) -> a -> Bool
|
||||
parseEquals parsed other =
|
||||
case parsed of
|
||||
Left e -> False
|
||||
Right v -> v == other
|
||||
|
||||
-- hunit tests
|
||||
|
||||
@ -229,36 +235,49 @@ parse' p ts = parse p "" ts
|
||||
-- parseTest ledger periodic_entry2_str
|
||||
-- parseLedgerFile ledgerFilePath >>= printParseResult
|
||||
|
||||
test_parse_ledgertransaction :: Assertion
|
||||
test_parse_ledgertransaction =
|
||||
test_ledgertransaction :: Assertion
|
||||
test_ledgertransaction =
|
||||
assertParseEqual transaction1 (parse' ledgertransaction transaction1_str)
|
||||
|
||||
test_parse_ledgerentry =
|
||||
test_ledgerentry =
|
||||
assertParseEqual entry1 (parse' ledgerentry entry1_str)
|
||||
|
||||
test_autofill_entry =
|
||||
test_autofillEntry =
|
||||
assertEqual'
|
||||
(Amount "$" (-47.18))
|
||||
(amount $ last $ transactions $ autofill entry1)
|
||||
(Amount "$" (-47.18))
|
||||
(amount $ last $ transactions $ autofillEntry entry1)
|
||||
|
||||
tests = TestList [
|
||||
t "test_parse_ledgertransaction" test_parse_ledgertransaction
|
||||
, t "test_parse_ledgerentry" test_parse_ledgerentry
|
||||
, t "test_autofill_entry" test_autofill_entry
|
||||
]
|
||||
where t label fn = TestLabel label $ TestCase fn
|
||||
test_expandAccounts =
|
||||
assertEqual'
|
||||
["assets","assets:cash","assets:checking","expenses","expenses:vacation"]
|
||||
(expandAccounts ["assets:cash","assets:checking","expenses:vacation"])
|
||||
|
||||
tests2 = Test.HUnit.test [
|
||||
"test1" ~: assertEqual "2 equals 2" 2 2
|
||||
]
|
||||
test_accountTree =
|
||||
assertEqual'
|
||||
["assets","assets:cash","assets:checking","equity","equity:opening balances","expenses","expenses:vacation"]
|
||||
(accountTree ledger7)
|
||||
|
||||
tests = let t l f = TestLabel l $ TestCase f in TestList
|
||||
[
|
||||
t "test_ledgertransaction" test_ledgertransaction
|
||||
, t "test_ledgerentry" test_ledgerentry
|
||||
, t "test_autofillEntry" test_autofillEntry
|
||||
, t "test_expandAccounts" test_expandAccounts
|
||||
, t "test_accountTree" test_accountTree
|
||||
]
|
||||
|
||||
tests2 = Test.HUnit.test
|
||||
[
|
||||
"test1" ~: assertEqual "2 equals 2" 2 2
|
||||
]
|
||||
|
||||
-- quickcheck properties
|
||||
|
||||
prop1 = 1 == 1
|
||||
--prop_test_parse_ledgertransaction =
|
||||
-- (Transaction "expenses:food:dining" (Amount "$" 10)) ==
|
||||
-- (parse' ledgertransaction transaction_str))
|
||||
|
||||
props = [
|
||||
prop1
|
||||
]
|
||||
props =
|
||||
[
|
||||
(parse' ledgertransaction transaction1_str) `parseEquals`
|
||||
(Transaction "expenses:food:dining" (Amount "$" 10))
|
||||
,
|
||||
(accountTree ledger7) ==
|
||||
["assets","assets:cash","assets:checking","equity","equity:opening balances","expenses","expenses:vacation"]
|
||||
]
|
||||
|
40
hledger.hs
40
hledger.hs
@ -6,8 +6,8 @@
|
||||
module Main -- almost all IO is handled here
|
||||
where
|
||||
|
||||
import System (getArgs)
|
||||
import Data.List (isPrefixOf)
|
||||
import System
|
||||
import Data.List
|
||||
import Test.HUnit (runTestTT)
|
||||
import Test.QuickCheck (quickCheck)
|
||||
import Text.ParserCombinators.Parsec (parseFromFile, ParseError)
|
||||
@ -33,32 +33,32 @@ main = do
|
||||
|
||||
test :: IO ()
|
||||
test = do
|
||||
putStrLn "hunit "
|
||||
runTestTT tests
|
||||
putStr "quickcheck "
|
||||
mapM quickCheck props
|
||||
hcounts <- runTestTT tests
|
||||
qcounts <- mapM quickCheck props
|
||||
--print $ "hunit: " ++ (showHunitCounts hcounts)
|
||||
--print $ "quickcheck: " ++ (concat $ intersperse " " $ map show qcounts)
|
||||
return ()
|
||||
where showHunitCounts c =
|
||||
reverse $ tail $ reverse ("passed " ++ (unwords $ drop 5 $ words (show c)))
|
||||
|
||||
register :: [String] -> IO ()
|
||||
register args = do
|
||||
p <- parseLedgerFile ledgerFilePath
|
||||
case p of Left e -> parseError e
|
||||
Right l -> printRegister l
|
||||
getLedgerFilePath >>= parseLedgerFile >>= doWithParsed (printRegister args)
|
||||
|
||||
balance :: [String] -> IO ()
|
||||
balance args = do
|
||||
p <- parseLedgerFile ledgerFilePath
|
||||
case p of Left e -> parseError e
|
||||
Right l -> printBalances l
|
||||
balance args =
|
||||
return ()
|
||||
|
||||
-- utils
|
||||
|
||||
parseLedgerFile :: IO String -> IO (Either ParseError Ledger)
|
||||
parseLedgerFile f = f >>= parseFromFile ledger
|
||||
-- doWithLedgerFile =
|
||||
-- getLedgerFilePath >>= parseLedgerFile >>= doWithParsed
|
||||
|
||||
printRegister :: Ledger -> IO ()
|
||||
printRegister l = putStr $ showRegisterEntries (entries l) 0
|
||||
|
||||
printBalances :: Ledger -> IO ()
|
||||
printBalances l = putStr $ showRegisterEntries (entries l) 0
|
||||
doWithParsed :: (a -> IO ()) -> (Either ParseError a) -> IO ()
|
||||
doWithParsed a p =
|
||||
case p of Left e -> parseError e
|
||||
Right v -> a v
|
||||
|
||||
printRegister :: [String] -> Ledger -> IO ()
|
||||
printRegister args ledger =
|
||||
putStr $ showEntriesWithBalances (entriesMatching (head (args ++ [""])) ledger) 0
|
||||
|
Loading…
Reference in New Issue
Block a user