packages feed

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
@@ -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]--