big overhaul of options, support -f-, rudimentary timelog file handling

This commit is contained in:
Simon Michael 2007-03-12 07:40:33 +00:00
parent 987659e3d4
commit 510d39095c
8 changed files with 232 additions and 169 deletions

View File

@ -4,7 +4,7 @@ module Models (
module AccountName, module AccountName,
module Transaction, module Transaction,
module Entry, module Entry,
module TimeLogEntry, module TimeLog,
module EntryTransaction, module EntryTransaction,
module Ledger, module Ledger,
module Account module Account
@ -16,7 +16,7 @@ import BasicTypes
import AccountName import AccountName
import Transaction import Transaction
import Entry import Entry
import TimeLogEntry import TimeLog
import EntryTransaction import EntryTransaction
import Ledger import Ledger
import Account import Account

View File

@ -1,86 +1,76 @@
module Options
module Options (module Options, usageInfo)
where where
import System.Console.GetOpt import System.Console.GetOpt
import System.Directory
import System.Environment (getEnv) import System.Environment (getEnv)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Utils import Utils
usageHeader = "Usage: hledger [OPTIONS] register|balance [MATCHARGS]" usage = "Usage: hledger [OPTIONS] "++commands++" [ACCTPATTERNS] [-- DESCPATTERNS]\nOptions:"
commands = "register|balance"
getOptions :: [String] -> IO ([Flag], [String]) defaultcmd = "register"
getOptions argv = ledgerFilePath = findFileFromOpts "~/ledger.dat" "LEDGER"
case getOpt RequireOrder options argv of
(o,n,[] ) -> return (o,n)
(_,_,errs) -> ioError (userError (concat errs ++ usageInfo usageHeader options))
options :: [OptDescr Flag] options :: [OptDescr Flag]
options = [ options = [
Option ['v'] ["version"] (NoArg Version) "show version number" Option ['f'] ["file"] (ReqArg File "FILE") "ledger file; - means use standard input",
, Option ['f'] ["file"] (OptArg readFileOpt "FILE") "ledger file, or - to read stdin" Option ['s'] ["showsubs"] (NoArg ShowSubs) "balance report: show subaccounts" -- register: show subtotals
, Option ['s'] ["subtotal"] (NoArg ShowSubs) "balance: show sub-accounts" --; register: show subtotals" --Option ['V'] ["version"] (NoArg Version) "show version"
] ]
data Flag = Version | File String | ShowSubs deriving (Show,Eq) data Flag =
File String |
ShowSubs |
Version
deriving (Show,Eq)
readFileOpt :: Maybe String -> Flag parseOptions :: [String] -> IO ([Flag], [String])
readFileOpt = File . fromMaybe "stdin" parseOptions argv =
case getOpt RequireOrder options argv of
(opts,[],[]) -> return (opts, [defaultcmd])
(opts,args,[]) -> return (opts, args)
(_,_,errs) -> ioError (userError (concat errs ++ showusage))
getFile :: Flag -> String -- testoptions RequireOrder ["foo","-v"]
getFile (File s) = s -- testoptions Permute ["foo","-v"]
getFile _ = [] -- testoptions (ReturnInOrder Arg) ["foo","-v"]
-- testoptions Permute ["foo","--","-v"]
-- testoptions Permute ["-?o","--name","bar","--na=baz"]
-- testoptions Permute ["--ver","foo"]
testoptions order cmdline = putStr $
case getOpt order options cmdline of
(o,n,[] ) -> "options=" ++ show o ++ " args=" ++ show n
(_,_,errs) -> concat errs ++ showusage
getLedgerFilePath :: [Flag] -> IO String showusage = usageInfo usage options
getLedgerFilePath opts = do
defaultpath <- tildeExpand "~/ledger.dat"
envordefault <- getEnv "LEDGER" `catch` \_ -> return defaultpath
path <- tildeExpand envordefault
return $ last $ [envordefault] ++ (filter (/= "") (map getFile opts))
-- ledger pattern args are a list of account patterns optionally followed -- find a file path from options, an env var or a default value
-- by -- and a list of description patterns findFileFromOpts :: FilePath -> String -> [Flag] -> IO String
ledgerPatternArgs :: [String] -> ([String],[String]) findFileFromOpts defaultpath envvar opts = do
ledgerPatternArgs args = envordefault <- getEnv envvar `catch` \_ -> return defaultpath
paths <- mapM tildeExpand $ [envordefault] ++ (concatMap getfile opts)
return $ last paths
where
getfile (File s) = [s]
getfile _ = []
tildeExpand :: FilePath -> IO FilePath
tildeExpand ('~':[]) = getHomeDirectory
tildeExpand ('~':'/':xs) = getHomeDirectory >>= return . (++ ('/':xs))
-- -- ~name, requires -fvia-C or ghc 6.8
-- --import System.Posix.User
-- -- tildeExpand ('~':xs) = do let (user, path) = span (/= '/') xs
-- -- pw <- getUserEntryForName user
-- -- return (homeDirectory pw ++ path)
tildeExpand xs = return xs
-- -- courtesy of allberry_b
-- ledger pattern args are 0 or more account patterns optionally followed
-- by -- and 0 or more description patterns
parseLedgerPatternArgs :: [String] -> ([String],[String])
parseLedgerPatternArgs args =
case "--" `elem` args of case "--" `elem` args of
True -> ((takeWhile (/= "--") args), tail $ (dropWhile (/= "--") args)) True -> ((takeWhile (/= "--") args), tail $ (dropWhile (/= "--") args))
False -> (args,[]) False -> (args,[])
getDepth :: [Flag] -> Int
getDepth opts =
maximum $ [1] ++ map depthval opts where
depthval (ShowSubs) = 9999
depthval _ = 1
-- example:
-- module Opts where
-- import System.Console.GetOpt
-- import Data.Maybe ( fromMaybe )
-- data Flag
-- = Verbose | Version
-- | Input String | Output String | LibDir String
-- deriving Show
-- options :: [OptDescr Flag]
-- options =
-- [ Option ['v'] ["verbose"] (NoArg Verbose) "chatty output on stderr"
-- , Option ['V','?'] ["version"] (NoArg Version) "show version number"
-- , Option ['o'] ["output"] (OptArg outp "FILE") "output FILE"
-- , Option ['c'] [] (OptArg inp "FILE") "input FILE"
-- , Option ['L'] ["libdir"] (ReqArg LibDir "DIR") "library directory"
-- ]
-- inp,outp :: Maybe String -> Flag
-- outp = Output . fromMaybe "stdout"
-- inp = Input . fromMaybe "stdin"
-- compilerOpts :: [String] -> IO ([Flag], [String])
-- compilerOpts argv =
-- case getOpt Permute options argv of
-- (o,n,[] ) -> return (o,n)
-- (_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
-- where header = "Usage: ic [OPTION...] files..."

View File

@ -1,14 +1,46 @@
module Parse module Parse
where where
import qualified Data.Map as Map import qualified Data.Map as Map
import Text.ParserCombinators.Parsec import Text.ParserCombinators.Parsec
import Text.ParserCombinators.Parsec.Language import Text.ParserCombinators.Parsec.Language
import qualified Text.ParserCombinators.Parsec.Token as P import qualified Text.ParserCombinators.Parsec.Token as P
import System.IO
import Utils import Utils
import Models import Models
-- set up token parsing, though we're not yet using these much
ledgerLanguageDef = LanguageDef {
commentStart = ""
, commentEnd = ""
, commentLine = ";"
, nestedComments = False
, identStart = letter <|> char '_'
, identLetter = alphaNum <|> oneOf "_':"
, opStart = opLetter emptyDef
, opLetter = oneOf "!#$%&*+./<=>?@\\^|-~"
, reservedOpNames= []
, reservedNames = []
, caseSensitive = False
}
lexer = P.makeTokenParser ledgerLanguageDef
whiteSpace = P.whiteSpace lexer
lexeme = P.lexeme lexer
symbol = P.symbol lexer
natural = P.natural lexer
parens = P.parens lexer
semi = P.semi lexer
identifier = P.identifier lexer
reserved = P.reserved lexer
reservedOp = P.reservedOp lexer
ledgerfile :: Parser Ledger
ledgerfile = ledger <|> ledgerfromtimelog
-- standard ledger file parser
{- {-
Here's the ledger 2.5 grammar: Here's the ledger 2.5 grammar:
"The ledger file format is quite simple, but also very flexible. It supports "The ledger file format is quite simple, but also very flexible. It supports
@ -109,33 +141,6 @@ i, o, b, h
-- parsec example: http://pandoc.googlecode.com/svn/trunk/src/Text/Pandoc/Readers/RST.hs -- parsec example: http://pandoc.googlecode.com/svn/trunk/src/Text/Pandoc/Readers/RST.hs
-- sample data in Tests.hs -- sample data in Tests.hs
-- set up token parsing, though we're not yet using these much
ledgerLanguageDef = LanguageDef {
commentStart = ""
, commentEnd = ""
, commentLine = ";"
, nestedComments = False
, identStart = letter <|> char '_'
, identLetter = alphaNum <|> oneOf "_':"
, opStart = opLetter emptyDef
, opLetter = oneOf "!#$%&*+./<=>?@\\^|-~"
, reservedOpNames= []
, reservedNames = []
, caseSensitive = False
}
lexer = P.makeTokenParser ledgerLanguageDef
whiteSpace = P.whiteSpace lexer
lexeme = P.lexeme lexer
symbol = P.symbol lexer
natural = P.natural lexer
parens = P.parens lexer
semi = P.semi lexer
identifier = P.identifier lexer
reserved = P.reserved lexer
reservedOp = P.reservedOp lexer
-- ledger file parsers
ledger :: Parser Ledger ledger :: Parser Ledger
ledger = do ledger = do
ledgernondatalines ledgernondatalines
@ -245,6 +250,7 @@ whiteSpace1 :: Parser ()
whiteSpace1 = do space; whiteSpace whiteSpace1 = do space; whiteSpace
-- timelog file parser
{- {-
timelog grammar, from timeclock.el 2.6 timelog grammar, from timeclock.el 2.6
@ -281,7 +287,16 @@ o 2007/03/10 17:26:02
-} -}
-- timelog file parsers ledgerfromtimelog :: Parser Ledger
ledgerfromtimelog = do
tl <- timelog
return $ ledgerFromTimeLog tl
timelog :: Parser TimeLog
timelog = do
entries <- many timelogentry
eof
return $ TimeLog entries
timelogentry :: Parser TimeLogEntry timelogentry :: Parser TimeLogEntry
timelogentry = do timelogentry = do
@ -306,5 +321,6 @@ printParseResult r = case r of Left e -> parseError e
Right v -> print v Right v -> print v
parseLedgerFile :: String -> IO (Either ParseError Ledger) parseLedgerFile :: String -> IO (Either ParseError Ledger)
parseLedgerFile f = parseFromFile ledger f parseLedgerFile "-" = fmap (parse ledgerfile "-") $ hGetContents stdin
parseLedgerFile f = parseFromFile ledgerfile f

7
TODO
View File

@ -1,7 +1,10 @@
feature: read timelog files feature: read timelog files
timelog parser timelog parser
convert timelog entries to ledger entries handle time amounts
read whole file fix arithmetic
calculate time intervals
find datetime type
auto-generate missing clock-out
optimization: add CookedLedger caching txns etc. optimization: add CookedLedger caching txns etc.
profile again profile again

View File

@ -225,12 +225,19 @@ ledger7 = Ledger
] ]
timelogentry1_str = "i 2007/03/11 16:19:00 hledger\n" timelogentry1_str = "i 2007/03/11 16:19:00 hledger\n"
timelogentry2_str = "o 2007/03/11 16:30:00\n"
timelogentry1 = TimeLogEntry 'i' "2007/03/11 16:19:00" "hledger" timelogentry1 = TimeLogEntry 'i' "2007/03/11 16:19:00" "hledger"
timelogentry2_str = "o 2007/03/11 16:30:00\n"
timelogentry2 = TimeLogEntry 'o' "2007/03/11 16:30:00" "" timelogentry2 = TimeLogEntry 'o' "2007/03/11 16:30:00" ""
timelog1_str = concat [
timelogentry1_str,
timelogentry2_str
]
timelog1 = TimeLog [
timelogentry1,
timelogentry2
]
-- utils -- utils
@ -304,14 +311,16 @@ props =
"expenses:phone","expenses:vacation","liabilities","liabilities:credit cards", "expenses:phone","expenses:vacation","liabilities","liabilities:credit cards",
"liabilities:credit cards:discover"] "liabilities:credit cards:discover"]
, ,
ledgerPatternArgs [] == ([],[]) parseLedgerPatternArgs [] == ([],[])
,ledgerPatternArgs ["a"] == (["a"],[]) ,parseLedgerPatternArgs ["a"] == (["a"],[])
,ledgerPatternArgs ["a","b"] == (["a","b"],[]) ,parseLedgerPatternArgs ["a","b"] == (["a","b"],[])
,ledgerPatternArgs ["a","b","--"] == (["a","b"],[]) ,parseLedgerPatternArgs ["a","b","--"] == (["a","b"],[])
,ledgerPatternArgs ["a","b","--","c","b"] == (["a","b"],["c","b"]) ,parseLedgerPatternArgs ["a","b","--","c","b"] == (["a","b"],["c","b"])
,ledgerPatternArgs ["--","c"] == ([],["c"]) ,parseLedgerPatternArgs ["--","c"] == ([],["c"])
,ledgerPatternArgs ["--"] == ([],[]) ,parseLedgerPatternArgs ["--"] == ([],[])
,parse' timelogentry timelogentry1_str `parseEquals` timelogentry1 ,parse' timelogentry timelogentry1_str `parseEquals` timelogentry1
,parse' timelogentry timelogentry2_str `parseEquals` timelogentry2 ,parse' timelogentry timelogentry2_str `parseEquals` timelogentry2
,parse' timelog timelog1_str `parseEquals` timelog1
] ]

