a partly-working parser for the ledger file format
This commit is contained in:
		
							parent
							
								
									85864b414e
								
							
						
					
					
						commit
						55eb391f50
					
				
							
								
								
									
										244
									
								
								hledger.hs
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								hledger.hs
									
									
									
									
									
								
							| @ -1,26 +1,230 @@ | |||||||
| -- ledger-compatible money management tools | -- hledger - ledger-like money management utilities | ||||||
| -- (c) 2007 Simon Michael & contributors, released under GPL v3 or later | -- GPLv3, (c) Simon Michael & contributors,  | ||||||
|  | -- ledger is at http://newartisans.com/ledger.html | ||||||
| 
 | 
 | ||||||
|  | import System.Directory (getHomeDirectory) | ||||||
|  | import System.Environment (getEnv) | ||||||
| import Control.Exception (assert) | import Control.Exception (assert) | ||||||
| 
 | import Text.ParserCombinators.Parsec | ||||||
| -- data model | import qualified Text.ParserCombinators.Parsec.Token as P | ||||||
| type Date = String | import Text.ParserCombinators.Parsec.Language (haskellDef) | ||||||
| type Account = String | --import TildeExpand -- confuses my ghc 6.7 | ||||||
| type Money = Float |  | ||||||
| -- a transaction records a movement of money between two accounts |  | ||||||
| data Transaction = Transaction { |  | ||||||
|                                 date :: Date, |  | ||||||
|                                 account :: Account,       -- debit this |  | ||||||
|                                 other_account :: Account, -- credit this |  | ||||||
|                                 description :: String, |  | ||||||
|                                 amount :: Money |  | ||||||
|                                } |  | ||||||
| 
 | 
 | ||||||
| -- sample data | -- sample data | ||||||
| t1 = Transaction "2007-01-01" "checking" "food" "joe's diner" 8.50 |  | ||||||
| 
 | 
 | ||||||
| -- tests | 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" --" | ||||||
|  | 
 | ||||||
|  | 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) | ||||||
|  | type Date = String | ||||||
|  | type Account = String | ||||||
|  | 
 | ||||||
|  | -- ledger file parsing | ||||||
|  | 
 | ||||||
|  | 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 []}) | ||||||
|  | 
 | ||||||
|  | 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" | ||||||
|  |   return (Transaction account amount) | ||||||
|  | 
 | ||||||
|  | --ledgeraccount = do {alphaNum; many (alphaNum <|> char ':' <|> try (do {spacenonewline; notFollowedBy spacenonewline; return ' '}))} | ||||||
|  | ledgeraccount = many1 (alphaNum <|> char ':' <|> try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})) | ||||||
|  | 
 | ||||||
|  | twoormorespaces = do spacenonewline; many1 spacenonewline | ||||||
|  | 
 | ||||||
|  | ledgeramount = try (do | ||||||
|  |                       many1 spacenonewline --twoormorespaces | ||||||
|  |                       currency <- many (noneOf "-.0123456789\n") <?> "currency" | ||||||
|  |                       quantity <- (oneOf "-.0123456789") `manyTill` ledgereol <?> "quantity" | ||||||
|  |                       return (Amount currency (read quantity)) | ||||||
|  |                    ) <|> do | ||||||
|  |                       ledgereol | ||||||
|  |                       return (Amount "" 0) -- change later to balance entry | ||||||
|  | 
 | ||||||
|  | ledgereol = do {newline; return []} <|> ledgercomment | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | 
 | ||||||
| main = do | main = do | ||||||
|   assert_ $ amount t1 == 8.50 |   showParseResult (parse ledgertransaction "" sample_transaction) | ||||||
|   putStrLn "ok" |   showParseResult (parse ledgertransaction "" sample_transaction2) | ||||||
|     where assert_ e = assert e return ()              |   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 ()              | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user