Este documento se refere ao exercício 4 da LE1, que pode ser encontrado neste documento.
Funções expostas por esse TAD
module LE1.Clientes.TAD
( criaCliente
, getCliente
, carregaClientes
, salvaCliente
, salvaClientes
, excluirCliente
, numClientes
, vazio
, isVazio
, isInvalido
, clientes
, toList
) where
Primeiro, a representação do TAD Cliente
e um apelido para um lista de Clientes
data Cliente = Invalido | Vazio | Cliente { codigo :: Integer
, nome :: B.ByteString
, endereco :: B.ByteString
, telefone :: B.ByteString
, dt_primeira_compra :: B.ByteString
, dt_ultima_compra :: B.ByteString
, valor_ultima_compra :: Decimal
} deriving (Show, Eq)
type Clientes = [Cliente]
Para os campos que são de fato uma String
, utilizo o tipo ByteString
em sua versão Lazy
.
Um ByteString
é a representação de cada byte num Array
que é o conteúdo. É Lazy
pois cada byte, ou
melhor cada chunk
ou naco
de bytes só é computado quando necessário.
Para efeito de testes, escolhi organizar os dados de um Cliente
em arquivos CSV
O exercício sugere a implementação de três funções:
A primeira é para inserir um Cliente
num arquivo. Também implementei uma para salvar diversos
Clientes
de uma vez em um arquivo, um por linha.
Cada função recebe um Cliente
ou uma lista deles e um FilePath
que é apenas
um apelido para o tipo String
.
Eu simplesmente “encodo”, ou, empacoto e converto todos os campos para um único
ByteString
, separado por vírgulas. Deopis, acrescento as novas linhas no arquivo caso ele exista
ou crio outro caso ele não exista.
Note que na função salvaClientes
eu uso a função mapM_
, que, assim como a map
, aplica uma função
em cada elemento de uma lista. Porém, nesse caso, é uma lista de operações IO
, que é uma
Mônada. Portanto, utilizo a versão mapM_
que permite iterar uma lista de valores envolvidos por
uma Mônada, aplicar a função desejada em cada elemento, ou nesse caso, apenas executar a tarefa IO
,
e ainda descartar a nova lista gerada por essa operação, pois não me importo com ela
salvaCliente :: Cliente -> FilePath -> IO ()
salvaCliente c path = do
nl <- return $ B.pack "\n"
conteudo <- return $ B.concat (nl:(converteCliente c):[])
B.appendFile path conteudo
salvaClientes :: Clientes -> FilePath -> IO ()
salvaClientes [] _ = putStrLn "Lista vazia"
salvaClientes xs path = mapM_ (\x -> salvaCliente x path) xs >> putStrLn "Os Clientes foram salvos!"
Para consultar um Cliente
, você precisa carregar os Clientes
de um arquivo, e dado um índice,
recuperar o valor da lista. Porém o operador (!!)
não é seguro, uma vez que dado um índice maior
que o tamanho da lista ou menor do que 0
, será lançada uma exeção.
Para evitar isso, faço uma simples conta onde se o índice dado for negativo, converto o índice para ser acessível na lista:
Tendo índice == x,
Se x < 0 -> troco o sinal e retorno o cliente na posição (-x)
Se x > tamanho lista -> retorno o Cliente na posição do resto do índice pelo tamanho da lista
getCliente :: IO Clientes -> Int -> IO Cliente
getCliente c_io idx = do
num <- numClientes c_io
c <- c_io
if idx > num || idx < 0
then return $ c !! (mod (negate idx) num)
else return $ c !! idx
Já para excluir um Cliente
, dado uma lista de Clientes
, um índice e um FilePath
, recupero o cliente
com a função getCliente
(atente-se à conta de “offset”), removo-o da lista e sobrescrevo um arquivo
já existente ou crio outro caso não exista
excluirCliente :: IO Clientes -> Int -> FilePath -> IO Cliente
excluirCliente cs idx path = do
cs' <- cs
cl <- getCliente cs idx
cl' <- return cl
cs'' <- return $ filter (/= cl') cs'
conteudo <- return . B.unlines $ map (converteCliente) cs''
_ <- B.writeFile path conteudo
return cl
Primeiro, defino algumas funções para lidar com um Cliente
inválido ou vazio
e algumas outras para compatibilidade com listas
vazio :: Cliente
vazio = Vazio
isVazio :: Cliente -> Bool
isVazio Vazio = True
isVazio _ = False
isInvalido :: Cliente -> Bool
isInvalido Invalido = True
isInvalido _ = False
toList :: Cliente -> [String]
toList Vazio = ["Cliente Vazio"]
toList Invalido = ["Cliente Inválido"]
toList (Cliente c n e t dt_p dt_u v_u) = list
where c' = show c
n' = B.unpack n
e' = B.unpack e
t' = B.unpack t
dt_p' = B.unpack dt_p
dt_u' = B.unpack dt_u
v_u' = show v_u
list = [c', n', e', t', dt_p', dt_u', v_u']
Dado os seguintes parâmetros, em ordem: código, nome, endereço, telefone,
data primeira compra, data última compra e valor da última compra, retorno um Cliente
criaCliente :: (Integer, String, String, String, String, String, Decimal) -> Cliente
criaCliente (c, n, e, t, dt_p, dt_u, v_u) = Cliente c n' e' t' dt_p' dt_u' v_u
where n' = B.pack n
e' = B.pack e
t' = B.pack t
dt_p' = B.pack dt_p
dt_u' = B.pack dt_u
Dado um FilePath
, leio o conteúdo deste arquivo e converto cada linha para um Cliente
carregaClientes :: FilePath -> IO Clientes
carregaClientes path = do
conteudo <- leArquivo path
case conteudo of
[[]] -> return []
_ -> do
clientes' <- return $ filter (/= Invalido) (map (leCliente) conteudo)
return clientes'
Converto um TAD Cliente
para uma representação de um único ByteString
, separado por vírgulas
converteCliente :: Cliente -> B.ByteString
converteCliente Invalido = B.pack ""
converteCliente Vazio = B.intercalate (B.pack ",") cliente
where (c:n:t:e:dt_p:dt_u:v_u:_) = packBist $ map (\_ -> "") ['a' .. 'g']
cliente = c:n:t:e:dt_p:dt_u:v_u:[]
converteCliente (Cliente c n t e dt_p dt_u v_u) = cliente'
where cod = B.pack $ show c
va = B.pack $ show v_u
cliente = cod:n:t:e:dt_p:dt_u:va:[]
cliente' = B.intercalate (B.pack ",") cliente
Dado uma lista de ByteString
, tento converter cada linha em um Cliente
válido.
Caso contrário, retorno um Cliente
inválido
leCliente :: [B.ByteString] -> Cliente
leCliente (x:_)
| x == B.empty = Invalido
leCliente (c:n:t:e:dt_p:dt_u:v_u:_) = Cliente cod n e t dt_p dt_u va_u
where cod = read (B.unpack c) :: Integer
va_u = read (B.unpack v_u) :: Decimal
leCliente _ = Invalido
Delimito que apenas arquivos CSV
podem ser lidos, separo todo o conteúdo em linhas
e depois separo cada linha em campos pelo delimitador de vírgula, retornando
um lista bidimensional envolvida por uma Mônada IO
leArquivo :: FilePath -> IO [[B.ByteString]]
leArquivo caminho = do
status <- getFileStatus caminho
if isDirectory status
then return [[]]
else if not $ isInfixOf ".csv" caminho
then return [[]]
else do
conteudo <- B.readFile caminho
return [B.split ',' l | l <- B.lines conteudo]
Converto uma lista de String
para uma lista de ByteString
packBist :: [String] -> [B.ByteString]
packBist xs = map (B.pack) xs