convert command for transforming bank CSV exports to ledger format

This commit is contained in:
Simon Michael 2009-04-10 03:10:58 +00:00
parent ae69a216ac
commit 393e7d98d4
5 changed files with 109 additions and 9 deletions

View File

@ -34,12 +34,10 @@ getAndAddTransactions l = (do
today <- getCurrentDay today <- getCurrentDay
date <- liftM (fixSmartDate today . fromparse . parse smartdate "" . lowercase) date <- liftM (fixSmartDate today . fromparse . parse smartdate "" . lowercase)
$ askFor "date" (Just $ showDate today) $ askFor "date" (Just $ showDate today)
-- cleared' <- askFor "cleared, y/n" (Just "n")
-- let cleared = if cleared' == "y" then True else False
description <- askFor "description" Nothing description <- askFor "description" Nothing
ps <- getPostings [] ps <- getPostings []
let t = nullledgertxn{ltdate=date let t = nullledgertxn{ltdate=date
,ltstatus=False -- cleared ,ltstatus=False
,ltdescription=description ,ltdescription=description
,ltpostings=ps ,ltpostings=ps
} }

96
ConvertCommand.hs Normal file
View File

@ -0,0 +1,96 @@
{-|
Convert account data in CSV format (eg downloaded from a bank) to ledger
format, and print it on stdout.
Usage: hledger convert CSVFILE ACCOUNTNAME RULESFILE
ACCOUNTNAME is the base account to use for transactions. RULESFILE
provides some rules to help convert the data. It should contain paragraphs
separated by one blank line. The first paragraph is a single line of five
comma-separated numbers, which are the csv field positions corresponding
to the ledger transaction's date, status, code, description, and amount.
All other paragraphs specify one or more regular expressions, followed by
the ledger account to use when a transaction's description matches any of
them. Here's an example rules file:
> 0,2,3,4,1
>
> ATM DEPOSIT
> assets:bank:checking
>
> (TO|FROM) SAVINGS
> assets:bank:savings
>
> ITUNES
> BLOCKBUSTER
> expenses:entertainment
Roadmap:
Support for other formats will be added. To update a ledger file, pipe the
output into the import command. The rules will move to a hledger config
file. When no rule matches, accounts will be guessed based on similarity
to descriptions in the current ledger, with interactive prompting and
optional rule saving.
-}
module ConvertCommand where
import Data.Maybe (isJust)
import Data.List.Split (splitOn)
import Options (Opt)
import Ledger.Types (Ledger)
import Ledger.Utils (strip)
import System (getArgs)
import Text.CSV (parseCSVFromFile, Record)
import Text.Printf (printf)
import Text.Regex.PCRE ((=~))
import Data.Maybe
import Ledger.Dates (firstJust, showDate)
import System.Locale (defaultTimeLocale)
import Data.Time.Format (parseTime)
import Control.Monad (when)
convert :: [Opt] -> [String] -> Ledger -> IO ()
convert opts args l = do
when (length args /= 3) (error "please specify a csv file, base account, and import rules file.")
let [csvfile,baseacct,rulesfile] = args
rulesstr <- readFile rulesfile
(fieldpositions,rules) <- parseRules rulesstr
parse <- parseCSVFromFile csvfile
let records = case parse of
Left e -> error $ show e
Right rs -> reverse rs
mapM_ (print_ledger_txn (baseacct,fieldpositions,rules)) records
parseRules s = do
let ls = map strip $ lines s
let paras = splitOn [""] ls
let fieldpositions = map read $ splitOn "," $ head $ head paras
let rules = [(last p,init p) | p <- tail paras]
return (fieldpositions,rules)
print_ledger_txn (baseacct,fieldpositions,rules) record@(a:b:c:d:e) = do
let [date,cleared,number,description,amount] = map (record !!) fieldpositions
amount' = strnegate amount where strnegate ('-':s) = s
strnegate s = '-':s
unknownacct | (read amount' :: Double) < 0 = "income:unknown"
| otherwise = "expenses:unknown"
putStrLn $ printf "%s%s %s" (fixdate date) (if not (null number) then printf " (%s)" number else "") description
putStrLn $ printf " %-30s %15s" (fromMaybe unknownacct $ choose_acct rules description) (printf "$%s" amount' :: String)
putStrLn $ printf " %s\n" baseacct
print_ledger_txn _ _ = return ()
choose_acct rules description | null matches = Nothing
| otherwise = Just $ fst $ head $ matches
where matches = filter (any (description =~) . snd) rules
fixdate :: String -> String
fixdate s = maybe "0000/00/00" showDate $
firstJust
[parseTime defaultTimeLocale "%Y/%m/%d" s
,parseTime defaultTimeLocale "%Y-%m-%d" s
,parseTime defaultTimeLocale "%m/%d/%Y" s
,parseTime defaultTimeLocale "%m-%d-%Y" s
]

View File

@ -25,15 +25,17 @@ timeprogname = "hours"
usagehdr = ( usagehdr = (
"Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" ++ "Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" ++
" hours [OPTIONS] [COMMAND [PATTERNS]]\n" ++ " hours [OPTIONS] [COMMAND [PATTERNS]]\n" ++
" hledger convert CSVFILE ACCOUNTNAME RULESFILE\n" ++
"\n" ++ "\n" ++
"When invoked as \"hours\", uses your timelog and --period today as defaults.\n" ++ "When invoked as \"hours\", uses your timelog and --period today as defaults.\n" ++
"\n" ++ "\n" ++
"COMMAND is one of (may be abbreviated):\n" ++ "COMMAND is one of (may be abbreviated):\n" ++
" add - read new transactions interactively\n" ++ " add - prompt for new transactions and add them to the ledger\n" ++
" balance - show account balances\n" ++ " balance - show accounts, with balances\n" ++
" histogram - show transaction counts per reporting interval\n" ++ " convert - convert CSV data to ledger format and print on stdout\n" ++
" print - show transactions as formatted data\n" ++ " histogram - show transaction counts per day or other interval\n" ++
" register - show transactions as a register\n" ++ " print - show transactions in ledger format\n" ++
" register - show transactions as a register with running balance\n" ++
#ifdef VTY #ifdef VTY
" ui - run a simple curses-based text ui\n" ++ " ui - run a simple curses-based text ui\n" ++
#endif #endif

View File

@ -52,7 +52,8 @@ Executable hledger
Build-Depends: base, containers, haskell98, directory, parsec, Build-Depends: base, containers, haskell98, directory, parsec,
regex-compat, regexpr>=0.5.1, old-locale, time, regex-compat, regexpr>=0.5.1, old-locale, time,
HUnit, mtl, bytestring, filepath, process, testpack HUnit, mtl, bytestring, filepath, process, testpack,
regex-pcre, csv, split
Other-Modules: Other-Modules:
BalanceCommand BalanceCommand

View File

@ -38,6 +38,7 @@ module Main (
module Utils, module Utils,
module Options, module Options,
module BalanceCommand, module BalanceCommand,
module ConvertCommand,
module PrintCommand, module PrintCommand,
module RegisterCommand, module RegisterCommand,
module HistogramCommand, module HistogramCommand,
@ -60,6 +61,7 @@ import Utils (withLedgerDo)
import Options import Options
import Tests import Tests
import BalanceCommand import BalanceCommand
import ConvertCommand
import PrintCommand import PrintCommand
import RegisterCommand import RegisterCommand
import HistogramCommand import HistogramCommand
@ -81,6 +83,7 @@ main = do
| Help `elem` opts = putStr $ usage | Help `elem` opts = putStr $ usage
| Version `elem` opts = putStr versionmsg | Version `elem` opts = putStr versionmsg
| cmd `isPrefixOf` "balance" = withLedgerDo opts args balance | cmd `isPrefixOf` "balance" = withLedgerDo opts args balance
| cmd `isPrefixOf` "convert" = withLedgerDo opts args convert
| cmd `isPrefixOf` "print" = withLedgerDo opts args print' | cmd `isPrefixOf` "print" = withLedgerDo opts args print'
| cmd `isPrefixOf` "register" = withLedgerDo opts args register | cmd `isPrefixOf` "register" = withLedgerDo opts args register
| cmd `isPrefixOf` "histogram" = withLedgerDo opts args histogram | cmd `isPrefixOf` "histogram" = withLedgerDo opts args histogram