packages feed

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 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.*+