-- | -- Module : Foundation.Format.CSV.Types -- License : BSD-style -- Maintainer : Foundation -- Stability : experimental -- Portability : portable -- {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-} module Foundation.Format.CSV.Types (-- * CSV CSV , unCSV -- * Row , Row , unRow , Record(..) -- * Field , Field(..) , Escaping(..) , IsField(..) -- ** helpers , integral , float , string ) where import Basement.Imports import Basement.BoxedArray (Array, length, unsafeIndex) import Basement.NormalForm (NormalForm(..)) import Basement.From (Into, into) import Basement.String (String, any, elem, null, uncons) 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) import Foundation.Collection.Indexed (IndexedCollection) import Foundation.Check.Arbitrary (Arbitrary(..), frequency) import Foundation.String.Read (readDouble, readInteger) -- | 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 IsField a where toField :: a -> Field fromField :: Field -> Either String a instance IsField Field where toField = id fromField = pure instance IsField a => IsField (Maybe a) where toField Nothing = FieldString mempty NoEscape toField (Just a) = toField a fromField stuff@(FieldString p NoEscape) | null p = pure Nothing | otherwise = Just <$> fromField stuff fromField stuff = Just <$> fromField stuff fromIntegralField :: Integral b => Field -> Either String b fromIntegralField (FieldString str NoEscape) = case readInteger str of Nothing -> Left "Invalid integral field" Just v -> pure $ fromInteger v fromIntegralField (FieldInteger v) = pure (fromInteger v) fromIntegralField _ = Left "Expected integral value" fromDoubleField :: Field -> Either String Double fromDoubleField (FieldString str NoEscape) = case readDouble str of Nothing -> Left "Invalid double field" Just v -> pure v fromDoubleField (FieldDouble v) = pure v fromDoubleField _ = Left "Expected double value" instance IsField Bool where toField = toField . show fromField (FieldString "True" NoEscape) = pure True fromField (FieldString "False" NoEscape) = pure False fromField _ = Left "not a boolean value" instance IsField Int8 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Int16 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Int32 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Int64 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Int where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word8 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word16 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word32 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word64 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word128 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Word256 where toField = FieldInteger . into fromField = fromIntegralField instance IsField Integer where toField = FieldInteger fromField = fromIntegralField instance IsField Natural where toField = FieldInteger . into fromField = fromIntegralField instance IsField Double where toField = FieldDouble fromField = fromDoubleField instance IsField Char where toField = string . String.singleton fromField (FieldString str _) = case uncons str of Just (c, str') | null str' -> pure c | otherwise -> Left "Expected a char, but received a String" Nothing -> Left "Expected a char" fromField _ = Left "Expected a char" instance IsField (Offset a) where toField = FieldInteger . into fromField = fromIntegralField instance IsField (CountOf a) where toField = FieldInteger . into fromField = fromIntegralField instance IsField [Char] where toField = string . fromString fromField (FieldString str _) = pure $ toList str fromField _ = Left "Expected a Lazy String" instance IsField String where toField = string fromField (FieldString str _) = pure str fromField _ = Left "Expected a UTF8 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 g c = c == '\"' f 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 Record a where toRow :: a -> Row fromRow :: Row -> Either String a instance Record Row where toRow = id fromRow = pure instance (IsField a, IsField b) => Record (a,b) where toRow (a,b) = fromList [toField a, toField b] fromRow (Row row) | length row == 2 = (,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1) | otherwise = Left (show row) instance (IsField a, IsField b, IsField c) => Record (a,b,c) where toRow (a,b,c) = fromList [toField a, toField b, toField c] fromRow (Row row) | length row == 3 = (,,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1) <*> fromField (row `unsafeIndex` 2) | otherwise = Left (show row) instance (IsField a, IsField b, IsField c, IsField d) => Record (a,b,c,d) where toRow (a,b,c,d) = fromList [toField a, toField b, toField c, toField d] fromRow (Row row) | length row == 4 = (,,,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1) <*> fromField (row `unsafeIndex` 2) <*> fromField (row `unsafeIndex` 3) | otherwise = Left (show row) instance (IsField a, IsField b, IsField c, IsField d, IsField e) => Record (a,b,c,d,e) where toRow (a,b,c,d,e) = fromList [toField a, toField b, toField c, toField d, toField e] fromRow (Row row) | length row == 5 = (,,,,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1) <*> fromField (row `unsafeIndex` 2) <*> fromField (row `unsafeIndex` 3) <*> fromField (row `unsafeIndex` 4) | otherwise = Left (show row) instance (IsField a, IsField b, IsField c, IsField d, IsField e, IsField f) => Record (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] fromRow (Row row) | length row == 6 = (,,,,,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1) <*> fromField (row `unsafeIndex` 2) <*> fromField (row `unsafeIndex` 3) <*> fromField (row `unsafeIndex` 4) <*> fromField (row `unsafeIndex` 5) | otherwise = Left (show row) -- | 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