hledger/Commands/Convert.hs
2009-09-22 16:51:27 +00:00

106 lines
4.2 KiB
Haskell

{-|
Convert account data in CSV format (eg downloaded from a bank) to ledger
format, and print it on stdout. See the manual for more details.
-}
module Commands.Convert where
import Data.List.Split (splitOn)
import Options -- (Opt,Debug)
import Ledger.Types (Ledger,AccountName)
import Ledger.Utils (strip)
import System.IO (stderr, hPutStrLn)
import Text.CSV (parseCSVFromFile)
import Text.Printf (printf)
import Text.RegexPR (matchRegexPR)
import Data.Maybe
import Ledger.Dates (firstJust, showDate)
import Locale (defaultTimeLocale)
import Data.Time.Format (parseTime)
import Control.Monad (when)
import Safe (readMay, readDef)
convert :: [Opt] -> [String] -> Ledger -> IO ()
convert opts args _ = 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 (Debug `elem` opts) (baseacct,fieldpositions,rules)) records
type Rule = (
[(String, Maybe String)] -- list of patterns and optional replacements
,AccountName -- account name to use for a matched transaction
)
parseRules :: String -> IO ([Maybe Int],[Rule])
parseRules s = do
let ls = map strip $ lines s
let paras = splitOn [""] ls
let fieldpositions = map readMay $ splitOn "," $ head $ head paras
let rules = [(map parsePatRepl $ init ls, last ls) | ls <- tail paras]
return (fieldpositions,rules)
parsePatRepl :: String -> (String, Maybe String)
parsePatRepl l = case splitOn "=" l of
(p:r:_) -> (p, Just r)
_ -> (l, Nothing)
print_ledger_txn :: Bool -> (String,[Maybe Int],[Rule]) -> [String] -> IO ()
print_ledger_txn _ (_,[],_) _ = return ()
print_ledger_txn _ (('#':_),_,_) _ = return ()
print_ledger_txn debug (baseacct,fieldpositions,rules) csvrecord
| length csvrecord < maximum (map (fromMaybe 0) fieldpositions) + 1 = return ()
| otherwise =
do
when debug $ hPutStrLn stderr $ show csvrecord
let date = maybe "" (csvrecord !!) (fieldpositions !! 0)
number = maybe "" (csvrecord !!) (fieldpositions !! 1)
description = maybe "" (csvrecord !!) (fieldpositions !! 2)
amount = maybe "" (csvrecord !!) (fieldpositions !! 3)
amount' = strnegate amount where strnegate ('-':s) = s
strnegate s = '-':s
unknownacct | (readDef 0 amount' :: Double) < 0 = "income:unknown"
| otherwise = "expenses:unknown"
(acct,desc) = choose_acct_desc rules (unknownacct,description)
when debug $ hPutStrLn stderr $ printf "using %s for %s" desc description
printf "%s%s %s\n" (fixdate date) (if not (null number) then printf " (%s)" number else "") desc
printf " %-30s %15s\n" acct (printf "$%s" amount' :: String)
printf " %s\n\n" baseacct
choose_acct_desc :: [Rule] -> (String,String) -> (String,String)
choose_acct_desc rules (acct,desc) | null matchingrules = (acct,desc)
| otherwise = (a,d)
where
matchingrules = filter ismatch rules :: [Rule]
where ismatch = any (isJust . flip matchregex desc . fst) . fst
(prs,a) = head matchingrules
mrs = filter (isJust . fst) $ map (\(p,r) -> (matchregex p desc, r)) prs
(m,repl) = head mrs
matched = fst $ fst $ fromJust m
d = fromMaybe matched repl
matchregex = matchRegexPR . ("(?i)" ++)
fixdate :: String -> String
fixdate s = maybe "0000/00/00" showDate $
firstJust
[parseTime defaultTimeLocale "%Y/%m/%e" s
-- can't parse a month without leading 0, try adding onee
,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)
,parseTime defaultTimeLocale "%Y-%m-%e" s
,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)
,parseTime defaultTimeLocale "%m/%e/%Y" s
,parseTime defaultTimeLocale "%m/%e/%Y" ('0':s)
,parseTime defaultTimeLocale "%m-%e-%Y" s
,parseTime defaultTimeLocale "%m-%e-%Y" ('0':s)
]