packages feed

qif 1.0.0 → 1.1.0

raw patch · 3 files changed

+575/−62 lines, 3 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Data.QIF: Account :: Text -> AccountType -> Text -> Maybe Currency -> Maybe Day -> Currency -> Account
- Data.QIF: Category :: Text -> Text -> CategoryKind -> Bool -> Maybe Currency -> Maybe Word -> Category
- Data.QIF: QIF :: [Account] -> [Category] -> [Security] -> [(Account, [InvTransaction])] -> [(Account, [Transaction])] -> QIF
- Data.QIF: Security :: Text -> Text -> SecurityType -> Maybe Text -> Security
- Data.QIF: SplitItem :: Text -> Currency -> Text -> SplitItem
- Data.QIF: TradeInfo :: Day -> Text -> Maybe Currency -> Maybe ShareQuantity -> Maybe Currency -> Currency -> TradeInfo
- Data.QIF: Transaction :: Day -> Text -> Text -> Currency -> Maybe Word -> Maybe Text -> Bool -> Bool -> [SplitItem] -> Transaction
- Data.QIF: TransferInfo :: Day -> Text -> Text -> Currency -> Bool -> Text -> [SplitItem] -> TransferInfo
- Data.QIF: [_accountBalanceDate] :: Account -> Maybe Day
- Data.QIF: [_accountBalance] :: Account -> Currency
- Data.QIF: [_accountCreditLimit] :: Account -> Maybe Currency
- Data.QIF: [_accountDescription] :: Account -> Text
- Data.QIF: [_accountName] :: Account -> Text
- Data.QIF: [_accountType] :: Account -> AccountType
- Data.QIF: [_catBudgetAmount] :: Category -> Maybe Currency
- Data.QIF: [_catDescription] :: Category -> Text
- Data.QIF: [_catIsTaxRelated] :: Category -> Bool
- Data.QIF: [_catKind] :: Category -> CategoryKind
- Data.QIF: [_catName] :: Category -> Text
- Data.QIF: [_catTaxScheduleInfo] :: Category -> Maybe Word
- Data.QIF: [_entAmount] :: Transaction -> Currency
- Data.QIF: [_entCategory] :: Transaction -> Maybe Text
- Data.QIF: [_entCleared] :: Transaction -> Bool
- Data.QIF: [_entDate] :: Transaction -> Day
- Data.QIF: [_entMemo] :: Transaction -> Text
- Data.QIF: [_entNumber] :: Transaction -> Maybe Word
- Data.QIF: [_entParty] :: Transaction -> Text
- Data.QIF: [_entReimbursable] :: Transaction -> Bool
- Data.QIF: [_entSplits] :: Transaction -> [SplitItem]
- Data.QIF: [_entryAmount] :: SplitItem -> Currency
- Data.QIF: [_entryCategory] :: SplitItem -> Text
- Data.QIF: [_entryMemo] :: SplitItem -> Text
- Data.QIF: [_qifAccounts] :: QIF -> [Account]
- Data.QIF: [_qifCategories] :: QIF -> [Category]
- Data.QIF: [_qifInvestmentActions] :: QIF -> [(Account, [InvTransaction])]
- Data.QIF: [_qifNormalActions] :: QIF -> [(Account, [Transaction])]
- Data.QIF: [_qifSecurities] :: QIF -> [Security]
- Data.QIF: [_secGoal] :: Security -> Maybe Text
- Data.QIF: [_secName] :: Security -> Text
- Data.QIF: [_secTicker] :: Security -> Text
- Data.QIF: [_secType] :: Security -> SecurityType
- Data.QIF: [_tradeCommission] :: TradeInfo -> Maybe Currency
- Data.QIF: [_tradeDate] :: TradeInfo -> Day
- Data.QIF: [_tradeQuantity] :: TradeInfo -> Maybe ShareQuantity
- Data.QIF: [_tradeSecurity] :: TradeInfo -> Text
- Data.QIF: [_tradeSharePrice] :: TradeInfo -> Maybe Currency
- Data.QIF: [_tradeTotalAmount] :: TradeInfo -> Currency
- Data.QIF: [_transAccount] :: TransferInfo -> Text
- Data.QIF: [_transAmount] :: TransferInfo -> Currency
- Data.QIF: [_transCleared] :: TransferInfo -> Bool
- Data.QIF: [_transDate] :: TransferInfo -> Day
- Data.QIF: [_transMemo] :: TransferInfo -> Text
- Data.QIF: [_transSplits] :: TransferInfo -> [SplitItem]
- Data.QIF: [_transSummary] :: TransferInfo -> Text
- Data.QIF: data E4
- Data.QIF: digitToNum :: Num a => Char -> a
- Data.QIF: getP :: (s -> Parser s) -> s -> ASetter' s a -> Parser a -> Parser s
- Data.QIF: getP' :: (s -> Parser s) -> s -> ASetter' s (Maybe a) -> Parser a -> Parser s
- Data.QIF: newline :: Builder
- Data.QIF: parseCleared :: Parser Bool
- Data.QIF: parseString :: Parser Text
- Data.QIF: parseTransactionList :: Parser [Transaction]
- Data.QIF: pennies :: Currency -> Integer
- Data.QIF: put :: Char -> s -> Lens' s a -> (a -> Builder) -> Builder
- Data.QIF: putm :: Char -> s -> Lens' s (Maybe a) -> (a -> Builder) -> Builder
- Data.QIF: putm' :: Char -> s -> Lens' s (Maybe a) -> (a -> Builder) -> Builder
- Data.QIF: qifInvestmentActions :: Lens' QIF [(Account, [InvTransaction])]
- Data.QIF: qifNormalActions :: Lens' QIF [(Account, [Transaction])]
- Data.QIF: renderCleared :: Bool -> Builder
- Data.QIF: renderTradeInfo :: Text -> TradeInfo -> Builder
- Data.QIF: renderTransactionList :: [Transaction] -> Builder
- Data.QIF: renderTransferInfo :: TransferInfo -> Builder
+ Data.QIF: parseBankEntryList :: Parser [Transaction]
+ Data.QIF: qifInvestmentTransactions :: Lens' QIF [(Account, [InvTransaction])]
+ Data.QIF: qifNormalTransactions :: Lens' QIF [(Account, [Transaction])]
+ Data.QIF: renderBankEntryList :: [Transaction] -> Builder
+ Data.QIF: renderQIF :: QIF -> Builder

Files

