csv-conduit 0.7.3.0 → 1.0.1.1
raw patch · 13 files changed
Files
- LICENSE +1/−0
- README.md +10/−8
- changelog.md +24/−0
- csv-conduit.cabal +67/−72
- src/Data/CSV/Conduit.hs +33/−28
- src/Data/CSV/Conduit/Conversion.hs +9/−11
- src/Data/CSV/Conduit/Parser/ByteString.hs +4/−4
- src/Data/CSV/Conduit/Parser/Text.hs +4/−2
- src/Data/CSV/Conduit/Types.hs +11/−3
- test/Test.hs +30/−7
- test/test-mac-excel.csv +1/−0
- test/test-windows-excel.csv +4/−0
- test/test.xls binary
LICENSE view
@@ -1,3 +1,4 @@+Copyright (c)2024, Daniel Vianna Copyright (c)2013, Ozgun Ataman Copyright (c)2012, Johan Tibell
README.md view
@@ -1,4 +1,7 @@-# README [](https://travis-ci.org/ozataman/csv-conduit)+# README+[](https://github.com/ozataman/csv-conduit/actions)+[](https://github.com/ozataman/csv-conduit/actions) ## CSV Files and Haskell @@ -50,7 +53,7 @@ - Dmitry Dzhus (@dzhus) - Niklas Hambüchen (@nh2) - Facundo Domínguez (@facundominguez)-+- Daniel Vianna (@dmvianna) ### Introduction @@ -90,9 +93,9 @@ myProcessor = CL.map reverse test :: IO ()-test = runResourceT $ - transformCSV defCSVSettings - (sourceFile "input.csv") +test = runResourceT $+ transformCSV defCSVSettings+ (sourceFile "input.csv") myProcessor (sinkFile "output.csv") ```@@ -113,11 +116,10 @@ -- Let's simply stream from a file, parse the CSV, reserialize it -- and push back into another file. test :: IO ()-test = runResourceT $ - sourceFile "test/BigFile.csv" $= +test = runResourceT $+ sourceFile "test/BigFile.csv" $= intoCSV defCSVSettings $= myProcessor $= fromCSV defCSVSettings $$ sinkFile "test/BigFileOut.csv" ```-
changelog.md view
@@ -1,3 +1,27 @@+1.0.1.1+* Fix test suite to build with text 2.1.2 and ghc 9.12.2, resolving [#62](https://github.com/ozataman/csv-conduit/issues/62)++1.0.1.0+* Use ConduitT instead of ConduitM (prettier type inference with newer conduit imports)++1.0.0.2+* Fixed [#17](https://github.com/ozataman/csv-conduit/issues/17),+ where CSV created with Excel in Mac OS failed to parse due to its+ newline characters.++1.0.0.1+* Removed dependencies: mmorph, monad-control, mtl,+ unordered-containers, primitive++1.0.0.0+* Removed `return` from the `Monad` instance for `Parser`, and+ transfered its definition to `pure` in the `Applicative` instance of+ `Parser`. This was necessary to support GHC 9.6.4.+* Create new API to choose whether to handle empty CSV cells as empty+ strings or NULLs.+* Added imports that were removed from `Prelude` in GHC 9.6.4.+* Bumped the default Stack resolver to LTS-22.20.+ 0.7.3.0 * Add ordered versions of named records for consistent, controllable header column ordering. [PR 44](https://github.com/ozataman/csv-conduit/pull/44) * Add support for GHC 9.0.1
csv-conduit.cabal view
@@ -1,27 +1,34 @@-Name: csv-conduit-Version: 0.7.3.0-Synopsis: A flexible, fast, conduit-based CSV parser library for Haskell.-Homepage: http://github.com/ozataman/csv-conduit-License: BSD3-License-file: LICENSE-Author: Ozgun Ataman-Maintainer: Ozgun Ataman <ozataman@gmail.com>-Category: Data, Conduit, CSV, Text-Build-type: Simple-Cabal-version: >= 1.10-Tested-with: GHC == 9.0.1, GHC == 8.10.4, GHC == 8.8.4, GHC == 8.8.3, GHC == 8.6.5, GHC == 8.4.4, GHC == 8.2.2-Description:+name: csv-conduit+version: 1.0.1.1+synopsis:+ A flexible, fast, conduit-based CSV parser library for Haskell.++homepage: http://github.com/ozataman/csv-conduit+license: BSD3+license-file: LICENSE+author: Ozgun Ataman+maintainer: Daniel Vianna <dmvianna@gmail.com>+category: Data, Conduit, CSV, Text+build-type: Simple+cabal-version: >=1.10+tested-with:+ GHC ==8.2.2+ || ==8.4.4+ || ==8.6.5+ || ==8.8.3+ || ==8.8.4+ || ==8.10.4+ || ==9.0.1+ || ==9.6.4++description: CSV files are the de-facto standard in many situations involving data transfer, particularly when dealing with enterprise application or disparate database systems.- .- While there are a number of CSV libraries in Haskell, at the time of this project's start in 2010, there wasn't one that provided all of the following:- .- * Full flexibility in quote characters, separators, input/output . * Constant space operation@@ -32,106 +39,94 @@ . * Fast operation .- This library is an attempt to close these gaps. Please note that this library started its life based on the enumerator package and has recently been ported to work with conduits instead. In the process, it has been greatly simplified thanks to the modular nature of the conduits library.- .- Following the port to conduits, the library has also gained the ability to parameterize on the stream type and work both with ByteString and Text.- .- For more documentation and examples, check out the README at: .- <http://github.com/ozataman/csv-conduit> . - extra-source-files:- README.md changelog.md+ README.md+ test/test-mac-excel.csv+ test/test-windows-excel.csv test/test.csv test/Test.hs+ test/test.xls flag lib-Werror default: False- manual: True+ manual: True library default-language: Haskell2010 exposed-modules:- Data.CSV.Conduit- Data.CSV.Conduit.Types- Data.CSV.Conduit.Conversion- Data.CSV.Conduit.Parser.ByteString- Data.CSV.Conduit.Parser.Text+ Data.CSV.Conduit+ Data.CSV.Conduit.Conversion+ Data.CSV.Conduit.Parser.ByteString+ Data.CSV.Conduit.Parser.Text+ Data.CSV.Conduit.Types+ other-modules:- Data.CSV.Conduit.Conversion.Internal- Data.CSV.Conduit.Monoid- ghc-options: -Wall -funbox-strict-fields- if flag(lib-Werror)+ Data.CSV.Conduit.Conversion.Internal+ Data.CSV.Conduit.Monoid++ ghc-options: -Wall -funbox-strict-fields++ if flag(lib-werror) ghc-options: -Werror- hs-source-dirs: src++ hs-source-dirs: src build-depends:- attoparsec >= 0.10- , base >= 4 && < 5+ array+ , attoparsec >=0.10+ , base >=4 && <5+ , blaze-builder , bytestring- , conduit >= 1.2.8+ , conduit >=1.3.0 && <2.0 , conduit-extra- , containers >= 0.3- , exceptions >= 0.3- , monad-control- , text+ , containers >=0.3 , data-default- , vector- , array- , blaze-builder- , unordered-containers+ , exceptions >=0.3 , ordered-containers- , transformers- , mtl- , mmorph , primitive- , resourcet >= 1.1.2.1- , semigroups-- if impl(ghc >= 7.2.1)- cpp-options: -DGENERICS- build-depends: ghc-prim >= 0.2-+ , resourcet >=1.1.2.1+ , text+ , transformers+ , vector test-suite test default-language: Haskell2010- type: exitcode-stdio-1.0- main-is: Test.hs- ghc-options: -Wall- if flag(lib-Werror)+ type: exitcode-stdio-1.0+ main-is: Test.hs+ ghc-options: -Wall -Wunused-packages++ if flag(lib-werror) ghc-options: -Werror- hs-source-dirs: test++ hs-source-dirs: test build-depends:- base >= 4 && < 5+ base >=4 && <5 , bytestring- , conduit >= 1.3.0- , containers >= 0.3+ , conduit >=1.3.0+ , containers >=0.3 , csv-conduit , directory- , vector- , HUnit >= 1.2+ , HUnit >=1.2+ , ordered-containers , test-framework , test-framework-hunit , text- , ordered-containers- , transformers- , mtl- , primitive-+ , vector source-repository head type: git
src/Data/CSV/Conduit.hs view
@@ -6,7 +6,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UndecidableInstances #-} module Data.CSV.Conduit@@ -36,11 +35,12 @@ ------------------------------------------------------------------------------- import Control.Exception-import Control.Monad.Catch.Pure (CatchT)-import Control.Monad.Catch.Pure (runCatchT)-import Control.Monad.Except+import Control.Monad.Catch.Pure (runCatchT, CatchT)+import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Primitive import Control.Monad.ST+import Control.Monad.Trans.Class (MonadTrans(lift))+import Control.Monad.Trans.Except (ExceptT(..), runExceptT) import Control.Monad.Trans.Resource (MonadResource, MonadThrow, runResourceT) import Data.Attoparsec.Types (Parser)@@ -135,13 +135,13 @@ ----------------------------------------------------------------------------- -- | Turn a stream of 's' into a stream of CSV row type. An example -- would be parsing a ByteString stream as rows of 'MapRow' 'Text'.- intoCSV :: (MonadThrow m) => CSVSettings -> ConduitM s r m ()+ intoCSV :: (MonadThrow m) => CSVSettings -> ConduitT s r m () ----------------------------------------------------------------------------- -- | Turn a stream of CSV row type back into a stream of 's'. An -- example would be rendering a stream of 'Row' 'ByteString' rows as -- 'Text'.- fromCSV :: Monad m => CSVSettings -> ConduitM r s m ()+ fromCSV :: Monad m => CSVSettings -> ConduitT r s m () @@ -153,9 +153,12 @@ rowToStr s !r = let sep = B.pack [c2w (csvSep s)]- wrapField !f = case csvQuoteChar s of- Just !x-> (x `B8.cons` escape x f) `B8.snoc` x- _ -> f+ wrapField !f = case csvQuoteCharAndStyle s of+ Just (x, quoteEmpty) ->+ case quoteEmpty == DoQuoteEmpty || B8.length f /= 0 of+ True -> (x `B8.cons` escape x f) `B8.snoc` x+ False -> f+ Nothing -> f escape c str = B8.intercalate (B8.pack [c,c]) $ B8.split c str in B.intercalate sep . map wrapField $ r @@ -169,9 +172,11 @@ rowToStr s !r = let sep = T.pack [csvSep s]- wrapField !f = case csvQuoteChar s of- Just !x-> x `T.cons` escape x f `T.snoc` x- _ -> f+ wrapField !f = case csvQuoteCharAndStyle s of+ Just (x, quoteEmpty) -> case quoteEmpty == DoQuoteEmpty || not (T.null f) of+ True -> x `T.cons` escape x f `T.snoc` x+ False -> f+ Nothing -> f escape c str = T.intercalate (T.pack [c,c]) $ T.split (== c) str in T.intercalate sep . map wrapField $ r @@ -208,13 +213,13 @@ ------------------------------------------------------------------------------- fromCSVRow :: (Monad m, IsString s, CSV s r)- => CSVSettings -> ConduitM r s m ()+ => CSVSettings -> ConduitT r s m () fromCSVRow set = awaitForever $ \row -> mapM_ yield [rowToStr set row, "\n"] --------------------------------------------------------------------------------intoCSVRow :: (MonadThrow m, AttoparsecInput i) => Parser i (Maybe o) -> ConduitM i o m ()+intoCSVRow :: (MonadThrow m, AttoparsecInput i) => Parser i (Maybe o) -> ConduitT i o m () intoCSVRow p = parse .| puller where parse = {-# SCC "conduitParser_p" #-} conduitParser p@@ -238,7 +243,7 @@ ------------------------------------------------------------------------------- intoCSVMap :: (Ord a, MonadThrow m, CSV s [a])- => CSVSettings -> ConduitM s (MapRow a) m ()+ => CSVSettings -> ConduitT s (MapRow a) m () intoCSVMap set = intoCSV set .| (headers >>= converter) where headers = do@@ -293,7 +298,7 @@ ------------------------------------------------------------------------------- fromCSVMap :: (Monad m, IsString s, CSV s [a])- => CSVSettings -> ConduitM (M.Map k a) s m ()+ => CSVSettings -> ConduitT (M.Map k a) s m () fromCSVMap set = awaitForever push where push r = mapM_ yield [rowToStr set (M.elems r), "\n"]@@ -316,7 +321,7 @@ writeHeaders :: (Monad m, CSV s (Row r), IsString s) => CSVSettings- -> ConduitM (MapRow r) s m ()+ -> ConduitT (MapRow r) s m () writeHeaders set = do mrow <- await case mrow of@@ -375,14 +380,14 @@ -> Either SomeException (v a) decodeCSV set bs = runST $ runExceptT pipeline where- src :: ConduitM () s (ExceptT SomeException (ST s1)) ()+ src :: ConduitT () s (ExceptT SomeException (ST s1)) () src = C.sourceList [bs]- csvConvert :: ConduitM s a (ExceptT SomeException (ST s1)) ()+ csvConvert :: ConduitT s a (ExceptT SomeException (ST s1)) () csvConvert = transPipe (ExceptT . runCatchT) csvConvert'- csvConvert' :: ConduitM s a (CatchT (ST s1)) ()+ csvConvert' :: ConduitT s a (CatchT (ST s1)) () csvConvert' = intoCSV set growthFactor = 10- sink :: ConduitM a Void.Void (ExceptT SomeException (ST s1)) (v a)+ sink :: ConduitT a Void.Void (ExceptT SomeException (ST s1)) (v a) sink = sinkVector growthFactor pipeline :: ExceptT SomeException (ST s1) (v a) pipeline = runConduit (src .| csvConvert .| sink)@@ -442,11 +447,11 @@ :: (MonadThrow m, CSV s a, CSV s' b) => CSVSettings -- ^ Settings to be used for both input and output- -> ConduitM () s m ()+ -> ConduitT () s m () -- ^ A raw stream data source. Ex: 'sourceFile inFile'- -> ConduitM a b m ()+ -> ConduitT a b m () -- ^ A transforming conduit- -> ConduitM s' Void.Void m ()+ -> ConduitT s' Void.Void m () -- ^ A raw stream data sink. Ex: 'sinkFile outFile' -> m () transformCSV set = transformCSV' set set@@ -470,11 +475,11 @@ -- ^ Settings to be used for input -> CSVSettings -- ^ Settings to be used for output- -> ConduitM () s m ()+ -> ConduitT () s m () -- ^ A raw stream data source. Ex: 'sourceFile inFile'- -> ConduitM a b m ()+ -> ConduitT a b m () -- ^ A transforming conduit- -> ConduitM s' Void.Void m ()+ -> ConduitT s' Void.Void m () -- ^ A raw stream data sink. Ex: 'sinkFile outFile' -> m () transformCSV' setIn setOut source c sink = runConduit $@@ -495,7 +500,7 @@ ------------------------------------------------------------------------------- -- | An efficient sink that incrementally grows a vector from the input stream-sinkVector :: (PrimMonad m, GV.Vector v a) => Int -> ConduitM a o m (v a)+sinkVector :: (PrimMonad m, GV.Vector v a) => Int -> ConduitT a o m (v a) sinkVector by = do v <- lift $ GMV.new by go 0 v
src/Data/CSV/Conduit/Conversion.hs view
@@ -68,7 +68,6 @@ import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as L import Data.Int (Int8, Int16, Int32, Int64)-import Data.Kind (Type) import qualified Data.Map as M import qualified Data.Map.Ordered as MO import Data.Semigroup as Semigroup@@ -160,7 +159,7 @@ -- > | otherwise = mzero class FromRecord a where parseRecord :: Record -> Parser a- + #ifdef GENERICS default parseRecord :: (Generic a, GFromRecord (Rep a)) => Record -> Parser a parseRecord r = to A.<$> gparseRecord r@@ -778,8 +777,6 @@ m >>= g = Parser $ \kf ks -> let ks' a = unParser (g a) kf ks in unParser m kf ks' {-# INLINE (>>=) #-}- return a = Parser $ \_kf ks -> ks a- {-# INLINE return #-} #if MIN_VERSION_base(4,13,0) instance MonadFail Parser where@@ -794,10 +791,11 @@ {-# INLINE fmap #-} instance Applicative Parser where- pure = return- {-# INLINE pure #-}+ (<*>) = apP {-# INLINE (<*>) #-}+ pure a = Parser $ \_kf ks -> ks a+ {-# INLINE pure #-} instance Alternative Parser where empty = fail "empty"@@ -842,9 +840,9 @@ gparseRecord :: Record -> Parser (f p) instance GFromRecordSum f Record => GFromRecord (M1 i n f) where- gparseRecord v = + gparseRecord v = case (IM.lookup n gparseRecordSum) of- Nothing -> lengthMismatch n v + Nothing -> lengthMismatch n v Just p -> M1 <$> p v where n = V.length v@@ -853,15 +851,15 @@ gparseNamedRecord :: NamedRecord -> Parser (f p) instance GFromRecordSum f NamedRecord => GFromNamedRecord (M1 i n f) where- gparseNamedRecord v = + gparseNamedRecord v = foldr (\f p -> p <|> M1 <$> f v) empty (IM.elems gparseRecordSum) class GFromRecordSum f r where gparseRecordSum :: IM.IntMap (r -> Parser (f p)) instance (GFromRecordSum a r, GFromRecordSum b r) => GFromRecordSum (a :+: b) r where- gparseRecordSum = - IM.unionWith (\a b r -> a r <|> b r) + gparseRecordSum =+ IM.unionWith (\a b r -> a r <|> b r) (fmap (L1 <$>) <$> gparseRecordSum) (fmap (R1 <$>) <$> gparseRecordSum)
src/Data/CSV/Conduit/Parser/ByteString.hs view
@@ -58,15 +58,17 @@ row :: CSVSettings -> Parser (Maybe (Row ByteString)) row csvs = csvrow csvs <|> badrow +csvEndOfLine :: Parser ()+csvEndOfLine = (word8 10 >> return ()) <|> ((word8 13 *> word8 10) >> return ()) <|> (word8 13 >> return ()) badrow :: Parser (Maybe (Row ByteString)) badrow = P.takeWhile (not . C8.isEndOfLine) *>- (C8.endOfLine <|> C8.endOfInput) *> return Nothing+ (csvEndOfLine <|> C8.endOfInput) *> return Nothing csvrow :: CSVSettings -> Parser (Maybe (Row ByteString)) csvrow c = let rowbody = (quotedField' <|> field c) `sepBy` C8.char (csvSep c)- properrow = rowbody <* (C8.endOfLine <|> P.endOfInput)+ properrow = rowbody <* (csvEndOfLine <|> P.endOfInput) quotedField' = case csvQuoteChar c of Nothing -> mzero Just q' -> try (quotedField q')@@ -93,5 +95,3 @@ f <- many (C8.notChar c <|> quoted) _ <- C8.char c return $ B8.pack f--
src/Data/CSV/Conduit/Parser/Text.hs view
@@ -58,15 +58,17 @@ row :: CSVSettings -> Parser (Maybe (Row Text)) row csvs = csvrow csvs <|> badrow +csvEndOfLine :: Parser ()+csvEndOfLine = (char '\n' >> return ()) <|> (string (T.pack "\r\n") >> return ()) <|> (char '\r' >> return ()) badrow :: Parser (Maybe (Row Text)) badrow = P.takeWhile (not . T.isEndOfLine) *>- (T.endOfLine <|> T.endOfInput) *> return Nothing+ (csvEndOfLine <|> T.endOfInput) *> return Nothing csvrow :: CSVSettings -> Parser (Maybe (Row Text)) csvrow c = let rowbody = (quotedField' <|> field c) `sepBy` T.char (csvSep c)- properrow = rowbody <* (T.endOfLine <|> P.endOfInput)+ properrow = rowbody <* (csvEndOfLine <|> P.endOfInput) quotedField' = case csvQuoteChar c of Nothing -> mzero Just q' -> try (quotedField q')
src/Data/CSV/Conduit/Types.hs view
@@ -9,6 +9,7 @@ import qualified Data.Map.Ordered as MO ------------------------------------------------------------------------------- +data QuoteEmpty = DoQuoteEmpty | DontQuoteEmpty deriving (Show, Eq) ------------------------------------------------------------------------------- -- | Settings for a CSV file. This library is intended to be flexible@@ -22,10 +23,12 @@ -- | Quote character that may sometimes be present around fields. -- If 'Nothing' is given, the library will never expect quotation -- even if it is present.- , csvQuoteChar :: !(Maybe Char)- } deriving (Read, Show, Eq)+ , csvQuoteCharAndStyle :: !(Maybe (Char, QuoteEmpty))+ } deriving (Show, Eq) +csvQuoteChar :: CSVSettings -> Maybe Char+csvQuoteChar = (fst <$>) . csvQuoteCharAndStyle ------------------------------------------------------------------------------- -- | Default settings for a CSV file.@@ -36,9 +39,14 @@ defCSVSettings :: CSVSettings defCSVSettings = CSVSettings { csvSep = ','- , csvQuoteChar = Just '"'+ , csvQuoteCharAndStyle = Just ('"', DoQuoteEmpty) } +defDontQuoteEmptyCSVSettings :: CSVSettings+defDontQuoteEmptyCSVSettings = CSVSettings+ { csvSep = ','+ , csvQuoteCharAndStyle = Just ('"', DontQuoteEmpty)+ } instance Default CSVSettings where def = defCSVSettings
test/Test.hs view
@@ -9,10 +9,11 @@ import qualified Data.ByteString.Char8 as B import Data.CSV.Conduit import Data.CSV.Conduit.Conversion+import Data.CSV.Conduit.Types import qualified Data.Map as Map import qualified Data.Map.Ordered as OMap import Data.Monoid as M-import Data.Text+import Data.Text (Text) import qualified Data.Vector as V import System.Directory import Test.Framework (Test, defaultMain, testGroup)@@ -31,7 +32,10 @@ baseTests :: [Test] baseTests = [ testCase "mapping with id works" test_identityMap,- testCase "simple parsing works" test_simpleParse,+ testCase "simple parsing works" (test_simpleParse testFile1),+ testCase "simple parsing works for Mac-Excel" (test_simpleParse testFile3),+ testCase "simple parsing works for Windows-Excel" (test_simpleParse testFile4),+ testCase "fails parsing gracefully" test_parseFail, testCase "OrderedMap" test_orderedMap ] @@ -73,9 +77,9 @@ f :: Row Text -> [Row Text] f = return -test_simpleParse :: IO ()-test_simpleParse = do- (d :: V.Vector (MapRow B.ByteString)) <- readCSVFile csvSettings testFile1+test_simpleParse :: FilePath -> IO ()+test_simpleParse fp = do+ (d :: V.Vector (MapRow B.ByteString)) <- readCSVFile csvSettings fp V.mapM_ assertRow d where assertRow r = v3 @=? (v1 + v2)@@ -84,6 +88,20 @@ v2 = readBS $ r Map.! "Col3" v3 = readBS $ r Map.! "Sum" +test_parseFail :: IO ()+test_parseFail = do+ (d :: V.Vector (MapRow B.ByteString)) <- readCSVFile csvSettings testXLS+ errored <- catch (V.mapM_ assertRow d >> pure False) handler+ if errored then pure () else assertFailure "readCSVFile shouldn't read XLS"+ where+ handler :: ErrorCall -> IO Bool+ handler _ = pure True+ assertRow r = v3 @=? (v1 + v2)+ where+ v1 = readBS $ r Map.! "Col2"+ v2 = readBS $ r Map.! "Col3"+ v3 = readBS $ r Map.! "Sum"+ test_orderedMap :: IO () test_orderedMap = do unorderedRes <-@@ -106,11 +124,16 @@ pairs = [("b", "bval"), ("a", "aval")] csvSettings :: CSVSettings-csvSettings = defCSVSettings {csvQuoteChar = Just '`'}+csvSettings = defCSVSettings {csvQuoteCharAndStyle = Just ('`', DontQuoteEmpty)} -testFile1, testFile2 :: FilePath+testFile1, testFile2, testFile3, testFile4 :: FilePath testFile1 = "test/test.csv" testFile2 = "test/test.csv"+testFile3 = "test/test-mac-excel.csv"+testFile4 = "test/test-windows-excel.csv"++testXLS :: FilePath+testXLS = "test/test.xls" readBS :: B.ByteString -> Int readBS = read . B.unpack
+ test/test-mac-excel.csv view
@@ -0,0 +1,1 @@+`Col1`,`Col2`,`Col3`,`Sum` `A`,`2`,`3`,`5` `B`,`3`,`4`,`7` `Field using the quote char ``this is the in-quoted value```,`4`,`5`,`9`
+ test/test-windows-excel.csv view
@@ -0,0 +1,4 @@+Col1,Col2,Col3,Sum +A,2,3,5 +B,3,4,7 +"Field using the quote char ""this is the in-quoted value""",4,5,9
+ test/test.xls view
binary file changed (absent → 6144 bytes)