#!/usr/bin/env runhaskell

-- convert account transactions in csv format to ledger entries
-- pesco, 2009

import Text.ParserCombinators.Parsec
import Data.List (findIndex, sortBy)


-- == CONFIGURATION == --

-- list your bank accounts and their ledger name below
accounts = [ ((".BANKNO.", "..ACCTNO.."), "assets:checking"),
             ((".BANKNO.", "..ACCTNO.."), "assets:savings"),
             ((".BANKNO.", "..ACCTNO.."), "liabilites:creditcard") ]

-- ledger names for the "remote" side of transactions
expenses = "expenses"   -- default for withdrawals
income   = "income"     -- default for deposits

-- names of relevant colums in the csv input
bank_col  = "localBankCode"
acc_col   = "localAccountNumber"
date_col  = "date"
vdate_col = "valutadate"
desc_col  = "purpose"
val_col   = "value_value"
cur_col   = "value_currency"


-- == MAIN ROUTINE == --

main = getContents >>= (putStr . showledger . readcsv)

showledger :: [[String]] -> String
showledger (t:rs) = concatMap showentry $ sortBy cmp $ map (mkentry t) rs
showledger [] = error "missing title row in csv data"

-- sort from latest to earliest date
cmp (E _ d1 _ _ _ _) (E _ d2 _ _ _ _) = compare d2 d1


-- == LEDGER ENTRIES == --

data Entry = E Account Date Date String Amount Currency
  deriving (Show)

data Date = D String String String  -- year month day
  deriving (Eq)

type Account = String
type Amount = String
type Currency = String

showentry :: Entry -> String
showentry (E acc date vdate desc val cur) = concat
  [ datespec, "  * ", desc, "\n",
    "  ", acc, replicate spaces ' ', val, " ", cur, "\n",
    "  ", remote_account, "\n",
    "\n" ]
  where
  datespec = show date ++ if vdate==date then "" else ("="++show vdate)
  spaces = 54 - 2 - length acc - length val
  value = read val :: Float
  remote_account = if value<0 then expenses else income

mkentry :: [String] -> [String] -> Entry
mkentry t = mkentry'
  where
  -- column indices
  ibank  = idx t bank_col
  iacc   = idx t acc_col
  idate  = idx t date_col
  ivdate = idx t vdate_col
  idesc  = idx t desc_col
  ival   = idx t val_col
  icur   = idx t cur_col
  
  mkentry' r = E accname date vdate desc val cur
    where
    bank  = r !! ibank
    acc   = r !! iacc
    date  = readdate (r !! idate)
    vdate = readdate (r !! ivdate)
    desc  = r !! idesc
    val   = r !! ival
    cur   = r !! icur
    accname = accounts ! (bank, acc)

-- Dates --

instance Show Date where
  show (D y m d) = concat [y, "-", m, "-", d]

readdate :: String -> Date
readdate s = either (error.show) id (parse p_date "date" s)

p_date :: Parser Date
p_date = do y <- many1 digit
            char '/'
            m <- many1 digit
            char '/'
            d <- many1 digit
            return (D y m d)

instance Ord Date where
  compare (D y1 m1 d1) (D y2 m2 d2) = compare [y1,m1,d1] [y2,m2,d2]


-- Utilities --

(!) :: (Eq a, Show a) => [(a,b)] -> a -> b
m!x = maybe (error (show x++": not found")) id (lookup x m)

idx :: (Eq a, Show a) => [a] -> a -> Int
idx xs x = maybe (error (show x++": not found")) id (findIndex (==x) xs)


-- == CSV PARSER == --

readcsv :: String -> [[String]]
readcsv = map readcsvrow . lines

readcsvrow :: String -> [String]
readcsvrow s = either (error.show) id (parse p_csvrow "stdin" s)

p_csvrow :: Parser [String]
p_csvrow = sepBy1 p_csvfield (char ';')

p_csvfield :: Parser String
p_csvfield = between (char '"') (char '"') p_csvstring <|> many (noneOf ";")

p_csvstring :: Parser String
p_csvstring = many (noneOf "\"" <|> (string "\\\"" >> return '"'))