59
TimeLog.hs Normal file
View File

@ -0,0 +1,59 @@
module TimeLog
where
import Utils
import BasicTypes
import Transaction
import Entry
import Ledger
data TimeLogEntry = TimeLogEntry {
tcode :: Char,
tdatetime :: DateTime,
tcomment :: String
} deriving (Eq,Ord)
instance Show TimeLogEntry where
show t = printf "%s %s %s" (show $ tcode t) (tdatetime t) (tcomment t)
data TimeLog = TimeLog {
timelog_entries :: [TimeLogEntry]
} deriving (Eq)
instance Show TimeLog where
show tl = printf "TimeLog with %d entries" $ length $ timelog_entries tl
ledgerFromTimeLog :: TimeLog -> Ledger
ledgerFromTimeLog tl =
Ledger [] [] (entriesFromTimeLogEntries $ timelog_entries tl)
entriesFromTimeLogEntries :: [TimeLogEntry] -> [Entry]
entriesFromTimeLogEntries [clockin] =
entriesFromTimeLogEntries [clockin, clockoutNowEntry]
entriesFromTimeLogEntries [clockin,clockout] =
[
Entry {
edate = indate,
estatus = True,
ecode = "",
edescription = accountname,
etransactions = [
Transaction accountname amount,
Transaction "TIME" (-amount)
]}
]
where
accountname = (tcomment clockin)
intime = tdatetime clockin
indate = dateFrom $ tdatetime clockin
outtime = tdatetime clockout
amount = timeAmount $ 0 -- read $ outtime - intime
entriesFromTimeLogEntries many =
(entriesFromTimeLogEntries $ take 2 many) ++
(entriesFromTimeLogEntries $ drop 2 many)
clockoutNowEntry = TimeLogEntry ' ' "" ""
timeAmount = Amount "h"
dateFrom = id