Test.hs view
@@ -9,6 +9,7 @@ import           Data.Text.Lazy(Text) import           Data.Text.Lazy.Builder(Builder, toLazyText) import           Data.Time(Day(..), fromGregorian, addDays)+import           Lens.Micro(set) import           Test.QuickCheck import           Test.Tasty(TestTree, testGroup, defaultMain) import           Test.Tasty.HUnit(testCase, (@=?))@@ -120,29 +121,36 @@ testSecurity =   testGroup "Security Block" [     testCase "Alaska Airlines"-      (Security "Alaska Air Group, Inc" "ALK" Stock Nothing @=?+      (security "Alaska Air Group, Inc" "ALK" Stock Nothing @=?         runP parseSecurity "NAlaska Air Group, Inc\nSALK\nTStock\n^\n")   , testCase "Vanguard S&P"-      (Security "Vanguard S&P 500 Index,Investor Shares" "VFINX"+      (security "Vanguard S&P 500 Index,Investor Shares" "VFINX"                 MutualFund Nothing          @=?         runP parseSecurity           "NVanguard S&P 500 Index,Investor Shares\nSVFINX\nTMutual Fund\n^\n")   , testCase "Vanguard ETF"-      (Security "Vanguard ETF" "VET" ETF (Just "House") @=?+      (security "Vanguard ETF" "VET" ETF (Just "House") @=?         runP parseSecurity "NVanguard ETF\nSVET\nTETF\nGHouse\n^\n")   , testProperty "Security block serialization"       (standardSerializer parseSecurity renderSecurity)   ] +security :: S.Text -> S.Text -> SecurityType -> Maybe S.Text -> Security+security n t y g =+  set secName   n $+  set secTicker t $+  set secType   y $+  set secGoal   g emptySecurity+ testAccount :: TestTree testAccount =   testGroup "Accounts" [     testCase "Umpqua Account"-      (Account "Umpqua Bank" BankAccount "Checking" Nothing Nothing 1234.12 @=?+      (account "Umpqua Bank" BankAccount "Checking" Nothing Nothing 1234.12 @=?         runP parseAccount "NUmpqua Bank\nDChecking\nX\nTBank\nB$1234.12\n^\n")   , testCase "Blue Bank"-      (Account "Blue Bank" CreditCardAccount "" (Just 40)+      (account "Blue Bank" CreditCardAccount "" (Just 40)                (Just (fromGregorian 2012 2 14)) (-520)         @=?          runP parseAccount@@ -153,63 +161,109 @@       (standardSerializer parseAccountHeader renderAccountHeader)   ] +account :: S.Text -> AccountType -> S.Text ->+           Maybe Currency -> Maybe Day -> Currency ->+           Account+account n t d cl bd b =+  set accountName        n  $+  set accountType        t  $+  set accountDescription d  $+  set accountCreditLimit cl $+  set accountBalanceDate bd $+  set accountBalance     b  emptyAccount+ testCategory :: TestTree testCategory =   testGroup "Categories" [     testCase "Auto"-      (Category "Auto" "Automobile-related expenses" Expense False Nothing Nothing @=?+      (category "Auto" "Automobile-related expenses" Expense False Nothing Nothing @=?         runP parseCategory "NAuto\nDAutomobile-related expenses\nE\n^\n")   , testCase "Registration"-      (Category "Registration" "" Expense True Nothing (Just 535) @=?+      (category "Registration" "" Expense True Nothing (Just 535) @=?         runP parseCategory "NRegistration\nD\nT\nE\nR535\n^\n")   , testCase "Salary"-      (Category "Salary" "Income" Income True Nothing Nothing @=?+      (category "Salary" "Income" Income True Nothing Nothing @=?         runP parseCategory "NSalary\nDIncome\nT\nI\n^\n")   , testCase "Dinner"-      (Category "Dinner" "Yum yum" Expense False (Just 200) Nothing @=?+      (category "Dinner" "Yum yum" Expense False (Just 200) Nothing @=?         runP parseCategory "NDinner\nDYum yum\nE\nB200.\n^\n")   , testProperty "Category serialization"       (standardSerializer parseCategory renderCategory)   ] +category :: S.Text -> S.Text -> CategoryKind -> Bool ->+            Maybe Currency -> Maybe Word ->+            Category+category n d k itr ba tsi =+  set catName            n   $+  set catDescription     d   $+  set catKind            k   $+  set catIsTaxRelated    itr $+  set catBudgetAmount    ba  $+  set catTaxScheduleInfo tsi emptyCategory+ testTransaction :: TestTree testTransaction =   testGroup "Transactions" [     testCase "Roost"-      (Transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing+      (transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing                    (Just "Dining") True False [] @=?         (runP parseTransaction           "D2/14/12\nPRoost\nMYay fun\nT-25\nCX\nLDining\n^\n"))   , testCase "Uncleared Roost"-      (Transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing+      (transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing                    (Just "Dining") False False [] @=?         (runP parseTransaction           "D2/14/12\nPRoost\nMYay fun\nT-25.\nLDining\n^\n"))   , testCase "Business Roost"-      (Transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing+      (transaction (fromGregorian 2012 2 14) "Roost" "Yay fun" (-25) Nothing                    (Just "Dining") True True [] @=?         (runP parseTransaction           "D2/14/12\nPRoost\nMYay fun\nT-25.00\nCX\nF\nLDining\n^\n"))   , testCase "Paycheck"-      (Transaction (fromGregorian 2018 3 4) "Galois" "" 50 Nothing Nothing-                   True False [SplitItem "" (-30) "Checking",-                               SplitItem "" (-20) "Savings"] @=?+      (transaction (fromGregorian 2018 3 4) "Galois" "" 50 Nothing Nothing+                   True False [set entryAmount (-30) $+                               set entryCategory "Checking" emptySplitItem,+                               set entryAmount (-20) $+                               set entryCategory "Savings" emptySplitItem] @=?         (runP parseTransaction          "D3/4/18\nPGalois\nM\nT50.0\nCX\nSChecking\nE\n$-30.0\nSSavings\nE\n$-20\n^\n"))   , testProperty "Transaction serialization"       (standardSerializer parseTransaction renderTransaction)   ] +transaction :: Day -> S.Text -> S.Text -> Currency ->+               Maybe Word -> Maybe S.Text -> Bool -> Bool -> [SplitItem] ->+               Transaction+transaction d p m a n c l r s =+  set entDate         d $+  set entParty        p $+  set entMemo         m $+  set entAmount       a $+  set entNumber       n $+  set entCategory     c $+  set entCleared      l $+  set entReimbursable r $+  set entSplits       s emptyTransaction+ testInvTransaction :: TestTree testInvTransaction =   testGroup "Interest Transactions" [     testCase "Dividend"-      (Dividend (TradeInfo (fromGregorian 2065 7 7) "SEC" Nothing (Just 0) (Just 0) 0)+      (Dividend (set tradeSecurity "SEC" $+                 set tradeSharePrice Nothing $+                 set tradeQuantity (Just 0) $+                 set tradeCommission (Just 0) $+                 set tradeTotalAmount 0 (emptyTrade (fromGregorian 2065 7 7)))         @=?          (runP parseInvTransaction             "D7/7/65\nNDiv\nYSEC\nQ0\nO0.00\nT0\n^\n"))   , testCase "Dividend, with share price"-      (Dividend (TradeInfo (fromGregorian 2065 7 7) "SEC" (Just 0) (Just 0) (Just 0) 0)+      (Dividend (set tradeSecurity "SEC" $+                 set tradeSharePrice (Just 0) $+                 set tradeQuantity (Just 0) $+                 set tradeCommission (Just 0) $+                 set tradeTotalAmount 0 (emptyTrade (fromGregorian 2065 7 7)))         @=?          (runP parseInvTransaction             "D7/7/65\nNDiv\nYSEC\nI0\nQ0\nO0.00\nT0\n^\n"))@@ -229,7 +283,7 @@   , testProperty  "Investment Entries"       (standardSerializer parseInvestmentEntries renderInvestmentEntries)   , testProperty  "Bank Entries"-      (standardSerializer parseTransactionList renderTransactionList)+      (standardSerializer parseBankEntryList renderBankEntryList)   , testProperty  "Credit Card Entries"       (standardSerializer parseCreditCardEntryList renderCreditCardEntryList)   , testProperty  "Asset Entries"@@ -277,8 +331,15 @@                          PreciousMetal, Commodity, StockOption, Other ]  instance Arbitrary Security where-  arbitrary = Security <$> arbitrary <*> arbitrary <*> arbitrary-                       <*> arbitrary+  arbitrary =+    do n <- arbitrary+       t <- arbitrary+       y <- arbitrary+       g <- arbitrary+       return $ set secName   n+              $ set secTicker t+              $ set secType   y+              $ set secGoal   g emptySecurity  instance Arbitrary S.Text where   arbitrary =@@ -291,21 +352,48 @@                          InvestmentAccount, AssetAccount, LiabilityAccount ]  instance Arbitrary Account where-  arbitrary = Account <$> arbitrary <*> arbitrary <*> arbitrary-                      <*> arbitrary <*> arbitrary <*> arbitrary+  arbitrary =+    do (n, t, d, cl, bd, b) <- arbitrary+       return $ set accountName        n+              $ set accountType        t+              $ set accountDescription d+              $ set accountCreditLimit cl+              $ set accountBalanceDate bd+              $ set accountBalance     b emptyAccount  instance Arbitrary Category where-  arbitrary = Category <$> arbitrary <*> arbitrary <*> elements [Income,Expense]-                       <*> arbitrary <*> arbitrary <*> arbitrary+  arbitrary =+    do k <- elements [Income, Expense]+       (n, d, it, ba, ts) <- arbitrary+       return $ set catName            n+              $ set catDescription     d+              $ set catKind            k+              $ set catIsTaxRelated    it+              $ set catBudgetAmount    ba+              $ set catTaxScheduleInfo ts emptyCategory  instance Arbitrary TradeInfo where-  arbitrary = TradeInfo <$> arbitrary <*> arbitrary <*> arbitrary-                        <*> arbitrary <*> arbitrary <*> arbitrary+  arbitrary =+    do (d, s, p, q, c, a) <- arbitrary+       day <- arbitrary+       return $ set tradeDate        d+              $ set tradeSecurity    s+              $ set tradeSharePrice  p+              $ set tradeQuantity    q+              $ set tradeCommission  c+              $ set tradeTotalAmount a (emptyTrade day)  instance Arbitrary TransferInfo where-  arbitrary = TransferInfo <$> arbitrary <*> arbitrary <*> arbitrary-                           <*> arbitrary <*> arbitrary <*> arbitrary-                           <*> arbitrary+  arbitrary =+    do (d, s, m, a, c, t, p) <- arbitrary+       day <- arbitrary+       return $ set transDate        d+              $ set transSummary     s+              $ set transMemo        m+              $ set transAmount      a+              $ set transCleared     c+              $ set transAccount     t+              $ set transSplits      p (emptyTransfer day)  instance Arbitrary InvTransaction where   arbitrary = oneof [ Buy      <$> arbitrary@@ -316,9 +404,31 @@                     ]  instance Arbitrary SplitItem where-  arbitrary = SplitItem <$> arbitrary <*> arbitrary <*> arbitrary+  arbitrary =+    do (m, a, c) <- arbitrary+       return $ set entryMemo     m+              $ set entryAmount   a+              $ set entryCategory c emptySplitItem  instance Arbitrary Transaction where-  arbitrary = Transaction <$> arbitrary <*> arbitrary <*> arbitrary-                        <*> arbitrary <*> arbitrary <*> arbitrary-                        <*> arbitrary <*> arbitrary <*> arbitrary+  arbitrary =+    do (d,p,m,a,n,c,l,r,s) <- arbitrary+       return $ set entDate         d+              $ set entParty        p+              $ set entMemo         m+              $ set entAmount       a+              $ set entNumber       n+              $ set entCategory     c+              $ set entCleared      l+              $ set entReimbursable r+              $ set entSplits       s emptyTransaction++instance Arbitrary QIF where+  arbitrary =+    do (a, c, s, i, n) <- arbitrary+       return $ set qifAccounts               a+              $ set qifCategories             c+              $ set qifSecurities             s+              $ set qifInvestmentTransactions i+              $ set qifNormalTransactions     n emptyQIF+
qif.cabal view
@@ -1,5 +1,5 @@ name:                qif-version:             1.0.0+version:             1.1.0 synopsis:            A simple QIF file format parser / printer description:         A handy little library for dealing with this awful little                      QIF files in your life.@@ -32,12 +32,14 @@ test-suite test-qif-serialization   type:                exitcode-stdio-1.0   main-is:             Test.hs+  ghc-options:         -Wall   default-language:    Haskell2010   other-extensions:    OverloadedStrings   build-depends:                        attoparsec       >= 0.13  && < 0.15,                        base             >= 4.7  && < 4.10,                        QuickCheck       >= 2.9  && < 3.0,+                       microlens        >= 0.4.7 && < 0.6,                        tasty            >= 0.11 && < 0.15,                        tasty-hunit      >= 0.9  && < 0.12,                        tasty-quickcheck >= 0.8  && < 0.12,
src/Data/QIF.hs view
@@ -1,7 +1,110 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types        #-} {-# LANGUAGE TemplateHaskell   #-}-module Data.QIF+-- |A module for parsing or rendering QIF files.+--+-- QIF is a fairly braindead format designed for transfering financial data+-- between applications. If you're writing a new financial application, you+-- might want to find a newer format to share with other applications (if you+-- can find one), and you definitely shouldn't use this as the database. For one+-- thing, this format uses two-digit years, which is just kind of lazy. Also, it+-- enforces absolutely no consistency constraints in terms of cross-references+-- or transaction sums.+--+-- To parse a QIF file, I suggest using "Data.Attoparsec.Text.Lazy" and a lazy+-- 'Text' data structure, as follows:+--+-- @+--    do txt <- Text.pack `fmap` readFile "my.qif"+--       case parse parseQIF txt of+--         Fail around _ err ->+--           fail ("Parse error (" ++ err ++ ") around :" +++--                 show (Text.take 10 around))+--         Done _ res ->+--           somethingInteresting res+-- @+--+-- To render a QIF file, you can run the builders from "Data.Text.LazyBuilder"+-- directly, as so:+--+-- @+--    Data.Text.Lazy.IO.writeFile "my.qif" (toLazyText (renderQIF myQIF))+-- @+--+module Data.QIF(+         QIF,           emptyQIF+       ,                qifAccounts,              qifCategories+       ,                qifSecurities,            qifInvestmentTransactions+       ,                qifNormalTransactions+       ,                parseQIF,                 renderQIF+       -- * Various lists in QIF+       ,                parseAccountList,         renderAccountList+       ,                parseCategoryList,        renderCategoryList+       ,                parseBankEntryList,       renderBankEntryList+       ,                parseInvestmentEntries,   renderInvestmentEntries+       ,                parseCashEntryList,       renderCashEntryList+       ,                parseCreditCardEntryList, renderCreditCardEntryList+       ,                parseAssetEntryList,      renderAssetEntryList+       ,                parseLiabilityEntryList,  renderLiabilityEntryList+       ,                parseSecurityList,        renderSecurityList+       -- * Account Information+       , Account,         emptyAccount+       ,                  accountName,           accountType+       ,                  accountDescription,    accountCreditLimit+       ,                  accountBalanceDate,    accountBalance+       ,                  parseAccount,          renderAccount+       , AccountType(..), parseAccountType,      renderAccountType+       ,                  parseShortAccountType, renderShortAccountType+       ,                  parseAccountHeader,    renderAccountHeader+       -- * Category Information+       , CategoryKind(..)+       , Category,      emptyCategory+       ,                catName,              catDescription+       ,                catKind,              catIsTaxRelated+       ,                catBudgetAmount,      catTaxScheduleInfo+       ,                parseCategory,        renderCategory+       -- * Transaction Information+       , SplitItem,        emptySplitItem+       ,                   entryMemo,         entryAmount+       ,                   entryCategory+       ,                   parseSplit,        renderSplit+       -- ** Standard Transactions (Bank, Credit Card, etc.)+       , Transaction,      emptyTransaction+       ,                   entDate,           entParty+       ,                   entMemo,           entAmount+       ,                   entNumber,         entCategory+       ,                   entCleared,        entReimbursable+       ,                   entSplits+       ,                   parseTransaction,  renderTransaction+       -- ** Investment Account Transactions+       -- *** Trade Information+       , TradeInfo,        emptyTrade+       ,                   tradeDate,         tradeSecurity+       ,                   tradeSharePrice,   tradeQuantity+       ,                   tradeCommission,   tradeTotalAmount+       -- *** Transfer Information+       , TransferInfo,     emptyTransfer+       ,                   transDate,         transSummary+       ,                   transMemo,         transAmount+       ,                   transCleared,      transAccount+       ,                   transSplits+       -- *** Actual Investment Actions+       , InvTransaction(..)+       ,                   invEntDate+       ,                   parseInvTransaction, renderInvTransaction+       -- * Security Types+       , SecurityType(..), parseSecurityType, renderSecurityType+       , Security,         emptySecurity+       ,                   secName,           secTicker+       ,                   secType,           secGoal+       ,                   parseSecurity,     renderSecurity+       -- * Fixed-width quantities+       , Currency,      parseCurrency,      renderCurrency+       , ShareQuantity, parseShareQuantity, renderShareQuantity+       ,                parseQuantity,      renderQuantity+       -- * Old-school dates+       ,                parseDate,          renderDate+       )  where  import           Control.Monad(when)@@ -20,7 +123,7 @@ import           Data.Word(Word) import           Lens.Micro(Lens',ASetter',lens,set,over) import           Lens.Micro.Extras(view)-import           Lens.Micro.TH(makeLenses)+import           Lens.Micro.TH(makeLensesWith,lensRules,generateSignatures)  -- Fixed-width Quantities: Currency and Share Counts --------------------------- @@ -28,9 +131,17 @@ instance HasResolution E4 where   resolution _ = 10000 +-- |A fixed width implementation of currency, based on the U.S. dollar. Future+-- versions of this library that wish to support other currencies may wish to+-- change this, or to abstract the rest of the library over a currency type. type Currency      = Fixed E2++-- |A fixed-width implementation of quantities for shares. So far, I have seen+-- sites report share quantities to up to four decimal points, henced the value. type ShareQuantity = Fixed E4 +-- |Parse a fixed-width number. Should parse negative values, as well. This does+-- support QIF's annoying "5." notation, as well. parseQuantity :: HasResolution a => Parser (Fixed a) parseQuantity =   do finally <- option id (char '-' >> return negate)@@ -54,9 +165,15 @@       do d <- digitToNum `fmap` digit          afterPoint (acc + (d / place)) (place * 10) +-- |Render a quantity. As opposed to the parser, this output function will+-- always represent numbers to their full precision. renderQuantity :: HasResolution a => Fixed a -> Builder renderQuantity = fromString . show +-- |Parse a currency. This is slightly differentiated from 'parseQuantity' in+-- that it will happily ignore a dollar sign placed in the correct location.+-- Note that this will support negative amounts written as either \"-$500\" or+-- as \"$-500\". parseCurrency :: Parser Currency parseCurrency =   do finally <- option id  (char '-' *> return negate) -- kept to deal with -$1@@ -64,15 +181,21 @@      amount  <- parseQuantity      return (finally amount) +-- |Render a currency. The boolean state whether or not to include a dollar+-- sign. When dollar signs are included, negatives are written as \"-$500\"+-- rather than \"$-500\". renderCurrency :: Bool -> Currency -> Builder renderCurrency showDollar x =   neg <> (if showDollar then singleton '$' else mempty) <> renderQuantity x'  where   (x',neg) = if x < 0 then (negate x, singleton '-') else (x,mempty) ++-- |Parse a share quantity. Currently an alias for 'parseQuantity'. parseShareQuantity :: Parser ShareQuantity parseShareQuantity = parseQuantity +-- |Render a share quantity. Currently an alias for 'renderQuantity'. renderShareQuantity :: ShareQuantity -> Builder renderShareQuantity = renderQuantity @@ -81,6 +204,7 @@  -- Account Types --------------------------------------------------------------- +-- |The type of an account; should be fairly self-explanatory. data AccountType = BankAccount                  | CashAccount                  | CreditCardAccount@@ -89,9 +213,13 @@                  | LiabilityAccount  deriving (Eq, Read, Show) +-- |Parse a fully-rendered account type (e.g, \"!Type:Bank\"), used for+-- section headings. parseAccountType :: Parser AccountType parseAccountType = string "!Type:" *> parseShortAccountType +-- |Parse the short version of an account type (e.g., "Bank"), which is used in+-- a couple different places. parseShortAccountType :: Parser AccountType parseShortAccountType =   choice [ string "Bank"  *> return BankAccount@@ -102,9 +230,12 @@          , string "Oth L" *> return LiabilityAccount          ] +-- |Render a fully-rendered account type (e.g., \"!Type:Bank\"), used for+-- section headings. renderAccountType :: AccountType -> Builder renderAccountType acc = fromText "!Type:" <> renderShortAccountType acc +-- |Render the short version of an account type (e.g., \"Bank\"). renderShortAccountType :: AccountType -> Builder renderShortAccountType BankAccount       = fromText "Bank" renderShortAccountType CashAccount       = fromText "Cash"@@ -115,6 +246,8 @@  -- Accounts -------------------------------------------------------------------- +-- |An account in the QIF file. This same structure applies for all the account+-- types. data Account = Account {        _accountName        :: Text      , _accountType        :: AccountType@@ -125,11 +258,24 @@      }  deriving (Eq, Show) -pennies :: Currency -> Integer-pennies x = truncate (x * 100.0)+makeLensesWith (set generateSignatures False lensRules) ''Account -makeLenses ''Account+-- |The name of the account+accountName        :: Lens' Account Text+-- |The type of the account+accountType        :: Lens' Account AccountType+-- |The description of the account; in my limited experience this can (and most+-- likely will) be empty.+accountDescription :: Lens' Account Text+-- |For accounts with limits, the credit limit for the account.+accountCreditLimit :: Lens' Account (Maybe Currency)+-- |The date at which the balance in the next field was current.+accountBalanceDate :: Lens' Account (Maybe Day)+-- |The current balance.+accountBalance     :: Lens' Account Currency +-- |A blank account. Defaults to 'BankAccount' for the type, with the obvious+-- zeros, empty strings, and Nothings elsewhere. emptyAccount :: Account emptyAccount = Account {     _accountName        = ""@@ -140,6 +286,7 @@   , _accountBalance     = 0   } +-- |Parse an account. parseAccount :: Parser Account parseAccount = go emptyAccount  where@@ -172,6 +319,7 @@ parseString :: Parser Text parseString = Atto.takeWhile (not . inClass "\r\n") +-- |Render an account. renderAccount :: Account -> Builder renderAccount acc = mconcat [     put  'N' acc accountName        fromText@@ -201,6 +349,12 @@  -- Dates ----------------------------------------------------------------------- +-- |Parse a date, using old-school, incredibly unwise, \"mm/dd/yy\" formats. To+-- simplify my life, this assumes that all dates start in 2000, rather than in+-- 1970 or some other date. Thus, if you have data going back before 2000,+-- you'll need to post-process this to the appropriate date, by subtracting 100+-- appropriately. Hopefully by 2100 noone will be using QIF anymore, and this+-- won't matter. parseDate :: Parser Day parseDate =   do str <- Atto.takeWhile isPrint@@ -210,11 +364,13 @@         then return (fromGregorian (2000 + (year `mod` 100)) mon day)         else return intime +-- |Render the date in QIF's silly format. renderDate :: Day -> Builder renderDate = fromString . formatTime defaultTimeLocale "%-m/%e/%y"  -- ----------------------------------------------------------------------------- +-- |Parse the list of accounts associated with this QIF file. parseAccountList :: Parser [Account] parseAccountList =   do _    <- string "!Option:AutoSwitch" *> many1 endOfLine@@ -223,6 +379,7 @@      _    <- string "!Clear:AutoSwitch"  *> many1 endOfLine      return accs +-- |Render the list of accounts associated with this QIF file. renderAccountList :: [Account] -> Builder renderAccountList accs =   fromString "!Option:AutoSwitch\n" <>@@ -232,6 +389,7 @@  -- Categories ------------------------------------------------------------------ +-- |Information about a category that one might mark a transaction against. data Category = Category {        _catName            :: Text      , _catDescription     :: Text@@ -242,9 +400,11 @@      }  deriving (Eq, Show) +-- |Whether a category is an income category or an expense category. data CategoryKind = Income | Expense  deriving (Eq, Show) +-- |A blank category. We default categories to 'Expense'. emptyCategory :: Category emptyCategory = Category {     _catName            = ""@@ -255,8 +415,22 @@   , _catTaxScheduleInfo = Nothing   } -makeLenses ''Category+makeLensesWith (set generateSignatures False lensRules) ''Category +-- |The name of the category.+catName            :: Lens' Category Text+-- |A description of the category in question. Often empty.+catDescription     :: Lens' Category Text+-- |The kind of category; 'Expense' or 'Income'+catKind            :: Lens' Category CategoryKind+-- |Whether or not this category might be tax-related.+catIsTaxRelated    :: Lens' Category Bool+-- |A budget amount, if a budget has been established and published.+catBudgetAmount    :: Lens' Category (Maybe Currency)+-- |A number describing the tax schedule to look at.+catTaxScheduleInfo :: Lens' Category (Maybe Word)++-- |Parse a category. parseCategory :: Parser Category parseCategory = go emptyCategory  where@@ -275,6 +449,7 @@         '^' -> many1 endOfLine *> return base         _   -> fail "Unknown, out of scope category label." +-- |Render a category. renderCategory :: Category -> Builder renderCategory cat = mconcat [     put  'N' cat catName         fromText@@ -288,12 +463,15 @@  -- Category Lists -------------------------------------------------------------- +-- |Parse the list of categories (and the header for said list). parseCategoryList :: Parser [Category] parseCategoryList =   do _    <- string "!Type:Cat"      _    <- many1 endOfLine      many' parseCategory +-- |Render the header for the list of categories followed by each of the+-- categories. renderCategoryList :: [Category] -> Builder renderCategoryList cats =   fromString "!Type:Cat\n" <> mconcat (map renderCategory cats)@@ -301,17 +479,26 @@  -- Account Headers ------------------------------------------------------------- +-- |Sections full of transactions start with the header demarcating what account+-- the transactions are in regard to. This parses that header, returning the+-- account. Note that if you were expecting to be a somewhat reasonable+-- standard, and just reference a previously-defined account, you're in for a+-- disappointment. This is a completely fresh 'Account' structure, and you'll+-- have to match things up (and merge any differences) yourself. parseAccountHeader :: Parser Account parseAccountHeader =   do _ <- string "!Account"      _ <- many1  endOfLine      parseAccount +-- |Render the header that should proceed any list of transactions. renderAccountHeader :: Account -> Builder renderAccountHeader acc = fromString "!Account\n" <> renderAccount acc  -- Bank Entries ---------------------------------------------------------------- +-- |When a single transaction is split across a couple categories, this is your+-- friend. data SplitItem = SplitItem {        _entryMemo     :: Text      , _entryAmount   :: Currency@@ -319,9 +506,20 @@      }  deriving (Eq, Show) +-- |An empty 'SplitItem'. No texts, no money. So sad. emptySplitItem :: SplitItem emptySplitItem = SplitItem "" 0 "" +makeLensesWith (set generateSignatures False lensRules) ''SplitItem++-- |Any memo taken as part of this split.+entryMemo :: Lens' SplitItem Text+-- |The amount of money in this split.+entryAmount :: Lens' SplitItem Currency+-- |The category associated with this split.+entryCategory :: Lens' SplitItem Text++-- |A normal transaction, that doesn't include an action in the stock market. data Transaction = Transaction {        _entDate         :: Day      , _entParty        :: Text@@ -335,13 +533,36 @@      }  deriving (Eq, Show) +-- |A transaction with no real data, that happened to occur on January 1st,+-- 2000. Happy new year! emptyTransaction :: Transaction emptyTransaction =   Transaction (fromGregorian 2000 1 1) "" "" 0 Nothing Nothing False False [] -makeLenses ''SplitItem-makeLenses ''Transaction+makeLensesWith (set generateSignatures False lensRules) ''Transaction +-- |The date of the transaction.+entDate :: Lens' Transaction Day+-- |The other party to the transaction.+entParty :: Lens' Transaction Text+-- |Any memos taken about the transaction.+entMemo :: Lens' Transaction Text+-- |The total amount of the transaction.+entAmount :: Lens' Transaction Currency+-- |The check or other number, as appropriate.+entNumber :: Lens' Transaction (Maybe Word)+-- |The category associated with the transaction, if provided.+entCategory :: Lens' Transaction (Maybe Text)+-- |Whether or not this transaction has cleared.+entCleared :: Lens' Transaction Bool+-- |Whether or not this transaction is reimbursable.+entReimbursable :: Lens' Transaction Bool+-- |Any splits assocaited with this transaction.+entSplits :: Lens' Transaction [SplitItem]++-- |Parse a transaction. Note that this function only does parsing, not+-- consistency checking. Thus, you may end up with a transaction whose splits do+-- not sum to the total transaction amount, or is missing a category, etc. parseTransaction :: Parser Transaction parseTransaction = go emptyTransaction  where@@ -365,7 +586,9 @@         '^' -> many1 endOfLine *> return base         _   -> fail "Unknown, out of scope bank entry label." -+-- |Render a transaction. This function assumes that you have performed any+-- consistency checking you're going to do before writing out this transaction.+-- It won't do any for you. renderTransaction :: Transaction -> Builder renderTransaction be =   put  'D' be entDate     renderDate                                     <>@@ -380,6 +603,9 @@   singleton '^' <> newline  where +-- |Parse a split. Note that some banking programs may end up emitting empty+-- splits, and we don't do anything about that. So you might want to check if+-- what you get back is 'emptyTransaction', or something morally similar. parseSplit :: SplitItem -> Parser SplitItem parseSplit base = option base $   do label <- satisfy (inClass "E$")@@ -388,6 +614,8 @@        '$' -> getP parseSplit base entryAmount parseCurrency        _   -> fail "Unknown, out of scope split entry label." +-- |Render a split. Please be sensible in what you emit; this function won't+-- check your work for you. renderSplit :: SplitItem -> Builder renderSplit s =   put 'S' s entryCategory fromText                <>@@ -406,17 +634,26 @@  -- Bank Entry Lists ------------------------------------------------------------ -parseTransactionList :: Parser [Transaction]-parseTransactionList =+-- |Parse a list of bank transactions. You should probably call this directly+-- after 'parseAccountHeader' and discovering that it's a 'Bank' account. You+-- should also not trust the results of this, as it does no consistency checking+-- on your behalf.+parseBankEntryList :: Parser [Transaction]+parseBankEntryList =   do _ <- string "!Type:Bank" >> many1 endOfLine      many' parseTransaction -renderTransactionList :: [Transaction] -> Builder-renderTransactionList ls =+-- |Render a list of bank transactions. Please do any consistency checking you+-- want before calling this. You probably also want to have called+-- 'renderAccountHeader' with an appropriate 'Bank' account before calling this+-- one.+renderBankEntryList :: [Transaction] -> Builder+renderBankEntryList ls =   fromText "!Type:Bank" <> newline <> mconcat (map renderTransaction ls)  -- Investment Entries --------------------------------------------------------- +-- |Information about a given trade. data TradeInfo = TradeInfo {        _tradeDate        :: Day      , _tradeSecurity    :: Text@@ -427,11 +664,31 @@      }  deriving (Eq, Show) +-- |Build an empty trade made on a given day. emptyTrade :: Day -> TradeInfo emptyTrade day = TradeInfo day "" Nothing Nothing Nothing 0 -makeLenses ''TradeInfo+makeLensesWith (set generateSignatures False lensRules) ''TradeInfo +-- |The date of the trade.+tradeDate :: Lens' TradeInfo Day+-- |The security this trade was about. Note that while we probably should be+-- doing some input validation on this, we're not. So if you're consuming this+-- value, be a bit paranoid.+tradeSecurity :: Lens' TradeInfo Text+-- |The share price of the security during the trade, if provided.+tradeSharePrice :: Lens' TradeInfo (Maybe Currency)+-- |The amount of the share traded, if provided.+tradeQuantity :: Lens' TradeInfo (Maybe ShareQuantity)+-- |The annoying commission taken out of the trade, if provided. Note that QIF+-- does differentiate between Nothing and (Just 0.00), for some reason.+tradeCommission :: Lens' TradeInfo (Maybe Currency)+-- |The total amount of the trade.+tradeTotalAmount :: Lens' TradeInfo Currency++-- |Information about a transfer into an investment account. This probably looks+-- like a normal transaction in a non-investment account, and each one probably+-- has a sibling that is exactly that. data TransferInfo = TransferInfo {        _transDate    :: Day      , _transSummary :: Text@@ -443,11 +700,33 @@      }  deriving (Eq, Show) +-- |An empty transfer that occurred on the given day. emptyTransfer :: Day -> TransferInfo emptyTransfer day = TransferInfo day "" "" 0 False "" [] -makeLenses ''TransferInfo+makeLensesWith (set generateSignatures False lensRules) ''TransferInfo +-- |The date of the transfer.+transDate :: Lens' TransferInfo Day+-- |A summary of the transfer. Sometimes the other party in the transfer, or+-- just a short name, and sometimes blank.+transSummary :: Lens' TransferInfo Text+-- |A memo or note about the transaction. Often blank, in our limited+-- experience.+transMemo :: Lens' TransferInfo Text+-- |The amount of the transfer.+transAmount :: Lens' TransferInfo Currency+-- |Whether or not the transfer has cleared.+transCleared :: Lens' TransferInfo Bool+-- |The account with which this transaction took place ... usually. Sometimes+-- this is empty. Make of that as you will.+transAccount :: Lens' TransferInfo Text+-- |Any splits associated with the transaction.+transSplits :: Lens' TransferInfo [SplitItem]++-- |An action in an investment account. These are the ones I've seen in QIF+-- files shown to me. If you run into other ones, please file a bug or submit a+-- patch. data InvTransaction = Buy           TradeInfo                     | Sell          TradeInfo                     | Transfer      TransferInfo@@ -455,6 +734,8 @@                     | Interest Text TradeInfo  deriving (Eq, Show) +-- |The date of an investment account action, regardless of what kind of+-- transaction it was. invEntDate :: Lens' InvTransaction Day invEntDate = lens dget dset  where@@ -471,6 +752,10 @@   dset (Dividend   tinfo) x = Dividend   (set  tradeDate x tinfo)   dset (Interest a tinfo) x = Interest a (set  tradeDate x tinfo) +-- |Parse an investment transaction. Like it's sister function,+-- 'parseTransaction', this function doesn't do any semantic validation. So it's+-- possible that the date in the transaction doesn't make any sense. So ...+-- that's on you. parseInvTransaction :: Parser InvTransaction parseInvTransaction =   do date <- char 'D' *> parseDate <* many1 endOfLine@@ -517,6 +802,8 @@        tr <- trade base        return (Interest label tr) +-- |Render an investment transaction. As you might expect, this doesn't check+-- your work. So be careful. renderInvTransaction :: InvTransaction -> Builder renderInvTransaction ent =   singleton 'D' <> renderDate (view invEntDate ent) <> singleton '\n' <>@@ -549,11 +836,15 @@  -- Investment Entry Lists ------------------------------------------------------ +-- |Parse a list of investment entries. You probably should've called+-- 'parseAccountHeader' right before this and found an investment account. parseInvestmentEntries :: Parser [InvTransaction] parseInvestmentEntries =   do _ <- string "!Type:Invst" >> many1 endOfLine      many' parseInvTransaction +-- |Render a list of investment transactions. You should probably have just+-- called 'renderAccountHeader' with an investment account. renderInvestmentEntries :: [InvTransaction] -> Builder renderInvestmentEntries ents =   fromText "!Type:Invst" <> newline <> mconcat (map renderInvTransaction ents)@@ -561,54 +852,81 @@  -- Cash Entry Lists ------------------------------------------------------------ +-- |Parse a list of cash transactions. You should probably have just called+-- 'parseAccountHeader' and found a 'Cash' account. You should probably also+-- be a bit paranoid about checking over the date you read, as we perform no+-- semantic checks on your behalf. parseCashEntryList :: Parser [Transaction] parseCashEntryList =   do _ <- string "!Type:Cash" >> many1 endOfLine      many' parseTransaction +-- |Render a list of cash transactions. You should have just called+-- 'renderAccountHeader' with a 'Cash' account. renderCashEntryList :: [Transaction] -> Builder renderCashEntryList ls =   fromText "!Type:Cash" <> newline <> mconcat (map renderTransaction ls)  -- Credit Card Entry Lists ----------------------------------------------------- +-- |Parse a list of credit card transactions. You should probably have just+-- called 'parseAccountHeader' and found a 'Cash' account. You should probably+-- also be a bit paranoid about checking over the date you read, as we perform+-- no semantic checks on your behalf. parseCreditCardEntryList :: Parser [Transaction] parseCreditCardEntryList =   do _ <- string "!Type:CCard" >> many1 endOfLine      many' parseTransaction +-- |Render a list of credit card transactions. You should have just called+-- 'renderAccountHeader' with a 'CreditCard' account. renderCreditCardEntryList :: [Transaction] -> Builder renderCreditCardEntryList ls =   fromText "!Type:CCard" <> newline <> mconcat (map renderTransaction ls)  -- Asset Entry Lists ----------------------------------------------------------- +-- |Parse a list of transactions in an asset account. Again, you probably should+-- have just called 'parseAccountHeader' and found an 'Asset' account, and you+-- should make sure to do any data validation you care about. Because this+-- library just doesn't care. parseAssetEntryList :: Parser [Transaction] parseAssetEntryList =   do _ <- string "!Type:Oth A" >> many1 endOfLine      many' parseTransaction +-- |Render a list of transactions on an asset. Did you call+-- 'renderAccountHeader' before this with an asset account? You should have! renderAssetEntryList :: [Transaction] -> Builder renderAssetEntryList ls =   fromText "!Type:Oth A" <> newline <> mconcat (map renderTransaction ls)  -- Liability Entry Lists ------------------------------------------------------- +-- |Last one! Parse a list of transactions about a liability. Probably a loan,+-- which you may or may not regret. You *will* regret it, however, if you didn't+-- call 'parseAccountHeader' first and find a 'Liability' account. You will also+-- regret it if you don't do some input validation on what you get from this+-- function. parseLiabilityEntryList :: Parser [Transaction] parseLiabilityEntryList =   do _ <- string "!Type:Oth L" >> many1 endOfLine      many' parseTransaction +-- |Render a list of transactions about a liability, probably right after you+-- called 'renderAccountHeader' with a liability account. renderLiabilityEntryList :: [Transaction] -> Builder renderLiabilityEntryList ls =   fromText "!Type:Oth L" <> newline <> mconcat (map renderTransaction ls)  -- Security Types -------------------------------------------------------------- +-- |The kinds of securities QIF files will reference. data SecurityType = Stock | Bond | CD | MutualFund | Index | ETF | MoneyMarket                   | PreciousMetal | Commodity | StockOption | Other  deriving (Eq, Show) +-- |Parse a security type. parseSecurityType :: Parser SecurityType parseSecurityType = choice [     -- these are intentionally out of order; "Stock Option" *MUST* precede@@ -626,6 +944,7 @@   , string "Other"             *> return Other   ] +-- |Render a security type. renderSecurityType :: SecurityType -> Builder renderSecurityType st =   case st of@@ -643,6 +962,7 @@  -- Securities ------------------------------------------------------------------ +-- |The information QIF keeps about a security. data Security = Security {        _secName   :: Text      , _secTicker :: Text@@ -651,11 +971,26 @@      }  deriving (Eq, Show) +-- |An empty security, forlorn and alone, with no name, no ticker, and no goals.+-- Definitely a stock, though. emptySecurity :: Security emptySecurity = Security "" "" Stock Nothing -makeLenses ''Security+makeLensesWith (set generateSignatures False lensRules) ''Security +-- |The name of the security.+secName :: Lens' Security Text+-- |The ticker symbol for the security. If I was a better person this would do+-- some validation on the input.+secTicker :: Lens' Security Text+-- |The type of security.+secType :: Lens' Security SecurityType+-- |The goal for the security. I think this is for things like "Buying a house"+-- or "Saving for college", but I've never actually seen this used in the wild.+secGoal :: Lens' Security (Maybe Text)++-- |Parse a security. Performs no validation that the name makes sense, the+-- ticker makes sense, or that the two go together. Good luck with that. parseSecurity :: Parser Security parseSecurity = go emptySecurity  where@@ -669,6 +1004,9 @@          '^' -> many1 endOfLine *> return base          _   -> fail "Unknown, out of scope security label." +-- |Render a security. You should probably make sure that your data structure+-- makes sense before you write it, but that's your thing. This function won't+-- judget you. renderSecurity :: Security -> Builder renderSecurity s =   put  'N' s secName   fromText           <>@@ -679,31 +1017,72 @@  -- Securities Lists ------------------------------------------------------------ +-- |Parse a list of securities out of the QIF file. parseSecurityList :: Parser [Security] parseSecurityList =   do _ <- string "!Type:Security" >> many1 endOfLine      many' parseSecurity +-- |Render a list of securities. renderSecurityList :: [Security] -> Builder renderSecurityList ls =   fromText "!Type:Security" <> newline <> mconcat (map renderSecurity ls)  -- Securities Lists ------------------------------------------------------------ +-- |The semantic content of a QIF file. (Explicitly this: very little semantic+-- processing has gone into this data structure, and it could contain semantic+-- errors in the underlying file. Checking for these things is your job.) data QIF = QIF {-       _qifAccounts          :: [Account]-     , _qifCategories        :: [Category]-     , _qifSecurities        :: [Security]-     , _qifInvestmentActions :: [(Account, [InvTransaction])]-     , _qifNormalActions     :: [(Account, [Transaction])]+       _qifAccounts               :: [Account]+     , _qifCategories             :: [Category]+     , _qifSecurities             :: [Security]+     , _qifInvestmentTransactions :: [(Account, [InvTransaction])]+     , _qifNormalTransactions     :: [(Account, [Transaction])]      }  deriving (Eq, Show) +-- |An empty QIF file. emptyQIF :: QIF emptyQIF = QIF [] [] [] [] [] -makeLenses ''QIF+makeLensesWith (set generateSignatures False lensRules) ''QIF +-- |The accounts associated with the QIF file. We hope. You might expect that+-- there would be an invariant that 'qifAccounts' would be the same as 'map'+-- 'fst' 'qifInvestmentTransactions' '++' 'map' 'fst' 'qifNormalTransactions'.+-- I would, and it'd be nice if you tried to maintain that in your code. But,+-- unfortuntely, there's nothing in the QIF file format that requires this. So+-- you should probably be careful, and make sure you handle the case in which+-- this item mentions accounts not seen anywhere else, and the case in which+-- 'qifNormalTransactions' and 'qifInvestmentTransactions' suddenly invent new+-- accounts.+qifAccounts :: Lens' QIF [Account]+-- |The list of categories saved in this QIF file. Like 'qifAccounts', there+-- doesn't seem to be anything enforcing consistency in the actual QIF file. So+-- you may find that this list mentions categories not referenced elsewhere --+-- which is not necessarily too surprising -- but also that there may be+-- transactions that mention new categories unlisted in this field.+qifCategories :: Lens' QIF [Category]+-- |A cached list of securities. As with the other fields, be warned, as this is+-- not required to be complete, as far as I can tell.+qifSecurities :: Lens' QIF [Security]+-- |A list of investment accounts and the transactions associated with those+-- accounts. Typically each 'Account' should reference an account in+-- 'qifAccount' and include exactly the same date, but there's nothing in the+-- file structure that enforces this invariant.+qifInvestmentTransactions :: Lens' QIF [(Account, [InvTransaction])]+-- |A list of non-investment accounts and the transactions associated with them.+-- Again, one might expect that each 'Account' here should reference an account+-- in 'qifAccount', and contain exactly the same data, but there's nothing in+-- the file structure that enforces this constraint.+qifNormalTransactions :: Lens' QIF [(Account, [Transaction])] ++-- |Parse a QIF file. This function is purely a syntactic parse, and makes no+-- attempt to verify that the data it parses makes sense. So please be a bit+-- paranoid with all the numbers and strings you receive, and perform any+-- validation you need on your own. Also, this function assumes that it is+-- parsing only a QIF file, and that it should run to the end of the input. parseQIF :: Parser QIF parseQIF = go emptyQIF  where@@ -723,17 +1102,17 @@     do acc <- parseAccountHeader        case view accountType acc of          BankAccount       ->-           getts parseTransactionList qifNormalActions base acc+           getts parseBankEntryList qifNormalTransactions base acc          CashAccount       ->-           getts parseCashEntryList qifNormalActions base acc+           getts parseCashEntryList qifNormalTransactions base acc          CreditCardAccount ->-           getts parseCreditCardEntryList qifNormalActions base acc+           getts parseCreditCardEntryList qifNormalTransactions base acc          InvestmentAccount ->-           getts parseInvestmentEntries qifInvestmentActions base acc+           getts parseInvestmentEntries qifInvestmentTransactions base acc          AssetAccount      ->-           getts parseAssetEntryList qifNormalActions base acc+           getts parseAssetEntryList qifNormalTransactions base acc          LiabilityAccount  ->-           getts parseLiabilityEntryList qifNormalActions base acc+           getts parseLiabilityEntryList qifNormalTransactions base acc   --   getts :: Parser [a] -> ASetter' QIF [(Account,[a])] ->            QIF -> Account ->@@ -742,3 +1121,25 @@     do list <- listParser        go (over field (++ [(account, list)]) base) +-- |Render out a QIF File. Because it's the order I've seen in my early example+-- QIF files, this renders in the following order: account list, category list,+-- investment accounts and their transactions, non-investment accounts and their+-- transactions, and then security lists.+renderQIF :: QIF -> Builder+renderQIF qif =+  renderAccountList (view qifAccounts qif) <>+  renderCategoryList (view qifCategories qif) <>+  mconcat+    (map (\ (acc,trans) -> renderAccountHeader acc <> renderInvestmentEntries trans)+         (view qifInvestmentTransactions qif)) <>+  mconcat+    (map (\ (acc,trans) -> renderAccountHeader acc <>+            case view accountType acc of+              BankAccount       -> renderBankEntryList trans+              CashAccount       -> renderCashEntryList trans+              CreditCardAccount -> renderCreditCardEntryList trans+              InvestmentAccount -> error "Investment account in normal list?"+              AssetAccount      -> renderAssetEntryList trans+              LiabilityAccount  -> renderLiabilityEntryList trans)+          (view qifNormalTransactions qif)) <>+  renderSecurityList (view qifSecurities qif)