packages feed

csv-to-qif (empty) → 0.3

raw patch · 8 files changed

+538/−0 lines, 8 filesdep +Cabaldep +basedep +explicit-exceptionsetup-changed

Dependencies added: Cabal, base, explicit-exception, hspec, regex-tdfa, split, spreadsheet

Files

+ LICENSE view
@@ -0,0 +1,24 @@+Copyright 2013, Ingolf Wagner+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++- Redistributions of source code must retain the above copyright notice,+this list of conditions and the following disclaimer.+ +- Redistributions in binary form must reproduce the above copyright notice,+this list of conditions and the following disclaimer in the documentation+and/or other materials provided with the distribution.+ +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND THE CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR THE+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain
+ csv-to-qif.cabal view
@@ -0,0 +1,49 @@+name: csv-to-qif+version: 0.3+cabal-version: >=1.10+build-type: Simple+license: BSD3+license-file: LICENSE+copyright: (c) Ingolf Wagner+maintainer: Ingolf Wagner <palipalo9@googlemail.com>+stability: stable+homepage: https://github.com/mrVanDalo/csv-to-qif/+bug-reports: mailto:palipalo9@gmail.com+synopsis: A small program that will read csv files and create qif files+description: Binary to convert a wide range of csv files to qif files.+            The main target is to read them into GnuCash.+category: Console, Text+author: Ingolf Wagner+data-dir: ""++source-repository head+    type: git+    location: https://github.com/mrVanDalo/csv-to-qif++flag threaded+    Default: False++executable to-qif+    build-depends: base >=4.2 && <5, split >=0.2.2, regex-tdfa >= 1.2.0,+                   spreadsheet >= 0.1.3, explicit-exception >= 0.1.7++    if flag(threaded)+        buildable: True+        ghc-options: -threaded+    main-is: Main.hs+    buildable: True+    default-language: Haskell2010+    hs-source-dirs: src main+    other-modules: Main Parser QifOptions Qifer++test-suite tester+    build-depends: base >=4.2 && <5, split >=0.2.2, regex-tdfa >= 1.2.0,+                   Cabal >=1.9.2, hspec >=1.8+    type: exitcode-stdio-1.0+    main-is: Tester.hs+    buildable: True+    default-language: Haskell2010+    default-extensions: OverloadedStrings+    hs-source-dirs: src test+    other-modules: Parser+    ghc-options: -Wall
+ main/Main.hs view
@@ -0,0 +1,120 @@+-----------------------------------------------------------------------------+--+-- Module      :  Main+-- Copyright   :  (c) Ingolf Wagner+-- License     :  BSD3+--+-- Maintainer  :  Ingolf Wagner <palipalo9@googlemail.com>+-- Stability   :  unstable+-- Portability :+--+-- |+--+-----------------------------------------------------------------------------++module Main (main) where++import QifOptions+import Qifer+import Parser+import Data.List.Split++import System.Environment+import System.Console.GetOpt+import System.IO+import Data.Maybe+import System.Exit++++++-- | the famous main method+main = do+    args <- getArgs++    -- Parse options, getting a list of option actions+    let (actions, nonOptions, errors) = getOpt RequireOrder options args++    -- Here we thread startOptions through all supplied option actions+    opts <- foldl (>>=) (return startOptions) actions++    let Options { optInput    = input+                , optOutput   = output   } = opts++    checkArguments opts+    let rules = rule opts++    parseResult <- parseCSVFromFile (optSkip opts) (optInput opts) (optSeparator opts)+    case parseResult of+        Left error -> do+            hPutStrLn stderr $ error+            exitFailure+        Right csv  -> do++            let actions     = toTransactions rules csv++            updatedActions  <- case (optUpdater opts) of+                    Nothing   ->+                        return $ actions+                    Just file -> do+                        updaterConfig <- readUpdaterFile file+                        return $ update updaterConfig actions++            let qif = transToQif updatedActions++            withFile (optOutput opts) WriteMode (\h ->+                mapM_ ( hPutStrLn h) qif)+++-- | reads updater file+readUpdaterFile :: String -> IO [(String,String)]+readUpdaterFile fileName = do+    content <- readFile fileName+    return $ tupler . onlyTuple . splitter . lines $ content+    where+        tupler    = map (\(x:(y:[])) -> (x,y))+        onlyTuple = filter (\l -> length l == 2)+        splitter  = map (\l -> splitOn "<->" l)+++++-- | this function is unsafe call it after checkArguments+-- @todo : make me safer+rule :: Options -> Rule+rule opts = Rule {+    dateField = fromJust $ optDate opts+    , balanceField = fromJust $ optBalance opts+    , textField = optLongText opts+    , descField = optText opts+    }++-- | checks input arguments for minimum of configuration+checkArguments :: Options -> IO ()+checkArguments opts = do+    case errors of+        [] ->+            return ()+        _  -> do+            mapM_ putStrLn  errors+            exitFailure+            return ()+    where+        errors = catMaybes list+        list =  [ (check optDate "need date column")+                , (check optBalance "need balance column")+                , (checkL optText "need text columns")+                , (checkL optLongText "need long text columns")+                , (checkL optInput "need input file")+                ]+        check getter text = case (getter opts) of+                                Nothing -> Just text+                                Just _  -> Nothing+        checkL getter text = case (getter opts) of+                                []      -> Just text+                                _       -> Nothing++++
+ main/QifOptions.hs view
@@ -0,0 +1,148 @@+-----------------------------------------------------------------------------+--+-- Module      :  QifOptions+-- Copyright   :  (c) Ingolf Wagner+-- License     :  BSD3+--+-- Maintainer  :  Ingolf Wagner <palipalo9@googlemail.com>+-- Stability   :  unstable+-- Portability :+--+-- |+--+-----------------------------------------------------------------------------++module QifOptions where++import System.Console.GetOpt+import System.Exit+import System.IO+import System.Environment+import Control.Monad+import Text.Read+import Data.List.Split+import Data.Maybe++++data Options = Options  { optVerbose    :: Bool+                        , optInput      :: String+                        , optOutput     :: String+                        , optDate       :: Maybe Int+                        , optBalance    :: Maybe Int+                        , optText       :: [Int]+                        , optLongText   :: [Int]+                        , optSkip       :: Int+                        , optSeparator  :: Char+                        , optUpdater    :: Maybe String+                        } deriving (Show)++startOptions :: Options+startOptions = Options  { optVerbose    = False+                        , optInput      = ""+                        , optOutput     = ""+                        , optDate       = Nothing+                        , optBalance    = Nothing+                        , optText       = []+                        , optLongText   = []+                        , optSkip       = 0+                        , optSeparator  = ','+                        , optUpdater    = Nothing+                        }++--instance Show (a -> b) where+--    show _ = "<function>"++instance Show (IO a) where+    show _ = "<IoString>"+++options :: [ OptDescr (Options -> IO Options) ]+options =+    [ Option "i" ["input"]+        (ReqArg+            (\arg opt -> return opt { optInput = arg })+            "FILE")+        "Input file (CSV Format)"++    , Option "o" ["output"]+        (ReqArg+            (\arg opt -> return opt { optOutput = arg })+            "FILE")+        "Output file (Qif Format)"++    , Option "u" ["updater"]+        (ReqArg+            (\arg opt -> return opt { optUpdater = Just arg })+            "FILE")+        "Updater file format is `match`<->`replacement`"+    , Option "z" ["separator"]+        (ReqArg+            (\arg opt -> return opt { optSeparator = head arg })+            "char")+        "separator of the csv file"++    , Option "d" ["date"]+        (ReqArg+            (\arg opt -> return opt { optDate = readColumn arg})+            "<column number>")+        "Date Column"+    , Option "b" ["balance"]+        (ReqArg+            (\arg opt -> return opt { optBalance = readColumn arg})+            "<column number>")+        "Balance Column"+    , Option "t" ["text"]+        (ReqArg+            (\arg opt -> return opt { optText = readColumns arg})+            "<column numbers>")+        "Text Columns"+    , Option "l" ["longtext"]+        (ReqArg+            (\arg opt -> return opt { optLongText = readColumns arg})+            "<column numbers>")+        "Long Text Columns"+    , Option "s" ["skip"]+        (ReqArg+            (\arg opt -> case (readColumn arg) of+                Nothing -> return opt+                Just r  -> return opt { optSkip = r} )+            "<number of rows>")+        "Rows to Skip before reading"++    , Option "v" ["verbose"]+        (NoArg+            (\opt -> return opt { optVerbose = True }))+        "Enable verbose messages"++    , Option "V" ["version"]+        (NoArg+            (\_ -> do+                hPutStrLn stderr "Version 0.2"+                exitWith ExitSuccess))+        "Print version"++    , Option "h" ["help"]+        (NoArg+            (\_ -> do+                prg <- getProgName+                hPutStrLn stderr (usageInfo (useage prg) options)+                exitWith ExitSuccess))+        "Show help"+    ]++useage :: String -> String+useage prog = prog ++ "\n\n" +++    "author : Ingolf Wagner\n\n" +++    header ++ "\n\n" +++    information ++ "\n\n"+    where   header      = "Converting CSV files to Qif Files"+            information = "all column numbers start at 0!"+++readColumn :: String -> Maybe Int+readColumn input = readMaybe input++readColumns :: String -> [Int]+readColumns input = catMaybes $ map readColumn $ splitOn "," input+
+ src/Parser.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE ScopedTypeVariables #-}+-----------------------------------------------------------------------------+--+-- Module      :  Parser+-- Copyright   :  (c) Ingolf Wagner+-- License     :  BSD3+--+-- Maintainer  :  Ingolf Wagner <palipalo9@googlemail.com>+-- Stability   :  unstable+-- Portability :+--+-- | This package should create a parser for csv files.+-- it's just a wrapper+--+-----------------------------------------------------------------------------++module Parser where++import Data.Char+import Data.Spreadsheet+import Control.Monad.Exception.Asynchronous.Lazy(Exceptional(..))+import Data.List+import System.IO.Error+import System.IO++import Qifer+++++type ParseError = String++-- | parse csv file to read transactions from it+parseCSVFromFile :: Int -> String -> Char -> IO (Either ParseError CSV)+parseCSVFromFile skip file separator = do+    firstContent <- readFile file+    let content           = unlines . separatorAtEndFix . drop skip . lines $ firstContent+        separatorAtEndFix = map (\l -> l ++ " ")+    case (fromString '\n' separator content) of+        Exceptional (Just s) result -> do+            hPutStrLn stderr s+            return $ Right $ strip result+        Exceptional Nothing result -> return $ Right $ strip result++strip :: CSV -> CSV+strip = map (\line -> map (\word -> stripWord word) line)+    where   stripWord     = dropWhile dontNeed . dropWhileEnd dontNeed+            dontNeed c    = (c == '"') || (isSpace c)+
+ src/Qifer.hs view
@@ -0,0 +1,91 @@+-----------------------------------------------------------------------------+--+-- Module      :  Qifer+-- Copyright   :  (c) Ingolf Wagner+-- License     :  BSD3+--+-- Maintainer  :  Ingolf Wagner <palipalo9@googlemail.com>+-- Stability   :  unstable+-- Portability :+--+-- |+--+-----------------------------------------------------------------------------++module Qifer where++import Data.List+import Text.Regex.TDFA++type CSV = [Row]+type Row = [Column]+type Column = String++data Transaction = Transaction { date :: String+                                , description :: String+                                , text :: String+                                , balance :: String+                               } deriving (Eq,Show)++type Position = Int++data Rule = Rule { dateField :: Position+                   , descField :: [Position]+                   , textField :: [Position]+                   , balanceField :: Position+                 } deriving (Show)++highestPosition :: Rule -> Position+highestPosition (Rule dF tF ltF bF) =+    maximum . concat $ [[dF],tF,ltF,[bF]]++toTransactions :: Rule -> CSV -> [Transaction]+toTransactions _ []        = []+toTransactions rule (c:sv)+    | (highestPosition rule) >= (length c) = stepDeeper+    | otherwise                            = transform : stepDeeper+    where pick n     = (c !! (n rule))+          pock n     = concat . intersperse " " . map (\k -> c !! k ) $ (n rule)+          stepDeeper = toTransactions rule sv+          transform  = (Transaction (pick dateField)+                                    (pock descField)+                                    (pock textField)+                                    (pick balanceField))++qifHeader :: String+qifHeader = "!Type:Bank"++toQif :: Transaction -> [String]+toQif trans = ["P" ++ d, "T" ++ money, "D" ++ time, "M" ++ msg]+    where   d     = (description trans)+            money = (balance trans)+            time  = (date trans)+            msg   = (text trans)++transToQif :: [Transaction] -> [String]+transToQif trans = qifHeader : (foo trans)+    where   foo []     = []+            foo (t:ts) = (toQif t) ++ ["^"] ++ (foo ts)++-- | updates a Transaction if regex works+updateTransaction :: String -> String -> Transaction -> Transaction+updateTransaction regex replacement transaction =+    transaction { description = updateDesc }+    where+        updateDesc  = if matches then replacement else (description transaction)+        matches     = (description transaction) =~ regex :: Bool++-- | very inefficient but better than nothing+update :: [(String,String)] -> [Transaction] -> [Transaction]+update _ []         = []+update regex (t:ts) = (updateSingle regex t) : (update regex ts)+    where+        updateSingle [] r                                 = r+        updateSingle ((rgx,replacement):rest) transaction =+            if updatedTrans == transaction+            then+                updateSingle rest transaction+            else+                updatedTrans+            where+                updatedTrans = updateTransaction rgx replacement transaction
+ test/Tester.hs view
@@ -0,0 +1,51 @@+-----------------------------------------------------------------------------+--+-- Module      :  Tester+-- Copyright   :  (c) Ingolf Wagner+-- License     :  BSD3+--+-- Maintainer  :  Ingolf Wagner <palipalo9@googlemail.com>+-- Stability   :  unstable+-- Portability :+--+-- |+--+-----------------------------------------------------------------------------++module Main (main) where+++import Test.Hspec+import Qifer++main :: IO ()+main = hspec $ do+  describe "Csv" $ do+    it "should transform CSV" $ do+        let rule = (Rule 1 [2] [0,3] 4)+            trans= (Transaction "2" "3" "1 4" "5")+            csv  = [["1","2","3","4","5"]]+        (toTransactions rule csv) `shouldBe` [trans]+    it "should generate Qif" $ do+        let trans = (Transaction "heute" "short" "long bla bla" "100$")+            qif   = ["Pshort","T100$","Dheute","Mlong bla bla"]+        (toQif trans) `shouldBe` qif+    it "should generate Qif File content" $ do+        let trans = (Transaction "heute" "short" "long bla bla" "100$")+            qif   = ["!Type:Bank","Pshort","T100$","Dheute","Mlong bla bla","^"]+        (transToQif [trans]) `shouldBe` qif+    it "should update a transaction" $ do+        let trans  = (Transaction "heute" "short" "long bla bla" "100$")+            theUpdate = (Transaction "heute" "bam" "long bla bla" "100$")+        (updateTransaction "ort" "bam" trans) `shouldBe` theUpdate+        (updateTransaction "foo" "bam" trans) `shouldBe` trans+    it "should update multiple transactions" $ do+        let transA       = (Transaction "heute"  "short story long its .." "long bla bla" "100$")+            transB       = (Transaction "morgen" "balla ball bam bam boom"   "long bla bla" "100$")+            transA1      = (Transaction "heute"  "found story" "long bla bla" "100$")+            transB1      = (Transaction "morgen" "found balla" "long bla bla" "100$")+            regex        = [("story","found story"),("balla","found balla")]+            transactions = [transA,transB]+        (update regex transactions) `shouldBe` [transA1,transB1]++