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 +5/−0
- LICENSE +20/−0
- javelin-io.cabal +71/−0
- src/Data/Series/Generic/IO.hs +218/−0
- src/Data/Series/IO.hs +179/−0
- src/Data/Series/Unboxed/IO.hs +199/−0
- test/Main.hs +9/−0
- test/Test/Data/Series/IO.hs +73/−0
- test/data/lat-long-city.csv +5/−0
+ 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