national-australia-bank-0.0.1: src/Data/Bank/NationalAustraliaBank/NationalAustraliaBank.hs
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module Data.Bank.NationalAustraliaBank.NationalAustraliaBank where
import Control.Applicative (Applicative(liftA2), Alternative((<|>)))
import Control.Category (Category(..) )
import Control.Lens
( view,
makePrisms,
(#),
makeLenses,
Field1(_1),
Field2(_2) )
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.State
( mapM, StateT, gets )
import Control.Monad.Trans.Except ( ExceptT(..) )
import Data.Bool(bool)
import Data.List ( sortBy )
import Data.Functor( Functor(..), (<$>) )
import Data.Functor.Identity ( Identity(..) )
import Data.ByteString(ByteString)
import qualified Data.ByteString.Lazy.UTF8 as L(fromString)
import Data.Digit
( charDecimal,
parseDecimal,
decDigitsIntegral,
integralDecDigits,
integralDecimal,
DecDigit(DecDigit0) )
import Data.Foldable ( Foldable, asum, toList )
import Data.List.NonEmpty ( NonEmpty(..), some1 )
import Data.String ( IsString(fromString) )
import Data.Sv
( parseDecodeFromFile,
(=:),
comma,
ParseOptions(ParseOptions),
Decode,
Decode',
NameEncode,
Headedness(Headed) )
import qualified Data.Sv.Decode as D
import qualified Data.Sv.Encode as E
import Data.Validation ( toEither )
import Data.Time ( fromGregorian, toGregorian, Day )
import Data.Functor.Contravariant ( Contravariant(contramap) )
import Data.Ratio ( Ratio, (%) )
import System.Directory
( doesDirectoryExist,
listDirectory )
import System.FilePath ( (</>) )
import Text.Parsec
( char,
string,
try,
ParsecT,
Stream )
import Prelude hiding (id, (.) )
data TransactionMonth =
Jan
| Feb
| Mar
| Apr
| May
| Jun
| Jul
| Aug
| Sep
| Oct
| Nov
| Dec
deriving (Eq, Ord, Show)
makePrisms ''TransactionMonth
parseTransactionMonth ::
Stream s m Char =>
ParsecT s u m TransactionMonth
parseTransactionMonth =
asum . fmap try $ [
Jan <$ string "Jan"
, Feb <$ string "Feb"
, Mar <$ string "Mar"
, Apr <$ string "Apr"
, May <$ string "May"
, Jun <$ string "Jun"
, Jul <$ string "Jul"
, Aug <$ string "Aug"
, Sep <$ string "Sep"
, Oct <$ string "Oct"
, Nov <$ string "Nov"
, Dec <$ string "Dec"
]
data TransactionDate =
TransactionDate {
_day1 ::
DecDigit
, _day2 ::
DecDigit
, _month ::
TransactionMonth
, _year1 ::
DecDigit
, _year2 ::
DecDigit
} deriving (Eq, Show)
makeLenses ''TransactionDate
instance Ord TransactionDate where
TransactionDate d1 d2 m y1 y2 `compare` TransactionDate e1 e2 n z1 z2 =
let twoDigits a b = decDigitsIntegral (Right (a :| [b]))
d = twoDigits d1 d2 :: Int
e = twoDigits e1 e2 :: Int
y = twoDigits y1 y2 :: Int
z = twoDigits z1 z2 :: Int
in (y, m, d) `compare` (z, n, e)
parseTransactionDate ::
Stream s Identity Char =>
ParsecT s u Identity TransactionDate
parseTransactionDate =
TransactionDate <$>
parseDecimal <*>
parseDecimal <*
char ' ' <*>
parseTransactionMonth <*
char ' ' <*>
parseDecimal <*>
parseDecimal
decodeTransactionDate ::
Decode' ByteString TransactionDate
decodeTransactionDate =
D.withParsec parseTransactionDate
encodeTransactionDate ::
NameEncode TransactionDate
encodeTransactionDate =
fromString "Date" =: E.mkEncodeBS (\(TransactionDate d1 d2 m y1 y2) ->
L.fromString (concat [[charDecimal # d1, charDecimal # d2, ' '], show m, [' ', charDecimal # y1], [charDecimal # y2]]))
transactionDayDate ::
Day
-> TransactionDate
transactionDayDate day =
let (y, m, d) = toGregorian day
twodigits x =
case either id id (integralDecDigits x) of
h :| [] ->
(DecDigit0, h)
h :| i : _ ->
(h, i)
(d1, d2) = twodigits d
m' = case m of
1 -> Jan
2 -> Feb
3 -> Mar
4 -> Apr
5 -> May
6 -> Jun
7 -> Jul
8 -> Aug
9 -> Sep
10 -> Oct
11 -> Nov
_ -> Dec
(y1, y2) = twodigits (y - 2000)
in TransactionDate d1 d2 m' y1 y2
transactionDateDay ::
TransactionDate
-> Day
transactionDateDay (TransactionDate d1 d2 m y1 y2) =
let year = 2000 + integralDecimal # y1 * 10 + integralDecimal # y2
mon Jan = 1
mon Feb = 2
mon Mar = 3
mon Apr = 4
mon May = 5
mon Jun = 6
mon Jul = 7
mon Aug = 8
mon Sep = 9
mon Oct = 10
mon Nov = 11
mon Dec = 12
dt = integralDecimal # d1 * 10 + integralDecimal # d2
in fromGregorian year (mon m) dt
data TransactionAmount =
TransactionAmount {
_negated ::
Bool
, _dollars ::
NonEmpty DecDigit
, _cents1 ::
DecDigit
, _cents2 ::
DecDigit
} deriving (Eq, Ord, Show)
makeLenses ''TransactionAmount
parseTransactionAmount ::
Stream s Identity Char =>
ParsecT s u Identity TransactionAmount
parseTransactionAmount =
TransactionAmount <$>
try (True <$ char '-' <|> pure False) <*>
some1 parseDecimal <*
char '.' <*>
parseDecimal <*>
parseDecimal
decodeTransactionAmount ::
Decode' ByteString TransactionAmount
decodeTransactionAmount =
D.withParsec parseTransactionAmount
encodeTransactionAmount ::
String
-> NameEncode TransactionAmount
encodeTransactionAmount label =
fromString label =: E.mkEncodeBS (\(TransactionAmount n d c1 c2) ->
L.fromString (concat [bool "" "-" n, fmap (charDecimal #) (toList d), ".", [charDecimal # c1], [charDecimal # c2]]))
realTransactionAmount ::
Integral a =>
TransactionAmount
-> Ratio a
realTransactionAmount (TransactionAmount n ds d1 d2) =
let neg =
if n then Left else Right
in (decDigitsIntegral (neg ds) * 100 + decDigitsIntegral (Right (d1 :| [d2]))) % 100
data Transaction =
Transaction {
_date ::
TransactionDate
, _amount ::
TransactionAmount
, _accountNumber ::
String
, _emptyField ::
String
, _transactionType ::
String
, _details ::
String
, _balance ::
TransactionAmount
, _category ::
String
, _merchantName ::
String
} deriving (Eq, Ord, Show)
makeLenses ''Transaction
encodeTransaction ::
NameEncode Transaction
encodeTransaction =
contramap _date encodeTransactionDate <>
contramap _amount (encodeTransactionAmount "Amount") <>
fromString "Account Number" =: contramap _accountNumber E.string <>
fromString "" =: contramap _emptyField E.string <>
fromString "Transaction Type" =: contramap _transactionType E.string <>
fromString "Transaction Details" =: contramap _details E.string <>
contramap _balance (encodeTransactionAmount "Balance") <>
fromString "Category" =: contramap _category E.string <>
fromString "Merchant Name" =: contramap _merchantName E.string
encodeTransactionNormalised ::
NameEncode Transaction
encodeTransactionNormalised =
contramap _date encodeTransactionDate <>
contramap _amount (encodeTransactionAmount "Amount") <>
fromString "Acc#" =: contramap _accountNumber E.string <>
fromString "Type" =: contramap _transactionType E.string <>
fromString "Details" =: contramap _details E.string <>
contramap _balance (encodeTransactionAmount "Balance") <>
fromString "Merchant" =: contramap _merchantName E.string
encodeTransactionencodeTransactionNormalisedOtherCat ::
NameEncode (Transaction, Int)
encodeTransactionencodeTransactionNormalisedOtherCat =
contramap (view _1) encodeTransactionNormalised <>
fromString "Other Categories" =: contramap (view _2) E.int
decodeTransaction ::
Decode ByteString ByteString Transaction
decodeTransaction =
Transaction <$>
decodeTransactionDate <*>
decodeTransactionAmount <*>
D.string <*>
D.string <*>
D.string <*>
D.string <*>
decodeTransactionAmount <*>
D.string <*>
D.string
type TransactionState f a =
StateT Transaction f a
foldMap'TransactionState ::
(Foldable t, Applicative f, Monoid b) =>
(a -> f b)
-> t a
-> f b
foldMap'TransactionState f =
foldl (\b a -> liftA2 (<>) (f a) b) (pure mempty)
(|>) ::
(Semigroup a, Applicative f) =>
f a
-> f a
-> f a
s |> t =
liftA2 (<>) s t
infixr 6 |>
date' ::
Monad f =>
TransactionState f TransactionDate
date' =
gets (view date)
dateDay' ::
Monad f =>
TransactionState f Day
dateDay' =
fmap transactionDateDay date'
amount' ::
Monad f =>
TransactionState f TransactionAmount
amount' =
gets (view amount)
amountRatio' ::
(Monad f, Integral b) =>
TransactionState f (Ratio b)
amountRatio' =
fmap realTransactionAmount amount'
accountNumber' ::
Monad f =>
TransactionState f String
accountNumber' =
gets (view accountNumber)
emptyField' ::
Monad f =>
TransactionState f String
emptyField' =
gets (view emptyField)
transactionType' ::
Monad f =>
TransactionState f String
transactionType' =
gets (view transactionType)
details' ::
Monad f =>
TransactionState f String
details' =
gets (view details)
balance' ::
Monad f =>
TransactionState f TransactionAmount
balance' =
gets (view balance)
balanceRatio' ::
(Monad f, Integral b) =>
TransactionState f (Ratio b)
balanceRatio' =
fmap realTransactionAmount balance'
category' ::
Monad f =>
TransactionState f String
category' =
gets (view category)
merchantName' ::
Monad f =>
TransactionState f String
merchantName' =
gets (view merchantName)
parseTransactionDirectory ::
MonadIO m =>
FilePath
-> ExceptT (D.DecodeErrors ByteString) m [Transaction]
parseTransactionDirectory p =
let parseCSVDirectory ::
MonadIO m =>
Decode' ByteString a
-> FilePath
-> ExceptT (D.DecodeErrors ByteString) m [a]
parseCSVDirectory dec fp =
let files =
do e <- liftIO (doesDirectoryExist fp)
if e
then
do d <- liftIO (listDirectory fp)
pure (fmap (fp </>) d)
else
pure [fp]
in ExceptT $
do f <- files
x <- mapM (parseDecodeFromFile dec (ParseOptions comma Headed)) f
pure (fmap concat (toEither (sequenceA x)))
sortTransactions ::
[Transaction]
-> [Transaction]
sortTransactions =
let
comp t1 t2 =
let d1 =
transactionDateDay (view date t1)
d2 =
transactionDateDay (view date t2)
a1 =
view accountNumber t1
a2 =
view accountNumber t2
in d1 `compare` d2 <> a2 `compare` a1
in sortBy comp
in fmap sortTransactions (parseCSVDirectory decodeTransaction p)