refactor: clarify the roles of Journal (primary data) and Ledger (derived report)
This commit is contained in:
parent
fc330b5c9f
commit
bf5ee88a30
@ -19,42 +19,42 @@ import System.IO ( stderr, hFlush, hPutStrLn, hPutStr )
|
||||
#endif
|
||||
import System.IO.Error
|
||||
import Text.ParserCombinators.Parsec
|
||||
import Hledger.Cli.Utils (ledgerFromStringWithOpts)
|
||||
import Hledger.Cli.Utils (journalFromStringWithOpts)
|
||||
import qualified Data.Foldable as Foldable (find)
|
||||
|
||||
-- | Read ledger transactions from the terminal, prompting for each field,
|
||||
-- and append them to the ledger file. If the ledger came from stdin, this
|
||||
-- command has no effect.
|
||||
add :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
add opts args l
|
||||
| filepath (journal l) == "-" = return ()
|
||||
add :: [Opt] -> [String] -> Journal -> IO ()
|
||||
add opts args j
|
||||
| filepath j == "-" = return ()
|
||||
| otherwise = do
|
||||
hPutStrLn stderr $
|
||||
"Enter one or more transactions, which will be added to your ledger file.\n"
|
||||
++"To complete a transaction, enter . as account name. To quit, press control-c."
|
||||
today <- getCurrentDay
|
||||
getAndAddTransactions l opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)
|
||||
getAndAddTransactions j opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)
|
||||
|
||||
-- | Read a number of ledger transactions from the command line,
|
||||
-- prompting, validating, displaying and appending them to the ledger
|
||||
-- file, until end of input (then raise an EOF exception). Any
|
||||
-- command-line arguments are used as the first transaction's description.
|
||||
getAndAddTransactions :: Ledger -> [Opt] -> [String] -> Day -> IO ()
|
||||
getAndAddTransactions l opts args defaultDate = do
|
||||
(ledgerTransaction,date) <- getTransaction l opts args defaultDate
|
||||
l <- ledgerAddTransaction l ledgerTransaction
|
||||
getAndAddTransactions l opts args date
|
||||
-- | Read a number of transactions from the command line, prompting,
|
||||
-- validating, displaying and appending them to the journal file, until
|
||||
-- end of input (then raise an EOF exception). Any command-line arguments
|
||||
-- are used as the first transaction's description.
|
||||
getAndAddTransactions :: Journal -> [Opt] -> [String] -> Day -> IO ()
|
||||
getAndAddTransactions j opts args defaultDate = do
|
||||
(t, d) <- getTransaction j opts args defaultDate
|
||||
j <- journalAddTransaction j t
|
||||
getAndAddTransactions j opts args d
|
||||
|
||||
-- | Read a transaction from the command line, with history-aware prompting.
|
||||
getTransaction :: Ledger -> [Opt] -> [String] -> Day -> IO (Transaction,Day)
|
||||
getTransaction l opts args defaultDate = do
|
||||
getTransaction :: Journal -> [Opt] -> [String] -> Day -> IO (Transaction,Day)
|
||||
getTransaction j opts args defaultDate = do
|
||||
today <- getCurrentDay
|
||||
datestr <- askFor "date"
|
||||
(Just $ showDate defaultDate)
|
||||
(Just $ \s -> null s ||
|
||||
isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))
|
||||
description <- askFor "description" Nothing (Just $ not . null)
|
||||
let historymatches = transactionsSimilarTo l args description
|
||||
let historymatches = transactionsSimilarTo j args description
|
||||
bestmatch | null historymatches = Nothing
|
||||
| otherwise = Just $ snd $ head historymatches
|
||||
bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch
|
||||
@ -63,7 +63,7 @@ getTransaction l opts args defaultDate = do
|
||||
if NoNewAccts `elem` opts
|
||||
then isJust $ Foldable.find (== x) ant
|
||||
else True
|
||||
where (ant,_,_,_) = groupPostings . journalPostings . journal $ l
|
||||
where (ant,_,_,_) = groupPostings $ journalPostings j
|
||||
getpostingsandvalidate = do
|
||||
ps <- getPostings accept bestmatchpostings []
|
||||
let t = nulltransaction{tdate=date
|
||||
@ -129,30 +129,26 @@ askFor prompt def validator = do
|
||||
Nothing -> return input
|
||||
where showdef s = " [" ++ s ++ "]"
|
||||
|
||||
-- | Append this transaction to the ledger's file. Also, to the ledger's
|
||||
-- | Append this transaction to the journal's file. Also, to the journal's
|
||||
-- transaction list, but we don't bother updating the other fields - this
|
||||
-- is enough to include new transactions in the history matching.
|
||||
ledgerAddTransaction :: Ledger -> Transaction -> IO Ledger
|
||||
ledgerAddTransaction l t = do
|
||||
appendToLedgerFile l $ showTransaction t
|
||||
putStrLn $ printf "\nAdded transaction to %s:" (filepath $ journal l)
|
||||
journalAddTransaction :: Journal -> Transaction -> IO Journal
|
||||
journalAddTransaction j@Journal{jtxns=ts} t = do
|
||||
appendToJournalFile j $ showTransaction t
|
||||
putStrLn $ printf "\nAdded transaction to %s:" (filepath j)
|
||||
putStrLn =<< registerFromString (show t)
|
||||
return l{journal=rl{jtxns=ts}}
|
||||
where rl = journal l
|
||||
ts = jtxns rl ++ [t]
|
||||
return j{jtxns=ts++[t]}
|
||||
|
||||
-- | Append data to the ledger's file, ensuring proper separation from any
|
||||
-- existing data; or if the file is "-", dump it to stdout.
|
||||
appendToLedgerFile :: Ledger -> String -> IO ()
|
||||
appendToLedgerFile l s =
|
||||
-- | Append data to the journal's file, ensuring proper separation from
|
||||
-- any existing data; or if the file is "-", dump it to stdout.
|
||||
appendToJournalFile :: Journal -> String -> IO ()
|
||||
appendToJournalFile Journal{filepath=f, jtext=t} s =
|
||||
if f == "-"
|
||||
then putStr $ sep ++ s
|
||||
else appendFile f $ sep++s
|
||||
where
|
||||
f = filepath $ journal l
|
||||
-- XXX we are looking at the original raw text from when the ledger
|
||||
-- was first read, but that's good enough for now
|
||||
t = jtext $ journal l
|
||||
sep | null $ strip t = ""
|
||||
| otherwise = replicate (2 - min 2 (length lastnls)) '\n'
|
||||
where lastnls = takeWhile (=='\n') $ reverse t
|
||||
@ -161,7 +157,7 @@ appendToLedgerFile l s =
|
||||
registerFromString :: String -> IO String
|
||||
registerFromString s = do
|
||||
now <- getCurrentLocalTime
|
||||
l <- ledgerFromStringWithOpts [] s
|
||||
l <- journalFromStringWithOpts [] s
|
||||
return $ showRegisterReport opts (optsToFilterSpec opts [] now) l
|
||||
where opts = [Empty]
|
||||
|
||||
@ -184,19 +180,19 @@ wordLetterPairs = concatMap letterPairs . words
|
||||
letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)
|
||||
letterPairs _ = []
|
||||
|
||||
compareLedgerDescriptions :: [Char] -> [Char] -> Double
|
||||
compareLedgerDescriptions s t = compareStrings s' t'
|
||||
compareDescriptions :: [Char] -> [Char] -> Double
|
||||
compareDescriptions s t = compareStrings s' t'
|
||||
where s' = simplify s
|
||||
t' = simplify t
|
||||
simplify = filter (not . (`elem` "0123456789"))
|
||||
|
||||
transactionsSimilarTo :: Ledger -> [String] -> String -> [(Double,Transaction)]
|
||||
transactionsSimilarTo l apats s =
|
||||
transactionsSimilarTo :: Journal -> [String] -> String -> [(Double,Transaction)]
|
||||
transactionsSimilarTo j apats s =
|
||||
sortBy compareRelevanceAndRecency
|
||||
$ filter ((> threshold).fst)
|
||||
[(compareLedgerDescriptions s $ tdescription t, t) | t <- ts]
|
||||
[(compareDescriptions s $ tdescription t, t) | t <- ts]
|
||||
where
|
||||
compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)
|
||||
ts = jtxns $ filterJournalTransactionsByAccount apats $ journal l
|
||||
ts = jtxns $ filterJournalTransactionsByAccount apats j
|
||||
threshold = 0
|
||||
|
||||
|
||||
@ -111,27 +111,27 @@ import System.IO.UTF8
|
||||
|
||||
|
||||
-- | Print a balance report.
|
||||
balance :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
balance opts args l = do
|
||||
balance :: [Opt] -> [String] -> Journal -> IO ()
|
||||
balance opts args j = do
|
||||
t <- getCurrentLocalTime
|
||||
putStr $ showBalanceReport opts (optsToFilterSpec opts args t) l
|
||||
putStr $ showBalanceReport opts (optsToFilterSpec opts args t) j
|
||||
|
||||
-- | Generate a balance report with the specified options for this ledger.
|
||||
showBalanceReport :: [Opt] -> FilterSpec -> Ledger -> String
|
||||
showBalanceReport opts filterspec l = acctsstr ++ totalstr
|
||||
showBalanceReport :: [Opt] -> FilterSpec -> Journal -> String
|
||||
showBalanceReport opts filterspec j = acctsstr ++ totalstr
|
||||
where
|
||||
l' = filterAndCacheLedger filterspec l
|
||||
l = journalToLedger filterspec j
|
||||
acctsstr = unlines $ map showacct interestingaccts
|
||||
where
|
||||
showacct = showInterestingAccount l' interestingaccts
|
||||
interestingaccts = filter (isInteresting opts l') acctnames
|
||||
showacct = showInterestingAccount l interestingaccts
|
||||
interestingaccts = filter (isInteresting opts l) acctnames
|
||||
acctnames = sort $ tail $ flatten $ treemap aname accttree
|
||||
accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l'
|
||||
accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l
|
||||
totalstr | NoTotal `elem` opts = ""
|
||||
| notElem Empty opts && isZeroMixedAmount total = ""
|
||||
| otherwise = printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmountWithoutPrice total
|
||||
where
|
||||
total = sum $ map abalance $ ledgerTopAccounts l'
|
||||
total = sum $ map abalance $ ledgerTopAccounts l
|
||||
|
||||
-- | Display one line of the balance report with appropriate indenting and eliding.
|
||||
showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String
|
||||
|
||||
@ -24,10 +24,10 @@ import Data.List
|
||||
import Safe (readDef)
|
||||
|
||||
-- | Generate an image with the pie chart and write it to a file
|
||||
chart :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
chart opts args l = do
|
||||
chart :: [Opt] -> [String] -> Journal -> IO ()
|
||||
chart opts args j = do
|
||||
t <- getCurrentLocalTime
|
||||
let chart = genPie opts (optsToFilterSpec opts args t) l
|
||||
let chart = genPie opts (optsToFilterSpec opts args t) j
|
||||
renderableToPNGFile (toRenderable chart) w h filename
|
||||
where
|
||||
filename = getOption opts ChartOutput chartoutput
|
||||
@ -48,8 +48,8 @@ parseSize str = (read w, read h)
|
||||
(w,_:h) = splitAt x str
|
||||
|
||||
-- | Generate pie chart
|
||||
genPie :: [Opt] -> FilterSpec -> Ledger -> PieLayout
|
||||
genPie opts filterspec l = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white
|
||||
genPie :: [Opt] -> FilterSpec -> Journal -> PieLayout
|
||||
genPie opts filterspec j = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white
|
||||
, pie_plot_ = pie_chart }
|
||||
where
|
||||
pie_chart = defaultPieChart { pie_data_ = map (uncurry accountPieItem) chartitems'
|
||||
@ -60,7 +60,7 @@ genPie opts filterspec l = defaultPieLayout { pie_background_ = solidFillStyle $
|
||||
chartitems' = debug "chart" $ top num samesignitems
|
||||
(samesignitems, sign) = sameSignNonZero rawitems
|
||||
rawitems = debug "raw" $ flatten $ balances $
|
||||
ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) $ cacheLedger'' filterspec l
|
||||
ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) $ journalToLedger filterspec j
|
||||
top n t = topn ++ [other]
|
||||
where
|
||||
(topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t
|
||||
|
||||
@ -6,7 +6,7 @@ format, and print it on stdout. See the manual for more details.
|
||||
module Hledger.Cli.Commands.Convert where
|
||||
import Hledger.Cli.Options (Opt(Debug))
|
||||
import Hledger.Cli.Version (versionstr)
|
||||
import Hledger.Data.Types (Ledger,AccountName,Transaction(..),Posting(..),PostingType(..))
|
||||
import Hledger.Data.Types (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))
|
||||
import Hledger.Data.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)
|
||||
import Hledger.Data.Parse (someamount, emptyCtx, ledgeraccountname)
|
||||
import Hledger.Data.Amount (nullmixedamt)
|
||||
@ -66,9 +66,9 @@ type AccountRule = (
|
||||
type CsvRecord = [String]
|
||||
|
||||
|
||||
-- | Read the CSV file named as an argument and print equivalent ledger transactions,
|
||||
-- | Read the CSV file named as an argument and print equivalent journal transactions,
|
||||
-- using/creating a .rules file.
|
||||
convert :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
convert :: [Opt] -> [String] -> Journal -> IO ()
|
||||
convert opts args _ = do
|
||||
when (null args) $ error "please specify a csv data file."
|
||||
let csvfile = head args
|
||||
|
||||
@ -19,23 +19,23 @@ barchar = '*'
|
||||
|
||||
-- | Print a histogram of some statistic per reporting interval, such as
|
||||
-- number of postings per day.
|
||||
histogram :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
histogram opts args l = do
|
||||
histogram :: [Opt] -> [String] -> Journal -> IO ()
|
||||
histogram opts args j = do
|
||||
t <- getCurrentLocalTime
|
||||
putStr $ showHistogram opts (optsToFilterSpec opts args t) l
|
||||
putStr $ showHistogram opts (optsToFilterSpec opts args t) j
|
||||
|
||||
showHistogram :: [Opt] -> FilterSpec -> Ledger -> String
|
||||
showHistogram opts filterspec l = concatMap (printDayWith countBar) dayps
|
||||
showHistogram :: [Opt] -> FilterSpec -> Journal -> String
|
||||
showHistogram opts filterspec j = concatMap (printDayWith countBar) dayps
|
||||
where
|
||||
i = intervalFromOpts opts
|
||||
interval | i == NoInterval = Daily
|
||||
| otherwise = i
|
||||
fullspan = journalDateSpan $ journal l
|
||||
fullspan = journalDateSpan j
|
||||
days = filter (DateSpan Nothing Nothing /=) $ splitSpan interval fullspan
|
||||
dayps = [(s, filter (isPostingInDateSpan s) ps) | s <- days]
|
||||
-- same as Register
|
||||
-- should count transactions, not postings ?
|
||||
ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ ledgerPostings l
|
||||
ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j
|
||||
filterempties
|
||||
| Empty `elem` opts = id
|
||||
| otherwise = filter (not . isZeroMixedAmount . pamount)
|
||||
|
||||
@ -16,14 +16,14 @@ import System.IO.UTF8
|
||||
|
||||
|
||||
-- | Print ledger transactions in standard format.
|
||||
print' :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
print' opts args l = do
|
||||
print' :: [Opt] -> [String] -> Journal -> IO ()
|
||||
print' opts args j = do
|
||||
t <- getCurrentLocalTime
|
||||
putStr $ showTransactions (optsToFilterSpec opts args t) l
|
||||
putStr $ showTransactions (optsToFilterSpec opts args t) j
|
||||
|
||||
showTransactions :: FilterSpec -> Ledger -> String
|
||||
showTransactions filterspec l =
|
||||
showTransactions :: FilterSpec -> Journal -> String
|
||||
showTransactions filterspec j =
|
||||
concatMap (showTransactionForPrint effective) $ sortBy (comparing tdate) txns
|
||||
where
|
||||
effective = EffectiveDate == whichdate filterspec
|
||||
txns = jtxns $ filterJournalTransactions filterspec $ journal l
|
||||
txns = jtxns $ filterJournalTransactions filterspec j
|
||||
|
||||
@ -22,21 +22,21 @@ import System.IO.UTF8
|
||||
|
||||
|
||||
-- | Print a register report.
|
||||
register :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
register opts args l = do
|
||||
register :: [Opt] -> [String] -> Journal -> IO ()
|
||||
register opts args j = do
|
||||
t <- getCurrentLocalTime
|
||||
putStr $ showRegisterReport opts (optsToFilterSpec opts args t) l
|
||||
putStr $ showRegisterReport opts (optsToFilterSpec opts args t) j
|
||||
|
||||
-- | Generate the register report, which is a list of postings with transaction
|
||||
-- info and a running balance.
|
||||
showRegisterReport :: [Opt] -> FilterSpec -> Ledger -> String
|
||||
showRegisterReport opts filterspec l = showPostingsWithBalance ps nullposting startbal
|
||||
showRegisterReport :: [Opt] -> FilterSpec -> Journal -> String
|
||||
showRegisterReport opts filterspec j = showPostingsWithBalance ps nullposting startbal
|
||||
where
|
||||
ps | interval == NoInterval = displayableps
|
||||
| otherwise = summarisePostings interval depth empty filterspan displayableps
|
||||
startbal = sumPostings precedingps
|
||||
(precedingps,displayableps,_) =
|
||||
postingsMatchingDisplayExpr (displayExprFromOpts opts) $ journalPostings $ filterJournalPostings filterspec $ journal l
|
||||
postingsMatchingDisplayExpr (displayExprFromOpts opts) $ journalPostings $ filterJournalPostings filterspec j
|
||||
(interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts)
|
||||
filterspan = datespan filterspec
|
||||
|
||||
@ -99,7 +99,7 @@ summarisePostingsInDateSpan (DateSpan b e) depth showempty ps
|
||||
summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps
|
||||
summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames]
|
||||
anames = sort $ nub $ map paccount ps
|
||||
-- aggregate balances by account, like cacheLedger, then do depth-clipping
|
||||
-- aggregate balances by account, like journalToLedger, then do depth-clipping
|
||||
(_,_,exclbalof,inclbalof) = groupPostings ps
|
||||
clippedanames = nub $ map (clipAccountName d) anames
|
||||
isclipped a = accountNameLevel a >= d
|
||||
|
||||
@ -17,10 +17,10 @@ import qualified Data.Map as Map
|
||||
|
||||
|
||||
-- | Print various statistics for the ledger.
|
||||
stats :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
stats opts args l = do
|
||||
stats :: [Opt] -> [String] -> Journal -> IO ()
|
||||
stats opts args j = do
|
||||
today <- getCurrentDay
|
||||
putStr $ showStats opts args (filterAndCacheLedger nullfilterspec l) today
|
||||
putStr $ showStats opts args (journalToLedger nullfilterspec j) today
|
||||
|
||||
showStats :: [Opt] -> [String] -> Ledger -> Day -> String
|
||||
showStats _ _ l today =
|
||||
|
||||
@ -27,7 +27,7 @@ data AppState = AppState {
|
||||
,amsg :: String -- ^ status message
|
||||
,aopts :: [Opt] -- ^ command-line opts
|
||||
,aargs :: [String] -- ^ command-line args at startup
|
||||
,aledger :: Ledger -- ^ parsed ledger
|
||||
,ajournal :: Journal -- ^ parsed journal
|
||||
,abuf :: [String] -- ^ lines of the current buffered view
|
||||
,alocs :: [Loc] -- ^ user's navigation trail within the UI
|
||||
-- ^ never null, head is current location
|
||||
@ -49,8 +49,8 @@ data Screen = BalanceScreen -- ^ like hledger balance, shows accounts
|
||||
deriving (Eq,Show)
|
||||
|
||||
-- | Run the vty (curses-style) ui.
|
||||
vty :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
vty opts args l = do
|
||||
vty :: [Opt] -> [String] -> Journal -> IO ()
|
||||
vty opts args j = do
|
||||
v <- mkVty
|
||||
DisplayRegion w h <- display_bounds $ terminal v
|
||||
let opts' = SubTotal:opts
|
||||
@ -63,7 +63,7 @@ vty opts args l = do
|
||||
,amsg=helpmsg
|
||||
,aopts=opts'
|
||||
,aargs=args
|
||||
,aledger=l
|
||||
,ajournal=j
|
||||
,abuf=[]
|
||||
,alocs=[]
|
||||
}
|
||||
@ -227,11 +227,11 @@ resetTrailAndEnter t scr a = enter t scr (aargs a) $ clearLocs a
|
||||
|
||||
-- | Regenerate the display data appropriate for the current screen.
|
||||
updateData :: LocalTime -> AppState -> AppState
|
||||
updateData t a@AppState{aopts=opts,aledger=l} =
|
||||
updateData t a@AppState{aopts=opts,ajournal=j} =
|
||||
case screen a of
|
||||
BalanceScreen -> a{abuf=lines $ showBalanceReport opts fspec l}
|
||||
RegisterScreen -> a{abuf=lines $ showRegisterReport opts fspec l}
|
||||
PrintScreen -> a{abuf=lines $ showTransactions fspec l}
|
||||
BalanceScreen -> a{abuf=lines $ showBalanceReport opts fspec j}
|
||||
RegisterScreen -> a{abuf=lines $ showRegisterReport opts fspec j}
|
||||
PrintScreen -> a{abuf=lines $ showTransactions fspec j}
|
||||
where fspec = optsToFilterSpec opts (currentArgs a) t
|
||||
|
||||
backout :: LocalTime -> AppState -> AppState
|
||||
@ -285,9 +285,9 @@ scrollToTransaction (Just t) a@AppState{abuf=buf} = setCursorY cy $ setScrollY s
|
||||
-- the cursor on the register screen (or best guess). Results undefined
|
||||
-- while on other screens.
|
||||
currentTransaction :: AppState -> Maybe Transaction
|
||||
currentTransaction a@AppState{aledger=l,abuf=buf} = ptransaction p
|
||||
currentTransaction a@AppState{ajournal=j,abuf=buf} = ptransaction p
|
||||
where
|
||||
p = headDef nullposting $ filter ismatch $ ledgerPostings l
|
||||
p = headDef nullposting $ filter ismatch $ journalPostings j
|
||||
ismatch p = postingDate p == parsedate (take 10 datedesc)
|
||||
&& take 70 (showPostingWithBalance False p nullmixedamt) == (datedesc ++ acctamt)
|
||||
datedesc = take 32 $ fromMaybe "" $ find (not . (" " `isPrefixOf`)) $ headDef "" rest : reverse above
|
||||
|
||||
@ -38,7 +38,7 @@ import Network.Loli.Utils (update)
|
||||
import HSP hiding (Request,catch)
|
||||
import qualified HSP (Request(..))
|
||||
|
||||
import Hledger.Cli.Commands.Add (ledgerAddTransaction)
|
||||
import Hledger.Cli.Commands.Add (journalAddTransaction)
|
||||
import Hledger.Cli.Commands.Balance
|
||||
import Hledger.Cli.Commands.Histogram
|
||||
import Hledger.Cli.Commands.Print
|
||||
@ -60,22 +60,22 @@ tcpport = 5000 :: Int
|
||||
homeurl = printf "http://localhost:%d/" tcpport
|
||||
browserdelay = 100000 -- microseconds
|
||||
|
||||
web :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
web opts args l = do
|
||||
web :: [Opt] -> [String] -> Journal -> IO ()
|
||||
web opts args j = do
|
||||
unless (Debug `elem` opts) $ forkIO browser >> return ()
|
||||
server opts args l
|
||||
server opts args j
|
||||
|
||||
browser :: IO ()
|
||||
browser = putStrLn "starting web browser" >> threadDelay browserdelay >> openBrowserOn homeurl >> return ()
|
||||
|
||||
server :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
server opts args l =
|
||||
server :: [Opt] -> [String] -> Journal -> IO ()
|
||||
server opts args j =
|
||||
-- server initialisation
|
||||
withStore "hledger" $ do -- IO ()
|
||||
printf "starting web server on port %d\n" tcpport
|
||||
t <- getCurrentLocalTime
|
||||
webfiles <- getDataFileName "web"
|
||||
putValue "hledger" "ledger" l
|
||||
putValue "hledger" "journal" j
|
||||
#ifdef WEBHAPPSTACK
|
||||
hostname <- readProcess "hostname" [] "" `catch` \_ -> return "hostname"
|
||||
runWithConfig (ServerConf tcpport hostname) $ -- (Env -> IO Response) -> IO ()
|
||||
@ -88,18 +88,18 @@ server opts args l =
|
||||
p = intercalate "+" $ reqparam env "p"
|
||||
opts' = opts ++ [Period p]
|
||||
args' = args ++ (map urlDecode $ words a)
|
||||
l' <- fromJust `fmap` getValue "hledger" "ledger"
|
||||
l'' <- reloadIfChanged opts' args' l'
|
||||
j' <- fromJust `fmap` getValue "hledger" "journal"
|
||||
j'' <- journalReloadIfChanged opts' args' j'
|
||||
-- declare path-specific request handlers
|
||||
let command :: [String] -> ([Opt] -> FilterSpec -> Ledger -> String) -> AppUnit
|
||||
command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) l''
|
||||
let command :: [String] -> ([Opt] -> FilterSpec -> Journal -> String) -> AppUnit
|
||||
command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) j''
|
||||
(loli $ -- State Loli () -> (Env -> IO Response)
|
||||
do
|
||||
get "/balance" $ command [] showBalanceReport -- String -> ReaderT Env (StateT Response IO) () -> State Loli ()
|
||||
get "/register" $ command [] showRegisterReport
|
||||
get "/histogram" $ command [] showHistogram
|
||||
get "/transactions" $ ledgerpage [] l'' (showTransactions (optsToFilterSpec opts' args' t))
|
||||
post "/transactions" $ handleAddform l''
|
||||
get "/transactions" $ ledgerpage [] j'' (showTransactions (optsToFilterSpec opts' args' t))
|
||||
post "/transactions" $ handleAddform j''
|
||||
get "/env" $ getenv >>= (text . show)
|
||||
get "/params" $ getenv >>= (text . show . Hack.Contrib.Request.params)
|
||||
get "/inputs" $ getenv >>= (text . show . Hack.Contrib.Request.inputs)
|
||||
@ -118,41 +118,33 @@ reqparam env p = map snd $ filter ((==p).fst) $ Hack.Contrib.Request.params env
|
||||
reqparam env p = map (decodeString.snd) $ filter ((==p).fst) $ Hack.Contrib.Request.params env
|
||||
#endif
|
||||
|
||||
ledgerFileModifiedTime :: Ledger -> IO ClockTime
|
||||
ledgerFileModifiedTime l
|
||||
| null path = getClockTime
|
||||
| otherwise = getModificationTime path `Prelude.catch` \_ -> getClockTime
|
||||
where path = filepath $ journal l
|
||||
|
||||
ledgerFileReadTime :: Ledger -> ClockTime
|
||||
ledgerFileReadTime l = filereadtime $ journal l
|
||||
|
||||
reload :: Ledger -> IO Ledger
|
||||
reload l = do
|
||||
l' <- readLedger (filepath $ journal l)
|
||||
putValue "hledger" "ledger" l'
|
||||
return l'
|
||||
|
||||
reloadIfChanged :: [Opt] -> [String] -> Ledger -> IO Ledger
|
||||
reloadIfChanged opts _ l = do
|
||||
tmod <- ledgerFileModifiedTime l
|
||||
let tread = ledgerFileReadTime l
|
||||
newer = diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)
|
||||
journalReloadIfChanged :: [Opt] -> [String] -> Journal -> IO Journal
|
||||
journalReloadIfChanged opts _ j@Journal{filepath=f,filereadtime=tread} = do
|
||||
tmod <- journalFileModifiedTime j
|
||||
let newer = diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)
|
||||
-- when (Debug `elem` opts) $ printf "checking file, last modified %s, last read %s, %s\n" (show tmod) (show tread) (show newer)
|
||||
if newer
|
||||
then do
|
||||
when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" (filepath $ journal l)
|
||||
reload l
|
||||
else return l
|
||||
when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" f
|
||||
reload j
|
||||
else return j
|
||||
|
||||
-- refilter :: [Opt] -> [String] -> Ledger -> LocalTime -> IO Ledger
|
||||
-- refilter opts args l t = return $ filterAndCacheLedgerWithOpts opts args t (jtext $ journal l) (journal l)
|
||||
journalFileModifiedTime :: Journal -> IO ClockTime
|
||||
journalFileModifiedTime Journal{filepath=f}
|
||||
| null f = getClockTime
|
||||
| otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime
|
||||
|
||||
ledgerpage :: [String] -> Ledger -> (Ledger -> String) -> AppUnit
|
||||
ledgerpage msgs l f = do
|
||||
reload :: Journal -> IO Journal
|
||||
reload Journal{filepath=f} = do
|
||||
j' <- readJournal f
|
||||
putValue "hledger" "journal" j'
|
||||
return j'
|
||||
|
||||
ledgerpage :: [String] -> Journal -> (Journal -> String) -> AppUnit
|
||||
ledgerpage msgs j f = do
|
||||
env <- getenv
|
||||
l' <- io $ reloadIfChanged [] [] l
|
||||
hsp msgs $ const <div><% addform env %><pre><% f l' %></pre></div>
|
||||
j' <- io $ journalReloadIfChanged [] [] j
|
||||
hsp msgs $ const <div><% addform env %><pre><% f j' %></pre></div>
|
||||
|
||||
-- | A loli directive to serve a string in pre tags within the hledger web
|
||||
-- layout.
|
||||
@ -305,8 +297,8 @@ transactionfields n env = do
|
||||
acctvar = numbered "acct"
|
||||
amtvar = numbered "amt"
|
||||
|
||||
handleAddform :: Ledger -> AppUnit
|
||||
handleAddform l = do
|
||||
handleAddform :: Journal -> AppUnit
|
||||
handleAddform j = do
|
||||
env <- getenv
|
||||
d <- io getCurrentDay
|
||||
t <- io getCurrentLocalTime
|
||||
@ -380,8 +372,8 @@ handleAddform l = do
|
||||
handle :: LocalTime -> Failing Transaction -> AppUnit
|
||||
handle _ (Failure errs) = hsp errs addform
|
||||
handle ti (Success t) = do
|
||||
io $ ledgerAddTransaction l t >> reload l
|
||||
ledgerpage [msg] l (showTransactions (optsToFilterSpec [] [] ti))
|
||||
io $ journalAddTransaction j t >> reload j
|
||||
ledgerpage [msg] j (showTransactions (optsToFilterSpec [] [] ti))
|
||||
where msg = printf "Added transaction:\n%s" (show t)
|
||||
|
||||
nbsp :: XML
|
||||
|
||||
@ -20,8 +20,8 @@ You can use the command line:
|
||||
or ghci:
|
||||
|
||||
> $ ghci hledger
|
||||
> > l <- readLedger "sample.ledger"
|
||||
> > register [] ["income","expenses"] l
|
||||
> > j <- readJournal "data/sample.journal"
|
||||
> > register [] ["income","expenses"] j
|
||||
> 2008/01/01 income income:salary $-1 $-1
|
||||
> 2008/06/01 gift income:gifts $-1 $-2
|
||||
> 2008/06/03 eat & shop expenses:food $1 $-1
|
||||
@ -48,7 +48,7 @@ import Hledger.Cli.Commands.All
|
||||
import Hledger.Data
|
||||
import Hledger.Cli.Options
|
||||
import Hledger.Cli.Tests
|
||||
import Hledger.Cli.Utils (withLedgerDo)
|
||||
import Hledger.Cli.Utils (withJournalDo)
|
||||
import Hledger.Cli.Version (versionmsg, binaryfilename)
|
||||
|
||||
main :: IO ()
|
||||
@ -60,21 +60,21 @@ main = do
|
||||
| Help `elem` opts = putStr usage
|
||||
| Version `elem` opts = putStrLn versionmsg
|
||||
| BinaryFilename `elem` opts = putStrLn binaryfilename
|
||||
| cmd `isPrefixOf` "balance" = withLedgerDo opts args cmd balance
|
||||
| cmd `isPrefixOf` "convert" = withLedgerDo opts args cmd convert
|
||||
| cmd `isPrefixOf` "print" = withLedgerDo opts args cmd print'
|
||||
| cmd `isPrefixOf` "register" = withLedgerDo opts args cmd register
|
||||
| cmd `isPrefixOf` "histogram" = withLedgerDo opts args cmd histogram
|
||||
| cmd `isPrefixOf` "add" = withLedgerDo opts args cmd add
|
||||
| cmd `isPrefixOf` "stats" = withLedgerDo opts args cmd stats
|
||||
| cmd `isPrefixOf` "balance" = withJournalDo opts args cmd balance
|
||||
| cmd `isPrefixOf` "convert" = withJournalDo opts args cmd convert
|
||||
| cmd `isPrefixOf` "print" = withJournalDo opts args cmd print'
|
||||
| cmd `isPrefixOf` "register" = withJournalDo opts args cmd register
|
||||
| cmd `isPrefixOf` "histogram" = withJournalDo opts args cmd histogram
|
||||
| cmd `isPrefixOf` "add" = withJournalDo opts args cmd add
|
||||
| cmd `isPrefixOf` "stats" = withJournalDo opts args cmd stats
|
||||
#ifdef VTY
|
||||
| cmd `isPrefixOf` "vty" = withLedgerDo opts args cmd vty
|
||||
| cmd `isPrefixOf` "vty" = withJournalDo opts args cmd vty
|
||||
#endif
|
||||
#if defined(WEB) || defined(WEBHAPPSTACK)
|
||||
| cmd `isPrefixOf` "web" = withLedgerDo opts args cmd web
|
||||
| cmd `isPrefixOf` "web" = withJournalDo opts args cmd web
|
||||
#endif
|
||||
#ifdef CHART
|
||||
| cmd `isPrefixOf` "chart" = withLedgerDo opts args cmd chart
|
||||
| cmd `isPrefixOf` "chart" = withJournalDo opts args cmd chart
|
||||
#endif
|
||||
| cmd `isPrefixOf` "test" = runtests opts args >> return ()
|
||||
| otherwise = putStr usage
|
||||
|
||||
@ -228,9 +228,9 @@ usingTimeProgramName = do
|
||||
progname <- getProgName
|
||||
return $ map toLower progname == timeprogname
|
||||
|
||||
-- | Get the ledger file path from options, an environment variable, or a default
|
||||
ledgerFilePathFromOpts :: [Opt] -> IO String
|
||||
ledgerFilePathFromOpts opts = do
|
||||
-- | Get the journal file path from options, an environment variable, or a default
|
||||
journalFilePathFromOpts :: [Opt] -> IO String
|
||||
journalFilePathFromOpts opts = do
|
||||
istimequery <- usingTimeProgramName
|
||||
f <- if istimequery then myTimelogPath else myLedgerPath
|
||||
return $ last $ f : optValuesForConstructor File opts
|
||||
|
||||
@ -237,14 +237,14 @@ tests = TestList [
|
||||
,""
|
||||
]
|
||||
let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment
|
||||
showBalanceReport [] nullfilterspec nullledger{journal=j'} `is`
|
||||
showBalanceReport [] nullfilterspec j' `is`
|
||||
unlines
|
||||
[" $500 a:b"
|
||||
," $-500 c:d"
|
||||
]
|
||||
|
||||
,"balance report elides zero-balance root account(s)" ~: do
|
||||
l <- ledgerFromStringWithOpts []
|
||||
l <- journalFromStringWithOpts []
|
||||
(unlines
|
||||
["2008/1/1 one"
|
||||
," test:a 1"
|
||||
@ -282,9 +282,6 @@ tests = TestList [
|
||||
Right e' -> (pamount $ last $ tpostings e')
|
||||
Left _ -> error "should not happen")
|
||||
|
||||
-- ,"cacheLedger" ~:
|
||||
-- length (Map.keys $ accountmap $ cacheLedger journal7) `is` 15
|
||||
|
||||
,"journalCanonicaliseAmounts" ~:
|
||||
"use the greatest precision" ~:
|
||||
(map precision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2]
|
||||
@ -471,7 +468,7 @@ tests = TestList [
|
||||
,"register report with cleared option" ~:
|
||||
do
|
||||
let opts = [Cleared]
|
||||
l <- ledgerFromStringWithOpts opts sample_ledger_str
|
||||
l <- journalFromStringWithOpts opts sample_ledger_str
|
||||
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
||||
["2008/06/03 eat & shop expenses:food $1 $1"
|
||||
," expenses:supplies $1 $2"
|
||||
@ -483,7 +480,7 @@ tests = TestList [
|
||||
,"register report with uncleared option" ~:
|
||||
do
|
||||
let opts = [UnCleared]
|
||||
l <- ledgerFromStringWithOpts opts sample_ledger_str
|
||||
l <- journalFromStringWithOpts opts sample_ledger_str
|
||||
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
||||
["2008/01/01 income assets:bank:checking $1 $1"
|
||||
," income:salary $-1 0"
|
||||
@ -495,7 +492,7 @@ tests = TestList [
|
||||
|
||||
,"register report sorts by date" ~:
|
||||
do
|
||||
l <- ledgerFromStringWithOpts [] $ unlines
|
||||
l <- journalFromStringWithOpts [] $ unlines
|
||||
["2008/02/02 a"
|
||||
," b 1"
|
||||
," c"
|
||||
@ -580,14 +577,14 @@ tests = TestList [
|
||||
,"show hours" ~: show (hours 1) ~?= "1.0h"
|
||||
|
||||
,"unicode in balance layout" ~: do
|
||||
l <- ledgerFromStringWithOpts []
|
||||
l <- journalFromStringWithOpts []
|
||||
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
||||
showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
||||
[" -100 актив:наличные"
|
||||
," 100 расходы:покупки"]
|
||||
|
||||
,"unicode in register layout" ~: do
|
||||
l <- ledgerFromStringWithOpts []
|
||||
l <- journalFromStringWithOpts []
|
||||
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
||||
showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
||||
["2009/01/01 медвежья шкура расходы:покупки 100 100"
|
||||
@ -629,7 +626,7 @@ tests = TestList [
|
||||
-- "next january" `gives` "2009/01/01"
|
||||
|
||||
,"subAccounts" ~: do
|
||||
l <- liftM (filterAndCacheLedger nullfilterspec) sampleledger
|
||||
l <- liftM (journalToLedger nullfilterspec) sampleledger
|
||||
let a = ledgerAccount l "assets"
|
||||
map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"]
|
||||
|
||||
@ -676,8 +673,8 @@ tests = TestList [
|
||||
date1 = parsedate "2008/11/26"
|
||||
t1 = LocalTime date1 midday
|
||||
|
||||
sampleledger = ledgerFromStringWithOpts [] sample_ledger_str
|
||||
sampleledgerwithopts opts _ = ledgerFromStringWithOpts opts sample_ledger_str
|
||||
sampleledger = journalFromStringWithOpts [] sample_ledger_str
|
||||
sampleledgerwithopts opts _ = journalFromStringWithOpts opts sample_ledger_str
|
||||
|
||||
sample_ledger_str = unlines
|
||||
["; A sample ledger file."
|
||||
@ -1050,7 +1047,7 @@ journal7 = Journal
|
||||
(TOD 0 0)
|
||||
""
|
||||
|
||||
ledger7 = filterAndCacheLedger nullfilterspec $ makeUncachedLedger journal7
|
||||
ledger7 = journalToLedger nullfilterspec journal7
|
||||
|
||||
ledger8_str = unlines
|
||||
["2008/1/1 test "
|
||||
|
||||
@ -7,10 +7,15 @@ Hledger.Data.Utils.
|
||||
-}
|
||||
|
||||
module Hledger.Cli.Utils
|
||||
(
|
||||
withJournalDo,
|
||||
journalFromStringWithOpts,
|
||||
openBrowserOn
|
||||
)
|
||||
where
|
||||
import Control.Monad.Error
|
||||
import Hledger.Data
|
||||
import Hledger.Cli.Options (Opt(..),ledgerFilePathFromOpts) -- ,optsToFilterSpec)
|
||||
import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)
|
||||
import System.Directory (doesFileExist)
|
||||
import System.IO (stderr)
|
||||
#if __GLASGOW_HASKELL__ <= 610
|
||||
@ -23,42 +28,29 @@ import System.Process (readProcessWithExitCode)
|
||||
import System.Info (os)
|
||||
|
||||
|
||||
-- | Parse the user's specified ledger file and run a hledger command on
|
||||
-- | Parse the user's specified journal file and run a hledger command on
|
||||
-- it, or report a parse error. This function makes the whole thing go.
|
||||
-- The command will receive an uncached/unfiltered ledger, so should
|
||||
-- process it further if needed.
|
||||
withLedgerDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> UncachedLedger -> IO ()) -> IO ()
|
||||
withLedgerDo opts args cmdname cmd = do
|
||||
withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()
|
||||
withJournalDo opts args cmdname cmd = do
|
||||
-- We kludgily read the file before parsing to grab the full text, unless
|
||||
-- it's stdin, or it doesn't exist and we are adding. We read it strictly
|
||||
-- to let the add command work.
|
||||
f <- ledgerFilePathFromOpts opts
|
||||
f <- journalFilePathFromOpts opts
|
||||
fileexists <- doesFileExist f
|
||||
let creating = not fileexists && cmdname == "add"
|
||||
cost = CostBasis `elem` opts
|
||||
let runcmd = cmd opts args . makeUncachedLedger . (if cost then journalConvertAmountsToCost else id)
|
||||
costify = (if CostBasis `elem` opts then journalConvertAmountsToCost else id)
|
||||
runcmd = cmd opts args . costify
|
||||
if creating
|
||||
then runcmd nulljournal
|
||||
else (runErrorT . parseJournalFile) f >>= either parseerror runcmd
|
||||
where parseerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1)
|
||||
|
||||
-- | Get an uncached ledger from the given string and options, or raise an error.
|
||||
ledgerFromStringWithOpts :: [Opt] -> String -> IO UncachedLedger
|
||||
ledgerFromStringWithOpts opts s = do
|
||||
-- | Get a journal from the given string and options, or throw an error.
|
||||
journalFromStringWithOpts :: [Opt] -> String -> IO Journal
|
||||
journalFromStringWithOpts opts s = do
|
||||
j <- journalFromString s
|
||||
let cost = CostBasis `elem` opts
|
||||
return $ makeUncachedLedger $ (if cost then journalConvertAmountsToCost else id) j
|
||||
|
||||
-- -- | Read a ledger from the given file, or give an error.
|
||||
-- readLedgerWithOpts :: [Opt] -> [String] -> FilePath -> IO Ledger
|
||||
-- readLedgerWithOpts opts args f = do
|
||||
-- t <- getCurrentLocalTime
|
||||
-- readLedger f
|
||||
|
||||
-- -- | Convert a Journal to a canonicalised, cached and filtered Ledger
|
||||
-- -- based on the command-line options/arguments and a reference time.
|
||||
-- filterAndCacheLedgerWithOpts :: [Opt] -> [String] -> LocalTime -> String -> Journal -> Ledger
|
||||
-- filterAndCacheLedgerWithOpts opts args = filterAndCacheLedger . optsToFilterSpec opts args
|
||||
return $ (if cost then journalConvertAmountsToCost else id) j
|
||||
|
||||
-- | Attempt to open a web browser on the given url, all platforms.
|
||||
openBrowserOn :: String -> IO ExitCode
|
||||
|
||||
@ -6,9 +6,8 @@ Utilities for doing I/O with ledger files.
|
||||
module Hledger.Data.IO
|
||||
where
|
||||
import Control.Monad.Error
|
||||
import Hledger.Data.Ledger (makeUncachedLedger)
|
||||
import Hledger.Data.Parse (parseJournal)
|
||||
import Hledger.Data.Types (FilterSpec(..),WhichDate(..),Journal(..),Ledger(..))
|
||||
import Hledger.Data.Types (FilterSpec(..),WhichDate(..),Journal(..))
|
||||
import Hledger.Data.Dates (nulldatespan)
|
||||
import System.Directory (getHomeDirectory)
|
||||
import System.Environment (getEnv)
|
||||
@ -52,32 +51,22 @@ myTimelogPath =
|
||||
home <- getHomeDirectory
|
||||
return $ home </> timelogdefaultfilename)
|
||||
|
||||
-- | Read the user's default ledger file, or give an error.
|
||||
myLedger :: IO Ledger
|
||||
myLedger = myLedgerPath >>= readLedger
|
||||
-- | Read the user's default journal file, or give an error.
|
||||
myJournal :: IO Journal
|
||||
myJournal = myLedgerPath >>= readJournal
|
||||
|
||||
-- | Read the user's default timelog file, or give an error.
|
||||
myTimelog :: IO Ledger
|
||||
myTimelog = myTimelogPath >>= readLedger
|
||||
myTimelog :: IO Journal
|
||||
myTimelog = myTimelogPath >>= readJournal
|
||||
|
||||
-- | Read an unfiltered, uncached ledger from this file, or give an error.
|
||||
readLedger :: FilePath -> IO Ledger
|
||||
readLedger f = do
|
||||
-- | Read a journal from this file, or give an error.
|
||||
readJournal :: FilePath -> IO Journal
|
||||
readJournal f = do
|
||||
s <- readFile f
|
||||
j <- journalFromString s
|
||||
return $ makeUncachedLedger j
|
||||
|
||||
-- -- | Read a ledger from this file, filtering according to the filter spec.,
|
||||
-- -- | or give an error.
|
||||
-- readLedgerWithFilterSpec :: FilterSpec -> FilePath -> IO Ledger
|
||||
-- readLedgerWithFilterSpec fspec f = do
|
||||
-- s <- readFile f
|
||||
-- t <- getClockTime
|
||||
-- j <- journalFromString s
|
||||
-- return $ filterAndCacheLedger fspec s j{filepath=f, filereadtime=t}
|
||||
journalFromString s
|
||||
|
||||
-- | Read a Journal from the given string, using the current time as
|
||||
-- reference time, or give a parse error.
|
||||
-- reference time, or throw an error.
|
||||
journalFromString :: String -> IO Journal
|
||||
journalFromString s = liftM (either error id) $ runErrorT $ parseJournal "(string)" s
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
{-|
|
||||
|
||||
A 'Journal' is a parsed ledger file, containing 'Transaction's.
|
||||
It can be filtered and massaged in various ways, then \"crunched\"
|
||||
to form a 'Ledger'.
|
||||
A 'Journal' is a set of 'Transaction's and related data, usually parsed
|
||||
from a hledger/ledger journal file or timelog.
|
||||
|
||||
-}
|
||||
|
||||
|
||||
@ -1,53 +1,9 @@
|
||||
{-|
|
||||
|
||||
A compound data type for efficiency. A 'Ledger' caches information derived
|
||||
from a 'Journal' for easier querying. Also it typically has had
|
||||
uninteresting 'Transaction's and 'Posting's filtered out. It
|
||||
contains:
|
||||
|
||||
- the original unfiltered 'Journal'
|
||||
|
||||
- a tree of 'AccountName's
|
||||
|
||||
- a map from account names to 'Account's
|
||||
|
||||
- the full text of the journal file, when available
|
||||
|
||||
This is the main object you'll deal with as a user of the Ledger
|
||||
library. The most useful functions also have shorter, lower-case
|
||||
aliases for easier interaction. Here's an example:
|
||||
|
||||
> > import Hledger.Data
|
||||
> > l <- readLedger "sample.ledger"
|
||||
> > accountnames l
|
||||
> ["assets","assets:bank","assets:bank:checking","assets:bank:saving",...
|
||||
> > accounts l
|
||||
> [Account assets with 0 txns and $-1 balance,Account assets:bank with...
|
||||
> > topaccounts l
|
||||
> [Account assets with 0 txns and $-1 balance,Account expenses with...
|
||||
> > account l "assets"
|
||||
> Account assets with 0 txns and $-1 balance
|
||||
> > accountsmatching ["ch"] l
|
||||
> accountsmatching ["ch"] l
|
||||
> [Account assets:bank:checking with 4 txns and $0 balance]
|
||||
> > subaccounts l (account l "assets")
|
||||
> subaccounts l (account l "assets")
|
||||
> [Account assets:bank with 0 txns and $1 balance,Account assets:cash...
|
||||
> > head $ transactions l
|
||||
> 2008/01/01 income assets:bank:checking $1 RegularPosting
|
||||
> > accounttree 2 l
|
||||
> Node {rootLabel = Account top with 0 txns and 0 balance, subForest = [...
|
||||
> > accounttreeat l (account l "assets")
|
||||
> Just (Node {rootLabel = Account assets with 0 txns and $-1 balance, ...
|
||||
> > datespan l -- disabled
|
||||
> DateSpan (Just 2008-01-01) (Just 2009-01-01)
|
||||
> > rawdatespan l
|
||||
> DateSpan (Just 2008-01-01) (Just 2009-01-01)
|
||||
> > ledgeramounts l
|
||||
> [$1,$-1,$1,$-1,$1,$-1,$1,$1,$-2,$1,$-1]
|
||||
> > commodities l
|
||||
> [Commodity {symbol = "$", side = L, spaced = False, comma = False, ...
|
||||
|
||||
A 'Ledger' is derived from a 'Journal' by applying a filter specification
|
||||
to select 'Transaction's and 'Posting's of interest. It contains the
|
||||
filtered journal and knows the resulting chart of accounts, account
|
||||
balances, and postings in each account.
|
||||
|
||||
-}
|
||||
|
||||
@ -77,23 +33,17 @@ nullledger = Ledger{
|
||||
accountmap = fromList []
|
||||
}
|
||||
|
||||
-- | Generate a ledger from a journal, but don't cache it yet.
|
||||
makeUncachedLedger :: Journal -> UncachedLedger
|
||||
makeUncachedLedger j = nullledger{journal=j}
|
||||
|
||||
-- | Filter a ledger's transactions as specified and generate derived data.
|
||||
filterAndCacheLedger :: FilterSpec -> UncachedLedger -> Ledger
|
||||
filterAndCacheLedger filterspec l@Ledger{journal=j} = l{journal=j',accountnametree=t,accountmap=m}
|
||||
where j' = filterJournalPostings filterspec{depth=Nothing} j
|
||||
journalToLedger :: FilterSpec -> Journal -> Ledger
|
||||
journalToLedger fs j = nullledger{journal=j',accountnametree=t,accountmap=m}
|
||||
where j' = filterJournalPostings fs{depth=Nothing} j
|
||||
(t, m) = journalAccountInfo j'
|
||||
|
||||
-- | List a ledger's account names.
|
||||
ledgerAccountNames :: Ledger -> [AccountName]
|
||||
ledgerAccountNames = drop 1 . flatten . accountnametree
|
||||
|
||||
-- | Get the named account from a (cached) ledger.
|
||||
-- If the ledger has not been cached (with crunchJournal or
|
||||
-- cacheLedger'), this returns the null account.
|
||||
-- | Get the named account from a ledger.
|
||||
ledgerAccount :: Ledger -> AccountName -> Account
|
||||
ledgerAccount l a = findWithDefault nullacct a $ accountmap l
|
||||
|
||||
|
||||
@ -4,26 +4,31 @@
|
||||
Most data types are defined here to avoid import cycles.
|
||||
Here is an overview of the hledger data model:
|
||||
|
||||
> Ledger -- hledger's ledger is a journal file plus cached/derived data
|
||||
> Journal -- a representation of the journal file, containing..
|
||||
> [Transaction] -- ..journal transactions, which have date, status, code, description and..
|
||||
> [Posting] -- ..two or more account postings (account name and amount)
|
||||
> Tree AccountName -- all account names as a tree
|
||||
> Map AccountName Account -- a map from account name to account info (postings and balances)
|
||||
> Journal -- a journal is derived from one or more data files. It contains..
|
||||
> [Transaction] -- journal transactions, which have date, status, code, description and..
|
||||
> [Posting] -- multiple account postings (entries), which have account name and amount.
|
||||
> [HistoricalPrice] -- historical commodity prices
|
||||
>
|
||||
> Ledger -- a ledger is derived from a journal, by applying a filter specification. It contains..
|
||||
> Journal -- the filtered journal, containing only the transactions and postings we are interested in
|
||||
> Tree AccountName -- account names referenced in the journal's transactions, as a tree
|
||||
> Map AccountName Account -- per-account postings and balances from the journal's transactions, as a map from account name to account info
|
||||
|
||||
For more detailed documentation on each type, see the corresponding modules.
|
||||
|
||||
Terminology has been in flux:
|
||||
Evolution of transaction/entry/posting terminology:
|
||||
|
||||
- ledger 2 had entries containing transactions.
|
||||
- ledger 2: entries contain transactions
|
||||
|
||||
- hledger 0.4 had Entrys containing RawTransactions, which were flattened to Transactions.
|
||||
- hledger 0.4: Entrys contain RawTransactions (which are flattened to Transactions)
|
||||
|
||||
- ledger 3 has transactions containing postings.
|
||||
- ledger 3: transactions contain postings
|
||||
|
||||
- hledger 0.5 had LedgerTransactions containing Postings, which were flattened to Transactions.
|
||||
- hledger 0.5: LedgerTransactions contain Postings (which are flattened to Transactions)
|
||||
|
||||
- hledger 0.8 has Transactions containing Postings, and no flattened type.
|
||||
- hledger 0.8: Transactions contain Postings (referencing Transactions, corecursively)
|
||||
|
||||
- hledger 0.10: Postings should be called Entrys, but are left as-is for now
|
||||
|
||||
-}
|
||||
|
||||
@ -123,7 +128,13 @@ data Journal = Journal {
|
||||
filepath :: FilePath,
|
||||
filereadtime :: ClockTime,
|
||||
jtext :: String
|
||||
} deriving (Eq)
|
||||
} deriving (Eq, Typeable)
|
||||
|
||||
data Ledger = Ledger {
|
||||
journal :: Journal,
|
||||
accountnametree :: Tree AccountName,
|
||||
accountmap :: Map.Map AccountName Account
|
||||
}
|
||||
|
||||
data Account = Account {
|
||||
aname :: AccountName,
|
||||
@ -131,18 +142,7 @@ data Account = Account {
|
||||
abalance :: MixedAmount -- ^ sum of postings in this account and subaccounts
|
||||
}
|
||||
|
||||
data Ledger = Ledger {
|
||||
journal :: Journal,
|
||||
accountnametree :: Tree AccountName,
|
||||
accountmap :: Map.Map AccountName Account
|
||||
} deriving Typeable
|
||||
|
||||
-- | An incomplete ledger, containing just the journal. Currently just a
|
||||
-- visual indicator used in a few places.
|
||||
type UncachedLedger = Ledger
|
||||
|
||||
-- | A generic, pure specification of how to filter transactions/postings.
|
||||
-- This exists to keep app-specific options out of the hledger library.
|
||||
-- | A generic, pure specification of how to filter transactions and postings.
|
||||
data FilterSpec = FilterSpec {
|
||||
datespan :: DateSpan -- ^ only include if in this date span
|
||||
,cleared :: Maybe Bool -- ^ only include if cleared\/uncleared\/don't care
|
||||
|
||||
Loading…
Reference in New Issue
Block a user