online-csv (empty) → 0.0.1
raw patch · 3 files changed
+250/−0 lines, 3 filesdep +attoparsecdep +basedep +box
Dependencies added: attoparsec, base, box, doctest, foldl, generic-lens, lens, numhask-prelude, scientific, text, text-format
Files
- online-csv.cabal +68/−0
- src/Data/Csv.hs +163/−0
- test/test.hs +19/−0
+ online-csv.cabal view
@@ -0,0 +1,68 @@+cabal-version: 2.4+name: online-csv+version: 0.0.1+synopsis: See readme.md+description: See readme.md for description.+category: project+author: Tony Day+maintainer: tonyday567@gmail.com+copyright: Tony Day (c) AfterTimes+license: BSD-3-Clause+homepage: https://github.com/tonyday567/online-csv#readme+bug-reports: https://github.com/tonyday567/online-csv/issues+build-type: Simple+source-repository head+ type: git+ location: https://github.com/tonyday567/online-csv++library+ hs-source-dirs:+ src+ default-extensions:+ NoImplicitPrelude+ NegativeLiterals+ OverloadedStrings+ UnicodeSyntax+ ghc-options:+ -Wall+ -Wcompat+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wredundant-constraints+ build-depends:+ base >=4.7 && <5+ , attoparsec+ , box+ , foldl+ , generic-lens+ , lens+ , text+ , scientific+ , text-format+ , numhask-prelude+ exposed-modules:+ Data.Csv+ other-modules:+ default-language: Haskell2010++test-suite test+ type: exitcode-stdio-1.0+ main-is: test.hs+ hs-source-dirs:+ test+ default-extensions:+ NoImplicitPrelude+ NegativeLiterals+ OverloadedStrings+ UnicodeSyntax+ ghc-options:+ -Wall+ -Wcompat+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wredundant-constraints+ build-depends:+ base >=4.7 && <5+ , doctest+ , numhask-prelude+ default-language: Haskell2010
+ src/Data/Csv.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE NoImplicitPrelude #-}++-- |+--+-- csv data type+module Data.Csv+ ( CsvConfig(..),+ defaultCsvConfig,+ file,+ Header(..),+ fileLines,+ rowEmitter,+ runCsv,+ sep,+ field,+ skipField,+ texts,+ scis,+ doubles,+ ints,+ ) where++import Box+import Control.Lens+import qualified Data.Attoparsec.Text as A+import Data.Generics.Labels ()+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import NumHask.Prelude+import Data.Scientific++data CsvConfig+ = CsvConfig+ { -- | data set name+ name :: Text,+ -- | file suffix+ suffix :: Text,+ -- | local directory+ dir :: Text,+ -- | separator+ csep :: Char,+ -- | first row is a header row+ header :: Header+ }+ deriving (Show, Generic, Eq)++-- | CsvConfig "Hill_Valley_with_noise" ".csv" "./other" '\t' HasHeader+defaultCsvConfig :: CsvConfig+defaultCsvConfig =+ CsvConfig+ "Hill_Valley_with_noise"+ ".csv"+ "./other"+ '\t'+ HasHeader++-- | filepath for teh config.+file :: CsvConfig -> FilePath+file cfg =+ cfg ^. #dir+ <> "/"+ <> cfg ^. #name+ <> cfg ^. #suffix+ & Text.unpack++-- | does the csv have a header row?+data Header = HasHeader | NoHeader deriving (Show, Eq)++-- | Emits a line of Text from a file.+fileLines :: Handle -> Emitter IO Text+fileLines h = Emitter $ do+ l :: (Either IOException Text) <- try (Text.hGetLine h)+ pure $ case l of+ Left _ -> Nothing+ Right a -> bool (Just a) Nothing (a == "")++-- | emits parsed csv rows+rowEmitter :: CsvConfig -> (Char -> A.Parser a) -> Cont IO (Emitter IO (Either Text a))+rowEmitter cfg p = Cont $ \eio -> withFile (file cfg) ReadMode (\h -> eio (eParse (p (cfg ^. #csep)) $ fileLines h))++-- | Run a parser across all lines of a file.+--+-- >>> xss <- rights <$> runCsv defaultCsvConfig doubles+--+-- >>> length xss+-- 1212+--+-- >>> all ((==101) . length) xss+-- True+--+-- >>> take 2 xss+-- [[39.02,36.49,38.2,38.85,39.38,39.74,37.02,39.53,38.81,38.79,37.65,39.34,38.55,39.03,37.21,36.32,37.81,38.95,36.7,39.72,37.06,37.29,36.43,36.53,36.19,38.17,37.3,36.15,36.68,36.7,36.68,36.99,38.92,37.25,37.47,36.32,35.75,35.68,34.66,34.26,35.62,36.6,34.78,34.67,34.3,33.4,31.4,31.75,31.75,32.84,33.76,35.74,34.01,33.91,36.88,34.41,35.52,36.94,36.95,35.57,38.02,37.32,39.05,37.97,37.01,38.98,38.83,38.87,38.03,38.4,38.25,38.61,36.23,37.81,37.98,38.58,38.96,38.97,39.08,38.79,38.79,36.31,36.59,38.19,37.95,39.63,39.27,37.19,37.13,37.47,37.57,36.62,36.92,38.8,38.52,38.07,36.73,39.46,37.5,39.1,0.0],[1.83,1.71,1.77,1.77,1.68,1.78,1.8,1.7,1.75,1.78,1.86,1.76,1.81,1.86,1.74,1.78,1.81,2.02,2.0,2.01,2.0,2.06,2.0,1.93,1.88,1.85,1.89,1.83,1.76,1.83,1.81,1.81,1.78,1.85,1.86,1.73,1.79,1.81,1.85,1.71,1.71,1.71,1.84,1.76,1.73,1.83,1.68,1.73,1.76,1.77,1.72,1.75,1.66,1.76,1.77,1.78,1.63,1.72,1.66,1.67,1.74,1.65,1.74,1.79,1.69,1.76,1.74,1.82,1.78,1.65,1.65,1.82,1.71,1.83,1.72,1.63,1.77,1.69,1.81,1.74,1.7,1.72,1.74,1.72,1.74,1.71,1.7,1.83,1.79,1.78,1.71,1.8,1.79,1.77,1.74,1.74,1.8,1.78,1.75,1.69,1.0]]+runCsv :: CsvConfig -> (Char -> A.Parser a) -> IO [Either Text a]+runCsv cfg p = with (rowEmitter cfg p) toList'++-- Convert an emitter to a list.+toList' :: Emitter IO a -> IO [a]+toList' e = go []+ where+ go xs = do+ r <- emit e+ case r of+ Nothing -> pure (reverse xs)+ Just x -> go (x : xs)++-- * low-level generic csv parser helpers++-- $setup+-- >>> :set -XOverloadedStrings++-- | Most parsing and building routines implicity assume comma separated, and newlines separating rows.+--+-- >>> A.parse (sep ',') ",ok"+-- Done "ok" ()+sep :: Char -> A.Parser ()+sep c = void (A.char c)++-- | an unquoted field+-- Does not consume the separator token+-- >>> A.parse (field ',') "field,ok"+-- Done ",ok" "field"+field :: Char -> A.Parser Text+field c = A.takeWhile (`notElem` [c])++-- | skipping a field+-- >>> A.parse (skipField ',') "field,ok"+-- Done ",ok" ()+skipField :: Char -> A.Parser ()+skipField c = A.skipWhile (`notElem` [c])+++-- * Block list parsers++-- | parser for a csv row of [Text]+-- >>> A.parseOnly (texts ',') "field1,field2"+-- Right ["field1","field2"]+texts :: Char -> A.Parser [Text]+texts c = field c `A.sepBy1` sep c++-- | parser for a csv row of [Scientific]+-- >>> A.parseOnly (scis ',') "1,2.2,3.3"+-- Right [1.0,2.2,3.3]+scis :: Char -> A.Parser [Scientific]+scis c = A.scientific `A.sepBy1` sep c++-- | parser for a csv row of [Double]+-- >>> A.parseOnly (doubles ',') "1,2,3"+-- Right [1.0,2.0,3.0]+doubles :: Char -> A.Parser [Double]+doubles c = A.double `A.sepBy1` sep c++-- | parser for a csv row of [Int]+-- >>> A.parseOnly (ints ',') "1,2,3"+-- Right [1,2,3]+ints :: Char -> A.Parser [Int]+ints c = (A.signed A.decimal) `A.sepBy1` sep c+
+ test/test.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -Wall #-}++module Main where++import NumHask.Prelude+import Test.DocTest++main :: IO ()+main =+ doctest+ [ "src/Data/Csv.hs"+ ]