Compare commits

11 Commits
Author SHA1 Message Date
jlamothe 82085eaaf9 note about pull requests 2022-04-24 22:11:28 -04:00
jlamothe 493b7dd9d4 version 0.1.0 2022-04-24 19:03:01 -04:00
jlamothe 723f046ea4 added homepage and bug reports information 2022-04-24 19:00:47 -04:00
jlamothe bb970e4f42 removed warning about API changes 2022-04-24 18:47:53 -04:00
jlamothe 1b20188dfc added executive summary to README.md 2022-04-24 18:47:28 -04:00
jlamothe 886f991ee3 implemented file producers/consumers
- readFromCSV
- readFromCSVRaw
- writeToCSV
- writeToCSVRaw
2022-04-24 18:32:30 -04:00
jlamothe 659b817252 removed writeCSVFromStream and writeRawCSVFromStream 2022-04-24 17:15:27 -04:00
jlamothe d27eb91952 implemented file writing 2022-04-24 16:47:24 -04:00
jlamothe 5c74085ada implemented encodeCSV and encodeRawCSV 2022-04-24 16:05:10 -04:00
jlamothe 3df133147f implemented encodeRows 2022-04-24 16:05:10 -04:00
jlamothe 5d6a7db6c5 implemented encodeRawRows 2022-04-24 14:23:49 -04:00
5 changed files with 218 additions and 7 deletions
+5 -2
View File
@@ -14,5 +14,8 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
## Important Note ## Executive Summary
This library is not yet ready for release. As such, all code should be considered to be unstable and subject to change at any time. This library allows for reading and writing to and from CSV files in a streaming manner. Files can be read and written to on a row-by-row basis allowing larger files to be worked with, since the whole file doesn't have to be loaded to manipulate it. It is based on the [conduit](https://hackage.haskell.org/package/conduit] library.
## Pull Requests
Please make pull requests to the `dev` branch.
+3 -1
View File
@@ -5,10 +5,12 @@ cabal-version: 2.2
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: csv-sip name: csv-sip
version: 0.0.0 version: 0.1.0
synopsis: extracts data from a CSV file synopsis: extracts data from a CSV file
description: extracts data from a CSV file - see README.md for more details description: extracts data from a CSV file - see README.md for more details
category: Data category: Data
homepage: https://codeberg.org/jlamothe/csv-sip
bug-reports: https://codeberg.org/jlamothe/csv-sip/issues
author: Jonathan Lamothe author: Jonathan Lamothe
maintainer: jonathan@jlamothe.net maintainer: jonathan@jlamothe.net
copyright: (C) 2022 Jonathan Lamothe copyright: (C) 2022 Jonathan Lamothe
+3 -1
View File
@@ -1,5 +1,5 @@
name: csv-sip name: csv-sip
version: 0.0.0 version: 0.1.0
license: GPL-3.0-or-later license: GPL-3.0-or-later
author: "Jonathan Lamothe" author: "Jonathan Lamothe"
maintainer: "jonathan@jlamothe.net" maintainer: "jonathan@jlamothe.net"
@@ -17,6 +17,8 @@ category: Data
# complications of embedding Haddock markup inside cabal files, it is # complications of embedding Haddock markup inside cabal files, it is
# common to point users to the README.md file. # common to point users to the README.md file.
description: extracts data from a CSV file - see README.md for more details description: extracts data from a CSV file - see README.md for more details
homepage: https://codeberg.org/jlamothe/csv-sip
bug-reports: https://codeberg.org/jlamothe/csv-sip/issues
ghc-options: ghc-options:
- -Wall - -Wall
+130 -3
View File
@@ -26,12 +26,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{-# LANGUAGE LambdaCase, OverloadedStrings #-} {-# LANGUAGE LambdaCase, OverloadedStrings #-}
module Data.CSV.Sip ( module Data.CSV.Sip (
-- * Read an entire CSV file -- * Working with Files
-- ** Read an entire CSV file
slurpCSV, slurpCSV,
slurpRawCSV, slurpRawCSV,
slurpLabelledCSV, slurpLabelledCSV,
slurpRawLabelledCSV, slurpRawLabelledCSV,
-- ** Write an entire CSV file
writeCSV,
writeRawCSV,
-- * Conduits -- * Conduits
-- ** Producers
readFromCSV,
readFromCSVRaw,
encodeCSV,
encodeRawCSV,
-- ** Consumers
writeToCSV,
writeToCSVRaw,
-- ** Transformers
-- *** Encoding
encodeRows,
encodeRawRows,
-- *** Decoding
labelFields, labelFields,
decodeRows, decodeRows,
decodeRawRows, decodeRawRows,
@@ -45,6 +62,7 @@ import Conduit
, await , await
, mapC , mapC
, runConduit , runConduit
, sinkFile
, sourceFile , sourceFile
, yield , yield
, (.|) , (.|)
@@ -53,11 +71,11 @@ import Control.Monad (unless)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (StateT, evalStateT, get, gets, modify) import Control.Monad.Trans.State (StateT, evalStateT, get, gets, modify)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Conduit.List (consume) import Data.Conduit.List (consume, sourceList)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8') import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Data.Word (Word8) import Data.Word (Word8)
-- | read an entire CSV file -- | read an entire CSV file
@@ -94,6 +112,90 @@ slurpRawLabelledCSV
slurpRawLabelledCSV file = runConduit $ slurpRawLabelledCSV file = runConduit $
sourceFile file .| decodeRawRows .| labelFields .|consume sourceFile file .| decodeRawRows .| labelFields .|consume
-- | write a CSV file from Text-based rows
writeCSV
:: MonadResource m
=> FilePath
-- ^ the path to the file to write to
-> [[T.Text]]
-- ^ the fields/rows being written
-> m ()
writeCSV file csv = runConduit $ encodeCSV csv .| sinkFile file
-- | write a CSV file from raw ByteString-based rows
writeRawCSV
:: MonadResource m
=> FilePath
-- ^ the path to the file to write to
-> [[BS.ByteString]]
-- ^ the fields/rows being written
-> m ()
writeRawCSV file csv = runConduit $ encodeRawCSV csv .| sinkFile file
-- | reads a stream of Text-based rows from a CSV file
readFromCSV
:: MonadResource m
=> FilePath
-- ^ the path to the CSV file to read from
-> ConduitT i [T.Text] m ()
readFromCSV file = sourceFile file .| decodeRows
-- | reads a stream of ByteString-based rows from a CSV file
readFromCSVRaw
:: MonadResource m
=> FilePath
-- ^ the path to the CSV file to read from
-> ConduitT i [BS.ByteString] m ()
readFromCSVRaw file = sourceFile file .| decodeRawRows
-- | encode an entire CSV file
encodeCSV
:: Monad m
=> [[T.Text]]
-- ^ the data being encoded, organized into rows and fields
-> ConduitT () BS.ByteString m ()
encodeCSV csv = sourceList csv .| encodeRows
-- | encode an entire CSV file
encodeRawCSV
:: Monad m
=> [[BS.ByteString]]
-- ^ the data being encoded, organized into rows and fields
-> ConduitT () BS.ByteString m ()
encodeRawCSV csv = sourceList csv .| encodeRawRows
-- | Writes a stream of Text-based rows to a CSV file
writeToCSV
:: MonadResource m
=> FilePath
-- ^ the path to the CSV file to write to
-> ConduitT [T.Text] o m ()
writeToCSV file = encodeRows .| sinkFile file
-- | Writes a stream of ByteString-based rows to a CSV file
writeToCSVRaw
:: MonadResource m
=> FilePath
-- ^ the path to the CSV file to write to
-> ConduitT [BS.ByteString] o m ()
writeToCSVRaw file = encodeRawRows .| sinkFile file
-- | encode a CSV stream row by row, each element in the list read
-- represents a field, with the entire list representing a row
encodeRows :: Monad m => ConduitT [T.Text] BS.ByteString m ()
encodeRows = mapC (map encodeUtf8) .| encodeRawRows
-- | encode raw CSV stream row by row, each element in the list read
-- represents a field, with the entire list representing a row
encodeRawRows :: Monad m => ConduitT [BS.ByteString] BS.ByteString m ()
encodeRawRows = await >>= \case
Just fs-> do
encodeFields fs
encodeRawRows
Nothing -> return ()
-- | read a CSV stream, using the first row as a header containing -- | read a CSV stream, using the first row as a header containing
-- field labels -- field labels
labelFields :: (Monad m, Ord a) => ConduitT [a] (M.Map a a) m () labelFields :: (Monad m, Ord a) => ConduitT [a] (M.Map a a) m ()
@@ -144,6 +246,16 @@ newDecodeState = DecodeState
-- Conduits -- Conduits
encodeFields
:: Monad m
=> [BS.ByteString]
-> ConduitT [BS.ByteString] BS.ByteString m ()
encodeFields [] = yield "\r\n"
encodeFields [f] = yield $ escapeField f `BS.append` "\r\n"
encodeFields (f:fs) = do
yield $ escapeField f `BS.append` ","
encodeFields fs
labelLoop :: (Monad m, Ord a) => [a] -> ConduitT [a] (M.Map a a) m () labelLoop :: (Monad m, Ord a) => [a] -> ConduitT [a] (M.Map a a) m ()
labelLoop headers = await >>= \case labelLoop headers = await >>= \case
Just values -> do Just values -> do
@@ -270,4 +382,19 @@ dropField s = s
setQuoted :: Modifier setQuoted :: Modifier
setQuoted s = s { isQuoted = True } setQuoted s = s { isQuoted = True }
-- Helpers
escapeField :: BS.ByteString -> BS.ByteString
escapeField field = let
bytes = BS.unpack field
in BS.concat
[ "\""
, BS.pack $ escapeLoop bytes
, "\""
]
escapeLoop :: [Word8] -> [Word8]
escapeLoop [] = []
escapeLoop (0x22:bs) = [0x22, 0x22] ++ escapeLoop bs -- escape quote
escapeLoop (b:bs) = b : escapeLoop bs
--jl --jl
+77
View File
@@ -23,6 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
module Data.CSV.SipSpec (spec) where module Data.CSV.SipSpec (spec) where
import Conduit (runConduit, (.|)) import Conduit (runConduit, (.|))
import qualified Data.ByteString as BS
import Data.Char (ord) import Data.Char (ord)
import Data.Conduit.List (consume, sourceList) import Data.Conduit.List (consume, sourceList)
import qualified Data.Map as M import qualified Data.Map as M
@@ -32,12 +33,88 @@ import Data.CSV.Sip
spec :: Spec spec :: Spec
spec = describe "Data.CSV.Sip" $ do spec = describe "Data.CSV.Sip" $ do
encodeCSVSpec
encodeRawCSVSpec
encodeRowsSpec
encodeRawRowsSpec
labelFieldsSpec labelFieldsSpec
decodeRowsSpec decodeRowsSpec
decodeRawRowsSpec decodeRawRowsSpec
decodeUTF8Spec decodeUTF8Spec
toBytesSpec toBytesSpec
encodeCSVSpec :: Spec
encodeCSVSpec = describe "encodeCSV" $ do
result <- BS.concat <$> runConduit
(encodeCSV input .| consume)
it ("shouldBe " ++ show expected) $
result `shouldBe` expected
where
input =
[ [ "foo", "a\"b" ]
, [ "a\rb", "a\nb" ]
]
expected = BS.concat
[ "\"foo\",\"a\"\"b\"\r\n"
, "\"a\rb\",\"a\nb\"\r\n"
]
encodeRawCSVSpec :: Spec
encodeRawCSVSpec = describe "encodeRawCSV" $ do
result <- BS.concat <$> runConduit
(encodeRawCSV input .| consume)
it ("shouldBe " ++ show expected) $
result `shouldBe` expected
where
input =
[ [ "foo", "a\"b" ]
, [ "a\rb", "a\nb" ]
]
expected = BS.concat
[ "\"foo\",\"a\"\"b\"\r\n"
, "\"a\rb\",\"a\nb\"\r\n"
]
encodeRowsSpec :: Spec
encodeRowsSpec = describe "encodeRows" $ do
result <- BS.concat <$> runConduit
(sourceList input .| encodeRows .| consume)
it ("shouldBe " ++ show expected) $
result `shouldBe` expected
where
input =
[ [ "foo", "a\"b" ]
, [ "a\rb", "a\nb" ]
]
expected = BS.concat
[ "\"foo\",\"a\"\"b\"\r\n"
, "\"a\rb\",\"a\nb\"\r\n"
]
encodeRawRowsSpec :: Spec
encodeRawRowsSpec = describe "encodeRawRows" $ do
result <- BS.concat <$> runConduit
(sourceList input .| encodeRawRows .| consume)
it ("should be " ++ show expected) $
result `shouldBe` expected
where
input =
[ [ "foo", "a\"b" ]
, [ "a\rb", "a\nb" ]
]
expected = BS.concat
[ "\"foo\",\"a\"\"b\"\r\n"
, "\"a\rb\",\"a\nb\"\r\n"
]
labelFieldsSpec :: Spec labelFieldsSpec :: Spec
labelFieldsSpec = describe "labelFields" $ mapM_ labelFieldsSpec = describe "labelFields" $ mapM_
( \(label, input, expected) -> context label $ do ( \(label, input, expected) -> context label $ do