Safe Haskell | Safe |
---|---|
Language | Haskell98 |
Parser for downloaded OFX files.
This parser was written based on the OFX version 1.03 specification, which is available at
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
- type Err = Either String
- type HeaderTag = String
- type HeaderValue = String
- data OFXHeader = OFXHeader HeaderTag HeaderValue
- type TagName = String
- type TagData = String
- data Tag = Tag TagName (Either TagData [Tag])
- data OFXFile = OFXFile {}
- find :: TagName -> Tag -> [Tag]
- findPath :: [TagName] -> Tag -> Maybe Tag
- tagData :: Tag -> Maybe TagData
- pathData :: [TagName] -> OFXFile -> Maybe TagData
- findData :: TagName -> Tag -> Maybe TagData
- fiName :: OFXFile -> Maybe TagData
- creditCardNumber :: OFXFile -> Maybe TagData
- bankAccountNumber :: OFXFile -> Maybe TagData
- accountNumber :: OFXFile -> Maybe TagData
- data Transaction = Transaction {
- txTRNTYPE :: TrnType
- txDTPOSTED :: ZonedTime
- txDTUSER :: Maybe ZonedTime
- txDTAVAIL :: Maybe ZonedTime
- txTRNAMT :: String
- txFITID :: String
- txCORRECTFITID :: Maybe String
- txCORRECTACTION :: Maybe CorrectAction
- txSRVRTID :: Maybe String
- txCHECKNUM :: Maybe String
- txREFNUM :: Maybe String
- txSIC :: Maybe String
- txPAYEEID :: Maybe String
- txPayeeInfo :: Maybe (Either String Payee)
- txAccountTo :: Maybe (Either BankAcctTo CCAcctTo)
- txMEMO :: Maybe String
- txCurrency :: Maybe (Either Currency OrigCurrency)
- transaction :: Tag -> Err Transaction
- transactions :: OFXFile -> Err [Transaction]
- data TrnType
- trnType :: TagData -> Maybe TrnType
- data Payee = Payee {}
- payee :: Tag -> Maybe (Err Payee)
- data CorrectAction
- data BankAcctTo = BankAcctTo {}
- bankAcctTo :: Tag -> Maybe (Err BankAcctTo)
- data CCAcctTo = CCAcctTo {}
- ccAcctTo :: Tag -> Maybe (Err CCAcctTo)
- data AcctType
- acctType :: String -> Err AcctType
- data CurrencyData = CurrencyData {}
- currencyData :: Tag -> Err CurrencyData
- data Currency = Currency CurrencyData
- currency :: Tag -> Maybe (Err Currency)
- data OrigCurrency = OrigCurrency CurrencyData
- origCurrency :: Tag -> Maybe (Err OrigCurrency)
- ofxFile :: Parser OFXFile
- newline :: Parser ()
- escChar :: Parser Char
- header :: Parser OFXHeader
- openingTag :: Parser TagName
- closingTag :: TagName -> Parser ()
- tag :: Parser Tag
- date :: Parser ZonedTime
- time :: Parser (TimeOfDay, TimeZone)
- tzOffset :: Parser TimeZone
- pPayee :: Payee -> Doc
- pTransaction :: Transaction -> Doc
- pTag :: Tag -> Doc
- pHeader :: OFXHeader -> Doc
- pFile :: OFXFile -> Doc
- pEither :: (a -> Doc) -> (b -> Doc) -> Either a b -> Doc
- pMaybe :: (a -> Doc) -> Maybe a -> Doc
- pList :: [Doc] -> Doc
- label :: String -> Doc -> Doc
- pExceptional :: (e -> Doc) -> (a -> Doc) -> Either e a -> Doc
Error handling
type Err = Either String Source
Error handling. Errors are indicated with a Left String; successes with a Right.
The OFX data tree
type HeaderValue = String Source
The value in an OFX header.
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.
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.
All the data from an OFX file.
Manipulating the OFX tag tree
find :: TagName -> Tag -> [Tag] Source
Finds child tags with the given name. When a tag is found, that tag is not searched for further children with the same name.
findPath :: [TagName] -> Tag -> Maybe Tag Source
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
pathData :: [TagName] -> OFXFile -> Maybe TagData Source
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
findData :: TagName -> Tag -> Maybe TagData Source
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.
Extracting specific data
fiName :: OFXFile -> Maybe TagData Source
Gets the name of the financial institution from the FI tag, if available. The OFX spec does not require this tag to be present.
creditCardNumber :: OFXFile -> Maybe TagData Source
Gets the credit card number, if available. The OFX spec does not require this tag to be present.
bankAccountNumber :: OFXFile -> Maybe TagData Source
Gets the bank account number, if available. The OFX spec does not require this tag to be present.
accountNumber :: OFXFile -> Maybe TagData Source
Gets either the credit card or bank account number, if available.
Types to represent specific OFX data
data Transaction Source
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.
Transaction | |
|
transaction :: Tag -> Err Transaction Source
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.
transactions :: OFXFile -> Err [Transaction] Source
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 :-)
OFX transaction types. These are used in STMTTRN aggregates, see OFX spec section 11.4.2.3.1.1.
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 |
:: Tag | The tag which contains the PAYEE tag, if there is one. This would typically be a STMTTRN tag. |
-> Maybe (Err Payee) | Nothing if there is no PAYEE tag. Just if a PAYEE tag is found, with a Left if the tag is lacking a required element, or a Right if the tag is successfully parsed. If there is more than one PAYEE tag, only the first one is considered. |
Parses a Payee record from its parent tag.
data CorrectAction Source
Can be either REPLACE or DELETE.
bankAcctTo :: Tag -> Maybe (Err BankAcctTo) Source
:: Tag | The tag that contains the data, e.g. CURRENCY or ORIGCURRENCY. |
-> Err CurrencyData |
Parses currency data.
origCurrency :: Tag -> Maybe (Err OrigCurrency) Source
Parsec 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.
Parses a character, possibly with an escape sequence. The greater-than sign, less-than sign, and ampersand must be entered with escape sequences.
According to OFX spec section 2.3.2.1, ampersands, less-than signs, and greater-than signs must appear as entities. However some banks deliver broken OFX files that do not use entities for ampersands (and possibly for less-than or greater-than signs too, although I have not yet observed such behavior.) There is now an error message that reflects this problem. Client code can filter the OFX data for known offenders before passing it to this library.
openingTag :: Parser TagName Source
Parses any opening tag. Returns the name of the tag.
closingTag :: TagName -> Parser () Source
Parses a closing tag with the given name.
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.
date :: Parser ZonedTime Source
Parses an OFX date. Fails if the date is not valid or if there is no date to be parsed.
time :: Parser (TimeOfDay, TimeZone) Source
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.
tzOffset :: Parser TimeZone Source
Parses a time zone offset. Fails if there is no time zone offset to parse.
Pretty printers
pTransaction :: Transaction -> Doc Source