View File

@ -8,7 +8,6 @@ module Utils (
quickCheck, quickCheck,
) )
where where
import System.Directory
import Data.List import Data.List
import Data.Tree import Data.Tree
import Debug.Trace import Debug.Trace
@ -25,17 +24,6 @@ splitAtElement e l =
where where
(first,rest) = break (e==) l' (first,rest) = break (e==) l'
-- courtesy of allberry_b
tildeExpand :: FilePath -> IO FilePath
tildeExpand ('~':[]) = getHomeDirectory
tildeExpand ('~':'/':xs) = getHomeDirectory >>= return . (++ ('/':xs))
-- ~name, requires -fvia-C or ghc 6.8
--import System.Posix.User
-- tildeExpand ('~':xs) = do let (user, path) = span (/= '/') xs
-- pw <- getUserEntryForName user
-- return (homeDirectory pw ++ path)
tildeExpand xs = return xs
-- tree tools -- tree tools

View File

@ -11,6 +11,8 @@ hledger
Tests Tests
Parse Parse
Models Models
TimeLog
TimeLogEntry
Account Account
Ledger Ledger
EntryTransaction EntryTransaction
@ -22,7 +24,6 @@ hledger
-} -}
-- application logic & most IO
module Main module Main
where where
import System import System
@ -38,20 +39,42 @@ import Parse
import Tests import Tests
import Utils import Utils
main :: IO () main :: IO ()
main = do main = do
(opts, args) <- (getArgs >>= getOptions) (opts, (cmd:args)) <- getArgs >>= parseOptions
if args == [] run cmd opts args
then register [] [] where run cmd opts args
else | cmd `isPrefixOf` "register" = register opts args
let (command, args') = (head args, tail args) in | cmd `isPrefixOf` "balance" = balance opts args
if "reg" `isPrefixOf` command then (register opts args') | cmd `isPrefixOf` "test" = test
else if "bal" `isPrefixOf` command then balance opts args' | otherwise = putStr showusage
else if "test" `isPrefixOf` command then test
else putStr $ usageInfo usageHeader options
-- commands -- commands
register :: [Flag] -> [String] -> IO ()
register opts args = do
doWithLedger opts $ printRegister
where
printRegister ledger =
putStr $ showTransactionsWithBalances
(ledgerTransactionsMatching (acctpats,descpats) ledger)
0
where (acctpats,descpats) = parseLedgerPatternArgs args
balance :: [Flag] -> [String] -> IO ()
balance opts args = do
doWithLedger opts $ printBalance
where
printBalance ledger =
putStr $ showLedgerAccounts ledger acctpats showsubs maxdepth
where
(acctpats,_) = parseLedgerPatternArgs args
showsubs = (ShowSubs `elem` opts)
maxdepth = case (acctpats, showsubs) of
([],False) -> 1
otherwise -> 9999
test :: IO () test :: IO ()
test = do test = do
hcounts <- runTestTT tests hcounts <- runTestTT tests
@ -60,45 +83,20 @@ test = do
where showHunitCounts c = where showHunitCounts c =
reverse $ tail $ reverse ("passed " ++ (unwords $ drop 5 $ words (show c))) reverse $ tail $ reverse ("passed " ++ (unwords $ drop 5 $ words (show c)))
register :: [Flag] -> [String] -> IO ()
register opts args = do
getLedgerFilePath opts >>= parseLedgerFile >>= doWithParsed (printRegister opts args)
balance :: [Flag] -> [String] -> IO ()
balance opts args = do
getLedgerFilePath opts >>= parseLedgerFile >>= doWithParsed (printBalance opts args)
-- utils -- utils
-- doWithLedgerFile = doWithLedger :: [Flag] -> (Ledger -> IO ()) -> IO ()
-- getLedgerFilePath >>= parseLedgerFile >>= doWithParsed doWithLedger opts cmd = do
ledgerFilePath opts >>= parseLedgerFile >>= doWithParsed cmd
doWithParsed :: Show a => (a -> IO ()) -> (Either ParseError a) -> IO () doWithParsed :: Show a => (a -> IO ()) -> (Either ParseError a) -> IO ()
doWithParsed a p = do doWithParsed action parsed = do
case p of Left e -> parseError e case parsed of Left e -> parseError e
Right v -> a v Right l -> action l
printRegister :: [Flag] -> [String] -> Ledger -> IO ()
printRegister opts args ledger = do
putStr $ showTransactionsWithBalances
(ledgerTransactionsMatching (acctpats,descpats) ledger)
0
where (acctpats,descpats) = ledgerPatternArgs args
printBalance :: [Flag] -> [String] -> Ledger -> IO ()
printBalance opts args ledger = do
putStr $ showLedgerAccounts ledger acctpats showsubs maxdepth
where
(acctpats,_) = ledgerPatternArgs args
showsubs = (ShowSubs `elem` opts)
maxdepth = case (acctpats, showsubs) of
([],False) -> 1
otherwise -> 9999
-- interactive testing: -- interactive testing:
-- --
-- p <- getLedgerFilePath [] >>= parseLedgerFile -- p <- ledgerFilePath [] >>= parseLedgerFile
-- let l = either (\_ -> Ledger [] [] []) id p -- let l = either (\_ -> Ledger [] [] []) id p
-- let ant = ledgerAccountNameTree l -- let ant = ledgerAccountNameTree l
-- let at = ledgerAccountTreeMatching l [] True 999 -- let at = ledgerAccountTreeMatching l [] True 999