foundation-0.0.20: Foundation/Format/CSV.hs
-- |
-- Module : Foundation.Format.CSV
-- License : BSD-style
-- Maintainer : Foundation
-- Stability : experimental
-- Portability : portable
--
-- Provies the support for Comma Separated Value
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
module Foundation.Format.CSV
(-- * CSV
CSV
-- ** Builder
-- ** String Bulider
, csvStringBuilder
, rowStringBuilder
, fieldStringBuilder
-- ** Block Builder
, csvBlockBuilder
, rowBlockBuilder
, fieldBlockBuilder
-- ** Conduit
, rowC
-- * Row
, Row
, ToRow(..)
-- * Field
, Field(..)
, Escaping(..)
, ToField(..)
-- ** helpers
, integral
, float
, string
) where
import Basement.Imports -- hiding (throw)
import Basement.BoxedArray (Array)
import Basement.NormalForm (NormalForm(..))
import Basement.From (Into, into)
import Basement.String (String, replace, any, elem)
import qualified Basement.String as String (singleton)
import Basement.Types.Word128 (Word128)
import Basement.Types.Word256 (Word256)
import Basement.Types.OffsetSize (Offset, CountOf)
import Foundation.Collection.Element (Element)
import Foundation.Collection.Collection (Collection, nonEmpty_)
import Foundation.Collection.Sequential (Sequential(intersperse))
import Foundation.Collection.Indexed (IndexedCollection)
import Foundation.Check.Arbitrary (Arbitrary(..), frequency)
import Foundation.Conduit.Internal
import qualified Foundation.String.Builder as String
import Basement.Block (Block)
import qualified Basement.Block.Builder as Block
import GHC.ST (runST)
-- | CSV field
data Field
= FieldInteger Integer
| FieldDouble Double
| FieldString String Escaping
deriving (Eq, Show, Typeable)
instance NormalForm Field where
toNormalForm (FieldInteger i) = toNormalForm i
toNormalForm (FieldDouble d) = toNormalForm d
toNormalForm (FieldString s e) = toNormalForm s `seq` toNormalForm e
instance Arbitrary Field where
arbitrary = frequency $ nonEmpty_ [ (1, FieldInteger <$> arbitrary)
, (1, FieldDouble <$> arbitrary)
, (3, string <$> arbitrary)
]
data Escaping = NoEscape | Escape | DoubleEscape
deriving (Eq, Ord, Enum, Bounded, Show, Typeable)
instance NormalForm Escaping where
toNormalForm !_ = ()
class ToField a where
toField :: a -> Field
instance ToField Field where
toField = id
instance ToField a => ToField (Maybe a) where
toField Nothing = FieldString mempty NoEscape
toField (Just a) = toField a
instance ToField Int8 where
toField = FieldInteger . into
instance ToField Int16 where
toField = FieldInteger . into
instance ToField Int32 where
toField = FieldInteger . into
instance ToField Int64 where
toField = FieldInteger . into
instance ToField Int where
toField = FieldInteger . into
instance ToField Word8 where
toField = FieldInteger . into
instance ToField Word16 where
toField = FieldInteger . into
instance ToField Word32 where
toField = FieldInteger . into
instance ToField Word64 where
toField = FieldInteger . into
instance ToField Word where
toField = FieldInteger . into
instance ToField Word128 where
toField = FieldInteger . into
instance ToField Word256 where
toField = FieldInteger . into
instance ToField Integer where
toField = FieldInteger
instance ToField Natural where
toField = FieldInteger . into
instance ToField Double where
toField = FieldDouble
instance ToField Char where
toField = string . String.singleton
instance ToField (Offset a) where
toField = FieldInteger . into
instance ToField (CountOf a) where
toField = FieldInteger . into
instance ToField [Char] where
toField = string . fromString
instance ToField String where
toField = string
-- | helper function to create a `FieldInteger`
--
integral :: Into Integer a => a -> Field
integral = FieldInteger . into
float :: Double -> Field
float = FieldDouble
-- | heler function to create a FieldString.
--
-- This function will findout automatically if an escaping is needed.
-- if you wish to perform the escaping manually, do not used this function
--
string :: String -> Field
string s = FieldString s encoding
where
encoding
| any g s = DoubleEscape
| any f s = Escape
| otherwise = NoEscape
f c = c == '\"'
g c = c `elem` ",\r\n"
-- | CSV Row
--
newtype Row = Row { unRow :: Array Field }
deriving (Eq, Show, Typeable, Semigroup, Monoid, Collection, NormalForm, Sequential, IndexedCollection)
type instance Element Row = Field
instance IsList Row where
type Item Row = Field
toList = toList . unRow
fromList = Row . fromList
class ToRow a where
toRow :: a -> Row
instance ToRow Row where
toRow = id
instance (ToField a, ToField b) => ToRow (a,b) where
toRow (a,b) = fromList [toField a, toField b]
instance (ToField a, ToField b, ToField c) => ToRow (a,b,c) where
toRow (a,b,c) = fromList [toField a, toField b, toField c]
instance (ToField a, ToField b, ToField c, ToField d) => ToRow (a,b,c,d) where
toRow (a,b,c,d) = fromList [toField a, toField b, toField c, toField d]
instance (ToField a, ToField b, ToField c, ToField d, ToField e) => ToRow (a,b,c,d,e) where
toRow (a,b,c,d,e) = fromList [toField a, toField b, toField c, toField d, toField e]
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f) => ToRow (a,b,c,d,e,f) where
toRow (a,b,c,d,e,f) = fromList [toField a, toField b, toField c, toField d, toField e, toField f]
-- | CSV Type
newtype CSV = CSV { unCSV :: Array Row }
deriving (Eq, Show, Typeable, Semigroup, Monoid, Collection, NormalForm, Sequential, IndexedCollection)
type instance Element CSV = Row
instance IsList CSV where
type Item CSV = Row
toList = toList . unCSV
fromList = CSV . fromList
-- | serialise the CSV document into a UTF8 string
csvStringBuilder :: CSV -> String.Builder
csvStringBuilder = String.unsafeStringBuilder . csvBlockBuilder
rowStringBuilder :: Row -> String.Builder
rowStringBuilder = String.unsafeStringBuilder . rowBlockBuilder
fieldStringBuilder :: Field -> String.Builder
fieldStringBuilder = String.unsafeStringBuilder . fieldBlockBuilder
-- | serialise the CSV document into a UTF8 encoded (Block Word8)
csvBlockBuilder :: CSV -> Block.Builder
csvBlockBuilder = mconcat . intersperse (Block.emitString "\r\n") . fmap rowBlockBuilder . toList . unCSV
rowBlockBuilder :: Row -> Block.Builder
rowBlockBuilder = mconcat . intersperse (Block.emitUTF8Char ',') . fmap fieldBlockBuilder . toList . unRow
fieldBlockBuilder :: Field -> Block.Builder
fieldBlockBuilder (FieldInteger i) = Block.emitString $ show i
fieldBlockBuilder (FieldDouble d) = Block.emitString $ show d
fieldBlockBuilder (FieldString s e) = case e of
NoEscape -> Block.emitString s
Escape -> Block.emitUTF8Char '"' <> Block.emitString s <> Block.emitUTF8Char '"'
DoubleEscape -> Block.emitUTF8Char '"' <> Block.emitString (replace "\"" "\"\"" s) <> Block.emitUTF8Char '"'
rowC :: (ToRow row, Monad m) => Conduit row (Block Word8) m ()
rowC = await >>= go
where
go Nothing = pure ()
go (Just r) =
let bytes = runST (Block.run $ rowBlockBuilder (toRow r) <> Block.emitString "\r\n")
in yield bytes >> await >>= go