hledger/hledger.hs

262 lines
8.3 KiB
Haskell
Raw Normal View History

-- hledger - ledger-like money management utilities
-- GPLv3, (c) Simon Michael & contributors,
-- ledger is at http://newartisans.com/ledger.html
2007-01-28 00:51:59 +03:00
import System.Directory (getHomeDirectory)
import System.Environment (getEnv)
2007-01-28 00:51:59 +03:00
import Control.Exception (assert)
import Text.ParserCombinators.Parsec
import qualified Text.ParserCombinators.Parsec.Token as P
2007-01-28 12:32:24 +03:00
import Text.ParserCombinators.Parsec.Language
--import TildeExpand -- confuses my ghc 6.7
2007-01-28 00:51:59 +03:00
-- sample data
sample_entry = "\
\2007/01/27 * joes diner\n\
\ expenses:food:dining $10.00\n\
\ expenses:gifts $10.00\n\
\ assets:checking $-20.00\n\
\\n\" --"
sample_entry2 = "\
\2007/01/28 coopportunity\n\
\ expenses:food:groceries $47.18\n\
\ assets:checking\n\
\\n" --"
sample_periodic_entry = "\
\~ monthly from 2007/2/2\n\
\ assets:saving $200.00\n\
\ assets:checking\n\
\\n" --"
sample_periodic_entry2 = "\
\~ monthly from 2007/2/2\n\
\ assets:saving $200.00 ;auto savings\n\
\ assets:checking\n\
\\n" --"
2007-01-28 12:32:24 +03:00
sample_transaction = " expenses:food:dining $10.00\n"
sample_transaction2 = " assets:checking\n"
sample_ledger = "\
\\n\
\2007/01/27 * joes diner\n\
\ expenses:food:dining $10.00\n\
\ expenses:gifts $10.00\n\
\ assets:checking $-20.00\n\
\\n\
\\n\
\2007/01/28 coopportunity\n\
\ expenses:food:groceries $47.18\n\
\ assets:checking $-47.18\n\
\\n\
\" --"
sample_ledger2 = "\
\;comment\n\
\2007/01/27 * joes diner\n\
\ expenses:food:dining $10.00\n\
\ assets:checking $-47.18\n\
\\n" --"
sample_ledger3 = "\
\2007/01/27 * joes diner\n\
\ expenses:food:dining $10.00\n\
\;intra-entry comment\n\
\ assets:checking $-47.18\n\
\\n" --"
sample_ledger4 = "\
\!include \"somefile\"\n\
\2007/01/27 * joes diner\n\
\ expenses:food:dining $10.00\n\
\ assets:checking $-47.18\n\
\\n" --"
sample_ledger5 = ""
-- a data model
data Ledger = Ledger {
modifier_entries :: [ModifierEntry],
periodic_entries :: [PeriodicEntry],
entries :: [Entry]
} deriving (Show, Eq)
data Entry = Entry {
date :: Date,
status :: Bool,
code :: String,
description :: String,
transactions :: [Transaction]
} deriving (Show, Eq)
data ModifierEntry = ModifierEntry {
valueexpr :: String,
m_transactions :: [Transaction]
} deriving (Show, Eq)
data PeriodicEntry = PeriodicEntry {
periodexpr :: String,
p_transactions :: [Transaction]
} deriving (Show, Eq)
data Transaction = Transaction {
account :: Account,
amount :: Amount
} deriving (Show, Eq)
data Amount = Amount {
currency :: String,
quantity :: Float
} deriving (Read, Show, Eq)
2007-01-28 00:51:59 +03:00
type Date = String
type Account = String
-- ledger file parsing
2007-01-28 12:32:24 +03:00
-- struggling.. easier with a token parser ?
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 = do
ledgernondatalines
-- unlike ledger these must be first for now
modifier_entries <- many ledgermodifierentry
periodic_entries <- many ledgerperiodicentry
--
entries <- (many ledgerentry) <?> "entry"
eof
return (Ledger modifier_entries periodic_entries entries)
ledgernondatalines = many (ledgercomment <|> ledgerdirective <|> do {space; return []})
2007-01-28 12:32:24 +03:00
--ledgernondatalines = many (ledgerdirective <|> do {whiteSpace; return []})
ledgercomment = char ';' >> anyChar `manyTill` newline <?> "comment"
ledgerdirective = char '!' >> anyChar `manyTill` newline <?> "directive"
ledgermodifierentry = do
ledgernondatalines
char '=' <?> "entry"
valueexpr <- anyChar `manyTill` newline
transactions <- (ledgertransaction <?> "transaction") `manyTill` (newline <?> "blank line")
spaces
return (ModifierEntry valueexpr transactions)
ledgerperiodicentry = do
ledgernondatalines
char '~' <?> "entry"
periodexpr <- anyChar `manyTill` newline
transactions <- (ledgertransaction <?> "transaction") `manyTill` (newline <?> "blank line")
spaces
return (PeriodicEntry periodexpr transactions)
ledgerentry = do
ledgernondatalines
date <- ledgerdate
many1 spacenonewline
status <- ledgerstatus
code <- ledgercode
description <- anyChar `manyTill` ledgereol
transactions <- (ledgertransaction <?> "transaction") `manyTill` (newline <?> "blank line")
-- unlike ledger, we need the file to end with a blank line
spaces
return (Entry date status code description transactions)
ledgerdate = many1 (digit <|> char '/')
ledgerstatus = try (do { char '*'; many1 spacenonewline; return True } ) <|> return False
ledgercode = try (do { char '('; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
ledgertransaction = do
many (ledgercomment <|> ledgerdirective)
many1 spacenonewline
account <- ledgeraccount <?> "account"
amount <- ledgeramount <?> "amount"
2007-01-28 12:32:24 +03:00
many spacenonewline
ledgereol
return (Transaction account amount)
2007-01-28 12:32:24 +03:00
--ledgeraccount = many1 (alphaNum <|> char ':')
ledgeraccount = many1 (alphaNum <|> char ':' <|> try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}}))
2007-01-28 12:32:24 +03:00
--twoormorespaces = do spacenonewline; many1 spacenonewline
ledgeramount = try (do
many1 spacenonewline --twoormorespaces
currency <- many (noneOf "-.0123456789\n") <?> "currency"
2007-01-28 12:32:24 +03:00
quantity <- many1 (oneOf "-.0123456789") <?> "quantity"
return (Amount currency (read quantity))
2007-01-28 12:32:24 +03:00
) <|>
return (Amount "" 0) -- change later to balance the entry
2007-01-28 12:32:24 +03:00
ledgereol = ledgercomment <|> do {newline; return []}
spacenonewline = satisfy (\c -> c `elem` " \v\f\t")
-- run tests
parseMyLedgerFile = do
fname <- ledgerFilePath
parsed <- parseFromFile ledger fname
return parsed
where
ledgerFilePath = do
filepath <- getEnv "LEDGER" `catch` \_ -> return "ledger.dat"
-- don't know how to accomplish this great feat
--ledger_file <- tildeExpand filepath
let ledger_file = filepath
return ledger_file
2007-01-28 00:51:59 +03:00
main = do
showParseResult (parse ledgertransaction "" sample_transaction)
showParseResult (parse ledgertransaction "" sample_transaction2)
showParseResult (parse ledgerentry "" sample_entry)
showParseResult (parse ledgerentry "" sample_entry2)
showParseResult (parse ledgerperiodicentry "" sample_periodic_entry)
showParseResult (parse ledgerperiodicentry "" sample_periodic_entry2)
showParseResult (parse ledger "" sample_ledger)
showParseResult (parse ledger "" sample_ledger2)
showParseResult (parse ledger "" sample_ledger3)
showParseResult (parse ledger "" sample_ledger4)
showParseResult (parse ledger "" sample_ledger5)
showParseResult (parse ledger "" sample_periodic_entry)
showParseResult (parse ledger "" sample_periodic_entry2)
parseMyLedgerFile >>= showParseResult
where
showParseResult r =
case r of
Left err -> do putStr "ledger parse error at "; print err
Right x -> print x
-- assert_ $ amount t1 == 8.50
-- putStrLn "ok"
-- where assert_ e = assert e return ()