packages feed

javelin-io (empty) → 0.1.1.0

raw patch · 9 files changed

+779/−0 lines, 9 filesdep +basedep +bytestringdep +cassava

Dependencies added: base, bytestring, cassava, containers, filepath, javelin, javelin-io, tasty, tasty-hedgehog, tasty-hunit, temporary, unordered-containers, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for javelin-io++## Release 0.1.1.0++* This is the first version of `javelin-io`.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) Laurent P. René de Cotret++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ javelin-io.cabal view
@@ -0,0 +1,71 @@+cabal-version:      3.0+name:               javelin-io+version:            0.1.1.0+synopsis:           IO operations for the `javelin` package+license:            MIT+license-file:       LICENSE+author:             Laurent P. René de Cotret+maintainer:         laurent.decotret@outlook.com+category:           Data, Data Structures, Data Science+build-type:         Simple+extra-doc-files:    CHANGELOG.md+extra-source-files: test/data/*.csv+tested-with:        GHC ==9.8.1 +                     || ==9.6.3+                     || ==9.4.7+description:+        +        This package implements serialization/deserialization of 'Series', labeled one-dimensional arrays+        combining properties from maps and arrays.+        +        The important modules are:+        +        ["Data.Series.IO"] Serialization/deserialization of series of arbitrary types.+        +        ["Data.Series.Unboxed.IO"] Serialization/deserialization of unboxed series for better performance, at the cost of flexibility.+        +        ["Data.Series.Generic.IO"] Serialization/deserialization of generic series to manipulate any type of 'Series'.+        +        If you don't know where to start, please take a look at the documentation in "Data.Series.IO".++common common+    default-language: GHC2021+    ghc-options: -Wall+                 -Wcompat+                 -Widentities+                 -Wincomplete-uni-patterns+                 -Wincomplete-record-updates+                 -Wredundant-constraints+                 -fhide-source-paths+                 -Wpartial-fields++library+    import:           common+    hs-source-dirs:   src+    exposed-modules:  Data.Series.IO+                      Data.Series.Generic.IO+                      Data.Series.Unboxed.IO+    build-depends:    base >=4.15.0.0 && <4.20,+                      bytestring ^>=0.12,+                      cassava ^>=0.5,+                      containers >=0.6 && <0.8,+                      unordered-containers ^>=0.2,+                      javelin ^>=0.1,+                      vector >=0.12.3.0 && <0.14,++test-suite javelin-io-test+    import:           common+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+    main-is:          Main.hs+    other-modules:    Test.Data.Series.IO+    build-depends:    base,+                      cassava,+                      filepath,+                      javelin,+                      javelin-io,+                      tasty,+                      tasty-hedgehog,+                      tasty-hunit,+                      temporary,+                      vector
+ src/Data/Series/Generic/IO.hs view
@@ -0,0 +1,218 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}+-----------------------------------------------------------------------------+-- |+-- Module      :  $module+-- Copyright   :  (c) Laurent P. René de Cotret+-- License     :  MIT+-- Maintainer  :  laurent.decotret@outlook.com+-- Portability :  portable+--+-- This module contains functions to serialize/deserialize generic 'Series'+-- to/from bytes.+--+-- Use this module if you want to support all types of 'Series'. Otherwise,+-- you should use either the modules "Data.Series.IO" or "Data.Series.Unboxed.IO". +module Data.Series.Generic.IO (+    -- * Deserialize 'Series'+    readCSV,+    readCSVFromFile,++    -- * Serialize 'Series'+    writeCSV,+    writeCSVToFile,+) where+++import           Control.Monad          ( forM )+import           Control.Monad.IO.Class ( MonadIO(liftIO) )+import qualified Data.ByteString        as BS+import qualified Data.ByteString.Lazy   as BL+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )+import qualified Data.Csv               as CSV+import           Data.Functor           ( (<&>) )+import qualified Data.HashMap.Strict    as HashMap+import qualified Data.List.NonEmpty     as NE+import           Data.Maybe             ( fromMaybe )+import           Data.Series.Generic    ( Series, fromVector, convert )+import qualified Data.Series.Generic    as GSeries+import qualified Data.Vector            as Boxed+import           Data.Vector.Generic    ( Vector )+import qualified System.IO              as IO+++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+data LatLong = MkLatLong { latitude  :: Double+                         , longitude :: Double+                         }+    deriving ( Show )++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' LatLong where+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"+                                   \<*\> r .: "longitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import "Data.Series.Generic"+import "Data.Series.Generic.IO"+import "Data.Vector" ++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latlongs  :: 'Series' Vector City LatLong) = either (error . show) id \<$\> `readCSV` stream+    print latlongs+@+-}+readCSV :: (Vector v a, Ord k, FromNamedRecord k, FromNamedRecord a)+        => BL.ByteString+        -> Either String (Series v k a)+readCSV bytes = do+    (_, records :: Boxed.Vector CSV.NamedRecord) <- CSV.decodeByName bytes++    rows <- CSV.runParser $ forM records+                          $ \record -> (,) <$> parseNamedRecord record+                                           <*> parseNamedRecord record++    pure $ convert $ fromVector rows+++fromFile :: MonadIO m +         => FilePath+         -> (BL.ByteString -> Either String b)+         -> m (Either String b)+fromFile fp f+    = liftIO $ IO.withFile fp IO.ReadMode $ \h -> do+        IO.hSetBinaryMode h True+        BS.hGetContents h <&> f . BL.fromStrict+++{-|+This is a helper function to read a CSV directly from a filepath.+See the documentation for 'readCSV' on how to prepare your types.+Then, for example, you can use 'readCSVFromFile' as:++@+import "Data.Series.Generic"+import "Data.Series.Generic.IO"+import "Data.Vector" ++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latlongs  :: 'Series' Vector City LatLong) = either (error . show) id \<$\> `readCSV` stream+    print latlongs+@+-}+readCSVFromFile :: (MonadIO m, Vector v a, Ord k, FromNamedRecord k, FromNamedRecord a)+                => FilePath+                -> m (Either String (Series v k a))+readCSVFromFile fp = fromFile fp readCSV ++++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+data LatLong = MkLatLong { latitude  :: Double+                         , longitude :: Double+                         }+    deriving ( Show )++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' LatLong where+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"+                                   \<*\> r .: "longitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import Data.Series+import Data.Series.IO++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream+    print latlongs+@+-}+writeCSV :: (Vector v a, ToNamedRecord k, ToNamedRecord a)+         => Series v k a+         -> BL.ByteString+writeCSV xs = fromMaybe mempty $ do+    recs   <- NE.nonEmpty [ toNamedRecord k <> toNamedRecord v | (k, v) <- GSeries.toList xs]+    let header =  CSV.header $ HashMap.keys $ NE.head recs+    pure $ CSV.encodeByName header $ NE.toList recs+++-- | This is a helper function to write a 'Series' directly to CSV.+-- See the documentation for 'writeCSV' on how to prepare your types.+writeCSVToFile :: (MonadIO m, Vector v a, ToNamedRecord k, ToNamedRecord a)+               => FilePath+               -> Series v k a+               -> m ()+writeCSVToFile fp +    = liftIO +    . BS.writeFile fp +    . BL.toStrict +    . writeCSV
+ src/Data/Series/IO.hs view
@@ -0,0 +1,179 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}+-----------------------------------------------------------------------------+-- |+-- Module      :  $module+-- Copyright   :  (c) Laurent P. René de Cotret+-- License     :  MIT+-- Maintainer  :  laurent.decotret@outlook.com+-- Portability :  portable+--+-- This module contains functions to serialize/deserialize 'Series'+-- to/from bytes.+module Data.Series.IO (+    -- * Deserialize 'Series'+    readCSV,+    readCSVFromFile,++    -- * Serialize 'Series'+    writeCSV,+    writeCSVToFile,+) where+++import           Control.Monad.IO.Class ( MonadIO )+import qualified Data.ByteString.Lazy   as BL+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )+import           Data.Series            ( Series )+import qualified Data.Series.Generic.IO as Generic.IO+++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+data LatLong = MkLatLong { latitude  :: Double+                         , longitude :: Double+                         }+    deriving ( Show )++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' LatLong where+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"+                                   \<*\> r .: "longitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import "Data.Series"+import "Data.Series.IO"++main :: IO ()+main = do+    let fp = "path/to/my/file.csv"+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSVFromFile` fp+    print latlongs+@+-}+readCSV :: (Ord k, FromNamedRecord k, FromNamedRecord a)+        => BL.ByteString+        -> Either String (Series k a)+readCSV = Generic.IO.readCSV+++{-|+This is a helper function to read a CSV directly from a filepath.+See the documentation for 'readCSV' on how to prepare your types.+Then, for example, you can use 'readCSVFromFile' as:++@+import "Data.Series"+import "Data.Series.IO"++main :: IO ()+main = do+    let fp = "path/to/my/file.csv"+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSVFromFile` fp+    print latlongs+@+-}+readCSVFromFile :: (MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a)+                => FilePath+                -> m (Either String (Series k a))+readCSVFromFile = Generic.IO.readCSVFromFile++++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+data LatLong = MkLatLong { latitude  :: Double+                         , longitude :: Double+                         }+    deriving ( Show )++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' LatLong where+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"+                                   \<*\> r .: "longitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import Data.Series+import Data.Series.IO++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream+    print latlongs+@+-}+writeCSV :: (ToNamedRecord k, ToNamedRecord a)+         => Series k a+         -> BL.ByteString+writeCSV = Generic.IO.writeCSV+++-- | This is a helper function to write a 'Series' directly to CSV.+-- See the documentation for 'writeCSV' on how to prepare your types.+writeCSVToFile :: (MonadIO m, ToNamedRecord k, ToNamedRecord a)+               => FilePath+               -> Series k a+               -> m ()+writeCSVToFile = Generic.IO.writeCSVToFile
+ src/Data/Series/Unboxed/IO.hs view
@@ -0,0 +1,199 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}+-----------------------------------------------------------------------------+-- |+-- Module      :  $module+-- Copyright   :  (c) Laurent P. René de Cotret+-- License     :  MIT+-- Maintainer  :  laurent.decotret@outlook.com+-- Portability :  portable+--+-- This module contains functions to serialize/deserialize unboxed 'Series'+-- to/from bytes.+--+-- = Why use unboxed series?+--+-- Unboxed series can have much better performance, at the cost of less flexibility. For example,+-- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of +-- `Functor` or `Foldable`.+--+-- If you are hesitating, you should prefer the series implementation in the "Data.Series" module (and therefore+-- the IO module "Data.Series.IO").+module Data.Series.Unboxed.IO (+    -- * Deserialize 'Series'+    readCSV,+    readCSVFromFile,++    -- * Serialize 'Series'+    writeCSV,+    writeCSVToFile,+) where+++import           Control.Monad.IO.Class ( MonadIO )+import qualified Data.ByteString.Lazy   as BL+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )+import           Data.Series.Unboxed    ( Series )+import qualified Data.Series.Generic.IO as Generic.IO+import           Data.Vector.Unboxed    ( Unbox )+++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+import qualified "Data.Vector.Unboxed"         as U+import qualified "Data.Vector.Unboxed.Mutable" as UM+import qualified "Data.Vector.Generic"         as G+import qualified "Data.Vector.Generic.Mutable" as GM++newtype Latitude = MkLatitude Double+    deriving (Show)++-- Special code to ensure that Latitude is unboxed+-- This is only required for unboxed 'Series'+newtype instance UM.MVector s Latitude = MV_Latitude (UM.MVector s Int)+newtype instance U.Vector Latitude = V_Latitude (U.Vector Int)+deriving instance GM.MVector UM.MVector Latitude+deriving instance G.Vector U.Vector Latitude +instance U.Unbox Latitude++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' Latitude where+    'parseNamedRecord' r = MkLatitude \<$\> r .: "latitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import "Data.Series.Unboxed"+import "Data.Series.Unboxed.IO"++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latitudes  :: 'Series' City Latitude) = either (error . show) id \<$\> `readCSV` stream+    print latitudes+@+-}+readCSV :: (Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)+        => BL.ByteString+        -> Either String (Series k a)+readCSV = Generic.IO.readCSV+++{-|+This is a helper function to read a CSV directly from a filepath.+See the documentation for 'readCSV' on how to prepare your types.+Then, for example, you can use 'readCSVFromFile' as:++@+import "Data.Series.Unboxed"+import "Data.Series.Unboxed.IO"++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latitudes  :: 'Series' City Latitude) = either (error . show) id \<$\> `readCSV` stream+    print latitudes+@+-}+readCSVFromFile :: (MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)+                => FilePath+                -> m (Either String (Series k a))+readCSVFromFile = Generic.IO.readCSVFromFile++++{-|+Read a comma-separated value (CSV) bytestream into a series.++Consider the following bytestream read from a file:++@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires+@++We want to get a series of the latitude an longitude, indexed by the column "city". First, we need+to do is to create a datatype representing the latitude and longitude information, and our index:++@+data LatLong = MkLatLong { latitude  :: Double+                         , longitude :: Double+                         }+    deriving ( Show )++newtype City = MkCity String+    deriving ( Eq, Ord, Show )+@++Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:++@+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )++instance 'FromNamedRecord' LatLong where+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"+                                   \<*\> r .: "longitude"+++instance 'FromNamedRecord' City where+    'parseNamedRecord' r = MkCity \<$\> r .: "city"+@++Finally, we're ready to read our stream:++@+import Data.Series+import Data.Series.IO++main :: IO ()+main = do+    stream <- (...) -- Read the bytestring from somewhere+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream+    print latlongs+@+-}+writeCSV :: (ToNamedRecord k, ToNamedRecord a, Unbox a)+         => Series k a+         -> BL.ByteString+writeCSV = Generic.IO.writeCSV+++-- | This is a helper function to write a 'Series' directly to CSV.+-- See the documentation for 'writeCSV' on how to prepare your types.+writeCSVToFile :: (MonadIO m, ToNamedRecord k, ToNamedRecord a, Unbox a)+               => FilePath+               -> Series k a+               -> m ()+writeCSVToFile = Generic.IO.writeCSVToFile
+ test/Main.hs view
@@ -0,0 +1,9 @@+module Main (main) where++import qualified Test.Data.Series.IO++import           Test.Tasty ( defaultMain, testGroup )++main :: IO ()+main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.IO.tests+                                            ]
+ test/Test/Data/Series/IO.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+module Test.Data.Series.IO (tests) where++import           Data.Csv             ( FromNamedRecord(..), (.:), ToNamedRecord(..), (.=), namedRecord )+import           Data.Series.Generic  ( Series, at)+import qualified Data.Series.Generic  as Series+import           Data.Series.IO       ( readCSVFromFile, writeCSVToFile )+import           Data.String          ( IsString )+import           Data.Vector          ( Vector )++import           System.FilePath      ( (</>) )+import           System.IO.Temp       ( withSystemTempDirectory )++import           Test.Tasty           ( testGroup, TestTree ) +import           Test.Tasty.HUnit     ( testCase, assertEqual )++tests :: TestTree+tests = testGroup "Data.Series.IO" [ testReadCSVFromFile+                                   , testWriteCSVToFile+                                   ]+++data LatLong = MkLatLong {lat :: Double, long :: Double} deriving (Eq, Show)++instance FromNamedRecord LatLong where+    parseNamedRecord r = MkLatLong <$> r .: "latitude"+                                   <*> r .: "longitude"++instance ToNamedRecord LatLong where+    toNamedRecord (MkLatLong lat long) = namedRecord [ "latitude" .= lat+                                                     , "longitude" .= long+                                                     ]++newtype City = MkCity String+    deriving (Eq, Ord, IsString, Show) ++instance FromNamedRecord City where+    parseNamedRecord r = MkCity <$> r .: "city"++instance ToNamedRecord City where+    toNamedRecord (MkCity city) = namedRecord ["city" .= city]+++testReadCSVFromFile :: TestTree+testReadCSVFromFile = testCase "Read CSV data" $ do+    (latlongs  :: Series Vector City LatLong) <- either (error . show) id <$> readCSVFromFile "test/data/lat-long-city.csv"++    let latitudes  = fmap lat latlongs+        longitudes = fmap long latlongs++    assertEqual mempty 4 (Series.length latitudes)+    assertEqual mempty (Just 48.856667) (latitudes `at` "Paris") +    assertEqual mempty (Just 40.712778) (latitudes `at` "New York City") +    assertEqual mempty (Just 25.0375) (latitudes `at` "Taipei") +    assertEqual mempty (Just (-34.603333)) (latitudes `at` "Buenos Aires") ++    assertEqual mempty 4 (Series.length longitudes)+    assertEqual mempty (Just 2.352222) (longitudes `at` "Paris") +    assertEqual mempty (Just (-74.006111)) (longitudes `at` "New York City") +    assertEqual mempty (Just 121.5625) (longitudes `at` "Taipei") +    assertEqual mempty (Just (-58.381667)) (longitudes `at` "Buenos Aires") +++testWriteCSVToFile :: TestTree+testWriteCSVToFile = testCase "Write CSV data" $ do+    (latlongs  :: Series Vector City LatLong) <- either (error . show) id <$> readCSVFromFile "test/data/lat-long-city.csv"+    +    (written :: Series Vector City LatLong) <- withSystemTempDirectory "test-javelin-op" $ \dirname -> do+        writeCSVToFile (dirname </> "myseries.csv") latlongs++        either (error . show) id <$> readCSVFromFile (dirname </> "myseries.csv")+    +    assertEqual mempty latlongs written
+ test/data/lat-long-city.csv view
@@ -0,0 +1,5 @@+latitude,longitude,city+48.856667,2.352222,Paris+40.712778,-74.006111,New York City+25.0375,121.5625,Taipei+-34.603333,-58.381667,Buenos Aires