Skip to content

Commit a1e9f15

Browse files
tomferonnkpart
authored andcommitted
Functions taking a custom record parser
decodeWithP and decodeByNameWithP take a parser function instead of using the one provided by the FromRecod / FromNamedRecord instances.
1 parent 341963b commit a1e9f15

File tree

4 files changed

+67
-22
lines changed

4 files changed

+67
-22
lines changed

Data/Csv.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ module Data.Csv
4848
, DecodeOptions(..)
4949
, defaultDecodeOptions
5050
, decodeWith
51+
, decodeWithP
5152
, decodeByNameWith
53+
, decodeByNameWithP
5254
, EncodeOptions(..)
5355
, Quoting(..)
5456
, defaultEncodeOptions

Data/Csv/Encoding.hs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ module Data.Csv.Encoding
2424
, DecodeOptions(..)
2525
, defaultDecodeOptions
2626
, decodeWith
27+
, decodeWithP
2728
, decodeByNameWith
29+
, decodeByNameWithP
2830
, EncodeOptions(..)
2931
, defaultEncodeOptions
3032
, encodeWith
@@ -57,6 +59,7 @@ import Data.Csv.Conversion (FromNamedRecord, FromRecord, ToNamedRecord,
5759
ToRecord, parseNamedRecord, parseRecord, runParser,
5860
toNamedRecord, toRecord)
5961
import Data.Csv.Parser hiding (csv, csvWithHeader)
62+
import qualified Data.Csv.Conversion as Conversion
6063
import qualified Data.Csv.Parser as Parser
6164
import Data.Csv.Types hiding (toNamedRecord)
6265
import qualified Data.Csv.Types as Types
@@ -117,7 +120,7 @@ decodeWith :: FromRecord a
117120
-- skipped
118121
-> L.ByteString -- ^ CSV data
119122
-> Either String (Vector a)
120-
decodeWith = decodeWithC csv
123+
decodeWith = decodeWithC (csv parseRecord)
121124
{-# INLINE [1] decodeWith #-}
122125

123126
{-# RULES
@@ -130,12 +133,23 @@ idDecodeWith :: DecodeOptions -> HasHeader -> L.ByteString
130133
-> Either String (Vector (Vector B.ByteString))
131134
idDecodeWith = decodeWithC Parser.csv
132135

136+
-- | Like 'decodeWith'', but lets you specify a parser function.
137+
decodeWithP :: (Record -> Conversion.Parser a)
138+
-- ^ Custom parser function
139+
-> DecodeOptions -- ^ Decoding options
140+
-> HasHeader -- ^ Data contains header that should be
141+
-- skipped
142+
-> L.ByteString -- ^ CSV data
143+
-> Either String (Vector a)
144+
decodeWithP _parseRecord = decodeWithC (csv _parseRecord)
145+
{-# INLINE [1] decodeWithP #-}
146+
133147
-- | Decode CSV data using the provided parser, skipping a leading
134148
-- header if 'hasHeader' is 'HasHeader'. Returns 'Left' @errMsg@ on
135149
-- failure.
136150
decodeWithC :: (DecodeOptions -> AL.Parser a) -> DecodeOptions -> HasHeader
137151
-> BL8.ByteString -> Either String a
138-
decodeWithC p !opts hasHeader = decodeWithP parser
152+
decodeWithC p !opts hasHeader = decodeWithP' parser
139153
where parser = case hasHeader of
140154
HasHeader -> header (decDelimiter opts) *> p opts
141155
NoHeader -> p opts
@@ -147,7 +161,16 @@ decodeByNameWith :: FromNamedRecord a
147161
=> DecodeOptions -- ^ Decoding options
148162
-> L.ByteString -- ^ CSV data
149163
-> Either String (Header, Vector a)
150-
decodeByNameWith !opts = decodeWithP (csvWithHeader opts)
164+
decodeByNameWith !opts = decodeWithP' (csvWithHeader parseNamedRecord opts)
165+
166+
-- | Like 'decodeByNameWith', but lets you specify a parser function.
167+
decodeByNameWithP :: (NamedRecord -> Conversion.Parser a)
168+
-- ^ Custom parser function
169+
-> DecodeOptions -- ^ Decoding options
170+
-> L.ByteString -- ^ CSV data
171+
-> Either String (Header, Vector a)
172+
decodeByNameWithP _parseNamedRecord !opts =
173+
decodeWithP' (csvWithHeader _parseNamedRecord opts)
151174

152175
-- | Should quoting be applied to fields, and at which level?
153176
data Quoting
@@ -328,8 +351,8 @@ prependToAll :: Builder -> [Builder] -> [Builder]
328351
prependToAll _ [] = []
329352
prependToAll sep (x:xs) = sep <> x : prependToAll sep xs
330353

331-
decodeWithP :: AL.Parser a -> L.ByteString -> Either String a
332-
decodeWithP p s =
354+
decodeWithP' :: AL.Parser a -> L.ByteString -> Either String a
355+
decodeWithP' p s =
333356
case AL.parse p s of
334357
AL.Done _ v -> Right v
335358
AL.Fail left _ msg -> Left errMsg
@@ -338,7 +361,7 @@ decodeWithP p s =
338361
(if BL8.length left > 100
339362
then (take 100 $ BL8.unpack left) ++ " (truncated)"
340363
else show (BL8.unpack left))
341-
{-# INLINE decodeWithP #-}
364+
{-# INLINE decodeWithP' #-}
342365

343366
-- These alternative implementation of the 'csv' and 'csvWithHeader'
344367
-- parsers from the 'Parser' module performs the
@@ -351,26 +374,27 @@ decodeWithP p s =
351374
-- "parse error: conversion error: ...".
352375

353376
-- | Parse a CSV file that does not include a header.
354-
csv :: FromRecord a => DecodeOptions -> AL.Parser (V.Vector a)
355-
csv !opts = do
377+
csv :: (Record -> Conversion.Parser a) -> DecodeOptions
378+
-> AL.Parser (V.Vector a)
379+
csv _parseRecord !opts = do
356380
vals <- records
357381
return $! V.fromList vals
358382
where
359383
records = do
360384
!r <- record (decDelimiter opts)
361385
if blankLine r
362386
then (endOfInput *> pure []) <|> (endOfLine *> records)
363-
else case runParser (parseRecord r) of
387+
else case runParser (_parseRecord r) of
364388
Left msg -> fail $ "conversion error: " ++ msg
365389
Right val -> do
366390
!vals <- (endOfInput *> AP.pure []) <|> (endOfLine *> records)
367391
return (val : vals)
368392
{-# INLINE csv #-}
369393

370394
-- | Parse a CSV file that includes a header.
371-
csvWithHeader :: FromNamedRecord a => DecodeOptions
395+
csvWithHeader :: (NamedRecord -> Conversion.Parser a) -> DecodeOptions
372396
-> AL.Parser (Header, V.Vector a)
373-
csvWithHeader !opts = do
397+
csvWithHeader _parseNamedRecord !opts = do
374398
!hdr <- header (decDelimiter opts)
375399
vals <- records hdr
376400
let !v = V.fromList vals
@@ -386,4 +410,4 @@ csvWithHeader !opts = do
386410
!vals <- (endOfInput *> pure []) <|> (endOfLine *> records hdr)
387411
return (val : vals)
388412

389-
convert hdr = parseNamedRecord . Types.toNamedRecord hdr
413+
convert hdr = _parseNamedRecord . Types.toNamedRecord hdr

Data/Csv/Incremental.hs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
-- > main = withFile "salaries.csv" ReadMode $ \ csvFile -> do
1212
-- > let loop !_ (Fail _ errMsg) = putStrLn errMsg >> exitFailure
1313
-- > loop acc (Many rs k) = loop (acc + sumSalaries rs) =<< feed k
14+
1415
-- > loop acc (Done rs) = putStrLn $ "Total salaries: " ++
1516
-- > show (sumSalaries rs + acc)
1617
-- >
@@ -55,11 +56,13 @@ module Data.Csv.Incremental
5556
, HasHeader(..)
5657
, decode
5758
, decodeWith
59+
, decodeWithP
5860

5961
-- ** Name-based record conversion
6062
-- $namebased
6163
, decodeByName
6264
, decodeByNameWith
65+
, decodeByNameWithP
6366

6467
-- * Encoding
6568
-- ** Index-based record conversion
@@ -247,12 +250,21 @@ decodeWith :: FromRecord a
247250
-> HasHeader -- ^ Data contains header that should be
248251
-- skipped
249252
-> Parser a
250-
decodeWith !opts hasHeader = case hasHeader of
253+
decodeWith !opts hasHeader = decodeWithP parseRecord opts hasHeader
254+
255+
-- | Like 'decodeWith', but lets you pass an explicit parser value instead of
256+
-- using a typeclass
257+
decodeWithP :: (Record -> Conversion.Parser a)
258+
-> DecodeOptions -- ^ Decoding options
259+
-> HasHeader -- ^ Data contains header that should be
260+
-- skipped
261+
-> Parser a
262+
decodeWithP p !opts hasHeader = case hasHeader of
251263
HasHeader -> go (decodeHeaderWith opts)
252-
NoHeader -> Many [] $ \ s -> decodeWithP parseRecord opts s
264+
NoHeader -> Many [] $ \ s -> decodeWithP' p opts s
253265
where go (FailH rest msg) = Fail rest msg
254266
go (PartialH k) = Many [] $ \ s' -> go (k s')
255-
go (DoneH _ rest) = decodeWithP parseRecord opts rest
267+
go (DoneH _ rest) = decodeWithP' p opts rest
256268

257269
------------------------------------------------------------------------
258270

@@ -269,22 +281,29 @@ decodeByName = decodeByNameWith defaultDecodeOptions
269281
decodeByNameWith :: FromNamedRecord a
270282
=> DecodeOptions -- ^ Decoding options
271283
-> HeaderParser (Parser a)
272-
decodeByNameWith !opts = go (decodeHeaderWith opts)
284+
decodeByNameWith !opts = decodeByNameWithP parseNamedRecord opts
285+
286+
-- | Like 'decodeByNameWith', but lets you pass an explicit parser value instead
287+
-- of using a typeclass
288+
decodeByNameWithP :: (NamedRecord -> Conversion.Parser a)
289+
-> DecodeOptions -- ^ Decoding options
290+
-> HeaderParser (Parser a)
291+
decodeByNameWithP p !opts = go (decodeHeaderWith opts)
273292
where
274293
go (FailH rest msg) = FailH rest msg
275294
go (PartialH k) = PartialH $ \ s -> go (k s)
276295
go (DoneH hdr rest) =
277-
DoneH hdr (decodeWithP (parseNamedRecord . toNamedRecord hdr) opts rest)
296+
DoneH hdr (decodeWithP' (p . toNamedRecord hdr) opts rest)
278297

279298
------------------------------------------------------------------------
280299

281300
-- TODO: 'decodeWithP' should probably not take an initial
282301
-- 'B.ByteString' input.
283302

284303
-- | Like 'decode', but lets you customize how the CSV data is parsed.
285-
decodeWithP :: (Record -> Conversion.Parser a) -> DecodeOptions -> B.ByteString
304+
decodeWithP' :: (Record -> Conversion.Parser a) -> DecodeOptions -> B.ByteString
286305
-> Parser a
287-
decodeWithP p !opts = go Incomplete [] . parser
306+
decodeWithP' p !opts = go Incomplete [] . parser
288307
where
289308
go !_ !acc (A.Fail rest _ msg)
290309
| null acc = Fail rest err
@@ -294,7 +313,7 @@ decodeWithP p !opts = go Incomplete [] . parser
294313
where cont s = go m [] (k s)
295314
where m | B.null s = Complete
296315
| otherwise = Incomplete
297-
go Complete _ (A.Partial _) = moduleError "decodeWithP" msg
316+
go Complete _ (A.Partial _) = moduleError "decodeWithP'" msg
298317
where msg = "attoparsec should never return Partial in this case"
299318
go m acc (A.Done rest r)
300319
| B.null rest = case m of
@@ -309,7 +328,7 @@ decodeWithP p !opts = go Incomplete [] . parser
309328

310329
parser = A.parse (record (decDelimiter opts) <* (endOfLine <|> endOfInput))
311330
convert = runParser . p
312-
{-# INLINE decodeWithP #-}
331+
{-# INLINE decodeWithP' #-}
313332

314333
blankLine :: V.Vector B.ByteString -> Bool
315334
blankLine v = V.length v == 1 && (B.null (V.head v))

cassava.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ Library
8383
Data.Csv.Incremental
8484
Data.Csv.Parser
8585
Data.Csv.Streaming
86+
Data.Csv.Conversion
8687

8788
Other-modules:
88-
Data.Csv.Conversion
8989
Data.Csv.Conversion.Internal
9090
Data.Csv.Encoding
9191
Data.Csv.Types

0 commit comments

Comments
 (0)