columnar (empty) → 1.0.0.0
raw patch · 7 files changed
+839/−0 lines, 7 filesdep +basedep +bytestringdep +cassavasetup-changed
Dependencies added: base, bytestring, cassava, containers, enum-text, fmt, lens, possibly, rio, template-haskell, text, time, unordered-containers, vector
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +3/−0
- Setup.hs +2/−0
- columnar.cabal +54/−0
- src/Text/Columnar.hs +7/−0
- src/Text/Columnar/Types.hs +740/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for columnar++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Chris Dornan (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Chris Dornan nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,3 @@+# columnar++A CSV toolkit based on cassava and enum-text.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ columnar.cabal view
@@ -0,0 +1,54 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: 3e2e13a95a2ac54cbe9cf4471f15aff7a91105973b3bd7b3e4f4c4d0fa3ac13a++name: columnar+version: 1.0.0.0+synopsis: A CSV toolkit based on cassava and enum-text+description: Please see the README on GitHub at <https://github.com/cdornan/columnar#readme>+category: Text, Web, CSV+homepage: https://github.com/cdornan/columnar#readme+bug-reports: https://github.com/cdornan/columnar/issues+author: Chris Dornan+maintainer: chris@chrisdornan.com+copyright: 2019 Chris Dornan+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/cdornan/columnar++library+ exposed-modules:+ Text.Columnar+ other-modules:+ Text.Columnar.Types+ Paths_columnar+ hs-source-dirs:+ src+ ghc-options: -Wall -Wcompat+ build-depends:+ base >=4.10 && <10+ , bytestring+ , cassava+ , containers+ , enum-text+ , fmt >=0.4.0.0+ , lens+ , possibly+ , rio >=0.1.1.0+ , template-haskell+ , text+ , time+ , unordered-containers+ , vector+ default-language: Haskell2010
+ src/Text/Columnar.hs view
@@ -0,0 +1,7 @@+{-# LANGUAGE TemplateHaskell #-}++module Text.Columnar+ ( module X+ ) where++import Text.Columnar.Types as X
+ src/Text/Columnar/Types.hs view
@@ -0,0 +1,740 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Text.Columnar.Types+ ( Columnar(..)+ , ColumnOptions(..)+ , IsNamed(..)+ , WithColHeader(..)+ , RowNo(..)+ , headerRowNo+ , firstRowNo+ , rowNoSupply+ , defaultPositionalColumnOptions+ , defaultColumnOptions+ -- IsRecord+ , IsRecord(..)+ , IsMonoidalRecord(..)+ , FieldMethods(..)+ , MonoidalFieldMethods(..)+ , Records(..)+ -- isRecordIntMap and friends+ , IsRecordIntMap(..)+ , encodeRecordMap+ , decodeRecordMap+ , memptyRecordMap+ , mappendRecordMap+ , summarizeMap+ -- IsRecordHashMap and friends+ , IsRecordHashMap(..)+ , encodeRecordHashMap+ , decodeRecordHashMap+ , memptyRecordHashMap+ , mappendRecordHashMap+ , summarizeHashMap+ -- Rows, Row+ , Row(..)+ , Rows(..)+ , mkRows+ , mkRowsWith+ , getRows+ , mkRow+ -- ColumnarIx+ , ColumnarIx(..)+ , listColumns+ -- construction toolkit: mkFM, mkMFM, etc+ , mkFieldMethods+ , mkMonoidalFieldMethods+ , convertMonoidalFieldMethodsToFieldMethods+ , constructRecordMappend+ -- encoding with Haskell+ , haskellRecords+ -- mkRecords, mkRecordsWith+ , mkRecords+ , mkRecordsWith+ -- en/decoding Records+ , buildRecord+ , encodeRecordsT+ , encodeRecords+ , decodeRecordsT+ , decodeRecords+ , parseRecord+ , decodeRecordsWithT+ , decodeRecordsWith+ -- converting between Records and Rows+ , recordsToRows+ , recordToRow+ , rowsToRecords+ , rowToRecord+ -- en/decoding Columnar+ , encodeColumnarCsv+ , csvHeader+ , decodeColumnarCsv+ , decodeColumnarCsvWith+ -- misc tools+ , recordFields+ , listRecords+ -- utilities+ , mkNamedRecord+ , mkIntMap+ -- for convenience+ , module Text.Enum.Text+ ) where++import qualified Control.Lens as L+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.Coerce+import qualified Data.Csv as CSV+import qualified Data.HashMap.Strict as HM+import qualified Data.IntMap.Strict as IM+import qualified Data.Map as Map+import Data.Maybe+import Data.Possibly+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Text.Encoding.Error as TE+import qualified Data.Vector as V+import Fmt+import Text.Enum.Text+import Text.Show.Functions()+++type TextParser a = T.Text -> Possibly a+++-- A toolkit for encoding and decoding CSV Records, and writing out+-- plain-text tables. It builds up the abstraction provided by cassava+-- and adds text decoders. Every type in this framework can automatically+-- work with named fields (a CSV with a header) or unlabeled fields+-- (no header). To generate tables that line up the function assigning+-- a formatter to each column must be supplied (otherwise default formatter+-- that places a space to the right of each column will be used).++-- While cassava is ByteString oriented, this API is somewhat more Text+-- text oriented, with each field being Buildable and TextParsable.++-- Support for auto-building Monoidal Records from Monoidal fields is also+-- supported.++-- The quickest way to see how this qworks is to look at it in actions,+-- for example, "Nike.Server.Types.JobTransition".+++-------------------------------------------------------------------------------+-- Columnar+-------------------------------------------------------------------------------++-- | a columnar type enumerates the columns of a CSV/table; it is just+-- an EnumText with options for CSV codecs and the table generators+class EnumText c => Columnar c where+ columnOptions :: ColumnOptions c+ columnOptions = defaultColumnOptions++data ColumnOptions c =+ ColumnOptions+ { _co_csv_nmd :: IsNamed -- ^ encoding CSV with headers?+ , _co_csv_eos :: CSV.EncodeOptions -- ^ Cassava encode options+ , _co_csv_dos :: CSV.DecodeOptions -- ^ Cassava decode options+ , _co_col_hdr :: WithColHeader -- ^ teabular: include a header line+ , _co_col_fmt :: RowNo -> c -> Builder -> Builder -- ^ teabular: field formatters+ }+ deriving (Show)++-- | are we generating/expecting a header for CSVs+data IsNamed+ = Named -- ^ we are generating and expecting headers+ | Positional CSV.HasHeader -- ^ we are not generating headers but possibly+ -- skipping them on read++-- | are we generating/expecting a header for tabular data+data WithColHeader+ = WithColHeader+ | WithoutColHeader+ deriving (Eq,Ord,Show)++-- | which Row is being formatted+newtype RowNo =+ RowNo+ { _RowNo :: Int+ }+ deriving (Enum,Eq,Num,Ord,Show)++-- | the header is row 0, first data row is 1+headerRowNo, firstRowNo :: RowNo+headerRowNo = 0+firstRowNo = 1++-- | the list of valid RowNo, starting with the first row (NOT the header)+rowNoSupply :: [RowNo]+rowNoSupply = [1..]++instance Show IsNamed where+ show Named = "Named"+ show (Positional CSV.HasHeader) = "(Positional HasHeader)"+ show (Positional CSV.NoHeader ) = "(Positional NoHeader)"++defaultPositionalColumnOptions :: ColumnOptions c+defaultPositionalColumnOptions =+ defaultColumnOptions+ { _co_csv_nmd = Positional CSV.NoHeader+ }++defaultColumnOptions :: ColumnOptions c+defaultColumnOptions =+ ColumnOptions+ { _co_csv_nmd = Named+ , _co_csv_eos = CSV.defaultEncodeOptions+ , _co_csv_dos = CSV.defaultDecodeOptions+ , _co_col_hdr = WithColHeader+ , _co_col_fmt = const $ const id+ }+++-------------------------------------------------------------------------------+-- IsRecordIntMap+-------------------------------------------------------------------------------++-- often Columnar records will be organized as an IntMap of records+class ( IsRecord r c+ , Coercible m (IM.IntMap r)+ ) => IsRecordIntMap r c m | r->c, c->r, r->m, m->r where+ recordKey :: r -> Int++-- | decoding into an Intmap+decodeRecordMap :: forall r c m . IsRecordIntMap r c m => LBS.ByteString -> Possibly m+decodeRecordMap = fmap mk . dec+ where+ dec :: LBS.ByteString -> Possibly (Records r c)+ dec = decodeRecords++ mk =+ coerce+ . IM.fromList+ . mkAssocList recordKey+ . V.toList+ . _recs_vector++-- | encoding to an Intmap+encodeRecordMap :: forall r c m . IsRecordIntMap r c m => m -> LBS.ByteString+encodeRecordMap = enc . mkRecords . IM.elems . coerce+ where+ enc :: Records r c -> LBS.ByteString+ enc = encodeRecords++-- | often we will want to construct a Monoid Intmap from Monoid records+memptyRecordMap :: forall r c m . (Monoid r, IsRecordIntMap r c m) => m+memptyRecordMap = coerce (IM.empty :: IM.IntMap r)++-- | often we will want to construct a Monoid Intmap from Monoid records+mappendRecordMap :: forall r c m . (Monoid r, IsRecordIntMap r c m) => m -> m -> m+mappendRecordMap x y =+ coerce $ (IM.unionWith (<>) (coerce x) (coerce y) :: IM.IntMap r)++-- | provide evidence that Map is well formed+summarizeMap :: forall r c m . IsRecordIntMap r c m => m -> T.Text+summarizeMap m = fmt $ "IntMap with "+|IM.size im|+" records"+ where+ im :: IM.IntMap r+ im = coerce m+++-------------------------------------------------------------------------------+-- IsRecordHashMap+-------------------------------------------------------------------------------++-- sometimes Columnar records will be organized as an HashMap of records+class ( IsRecord r c+ , Coercible m (HM.HashMap T.Text r)+ ) => IsRecordHashMap r c m | r->c, c->r, r->m, m->r where+ recordTextKey :: r -> T.Text++-- | decoding into an Intmap+decodeRecordHashMap :: forall r c m . IsRecordHashMap r c m+ => LBS.ByteString+ -> Possibly m+decodeRecordHashMap = fmap mk . dec+ where+ dec :: LBS.ByteString -> Possibly (Records r c)+ dec = decodeRecords++ mk =+ coerce+ . HM.fromList+ . mkAssocList recordTextKey+ . V.toList+ . _recs_vector++-- | encoding to an Intmap+encodeRecordHashMap :: forall r c m . IsRecordHashMap r c m+ => m+ -> LBS.ByteString+encodeRecordHashMap = enc . mkRecords . HM.elems . coerce+ where+ enc :: Records r c -> LBS.ByteString+ enc = encodeRecords++-- | often we will want to construct a Monoid HashMap from Monoid records+memptyRecordHashMap :: forall r c m . (Monoid r, IsRecordHashMap r c m) => m+memptyRecordHashMap = coerce (HM.empty :: HM.HashMap T.Text r)++-- | often we will want to construct a Monoid HashMap from Monoid records+mappendRecordHashMap :: forall r c m . (Monoid r, IsRecordHashMap r c m)+ => m+ -> m+ -> m+mappendRecordHashMap x y =+ coerce $ (HM.unionWith (<>) (coerce x) (coerce y) :: HM.HashMap T.Text r)++-- | provide evidence that HashMap is well formed+summarizeHashMap :: forall r c m . IsRecordHashMap r c m => m -> T.Text+summarizeHashMap m = fmt $ "HashMap with "+|HM.size hm|+" records"+ where+ hm :: HM.HashMap T.Text r+ hm = coerce m+++-------------------------------------------------------------------------------+-- IsRecord+-------------------------------------------------------------------------------++-- | IsRecord combines the column type with the record type, each record+-- type determining the column type and vice versa+class Columnar c => IsRecord r c | r->c, c->r where+ zeroRecord :: r+ fieldMethods :: c -> FieldMethods r c++-- | IsRecord combines the column type with the record type, each record+-- type determining the column type and vice versa+class (Monoid r,IsRecord r c) => IsMonoidalRecord r c where+ monoidalFieldMethods :: c -> MonoidalFieldMethods r c++-- | for each column we need a lens the field of the record, which must be+-- Buildable and TextParsable+data FieldMethods r c = forall f . (Buildable f,TextParsable f) =>+ FieldMethods+ { _fm_lens :: L.Lens' r f+ }++-- | if we need monoidal records then the fields must be Monoidal too.+data MonoidalFieldMethods r c =+ forall f . (Buildable f,TextParsable f,Monoid f) =>+ MonoidalFieldMethods+ { _mfm_lens :: L.Lens' r f+ }++-- | a record set contains the ColumnOptions along with the vector of+-- records+data Records r c =+ Records+ { _recs_options :: ColumnOptions c+ , _recs_vector :: V.Vector r+ }+ deriving (Show)++instance (IsRecord r c,Buildable (Rows c)) => Buildable (Records r c) where+ build = build . recordsToRows+++-------------------------------------------------------------------------------+-- Rows, Row+-------------------------------------------------------------------------------++-- | Rows are used for generating tabular output and do not need access+-- to any record type+data Rows c =+ Rows+ { _rows_options :: ColumnOptions c+ , _rows_vector :: V.Vector (Row c)+ }+ deriving (Show)++newtype Row c = Row { _Row :: c->Builder }+ deriving (Show)++mkRows :: Columnar c => [Row c] -> Rows c+mkRows = mkRowsWith columnOptions++mkRowsWith :: ColumnOptions c -> [Row c] -> Rows c+mkRowsWith opts rs =+ Rows+ { _rows_options = opts+ , _rows_vector = V.fromList rs+ }++getRows :: Rows c -> [Row c]+getRows = V.toList . _rows_vector++mkRow :: (c -> Builder) -> Row c+mkRow = coerce++instance Columnar c => Buildable (Rows c) where+ build = build_columnar++instance Columnar c => Eq (Row c) where+ (==) (Row f) (Row g) = test_row_equality f g++instance Columnar c => CSV.ToField (CSV.Only c) where+ toField = toFieldEnumText . CSV.fromOnly++instance Columnar c => CSV.FromField (CSV.Only c) where+ parseField = fmap CSV.Only . fromFieldEnumText++instance Columnar c => CSV.ToRecord (Row c) where+ toRecord row = V.fromList $ map mk $ lst row+ where+ mk c = TE.encodeUtf8 $ fmt $ coerce row c++ lst _ = [minBound..maxBound :: c]++instance Columnar c => CSV.FromRecord (Row c) where+ parseRecord = parse_record++instance Columnar c => CSV.ToNamedRecord (Row c) where+ toNamedRecord row = HM.fromList $ map mk $ listColumns $ mk_ix row+ where+ mk c =+ ( CSV.toField $ CSV.Only c+ , TE.encodeUtf8 $ fmt $ coerce row c+ )++ mk_ix :: Row c -> ColumnarIx c+ mk_ix = const ColumnarIx++instance Columnar c => CSV.FromNamedRecord (Row c) where+ parseNamedRecord = parse_named_record+++-------------------------------------------------------------------------------+-- ColumnarIx+-------------------------------------------------------------------------------++-- | sometimes we just need the column type, as with listColumns+data ColumnarIx c = ColumnarIx+ deriving (Show)++listColumns :: Columnar c => ColumnarIx c -> [c]+listColumns = const [minBound..maxBound]+++-------------------------------------------------------------------------------+-- Encoding as Haskell+-------------------------------------------------------------------------------++-- | encode the list of records as a Haskell list of strings,+-- one line per line, indented at two spaces+haskellRecords :: IsRecord r c+ => [r] -- ^ list of records to encode+ -> Builder -- ^ Haskell defining LBS for decoding+haskellRecords rs = mconcat $+ [ " [ "+|show hdr|+"\n"+ ] +++ [ " , "+|show rcd|+"\n"+ | rcd <- bdy+ ] +++ [ " ]"+ ]+ where+ (hdr,bdy) = case T.lines $ encodeRecordsT $ mkRecords rs of+ [] -> error "haskellRecords: empty encoding"+ h:b -> (h,b)+++-------------------------------------------------------------------------------+-- construction toolkit: mkFM, mkMFM, etc+-------------------------------------------------------------------------------++-- | for constructing each field's 'FieldMethods'+mkFieldMethods :: (TextParsable f,Buildable f) => L.Lens' r f -> FieldMethods r c+mkFieldMethods = FieldMethods++-- | for constructing each field's 'MonoidalFieldMethods'+mkMonoidalFieldMethods :: (TextParsable f,Buildable f,Monoid f)+ => L.Lens' r f+ -> MonoidalFieldMethods r c+mkMonoidalFieldMethods = MonoidalFieldMethods++convertMonoidalFieldMethodsToFieldMethods+ :: MonoidalFieldMethods r c -> FieldMethods r c+convertMonoidalFieldMethodsToFieldMethods+ MonoidalFieldMethods{..} = FieldMethods _mfm_lens++constructRecordMappend :: (Bounded c,Enum c)+ => (c->MonoidalFieldMethods r c)+ -> r -> r -> r+constructRecordMappend mfm x = foldr (.) id $ map ma_f [minBound..maxBound]+ where+ ma_f lb = case mfm lb of+ MonoidalFieldMethods{..} ->+ L.over _mfm_lens (L.view _mfm_lens x<>)+++-------------------------------------------------------------------------------+-- mkRecords, mkRecordsWith+-------------------------------------------------------------------------------++mkRecords :: Columnar c => [r] -> Records r c+mkRecords = mkRecordsWith columnOptions++mkRecordsWith :: ColumnOptions c -> [r] -> Records r c+mkRecordsWith co = Records co . V.fromList+++-------------------------------------------------------------------------------+-- en/decoding Records+-------------------------------------------------------------------------------++buildRecord :: forall r c . IsRecord r c => r -> Builder+buildRecord r = build $ encodeRecordsT $ mkRecordsWith co [r]+ where+ co = defaultPositionalColumnOptions++encodeRecordsT :: forall r c . IsRecord r c => Records r c -> T.Text+encodeRecordsT = TE.decodeUtf8With TE.lenientDecode . LBS.toStrict . encodeRecords++encodeRecords :: forall r c . IsRecord r c => Records r c -> LBS.ByteString+encodeRecords = encodeColumnarCsv . recordsToRows++decodeRecordsT :: IsRecord r c => T.Text -> Possibly (Records r c)+decodeRecordsT = decodeRecordsWithT columnOptions++decodeRecords :: IsRecord r c => LBS.ByteString -> Possibly (Records r c)+decodeRecords = decodeRecordsWith columnOptions++parseRecord :: IsRecord r c => TextParser r+parseRecord txt = decodeRecordsWithT co txt >>= extr+ where+ extr Records{..} = case V.toList _recs_vector of+ [r] -> Right r+ _ -> Left "parseRecord: expecting a single record"++ co = defaultPositionalColumnOptions++decodeRecordsWithT :: IsRecord r c+ => ColumnOptions c+ -> T.Text+ -> Possibly (Records r c)+decodeRecordsWithT co = decodeRecordsWith co . LBS.fromStrict . TE.encodeUtf8++decodeRecordsWith :: IsRecord r c+ => ColumnOptions c+ -> LBS.ByteString+ -> Possibly (Records r c)+decodeRecordsWith co lbs = decodeColumnarCsvWith co lbs >>= rowsToRecords+++-------------------------------------------------------------------------------+-- converting between Records and Rows+-------------------------------------------------------------------------------++recordsToRows :: IsRecord r c => Records r c -> Rows c+recordsToRows Records{..} =+ Rows+ { _rows_options = _recs_options+ , _rows_vector = V.map recordToRow _recs_vector+ }++recordToRow :: IsRecord r c => r -> Row c+recordToRow r = Row $ \c -> case fieldMethods c of+ FieldMethods{..} -> build $ L.view _fm_lens r++rowsToRecords :: forall r c . IsRecord r c => Rows c -> Possibly (Records r c)+rowsToRecords Rows{..} = do+ recs <- mapM rowToRecord $ V.toList _rows_vector+ return+ Records+ { _recs_options = _rows_options+ , _recs_vector = V.fromList recs+ }++rowToRecord :: forall r c . IsRecord r c => Row c -> Possibly r+rowToRecord row = foldr op (Right zeroRecord) $ listColumns ix+ where+ op c pr = pr >>= prs (fmt $ _Row row c) c++ ix :: ColumnarIx c+ ix = ColumnarIx++ prs :: T.Text -> c -> r -> Possibly r+ prs txt c r = case fieldMethods c of+ FieldMethods{..} -> do+ f <- parseText txt+ return $ L.set _fm_lens f r+++-------------------------------------------------------------------------------+-- en/decoding Columnar with Rows+-------------------------------------------------------------------------------++encodeColumnarCsv :: forall c . Columnar c+ => Rows c+ -> LBS.ByteString+encodeColumnarCsv Rows{..} =+ enc $ V.toList _rows_vector+ where+ enc :: [Row c] -> LBS.ByteString+ enc = case _co_csv_nmd _rows_options of+ Named -> CSV.encodeByNameWith eos $ csvHeader ix+ Positional _ -> CSV.encodeWith eos++ ix :: ColumnarIx c+ ix = ColumnarIx++ eos = _co_csv_eos _rows_options++csvHeader :: Columnar c => ColumnarIx c -> CSV.Header+csvHeader ci = V.fromList+ [ TE.encodeUtf8 $ fmt $ build c+ | c <- listColumns ci+ ]++decodeColumnarCsv :: Columnar c+ => LBS.ByteString+ -> Possibly (Rows c)+decodeColumnarCsv = decodeColumnarCsvWith columnOptions++decodeColumnarCsvWith :: Columnar c+ => ColumnOptions c+ -> LBS.ByteString+ -> Possibly (Rows c)+decodeColumnarCsvWith opts@ColumnOptions{..} = fmap mk . dec+ where+ dec = case _co_csv_nmd of+ Named -> fmap snd . CSV.decodeByNameWith _co_csv_dos+ Positional hh -> CSV.decodeWith _co_csv_dos hh++ mk vec =+ Rows+ { _rows_options = opts+ , _rows_vector = vec+ }+++-------------------------------------------------------------------------------+-- misc tools+-------------------------------------------------------------------------------++recordFields :: forall r c . IsRecord r c => [(B.ByteString,r->B.ByteString)]+recordFields =+ [ (build_bs c, extr c)+ | c<-[minBound..maxBound :: c]+ ]+ where+ extr :: IsRecord r c => c -> r -> B.ByteString+ extr c r = case fieldMethods c of+ FieldMethods{..} -> build_bs $ L.view _fm_lens r++ build_bs :: Buildable a => a -> B.ByteString+ build_bs = TE.encodeUtf8 . fmt . build++listRecords :: Columnar c => Records r c -> [r]+listRecords = V.toList . _recs_vector+++-------------------------------------------------------------------------------+-- utilities+-------------------------------------------------------------------------------++mkNamedRecord :: [(B.ByteString,a->B.ByteString)]+ -> a+ -> CSV.NamedRecord+mkNamedRecord al r = HM.fromList [ (lbl,prj r) | (lbl,prj) <- al ]++mkIntMap :: (a->Int) -> [a] -> IM.IntMap a+mkIntMap prj rs = IM.fromList [ (prj r,r) | r <- rs ]+++-------------------------------------------------------------------------------+-- internal+-------------------------------------------------------------------------------++build_columnar :: forall c . Columnar c => Rows c -> Builder+build_columnar Rows{..} = mconcat+ [ fmt_row rn row <> "\n"+ | (rn,row) <-+ [ (headerRowNo,hdr) | _co_col_hdr==WithColHeader ] +++ zip rowNoSupply rows+ ]+ where+ fmt_row :: RowNo -> Row c -> Builder+ fmt_row rn row = mconcat $ map gen $ listColumns ix+ where+ gen c = _co_col_fmt rn c $ coerce row c++ hdr = coerce (buildEnumText :: c -> Builder)++ ix :: ColumnarIx c+ ix = ColumnarIx++ rows :: [Row c]+ rows = V.toList _rows_vector++ ColumnOptions{..} = _rows_options++parse_record :: forall c . Columnar c+ => CSV.Record+ -> CSV.Parser (Row c)+parse_record v = case len == v_len of+ True -> return $ Row lu+ False -> fail $ unwords+ [ "Columnar.parseRecord: expected"+ , show len+ , "columns but found"+ , show v_len+ , "in the input"+ ]+ where+ lu :: c -> Builder+ lu c = build $ TE.decodeUtf8 $ v V.! (fromEnum c - fromEnum mnb)++ v_len = V.length v+ len = fromEnum mxb + 1 - fromEnum mnb++ mnb, mxb :: c+ mnb = minBound+ mxb = maxBound++parse_named_record :: forall c . Columnar c+ => CSV.NamedRecord+ -> CSV.Parser (Row c)+parse_named_record hm = do+ mp <- fmap Map.fromList $ mapM prs $ listColumns ix+ return $ Row $ \c -> fromMaybe oops $ Map.lookup c mp+ where+ prs :: c -> CSV.Parser (c,Builder)+ prs c = case HM.lookup bs hm of+ Nothing -> fail $ "CSV record missing column: "++B.unpack bs+ Just val -> return (c,build $ TE.decodeUtf8 val)+ where+ bs = TE.encodeUtf8 $ renderEnumText c++ ix :: ColumnarIx c+ ix = ColumnarIx++ oops = error "parse_named_record: the impossible happened!"++test_row_equality :: forall c . Columnar c+ => (c->Builder)+ -> (c->Builder)+ -> Bool+test_row_equality f g = all chk $ listColumns ix+ where+ chk c = f c == g c++ ix :: ColumnarIx c+ ix = ColumnarIx++mkAssocList :: (a->k) -> [a] -> [(k,a)]+mkAssocList prj rs = [ (prj r,r) | r<-rs ]++fromFieldEnumText :: EnumText c => B.ByteString -> CSV.Parser c+fromFieldEnumText = either fail return . parseEnumText . TE.decodeUtf8