ofx (empty) → 0.2.0.0
raw patch · 4 files changed
+1078/−0 lines, 4 filesdep +basedep +explicit-exceptiondep +parsecsetup-changed
Dependencies added: base, explicit-exception, parsec, pretty, time
Files
- Data/OFX.hs +952/−0
- LICENSE +30/−0
- Setup.hs +2/−0
- ofx.cabal +94/−0
+ Data/OFX.hs view
@@ -0,0 +1,952 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Parser for downloaded OFX files.+--+-- This parser was written based on the OFX version 1.03+-- specification, which is available at+--+-- <http://www.ofx.net>+--+-- It will probably work on earlier versions of OFX without+-- incident. However, it may or may not not work on newer versions of+-- OFX, which are XML based (this version of OFX is SGML based.)+--+-- It will also parse QFX files, which are OFX files with minor+-- proprietary additions by Intuit, the maker of Quicken.+--+-- An OFX file consists of three parts: the HTTP headers (which this+-- parser does NOT handle because typically they will not show up in+-- files downloaded to disk), the OFX headers, and the OFX data. This+-- parser handles the OFX headers and the OFX data.+--+-- The parser in this module simply parses the tags and data into a+-- tree, which you can manipulate with other functions. Some functions+-- are provided to find the transactions in the tree and place them+-- into a 'Transaction' type, which is the data you are most likely+-- interested in. If you are interested in other data you can query+-- the 'Tag' tree for what you need.+--+-- For example, to read in the filename given on the command line and+-- parse it and print it nicely:+--+-- > import System.Environment+-- > import Text.Parsec+-- > import Text.PrettyPrint+-- > import Data.OFX+-- > import System.IO+-- > import System.Exit+-- >+-- > main :: IO ()+-- > main = do+-- > filename:[] <- getArgs+-- > contents <- readFile filename+-- > ofx <- case parse ofxFile filename contents of+-- > Left e -> do+-- > hPutStrLn stderr . show $ e+-- > exitFailure+-- > Right g -> return g+-- > putStrLn . render . pFile $ ofx+-- > putStrLn+-- > . render+-- > . pExceptional text (pList . map pTransaction)+-- > . transactions+-- > $ ofx+-- > putStrLn . render . pMaybe text . fiName $ ofx+-- > putStrLn . render . pMaybe text . accountNumber $ ofx+--++module Data.OFX+ ( -- * The OFX data tree+ HeaderTag+ , HeaderValue+ , OFXHeader(..)+ , TagName+ , TagData+ , Tag(..)+ , OFXFile(..)++ -- * Manipulating the OFX tag tree+ , find+ , findPath+ , tagData+ , pathData+ , findData++ -- * Extracting specific data+ , fiName+ , creditCardNumber+ , bankAccountNumber+ , accountNumber++ -- * Types to represent specific OFX data+ , Transaction(..)+ , transaction+ , transactions+ , TrnType(..)+ , trnType+ , Payee(..)+ , payee+ , CorrectAction(..)+ , BankAcctTo(..)+ , bankAcctTo+ , CCAcctTo(..)+ , ccAcctTo+ , AcctType(..)+ , acctType+ , CurrencyData(..)+ , currencyData+ , Currency(..)+ , currency+ , OrigCurrency(..)+ , origCurrency++ -- * Parsec parsers+ , ofxFile+ , newline+ , escChar+ , header+ , openingTag+ , closingTag+ , tag+ , date+ , time+ , tzOffset++ -- * Pretty printers+ , pPayee+ , pTransaction+ , pTag+ , pHeader+ , pFile+ , pEither+ , pMaybe+ , pList+ , label+ , pExceptional+ ) where++import Control.Applicative+ ( (<$), (<|>), (*>), many, optional,+ (<$>), (<*), (<*>), pure )+import Control.Monad (replicateM)+import qualified Control.Monad.Exception.Synchronous as Ex+import qualified Data.Time as T+ +import Text.Parsec.String (Parser)+import Text.Parsec+ ( lookAhead, char, manyTill, anyChar, (<?>), eof,+ try, digit, many1, spaces )+import qualified Text.Parsec as P+import Data.Maybe (listToMaybe)+import qualified Data.Monoid as M+import Data.Monoid ((<>), mempty)+import Text.PrettyPrint+ ( Doc, hang, text, sep, vcat, nest, (<+>), ($$),+ parens, brackets )+++--+-- Data types+--++-- | Headers consists of simple @tag:value@ pairs; this represents the+-- tag.+type HeaderTag = String++-- | The value in an OFX header.+type HeaderValue = String++-- | An OFX file starts with a number of headers, which take the form+-- @tag:value@ followed by a newline. These are followed by a blank+-- line.+data OFXHeader = OFXHeader HeaderTag HeaderValue+ deriving (Eq, Show)++-- | The name of an OFX tag+type TagName = String++-- | The data accompanying an OFX tag.+type TagData = String++-- | The main OFX data consists of a series of tags. OFX 1.03 is SGML,+-- not XML. This means that opening tags need not have closing+-- tags. In OFX, a tag either has data and no child elements, or it+-- has no data and it has child elements.+data Tag = Tag TagName (Either TagData [Tag])+ deriving (Eq, Show)++-- | All the data from an OFX file.+data OFXFile = OFXFile+ { fHeader :: [OFXHeader]++ , fTag :: Tag+ -- ^ All the data will be contained in a root tag with the TagName+ -- @OFX@.++ } deriving (Eq, Show)++--+-- Parsers+--++-- | Parses either a UNIX or an MS-DOS newline. According to 1.2.2,+-- OFX does not contain any white space between tags. However, since I+-- have seen OFX files that do have whitespace between tags, the+-- parser makes allowance for this.+newline :: Parser ()+newline = () <$ char '\n' <|> () <$ (char '\r' *> char '\n')+ <?> "newline"++-- | Parses a character, possibly with an escape sequence. The+-- greater-than sign, less-than sign, and ampersand must be entered+-- with escape sequences.+escChar :: Parser Char+escChar =+ do+ c <- anyChar+ case c of+ '&' -> do+ cs <- manyTill anyChar (char ';')+ case cs of+ "lt" -> return '<'+ "gt" -> return '>'+ "amp" -> return '&'+ _ -> fail $ "unrecognized esacpe sequence: \"&"+ ++ cs ++ ";\""+ _ -> return c+ <?> "character"++header :: Parser OFXHeader+header+ = OFXHeader+ <$> manyTill anyChar (char ':')+ <* optional (many (char ' '))+ <*> manyTill anyChar newline+ <?> "OFX header"+ +-- | Parses any opening tag. Returns the name of the tag.+openingTag :: Parser TagName+openingTag =+ do+ _ <- char '<'+ cs <- manyTill escChar (char '>')+ case cs of+ [] -> fail "opening tag with empty name"+ x:_ ->+ if x == '/'+ then fail "this is a closing tag"+ else return cs+ <?> "opening tag"++-- | Parses a closing tag with the given name.+closingTag :: TagName -> Parser ()+closingTag n =+ do+ _ <- char '<'+ _ <- char '/'+ cs <- manyTill escChar (char '>')+ if cs == n+ then return ()+ else fail $ "expecting closing tag named " ++ n+ ++ "; saw closing tag named " ++ cs+ <?> "closing tag named " ++ n++-- | Parses any tag. The tag itself must be followed by at least one+-- character: either the next tag if this is an aggregate tag, or the+-- data if this is a data tag. OFX does not allow empty tags.+--+-- The OFX spec seems to say that OFX files do not include trailing+-- newlines after tags or data, but I have seen these newlines in QFX+-- files, so this parses optional trailing newlines and spaces.+tag :: Parser Tag+tag =+ do+ -- try is needed because openingTag will overlap with closingTag+ n <- try (openingTag <* spaces)+ children <- many tag+ if null children+ then Tag n . Left+ <$> manyTill escChar+ (eof <|> lookAhead (() <$ char '<') <|> newline)+ <* spaces+ <* optional (try (closingTag n))+ <* spaces+ else Tag n (Right children) <$ spaces <* closingTag n+ <* spaces+ <?> "OFX tag"+ ++-- | Parses an entire OFX file, including headers.+ofxFile :: Parser OFXFile+ofxFile+ = OFXFile+ <$> manyTill header newline+ <*> tag+ <* spaces+ <* eof+ <?> "OFX file"++-- | Parses an OFX date; provides an error message if the parse fails.+parseDate :: String -> Ex.Exceptional String T.ZonedTime+parseDate s = case P.parse date "" s of+ Left e -> Ex.throw $ "could not parse date: " ++ s ++ ": "+ ++ show e+ Right g -> return g++-- | Parses an OFX date. Fails if the date is not valid or if there is+-- no date to be parsed.+date :: Parser T.ZonedTime+date =+ do+ ys <- fmap read $ replicateM 4 digit+ ms <- fmap read $ replicateM 2 digit+ ds <- fmap read $ replicateM 2 digit+ day <- case T.fromGregorianValid ys ms ds of+ Nothing -> fail $ "invalid date: " ++ show ys+ ++ "-" ++ show ms ++ "-" ++ show ds+ Just d -> return d+ mayTime <- optional time+ case mayTime of+ Nothing ->+ let localTime = T.LocalTime day T.midnight+ in return $ T.ZonedTime localTime T.utc+ Just (t, z) -> return $ T.ZonedTime (T.LocalTime day t) z+ <?> "date"++ +-- | Parses an OFX time. Fails if the time is not valid or if there is+-- no time to parse. Fails if there is no time to parse; however, if+-- there is a time but no zone, returns the time and UTC for the zone.+time :: Parser (T.TimeOfDay, T.TimeZone)+time =+ do+ h <- fmap read $ replicateM 2 digit+ m <- fmap read $ replicateM 2 digit+ s <- fmap read $ replicateM 2 digit+ (milli, tz) <- do+ mayDot <- optional (char '.')+ case mayDot of+ Nothing -> return (0, T.utc)+ Just _ -> do+ mil <- fmap ((/ 1000) . read) $ replicateM 3 digit+ mayTz <- optional tzOffset+ case mayTz of+ Nothing -> return (mil, T.utc)+ Just t -> return (mil, t)+ let sec = s + milli+ return (T.TimeOfDay h m sec, tz)+ <?> "time"+ ++-- | Parses a time zone offset. Fails if there is no time zone offset+-- to parse.+tzOffset :: Parser T.TimeZone+tzOffset =+ do+ _ <- char '['+ sn <- parseSign+ whole <- many1 digit+ mayDot <- optional (char '.')+ frac <- case mayDot of+ Nothing -> return "0"+ Just _ -> many1 digit+ mayColon <- optional (char ':')+ name <- case mayColon of+ Nothing -> return ""+ Just _ -> many1 P.letter+ _ <- char ']'+ let off = sn $ round ((read (whole ++ "." ++ frac))+ * (60 :: Double))+ return $ T.TimeZone off False name+ <?> "time zone offset"+ where+ parseSign = do+ mayMinus <- optional (char '-')+ case mayMinus of+ Nothing -> do+ mayPlus <- optional (char '+')+ return $ case mayPlus of+ Nothing -> id+ Just _ -> negate+ Just _ -> return negate++--+-- Manipulating trees of tags+--++-- | Finds child tags with the given name. When a tag is found, that+-- tag is not searched for further children with the same name.+find :: TagName -> Tag -> [Tag]+find n t@(Tag x p)+ | n == x = [t]+ | otherwise = case p of+ Left _ -> []+ Right ts -> concatMap (find n) ts++-- | Descends through a tree of tags to find a tag at a specific+-- location in the tree. Fails if any part of the search fails. For+-- example, to find the financial institution ORG tag, where @t@ is+-- the root @OFX@ tag:+--+-- > findPath ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"] t++findPath :: [TagName] -> Tag -> Maybe Tag+findPath [] t = Just t+findPath (n:ns) t = case listToMaybe (find n t) of+ Nothing -> Nothing+ Just r -> findPath ns r++-- | Gets the data from a tag, if it is a tag with data.+tagData :: Tag -> Maybe TagData+tagData (Tag _ ei) = either return (const Nothing) ei++-- | Goes to a certain path in the tag hierarchy and pulls the+-- requested data, if the tag is present and it is a data tag. For+-- example, to get the name of the financial institution:+--+-- > pathData ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"] f+pathData :: [TagName] -> OFXFile -> Maybe TagData+pathData p (OFXFile _ t) = findPath p t >>= tagData+++-- | Gets the name of the financial institution from the FI tag, if+-- available. The OFX spec does not require this tag to be present.+fiName :: OFXFile -> Maybe TagData+fiName = pathData ["SIGNONMSGSRSV1", "SONRS", "FI", "ORG"]+++-- | Gets the credit card number, if available. The OFX spec does not+-- require this tag to be present.+creditCardNumber :: OFXFile -> Maybe TagData+creditCardNumber =+ pathData [ "CREDITCARDMSGSRSV1", "CCSTMTTRNRS", "CCSTMTRS",+ "CCACCTFROM", "ACCTID" ]++-- | Gets the bank account number, if available. The OFX spec does not+-- require this tag to be present.+bankAccountNumber :: OFXFile -> Maybe TagData+bankAccountNumber =+ pathData [ "BANKMSGSRSV1", "STMTTRNRS", "STMTRS",+ "BANKACCTFROM", "ACCTID" ]++-- | Gets either the credit card or bank account number, if available.+accountNumber :: OFXFile -> Maybe TagData+accountNumber f = creditCardNumber f <|> bankAccountNumber f+ ++-- | Finds the first tag (either this tag or any children) that has+-- the given name and that is a data tag (not an aggregate tag.) If no+-- data tag with the given name is found, returns Nothing.+findData :: TagName -> Tag -> Maybe TagData+findData n (Tag tn e) = case e of+ Left d -> if tn == n then Just d else Nothing+ Right ts -> M.getFirst . M.mconcat . map M.First+ . map (findData n) $ ts+++-- | Finds the first tag (either this tag or any children) that has+-- the given name and that is a data tag. Gives an error message if+-- the tag is not found.+required :: TagName -> Tag -> Ex.Exceptional String TagData+required n t = case findData n t of+ Nothing -> Ex.throw $ "required tag missing: " ++ n+ Just r -> return r+++--+-- OFX data+-- ++-- | OFX transaction types. These are used in STMTTRN aggregates, see+-- OFX spec section 11.4.2.3.1.1.+data TrnType+ = TCREDIT+ | TDEBIT++ | TINT+ -- ^ Interest earned or paid (which it is depends on sign of amount)++ | TDIV+ -- ^ Dividend++ | TFEE+ | TSRVCHG++ | TDEP+ -- ^ Deposit++ | TATM+ -- ^ ATM debit or credit (which it is depends on sign of amount)++ | TPOS+ -- ^ Point of sale debit or credit (which it is depends on sign of+ -- amount)++ | TXFER+ -- ^ Transfer++ | TCHECK+ | TPAYMENT+ -- ^ Electronic payment++ | TCASH+ -- ^ Cash withdrawal++ | TDIRECTDEP+ -- ^ Direct deposit++ | TDIRECTDEBIT+ -- ^ Merchant initiated debit++ | TREPEATPMT+ -- ^ Repeating payment / standing order++ | TOTHER+ deriving (Eq, Ord, Show)++-- | A single STMTTRN, see OFX spec section 11.4.2.3.1. This is most+-- likely what you are interested in after downloading a statement+-- from a bank.+data Transaction = Transaction+ { txTRNTYPE :: TrnType+ -- ^ Transaction type++ , txDTPOSTED :: T.ZonedTime+ -- ^ Date transaction was posted to account++ , txDTUSER :: Maybe T.ZonedTime+ -- ^ Date user initiated transaction, if known++ , txDTAVAIL :: Maybe T.ZonedTime+ -- ^ Date funds are available++ , txTRNAMT :: String+ -- ^ Amount of transaction. This is left as the string that was+ -- originally in the download. That means the transaction may+ -- include a plus or minus sign (no sign is the same as a plus+ -- sign.) According to section 3.2.9.2, amounts are always signed+ -- from the perspective of the customer.+ --+ -- Typically negative amounts:+ --+ -- * Investment buy amount, investment sell quantity+ --+ -- * Bank statement debit amounts, checks, fees+ --+ -- * Credit card purchases+ --+ -- * Margin balance (unless the institution owes the client money)+ --+ -- Typically positive amounts:+ --+ -- * Investment sell amount, investment buy quantity+ --+ -- * Bank statement credits+ --+ -- * Credit card payments+ --+ -- * Ledger balance (unless the account is overdrawn)+ --+ -- Formats for amounts are described in 3.2.9.1. If there is no+ -- decimal point, there is an implied decimal point at the end of+ -- the value. Trailing and leading spaces \"should\" be+ -- stripped. Positive or minus is indicated with a leading sign; a+ -- plus sign is assumed if there is no sign.+ --+ -- An amount has a maximum of 32 alphanumeric characters,+ -- including digits and punctuation.+ --+ -- The radix point is indicated with either a period or a+ -- comma. Amounts \"should\" not include any digit grouping+ -- characters.++ , txFITID :: String+ -- ^ Transaction ID issued by financial institution. Used to+ -- detect duplicate downloads.++ , txCORRECTFITID :: Maybe String+ -- ^ If present, this indicates the FITID of a previously sent+ -- transaction that is corrected by this record. This transaction+ -- replaces or deletes the transaction that it corrects, based on+ -- the value of CORRECTACTION below.++ , txCORRECTACTION :: Maybe CorrectAction+ -- ^ See 'CorrectAction' and 'txCORRECTFITID'++ , txSRVRTID :: Maybe String+ -- ^ Server assigned transaction ID; used for transactions+ -- initiated by client, such as payment or funds transfer++ , txCHECKNUM :: Maybe String+ -- ^ Check or other reference number++ , txREFNUM :: Maybe String+ -- ^ Reference number that uniquely identifies the+ -- transaction. Can be used in addition to or instead of a+ -- CHECKNUM.++ , txSIC :: Maybe String+ -- ^ Standard Industrial Code++ , txPAYEEID :: Maybe String+ -- ^ Payee identifier if available++ , txPayeeInfo :: Maybe (Either String Payee)+ -- ^ Information on the payee. The OFX spec seems to be saying+ -- that every transaction must have either NAME, wich is \"name of+ -- payee or description of transaction\", or the Payee+ -- aggregate. The former is indicated with a Left, the latter with+ -- a Right.++ , txAccountTo :: Maybe (Either BankAcctTo CCAcctTo)+ -- ^ Information on a transfer. If this transaction wa sa+ -- transfer, this may contain information about the account being+ -- transferred to.++ , txMEMO :: Maybe String+ -- ^ Extra information not in NAME++ , txCurrency :: Maybe (Either Currency OrigCurrency)+ -- ^ Currency option. OFX spec says to choose either CURRENCY or+ -- ORIGCURRENCY.+ } deriving (Show)++data Payee = Payee+ { peNAME :: String+ , peADDR1 :: String+ , peADDR2 :: Maybe String+ , peADDR3 :: Maybe String+ , peCITY :: String+ , peSTATE :: String+ , pePOSTALCODE :: String+ , peCOUNTRY :: Maybe String+ , pePHONE :: String+ } deriving (Eq, Show)++-- | Can be either REPLACE or DELETE.+data CorrectAction+ = REPLACE+ -- ^ Replaces the transaction referenced by the CORRECTFITID++ | DELETE+ -- ^ Deletes the transaction referenced by the CORRECTFITID+ deriving (Eq, Show, Read)++data BankAcctTo = BankAcctTo+ { btBANKID :: String+ -- ^ Routing and transit number++ , btBRANCHID :: Maybe String+ -- ^ Bank identifier for international banks++ , btACCTID :: String+ -- ^ Account number++ , btACCTTYPE :: AcctType+ -- ^ Type of account++ , btACCTKEY :: Maybe String+ -- ^ Checksum for international banks+ } deriving Show++data CCAcctTo = CCAcctTo+ { ctACCTID :: String+ -- ^ Account number++ , ctACCTKEY :: Maybe String+ -- ^ Checksum for international banks++ } deriving (Eq, Show)++data AcctType+ = ACHECKING+ | ASAVINGS+ | AMONEYMRKT+ | ACREDITLINE+ deriving (Eq, Show, Ord)++acctType :: String -> Ex.Exceptional String AcctType+acctType s+ | s == "CHECKING" = return ACHECKING+ | s == "SAVINGS" = return ASAVINGS+ | s == "MONEYMRKT" = return AMONEYMRKT+ | s == "CREDITLINE" = return ACREDITLINE+ | otherwise = Ex.throw $ "unrecognized account type: " ++ s++-- | Holds all data both for CURRENCY and for ORIGCURRENCY.+data CurrencyData = CurrencyData++ { cdCURRATE :: String+ -- ^ Ratio of CURDEF currency to CURSYM currency, in decimal form++ , cdCURSYM :: String+ -- ^ ISO-4217 3-letter currency identifier+ } deriving (Eq, Show)++data Currency = Currency CurrencyData+ deriving (Eq, Show)++data OrigCurrency = OrigCurrency CurrencyData+ deriving (Eq, Show)++--+-- Helpers to build aggregates+--+trnType :: TagData -> Maybe TrnType+trnType d = case d of+ "CREDIT" -> Just TCREDIT+ "DEBIT" -> Just TDEBIT+ "INT" -> Just TINT+ "DIV" -> Just TDIV+ "FEE" -> Just TFEE+ "SRVCHG" -> Just TSRVCHG+ "DEP" -> Just TDEP+ "ATM" -> Just TATM+ "POS" -> Just TPOS+ "XFER" -> Just TXFER+ "CHECK" -> Just TCHECK+ "PAYMENT" -> Just TPAYMENT+ "CASH" -> Just TCASH+ "DIRECTDEP" -> Just TDIRECTDEP+ "DIRECTDEBIT" -> Just TDIRECTDEBIT+ "REPEATPMT" -> Just TREPEATPMT+ "OTHER" -> Just TOTHER+ _ -> Nothing++-- | Gets a single Transaction from a tag. The tag should be the one+-- named STMTTRN. Fails with an error message if any required field+-- was not present.+transaction :: Tag -> Ex.Exceptional String Transaction+transaction t = do+ trntyStr <- required "TRNTYPE" t+ trnTy <- Ex.fromMaybe ("could not parse transaction type: " ++ trntyStr)+ $ trnType trntyStr++ dtpStr <- required "DTPOSTED" t+ dtp <- parseDate dtpStr++ let mayDtuStr = findData "DTUSER" t+ dtu <- maybe (return Nothing) (fmap Just . parseDate) mayDtuStr+ + let mayDtAvail = findData "DTAVAIL" t++ dta <- maybe (return Nothing) (fmap Just . parseDate) mayDtAvail+ amt <- required "TRNAMT" t+ fitid <- required "FITID" t+ let correctFitId = findData "CORRECTFITID" t+ correctAct <-+ case findData "CORRECTACTION" t of+ Nothing -> return Nothing+ Just d -> + maybe (return Nothing)+ (Ex.fromMaybe ("could not parse correct action: " ++ d))+ . safeRead+ $ d+ let srvrtid = findData "SRVRTID" t+ checknum = findData "CHECKNUM" t+ refnum = findData "REFNUM" t+ sic = findData "SIC" t+ payeeId = findData "PAYEEID" t++ let mayPyeInfo = fmap (return . Left) (findData "NAME" t)+ <|> fmap (fmap Right) (payee t)+ pyeInfo <- maybe (return Nothing) (fmap Just) mayPyeInfo+ + let mayAcctTo = (fmap (fmap Left) $ bankAcctTo t)+ <|> (fmap (fmap Right) $ ccAcctTo t)+ mayCcy = (fmap (fmap Left) $ currency t)+ <|> (fmap (fmap Right) $ origCurrency t)+ acctTo <- maybe (return Nothing) (fmap Just) mayAcctTo+ ccy <- maybe (return Nothing) (fmap Just) mayCcy+ let memo = findData "MEMO" t++ return Transaction+ { txTRNTYPE = trnTy+ , txDTPOSTED = dtp+ , txDTUSER = dtu+ , txDTAVAIL = dta+ , txTRNAMT = amt+ , txFITID = fitid+ , txCORRECTFITID = correctFitId+ , txCORRECTACTION = correctAct+ , txSRVRTID = srvrtid+ , txCHECKNUM = checknum+ , txREFNUM = refnum+ , txSIC = sic+ , txPAYEEID = payeeId+ , txPayeeInfo = pyeInfo+ , txAccountTo = acctTo+ , txMEMO = memo+ , txCurrency = ccy+ } ++-- | Parses a Payee record from its parent tag.+payee+ :: Tag+ -- ^ The tag which contains the PAYEE tag, if there is one. This+ -- would typically be a STMTTRN tag.++ -> Maybe (Ex.Exceptional String Payee)+ -- ^ Nothing if there is no PAYEE tag. Just if a PAYEE tag is found,+ -- with an Exception if the tag is lacking a required element, or a+ -- Success if the tag is successfully parsed.+ --+ -- If there is more than one PAYEE tag, only the first one is+ -- considered.+payee = fmap getPayee . listToMaybe . find "PAYEE"+ where+ getPayee t = Payee+ <$> required "NAME" t+ <*> required "ADDR1" t+ <*> pure (findData "ADDR2" t)+ <*> pure (findData "ADDR3" t)+ <*> required "CITY" t+ <*> required "STATE" t+ <*> required "POSTALCODE" t+ <*> pure (findData "COUNTRY" t)+ <*> required "PHONE" t+ ++currency :: Tag -> Maybe (Ex.Exceptional String Currency)+currency+ = fmap (fmap Currency)+ . fmap currencyData+ . listToMaybe+ . find "CURRENCY"++origCurrency :: Tag -> Maybe (Ex.Exceptional String OrigCurrency)+origCurrency+ = fmap (fmap OrigCurrency)+ . fmap currencyData+ . listToMaybe+ . find "ORIGCURRENCY"+++-- | Parses currency data.+currencyData+ :: Tag+ -- ^ The tag that contains the data, e.g. CURRENCY or ORIGCURRENCY.++ -> Ex.Exceptional String CurrencyData+currencyData t = CurrencyData+ <$> required "CURRATE" t+ <*> required "CURSYM" t++bankAcctTo :: Tag -> Maybe (Ex.Exceptional String BankAcctTo)+bankAcctTo = fmap getTo . listToMaybe . find "BANKACCTTO"+ where+ getTo t = BankAcctTo+ <$> required "BANKID" t+ <*> pure (findData "BRANCHID" t)+ <*> required "ACCTID" t+ <*> (required "ACCTTYPE" t >>= acctType)+ <*> pure (findData "ACCTKEY" t)++ccAcctTo :: Tag -> Maybe (Ex.Exceptional String CCAcctTo)+ccAcctTo = fmap getTo . listToMaybe . find "CCACCTTO"+ where+ getTo t = CCAcctTo+ <$> required "ACCTID" t+ <*> pure (findData "ACCTKEY" t)++safeRead :: Read a => String -> Maybe a+safeRead s = case reads s of+ (x, ""):[] -> Just x+ _ -> Nothing+++-- | Pulls all Transactions from a file. Might fail if the OFX file+-- does not conform to the specification (or if there are bugs in this+-- library.) In case of the former, you can manually parse the+-- transaction information yourself using functions like+-- 'pathData'. In case of the latter, please send bugreports :-)+transactions :: OFXFile -> Ex.Exceptional String [Transaction]+transactions = mapM transaction . find "STMTTRN" . fTag++--+-- Pretty printers+--+pPayee :: Payee -> Doc+pPayee p = hang "Payee:" 2 ls+ where+ ls = sep [ label "Name" (text . peNAME $ p)+ , label "Addr1" (text . peADDR1 $ p)+ , label "Addr2" (pMaybe text . peADDR2 $ p)+ , label "Addr3" (pMaybe text . peADDR3 $ p)+ , label "City" (text . peCITY $ p)+ , label "State" (text . peSTATE $ p)+ , label "Postal" (text . pePOSTALCODE $ p)+ , label "Country" (pMaybe text . peCOUNTRY $ p)+ , label "Phone" (text . pePHONE $ p)+ ]++pTransaction :: Transaction -> Doc+pTransaction a = hang "Transaction:" 2 ls+ where+ ls = sep [ label "TRNTYPE" (text . show . txTRNTYPE $ a)+ , label "DTPOSTED" (text . show . txDTPOSTED $ a)+ , label "DTUSER" (text . show . txDTUSER $ a)+ , label "DTAVAIL" (text . show . txDTAVAIL $ a)+ , label "TRNAMT" (text . txTRNAMT $ a)+ , label "FITID" (text . txFITID $ a)+ , label "CORRECTFITID"+ (pMaybe text . txCORRECTFITID $ a)+ , label "CORRECTACTION"+ (text . show . txCORRECTACTION $ a)+ , label "SRVRTID" (pMaybe text . txSRVRTID $ a)+ , label "CHECKNUM" (pMaybe text . txCHECKNUM $ a)+ , label "REFNUM" (pMaybe text . txREFNUM $ a)+ , label "SIC" (pMaybe text . txSIC $ a)+ , label "PAYEEID" (pMaybe text . txPAYEEID $ a)+ , label "PAYEEINFO"+ (pMaybe (pEither text (text . show)) . txPayeeInfo $ a)+ , label "ACCOUNTTO"+ (pMaybe id . fmap (text . show)+ . txAccountTo $ a)+ , label "MEMO" (pMaybe text . txMEMO $ a)+ , label "CURRENCY"+ (pMaybe (text . show) . txCurrency $ a)+ ]++pTag :: Tag -> Doc+pTag (Tag n ei) = case ei of+ Left d -> "<" <> text n <> ">" <> text d+ Right ts -> vcat $ "<" <> text n <> ">"+ : map (nest 2 . pTag) ts+ ++ ["</" <> text n <> ">"]++pHeader :: OFXHeader -> Doc+pHeader (OFXHeader t v) = text t <> ": " <> text v++pFile :: OFXFile -> Doc+pFile (OFXFile hs t)+ = "OFX file:"+ $$ nest 2 (vcat [ pList . map pHeader $ hs+ , mempty+ , pTag t ])++pEither :: (a -> Doc) -> (b -> Doc) -> Either a b -> Doc+pEither fa fb = either (\l -> "Left" <+> parens (fa l))+ (\r -> "Right" <+> parens (fb r))++pMaybe :: (a -> Doc) -> Maybe a -> Doc+pMaybe f = maybe "Nothing" (\x -> "Just" <+> parens (f x))++pList :: [Doc] -> Doc+pList ds = case ds of+ [] -> "[]"+ x:[] -> brackets x+ x:xs -> sep $ hang "[" 2 x+ : map (\d -> hang "," 2 d) xs+ ++ [ "]" ]++label :: String -> Doc -> Doc+label s = hang (text (s ++ ":")) (length s + 2)++pExceptional+ :: (e -> Doc)+ -> (a -> Doc)+ -> Ex.Exceptional e a+ -> Doc+pExceptional fe fa =+ Ex.switch (\e -> hang "Exception:" 2 $ parens (fe e))+ (\g -> hang "Success:" 2 $ parens (fa g))
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Omari Norman++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.++ * Neither the name of Omari Norman nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ofx.cabal view
@@ -0,0 +1,94 @@+-- Initial ofx.cabal generated by cabal init. For further documentation, +-- see http://haskell.org/cabal/users-guide/++-- The name of the package.+name: ofx++-- The package version. See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary: +-+------- breaking API changes+-- | | +----- non-breaking API additions+-- | | | +--- code changes with no API change+version: 0.2.0.0++-- A short (one-line) description of the package.+synopsis: Parser for OFX data++-- A longer description of the package.+description:++ A parser for Open Financial Exchange (OFX) financial data. This+ handles OFX version 1.03, which is documented at http://www.ofx.net+ This also handles QFX, which is OFX plus some minor additions made+ by Intuit, the publishers of Quicken.++ .++ The parser will place all the data into a tree, which you can query+ for whatever data you may need, although you will need to be+ familiar with the OFX spec to do this. There is also a Transaction+ type which you can easily parse from the tree; this will contain+ most, if not all, of the data you will be interested in if your OFX+ file is from a bank or credit card account.++ .++ All the OFX and QFX files I have seen use the format from the 1.0+ series of OFX, which is primarily SGML based. OFX version 2 is XML+ based. If this parser works on those files, consider it a happy+ accident.++ .++ The OFX spec is available at <http://www.ofx.net>++-- URL for the project homepage or repository.+homepage: http://www.github.com/massysett/ofx++-- The license under which the package is released.+license: BSD3++-- The file containing the license text.+license-file: LICENSE++-- The package author(s).+author: Omari Norman++-- An email address to which users can send suggestions, bug reports, and +-- patches.+maintainer: omari@smileystation.com++-- A copyright notice.+-- copyright: ++category: Finance++build-type: Simple++-- Constraint on the version of Cabal needed to build this package.+cabal-version: >=1.8+++source-repository head+ type: git+ location: git://github.com/massysett/ofx.git++library+ -- Modules exported by the library.+ ghc-options: -Wall++ exposed-modules:+ Data.OFX+ + -- Modules included in this library but not exported.+ -- other-modules: + + -- Other library packages from which modules are imported.+ build-depends:+ base ==4.6.*+ , explicit-exception ==0.1.*+ , parsec ==3.1.*+ , pretty ==1.1.*+ , time ==1.4.*+