1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
|
-- |
-- 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 (length, unsafeIndex)
import Basement.NormalForm (NormalForm(..))
import Basement.From (Into, into)
import Basement.String (any, elem, null, uncons)
import qualified Basement.String as String (singleton)
import Basement.Types.Word128 (Word128)
import Basement.Types.Word256 (Word256)
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
|