csv-to-qif 0.3.2 → 0.3.3
raw patch · 11 files changed
+571/−383 lines, 11 filesdep +QuickCheckdep +parsec
Dependencies added: QuickCheck, parsec
Files
- LICENSE +0/−24
- LICENSE.txt +14/−0
- csv-to-qif.cabal +41/−16
- main/Main.hs +71/−91
- main/QifOptions.hs +123/−122
- src/Parser.hs +19/−23
- src/QifData.hs +48/−0
- src/Qifer.hs +43/−56
- test/Main.hs +80/−0
- test/QifParser.hs +132/−0
- test/Tester.hs +0/−51
− LICENSE
@@ -1,24 +0,0 @@-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.
+ LICENSE.txt view
@@ -0,0 +1,14 @@+This file is part of csv-to-qif.++csv-to-qif is free software: you can redistribute it and/or modify+it under the terms of the GNU General Public License as published by+the Free Software Foundation, either version 3 of the License, or+(at your option) any later version.++csv-to-qif is distributed in the hope that it will be useful,+but WITHOUT ANY WARRANTY; without even the implied warranty of+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+GNU General Public License for more details.++You should have received a copy of the GNU General Public License+along with csv-to-qif. If not, see <http://www.gnu.org/licenses/>.
csv-to-qif.cabal view
@@ -1,14 +1,14 @@ name: csv-to-qif-version: 0.3.2+version: 0.3.3 cabal-version: >=1.10 build-type: Simple license: BSD3-license-file: LICENSE+license-file: LICENSE.txt copyright: (c) Ingolf Wagner-maintainer: Ingolf Wagner <palipalo9@googlemail.com>+maintainer: Ingolf Wagner <csv-to-qif@ingolf-wagner.de> stability: stable-homepage: https://github.com/mrVanDalo/csv-to-qif/-bug-reports: mailto:palipalo9@gmail.com+homepage: http://mrvandalo.github.io/csv-to-qif/+bug-reports: mailto:csv-to-qif@ingolf-wagner.de 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.@@ -16,6 +16,19 @@ author: Ingolf Wagner data-dir: "" +tested-With: GHC == 7.10.1+ , GHC == 7.10.2+ , GHC == 7.10.3+ , GHC == 7.6.1+ , GHC == 7.6.2+ , GHC == 7.6.3+ , GHC == 7.8.1+ , GHC == 7.8.2+ , GHC == 7.8.3+ , GHC == 7.8.4+ , GHC == 8.0.1+ , GHC == 8.0.2+ source-repository head type: git location: git@github.com:mrVanDalo/csv-to-qif.git@@ -28,29 +41,41 @@ split >= 0.2.2, regex-tdfa >= 1.2.0, spreadsheet >= 0.1.3.3, - explicit-exception >= 0.1.7+ explicit-exception >= 0.1.7,+ parsec >= 3.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+ hs-source-dirs: main src+ other-modules: QifOptions+ Parser+ QifData+ Qifer test-suite tester- build-depends: base >= 4.6 && < 5, - split >= 0.2.2, - regex-tdfa >= 1.2.0,- Cabal >= 1.9.2, - hspec >= 1.8+ build-depends: base >= 4.6 && < 5,+ split >= 0.2.2,+ regex-tdfa >= 1.2.0,+ Cabal >= 1.9.2,+ hspec >= 1.8,+ spreadsheet >= 0.1.3.3, + explicit-exception >= 0.1.7,+ parsec >= 3.1.7,+ QuickCheck >= 2.8.1 type: exitcode-stdio-1.0- main-is: Tester.hs++ main-is: Main.hs buildable: True default-language: Haskell2010 default-extensions: OverloadedStrings- hs-source-dirs: src test- other-modules: Parser+ hs-source-dirs: test src+ other-modules: Parser+ QifData+ QifParser ghc-options: -Wall
main/Main.hs view
@@ -8,113 +8,93 @@ -- 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--+module Main+ ( main+ ) where +import Data.List.Split+import Data.Maybe+import System.Console.GetOpt+import System.Environment+import System.Exit+import System.IO +import Parser+import Qifer+import QifOptions -- | the famous main method main = do- args <- getArgs-+ args <- getArgs -- Parse options, getting a list of option actions- let (actions, nonOptions, errors) = getOpt RequireOrder options args-+ 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)-+ 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 :: 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)---+ 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- }+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----+ 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
@@ -11,138 +11,139 @@ -- | -- ------------------------------------------------------------------------------ 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--+import Data.List.Split (splitOn)+import Data.Maybe (mapMaybe)+import System.Console.GetOpt (ArgDescr (..), OptDescr (..), usageInfo)+import System.Environment (getProgName)+import System.Exit (exitSuccess)+import System.IO (hPutStrLn, stderr)+import Text.Read (readMaybe) -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)+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>"-+startOptions =+ Options+ { optVerbose = False+ , optInput = ""+ , optOutput = ""+ , optDate = Nothing+ , optBalance = Nothing+ , optText = []+ , optLongText = []+ , optSkip = 0+ , optSeparator = ','+ , optUpdater = Nothing+ } -options :: [ OptDescr (Options -> IO Options) ]+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"- ]+ [ 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"+ exitSuccess))+ "Print version"+ , Option+ "h"+ ["help"]+ (NoArg+ (\_ -> do+ prg <- getProgName+ hPutStrLn stderr (usageInfo (useage prg) options)+ 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!"-+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+readColumn = readMaybe readColumns :: String -> [Int]-readColumns input = catMaybes $ map readColumn $ splitOn "," input-+readColumns input = mapMaybe readColumn (splitOn "," input)
src/Parser.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-}+ ----------------------------------------------------------------------------- -- -- Module : Parser@@ -13,37 +14,32 @@ -- 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--+import Control.Monad.Exception.Asynchronous.Lazy (Exceptional (..))+import Data.Char (isSpace)+import Data.List (dropWhileEnd)+import Data.Spreadsheet (fromString)+import System.IO (hPutStrLn, stderr) +import Qifer (CSV) 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+ firstContent <- readFile file+ let content = unlines . separatorAtEndFix . drop skip . lines $ firstContent+ separatorAtEndFix = map (++ " ")+ case fromString '\n' separator content of+ Exceptional (Just s) res -> do+ hPutStrLn stderr s+ return $ Right $ strip res+ Exceptional Nothing res -> return $ Right $ strip res strip :: CSV -> CSV-strip = map (\line -> map (\word -> stripWord word) line)- where stripWord = dropWhile dontNeed . dropWhileEnd dontNeed- dontNeed c = (c == '"') || (isSpace c)-+strip = map (map stripWord)+ where+ stripWord = dropWhile dontNeed . dropWhileEnd dontNeed+ dontNeed c = (c == '"') || isSpace c
+ src/QifData.hs view
@@ -0,0 +1,48 @@+-----------------------------------------------------------------------------+--+-- Module : QifData+-- Copyright : (c) Ingolf Wagner, (c) Jonathan Kochems+-- License : BSD3+--+-- Maintainer : Jonathan Kochems <jonathan.kochems@gmail.com>+-- Stability : unstable+-- Portability :+--+--+--+-----------------------------------------------------------------------------+module QifData where++data Qif = Qif+ { typeinfo :: String+ , transactions :: [Transaction]+ } deriving (Eq, Show)++typeinfoToString :: String -> String+typeinfoToString typevalue = "!Type:" ++ typevalue++qifToLines :: Qif -> [String]+qifToLines qif = typeinfoToString tinfo : foo trans+ where+ tinfo = typeinfo qif+ trans = transactions qif+ foo [] = []+ foo (t:ts) = toQif t ++ ["^"] ++ foo ts++qifToString :: Qif -> String+qifToString = unlines . qifToLines++data Transaction = Transaction+ { date :: String+ , description :: String+ , text :: String+ , balance :: String+ } deriving (Eq, Show)++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
src/Qifer.hs view
@@ -11,81 +11,68 @@ -- | -- ------------------------------------------------------------------------------ module Qifer where -import Data.List-import Text.Regex.TDFA+import QifData (Qif (..), Transaction (..), qifToLines)+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 Column = String type Position = Int -data Rule = Rule { dateField :: Position- , descField :: [Position]- , textField :: [Position]- , balanceField :: Position- } deriving (Show)+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]]+highestPosition (Rule dF tF ltF bF) = maximum . concat $ [[dF], tF, ltF, [bF]] toTransactions :: Rule -> CSV -> [Transaction]-toTransactions _ [] = []+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)+ | highestPosition rule >= length c = stepDeeper+ | otherwise = transform : stepDeeper+ where+ pick n = c !! n rule+ pock n = unwords . map (\k -> c !! k) $ n rule+ stepDeeper = toTransactions rule sv+ transform =+ Transaction+ (pick dateField)+ (pock descField)+ (pock textField)+ (pick balanceField) transToQif :: [Transaction] -> [String]-transToQif trans = qifHeader : (foo trans)- where foo [] = []- foo (t:ts) = (toQif t) ++ ["^"] ++ (foo ts)+transToQif trans = qifToLines Qif {typeinfo = "Bank", transactions = trans} -- | 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+ 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+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/Main.hs view
@@ -0,0 +1,80 @@+-----------------------------------------------------------------------------+--+-- 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 Test.QuickCheck+import Data.Char(isPrint)++import Qifer+import QifData+import QifParser+++main :: IO ()+main = hspec $+ 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")]+ transactionsToTest = [transA,transB]+ update regex transactionsToTest `shouldBe` [transA1,transB1]+ it "taking a qif, printing it as a string, and parsing it should result in the original qif" $+ property $ forAll qif $ \qif -> qifFromString (qifToString qif) == qif++++transaction :: Gen Transaction+transaction = do+ i <- choose(0,20)+ j <- choose(0,20)+ k <- choose(0,20)+ m <- choose(0,20)+ transactionDate <- suchThat (vector i) isText+ transactionDescription <- suchThat (vector j) isText+ transactionText <- suchThat (vector k) isText+ transactionBalance <- suchThat (vector m) isText+ return Transaction{ date = transactionDate, description = transactionDescription, text = transactionText , balance = transactionBalance }++qif :: Gen Qif+qif = do i <- choose(0,20)+ typeinfoToTest <- suchThat (vector i) isText+ transactionsToTest <- listOf transaction+ return Qif{ typeinfo = typeinfoToTest, transactions = transactionsToTest }++isText :: String -> Bool+isText = all isPrint
+ test/QifParser.hs view
@@ -0,0 +1,132 @@+-----------------------------------------------------------------------------+--+-- Module : QifParser+-- Copyright : (c) Jonathan Kochems+-- License : BSD3+--+-- Maintainer : Jonathan Kochems <jonathan.kochems@google.com>+-- Stability : unstable+-- Portability :+--+--+--+-----------------------------------------------------------------------------+module QifParser where++import QifData+import Text.ParserCombinators.Parsec++{- **************************************************+ Parsers for transaction fields+*************************************************** -}+-- | Helper type to tag transaction field data+data TransactionField+ = D String+ | P String+ | M String+ | T String++-- | Parser for date field in transaction+date_parser :: GenParser Char st TransactionField+date_parser = do+ _ <- string "D"+ date <- manyTill (noneOf ['\n', '\r']) (newline_or_eof)+ return $ D date++-- | Parser for description field in transaction+description_parser :: GenParser Char st TransactionField+description_parser = do+ _ <- string "P"+ description <- manyTill (noneOf ['\n', '\r']) newline_or_eof+ return $ P description++-- | Parser for text field in transaction+text_parser :: GenParser Char st TransactionField+text_parser = do+ _ <- string "M"+ text <- manyTill (noneOf ['\n', '\r']) newline_or_eof+ return $ M text++-- | Parser for balance field in transaction+balance_parser :: GenParser Char st TransactionField+balance_parser = do+ _ <- string "T"+ balance <- manyTill (noneOf ['\n', '\r']) newline_or_eof+ return $ T balance++{- **************************************************+ transactions+*************************************************** -}+-- | Parser for one whole transaction+transaction_parser :: GenParser Char st Transaction+transaction_parser = do+ fields <-+ manyTill+ (choice [date_parser, description_parser, text_parser, balance_parser])+ (try $ lookAhead $ seperator_parser)+ return $+ foldl+ fieldToTransaction+ Transaction {date = "", description = "", text = "", balance = ""}+ fields+ where+ fieldToTransaction trans (D date) = trans {date = date}+ fieldToTransaction trans (P desc) = trans {description = desc}+ fieldToTransaction trans (M text) = trans {text = text}+ fieldToTransaction trans (T bal) = trans {balance = bal}++-- | Parser for transaction seperator '^'+seperator_parser :: GenParser Char st ()+seperator_parser = do+ _ <- string "^"+ _ <- newline_or_eof+ return ()++-- | Parser for a list of transactions+transactions_parser :: GenParser Char st [Transaction]+transactions_parser = sepEndBy transaction_parser seperator_parser++{- **************************************************+ Qif file+*************************************************** -}+-- | Parser for type of qif (Bank, CCard, etc.)+type_parser :: GenParser Char st String+type_parser = do+ _ <- string "!Type:"+ typeinfo <- manyTill (noneOf ['\n', '\r']) newline_or_eof+ return typeinfo++-- | Parser for a qif file+qif_file_parser :: GenParser Char st Qif+qif_file_parser = do+ typeinfo <- type_parser+ transactions <- transactions_parser+ return Qif {typeinfo = typeinfo, transactions = transactions}++-- | obtain qif from string+qifFromString s =+ fromRight (Qif {typeinfo = "Bank", transactions = []}) $+ parse qif_file_parser "(unknown)" s++-- | IO Monad helper function: reads and parses qif file+-- * if parsing successful returns qif data structure, otherwise returns "empty" qif+parse_qif_file filename = do+ contents <- readFile filename+ return $ qifFromString contents++{- **************************************************+ Helper functions+*************************************************** -}+-- | test out parser on string+-- test_parser p s = parse p "(unknown)" s+-- | parses newline but throws away '\n'+newline_skip = do+ _ <- newline+ return ()++-- | skips newline but also succeeds at end-of-file+newline_or_eof = choice [newline_skip, eof]++-- | quick and dirty Either exception handling with default value+fromRight d (Right x) = x+fromRight d _ = d
− test/Tester.hs
@@ -1,51 +0,0 @@------------------------------------------------------------------------------------ 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]--