packages feed

dataframe 1.0.0.1 → 1.1.0.0

raw patch · 60 files changed

+4784/−8942 lines, 60 filesdep −mmapdep −paralleldep ~dataframePVP ok

version bump matches the API change (PVP)

Dependencies removed: mmap, parallel

Dependency ranges changed: dataframe

API changes (from Hackage documentation)

- DataFrame: fastReadCsvUnstable :: FilePath -> IO DataFrame
- DataFrame: fastReadTsvUnstable :: FilePath -> IO DataFrame
- DataFrame: readCsvUnstable :: FilePath -> IO DataFrame
- DataFrame: readTsvUnstable :: FilePath -> IO DataFrame
- DataFrame.Errors: [ColumnNotFoundException] :: Text -> Text -> [Text] -> DataFrameException
- DataFrame.IO.CSV: getRowAsText :: DataFrame -> Int -> [Text]
- DataFrame.IO.Unstable.CSV: fastReadCsvUnstable :: FilePath -> IO DataFrame
- DataFrame.IO.Unstable.CSV: fastReadTsvUnstable :: FilePath -> IO DataFrame
- DataFrame.IO.Unstable.CSV: getDelimiterIndices :: Word8 -> Int -> Vector Word8 -> IO (Vector CSize)
- DataFrame.IO.Unstable.CSV: instance GHC.Enum.Enum DataFrame.IO.Unstable.CSV.State
- DataFrame.IO.Unstable.CSV: readCsvUnstable :: FilePath -> IO DataFrame
- DataFrame.IO.Unstable.CSV: readTsvUnstable :: FilePath -> IO DataFrame
- DataFrame.Internal.Column: [MOptionalColumn] :: forall a. Columnable a => IOVector (Maybe a) -> MutableColumn
- DataFrame.Internal.Column: [OptionalColumn] :: forall a. Columnable a => Vector (Maybe a) -> Column
- DataFrame.Internal.Column: instance DataFrame.Internal.Column.Columnable a => DataFrame.Internal.Column.ColumnifyRep 'DataFrame.Internal.Types.ROptional (GHC.Maybe.Maybe a)
- DataFrame.Internal.Types: ROptional :: Rep
+ DataFrame: [safeColumns] :: ParquetReadOptions -> Bool
+ DataFrame: toCsv :: DataFrame -> Text
+ DataFrame: toSeparated :: Char -> DataFrame -> Text
+ DataFrame.Errors: [ColumnsNotFoundException] :: [Text] -> Text -> [Text] -> DataFrameException
+ DataFrame.Errors: columnsNotFound :: [Text] -> Text -> [Text] -> String
+ DataFrame.IO.Parquet: [safeColumns] :: ParquetReadOptions -> Bool
+ DataFrame.IO.Parquet: applySafeRead :: ParquetReadOptions -> DataFrame -> DataFrame
+ DataFrame.Internal.Column: allValidBitmap :: Int -> Bitmap
+ DataFrame.Internal.Column: bitmapConcat :: Int -> Bitmap -> Int -> Bitmap -> Bitmap
+ DataFrame.Internal.Column: bitmapSlice :: Int -> Int -> Bitmap -> Bitmap
+ DataFrame.Internal.Column: bitmapTestBit :: Bitmap -> Int -> Bool
+ DataFrame.Internal.Column: buildBitmapFromNulls :: Int -> [Int] -> Bitmap
+ DataFrame.Internal.Column: buildBitmapFromValid :: Vector Word8 -> Bitmap
+ DataFrame.Internal.Column: columnBitmap :: Column -> Maybe Bitmap
+ DataFrame.Internal.Column: columnElemIsNull :: Column -> Int -> Bool
+ DataFrame.Internal.Column: ensureOptional :: Column -> Column
+ DataFrame.Internal.Column: eqBoxedCols :: Eq a => Maybe Bitmap -> Vector a -> Maybe Bitmap -> Vector a -> Bool
+ DataFrame.Internal.Column: fromMaybeVec :: Columnable a => Vector (Maybe a) -> Column
+ DataFrame.Internal.Column: fromMaybeVecUnboxed :: (Columnable a, Unbox a) => Vector (Maybe a) -> Column
+ DataFrame.Internal.Column: instance DataFrame.Internal.Column.Columnable a => DataFrame.Internal.Column.ColumnifyRep 'DataFrame.Internal.Types.RNullableBoxed (GHC.Maybe.Maybe a)
+ DataFrame.Internal.Column: mergeBitmaps :: Bitmap -> Bitmap -> Bitmap
+ DataFrame.Internal.Column: type Bitmap = Vector Word8
+ DataFrame.Internal.Column: zipWithColumnsGeneral :: (Columnable a, Columnable b, Columnable c) => (a -> b -> c) -> Column -> Column -> Either DataFrameException Column
+ DataFrame.Internal.DataFrame: getRowAsText :: DataFrame -> Int -> [Text]
+ DataFrame.Internal.DataFrame: showElement :: Column -> Int -> Text
+ DataFrame.Internal.DataFrame: stripJust :: Text -> Text
+ DataFrame.Internal.DataFrame: toCsv :: DataFrame -> Text
+ DataFrame.Internal.DataFrame: toSeparated :: Char -> DataFrame -> Text
+ DataFrame.Internal.Types: RNullableBoxed :: Rep
+ DataFrame.Operations.Join: fullOuterJoinNonEmpty :: [Text] -> DataFrame -> DataFrame -> DataFrame
+ DataFrame.Operations.Join: innerJoinNonEmpty :: [Text] -> DataFrame -> DataFrame -> DataFrame
+ DataFrame.Operations.Join: leftJoinNonEmpty :: Text -> [Text] -> DataFrame -> DataFrame -> DataFrame
+ DataFrame.Operations.Join: leftJoinWithCallPoint :: Text -> [Text] -> DataFrame -> DataFrame -> DataFrame
+ DataFrame.Operations.Join: validatedKeyColIndices :: Text -> Set Text -> DataFrame -> [Int]
+ DataFrame.Operations.Subset: splitForStratified :: SplittableGen g => g -> (g, g)
+ DataFrame.Operations.Subset: type SplittableGen g = (SplitGen g, RandomGen g)
+ DataFrame.Typed: deriveSchemaFromCsvFileWith :: ReadOptions -> String -> String -> DecsQ
+ DataFrame.Typed.TH: deriveSchemaFromCsvFileWith :: ReadOptions -> String -> String -> DecsQ
- DataFrame: ParquetReadOptions :: Maybe [Text] -> Maybe (Expr Bool) -> Maybe (Int, Int) -> ParquetReadOptions
+ DataFrame: ParquetReadOptions :: Maybe [Text] -> Maybe (Expr Bool) -> Maybe (Int, Int) -> Bool -> ParquetReadOptions
- DataFrame: stratifiedSample :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
+ DataFrame: stratifiedSample :: forall a g. (SplittableGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
- DataFrame: stratifiedSplit :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
+ DataFrame: stratifiedSplit :: forall a g. (SplittableGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
- DataFrame.IO.Parquet: ParquetReadOptions :: Maybe [Text] -> Maybe (Expr Bool) -> Maybe (Int, Int) -> ParquetReadOptions
+ DataFrame.IO.Parquet: ParquetReadOptions :: Maybe [Text] -> Maybe (Expr Bool) -> Maybe (Int, Int) -> Bool -> ParquetReadOptions
- DataFrame.Internal.Column: [BoxedColumn] :: forall a. Columnable a => Vector a -> Column
+ DataFrame.Internal.Column: [BoxedColumn] :: forall a. Columnable a => Maybe Bitmap -> Vector a -> Column
- DataFrame.Internal.Column: [UnboxedColumn] :: forall a. (Columnable a, Unbox a) => Vector a -> Column
+ DataFrame.Internal.Column: [UnboxedColumn] :: forall a. (Columnable a, Unbox a) => Maybe Bitmap -> Vector a -> Column
- DataFrame.Operations.Subset: stratifiedSample :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
+ DataFrame.Operations.Subset: stratifiedSample :: forall a g. (SplittableGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
- DataFrame.Operations.Subset: stratifiedSplit :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
+ DataFrame.Operations.Subset: stratifiedSplit :: forall a g. (SplittableGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
- DataFrame.Typed: col :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TExpr cols a
+ DataFrame.Typed: col :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TExpr cols a
- DataFrame.Typed: columnAsList :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> [a]
+ DataFrame.Typed: columnAsList :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> [a]
- DataFrame.Typed: columnAsVector :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> Vector a
+ DataFrame.Typed: columnAsVector :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> Vector a
- DataFrame.Typed: replaceColumn :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, a ~ Lookup name cols, AssertPresent name cols) => TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
+ DataFrame.Typed: replaceColumn :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, a ~ SafeLookup name cols, AssertPresent name cols) => TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
- DataFrame.Typed.Access: columnAsList :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> [a]
+ DataFrame.Typed.Access: columnAsList :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> [a]
- DataFrame.Typed.Access: columnAsVector :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> Vector a
+ DataFrame.Typed.Access: columnAsVector :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TypedDataFrame cols -> Vector a
- DataFrame.Typed.Expr: col :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ Lookup name cols, Columnable a, AssertPresent name cols) => TExpr cols a
+ DataFrame.Typed.Expr: col :: forall (name :: Symbol) (cols :: [Type]) a. (KnownSymbol name, a ~ SafeLookup name cols, Columnable a, AssertPresent name cols) => TExpr cols a
- DataFrame.Typed.Operations: replaceColumn :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, a ~ Lookup name cols, AssertPresent name cols) => TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
+ DataFrame.Typed.Operations: replaceColumn :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, a ~ SafeLookup name cols, AssertPresent name cols) => TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols

Files

CHANGELOG.md view
@@ -1,5 +1,26 @@ # Revision history for dataframe +## 1.1.0.0+### Breaking changes+* Remove `OptionalColumn` constructor; fold nullability into `BoxedColumn`/`UnboxedColumn` via bit-packed bitmap.+* Remove `NFData` instance from `Columnable` constraint.++### New features+* Add `toCsv` and `toSeparated` for converting a DataFrame to CSV/delimited text without writing to a file.+* `safeRead` now defaults reading columns to `Maybe a`.+* Split SIMD CSV reader into a separate `dataframe-fastcsv` package.++### Bug fixes+* Fix joins for missing key columns (#187).+* Fix single column not found error when using typed dataframe.+* Fix Synthesis to use `SafeLookup` constraint.+* Fix `writeSeparated` ignoring separator parameter (was hardcoded to comma).++### Internal+* Slice groups now use custom backpermute instead of converting unboxed vectors.+* Reuse comparison operators in Subset.+* Refactor `getRowAsText` for readability using pattern guards.+ ## 1.0.0.1 * toMarkdownTable is now toMarkdown (mostly used internally) * Provide toMarkdown' that outputs string
app/Synthesis.hs view
@@ -1,53 +1,51 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} +import Data.Char import qualified Data.Text as T import qualified DataFrame as D+import DataFrame.DecisionTree import qualified DataFrame.Functions as F+import DataFrame.Operators+import qualified DataFrame.Typed as DT+import System.Random -import Data.Char-import DataFrame.DecisionTree-import DataFrame.Operators hiding (name)+$( DT.deriveSchemaFromCsvFileWith+    D.defaultReadOptions{D.safeRead = True}+    "TrainSchema"+    "./data/titanic/train.csv"+ )+$( DT.deriveSchemaFromCsvFileWith+    D.defaultReadOptions{D.safeRead = True}+    "TestSchema"+    "./data/titanic/test.csv"+ ) -import System.Random+-- Survived is Maybe Int (safeRead = True); prediction is Int (model output).+type RawPredSchema =+    '[DT.Column "Survived" (Maybe Int), DT.Column "prediction" Int] -$(F.declareColumnsFromCsvFile "./data/titanic/train.csv")+prediction :: D.Expr Int+prediction = F.col @Int "prediction"  main :: IO () main = do-    train <- D.readCsv "./data/titanic/train.csv"-    test <- D.readCsv "./data/titanic/test.csv"+    rawTrain <- D.readCsv "./data/titanic/train.csv"+    rawTest <- D.readCsv "./data/titanic/test.csv" -    -- Apply the same transformations to training and test.-    let combined =-            (train <> test)-                |> D.deriveMany-                    [ "Ticket" .= F.lift (T.filter isAlpha) ticket-                    , "Name" .= F.match "\\s*([A-Za-z]+)\\." name-                    , "Cabin" .= F.whenPresent (T.take 1) cabin-                    ]-                |> D.renameMany-                    [ (F.name name, "title")-                    , (F.name cabin, "cabin_prefix")-                    , (F.name pclass, "passenger_class")-                    , (F.name sibsp, "number_of_siblings_and_spouses")-                    , (F.name parch, "number_of_parents_and_children")-                    ]-    print combined+    train <-+        maybe (fail "train.csv schema mismatch") pure (DT.freeze @TrainSchema rawTrain)+    test <-+        maybe (fail "test.csv schema mismatch") pure (DT.freeze @TestSchema rawTest) -    let (train', validation) =-            D.take-                (D.nRows train)-                combined-                |> D.filterJust (F.name survived)-                |> D.randomSplit (mkStdGen 4232) 0.7-        -- Split the test out again.-        test' =-            D.drop-                (D.nRows train)-                combined+    let (trainDf, validDf) =+            D.randomSplit (mkStdGen 4232) 0.7 (DT.thaw (clean train))+        testDf = DT.thaw (clean test)          model =             fitDecisionTree@@ -61,70 +59,84 @@                             { complexityPenalty = 0.1                             , maxExprDepth = 3                             , disallowedCombinations =-                                [ (F.name age, F.name fare)+                                [ ("Age", "Fare")                                 , ("passenger_class", "number_of_siblings_and_spouses")                                 , ("passenger_class", "number_of_parents_and_children")                                 ]                             }                     }                 )-                survived -- Label to predict-                ( train'-                    |> D.exclude [F.name passengerid]-                )+                (F.fromMaybe 0 (F.col @(Maybe Int) "Survived"))+                (trainDf |> D.exclude ["PassengerId"])      print model      putStrLn "Training accuracy: "-    print $-        computeAccuracy-            (train' |> D.derive (F.name prediction) model)+    print $ computeAccuracy (trainDf |> D.derive (F.name prediction) model)      putStrLn "Validation accuracy: "-    print $-        computeAccuracy-            ( validation-                |> D.derive (F.name prediction) model-            )+    print $ computeAccuracy (validDf |> D.derive (F.name prediction) model) -    let predictions = D.derive (F.name survived) model test'     D.writeCsv         "./predictions.csv"-        (predictions |> D.select [F.name passengerid, F.name survived])+        ( testDf+            |> D.derive "Survived" model+            |> D.select ["PassengerId", "Survived"]+        ) -prediction :: D.Expr Int-prediction = F.col @Int "prediction"+clean ::+    ( DT.AssertPresent "Ticket" cols+    , DT.SafeLookup "Ticket" cols ~ Maybe T.Text+    , DT.AssertPresent "Name" cols+    , DT.SafeLookup "Name" cols ~ Maybe T.Text+    , DT.AssertPresent "Cabin" cols+    , DT.SafeLookup "Cabin" cols ~ Maybe T.Text+    ) =>+    DT.TypedDataFrame cols ->+    DT.TypedDataFrame+        ( DT.RenameManyInSchema+            '[ '("Name", "title")+             , '("Cabin", "cabin_prefix")+             , '("Pclass", "passenger_class")+             , '("SibSp", "number_of_siblings_and_spouses")+             , '("Parch", "number_of_parents_and_children")+             ]+            cols+        )+clean tdf =+    tdf+        |> DT.replaceColumn @"Ticket" (DT.nullLift (T.filter isAlpha) (DT.col @"Ticket"))+        |> DT.replaceColumn @"Name" (DT.nullLift extractTitle (DT.col @"Name"))+        |> DT.replaceColumn @"Cabin" (DT.nullLift (T.take 1) (DT.col @"Cabin"))+        |> DT.renameMany+            @'[ '("Name", "title")+              , '("Cabin", "cabin_prefix")+              , '("Pclass", "passenger_class")+              , '("SibSp", "number_of_siblings_and_spouses")+              , '("Parch", "number_of_parents_and_children")+              ] +-- | Extract title (e.g. "Mr", "Mrs") from a full Titanic passenger name.+extractTitle :: T.Text -> T.Text+extractTitle name =+    case filter (T.isSuffixOf ".") (T.words name) of+        (w : _) -> T.dropEnd 1 w+        [] -> ""++{- | Compute binary classification accuracy from a DataFrame containing+  "Survived" and "prediction" columns.+-} computeAccuracy :: D.DataFrame -> Double computeAccuracy df =-    let-        tp =-            fromIntegral $-                D.nRows-                    ( D.filterWhere-                        (survived .== F.lit (1 :: Int) .&& prediction .== F.lit (1 :: Int))-                        df-                    )-        tn =-            fromIntegral $-                D.nRows-                    ( D.filterWhere-                        (survived .== F.lit (0 :: Int) .&& prediction .== F.lit (0 :: Int))-                        df-                    )-        fp =-            fromIntegral $-                D.nRows-                    ( D.filterWhere-                        (survived .== F.lit (0 :: Int) .&& prediction .== F.lit (1 :: Int))-                        df-                    )-        fn =-            fromIntegral $-                D.nRows-                    ( D.filterWhere-                        (survived .== F.lit (1 :: Int) .&& prediction .== F.lit (0 :: Int))-                        df-                    )-     in-        (tp + tn) / (tp + tn + fp + fn)+    let tdf =+            DT.impute @"Survived" 0 $+                DT.unsafeFreeze @RawPredSchema $+                    df |> D.select ["Survived", "prediction"]+        survived = DT.col @"Survived"+        pred = DT.col @"prediction"+        count expr = fromIntegral (DT.nRows (DT.filterWhere expr tdf))+        tp = count ((survived DT..==. DT.lit 1) DT..&&. (pred DT..==. DT.lit 1))+        tn = count ((survived DT..==. DT.lit 0) DT..&&. (pred DT..==. DT.lit 0))+        fp = count ((survived DT..==. DT.lit 0) DT..&&. (pred DT..==. DT.lit 1))+        fn = count ((survived DT..==. DT.lit 1) DT..&&. (pred DT..==. DT.lit 0))+     in (tp + tn) / (tp + tn + fp + fn)
benchmark/Main.hs view
@@ -43,7 +43,7 @@  groupByHaskell :: IO () groupByHaskell = do-    df <- D.fastReadCsvUnstable "./data/housing.csv"+    df <- D.readCsv "./data/housing.csv"     print $         df             |> D.groupBy ["ocean_proximity"]@@ -84,12 +84,6 @@ parseFile :: String -> IO () parseFile = void . D.readCsv -parseFileUnstable :: String -> IO ()-parseFileUnstable = void . D.readCsvUnstable--parseFileUnstableSIMD :: String -> IO ()-parseFileUnstableSIMD = void . D.fastReadCsvUnstable- parseHousingCSV :: IO () parseHousingCSV = parseFile "./data/housing.csv" @@ -215,22 +209,12 @@         , bgroup             "housing.csv (1.4 MB)"             [ bench "Attoparsec" $ nfIO $ parseFile "./data/housing.csv"-            , bench "Native Haskell" $ nfIO $ parseFileUnstable "./data/housing.csv"-            , bench "SIMD" $ nfIO $ parseFileUnstableSIMD "./data/housing.csv"             ]         , bgroup             "effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv (9.1 MB)"             [ bench "Attoparsec" $                 nfIO $                     parseFile-                        "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv"-            , bench "Native Haskell" $-                nfIO $-                    parseFileUnstable-                        "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv"-            , bench "SIMD" $-                nfIO $-                    parseFileUnstableSIMD                         "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv"             ]         , bgroup
− cbits/process_csv.c
@@ -1,198 +0,0 @@-#include "process_csv.h"-#include <stdlib.h>-#include <stdio.h>--/*- * compile with clang -O3- * in cabal use - *   cc-options: -O3- *- * Produce an array of field delimiter indices- * Fields can be delimited by commas and newlines- * TODO: allow the user to provide a custom delimiter- * character to replace commas.- * This should work with UTF-8, so long as the delimiter- * character is a single byte.- *- * Delimiters can be escaped inside of quotes. Quotes- * can also be placed inside quotes by double quoting.- * For the purposes of this parser we can ignore double- * quotes inside quotes, thereby treating the first quote- * as the closing of the string and the next one the- * immediate opening of a new one- * - * We can find the quoted regions by first finding- * the positions of the quotes (cmpeq and then movemask)- * and then using the carryless multiplication operation- * to know the regions that are quoted. We can then simply- * and the inverse of the quotemask to exclude commas and- * newlines inside quotes- *- */--// if the character is found at a particular-// position in the array of bytes, the-// corresponding bit in the returned uint64_t should-// be turned on.-// Example: searching for commas in-// input:  one, two, three-// result: 000100001000000-#ifdef HAS_SIMD_CSV-static uint64_t find_character_in_chunk(uint8_t *in, uint8_t c) {-#ifdef USE_AVX2-  // AVX2 implementation: load two 32-byte chunks-  __m256i v0 = _mm256_loadu_si256((const __m256i *)(in));-  __m256i v1 = _mm256_loadu_si256((const __m256i *)(in + 32));-  __m256i b = _mm256_set1_epi8((char)c);-  __m256i m0 = _mm256_cmpeq_epi8(v0, b);-  __m256i m1 = _mm256_cmpeq_epi8(v1, b);-  uint32_t lo = (uint32_t)_mm256_movemask_epi8(m0);-  uint32_t hi = (uint32_t)_mm256_movemask_epi8(m1);-  return ((uint64_t)hi << 32) | (uint64_t)lo;-#else // USE_NEON-  // ARM NEON implementation: load 64 bytes deinterleaved-  uint8x16x4_t src = vld4q_u8(in);-  uint8x16_t mask = vmovq_n_u8(c);-  uint8x16_t cmp0 = vceqq_u8(src.val[0], mask);-  uint8x16_t cmp1 = vceqq_u8(src.val[1], mask);-  uint8x16_t cmp2 = vceqq_u8(src.val[2], mask);-  uint8x16_t cmp3 = vceqq_u8(src.val[3], mask);--  // For an explanation of how to do movemask in-  // NEON, see: https://branchfree.org/2019/04/01/fitting-my-head-through-the-arm-holes-or-two-sequences-to-substitute-for-the-missing-pmovmskb-instruction-on-arm-neon/-  // The specific implementation below is owed to the -  // user 'aqrit' in a comment on the blog above-  // There's also https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon-  // -  // The input to the move mask must be de-interleaved.-  // That is, for a string after vceqq_u8  we have,-  // cmp0 = aaaaaaaa / eeeeeeee / ...-  // cmp1 = bbbbbbbb / ffffffff / ...-  // cmp2 = cccccccc / gggggggg / ...-  // cmp3 = dddddddd / hhhhhhhh / ...-  // Luckily vld4q_u8 does this for us. Now we want-  // to interleave this into a 64bit integer with bits-  // abcdefgh...-  // cmp0 holds bits for positions-  // 0,4,8,..., cmp1 holds bits for 1,5,9,..., and so on.-  // So to bring together the bits for different positions-  // we right shift and combine with vsriq_n_u8-  //-  // vsriq_n_u8 shifts each byte of the first operand -  // right by n bits, and combines it with the bits of-  // the second operand.-  // example:-  // uint8_t mask = 0xFF >> n; // if n = 1, 0111 1111-  // uint8_t shifted = operand1 >> n // 0bbb, bbbb-  // operand1 = (operand2 & (!mask)) | shifted-  // (aaaa aaaa & 1000 0000) | 0bbb bbbb = abbb bbbb-  // -  // So we first bring together the bits the first two-  // rows and the next two rows (so to speak)-  // t0 = abbbbbbb/efffffff/...-  uint8x16_t t0 = vsriq_n_u8(cmp1, cmp0, 1);-  // t1 = cddddddd/ghhhhhhh/...-  uint8x16_t t1 = vsriq_n_u8(cmp3, cmp2, 1);-  // Now we must combine each of our combined rows-  // so that we get back our column interleaved-  // t2 = abcddddd/efghhhhh/...-  uint8x16_t t2 = vsriq_n_u8(t1, t0, 2);-  // Then to get rid of the repeated bits in the upper half-  // t3 = abcdabcd/efghefgh/...-  uint8x16_t t3 = vsriq_n_u8(t2, t2, 4);-  // and now it's the relatively simple matter of getting-  // rid of half the bits. We combine our 8bit words into 16-  // bit words for this step, and then we shift right by 4-  // and turn the result into an 8 bit word-  // afterreinterpert: abcdabcdefghefgh/...-  // afterrightshift: 0000abcdabcdefgh/...-  // take the lower bits: abcdefgh/...-  uint8x8_t t4 = vshrn_n_u16(vreinterpretq_u16_u8(t3), 4);-  // Finally we recombine them into a 64 bit integer-  // (vreinterpret_u64_u8 here does uint8x8 -> uint64x1-  // and vget_lane_u64 does uint64x1 -> uint64) -  return vget_lane_u64(vreinterpret_u64_u8(t4), 0);-#endif-}-#endif--// I owe a debt to https://github.com/geofflangdale/simdcsv-// Let's go ahead and assume `in` will only ever get 64 bytes-// initial_quoted will be either all_ones ~0ULL or all_zeros 0ULL-#ifdef HAS_SIMD_CSV-static uint64_t parse_chunk(uint8_t *in, uint8_t separator, uint64_t *initial_quoted) {-  uint64_t quotebits = find_character_in_chunk(in, QUOTE_CHAR);-  // See https://wunkolo.github.io/post/2020/05/pclmulqdq-tricks/-  // Also, section 3.1.1 of Parsing Gigabytes of JSON per Second,-  // Geoff Langdale, Daniel Lemire, https://arxiv.org/pdf/1902.08318-#ifdef USE_AVX2-  // Use PCLMUL for carryless multiplication on x86-  __m128i a = _mm_set_epi64x(0, (int64_t)ALL_ONES_MASK);-  __m128i b = _mm_set_epi64x(0, (int64_t)quotebits);-  __m128i result = _mm_clmulepi64_si128(a, b, 0);-  uint64_t quotemask = (uint64_t)_mm_cvtsi128_si64(result);-#else // USE_NEON-  // Use vmull_p64 (PMULL) for carryless multiplication on ARM-  // Requires ARM crypto extensions (compile: __ARM_FEATURE_AES, runtime: pmull flag)-  uint64_t quotemask = vmull_p64(ALL_ONES_MASK, quotebits);-#endif-  quotemask ^= (*initial_quoted);-  // Find out if the chunk ends in a quoted region by looking-  // at the last bit-  (*initial_quoted) = (uint64_t)((int64_t)quotemask >> 63);--  uint64_t commabits = find_character_in_chunk(in, separator);-  uint64_t newlinebits = find_character_in_chunk(in, NEWLINE_CHAR);--  uint64_t delimiter_bits = (commabits | newlinebits) & ~quotemask;-  return delimiter_bits;-}-#endif--#ifdef HAS_SIMD_CSV-static size_t find_one_indices(size_t start_index, uint64_t bits, size_t *indices, size_t *base) {-  size_t position = 0;-  uint64_t bitset = bits;-  while (bitset != 0) {-    // temp only has the least significant bit of-    // bitset turned on.-    // In twos complement: 0 - x = ~ x + 1-    uint64_t temp = bitset & -bitset;-    // count trailing zeros-    size_t r = __builtin_ctzll(bitset);-    indices[(*base) + position] = start_index + r;-    position++;--    bitset ^= temp;-  }-  *base += position;-  return position;-}-#endif--size_t get_delimiter_indices(uint8_t *buf, size_t len, uint8_t separator, size_t *indices) {-  // Recall we padded our file with 64 empty bytes.-  // So if, for example, we had a file of 187 bytes-  // We pad it with zeros and so we have 251 bytes-  // The chunks we have are ptr + 0, ptr + 64, and pt-  // (we don't do ptr + 192 since we do len - 64-  // below. This way, in ptr + 128 we have 59 bytes of-  // actual data and 5 bytes of zeros. If we didn't do this-  // we'd be reading past the end of file on the last row-#ifdef HAS_SIMD_CSV-  size_t unpaddedLen = len < 64 ? 0 : len - 64;-  uint64_t initial_quoted = 0ULL;-  size_t base = 0;-  for (size_t i = 0; i < unpaddedLen; i += 64) {-    uint8_t *in = buf + i;-    uint64_t delimiter_bits = parse_chunk(in, separator, &initial_quoted);-    find_one_indices(i, delimiter_bits, indices, &base);-  }-  return base;-#else-  // SIMD not available or carryless multiplication not supported.-  // Signal fallback to Haskell implementation.-  (void)buf; (void)len; (void)indices;-  return (size_t)-1;-#endif-}
− cbits/process_csv.h
@@ -1,33 +0,0 @@-#ifndef PROCESS_CSV-#define PROCESS_CSV--#include <stddef.h>-#include <stdint.h>--// Define feature macros for SIMD support with carryless multiplication-// We need both SIMD instructions AND carryless multiplication for the CSV parser-#if defined(__AVX2__) && defined(__PCLMUL__)-  #define HAS_SIMD_CSV 1-  #define USE_AVX2 1-  #include <immintrin.h>-  #include <wmmintrin.h>-#elif defined(__ARM_NEON) && (defined(__ARM_FEATURE_AES) || defined(__ARM_FEATURE_CRYPTO))-  // Note: __ARM_FEATURE_CRYPTO is deprecated; prefer __ARM_FEATURE_AES-  // We need polynomial multiply (vmull_p64/PMULL) for carryless multiplication-  // Runtime check: 'pmull' flag in /proc/cpuinfo on Linux-  // We support both macros for compatibility with older compilers-  #define HAS_SIMD_CSV 1-  #define USE_NEON 1-  #include <arm_neon.h>-#endif--// CSV parsing constants-#define COMMA_CHAR 0x2C-#define NEWLINE_CHAR 0x0A-#define QUOTE_CHAR 0x22-#define ALL_ONES_MASK ~0ULL-#define ALL_ZEROS_MASK 0ULL--size_t get_delimiter_indices(uint8_t *buf, size_t len, uint8_t separator, size_t* indices);--#endif
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               dataframe-version:            1.0.0.1+version:            1.1.0.0  synopsis: A fast, safe, and intuitive DataFrame library. @@ -16,10 +16,12 @@ category: Data tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2 extra-doc-files: CHANGELOG.md README.md-extra-source-files: cbits/process_csv.h-                    cbits/arrow_abi.h+extra-source-files: cbits/arrow_abi.h                     cbits/dataframe_arrow.h                     cbits/rts_init.c+                    tests/data/typing/texts.txt+                    tests/data/typing/texts_with_empties.txt+                    tests/data/typing/texts_with_empties_and_nullish.txt                     data/titanic/*.csv                     data/sharded/*.parquet                     tests/data/*.csv@@ -82,7 +84,6 @@                     DataFrame.Display.Terminal.Plot,                     DataFrame.IO.CSV,                     DataFrame.IO.JSON,-                    DataFrame.IO.Unstable.CSV,                     DataFrame.IO.Parquet,                     DataFrame.IO.Parquet.Binary,                     DataFrame.IO.Parquet.Dictionary,@@ -140,8 +141,6 @@                       vector-algorithms ^>= 0.9,                       zlib >= 0.5 && < 1,                       zstd >= 0.1.2.0 && < 0.2,-                      mmap >= 0.5.8 && < 0.6,-                      parallel >= 3.2.2.0 && < 5,                       stm >= 2.5 && < 3,                       filepath >= 1.4 && < 2,                       Glob >= 0.10 && < 1,@@ -149,10 +148,6 @@                       streamly-core >= 0.2.3 && < 0.4,                       streamly-bytestring >= 0.2.0 && < 0.4     hs-source-dirs:   src-    c-sources:        cbits/process_csv.c-    include-dirs:     cbits-    includes:         process_csv.h-    install-includes: process_csv.h     default-language: Haskell2010  foreign-library dataframe-arrow@@ -163,7 +158,7 @@                       DataFrame.IR     build-depends:         base        >= 4   && < 5,-        dataframe   ^>= 1,+        dataframe   ^>= 1.1,         text        >= 2.0 && < 3,         aeson       >= 0.11 && < 3,         bytestring  >= 0.11 && < 0.13,@@ -180,7 +175,7 @@     import: warnings     main-is: Benchmark.hs     build-depends:    base >= 4 && < 5,-                      dataframe ^>= 1,+                      dataframe ^>= 1.1,                       random >= 1 && < 2,                       time >= 1.12 && < 2,                       vector ^>= 0.13,@@ -192,7 +187,7 @@     import: warnings     main-is: Synthesis.hs     build-depends:    base >= 4 && < 5,-                      dataframe ^>= 1,+                      dataframe ^>= 1.1,                       random >= 1 && < 2,                       text >= 2.0 && < 3     hs-source-dirs:   app@@ -219,7 +214,7 @@     build-depends:    base >= 4 && < 5,                       bytestring >= 0.11 && < 0.13,                       containers >= 0.6.7 && < 0.9,-                      dataframe ^>= 1,+                      dataframe ^>= 1.1,                       directory >= 1.3.0.0 && < 2,                       random >= 1 && < 2,                       text >= 2.0 && < 3,@@ -236,7 +231,7 @@     build-depends: base >= 4 && < 5,                    criterion >= 1 && < 2,                    process >= 1.6 && < 2,-                   dataframe ^>= 1,+                   dataframe ^>= 1.1,                    random >= 1 && < 2,     default-language: Haskell2010     ghc-options:@@ -266,6 +261,7 @@                    Operations.Nullable,                    Operations.Provenance,                    Operations.ReadCsv,+                   Operations.WriteCsv,                    Operations.Shuffle,                    Operations.Sort,                    Operations.Subset,@@ -274,13 +270,12 @@                    Operations.Typing,                    LazyParquet,                    Parquet,+                   ParquetTestData,                    Properties,                    Monad     build-depends:  base >= 4 && < 5,                     bytestring >= 0.11 && < 0.13,-                    dataframe ^>= 1,-                    bytestring >= 0.11 && < 0.13,-                    directory >= 1.3.0.0 && < 2,+                    dataframe ^>= 1.1,                     HUnit ^>= 1.6,                     QuickCheck >= 2 && < 3,                     random-shuffle >= 0.0.4 && < 1,
ffi/DataFrame/IO/Arrow.hs view
@@ -25,7 +25,7 @@ import qualified DataFrame.Internal.Column as DI  import Control.Monad (foldM_, forM, join, void, when, zipWithM_)-import Data.Bits (setBit, testBit)+import Data.Bits (popCount, setBit, testBit) import Data.Int (Int32, Int64) import Data.Maybe (fromMaybe, isNothing) import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))@@ -188,25 +188,28 @@     p `at` _arrayPrivateData $ castStablePtrToPtr cleanup     return p -buildBitmap :: V.Vector (Maybe a) -> IO (Ptr Word8, Int)-buildBitmap vec = do-    let n = V.length vec-        bitmapBytes = max 1 ((n + 7) `div` 8)-        nullCount = V.length (V.filter isNothing vec)-    bitmapPtr <- mallocBytes bitmapBytes :: IO (Ptr Word8)-    mapM_ (\i -> pokeElemOff bitmapPtr i (0 :: Word8)) [0 .. bitmapBytes - 1]-    V.iforM_ vec $ \i mv ->-        case mv of-            Nothing -> return ()-            Just _ -> do-                let byteIdx = i `div` 8-                    bitIdx = i `mod` 8-                b <- peekElemOff bitmapPtr byteIdx-                pokeElemOff bitmapPtr byteIdx (setBit b bitIdx)+{- | Allocate an Arrow-format validity bitmap from a 'DI.Bitmap'.+Returns (ptr, nullCount). Caller must 'free' the pointer.+-}+bitmapToPtr :: Int -> DI.Bitmap -> IO (Ptr Word8, Int)+bitmapToPtr n bm = do+    let numBytes = max 1 ((n + 7) `div` 8)+        validCount = VU.foldl' (\acc b -> acc + popCount b) 0 bm+        nullCount = n - validCount+    bitmapPtr <- mallocBytes numBytes :: IO (Ptr Word8)+    VU.imapM_ (pokeElemOff bitmapPtr) bm+    when (VU.length bm < numBytes) $+        mapM_+            (\i -> pokeElemOff bitmapPtr i (0 :: Word8))+            [VU.length bm .. numBytes - 1]     return (bitmapPtr, nullCount) +-- | Read an Arrow validity bitmap into a 'DI.Bitmap'.+readArrowBitmap :: Ptr Word8 -> Int -> IO DI.Bitmap+readArrowBitmap bitmapPtr n = VU.generateM ((n + 7) `div` 8) (peekElemOff bitmapPtr)+ columnToArrow :: T.Text -> Column -> IO (Ptr ArrowSchema, Ptr ArrowArray)-columnToArrow colName (UnboxedColumn (vec :: VU.Vector a))+columnToArrow colName (UnboxedColumn _ (vec :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do         let n = VU.length vec         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)@@ -214,7 +217,7 @@         sPtr <- makeLeafSchema "l" colName         aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (UnboxedColumn (vec :: VU.Vector a))+columnToArrow colName (UnboxedColumn _ (vec :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do         let n = VU.length vec         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)@@ -222,7 +225,7 @@         sPtr <- makeLeafSchema "g" colName         aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (BoxedColumn (vec :: V.Vector a))+columnToArrow colName (BoxedColumn _ (vec :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do         let n = V.length vec             bss = map TE.encodeUtf8 (V.toList vec)@@ -250,7 +253,7 @@                 [nullPtr, castPtr offPtr, castPtr charsPtr]                 (free offPtr >> free charsPtr)         return (sPtr, aPtr)-columnToArrow colName (BoxedColumn (vec :: V.Vector a))+columnToArrow colName (BoxedColumn _ (vec :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do         let n = V.length vec         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)@@ -258,7 +261,7 @@         sPtr <- makeLeafSchema "g" colName         aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (BoxedColumn (vec :: V.Vector a))+columnToArrow colName (BoxedColumn _ (vec :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do         let n = V.length vec         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)@@ -266,16 +269,13 @@         sPtr <- makeLeafSchema "l" colName         aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+-- Nullable Int (UnboxedColumn with bitmap)+columnToArrow colName (UnboxedColumn (Just bm) (vec :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do-        let n = V.length vec-        (bitmapPtr, nullCount) <- buildBitmap vec+        let n = VU.length vec+        (bitmapPtr, nullCount) <- bitmapToPtr n bm         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)-        V.imapM_-            ( \i mv ->-                pokeElemOff dataPtr i (fromIntegral (fromMaybe 0 mv) :: Int64)-            )-            vec+        VU.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v :: Int64)) vec         sPtr <- makeLeafSchema "l" colName         aPtr <-             makeLeafArray@@ -284,16 +284,13 @@                 [castPtr bitmapPtr, castPtr dataPtr]                 (free bitmapPtr >> free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+-- Nullable Double (UnboxedColumn with bitmap)+columnToArrow colName (UnboxedColumn (Just bm) (vec :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do-        let n = V.length vec-        (bitmapPtr, nullCount) <- buildBitmap vec+        let n = VU.length vec+        (bitmapPtr, nullCount) <- bitmapToPtr n bm         dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)-        V.imapM_-            ( \i mv ->-                pokeElemOff dataPtr i (maybe 0.0 realToFrac mv :: Double)-            )-            vec+        VU.imapM_ (\i v -> pokeElemOff dataPtr i (realToFrac v :: Double)) vec         sPtr <- makeLeafSchema "g" colName         aPtr <-             makeLeafArray@@ -302,14 +299,18 @@                 [castPtr bitmapPtr, castPtr dataPtr]                 (free bitmapPtr >> free dataPtr)         return (sPtr, aPtr)-columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+-- Nullable Text (BoxedColumn with bitmap)+columnToArrow colName (BoxedColumn (Just bm) (vec :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do         let n = V.length vec-            texts = V.toList vec-            bss = map (maybe BS.empty TE.encodeUtf8) texts+            -- For null positions, use empty BS (null placeholder in vec is never evaluated)+            bss =+                map+                    (\i -> if DI.bitmapTestBit bm i then TE.encodeUtf8 (vec V.! i) else BS.empty)+                    [0 .. n - 1]             cumOff = scanl (+) 0 (map BS.length bss)             total = last cumOff-        (bitmapPtr, nullCount) <- buildBitmap vec+        (bitmapPtr, nullCount) <- bitmapToPtr n bm         offPtr <- mallocArray (n + 1) :: IO (Ptr Int32)         zipWithM_             (\i o -> pokeElemOff offPtr i (fromIntegral o :: Int32))@@ -353,7 +354,7 @@      let nRows = case colsInOrder of             [] -> 0-            _ -> DI.columnLength (snd (head colsInOrder))+            (_, col) : _ -> DI.columnLength col     topSchema <- mallocBytes arrowSchemaSize     fmtStr <- newCString "+s"     nameStr <- newCString ""@@ -463,19 +464,12 @@     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then do-                        v <- peekElemOff dataPtr i-                        return (Just (fromIntegral v :: Int))-                    else return Nothing-            return $ OptionalColumn (vec :: V.Vector (Maybe Int))+            bm <- readArrowBitmap bitmapPtr n+            vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int64)+            return $ UnboxedColumn (Just bm) (vec :: VU.Vector Int)         else do-            vec <- VU.generateM n $ \i -> do-                v <- peekElemOff dataPtr i-                return (fromIntegral v :: Int)-            return $ UnboxedColumn (vec :: VU.Vector Int)+            vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int64)+            return $ UnboxedColumn Nothing (vec :: VU.Vector Int)  readInt32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column readInt32Col n nullCnt bufArr = do@@ -485,19 +479,12 @@     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then do-                        v <- peekElemOff dataPtr i-                        return (Just (fromIntegral v :: Int))-                    else return Nothing-            return $ OptionalColumn (vec :: V.Vector (Maybe Int))+            bm <- readArrowBitmap bitmapPtr n+            vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int32)+            return $ UnboxedColumn (Just bm) (vec :: VU.Vector Int)         else do-            vec <- VU.generateM n $ \i -> do-                v <- peekElemOff dataPtr i-                return (fromIntegral v :: Int)-            return $ UnboxedColumn (vec :: VU.Vector Int)+            vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int32)+            return $ UnboxedColumn Nothing (vec :: VU.Vector Int)  readFloat64Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column readFloat64Col n nullCnt bufArr = do@@ -507,15 +494,12 @@     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then Just <$> (peekElemOff dataPtr i :: IO Double)-                    else return Nothing-            return $ OptionalColumn (vec :: V.Vector (Maybe Double))+            bm <- readArrowBitmap bitmapPtr n+            vec <- VU.generateM n (peekElemOff dataPtr)+            return $ UnboxedColumn (Just bm) (vec :: VU.Vector Double)         else do             vec <- VU.generateM n (peekElemOff dataPtr)-            return $ UnboxedColumn (vec :: VU.Vector Double)+            return $ UnboxedColumn Nothing (vec :: VU.Vector Double)  readFloat32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column readFloat32Col n nullCnt bufArr = do@@ -525,19 +509,12 @@     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then do-                        v <- peekElemOff dataPtr i-                        return (Just (realToFrac v :: Double))-                    else return Nothing-            return $ OptionalColumn (vec :: V.Vector (Maybe Double))+            bm <- readArrowBitmap bitmapPtr n+            vec <- VU.generateM n $ \i -> fmap (realToFrac :: Float -> Double) (peekElemOff dataPtr i)+            return $ UnboxedColumn (Just bm) (vec :: VU.Vector Double)         else do-            vec <- VU.generateM n $ \i -> do-                v <- peekElemOff dataPtr i-                return (realToFrac v :: Double)-            return $ UnboxedColumn (vec :: VU.Vector Double)+            vec <- VU.generateM n $ \i -> fmap (realToFrac :: Float -> Double) (peekElemOff dataPtr i)+            return $ UnboxedColumn Nothing (vec :: VU.Vector Double)  -- | Read a large_string (format "U") column with int64 offsets. readLargeUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column@@ -547,30 +524,20 @@     charVoid <- peekElemOff bufArr 2     let offsetPtr = castPtr offsetVoid :: Ptr Int64         charPtr = castPtr charVoid :: Ptr Word8+    let readText i = do+            start <- fromIntegral <$> peekElemOff offsetPtr i+            end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+            TE.decodeUtf8+                <$> BS.packCStringLen (castPtr (charPtr `plusPtr` start), end - start)     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then do-                        start <- fromIntegral <$> peekElemOff offsetPtr i-                        end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)-                        bs <--                            BS.packCStringLen-                                (castPtr (charPtr `plusPtr` start), end - start)-                        return $ Just (TE.decodeUtf8 bs)-                    else return Nothing-            return $ OptionalColumn vec+            bm <- readArrowBitmap bitmapPtr n+            vec <- V.generateM n readText+            return $ BoxedColumn (Just bm) vec         else do-            vec <- V.generateM n $ \i -> do-                start <- fromIntegral <$> peekElemOff offsetPtr i-                end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)-                bs <--                    BS.packCStringLen-                        (castPtr (charPtr `plusPtr` start), end - start)-                return $ TE.decodeUtf8 bs-            return $ BoxedColumn vec+            vec <- V.generateM n readText+            return $ BoxedColumn Nothing vec  -- | Read a utf8 (format "u") column with int32 offsets. readUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column@@ -580,27 +547,17 @@     charVoid <- peekElemOff bufArr 2     let offsetPtr = castPtr offsetVoid :: Ptr Int32         charPtr = castPtr charVoid :: Ptr Word8+        readText i = do+            start <- fromIntegral <$> peekElemOff offsetPtr i+            end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+            TE.decodeUtf8+                <$> BS.packCStringLen (castPtr (charPtr `plusPtr` start), end - start)     if nullCnt > 0         then do             let bitmapPtr = castPtr bitmapVoid :: Ptr Word8-            vec <- V.generateM n $ \i -> do-                valid <- bitmapIsSet bitmapPtr i-                if valid-                    then do-                        start <- fromIntegral <$> peekElemOff offsetPtr i-                        end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)-                        bs <--                            BS.packCStringLen-                                (castPtr (charPtr `plusPtr` start), end - start)-                        return $ Just (TE.decodeUtf8 bs)-                    else return Nothing-            return $ OptionalColumn vec+            bm <- readArrowBitmap bitmapPtr n+            vec <- V.generateM n readText+            return $ BoxedColumn (Just bm) vec         else do-            vec <- V.generateM n $ \i -> do-                start <- fromIntegral <$> peekElemOff offsetPtr i-                end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)-                bs <--                    BS.packCStringLen-                        (castPtr (charPtr `plusPtr` start), end - start)-                return $ TE.decodeUtf8 bs-            return $ BoxedColumn vec+            vec <- V.generateM n readText+            return $ BoxedColumn Nothing vec
ffi/DataFrame/IR.hs view
@@ -113,12 +113,14 @@  -- | Build a SortOrder from a column's runtime type. mkSortOrder :: Bool -> T.Text -> Column -> SortOrder-mkSortOrder True n (UnboxedColumn (_ :: VU.Vector a)) = Asc (Col @a n)-mkSortOrder False n (UnboxedColumn (_ :: VU.Vector a)) = Desc (Col @a n)-mkSortOrder True n (BoxedColumn (_ :: V.Vector a)) = Asc (Col @a n)-mkSortOrder False n (BoxedColumn (_ :: V.Vector a)) = Desc (Col @a n)-mkSortOrder True n (OptionalColumn (_ :: V.Vector (Maybe a))) = Asc (Col @(Maybe a) n)-mkSortOrder False n (OptionalColumn (_ :: V.Vector (Maybe a))) = Desc (Col @(Maybe a) n)+mkSortOrder True n (UnboxedColumn Nothing (_ :: VU.Vector a)) = Asc (Col @a n)+mkSortOrder False n (UnboxedColumn Nothing (_ :: VU.Vector a)) = Desc (Col @a n)+mkSortOrder True n (UnboxedColumn (Just _) (_ :: VU.Vector a)) = Asc (Col @(Maybe a) n)+mkSortOrder False n (UnboxedColumn (Just _) (_ :: VU.Vector a)) = Desc (Col @(Maybe a) n)+mkSortOrder True n (BoxedColumn Nothing (_ :: V.Vector a)) = Asc (Col @a n)+mkSortOrder False n (BoxedColumn Nothing (_ :: V.Vector a)) = Desc (Col @a n)+mkSortOrder True n (BoxedColumn (Just _) (_ :: V.Vector a)) = Asc (Col @(Maybe a) n)+mkSortOrder False n (BoxedColumn (Just _) (_ :: V.Vector a)) = Desc (Col @(Maybe a) n)  -- | Dispatch aggregation by fn name and runtime column type. buildNamedExpr :: DataFrame -> AggSpec -> IO NamedExpr@@ -133,22 +135,28 @@                     "DataFrame.IR: unknown aggregation '" ++ T.unpack other ++ "'"  countExpr :: T.Text -> T.Text -> Column -> IO NamedExpr-countExpr name colName (UnboxedColumn (_ :: VU.Vector a)) = return $ name .= count (Col @a colName)-countExpr name colName (BoxedColumn (_ :: V.Vector a)) = return $ name .= count (Col @a colName)-countExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a))) = return $ name .= count (Col @(Maybe a) colName)+countExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a)) = return $ name .= count (Col @(Maybe a) colName)+countExpr name colName (BoxedColumn Nothing (_ :: V.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a)) = return $ name .= count (Col @(Maybe a) colName)  sumExpr :: T.Text -> T.Text -> Column -> IO NamedExpr-sumExpr name colName (UnboxedColumn (_ :: VU.Vector a))+sumExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =         return $ name .= Functions.sum (Col @Int colName)     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =         return $ name .= Functions.sum (Col @Double colName)-sumExpr name colName (BoxedColumn (_ :: V.Vector a))+sumExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= sumMaybe (Col @(Maybe Int) colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= sumMaybe (Col @(Maybe Double) colName)+sumExpr name colName (BoxedColumn Nothing (_ :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =         return $ name .= Functions.sum (Col @Int colName)     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =         return $ name .= Functions.sum (Col @Double colName)-sumExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a)))+sumExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =         return $ name .= sumMaybe (Col @(Maybe Int) colName)     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =@@ -159,15 +167,20 @@             "DataFrame.IR: sum: unsupported column type for '" ++ T.unpack colName ++ "'"  meanExpr :: T.Text -> T.Text -> Column -> IO NamedExpr-meanExpr name colName (UnboxedColumn (_ :: VU.Vector a))+meanExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =         return $ name .= mean (Col @Int colName)     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =         return $ name .= mean (Col @Double colName)-meanExpr name colName (BoxedColumn (_ :: V.Vector a))+meanExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= meanMaybe (Col @(Maybe Double) colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= meanMaybe (Col @(Maybe Int) colName)+meanExpr name colName (BoxedColumn Nothing (_ :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =         return $ name .= mean (Col @Double colName)-meanExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a)))+meanExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a))     | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =         return $ name .= meanMaybe (Col @(Maybe Double) colName)     | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =
src/DataFrame.hs view
@@ -216,7 +216,6 @@      -- * I/O     module CSV,-    module UnstableCSV,     module Parquet,      -- * Type conversion@@ -266,12 +265,6 @@     readParquetFilesWithOpts,     readParquetWithOpts,  )-import DataFrame.IO.Unstable.CSV as UnstableCSV (-    fastReadCsvUnstable,-    fastReadTsvUnstable,-    readCsvUnstable,-    readTsvUnstable,- ) import DataFrame.Internal.Column as Column (     Column,     fromList,@@ -288,8 +281,10 @@     GroupedDataFrame,     empty,     null,+    toCsv,     toMarkdown,     toMarkdown',+    toSeparated,  ) import DataFrame.Internal.Expression as Expression (Expr, prettyPrint) import DataFrame.Internal.Row as Row (
src/DataFrame/DecisionTree.hs view
@@ -638,18 +638,24 @@ numericCols df = concatMap extract (columnNames df)   where     extract col = case unsafeGetColumn col df of-        UnboxedColumn (_ :: VU.Vector b) ->+        UnboxedColumn Nothing (_ :: VU.Vector b) ->             case testEquality (typeRep @b) (typeRep @Double) of                 Just Refl -> [NDouble (Col col)]                 Nothing -> case sIntegral @b of                     STrue -> [NDouble (F.toDouble (Col @b col))]                     SFalse -> []-        OptionalColumn (_ :: V.Vector (Maybe b)) ->+        BoxedColumn (Just _) (_ :: V.Vector b) ->             case testEquality (typeRep @b) (typeRep @Double) of                 Just Refl -> [NMaybeDouble (Col @(Maybe b) col)]                 Nothing -> case sIntegral @b of                     STrue -> [NMaybeDouble (F.whenPresent (realToFrac @b @Double) (Col @(Maybe b) col))]                     SFalse -> []+        UnboxedColumn (Just _) (_ :: VU.Vector b) ->+            case testEquality (typeRep @b) (typeRep @Double) of+                Just Refl -> [NMaybeDouble (Col @(Maybe b) col)]+                Nothing -> case sIntegral @b of+                    STrue -> [NMaybeDouble (F.whenPresent (realToFrac @b @Double) (Col @(Maybe b) col))]+                    SFalse -> []         _ -> []  numericExprs ::@@ -697,18 +703,18 @@     let         genConds :: T.Text -> [Expr Bool]         genConds colName = case unsafeGetColumn colName df of-            (BoxedColumn (col :: V.Vector a)) ->+            (BoxedColumn Nothing (col :: V.Vector a)) ->                 let ps = map (Lit . (`percentileOrd'` col)) [1, 25, 75, 99]                  in map (F.lift2 (==) (Col @a colName)) ps-            (OptionalColumn (col :: V.Vector (Maybe a))) -> case sFloating @a of+            (BoxedColumn (Just _) (col :: V.Vector a)) -> case sFloating @a of                 STrue -> [] -- handled by numericCols / numericExprs                 SFalse -> case sIntegral @a of                     STrue -> [] -- handled by numericCols / numericExprs                     SFalse ->                         map-                            (F.lift2 (==) (Col @(Maybe a) colName) . Lit . (`percentileOrd'` col))+                            (F.lift2 (==) (Col @(Maybe a) colName) . Lit . Just . (`percentileOrd'` col))                             [1, 25, 75, 99]-            (UnboxedColumn (_ :: VU.Vector a)) -> []+            (UnboxedColumn _ (_ :: VU.Vector a)) -> []          columnConds =             concatMap@@ -724,13 +730,15 @@                 ]           where             colConds (!l, !r) = case (unsafeGetColumn l df, unsafeGetColumn r df) of-                (BoxedColumn (col1 :: V.Vector a), BoxedColumn (_ :: V.Vector b)) ->-                    case testEquality (typeRep @a) (typeRep @b) of-                        Nothing -> []-                        Just Refl -> [F.lift2 (==) (Col @a l) (Col @a r)]-                (UnboxedColumn (_ :: VU.Vector a), UnboxedColumn (_ :: VU.Vector b)) -> []-                ( OptionalColumn (_ :: V.Vector (Maybe a))-                    , OptionalColumn (_ :: V.Vector (Maybe b))+                ( BoxedColumn Nothing (col1 :: V.Vector a)+                    , BoxedColumn Nothing (_ :: V.Vector b)+                    ) ->+                        case testEquality (typeRep @a) (typeRep @b) of+                            Nothing -> []+                            Just Refl -> [F.lift2 (==) (Col @a l) (Col @a r)]+                (UnboxedColumn _ (_ :: VU.Vector a), UnboxedColumn _ (_ :: VU.Vector b)) -> []+                ( BoxedColumn (Just _) (_ :: V.Vector a)+                    , BoxedColumn (Just _) (_ :: V.Vector b)                     ) -> case testEquality (typeRep @a) (typeRep @b) of                         Nothing -> []                         Just Refl -> case testEquality (typeRep @a) (typeRep @T.Text) of
src/DataFrame/Display/Terminal/Plot.hs view
@@ -339,15 +339,12 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn vec ->+                    BoxedColumn _ vec ->                         let counts = countValues vec                          in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]-                    UnboxedColumn vec ->+                    UnboxedColumn _ vec ->                         let counts = countValuesUnboxed vec                          in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]-                    OptionalColumn vec ->-                        let counts = countValues vec-                         in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]   where     countValues :: (Ord a, Show a) => V.Vector a -> [(a, Int)]     countValues vec = M.toList $ V.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec@@ -365,9 +362,8 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn vec -> V.toList $ V.map (T.pack . show) vec-                    UnboxedColumn vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)-                    OptionalColumn vec -> V.toList $ V.map (T.pack . show) vec+                    BoxedColumn _ vec -> V.toList $ V.map (T.pack . show) vec+                    UnboxedColumn _ vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)  extractNumericColumn :: (HasCallStack) => T.Text -> DataFrame -> [Double] extractNumericColumn colName df =@@ -376,9 +372,8 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn vec -> vectorToDoubles vec-                    UnboxedColumn vec -> unboxedVectorToDoubles vec-                    _ -> []+                    BoxedColumn _ vec -> vectorToDoubles vec+                    UnboxedColumn _ vec -> unboxedVectorToDoubles vec  vectorToDoubles :: forall a. (Columnable a, Show a) => V.Vector a -> [Double] vectorToDoubles vec =
src/DataFrame/Display/Web/Plot.hs view
@@ -850,13 +850,10 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn (vec :: V.Vector a) -> case testEquality (typeRep @a) (typeRep @T.Text) of+                    BoxedColumn _ (vec :: V.Vector a) -> case testEquality (typeRep @a) (typeRep @T.Text) of                         Just Refl -> V.toList vec                         Nothing -> V.toList $ V.map (T.pack . show) vec-                    UnboxedColumn vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)-                    OptionalColumn (vec :: V.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @T.Text) of-                        Nothing -> V.toList $ V.map (T.pack . show) vec-                        Just Refl -> V.toList $ V.map (maybe "Nothing" ("Just " <>)) vec+                    UnboxedColumn _ vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)  extractNumericColumn :: (HasCallStack) => T.Text -> DataFrame -> [Double] extractNumericColumn colName df =@@ -865,9 +862,8 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn vec -> vectorToDoubles vec-                    UnboxedColumn vec -> unboxedVectorToDoubles vec-                    _ -> []+                    BoxedColumn _ vec -> vectorToDoubles vec+                    UnboxedColumn _ vec -> unboxedVectorToDoubles vec  vectorToDoubles :: forall a. (Columnable a, Show a) => V.Vector a -> [Double] vectorToDoubles vec =@@ -898,21 +894,14 @@         Just idx ->             let col = columns df V.! idx              in case col of-                    BoxedColumn (vec :: V.Vector a) ->+                    BoxedColumn _ (vec :: V.Vector a) ->                         let counts = countValues vec                          in case testEquality (typeRep @a) (typeRep @T.Text) of                                 Nothing -> Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]                                 Just Refl -> Just [(k, fromIntegral v) | (k, v) <- counts]-                    UnboxedColumn vec ->+                    UnboxedColumn _ vec ->                         let counts = countValuesUnboxed vec                          in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]-                    OptionalColumn (vec :: V.Vector (Maybe a)) ->-                        let counts = countValues vec-                         in case testEquality (typeRep @a) (typeRep @T.Text) of-                                Nothing -> Just [((T.pack . show) k, fromIntegral v) | (k, v) <- counts]-                                Just Refl ->-                                    Just-                                        [(maybe "Nothing" ("Just " <>) k, fromIntegral v) | (k, v) <- counts]   where     countValues :: (Ord a, Show a) => V.Vector a -> [(a, Int)]     countValues vec = M.toList $ V.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec
src/DataFrame/Errors.hs view
@@ -11,6 +11,7 @@  import Control.Exception import Data.Array+import qualified Data.List as L import Data.Typeable (Typeable) import DataFrame.Display.Terminal.Colours import Type.Reflection (TypeRep)@@ -29,7 +30,7 @@         TypeErrorContext a b ->         DataFrameException     AggregatedAndNonAggregatedException :: T.Text -> T.Text -> DataFrameException-    ColumnNotFoundException :: T.Text -> T.Text -> [T.Text] -> DataFrameException+    ColumnsNotFoundException :: [T.Text] -> T.Text -> [T.Text] -> DataFrameException     EmptyDataSetException :: T.Text -> DataFrameException     InternalException :: T.Text -> DataFrameException     NonColumnReferenceException :: T.Text -> DataFrameException@@ -51,7 +52,7 @@                 (errorColumnName context)                 (callingFunctionName context)                 errorString-    show (ColumnNotFoundException columnName callPoint availableColumns) = columnNotFound columnName callPoint availableColumns+    show (ColumnsNotFoundException columnNames callPoint availableColumns) = columnsNotFound columnNames callPoint availableColumns     show (EmptyDataSetException callPoint) = emptyDataSetError callPoint     show (WrongQuantileNumberException q) = wrongQuantileNumberError q     show (WrongQuantileIndexException qs q) = wrongQuantileIndexError qs q@@ -65,15 +66,46 @@             ++ T.unpack expr2  columnNotFound :: T.Text -> T.Text -> [T.Text] -> String-columnNotFound name callPoint columns =+columnNotFound missingColumn = columnsNotFound [missingColumn]++columnsNotFound :: [T.Text] -> T.Text -> [T.Text] -> String+columnsNotFound missingColumns callPoint availableColumns =     red "\n\n[ERROR] "-        ++ "Column not found: "-        ++ T.unpack name+        ++ missingColumnsLabel missingColumns+        ++ ": "+        ++ T.unpack (T.intercalate ", " missingColumns)         ++ " for operation "         ++ T.unpack callPoint-        ++ "\n\tDid you mean "-        ++ T.unpack (guessColumnName name columns)-        ++ "?\n\n"+        ++ formatSuggestions missingColumns availableColumns+        ++ "\n\n"+  where+    missingColumnsLabel [_] = "Column not found"+    missingColumnsLabel _ = "Columns not found"++    formatSuggestions [missingColumn] columns =+        case guessColumnName missingColumn columns of+            "" -> ""+            guessed ->+                "\n\tDid you mean "+                    ++ T.unpack guessed+                    ++ "?"+    formatSuggestions names columns =+        case traverse (`suggestColumnName` columns) names of+            Just guessedColumns+                | not (null guessedColumns) ->+                    "\n\tDid you mean "+                        ++ formatColumnSuggestions guessedColumns+                        ++ "?"+            _ -> ""++    suggestColumnName missingColumn columns = case guessColumnName missingColumn columns of+        "" -> Nothing+        guessed -> Just guessed++    formatColumnSuggestions guessedColumns =+        "["+            ++ L.intercalate ", " (map (show . T.unpack) guessedColumns)+            ++ "]"  typeMismatchError :: String -> String -> String typeMismatchError givenType expectedType =
src/DataFrame/Functions.hs view
@@ -628,7 +628,8 @@      in         fmap concat $ forM specs $ \(raw, nm, tyStr) -> do             ty <- typeFromString (words tyStr)-            trace (T.unpack (nm <> " :: Expr " <> T.pack tyStr)) pure ()+            let tyDisplay = if ' ' `elem` tyStr then "(" <> T.pack tyStr <> ")" else T.pack tyStr+            trace (T.unpack (nm <> " :: Expr " <> tyDisplay)) pure ()             let n = mkName (T.unpack nm)             sig <- sigD n [t|Expr $(pure ty)|]             val <- valD (varP n) (normalB [|col $(TH.lift raw)|]) []
src/DataFrame/IO/CSV.hs view
@@ -39,7 +39,7 @@ import Data.Type.Equality (TestEquality (testEquality)) import Data.Word (Word8) import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Internal.DataFrame (DataFrame (..), toSeparated) import DataFrame.Internal.Parsing import DataFrame.Internal.Schema import DataFrame.Operations.Typing@@ -220,7 +220,7 @@     ReadOptions         { headerSpec = UseFirstRow         , typeSpec = InferFromSample 100-        , safeRead = True+        , safeRead = False         , dateFormat = "%Y-%m-%d"         , columnSeparator = ','         , numColumns = Nothing@@ -389,30 +389,33 @@     vec <- freezePagedUnboxedVector gv     valid <- freezePagedUnboxedVector validRef     if VU.all (== 1) valid-        then return $! UnboxedColumn vec+        then return $! UnboxedColumn Nothing vec         else constructOptional vec valid freezeBuilderColumn (BuilderDouble gv validRef) = do     vec <- freezePagedUnboxedVector gv     valid <- freezePagedUnboxedVector validRef     if VU.all (== 1) valid-        then return $! UnboxedColumn vec+        then return $! UnboxedColumn Nothing vec         else constructOptional vec valid freezeBuilderColumn (BuilderText gv validRef) = do     vec <- freezePagedVector gv     valid <- freezePagedUnboxedVector validRef     if VU.all (== 1) valid-        then return $! BoxedColumn vec+        then return $! BoxedColumn Nothing vec         else constructOptionalBoxed vec valid freezeBuilderColumn (BuilderBS _ _) =     error         "freezeBuilderColumn: BuilderBS must be finalized via finalizeBuilderColumn"  finalizeBuilderColumn :: ReadOptions -> BuilderColumn -> IO Column-finalizeBuilderColumn opts (BuilderBS gv validRef) = do-    vec <- freezePagedVector gv-    valid <- freezePagedUnboxedVector validRef-    return $! inferColumnFromBS opts vec valid-finalizeBuilderColumn _ bc = freezeBuilderColumn bc+finalizeBuilderColumn opts bc = do+    col <- case bc of+        BuilderBS gv validRef -> do+            vec <- freezePagedVector gv+            valid <- freezePagedUnboxedVector validRef+            return $! inferColumnFromBS opts vec valid+        _ -> freezeBuilderColumn bc+    return $! if safeRead opts then ensureOptional col else col  inferColumnFromBS ::     ReadOptions -> V.Vector BS.ByteString -> VU.Vector Word8 -> Column@@ -523,23 +526,13 @@ constructOptional ::     (VU.Unbox a, Columnable a) => VU.Vector a -> VU.Vector Word8 -> IO Column constructOptional vec valid = do-    let size = VU.length vec-    mvec <- VM.new size-    forM_ [0 .. size - 1] $ \i ->-        if (valid VU.! i) == 0-            then VM.write mvec i Nothing-            else VM.write mvec i (Just (vec VU.! i))-    OptionalColumn <$> V.freeze mvec+    let bm = buildBitmapFromValid valid+    pure $ UnboxedColumn (Just bm) vec  constructOptionalBoxed :: V.Vector T.Text -> VU.Vector Word8 -> IO Column constructOptionalBoxed vec valid = do-    let size = V.length vec-    mvec <- VM.new size-    forM_ [0 .. size - 1] $ \i ->-        if (valid VU.! i) == 0-            then VM.write mvec i Nothing-            else VM.write mvec i (Just (vec V.! i))-    OptionalColumn <$> V.freeze mvec+    let bm = buildBitmapFromValid valid+    pure $ BoxedColumn (Just bm) vec  writeCsv :: FilePath -> DataFrame -> IO () writeCsv = writeSeparated ','@@ -554,61 +547,7 @@     FilePath ->     DataFrame ->     IO ()-writeSeparated c filepath df = withFile filepath WriteMode $ \handle -> do-    let (rows, _) = dataframeDimensions df-    let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df)))-    TIO.hPutStrLn handle (T.intercalate "," headers)-    forM_ [0 .. (rows - 1)] $ \i -> do-        let row = getRowAsText df i-        TIO.hPutStrLn handle (T.intercalate "," row)--getRowAsText :: DataFrame -> Int -> [T.Text]-getRowAsText df i = V.ifoldr go [] (columns df)-  where-    indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df))-    go k (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of-        Just e -> textRep : acc-          where-            textRep = case testEquality (typeRep @a) (typeRep @T.Text) of-                Just Refl -> e-                Nothing -> case typeRep @a of-                    App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of-                        Just HRefl -> case testEquality t2 (typeRep @T.Text) of-                            Just Refl -> fromMaybe "null" e-                            Nothing -> (fromOptional . T.pack . show) e-                              where-                                fromOptional s-                                    | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s-                                    | otherwise = "null"-                        Nothing -> (T.pack . show) e-                    _ -> (T.pack . show) e-        Nothing ->-            error $-                "Column "-                    ++ T.unpack (indexMap M.! k)-                    ++ " has less items than "-                    ++ "the other columns at index "-                    ++ show i-    go k (UnboxedColumn c) acc = case c VU.!? i of-        Just e -> T.pack (show e) : acc-        Nothing ->-            error $-                "Column "-                    ++ T.unpack (indexMap M.! k)-                    ++ " has less items than "-                    ++ "the other columns at index "-                    ++ show i-    go k (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of-        Just e -> case testEquality (typeRep @a) (typeRep @T.Text) of-            Just Refl -> fromMaybe T.empty e : acc-            Nothing -> maybe T.empty (T.pack . show) e : acc-        Nothing ->-            error $-                "Column "-                    ++ T.unpack (indexMap M.! k)-                    ++ " has less items than "-                    ++ "the other columns at index "-                    ++ show i+writeSeparated c filepath df = TIO.writeFile filepath (toSeparated c df)  stripQuotes :: T.Text -> T.Text stripQuotes txt =
src/DataFrame/IO/Parquet.hs view
@@ -19,10 +19,11 @@ import Data.Text.Encoding import Data.Time import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import DataFrame.Errors (DataFrameException (ColumnNotFoundException))+import qualified Data.Vector as V+import DataFrame.Errors (DataFrameException (ColumnsNotFoundException)) import DataFrame.Internal.Binary (littleEndianWord32) import qualified DataFrame.Internal.Column as DI-import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.DataFrame (DataFrame, columns) import DataFrame.Internal.Expression (Expr, getColumns) import qualified DataFrame.Operations.Core as DI import DataFrame.Operations.Merge ()@@ -75,6 +76,8 @@     -- ^ Optional row filter expression applied before projection.     , rowRange :: Maybe (Int, Int)     -- ^ Optional row slice @(start, end)@ with start-inclusive/end-exclusive semantics.+    , safeColumns :: Bool+    -- ^ When True, every column is promoted to OptionalColumn after read, regardless of nullability in the schema.     }     deriving (Eq, Show) @@ -87,6 +90,7 @@     { selectedColumns = Nothing     , predicate = Nothing     , rowRange = Nothing+    , safeColumns = False     } @ -}@@ -96,6 +100,7 @@         { selectedColumns = Nothing         , predicate = Nothing         , rowRange = Nothing+        , safeColumns = False         }  -- Public API --------------------------------------------------------------@@ -179,16 +184,15 @@              in unless                     (L.null missing)                     ( throw-                        ( ColumnNotFoundException-                            (T.pack $ show missing)+                        ( ColumnsNotFoundException+                            missing                             "readParquetWithOpts"                             availableSelectedColumns                         )                     ) -    let totalRows = sum (map (fromIntegral . rowGroupNumRows) (rowGroups fileMetadata)) :: Int-    colMutMap <- newIORef (M.empty :: M.Map T.Text DI.MutableColumn)-    colOffMap <- newIORef (M.empty :: M.Map T.Text Int)+    -- Collect per-column chunk lists; concatenate at the end to preserve bitmaps.+    colListMap <- newIORef (M.empty :: M.Map T.Text [DI.Column])     lTypeMap <- newIORef (M.empty :: M.Map T.Text LogicalType)      let schemaElements = schema fileMetadata@@ -261,22 +265,13 @@                         maybeTypeLength                         lType -                mutMapSnap <- readIORef colMutMap-                case M.lookup colFullName mutMapSnap of-                    Nothing -> do-                        mc <- DI.newMutableColumn totalRows column-                        DI.copyIntoMutableColumn mc 0 column-                        modifyIORef' colMutMap (M.insert colFullName mc)-                        modifyIORef' colOffMap (M.insert colFullName (DI.columnLength column))-                    Just mc -> do-                        off <- (M.! colFullName) <$> readIORef colOffMap-                        DI.copyIntoMutableColumn mc off column-                        modifyIORef' colOffMap (M.adjust (+ DI.columnLength column) colFullName)+                modifyIORef' colListMap (M.insertWith (++) colFullName [column])                 modifyIORef' lTypeMap (M.insert colFullName lType) -    finalMutMap <- readIORef colMutMap-    finalColMap <--        M.traverseWithKey (\_ mc -> DI.freezeMutableColumn mc) finalMutMap+    finalListMap <- readIORef colListMap+    -- Reverse the accumulated lists (they were prepended) and concat columns per-name,+    -- preserving bitmaps correctly via concatManyColumns.+    let finalColMap = M.map (DI.concatManyColumns . reverse) finalListMap     finalLTypeMap <- readIORef lTypeMap     let orderedColumns =             map@@ -349,9 +344,15 @@ applyPredicate opts df =     maybe df (`DS.filterWhere` df) (predicate opts) +applySafeRead :: ParquetReadOptions -> DataFrame -> DataFrame+applySafeRead opts df+    | safeColumns opts = df{columns = V.map DI.ensureOptional (columns df)}+    | otherwise = df+ applyReadOptions :: ParquetReadOptions -> DataFrame -> DataFrame applyReadOptions opts =-    applyRowRange opts+    applySafeRead opts+        . applyRowRange opts         . applySelectedColumns opts         . applyPredicate opts 
src/DataFrame/IO/Parquet/Thrift.hs view
@@ -68,9 +68,8 @@         let             colType :: TType             colType = case unsafeGetColumn colName df of-                (DI.BoxedColumn (col :: V.Vector a)) -> haskellToTType @a-                (DI.UnboxedColumn (col :: VU.Vector a)) -> haskellToTType @a-                (DI.OptionalColumn (col :: V.Vector (Maybe a))) -> haskellToTType @a+                (DI.BoxedColumn _ (col :: V.Vector a)) -> haskellToTType @a+                (DI.UnboxedColumn _ (col :: VU.Vector a)) -> haskellToTType @a             lType =                 if DI.hasElemType @T.Text (unsafeGetColumn colName df)                     || DI.hasElemType @(Maybe T.Text) (unsafeGetColumn colName df)
− src/DataFrame/IO/Unstable/CSV.hs
@@ -1,352 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CApiFFI #-}-{-# LANGUAGE ForeignFunctionInterface #-}--module DataFrame.IO.Unstable.CSV (-    fastReadCsvUnstable,-    readCsvUnstable,-    fastReadTsvUnstable,-    readTsvUnstable,-    getDelimiterIndices,-) where--import qualified Data.Vector as Vector-import qualified Data.Vector.Storable as VS-import Data.Vector.Storable.Mutable (-    grow,-    unsafeFromForeignPtr,- )-import qualified Data.Vector.Storable.Mutable as VSM-import System.IO.MMap (-    Mode (WriteCopy),-    mmapFileForeignPtr,- )--import Foreign (-    Ptr,-    castForeignPtr,-    castPtr,-    mallocArray,-    newForeignPtr_,- )-import Foreign.C.Types--import qualified Data.ByteString as BS-import Data.ByteString.Internal (ByteString (PS))-import qualified Data.Map as M-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as TextEncoding-import Data.Word (Word8)--import Control.Parallel.Strategies (parList, rpar, using)-import Data.Array.IArray (array, (!))-import Data.Array.Unboxed (UArray)-import Data.Ix (range)--import DataFrame.IO.CSV (-    HeaderSpec (..),-    ReadOptions (..),-    defaultReadOptions,-    shouldInferFromSample,-    stripQuotes,-    typeInferenceSampleSize,- )-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Operations.Typing (ParseOptions (..), parseFromExamples)--readSeparatedDefaultFast :: Word8 -> FilePath -> IO DataFrame-readSeparatedDefaultFast separator =-    readSeparated-        separator-        defaultReadOptions-        getDelimiterIndices--readSeparatedDefault :: Word8 -> FilePath -> IO DataFrame-readSeparatedDefault separator =-    readSeparated-        separator-        defaultReadOptions-        ( \separator originalLen v -> do-            indices <- mallocArray originalLen-            getDelimiterIndices_ separator originalLen v indices-        )--fastReadCsvUnstable :: FilePath -> IO DataFrame-fastReadCsvUnstable = readSeparatedDefaultFast comma--readCsvUnstable :: FilePath -> IO DataFrame-readCsvUnstable = readSeparatedDefault comma--fastReadTsvUnstable :: FilePath -> IO DataFrame-fastReadTsvUnstable = readSeparatedDefaultFast tab--readTsvUnstable :: FilePath -> IO DataFrame-readTsvUnstable = readSeparatedDefault tab--readSeparated ::-    Word8 ->-    ReadOptions ->-    (Word8 -> Int -> VS.Vector Word8 -> IO (VS.Vector CSize)) ->-    FilePath ->-    IO DataFrame-readSeparated separator opts delimiterIndices filePath = do-    -- We use write copy mode so that we can append-    -- padding to the end of the memory space-    (bufferPtr, offset, len) <--        mmapFileForeignPtr-            filePath-            WriteCopy-            Nothing-    let mutableFile = unsafeFromForeignPtr bufferPtr offset len-    paddedMutableFile <- grow mutableFile 64-    paddedCSVFile <- VS.unsafeFreeze paddedMutableFile-    indices <- delimiterIndices separator len paddedCSVFile-    let numCol = countColumnsInFirstRow paddedCSVFile indices-        totalRows = VS.length indices `div` numCol-        extractField' = extractField paddedCSVFile indices-        (columnNames, dataStartRow) = case headerSpec opts of-            NoHeader ->-                ( Vector.fromList $-                    map (Text.pack . show) [0 .. numCol - 1]-                , 0-                )-            UseFirstRow ->-                ( Vector.fromList $-                    map (stripQuotes . extractField') [0 .. numCol - 1]-                , 1-                )-            ProvideNames ns ->-                (Vector.fromList ns, 0)-        numRow = totalRows - dataStartRow-        parseTypes col =-            let n =-                    if shouldInferFromSample (typeSpec opts)-                        then typeInferenceSampleSize (typeSpec opts)-                        else 0-                parseOpts =-                    ParseOptions-                        { missingValues = missingIndicators opts-                        , sampleSize = n-                        , parseSafe = safeRead opts-                        , parseDateFormat = dateFormat opts-                        }-             in parseFromExamples parseOpts col-        generateColumn col =-            parseTypes $-                Vector.fromListN-                    numRow-                    ( map-                        ( \row ->-                            (stripQuotes . extractField')-                                (row * numCol + col)-                        )-                        [dataStartRow .. totalRows - 1]-                    )-        columns =-            Vector.fromListN-                numCol-                ( map generateColumn [0 .. numCol - 1]-                    `using` parList rpar-                )-        columnIndices =-            M.fromList $-                zip (Vector.toList columnNames) [0 ..]-        dataframeDimensions = (numRow, numCol)-    return $-        DataFrame columns columnIndices dataframeDimensions M.empty--{-# INLINE extractField #-}-extractField ::-    VS.Vector Word8 ->-    VS.Vector CSize ->-    Int ->-    Text-extractField file indices position =-    Text.strip-        . TextEncoding.decodeUtf8Lenient-        . unsafeToByteString-        $ VS.slice-            previous-            (next - previous)-            file-  where-    previous =-        if position == 0-            then 0-            else 1 + fromIntegral (indices VS.! (position - 1))-    next = fromIntegral $ indices VS.! position-    unsafeToByteString :: VS.Vector Word8 -> BS.ByteString-    unsafeToByteString v = PS (castForeignPtr ptr) 0 len-      where-        (ptr, len) = VS.unsafeToForeignPtr0 v--foreign import capi "process_csv.h get_delimiter_indices"-    get_delimiter_indices ::-        Ptr CUChar -> -- input-        CSize -> -- input size-        CUChar -> -- separator character-        Ptr CSize -> -- result array-        IO CSize -- occupancy of result array--{-# INLINE getDelimiterIndices #-}-getDelimiterIndices ::-    Word8 ->-    Int ->-    VS.Vector Word8 ->-    IO (VS.Vector CSize)-getDelimiterIndices separator originalLen csvFile =-    VS.unsafeWith csvFile $ \buffer -> do-        let paddedLen = VS.length csvFile-        -- GC-managed pinned memory: freed automatically, no leak in streaming use.-        resultMV <- VSM.unsafeNew paddedLen-        num_fields <--            VSM.unsafeWith resultMV $ \indicesPtr ->-                get_delimiter_indices-                    (castPtr buffer)-                    (fromIntegral paddedLen)-                    (fromIntegral separator)-                    (castPtr indicesPtr)-        if num_fields == -1-            then do-                -- Haskell state-machine fallback, writing directly into resultMV.-                let trans = stateTransitionTable separator-                    processChar (!state, !idx) i byte =-                        case state of-                            UnEscaped ->-                                if byte == lf || byte == separator-                                    then do-                                        VSM.unsafeWrite resultMV idx (fromIntegral i)-                                        return (toEnum (trans ! (fromEnum state, byte)), idx + 1)-                                    else return (toEnum (trans ! (fromEnum state, byte)), idx)-                            Escaped ->-                                return (toEnum (trans ! (fromEnum state, byte)), idx)-                (_, finalIdx) <- VS.ifoldM' processChar (UnEscaped, 0 :: Int) csvFile-                finalLen <--                    if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf-                        then do-                            VSM.unsafeWrite resultMV finalIdx (fromIntegral originalLen)-                            return (finalIdx + 1)-                        else return finalIdx-                VS.unsafeFreeze (VSM.slice 0 finalLen resultMV)-            else do-                let n = fromIntegral num_fields-                finalLen <--                    if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf-                        then do-                            VSM.write resultMV n (fromIntegral originalLen)-                            return (n + 1)-                        else return n-                VS.unsafeFreeze (VSM.slice 0 finalLen resultMV)---- We have a Native version in case the C version--- cannot be used. For example if neither ARM_NEON--- nor AVX2 are available--lf, cr, comma, tab, quote :: Word8-lf = 0x0A-cr = 0x0D-comma = 0x2C-tab = 0x09-quote = 0x22---- We parse using a state machine-data State-    = UnEscaped -- non quoted-    | Escaped -- quoted-    deriving (Enum)--{-# INLINE stateTransitionTable #-}-stateTransitionTable :: Word8 -> UArray (Int, Word8) Int-stateTransitionTable separator = array ((0, 0), (1, 255)) [(i, f i) | i <- range ((0, 0), (1, 255))]-  where-    f (0, character)-        -- Unescaped newline-        | character == 0x0A = fromEnum UnEscaped-        -- Unescaped separator-        | character == separator = fromEnum UnEscaped-        -- Unescaped quote-        | character == 0x22 = fromEnum Escaped-        | otherwise = fromEnum UnEscaped-    -- Escaped quote-    -- escaped quote in fields are dealt as-    -- consecutive quoted sections of a field-    -- example: If we have-    -- field1, "abc""def""ghi, field3-    -- we end up processing abc, def, and ghi-    -- as consecutive quoted strings.-    f (1, 0x22) = fromEnum UnEscaped-    -- Everything else-    f (state, _) = state--{-# INLINE getDelimiterIndices_ #-}-getDelimiterIndices_ ::-    Word8 ->-    Int ->-    VS.Vector Word8 ->-    Ptr CSize ->-    IO (VS.Vector CSize)-getDelimiterIndices_ separator originalLen csvFile resultPtr = do-    resultVector <- resultVectorM-    (_, resultLen) <--        VS.ifoldM'-            (processCharacter resultVector)-            (UnEscaped, 0)-            csvFile-    -- Handle the case where the file doesn't end with a newline-    -- We need to add a final delimiter for the last field-    finalResultLen <--        if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf-            then do-                VSM.write resultVector resultLen (fromIntegral originalLen)-                return (resultLen + 1)-            else return resultLen-    VS.unsafeFreeze $ VSM.slice 0 finalResultLen resultVector-  where-    paddedLen = VS.length csvFile-    resultVectorM = do-        resultForeignPtr <- newForeignPtr_ resultPtr-        return $ VSM.unsafeFromForeignPtr0 resultForeignPtr paddedLen-    transitionTable = stateTransitionTable separator-    processCharacter ::-        VSM.IOVector CSize ->-        (State, Int) ->-        Int ->-        Word8 ->-        IO (State, Int)-    processCharacter-        resultVector-        (!state, !resultIndex)-        index-        character =-            case state of-                UnEscaped ->-                    if character == lf || character == separator-                        then do-                            VSM.write-                                resultVector-                                resultIndex-                                (fromIntegral index)-                            return (newState, resultIndex + 1)-                        else return (newState, resultIndex)-                Escaped -> return (newState, resultIndex)-          where-            newState =-                toEnum $-                    transitionTable-                        ! (fromEnum state, character)--{-# INLINE countColumnsInFirstRow #-}-countColumnsInFirstRow ::-    VS.Vector Word8 ->-    VS.Vector CSize ->-    Int-countColumnsInFirstRow file indices-    | VS.length indices == 0 = 0-    | otherwise =-        1-            + VS.length-                ( VS.takeWhile-                    (\i -> file VS.! fromIntegral i /= lf)-                    indices-                )
src/DataFrame/Internal/Column.hs view
@@ -28,1329 +28,1698 @@  import Control.DeepSeq (NFData (..), rnf) import Control.Exception (throw)-import Control.Monad.ST (runST)-import Data.Kind (Type)-import Data.Maybe-import Data.These-import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors-import DataFrame.Internal.Parsing-import DataFrame.Internal.Types-import System.IO.Unsafe (unsafePerformIO)-import Type.Reflection--{- | Our representation of a column is a GADT that can store data based on the underlying data.--This allows us to pattern match on data kinds and limit some operations to only some-kinds of vectors. E.g. operations for missing data only happen in an OptionalColumn.--}-data Column where-    BoxedColumn :: (Columnable a) => VB.Vector a -> Column-    UnboxedColumn :: (Columnable a, VU.Unbox a) => VU.Vector a -> Column-    OptionalColumn :: (Columnable a) => VB.Vector (Maybe a) -> Column--{- | A mutable companion struct to dataframe columns.--Used mostly as an intermediate structure for I/O.--}-data MutableColumn where-    MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn-    MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn-    MOptionalColumn :: (Columnable a) => VBM.IOVector (Maybe a) -> MutableColumn--{- | A TypedColumn is a wrapper around our type-erased column.-It is used to type check expressions on columns.--Note: there is no guarantee that the Phanton type is the-same as the underlying vector type.--}-data TypedColumn a where-    TColumn :: (Columnable a) => Column -> TypedColumn a--instance (Eq a) => Eq (TypedColumn a) where-    (==) :: (Eq a) => TypedColumn a -> TypedColumn a -> Bool-    (==) (TColumn a) (TColumn b) = a == b--instance (Ord a) => Ord (TypedColumn a) where-    compare :: (Ord a) => TypedColumn a -> TypedColumn a -> Ordering-    compare (TColumn a) (TColumn b) = compare a b---- | Gets the underlying value from a TypedColumn.-unwrapTypedColumn :: TypedColumn a -> Column-unwrapTypedColumn (TColumn value) = value---- | Gets the underlying vector from a TypedColumn.-vectorFromTypedColumn :: TypedColumn a -> VB.Vector a-vectorFromTypedColumn (TColumn value) = either throw id (toVector value)---- | Checks if a column contains missing values.-hasMissing :: Column -> Bool-hasMissing (OptionalColumn column) = True-hasMissing _ = False---- | Checks if a column contains only missing values.-allMissing :: Column -> Bool-allMissing (OptionalColumn column) = VB.length (VB.filter isNothing column) == VB.length column-allMissing _ = False---- | Checks if a column contains numeric values.-isNumeric :: Column -> Bool-isNumeric (UnboxedColumn (vec :: VU.Vector a)) = case sNumeric @a of-    STrue -> True-    _ -> False-isNumeric (BoxedColumn (vec :: VB.Vector a)) = case testEquality (typeRep @a) (typeRep @Integer) of-    Nothing -> False-    Just Refl -> True-isNumeric _ = False---- | Checks if a column is of a given type values.-hasElemType :: forall a. (Columnable a) => Column -> Bool-hasElemType = \case-    BoxedColumn (column :: VB.Vector b) -> check (typeRep @b)-    UnboxedColumn (column :: VU.Vector b) -> check (typeRep @b)-    OptionalColumn (column :: VB.Vector b) -> check (typeRep @b)-  where-    check :: forall (b :: Type). TypeRep b -> Bool-    check = isJust . testEquality (typeRep @a)---- | An internal/debugging function to get the column type of a column.-columnVersionString :: Column -> String-columnVersionString column = case column of-    BoxedColumn _ -> "Boxed"-    UnboxedColumn _ -> "Unboxed"-    OptionalColumn _ -> "Optional"--{- | An internal/debugging function to get the type stored in the outermost vector-of a column.--}-columnTypeString :: Column -> String-columnTypeString column = case column of-    BoxedColumn (column :: VB.Vector a) -> show (typeRep @a)-    UnboxedColumn (column :: VU.Vector a) -> show (typeRep @a)-    OptionalColumn (column :: VB.Vector a) -> show (typeRep @a)--instance (Show a) => Show (TypedColumn a) where-    show :: (Show a) => TypedColumn a -> String-    show (TColumn col) = show col--instance NFData Column where-    rnf (BoxedColumn (v :: VB.Vector a)) = VB.foldl' (const (`seq` ())) () v-    rnf (UnboxedColumn v) = v `seq` ()-    rnf (OptionalColumn (v :: VB.Vector (Maybe a))) = VB.foldl' (const (`seq` ())) () v--instance Show Column where-    show :: Column -> String-    show (BoxedColumn column) = show column-    show (UnboxedColumn column) = show column-    show (OptionalColumn column) = show column--instance Eq Column where-    (==) :: Column -> Column -> Bool-    (==) (BoxedColumn (a :: VB.Vector t1)) (BoxedColumn (b :: VB.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a == b-    (==) (OptionalColumn (a :: VB.Vector t1)) (OptionalColumn (b :: VB.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a == b-    (==) (UnboxedColumn (a :: VU.Vector t1)) (UnboxedColumn (b :: VU.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a == b-    (==) _ _ = False---- Generalised LEQ that does reflection.-generalLEQ ::-    forall a b. (Typeable a, Typeable b, Ord a, Ord b) => a -> b -> Bool-generalLEQ x y = case testEquality (typeRep @a) (typeRep @b) of-    Nothing -> False-    Just Refl -> x <= y--instance Ord Column where-    (<=) :: Column -> Column -> Bool-    (<=) (BoxedColumn (a :: VB.Vector t1)) (BoxedColumn (b :: VB.Vector t2)) = generalLEQ a b-    (<=) (OptionalColumn (a :: VB.Vector t1)) (OptionalColumn (b :: VB.Vector t2)) = generalLEQ a b-    (<=) (UnboxedColumn (a :: VU.Vector t1)) (UnboxedColumn (b :: VU.Vector t2)) = generalLEQ a b-    (<=) _ _ = False--{- | A class for converting a vector to a column of the appropriate type.-Given each Rep we tell the `toColumnRep` function which Column type to pick.--}-class ColumnifyRep (r :: Rep) a where-    toColumnRep :: VB.Vector a -> Column---- | Constraint synonym for what we can put into columns.-type Columnable a =-    ( Columnable' a-    , ColumnifyRep (KindOf a) a-    , UnboxIf a-    , IntegralIf a-    , FloatingIf a-    , SBoolI (Unboxable a)-    , SBoolI (Numeric a)-    , SBoolI (IntegralTypes a)-    , SBoolI (FloatingTypes a)-    )--instance-    (Columnable a, VU.Unbox a) =>-    ColumnifyRep 'RUnboxed a-    where-    toColumnRep :: (Columnable a, VUM.Unbox a) => VB.Vector a -> Column-    toColumnRep = UnboxedColumn . VU.convert--instance-    (Columnable a) =>-    ColumnifyRep 'RBoxed a-    where-    toColumnRep :: (Columnable a) => VB.Vector a -> Column-    toColumnRep = BoxedColumn--instance-    (Columnable a) =>-    ColumnifyRep 'ROptional (Maybe a)-    where-    toColumnRep = OptionalColumn--{- | O(n) Convert a vector to a column. Automatically picks the best representation of a vector to store the underlying data in.--__Examples:__--@-> import qualified Data.Vector as V-> fromVector (VB.fromList [(1 :: Int), 2, 3, 4])-[1,2,3,4]-@--}-fromVector ::-    forall a.-    (Columnable a, ColumnifyRep (KindOf a) a) =>-    VB.Vector a -> Column-fromVector = toColumnRep @(KindOf a)--{- | O(n) Convert an unboxed vector to a column. This avoids the extra conversion if you already have the data in an unboxed vector.--__Examples:__--@-> import qualified Data.Vector.Unboxed as V-> fromUnboxedVector (VB.fromList [(1 :: Int), 2, 3, 4])-[1,2,3,4]-@--}-fromUnboxedVector ::-    forall a. (Columnable a, VU.Unbox a) => VU.Vector a -> Column-fromUnboxedVector = UnboxedColumn--{- | O(n) Convert a list to a column. Automatically picks the best representation of a vector to store the underlying data in.--__Examples:__--@-> fromList [(1 :: Int), 2, 3, 4]-[1,2,3,4]-@--}-fromList ::-    forall a.-    (Columnable a, ColumnifyRep (KindOf a) a) =>-    [a] -> Column-fromList = toColumnRep @(KindOf a) . VB.fromList---- An internal helper for type errors-throwTypeMismatch ::-    forall (a :: Type) (b :: Type).-    (Typeable a, Typeable b) => Either DataFrameException Column-throwTypeMismatch =-    Left $-        TypeMismatchException-            MkTypeErrorContext-                { userType = Right (typeRep @b)-                , expectedType = Right (typeRep @a)-                , callingFunctionName = Nothing-                , errorColumnName = Nothing-                }---- | An internal function to map a function over the values of a column.-mapColumn ::-    forall b c.-    (Columnable b, Columnable c) =>-    (b -> c) -> Column -> Either DataFrameException Column-mapColumn f = \case-    BoxedColumn (col :: VB.Vector a) -> run col-    OptionalColumn (col :: VB.Vector (Maybe a)) -> runOpt col-    UnboxedColumn (col :: VU.Vector a) -> runUnboxed col-  where-    run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column-    run col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right (fromVector @c (VB.map f col))-        Nothing -> throwTypeMismatch @a @b--    -- For OptionalColumn: first try exact match (b = Maybe a, user maps over Maybe-    -- values directly), then try lenient fmap fallback (b = a, auto-fmap over inner).-    runOpt ::-        forall a.-        (Columnable a) => VB.Vector (Maybe a) -> Either DataFrameException Column-    runOpt col = case testEquality (typeRep @(Maybe a)) (typeRep @b) of-        Just Refl -> Right (fromVector @c (VB.map f col))-        Nothing -> case testEquality (typeRep @a) (typeRep @b) of-            Just Refl -> Right (OptionalColumn (VB.map (fmap f) col))-            Nothing -> throwTypeMismatch @(Maybe a) @b--    runUnboxed ::-        forall a.-        (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column-    runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ case sUnbox @c of-            STrue -> UnboxedColumn (VU.map f col)-            SFalse -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col))-        Nothing -> throwTypeMismatch @a @b-{-# INLINEABLE mapColumn #-}---- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.-imapColumn ::-    forall b c.-    (Columnable b, Columnable c) =>-    (Int -> b -> c) -> Column -> Either DataFrameException Column-imapColumn f = \case-    BoxedColumn (col :: VB.Vector a) -> run col-    OptionalColumn (col :: VB.Vector (Maybe a)) -> runOpt col-    UnboxedColumn (col :: VU.Vector a) -> runUnboxed col-  where-    run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column-    run col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right (fromVector @c (VB.imap f col))-        Nothing -> throwTypeMismatch @a @b--    runOpt ::-        forall a.-        (Columnable a) => VB.Vector (Maybe a) -> Either DataFrameException Column-    runOpt col = case testEquality (typeRep @(Maybe a)) (typeRep @b) of-        Just Refl -> Right (fromVector @c (VB.imap f col))-        Nothing -> case testEquality (typeRep @a) (typeRep @b) of-            Just Refl -> Right (OptionalColumn (VB.imap (fmap . f) col))-            Nothing -> throwTypeMismatch @(Maybe a) @b--    runUnboxed ::-        forall a.-        (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column-    runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ case sUnbox @c of-            STrue -> UnboxedColumn (VU.imap f col)-            SFalse -> BoxedColumn (VB.imap f (VG.convert col))-        Nothing -> throwTypeMismatch @a @b---- | O(1) Gets the number of elements in the column.-columnLength :: Column -> Int-columnLength (BoxedColumn xs) = VB.length xs-columnLength (UnboxedColumn xs) = VU.length xs-columnLength (OptionalColumn xs) = VB.length xs-{-# INLINE columnLength #-}---- | O(n) Gets the number of elements in the column.-numElements :: Column -> Int-numElements (BoxedColumn xs) = VB.length xs-numElements (UnboxedColumn xs) = VU.length xs-numElements (OptionalColumn xs) = VB.foldl' (\acc x -> acc + fromEnum (isJust x)) 0 xs-{-# INLINE numElements #-}---- | O(n) Takes the first n values of a column.-takeColumn :: Int -> Column -> Column-takeColumn n (BoxedColumn xs) = BoxedColumn $ VG.take n xs-takeColumn n (UnboxedColumn xs) = UnboxedColumn $ VG.take n xs-takeColumn n (OptionalColumn xs) = OptionalColumn $ VG.take n xs-{-# INLINE takeColumn #-}---- | O(n) Takes the last n values of a column.-takeLastColumn :: Int -> Column -> Column-takeLastColumn n column = sliceColumn (columnLength column - n) n column-{-# INLINE takeLastColumn #-}---- | O(n) Takes n values after a given column index.-sliceColumn :: Int -> Int -> Column -> Column-sliceColumn start n (BoxedColumn xs) = BoxedColumn $ VG.slice start n xs-sliceColumn start n (UnboxedColumn xs) = UnboxedColumn $ VG.slice start n xs-sliceColumn start n (OptionalColumn xs) = OptionalColumn $ VG.slice start n xs-{-# INLINE sliceColumn #-}---- | O(n) Selects the elements at a given set of indices. Does not change the order.-atIndicesStable :: VU.Vector Int -> Column -> Column-atIndicesStable indexes (BoxedColumn column) =-    BoxedColumn $-        VB.generate-            (VU.length indexes)-            (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i))-atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ VU.unsafeBackpermute column indexes-atIndicesStable indexes (OptionalColumn column) =-    OptionalColumn $-        VB.generate-            (VU.length indexes)-            (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i))-{-# INLINE atIndicesStable #-}--{- | Like 'atIndicesStable' but treats negative indices as null,-producing an 'OptionalColumn'. Keeps the index vector fully-unboxed (no @VB.Vector (Maybe Int)@).--}-gatherWithSentinel :: VU.Vector Int -> Column -> Column-gatherWithSentinel indices col =-    let !n = VU.length indices-     in case col of-            BoxedColumn v ->-                OptionalColumn $-                    VB.generate n $ \i ->-                        let !idx = indices `VU.unsafeIndex` i-                         in if idx < 0 then Nothing else Just (v `VB.unsafeIndex` idx)-            UnboxedColumn v ->-                OptionalColumn $-                    VB.generate n $ \i ->-                        let !idx = indices `VU.unsafeIndex` i-                         in if idx < 0 then Nothing else Just (v `VU.unsafeIndex` idx)-            OptionalColumn v ->-                OptionalColumn $-                    VB.generate n $ \i ->-                        let !idx = indices `VU.unsafeIndex` i-                         in if idx < 0 then Nothing else v `VB.unsafeIndex` idx-{-# INLINE gatherWithSentinel #-}---- | Internal helper to get indices in a boxed vector.-getIndices :: VU.Vector Int -> VB.Vector a -> VB.Vector a-getIndices indices xs = VB.generate (VU.length indices) (\i -> xs VB.! (indices VU.! i))-{-# INLINE getIndices #-}---- | Internal helper to get indices in an unboxed vector.-getIndicesUnboxed :: (VU.Unbox a) => VU.Vector Int -> VU.Vector a -> VU.Vector a-getIndicesUnboxed indices xs = VU.generate (VU.length indices) (\i -> xs VU.! (indices VU.! i))-{-# INLINE getIndicesUnboxed #-}--findIndices ::-    forall a.-    (Columnable a) =>-    (a -> Bool) ->-    Column ->-    Either DataFrameException (VU.Vector Int)-findIndices pred = \case-    BoxedColumn (v :: VB.Vector b) -> run v VG.convert-    OptionalColumn (v :: VB.Vector b) -> run v VG.convert-    UnboxedColumn (v :: VU.Vector b) -> run v id-  where-    run ::-        forall b v.-        (Typeable b, VG.Vector v b, VG.Vector v Int) =>-        v b ->-        (v Int -> VU.Vector Int) ->-        Either DataFrameException (VU.Vector Int)-    run column finalize = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right . finalize $ VG.findIndices pred column-        Nothing ->-            Left $-                TypeMismatchException-                    MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @b)-                        , callingFunctionName = Just "findIndices"-                        , errorColumnName = Nothing-                        }---- | An internal function that returns a vector of how indexes change after a column is sorted.-sortedIndexes :: Bool -> Column -> VU.Vector Int-sortedIndexes asc = \case-    BoxedColumn column -> sortWorker VG.convert column-    UnboxedColumn column -> sortWorker id column-    OptionalColumn column -> sortWorker VG.convert column-  where-    sortWorker ::-        (VG.Vector v a, Ord a, VG.Vector v (Int, a), VG.Vector v Int) =>-        (v Int -> VU.Vector Int) -> v a -> VU.Vector Int-    sortWorker finalize column = runST $ do-        withIndexes <- VG.thaw $ VG.indexed column-        let cmp = if asc then compare else flip compare-        VA.sortBy (\(_, b) (_, b') -> cmp b b') withIndexes-        sorted <- VG.unsafeFreeze withIndexes-        return $ finalize $ VG.map fst sorted-{-# INLINE sortedIndexes #-}---- | Fold (right) column with index.-ifoldrColumn ::-    forall a b.-    (Columnable a, Columnable b) =>-    (Int -> a -> b -> b) -> b -> Column -> Either DataFrameException b-ifoldrColumn f acc = \case-    BoxedColumn column -> foldrWorker column-    UnboxedColumn column -> foldrWorker column-    OptionalColumn column -> foldrWorker column-  where-    foldrWorker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException b-    foldrWorker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl -> pure $ VG.ifoldr f acc vec-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "ifoldrColumn"-                        , errorColumnName = Nothing-                        }-                    )--foldlColumn ::-    forall a b.-    (Columnable a, Columnable b) =>-    (b -> a -> b) -> b -> Column -> Either DataFrameException b-foldlColumn f acc = \case-    BoxedColumn column -> foldlWorker column-    UnboxedColumn column -> foldlWorker column-    OptionalColumn column -> foldlWorker column-  where-    foldlWorker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException b-    foldlWorker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl -> pure $ VG.foldl' f acc vec-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "ifoldrColumn"-                        , errorColumnName = Nothing-                        }-                    )--foldl1Column ::-    forall a.-    (Columnable a) =>-    (a -> a -> a) -> Column -> Either DataFrameException a-foldl1Column f = \case-    BoxedColumn column -> foldl1Worker column-    UnboxedColumn column -> foldl1Worker column-    OptionalColumn column -> foldl1Worker column-  where-    foldl1Worker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException a-    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl -> pure $ VG.foldl1' f vec-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "foldl1Column"-                        , errorColumnName = Nothing-                        }-                    )--{- | O(n) Seedless fold over groups using the first element of each group as seed.-Like 'foldDirectGroups' but for the case where no initial accumulator is available.--}-foldl1DirectGroups ::-    forall a.-    (Columnable a) =>-    (a -> a -> a) ->-    Column ->-    VU.Vector Int ->-    VU.Vector Int ->-    Either DataFrameException Column-foldl1DirectGroups f col valueIndices offsets-    | VU.length offsets <= 1 = pure $ fromVector @a VB.empty-    | otherwise = case col of-        UnboxedColumn (vec :: VU.Vector d) -> UnboxedColumn <$> foldl1Worker vec-        BoxedColumn (vec :: VB.Vector d) -> BoxedColumn <$> foldl1Worker vec-        OptionalColumn (vec :: VB.Vector (Maybe d)) -> OptionalColumn <$> foldl1Worker vec-  where-    foldl1Worker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException (v c)-    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl ->-            Right $-                VG.generate (VU.length offsets - 1) foldGroup-          where-            foldGroup k =-                let !s = VU.unsafeIndex offsets k-                    !e = VU.unsafeIndex offsets (k + 1)-                    !seed = VG.unsafeIndex vec (VU.unsafeIndex valueIndices s)-                 in go (s + 1) e seed-            go !i !e !acc-                | i >= e = acc-                | otherwise =-                    go (i + 1) e $!-                        f acc (VG.unsafeIndex vec (VU.unsafeIndex valueIndices i))-        Nothing ->-            Left $-                TypeMismatchException-                    MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "foldl1DirectGroups"-                        , errorColumnName = Nothing-                        }-{-# INLINEABLE foldl1DirectGroups #-}--{- | O(n) fold over groups by scanning the column LINEARLY.-rowToGroup[i] = group index for row i.-Avoids random column reads; random writes go to the accumulator array which is-small (nGroups entries) and typically cache-resident.-When @acc@ is unboxable, uses an unboxed mutable vector for the accumulator-array, eliminating pointer indirection on every read/write.--}-foldLinearGroups ::-    forall b acc.-    (Columnable b, Columnable acc) =>-    (acc -> b -> acc) ->-    acc ->-    Column ->-    VU.Vector Int -> -- rowToGroup (length n)-    Int -> -- nGroups-    Either DataFrameException Column-foldLinearGroups f seed col rowToGroup nGroups-    | nGroups == 0 = Right (fromVector @acc VB.empty)-    | otherwise = case col of-        UnboxedColumn (vec :: VU.Vector d) -> foldLinearWorker vec-        BoxedColumn (vec :: VB.Vector d) -> foldLinearWorker vec-        OptionalColumn (vec :: VB.Vector (Maybe d)) -> foldLinearWorker vec-  where-    foldLinearWorker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException Column-    foldLinearWorker vec = case testEquality (typeRep @b) (typeRep @c) of-        Just Refl ->-            Right $-                unsafePerformIO $-                    runWith-                        ( \readAt writeAt ->-                            VG.iforM_ vec $ \row x -> do-                                let !k = VG.unsafeIndex rowToGroup row-                                cur <- readAt k-                                writeAt k $! f cur x-                        )-        Nothing ->-            Left $-                TypeMismatchException-                    MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "foldLinearGroups"-                        , errorColumnName = Nothing-                        }--    -- \| Allocate accumulators, run the traversal, return a frozen Column.-    -- When @acc@ is unboxable, uses an unboxed mutable vector (no pointer-    -- indirection per read/write) and returns UnboxedColumn directly —-    -- avoiding a round-trip through VB.Vector.-    runWith :: ((Int -> IO acc) -> (Int -> acc -> IO ()) -> IO ()) -> IO Column-    runWith body = case sUnbox @acc of-        STrue -> do-            accs <- VUM.replicate nGroups seed-            body (VUM.unsafeRead accs) (VUM.unsafeWrite accs)-            UnboxedColumn <$> VU.unsafeFreeze accs-        SFalse -> do-            accs <- VBM.replicate nGroups seed-            body (VBM.unsafeRead accs) (VBM.unsafeWrite accs)-            fromVector @acc <$> VB.unsafeFreeze accs-    {-# INLINE runWith #-}-{-# INLINEABLE foldLinearGroups #-}--headColumn :: forall a. (Columnable a) => Column -> Either DataFrameException a-headColumn = \case-    BoxedColumn col -> headWorker col-    UnboxedColumn col -> headWorker col-    OptionalColumn col -> headWorker col-  where-    headWorker ::-        forall c v.-        (Typeable c, VG.Vector v c) =>-        v c ->-        Either DataFrameException a-    headWorker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl ->-            if VG.null vec-                then Left (EmptyDataSetException "headColumn")-                else pure (VG.head vec)-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "headColumn"-                        , errorColumnName = Nothing-                        }-                    )---- | An internal, column version of zip.-zipColumns :: Column -> Column -> Column-zipColumns (BoxedColumn column) (BoxedColumn other) = BoxedColumn (VG.zip column other)-zipColumns (BoxedColumn column) (UnboxedColumn other) =-    BoxedColumn-        ( VB.generate-            (min (VG.length column) (VG.length other))-            (\i -> (column VG.! i, other VG.! i))-        )-zipColumns (BoxedColumn column) (OptionalColumn optcolumn) = BoxedColumn (VG.zip (VB.convert column) optcolumn)-zipColumns (UnboxedColumn column) (BoxedColumn other) =-    BoxedColumn-        ( VB.generate-            (min (VG.length column) (VG.length other))-            (\i -> (column VG.! i, other VG.! i))-        )-zipColumns (UnboxedColumn column) (UnboxedColumn other) = UnboxedColumn (VG.zip column other)-zipColumns (UnboxedColumn column) (OptionalColumn optcolumn) = BoxedColumn (VG.zip (VB.convert column) optcolumn)-zipColumns (OptionalColumn optcolumn) (BoxedColumn column) = BoxedColumn (VG.zip optcolumn (VB.convert column))-zipColumns (OptionalColumn optcolumn) (UnboxedColumn column) = BoxedColumn (VG.zip optcolumn (VB.convert column))-zipColumns (OptionalColumn optcolumn) (OptionalColumn optother) = BoxedColumn (VG.zip optcolumn optother)-{-# INLINE zipColumns #-}---- | Merge two columns using `These`.-mergeColumns :: Column -> Column -> Column-mergeColumns colA colB = case (colA, colB) of-    (OptionalColumn c1, OptionalColumn c2) ->-        OptionalColumn $ mkVec c1 c2 $ \v1 v2 ->-            case (v1, v2) of-                (Nothing, Nothing) -> Nothing-                (Just x, Nothing) -> Just (This x)-                (Nothing, Just y) -> Just (That y)-                (Just x, Just y) -> Just (These x y)-    (OptionalColumn c1, BoxedColumn c2) -> optReq c1 c2-    (OptionalColumn c1, UnboxedColumn c2) -> optReq c1 c2-    (BoxedColumn c1, OptionalColumn c2) -> reqOpt c1 c2-    (UnboxedColumn c1, OptionalColumn c2) -> reqOpt c1 c2-    (BoxedColumn c1, BoxedColumn c2) -> reqReq c1 c2-    (BoxedColumn c1, UnboxedColumn c2) -> reqReq c1 c2-    (UnboxedColumn c1, BoxedColumn c2) -> reqReq c1 c2-    (UnboxedColumn c1, UnboxedColumn c2) -> reqReq c1 c2-  where-    mkVec c1 c2 combineElements =-        VB.generate-            (min (VG.length c1) (VG.length c2))-            (\i -> combineElements (c1 VG.! i) (c2 VG.! i))-    {-# INLINE mkVec #-}--    reqReq c1 c2 = BoxedColumn $ mkVec c1 c2 These--    reqOpt c1 c2 = BoxedColumn $ mkVec c1 c2 $ \v1 v2 ->-        case v2 of-            Nothing -> This v1-            Just y -> These v1 y--    optReq c1 c2 = BoxedColumn $ mkVec c1 c2 $ \v1 v2 ->-        case v1 of-            Nothing -> That v2-            Just x -> These x v2-{-# INLINE mergeColumns #-}---- | An internal, column version of zipWith.-zipWithColumns ::-    forall a b c.-    (Columnable a, Columnable b, Columnable c) =>-    (a -> b -> c) -> Column -> Column -> Either DataFrameException Column-zipWithColumns f (UnboxedColumn (column :: VU.Vector d)) (UnboxedColumn (other :: VU.Vector e)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> case testEquality (typeRep @b) (typeRep @e) of-        Just Refl -> pure $ case sUnbox @c of-            STrue -> UnboxedColumn (VU.zipWith f column other)-            SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @e)-                        , callingFunctionName = Just "zipWithColumns"-                        , errorColumnName = Nothing-                        }-                    )-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "zipWithColumns"-                    , errorColumnName = Nothing-                    }-                )--- TODO: mchavinda - reuse pattern from interpret where we augment the--- error at the end.-zipWithColumns f left right = case toVector @a left of-    Left (TypeMismatchException context) ->-        Left $-            TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})-    Left e -> Left e-    Right left' -> case toVector @b right of-        Left (TypeMismatchException context) ->-            Left $-                TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})-        Left e -> Left e-        Right right' -> pure $ fromVector $ VB.zipWith f left' right'-{-# INLINE zipWithColumns #-}---- Functions for mutable columns (intended for IO).-writeColumn :: Int -> T.Text -> MutableColumn -> IO (Either T.Text Bool)-writeColumn i value (MBoxedColumn (col :: VBM.IOVector a)) =-    let-     in case testEquality (typeRep @a) (typeRep @T.Text) of-            Just Refl ->-                ( if isNullish value-                    then VBM.unsafeWrite col i "" >> return (Left $! value)-                    else VBM.unsafeWrite col i value >> return (Right True)-                )-            Nothing -> return (Left value)-writeColumn i value (MOptionalColumn (col :: VBM.IOVector (Maybe a))) =-    let-     in case testEquality (typeRep @a) (typeRep @T.Text) of-            Just Refl ->-                ( if isNullish value-                    then VBM.unsafeWrite col i Nothing >> return (Left $! value)-                    else VBM.unsafeWrite col i (Just value) >> return (Right True)-                )-            Nothing -> return (Left value)-writeColumn i value (MUnboxedColumn (col :: VUM.IOVector a)) =-    case testEquality (typeRep @a) (typeRep @Int) of-        Just Refl -> case readInt value of-            Just v -> VUM.unsafeWrite col i v >> return (Right True)-            Nothing -> VUM.unsafeWrite col i 0 >> return (Left value)-        Nothing -> case testEquality (typeRep @a) (typeRep @Double) of-            Nothing -> return (Left $! value)-            Just Refl -> case readDouble value of-                Just v -> VUM.unsafeWrite col i v >> return (Right True)-                Nothing -> VUM.unsafeWrite col i 0 >> return (Left $! value)-{-# INLINE writeColumn #-}--freezeColumn' :: [(Int, T.Text)] -> MutableColumn -> IO Column-freezeColumn' nulls (MOptionalColumn col) = OptionalColumn <$> VB.unsafeFreeze col-freezeColumn' nulls (MBoxedColumn col)-    | null nulls = BoxedColumn <$> VB.unsafeFreeze col-    | all (isNullish . snd) nulls =-        OptionalColumn-            . VB.imap (\i v -> if i `elem` map fst nulls then Nothing else Just v)-            <$> VB.unsafeFreeze col-    | otherwise =-        BoxedColumn-            . VB.imap-                ( \i v ->-                    if i `elem` map fst nulls-                        then Left (fromMaybe (error "UNEXPECTED ERROR DURING FREEZE") (lookup i nulls))-                        else Right v-                )-            <$> VB.unsafeFreeze col-freezeColumn' nulls (MUnboxedColumn col)-    | null nulls = UnboxedColumn <$> VU.unsafeFreeze col-    | all (isNullish . snd) nulls =-        VU.unsafeFreeze col >>= \c ->-            return $-                OptionalColumn $-                    VB.generate-                        (VU.length c)-                        (\i -> if i `elem` map fst nulls then Nothing else Just (c VU.! i))-    | otherwise =-        VU.unsafeFreeze col >>= \c ->-            return $-                BoxedColumn $-                    VB.generate-                        (VU.length c)-                        ( \i ->-                            if i `elem` map fst nulls-                                then Left (fromMaybe (error "UNEXPECTED ERROR DURING FREEZE") (lookup i nulls))-                                else Right (c VU.! i)-                        )-{-# INLINE freezeColumn' #-}---- | Fills the end of a column, up to n, with Nothing. Does nothing if column has length greater than n.-expandColumn :: Int -> Column -> Column-expandColumn n (OptionalColumn col) = OptionalColumn $ col <> VB.replicate (n - VG.length col) Nothing-expandColumn n column@(BoxedColumn col)-    | n > VG.length col =-        OptionalColumn $ VB.map Just col <> VB.replicate (n - VG.length col) Nothing-    | otherwise = column-expandColumn n column@(UnboxedColumn col)-    | n > VG.length col =-        OptionalColumn $-            VB.map Just (VU.convert col) <> VB.replicate (n - VG.length col) Nothing-    | otherwise = column---- | Fills the beginning of a column, up to n, with Nothing. Does nothing if column has length greater than n.-leftExpandColumn :: Int -> Column -> Column-leftExpandColumn n column@(OptionalColumn col)-    | n > VG.length col =-        OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> col-    | otherwise = column-leftExpandColumn n column@(BoxedColumn col)-    | n > VG.length col =-        OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> VG.map Just col-    | otherwise = column-leftExpandColumn n column@(UnboxedColumn col)-    | n > VG.length col =-        OptionalColumn $-            VG.replicate (n - VG.length col) Nothing <> VG.map Just (VU.convert col)-    | otherwise = column--{- | Concatenates two columns.-Returns Nothing if the columns are of different types.--}-concatColumns :: Column -> Column -> Either DataFrameException Column-concatColumns left right = case (left, right) of-    (OptionalColumn l, OptionalColumn r) -> case testEquality (typeOf l) (typeOf r) of-        Just Refl -> pure (OptionalColumn $ l <> r)-        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))-    (BoxedColumn l, BoxedColumn r) -> case testEquality (typeOf l) (typeOf r) of-        Just Refl -> pure (BoxedColumn $ l <> r)-        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))-    (UnboxedColumn l, UnboxedColumn r) -> case testEquality (typeOf l) (typeOf r) of-        Just Refl -> pure (UnboxedColumn $ l <> r)-        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))-    _ -> Left (mismatchErr (typeOf right) (typeOf left))-  where-    mismatchErr ::-        forall (x :: Type) (y :: Type). TypeRep x -> TypeRep y -> DataFrameException-    mismatchErr ta tb =-        withTypeable ta $-            withTypeable tb $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right ta-                        , expectedType = Right tb-                        , callingFunctionName = Just "concatColumns"-                        , errorColumnName = Nothing-                        }-                    )--{- | Concatenates two columns.--Works similar to 'concatColumns', but unlike that function, it will also combine columns of different types-by wrapping the values in an Either.--E.g. combining Column containing [1,2] with Column containing ["a","b"]-will result in a Column containing [Left 1, Left 2, Right "a", Right "b"].--}--{- | O(n) Concatenate a list of same-type columns in a single allocation.-All columns must have the same constructor and element type (as they will-within a single Parquet column). Calls 'error' on mismatch.--}-concatManyColumns :: [Column] -> Column-concatManyColumns [] = fromList ([] :: [Maybe Int])-concatManyColumns [c] = c-concatManyColumns (c0 : cs) = case c0 of-    OptionalColumn v0 ->-        let getVec (OptionalColumn v) = case testEquality (typeOf v0) (typeOf v) of-                Just Refl -> v-                Nothing -> error "concatManyColumns: OptionalColumn type mismatch"-            getVec _ = error "concatManyColumns: column constructor mismatch"-         in OptionalColumn (VB.concat (v0 : map getVec cs))-    BoxedColumn v0 ->-        let getVec (BoxedColumn v) = case testEquality (typeOf v0) (typeOf v) of-                Just Refl -> v-                Nothing -> error "concatManyColumns: BoxedColumn type mismatch"-            getVec _ = error "concatManyColumns: column constructor mismatch"-         in BoxedColumn (VB.concat (v0 : map getVec cs))-    UnboxedColumn v0 ->-        let getVec (UnboxedColumn v) = case testEquality (typeOf v0) (typeOf v) of-                Just Refl -> v-                Nothing -> error "concatManyColumns: UnboxedColumn type mismatch"-            getVec _ = error "concatManyColumns: column constructor mismatch"-         in UnboxedColumn (VU.concat (v0 : map getVec cs))--concatColumnsEither :: Column -> Column -> Column-concatColumnsEither (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of-    Nothing ->-        OptionalColumn $ fmap (fmap Left) left <> fmap (fmap Right) right-    Just Refl ->-        OptionalColumn $ left <> right-concatColumnsEither (BoxedColumn left) (BoxedColumn right) = case testEquality (typeOf left) (typeOf right) of-    Nothing ->-        BoxedColumn $ fmap Left left <> fmap Right right-    Just Refl ->-        BoxedColumn $ left <> right-concatColumnsEither (UnboxedColumn left) (UnboxedColumn right) = case testEquality (typeOf left) (typeOf right) of-    Nothing ->-        BoxedColumn $ fmap Left (VG.convert left) <> fmap Right (VG.convert right)-    Just Refl ->-        UnboxedColumn $ left <> right-concatColumnsEither (BoxedColumn left) (UnboxedColumn right) =-    BoxedColumn $ fmap Left left <> fmap Right (VG.convert right)-concatColumnsEither (UnboxedColumn left) (BoxedColumn right) =-    BoxedColumn $ fmap Left (VG.convert left) <> fmap Right right-concatColumnsEither (OptionalColumn (left :: VB.Vector (Maybe a))) (BoxedColumn (right :: VB.Vector b)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> OptionalColumn $ left <> fmap Just right-        Nothing -> OptionalColumn $ fmap (fmap Left) left <> fmap (Just . Right) right-concatColumnsEither (BoxedColumn (left :: VB.Vector a)) (OptionalColumn (right :: VB.Vector (Maybe b))) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> OptionalColumn $ fmap Just left <> right-        Nothing -> OptionalColumn $ fmap (Just . Left) left <> fmap (fmap Right) right-concatColumnsEither (OptionalColumn (left :: VB.Vector (Maybe a))) (UnboxedColumn (right :: VU.Vector b)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> OptionalColumn $ left <> fmap Just (VG.convert right)-        Nothing ->-            OptionalColumn $ fmap (fmap Left) left <> fmap (Just . Right) (VG.convert right)-concatColumnsEither (UnboxedColumn (left :: VU.Vector a)) (OptionalColumn (right :: VB.Vector (Maybe b))) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> OptionalColumn $ fmap Just (VG.convert left) <> right-        Nothing ->-            OptionalColumn $ fmap (Just . Left) (VG.convert left) <> fmap (fmap Right) right---- | Allocate a mutable column of size @n@ matching the constructor/type of the given column.-newMutableColumn :: Int -> Column -> IO MutableColumn-newMutableColumn n (OptionalColumn (_ :: VB.Vector (Maybe a))) =-    MOptionalColumn <$> (VBM.new n :: IO (VBM.IOVector (Maybe a)))-newMutableColumn n (BoxedColumn (_ :: VB.Vector a)) =-    MBoxedColumn <$> (VBM.new n :: IO (VBM.IOVector a))-newMutableColumn n (UnboxedColumn (_ :: VU.Vector a)) =-    MUnboxedColumn <$> (VUM.new n :: IO (VUM.IOVector a))---- | Copy a column chunk into a mutable column starting at offset @off@.-copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()-copyIntoMutableColumn (MOptionalColumn (mv :: VBM.IOVector (Maybe b))) off (OptionalColumn (v :: VB.Vector (Maybe a))) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v-        Nothing -> error "copyIntoMutableColumn: Optional type mismatch"-copyIntoMutableColumn (MBoxedColumn (mv :: VBM.IOVector b)) off (BoxedColumn (v :: VB.Vector a)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v-        Nothing -> error "copyIntoMutableColumn: Boxed type mismatch"-copyIntoMutableColumn (MUnboxedColumn (mv :: VUM.IOVector b)) off (UnboxedColumn (v :: VU.Vector a)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> VG.imapM_ (\i x -> VUM.unsafeWrite mv (off + i) x) v-        Nothing -> error "copyIntoMutableColumn: Unboxed type mismatch"-copyIntoMutableColumn _ _ _ =-    error "copyIntoMutableColumn: constructor mismatch"---- | Freeze a mutable column into an immutable column.-freezeMutableColumn :: MutableColumn -> IO Column-freezeMutableColumn (MOptionalColumn mv) = OptionalColumn <$> VB.unsafeFreeze mv-freezeMutableColumn (MBoxedColumn mv) = BoxedColumn <$> VB.unsafeFreeze mv-freezeMutableColumn (MUnboxedColumn mv) = UnboxedColumn <$> VU.unsafeFreeze mv--{- | O(n) Converts a column to a list. Throws an exception if the wrong type is specified.--__Examples:__--@-> column = fromList [(1 :: Int), 2, 3, 4]-> toList @Int column-[1,2,3,4]-> toList @Double column-exception: ...-@--}-toList :: forall a. (Columnable a) => Column -> [a]-toList xs = case toVector @a xs of-    Left err -> throw err-    Right val -> VB.toList val--{- | Converts a column to a vector of a specific type.--This is a type-safe conversion that requires the column's element type-to exactly match the requested type. You must specify the desired type-via type applications.--==== __Type Parameters__--[@a@] The element type to convert to-[@v@] The vector type (e.g., 'VU.Vector', 'VB.Vector')--==== __Examples__-->>> toVector @Int @VU.Vector column-Right (unboxed vector of Ints)-->>> toVector @Text @VB.Vector column-Right (boxed vector of Text)--==== __Returns__--* 'Right' - The converted vector if types match-* 'Left' 'TypeMismatchException' - If the column's type doesn't match the requested type--==== __See also__--For numeric conversions with automatic type coercion, see 'toDoubleVector',-'toFloatVector', and 'toIntVector'.--}-toVector ::-    forall a v.-    (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a)-toVector = \case-    OptionalColumn col -> toVectorWorker col-    BoxedColumn col -> toVectorWorker col-    UnboxedColumn col -> toVectorWorker col-  where-    toVectorWorker ::-        forall c w.-        (Typeable c, VG.Vector w c) =>-        w c ->-        Either DataFrameException (v a)-    toVectorWorker vec = case testEquality (typeRep @a) (typeRep @c) of-        Just Refl -> Right $ VG.convert vec-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @c)-                        , callingFunctionName = Just "toVector"-                        , errorColumnName = Nothing-                        }-                    )---- Some common types we will use for numerical computing.--{- | Converts a column to an unboxed vector of 'Double' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Double', returns it directly-* If the column contains other floating-point types, converts via 'realToFrac'-* If the column contains integral types, converts via 'fromIntegral' (beware of overflow if the type is `Integer`).--==== __Optional column handling__--For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).-This allows optional numeric data to be represented in the resulting vector.--==== __Returns__--* 'Right' - The converted 'Double' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--}-toDoubleVector :: Column -> Either DataFrameException (VU.Vector Double)-toDoubleVector column =-    case column of-        UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Double) of-            Just Refl -> Right f-            Nothing -> case sFloating @a of-                STrue -> Right (VU.map realToFrac f)-                SFalse -> case sIntegral @a of-                    STrue -> Right (VU.map fromIntegral f)-                    SFalse ->-                        Left $-                            TypeMismatchException-                                ( MkTypeErrorContext-                                    { userType = Right (typeRep @Double)-                                    , expectedType = Right (typeRep @a)-                                    , callingFunctionName = Just "toDoubleVector"-                                    , errorColumnName = Nothing-                                    }-                                )-        OptionalColumn (f :: VB.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @Double) of-            Just Refl -> Right (VB.convert $ VB.map (fromMaybe (read @Double "NaN")) f)-            Nothing -> case sFloating @a of-                STrue ->-                    Right-                        (VB.convert $ VB.map (maybe (read @Double "NaN") realToFrac) f)-                SFalse -> case sIntegral @a of-                    STrue ->-                        Right-                            (VB.convert $ VB.map (maybe (read @Double "NaN") fromIntegral) f)-                    SFalse ->-                        Left $-                            TypeMismatchException-                                ( MkTypeErrorContext-                                    { userType = Right (typeRep @Double)-                                    , expectedType = Right (typeRep @a)-                                    , callingFunctionName = Just "toDoubleVector"-                                    , errorColumnName = Nothing-                                    }-                                )-        BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of-            Just Refl -> Right (VB.convert $ VB.map fromIntegral f)-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @Double)-                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())-                            , callingFunctionName = Just "toDoubleVector"-                            , errorColumnName = Nothing-                            }-                        )--{- | Converts a column to an unboxed vector of 'Float' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Float', returns it directly-* If the column contains other floating-point types, converts via 'realToFrac'-* If the column contains integral types, converts via 'fromIntegral'-* If the column is boxed 'Integer', converts via 'fromIntegral' (beware of overflow for 64-bit integers and `Integer`)--==== __Optional column handling__--For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).-This allows optional numeric data to be represented in the resulting vector.--==== __Returns__--* 'Right' - The converted 'Float' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--==== __Precision warning__--Converting from 'Double' to 'Float' may result in loss of precision.--}-toFloatVector :: Column -> Either DataFrameException (VU.Vector Float)-toFloatVector column =-    case column of-        UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Float) of-            Just Refl -> Right f-            Nothing -> case sFloating @a of-                STrue -> Right (VU.map realToFrac f)-                SFalse -> case sIntegral @a of-                    STrue -> Right (VU.map fromIntegral f)-                    SFalse ->-                        Left $-                            TypeMismatchException-                                ( MkTypeErrorContext-                                    { userType = Right (typeRep @Float)-                                    , expectedType = Right (typeRep @a)-                                    , callingFunctionName = Just "toFloatVector"-                                    , errorColumnName = Nothing-                                    }-                                )-        OptionalColumn (f :: VB.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @Float) of-            Just Refl -> Right (VB.convert $ VB.map (fromMaybe (read @Float "NaN")) f)-            Nothing -> case sFloating @a of-                STrue ->-                    Right-                        (VB.convert $ VB.map (maybe (read @Float "NaN") realToFrac) f)-                SFalse -> case sIntegral @a of-                    STrue ->-                        Right-                            (VB.convert $ VB.map (maybe (read @Float "NaN") fromIntegral) f)-                    SFalse ->-                        Left $-                            TypeMismatchException-                                ( MkTypeErrorContext-                                    { userType = Right (typeRep @Float)-                                    , expectedType = Right (typeRep @a)-                                    , callingFunctionName = Just "toFloatVector"-                                    , errorColumnName = Nothing-                                    }-                                )-        BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of-            Just Refl -> Right (VB.convert $ VB.map fromIntegral f)-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @Float)-                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())-                            , callingFunctionName = Just "toFloatVector"-                            , errorColumnName = Nothing-                            }-                        )--{- | Converts a column to an unboxed vector of 'Int' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Int', returns it directly-* If the column contains floating-point types, rounds via 'round' and converts-* If the column contains other integral types, converts via 'fromIntegral'-* If the column is boxed 'Integer', converts via 'fromIntegral'--==== __Returns__--* 'Right' - The converted 'Int' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--==== __Note__--Unlike 'toDoubleVector' and 'toFloatVector', this function does NOT support-'OptionalColumn'. Optional columns must be handled separately.--==== __Rounding behavior__--Floating-point values are rounded to the nearest integer using 'round'.-For example: 2.5 rounds to 2, 3.5 rounds to 4 (banker's rounding).--}-toIntVector :: Column -> Either DataFrameException (VU.Vector Int)-toIntVector column =-    case column of-        UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Int) of-            Just Refl -> Right f-            Nothing -> case sFloating @a of-                STrue -> Right (VU.map (round . realToFrac) f)-                SFalse -> case sIntegral @a of-                    STrue -> Right (VU.map fromIntegral f)-                    SFalse ->-                        Left $-                            TypeMismatchException-                                ( MkTypeErrorContext-                                    { userType = Right (typeRep @Int)-                                    , expectedType = Right (typeRep @a)-                                    , callingFunctionName = Just "toIntVector"-                                    , errorColumnName = Nothing-                                    }-                                )-        BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of-            Just Refl -> Right (VB.convert $ VB.map fromIntegral f)-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @Int)-                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())-                            , callingFunctionName = Just "toIntVector"-                            , errorColumnName = Nothing-                            }-                        )-        _ ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @Int)-                        , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())-                        , callingFunctionName = Just "toIntVector"-                        , errorColumnName = Nothing-                        }-                    )--toUnboxedVector ::-    forall a.-    (Columnable a, VU.Unbox a) => Column -> Either DataFrameException (VU.Vector a)-toUnboxedVector column =-    case column of-        UnboxedColumn (f :: VU.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of-            Just Refl -> Right f-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @Int)-                            , expectedType = Right (typeRep @a)-                            , callingFunctionName = Just "toUnboxedVector"-                            , errorColumnName = Nothing-                            }-                        )-        _ ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())-                        , callingFunctionName = Just "toUnboxedVector"-                        , errorColumnName = Nothing-                        }-                    )-{-# SPECIALIZE toUnboxedVector ::-    Column -> Either DataFrameException (VU.Vector Double)-    #-}+import Control.Monad (forM_, when)+import Control.Monad.ST (runST)+import Data.Bits (+    complement,+    popCount,+    setBit,+    shiftL,+    shiftR,+    testBit,+    (.&.),+ )+import Data.Kind (Type)+import Data.Maybe+import Data.These+import Data.Type.Equality (TestEquality (..))+import Data.Word (Word8)+import DataFrame.Errors+import DataFrame.Internal.Parsing+import DataFrame.Internal.Types+import System.IO.Unsafe (unsafePerformIO)+import Type.Reflection++-- | A bit-packed validity bitmap. Bit @i@ = 1 means row @i@ is valid (not null).+type Bitmap = VU.Vector Word8++{- | Our representation of a column is a GADT that can store data based on the underlying data.++This allows us to pattern match on data kinds and limit some operations to only some+kinds of vectors. Nullability is represented via an optional bit-packed 'Bitmap':+@Nothing@ = no nulls; @Just bm@ = bit @i@ of @bm@ is 1 iff row @i@ is valid.+-}+data Column where+    BoxedColumn :: (Columnable a) => Maybe Bitmap -> VB.Vector a -> Column+    UnboxedColumn ::+        (Columnable a, VU.Unbox a) => Maybe Bitmap -> VU.Vector a -> Column++{- | A mutable companion struct to dataframe columns.++Used mostly as an intermediate structure for I/O.+-}+data MutableColumn where+    MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn+    MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn++-- ---------------------------------------------------------------------------+-- Bitmap helpers+-- ---------------------------------------------------------------------------++-- | Test whether row @i@ is valid (not null) in a bitmap.+bitmapTestBit :: Bitmap -> Int -> Bool+bitmapTestBit bm i = testBit (VU.unsafeIndex bm (i `shiftR` 3)) (i .&. 7)+{-# INLINE bitmapTestBit #-}++-- | Build a fully-valid bitmap for @n@ rows (all bits set).+allValidBitmap :: Int -> Bitmap+allValidBitmap n =+    let bytes = (n + 7) `shiftR` 3+        lastBits = n .&. 7+        full = VU.replicate (bytes - 1) 0xFF+        lastByte = if lastBits == 0 then 0xFF else (1 `shiftL` lastBits) - 1+     in if bytes == 0 then VU.empty else VU.snoc full lastByte+{-# INLINE allValidBitmap #-}++{- | Build a bitmap from a @VU.Vector Word8@ validity vector+(1 = valid, 0 = null), as produced by Arrow / Parquet decoders.+-}+buildBitmapFromValid :: VU.Vector Word8 -> Bitmap+buildBitmapFromValid valid =+    let n = VU.length valid+        bytes = (n + 7) `shiftR` 3+     in VU.generate bytes $ \b ->+            let base = b `shiftL` 3+                setBitIf acc bit =+                    let idx = base + bit+                     in if idx < n && VU.unsafeIndex valid idx /= 0+                            then setBit acc bit+                            else acc+             in foldl setBitIf (0 :: Word8) [0 .. 7]++{- | Build a bitmap from a list of null-row indices.+@nullIdxs@ are the positions that are NULL.+-}+buildBitmapFromNulls :: Int -> [Int] -> Bitmap+buildBitmapFromNulls n nullIdxs =+    let base = allValidBitmap n+     in VU.modify+            ( \mv ->+                forM_ nullIdxs $ \i -> do+                    let byteIdx = i `shiftR` 3+                        bitIdx = i .&. 7+                    v <- VUM.unsafeRead mv byteIdx+                    VUM.unsafeWrite mv byteIdx (clearBit8 v bitIdx)+            )+            base+  where+    clearBit8 :: Word8 -> Int -> Word8+    clearBit8 b bit = b .&. complement (1 `shiftL` bit)++-- | Slice a bitmap for rows @[start .. start+len-1]@.+bitmapSlice :: Int -> Int -> Bitmap -> Bitmap+bitmapSlice start len bm+    | start .&. 7 == 0 =+        -- byte-aligned: simple slice; clamp so we never ask for more bytes than exist+        let startByte = start `shiftR` 3+            bytes = min ((len + 7) `shiftR` 3) (VU.length bm - startByte)+         in VU.slice startByte bytes bm+    | otherwise =+        -- non-aligned: unpack bit-by-bit and repack+        let n = min len (VU.length bm `shiftL` 3 - start)+         in buildBitmapFromValid $+                VU.generate n $+                    \i -> if bitmapTestBit bm (start + i) then 1 else 0++-- | Concatenate two bitmaps covering @n1@ and @n2@ rows respectively.+bitmapConcat :: Int -> Bitmap -> Int -> Bitmap -> Bitmap+bitmapConcat n1 bm1 n2 bm2 =+    buildBitmapFromValid $+        VU.generate (n1 + n2) $ \i ->+            if i < n1+                then if bitmapTestBit bm1 i then 1 else 0+                else if bitmapTestBit bm2 (i - n1) then 1 else 0++-- | Combine two bitmaps with AND (both must be valid for result to be valid).+mergeBitmaps :: Bitmap -> Bitmap -> Bitmap+mergeBitmaps = VU.zipWith (.&.)++{- | Materialize a nullable column from @VB.Vector (Maybe a)@.+When @a@ is unboxable, creates an 'UnboxedColumn' (more compact).+Otherwise creates a 'BoxedColumn'.+Always attaches a bitmap so the column is recognized as nullable even when+no 'Nothing' values are present (preserves the Maybe type marker).+-}+fromMaybeVec :: forall a. (Columnable a) => VB.Vector (Maybe a) -> Column+fromMaybeVec v = case sUnbox @a of+    STrue -> fromMaybeVecUnboxed v+    SFalse ->+        let n = VB.length v+            nullIdxs = [i | i <- [0 .. n - 1], isNothing (VB.unsafeIndex v i)]+            bm = if null nullIdxs then allValidBitmap n else buildBitmapFromNulls n nullIdxs+            dat = VB.map (fromMaybe (errorWithoutStackTrace "fromMaybeVec: Nothing slot")) v+         in BoxedColumn (Just bm) dat++{- | Materialize a nullable 'UnboxedColumn' to @VB.Vector (Maybe a)@ using runST.+Always attaches a bitmap so the column is recognized as nullable even when+no 'Nothing' values are present (preserves the Maybe type marker).+-}+fromMaybeVecUnboxed ::+    forall a. (Columnable a, VU.Unbox a) => VB.Vector (Maybe a) -> Column+fromMaybeVecUnboxed v =+    let n = VB.length v+        nullIdxs = [i | i <- [0 .. n - 1], isNothing (VB.unsafeIndex v i)]+        bm = if null nullIdxs then allValidBitmap n else buildBitmapFromNulls n nullIdxs+        dat = runST $ do+            mv <- VUM.new n+            VG.iforM_ v $ \i mx -> forM_ mx (VUM.unsafeWrite mv i)+            VU.unsafeFreeze mv+     in UnboxedColumn (Just bm) dat++-- | Materialize an element from a column at index @i@, respecting the bitmap.+columnElemIsNull :: Column -> Int -> Bool+columnElemIsNull (BoxedColumn (Just bm) _) i = not (bitmapTestBit bm i)+columnElemIsNull (UnboxedColumn (Just bm) _) i = not (bitmapTestBit bm i)+columnElemIsNull _ _ = False++-- | Return the 'Maybe Bitmap' from a column.+columnBitmap :: Column -> Maybe Bitmap+columnBitmap (BoxedColumn bm _) = bm+columnBitmap (UnboxedColumn bm _) = bm++-- ---------------------------------------------------------------------------+-- End bitmap helpers+-- ---------------------------------------------------------------------------++{- | A TypedColumn is a wrapper around our type-erased column.+It is used to type check expressions on columns.++Note: there is no guarantee that the Phanton type is the+same as the underlying vector type.+-}+data TypedColumn a where+    TColumn :: (Columnable a) => Column -> TypedColumn a++instance (Eq a) => Eq (TypedColumn a) where+    (==) :: (Eq a) => TypedColumn a -> TypedColumn a -> Bool+    (==) (TColumn a) (TColumn b) = a == b++instance (Ord a) => Ord (TypedColumn a) where+    compare :: (Ord a) => TypedColumn a -> TypedColumn a -> Ordering+    compare (TColumn a) (TColumn b) = compare a b++-- | Gets the underlying value from a TypedColumn.+unwrapTypedColumn :: TypedColumn a -> Column+unwrapTypedColumn (TColumn value) = value++-- | Gets the underlying vector from a TypedColumn.+vectorFromTypedColumn :: TypedColumn a -> VB.Vector a+vectorFromTypedColumn (TColumn value) = either throw id (toVector value)++-- | Checks if a column contains missing values (has a bitmap).+hasMissing :: Column -> Bool+hasMissing (BoxedColumn (Just _) _) = True+hasMissing (UnboxedColumn (Just _) _) = True+hasMissing _ = False++-- | Checks if a column contains only missing values.+allMissing :: Column -> Bool+allMissing (BoxedColumn (Just bm) col) = VU.all (== 0) bm && not (VB.null col)+allMissing (UnboxedColumn (Just bm) col) = VU.all (== 0) bm && not (VU.null col)+allMissing _ = False++-- | Checks if a column contains numeric values.+isNumeric :: Column -> Bool+isNumeric (UnboxedColumn _ (vec :: VU.Vector a)) = case sNumeric @a of+    STrue -> True+    _ -> False+isNumeric (BoxedColumn _ (vec :: VB.Vector a)) = case testEquality (typeRep @a) (typeRep @Integer) of+    Nothing -> False+    Just Refl -> True++{- | Checks if a column is of a given type values.+For nullable columns (@BoxedColumn (Just _)@ or @UnboxedColumn (Just _)@),+also returns @True@ when @a = Maybe b@ and the column stores @b@ internally.+-}+hasElemType :: forall a. (Columnable a) => Column -> Bool+hasElemType = \case+    BoxedColumn bm (column :: VB.Vector b) -> checkBoxed bm (typeRep @b)+    UnboxedColumn bm (column :: VU.Vector b) -> checkUnboxed bm (typeRep @b)+  where+    -- Direct type match+    directMatch :: forall (b :: Type). TypeRep b -> Bool+    directMatch = isJust . testEquality (typeRep @a)+    -- For a nullable column (has bitmap), also accept a = Maybe b+    checkMaybe :: forall (b :: Type). TypeRep b -> Bool+    checkMaybe tb = case typeRep @a of+        App tMaybe tInner -> case eqTypeRep tMaybe (typeRep @Maybe) of+            Just HRefl -> isJust (testEquality tInner tb)+            Nothing -> False+        _ -> False+    checkBoxed :: forall (b :: Type). Maybe Bitmap -> TypeRep b -> Bool+    checkBoxed bm tb = directMatch tb || (isJust bm && checkMaybe tb)+    checkUnboxed :: forall (b :: Type). Maybe Bitmap -> TypeRep b -> Bool+    checkUnboxed bm tb = directMatch tb || (isJust bm && checkMaybe tb)++-- | An internal/debugging function to get the column type of a column.+columnVersionString :: Column -> String+columnVersionString column = case column of+    BoxedColumn Nothing _ -> "Boxed"+    BoxedColumn (Just _) _ -> "NullableBoxed"+    UnboxedColumn Nothing _ -> "Unboxed"+    UnboxedColumn (Just _) _ -> "NullableUnboxed"++{- | An internal/debugging function to get the type stored in the outermost vector+of a column.+-}+columnTypeString :: Column -> String+columnTypeString column = case column of+    BoxedColumn Nothing (_ :: VB.Vector a) -> show (typeRep @a)+    BoxedColumn (Just _) (_ :: VB.Vector a) -> showMaybeType @a+    UnboxedColumn Nothing (_ :: VU.Vector a) -> show (typeRep @a)+    UnboxedColumn (Just _) (_ :: VU.Vector a) -> showMaybeType @a+  where+    showMaybeType :: forall a. (Typeable a) => String+    showMaybeType =+        let s = show (typeRep @a)+         in "Maybe " ++ if ' ' `elem` s then "(" ++ s ++ ")" else s++instance (Show a) => Show (TypedColumn a) where+    show :: (Show a) => TypedColumn a -> String+    show (TColumn col) = show col++instance NFData Column where+    rnf (BoxedColumn Nothing (v :: VB.Vector a)) = VB.foldl' (const (`seq` ())) () v+    rnf (BoxedColumn (Just bm) (v :: VB.Vector a)) =+        let n = VB.length v+            go !i+                | i >= n = ()+                | bitmapTestBit bm i = VB.unsafeIndex v i `seq` go (i + 1)+                | otherwise = go (i + 1)+         in go 0+    rnf (UnboxedColumn _ v) = v `seq` ()++instance Show Column where+    show :: Column -> String+    show (BoxedColumn Nothing column) = show column+    show (BoxedColumn (Just bm) column) =+        let n = VB.length column+            elems =+                [ if bitmapTestBit bm i then show (VB.unsafeIndex column i) else "null"+                | i <- [0 .. n - 1]+                ]+         in "[" ++ foldl (\acc e -> if null acc then e else acc ++ "," ++ e) "" elems ++ "]"+    show (UnboxedColumn Nothing column) = show column+    show (UnboxedColumn (Just bm) column) =+        let n = VU.length column+            elems =+                [ if bitmapTestBit bm i then show (VU.unsafeIndex column i) else "null"+                | i <- [0 .. n - 1]+                ]+         in "[" ++ foldl (\acc e -> if null acc then e else acc ++ "," ++ e) "" elems ++ "]"++{- | Compare two nullable boxed columns element by element, skipping null slots.+Uses a manual loop to avoid stream fusion forcing null-slot error thunks.+-}+eqBoxedCols ::+    (Eq a) => Maybe Bitmap -> VB.Vector a -> Maybe Bitmap -> VB.Vector a -> Bool+eqBoxedCols bm1 a bm2 b+    | VB.length a /= VB.length b = False+    | otherwise = go 0+  where+    !n = VB.length a+    go !i+        | i >= n = True+        | nullA || nullB = (nullA == nullB) && go (i + 1)+        | VB.unsafeIndex a i == VB.unsafeIndex b i = go (i + 1)+        | otherwise = False+      where+        nullA = maybe False (\bm -> not (bitmapTestBit bm i)) bm1+        nullB = maybe False (\bm -> not (bitmapTestBit bm i)) bm2+{-# INLINE eqBoxedCols #-}++instance Eq Column where+    (==) :: Column -> Column -> Bool+    (==) (BoxedColumn bm1 (a :: VB.Vector t1)) (BoxedColumn bm2 (b :: VB.Vector t2)) =+        case testEquality (typeRep @t1) (typeRep @t2) of+            Nothing -> False+            Just Refl -> eqBoxedCols bm1 a bm2 b+    (==) (UnboxedColumn bm1 (a :: VU.Vector t1)) (UnboxedColumn bm2 (b :: VU.Vector t2)) =+        case testEquality (typeRep @t1) (typeRep @t2) of+            Nothing -> False+            Just Refl ->+                VU.length a == VU.length b+                    && VU.and+                        ( VU.imap+                            ( \i x ->+                                let nullA = maybe False (\bm -> not (bitmapTestBit bm i)) bm1+                                    nullB = maybe False (\bm -> not (bitmapTestBit bm i)) bm2+                                 in if nullA || nullB then nullA == nullB else x == VU.unsafeIndex b i+                            )+                            a+                        )+    (==) _ _ = False++-- Generalised LEQ that does reflection.+generalLEQ ::+    forall a b. (Typeable a, Typeable b, Ord a, Ord b) => a -> b -> Bool+generalLEQ x y = case testEquality (typeRep @a) (typeRep @b) of+    Nothing -> False+    Just Refl -> x <= y++instance Ord Column where+    (<=) :: Column -> Column -> Bool+    (<=) (BoxedColumn _ (a :: VB.Vector t1)) (BoxedColumn _ (b :: VB.Vector t2)) = generalLEQ a b+    (<=) (UnboxedColumn _ (a :: VU.Vector t1)) (UnboxedColumn _ (b :: VU.Vector t2)) = generalLEQ a b+    (<=) _ _ = False++{- | A class for converting a vector to a column of the appropriate type.+Given each Rep we tell the `toColumnRep` function which Column type to pick.+-}+class ColumnifyRep (r :: Rep) a where+    toColumnRep :: VB.Vector a -> Column++-- | Constraint synonym for what we can put into columns.+type Columnable a =+    ( Columnable' a+    , ColumnifyRep (KindOf a) a+    , UnboxIf a+    , IntegralIf a+    , FloatingIf a+    , SBoolI (Unboxable a)+    , SBoolI (Numeric a)+    , SBoolI (IntegralTypes a)+    , SBoolI (FloatingTypes a)+    )++instance+    (Columnable a, VU.Unbox a) =>+    ColumnifyRep 'RUnboxed a+    where+    toColumnRep :: (Columnable a, VUM.Unbox a) => VB.Vector a -> Column+    toColumnRep v = UnboxedColumn Nothing (VU.convert v)++instance+    (Columnable a) =>+    ColumnifyRep 'RBoxed a+    where+    toColumnRep :: (Columnable a) => VB.Vector a -> Column+    toColumnRep = BoxedColumn Nothing++instance+    (Columnable a) =>+    ColumnifyRep 'RNullableBoxed (Maybe a)+    where+    toColumnRep :: (Columnable a) => VB.Vector (Maybe a) -> Column+    toColumnRep = fromMaybeVec++{- | O(n) Convert a vector to a column. Automatically picks the best representation of a vector to store the underlying data in.++__Examples:__++@+> import qualified Data.Vector as V+> fromVector (VB.fromList [(1 :: Int), 2, 3, 4])+[1,2,3,4]+@+-}+fromVector ::+    forall a.+    (Columnable a, ColumnifyRep (KindOf a) a) =>+    VB.Vector a -> Column+fromVector = toColumnRep @(KindOf a)++{- | O(n) Convert an unboxed vector to a column. This avoids the extra conversion if you already have the data in an unboxed vector.++__Examples:__++@+> import qualified Data.Vector.Unboxed as V+> fromUnboxedVector (VB.fromList [(1 :: Int), 2, 3, 4])+[1,2,3,4]+@+-}+fromUnboxedVector ::+    forall a. (Columnable a, VU.Unbox a) => VU.Vector a -> Column+fromUnboxedVector = UnboxedColumn Nothing++{- | O(n) Convert a list to a column. Automatically picks the best representation of a vector to store the underlying data in.++__Examples:__++@+> fromList [(1 :: Int), 2, 3, 4]+[1,2,3,4]+@+-}+fromList ::+    forall a.+    (Columnable a, ColumnifyRep (KindOf a) a) =>+    [a] -> Column+fromList = toColumnRep @(KindOf a) . VB.fromList++-- An internal helper for type errors+throwTypeMismatch ::+    forall (a :: Type) (b :: Type).+    (Typeable a, Typeable b) => Either DataFrameException Column+throwTypeMismatch =+    Left $+        TypeMismatchException+            MkTypeErrorContext+                { userType = Right (typeRep @b)+                , expectedType = Right (typeRep @a)+                , callingFunctionName = Nothing+                , errorColumnName = Nothing+                }++-- | An internal function to map a function over the values of a column.+mapColumn ::+    forall b c.+    (Columnable b, Columnable c) =>+    (b -> c) -> Column -> Either DataFrameException Column+mapColumn f = \case+    BoxedColumn bm (col :: VB.Vector a) -> runBoxed bm col+    UnboxedColumn bm (col :: VU.Vector a) -> runUnboxed bm col+  where+    runBoxed ::+        forall a.+        (Columnable a) =>+        Maybe Bitmap -> VB.Vector a -> Either DataFrameException Column+    runBoxed bm col = case testEquality (typeRep @b) (typeRep @(Maybe a)) of+        -- user maps over Maybe a (nullable column as Maybe)+        Just Refl ->+            let !n = VB.length col+             in -- Build result directly without intermediate Maybe vector to avoid+                -- fusion forcing null slots via VU.convert.+                Right $ case sUnbox @c of+                    STrue -> UnboxedColumn Nothing $+                        VU.generate n $ \i ->+                            f+                                ( if maybe True (`bitmapTestBit` i) bm+                                    then Just (VB.unsafeIndex col i)+                                    else Nothing+                                )+                    SFalse -> fromVector @c $+                        VB.generate n $ \i ->+                            f+                                ( if maybe True (`bitmapTestBit` i) bm+                                    then Just (VB.unsafeIndex col i)+                                    else Nothing+                                )+        Nothing -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl ->+                -- user maps over inner type a; preserve bitmap+                Right $ case sUnbox @c of+                    STrue -> UnboxedColumn bm (VU.generate (VB.length col) (f . VB.unsafeIndex col))+                    SFalse -> BoxedColumn bm (VB.map f col)+            Nothing -> throwTypeMismatch @a @b++    runUnboxed ::+        forall a.+        (Columnable a, VU.Unbox a) =>+        Maybe Bitmap -> VU.Vector a -> Either DataFrameException Column+    runUnboxed bm col = case testEquality (typeRep @b) (typeRep @(Maybe a)) of+        Just Refl ->+            let !n = VU.length col+             in Right $ case sUnbox @c of+                    STrue -> UnboxedColumn Nothing $+                        VU.generate n $ \i ->+                            f+                                ( if maybe True (`bitmapTestBit` i) bm+                                    then Just (VU.unsafeIndex col i)+                                    else Nothing+                                )+                    SFalse -> fromVector @c $+                        VB.generate n $ \i ->+                            f+                                ( if maybe True (`bitmapTestBit` i) bm+                                    then Just (VU.unsafeIndex col i)+                                    else Nothing+                                )+        Nothing -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> Right $ case sUnbox @c of+                STrue -> UnboxedColumn bm (VU.map f col)+                SFalse -> BoxedColumn bm (VB.generate (VU.length col) (f . VU.unsafeIndex col))+            Nothing -> throwTypeMismatch @a @b+{-# INLINEABLE mapColumn #-}++-- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.+imapColumn ::+    forall b c.+    (Columnable b, Columnable c) =>+    (Int -> b -> c) -> Column -> Either DataFrameException Column+imapColumn f = \case+    BoxedColumn bm (col :: VB.Vector a) -> runBoxed bm col+    UnboxedColumn bm (col :: VU.Vector a) -> runUnboxed bm col+  where+    runBoxed ::+        forall a.+        (Columnable a) =>+        Maybe Bitmap -> VB.Vector a -> Either DataFrameException Column+    runBoxed bm col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right $ case sUnbox @c of+            STrue ->+                UnboxedColumn+                    bm+                    (VU.generate (VB.length col) (\i -> f i (VB.unsafeIndex col i)))+            SFalse -> BoxedColumn bm (VB.imap f col)+        Nothing -> throwTypeMismatch @a @b++    runUnboxed ::+        forall a.+        (Columnable a, VU.Unbox a) =>+        Maybe Bitmap -> VU.Vector a -> Either DataFrameException Column+    runUnboxed bm col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right $ case sUnbox @c of+            STrue -> UnboxedColumn bm (VU.imap f col)+            SFalse -> BoxedColumn bm (VB.imap f (VG.convert col))+        Nothing -> throwTypeMismatch @a @b++-- | O(1) Gets the number of elements in the column.+columnLength :: Column -> Int+columnLength (BoxedColumn _ xs) = VB.length xs+columnLength (UnboxedColumn _ xs) = VU.length xs+{-# INLINE columnLength #-}++-- | O(n) Gets the number of non-null elements in the column.+numElements :: Column -> Int+numElements (BoxedColumn Nothing xs) = VB.length xs+numElements (BoxedColumn (Just bm) xs) = VU.foldl' (\acc b -> acc + popCount b) 0 bm+numElements (UnboxedColumn Nothing xs) = VU.length xs+numElements (UnboxedColumn (Just bm) xs) = VU.foldl' (\acc b -> acc + popCount b) 0 bm+{-# INLINE numElements #-}++-- | O(n) Takes the first n values of a column.+takeColumn :: Int -> Column -> Column+takeColumn n (BoxedColumn bm xs) =+    BoxedColumn (fmap (bitmapSlice 0 n) bm) (VG.take n xs)+takeColumn n (UnboxedColumn bm xs) =+    UnboxedColumn (fmap (bitmapSlice 0 n) bm) (VG.take n xs)+{-# INLINE takeColumn #-}++-- | O(n) Takes the last n values of a column.+takeLastColumn :: Int -> Column -> Column+takeLastColumn n column = sliceColumn (columnLength column - n) n column+{-# INLINE takeLastColumn #-}++-- | O(n) Takes n values after a given column index.+sliceColumn :: Int -> Int -> Column -> Column+sliceColumn start n (BoxedColumn bm xs) =+    BoxedColumn (fmap (bitmapSlice start n) bm) (VG.slice start n xs)+sliceColumn start n (UnboxedColumn bm xs) =+    UnboxedColumn (fmap (bitmapSlice start n) bm) (VG.slice start n xs)+{-# INLINE sliceColumn #-}++-- | O(n) Selects the elements at a given set of indices. Does not change the order.+atIndicesStable :: VU.Vector Int -> Column -> Column+atIndicesStable indexes (BoxedColumn bm column) =+    BoxedColumn+        ( fmap+            ( \bm0 ->+                buildBitmapFromValid $+                    VU.map (\i -> if bitmapTestBit bm0 i then 1 else 0) indexes+            )+            bm+        )+        ( VB.generate+            (VU.length indexes)+            ((column `VB.unsafeIndex`) . (indexes `VU.unsafeIndex`))+        )+atIndicesStable indexes (UnboxedColumn bm column) =+    UnboxedColumn+        ( fmap+            ( \bm0 ->+                buildBitmapFromValid $+                    VU.map (\i -> if bitmapTestBit bm0 i then 1 else 0) indexes+            )+            bm+        )+        (VU.unsafeBackpermute column indexes)+{-# INLINE atIndicesStable #-}++{- | Like 'atIndicesStable' but treats negative indices as null.+Keeps the index vector fully unboxed (no @VB.Vector (Maybe Int)@).+-}+gatherWithSentinel :: VU.Vector Int -> Column -> Column+gatherWithSentinel indices col =+    let !n = VU.length indices+        newBm = buildBitmapFromValid $ VU.generate n $ \i ->+            if VU.unsafeIndex indices i < 0 then 0 else 1+     in case col of+            BoxedColumn srcBm v ->+                let dat = VB.generate n $ \i ->+                        let !idx = VU.unsafeIndex indices i+                         in if idx < 0 then VB.unsafeIndex v 0 else VB.unsafeIndex v idx+                    bm = case srcBm of+                        Nothing -> Just newBm+                        Just sb ->+                            Just+                                ( mergeBitmaps+                                    newBm+                                    ( buildBitmapFromValid $ VU.generate n $ \i ->+                                        let idx = VU.unsafeIndex indices i+                                         in if idx >= 0 && bitmapTestBit sb idx then 1 else 0+                                    )+                                )+                 in BoxedColumn bm dat+            UnboxedColumn srcBm v ->+                let dat = runST $ do+                        mv <- VUM.new n+                        VG.iforM_ indices $ \i idx ->+                            when (idx >= 0) $ VUM.unsafeWrite mv i (VU.unsafeIndex v idx)+                        VU.unsafeFreeze mv+                    bm = case srcBm of+                        Nothing -> Just newBm+                        Just sb ->+                            Just+                                ( mergeBitmaps+                                    newBm+                                    ( buildBitmapFromValid $ VU.generate n $ \i ->+                                        let idx = VU.unsafeIndex indices i+                                         in if idx >= 0 && bitmapTestBit sb idx then 1 else 0+                                    )+                                )+                 in UnboxedColumn bm dat+{-# INLINE gatherWithSentinel #-}++-- | Internal helper to get indices in a boxed vector.+getIndices :: VU.Vector Int -> VB.Vector a -> VB.Vector a+getIndices indices xs = VB.generate (VU.length indices) (\i -> xs VB.! (indices VU.! i))+{-# INLINE getIndices #-}++-- | Internal helper to get indices in an unboxed vector.+getIndicesUnboxed :: (VU.Unbox a) => VU.Vector Int -> VU.Vector a -> VU.Vector a+getIndicesUnboxed indices xs = VU.generate (VU.length indices) (\i -> xs VU.! (indices VU.! i))+{-# INLINE getIndicesUnboxed #-}++findIndices ::+    forall a.+    (Columnable a) =>+    (a -> Bool) ->+    Column ->+    Either DataFrameException (VU.Vector Int)+findIndices pred = \case+    BoxedColumn _ (v :: VB.Vector b) -> run v VG.convert+    UnboxedColumn _ (v :: VU.Vector b) -> run v id+  where+    run ::+        forall b v.+        (Typeable b, VG.Vector v b, VG.Vector v Int) =>+        v b ->+        (v Int -> VU.Vector Int) ->+        Either DataFrameException (VU.Vector Int)+    run column finalize = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right . finalize $ VG.findIndices pred column+        Nothing ->+            Left $+                TypeMismatchException+                    MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @b)+                        , callingFunctionName = Just "findIndices"+                        , errorColumnName = Nothing+                        }++-- | An internal function that returns a vector of how indexes change after a column is sorted.+sortedIndexes :: Bool -> Column -> VU.Vector Int+sortedIndexes asc = \case+    BoxedColumn _ column -> sortWorker VG.convert column+    UnboxedColumn _ column -> sortWorker id column+  where+    sortWorker ::+        (VG.Vector v a, Ord a, VG.Vector v (Int, a), VG.Vector v Int) =>+        (v Int -> VU.Vector Int) -> v a -> VU.Vector Int+    sortWorker finalize column = runST $ do+        withIndexes <- VG.thaw $ VG.indexed column+        let cmp = if asc then compare else flip compare+        VA.sortBy (\(_, b) (_, b') -> cmp b b') withIndexes+        sorted <- VG.unsafeFreeze withIndexes+        return $ finalize $ VG.map fst sorted+{-# INLINE sortedIndexes #-}++-- | Fold (right) column with index.+ifoldrColumn ::+    forall a b.+    (Columnable a, Columnable b) =>+    (Int -> a -> b -> b) -> b -> Column -> Either DataFrameException b+ifoldrColumn f acc = \case+    BoxedColumn _ column -> foldrWorker column+    UnboxedColumn _ column -> foldrWorker column+  where+    foldrWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException b+    foldrWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.ifoldr f acc vec+        Nothing ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "ifoldrColumn"+                        , errorColumnName = Nothing+                        }+                    )++foldlColumn ::+    forall a b.+    (Columnable a, Columnable b) =>+    (b -> a -> b) -> b -> Column -> Either DataFrameException b+foldlColumn f acc = \case+    BoxedColumn _ column -> foldlWorker column+    UnboxedColumn _ column -> foldlWorker column+  where+    foldlWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException b+    foldlWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.foldl' f acc vec+        Nothing ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "ifoldrColumn"+                        , errorColumnName = Nothing+                        }+                    )++foldl1Column ::+    forall a.+    (Columnable a) =>+    (a -> a -> a) -> Column -> Either DataFrameException a+foldl1Column f = \case+    BoxedColumn _ column -> foldl1Worker column+    UnboxedColumn _ column -> foldl1Worker column+  where+    foldl1Worker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException a+    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.foldl1' f vec+        Nothing ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldl1Column"+                        , errorColumnName = Nothing+                        }+                    )++{- | O(n) Seedless fold over groups using the first element of each group as seed.+Like 'foldDirectGroups' but for the case where no initial accumulator is available.+-}+foldl1DirectGroups ::+    forall a.+    (Columnable a) =>+    (a -> a -> a) ->+    Column ->+    VU.Vector Int ->+    VU.Vector Int ->+    Either DataFrameException Column+foldl1DirectGroups f col valueIndices offsets+    | VU.length offsets <= 1 = pure $ fromVector @a VB.empty+    | otherwise = case col of+        UnboxedColumn _ (vec :: VU.Vector d) -> UnboxedColumn Nothing <$> foldl1Worker vec+        BoxedColumn _ (vec :: VB.Vector d) -> BoxedColumn Nothing <$> foldl1Worker vec+  where+    foldl1Worker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException (v c)+    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl ->+            Right $+                VG.generate (VU.length offsets - 1) foldGroup+          where+            foldGroup k =+                let !s = VU.unsafeIndex offsets k+                    !e = VU.unsafeIndex offsets (k + 1)+                    !seed = VG.unsafeIndex vec (VU.unsafeIndex valueIndices s)+                 in go (s + 1) e seed+            go !i !e !acc+                | i >= e = acc+                | otherwise =+                    go (i + 1) e $!+                        f acc (VG.unsafeIndex vec (VU.unsafeIndex valueIndices i))+        Nothing ->+            Left $+                TypeMismatchException+                    MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldl1DirectGroups"+                        , errorColumnName = Nothing+                        }+{-# INLINEABLE foldl1DirectGroups #-}++{- | O(n) fold over groups by scanning the column LINEARLY.+rowToGroup[i] = group index for row i.+Avoids random column reads; random writes go to the accumulator array which is+small (nGroups entries) and typically cache-resident.+When @acc@ is unboxable, uses an unboxed mutable vector for the accumulator+array, eliminating pointer indirection on every read/write.+-}+foldLinearGroups ::+    forall b acc.+    (Columnable b, Columnable acc) =>+    (acc -> b -> acc) ->+    acc ->+    Column ->+    VU.Vector Int -> -- rowToGroup (length n)+    Int -> -- nGroups+    Either DataFrameException Column+foldLinearGroups f seed col rowToGroup nGroups+    | nGroups == 0 = Right (fromVector @acc VB.empty)+    | otherwise = case col of+        UnboxedColumn _ (vec :: VU.Vector d) -> foldLinearWorker vec+        BoxedColumn _ (vec :: VB.Vector d) -> foldLinearWorker vec+  where+    foldLinearWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException Column+    foldLinearWorker vec = case testEquality (typeRep @b) (typeRep @c) of+        Just Refl ->+            Right $+                unsafePerformIO $+                    runWith+                        ( \readAt writeAt ->+                            VG.iforM_ vec $ \row x -> do+                                let !k = VG.unsafeIndex rowToGroup row+                                cur <- readAt k+                                writeAt k $! f cur x+                        )+        Nothing ->+            Left $+                TypeMismatchException+                    MkTypeErrorContext+                        { userType = Right (typeRep @b)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldLinearGroups"+                        , errorColumnName = Nothing+                        }++    -- \| Allocate accumulators, run the traversal, return a frozen Column.+    -- When @acc@ is unboxable, uses an unboxed mutable vector (no pointer+    -- indirection per read/write) and returns UnboxedColumn directly —+    -- avoiding a round-trip through VB.Vector.+    runWith :: ((Int -> IO acc) -> (Int -> acc -> IO ()) -> IO ()) -> IO Column+    runWith body = case sUnbox @acc of+        STrue -> do+            accs <- VUM.replicate nGroups seed+            body (VUM.unsafeRead accs) (VUM.unsafeWrite accs)+            UnboxedColumn Nothing <$> VU.unsafeFreeze accs+        SFalse -> do+            accs <- VBM.replicate nGroups seed+            body (VBM.unsafeRead accs) (VBM.unsafeWrite accs)+            fromVector @acc <$> VB.unsafeFreeze accs+    {-# INLINE runWith #-}+{-# INLINEABLE foldLinearGroups #-}++headColumn :: forall a. (Columnable a) => Column -> Either DataFrameException a+headColumn = \case+    BoxedColumn _ col -> headWorker col+    UnboxedColumn _ col -> headWorker col+  where+    headWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException a+    headWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl ->+            if VG.null vec+                then Left (EmptyDataSetException "headColumn")+                else pure (VG.head vec)+        Nothing ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "headColumn"+                        , errorColumnName = Nothing+                        }+                    )++-- | An internal, column version of zip.+zipColumns :: Column -> Column -> Column+zipColumns (BoxedColumn _ column) (BoxedColumn _ other) = BoxedColumn Nothing (VG.zip column other)+zipColumns (BoxedColumn _ column) (UnboxedColumn _ other) =+    BoxedColumn+        Nothing+        ( VB.generate+            (min (VG.length column) (VG.length other))+            (\i -> (column VG.! i, other VG.! i))+        )+zipColumns (UnboxedColumn _ column) (BoxedColumn _ other) =+    BoxedColumn+        Nothing+        ( VB.generate+            (min (VG.length column) (VG.length other))+            (\i -> (column VG.! i, other VG.! i))+        )+zipColumns (UnboxedColumn _ column) (UnboxedColumn _ other) = UnboxedColumn Nothing (VG.zip column other)+{-# INLINE zipColumns #-}++-- | Merge two columns using `These`.+mergeColumns :: Column -> Column -> Column+mergeColumns colA colB = case (colA, colB) of+    (BoxedColumn bmA c1, BoxedColumn bmB c2) -> case (bmA, bmB) of+        (Just ba, Just bb) ->+            BoxedColumn Nothing $ mkVec c1 c2 $ \i v1 v2 ->+                let nullA = not (bitmapTestBit ba i)+                    nullB = not (bitmapTestBit bb i)+                 in case (nullA, nullB) of+                        (True, True) -> error "mergeColumns: both null"+                        (False, True) -> This v1+                        (True, False) -> That v2+                        (False, False) -> These v1 v2+        (Just ba, Nothing) ->+            BoxedColumn Nothing $ mkVec c1 c2 $ \i v1 v2 ->+                if not (bitmapTestBit ba i) then That v2 else These v1 v2+        (Nothing, Just bb) ->+            BoxedColumn Nothing $ mkVec c1 c2 $ \i v1 v2 ->+                if not (bitmapTestBit bb i) then This v1 else These v1 v2+        (Nothing, Nothing) ->+            BoxedColumn Nothing $ mkVecSimple c1 c2 These+    (BoxedColumn _ c1, UnboxedColumn _ c2) ->+        BoxedColumn Nothing $ mkVecSimple c1 c2 These+    (UnboxedColumn _ c1, BoxedColumn _ c2) ->+        BoxedColumn Nothing $ mkVecSimple c1 c2 These+    (UnboxedColumn _ c1, UnboxedColumn _ c2) ->+        BoxedColumn Nothing $ mkVecSimple c1 c2 These+  where+    mkVec c1 c2 combineElements =+        VB.generate+            (min (VG.length c1) (VG.length c2))+            (\i -> combineElements i (c1 VG.! i) (c2 VG.! i))+    {-# INLINE mkVec #-}++    mkVecSimple c1 c2 f =+        VB.generate+            (min (VG.length c1) (VG.length c2))+            (\i -> f (c1 VG.! i) (c2 VG.! i))+    {-# INLINE mkVecSimple #-}+{-# INLINE mergeColumns #-}++-- | An internal, column version of zipWith.+zipWithColumns ::+    forall a b c.+    (Columnable a, Columnable b, Columnable c) =>+    (a -> b -> c) -> Column -> Column -> Either DataFrameException Column+zipWithColumns f (UnboxedColumn bmL (column :: VU.Vector d)) (UnboxedColumn bmR (other :: VU.Vector e)) = case testEquality (typeRep @a) (typeRep @d) of+    Just Refl -> case testEquality (typeRep @b) (typeRep @e) of+        Just Refl+            -- Fast path: both plain unboxed, no bitmaps involved in the output type+            | isNothing bmL+            , isNothing bmR ->+                pure $ case sUnbox @c of+                    STrue -> UnboxedColumn Nothing (VU.zipWith f column other)+                    SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)+        -- Type mismatch or bitmap involvement: fall through to general toVector path+        _ -> zipWithColumnsGeneral f (UnboxedColumn bmL column) (UnboxedColumn bmR other)+    Nothing -> zipWithColumnsGeneral f (UnboxedColumn bmL column) (UnboxedColumn bmR other)+-- TODO: mchavinda - reuse pattern from interpret where we augment the+-- error at the end.+zipWithColumns f left right = zipWithColumnsGeneral f left right++zipWithColumnsGeneral ::+    forall a b c.+    (Columnable a, Columnable b, Columnable c) =>+    (a -> b -> c) -> Column -> Column -> Either DataFrameException Column+zipWithColumnsGeneral f left right = case toVector @a left of+    Left (TypeMismatchException context) ->+        Left $+            TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})+    Left e -> Left e+    Right left' -> case toVector @b right of+        Left (TypeMismatchException context) ->+            Left $+                TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})+        Left e -> Left e+        Right right' -> pure $ fromVector $ VB.zipWith f left' right'+{-# INLINE zipWithColumnsGeneral #-}+{-# INLINE zipWithColumns #-}++-- Functions for mutable columns (intended for IO).+writeColumn :: Int -> T.Text -> MutableColumn -> IO (Either T.Text Bool)+writeColumn i value (MBoxedColumn (col :: VBM.IOVector a)) =+    case testEquality (typeRep @a) (typeRep @T.Text) of+        Just Refl ->+            if isNullish value+                then VBM.unsafeWrite col i "" >> return (Left $! value)+                else VBM.unsafeWrite col i value >> return (Right True)+        Nothing -> return (Left value)+writeColumn i value (MUnboxedColumn (col :: VUM.IOVector a)) =+    case testEquality (typeRep @a) (typeRep @Int) of+        Just Refl -> case readInt value of+            Just v -> VUM.unsafeWrite col i v >> return (Right True)+            Nothing -> VUM.unsafeWrite col i 0 >> return (Left value)+        Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+            Nothing -> return (Left $! value)+            Just Refl -> case readDouble value of+                Just v -> VUM.unsafeWrite col i v >> return (Right True)+                Nothing -> VUM.unsafeWrite col i 0 >> return (Left $! value)+{-# INLINE writeColumn #-}++freezeColumn' :: [(Int, T.Text)] -> MutableColumn -> IO Column+freezeColumn' nulls (MBoxedColumn col)+    | null nulls = BoxedColumn Nothing <$> VB.unsafeFreeze col+    | all (isNullish . snd) nulls = do+        frozen <- VB.unsafeFreeze col+        let n = VB.length frozen+            bm = buildBitmapFromNulls n (map fst nulls)+        return $ BoxedColumn (Just bm) frozen+    | otherwise =+        BoxedColumn Nothing+            . VB.imap+                ( \i v ->+                    if i `elem` map fst nulls+                        then Left (fromMaybe (error "UNEXPECTED ERROR DURING FREEZE") (lookup i nulls))+                        else Right v+                )+            <$> VB.unsafeFreeze col+freezeColumn' nulls (MUnboxedColumn col)+    | null nulls = UnboxedColumn Nothing <$> VU.unsafeFreeze col+    | all (isNullish . snd) nulls = do+        c <- VU.unsafeFreeze col+        let n = VU.length c+            bm = buildBitmapFromNulls n (map fst nulls)+        return $ UnboxedColumn (Just bm) c+    | otherwise = do+        c <- VU.unsafeFreeze col+        return $+            BoxedColumn Nothing $+                VB.generate+                    (VU.length c)+                    ( \i ->+                        if i `elem` map fst nulls+                            then Left (fromMaybe (error "UNEXPECTED ERROR DURING FREEZE") (lookup i nulls))+                            else Right (c VU.! i)+                    )+{-# INLINE freezeColumn' #-}++{- | Promote a non-nullable column to a nullable one (add an all-valid bitmap).+No-op when already nullable.+-}+ensureOptional :: Column -> Column+ensureOptional c@(BoxedColumn (Just _) _) = c+ensureOptional (BoxedColumn Nothing col) =+    BoxedColumn (Just (allValidBitmap (VB.length col))) col+ensureOptional c@(UnboxedColumn (Just _) _) = c+ensureOptional (UnboxedColumn Nothing col) =+    UnboxedColumn (Just (allValidBitmap (VU.length col))) col++-- | Fills the end of a column, up to n, with null rows. Does nothing if column has length >= n.+expandColumn :: Int -> Column -> Column+expandColumn n column@(BoxedColumn bm col)+    | n <= VG.length col = column+    | otherwise =+        let extra = n - VG.length col+            newBm = case bm of+                Nothing -> Just (buildBitmapFromNulls n [VG.length col .. n - 1])+                Just b ->+                    Just+                        (bitmapConcat (VG.length col) b extra (VU.replicate ((extra + 7) `shiftR` 3) 0))+            -- pad data with default (undefined slot, protected by bitmap)+            newCol = col <> VB.replicate extra (errorWithoutStackTrace "expandColumn: null slot")+         in BoxedColumn newBm newCol+expandColumn n column@(UnboxedColumn bm col)+    | n <= VG.length col = column+    | otherwise =+        let extra = n - VG.length col+            newBm = case bm of+                Nothing -> Just (buildBitmapFromNulls n [VG.length col .. n - 1])+                Just b ->+                    Just+                        (bitmapConcat (VG.length col) b extra (VU.replicate ((extra + 7) `shiftR` 3) 0))+            newCol = runST $ do+                mv <- VUM.new n+                VU.imapM_ (VUM.unsafeWrite mv) col+                VU.unsafeFreeze mv+         in UnboxedColumn newBm newCol++-- | Fills the beginning of a column, up to n, with null rows. Does nothing if column has length >= n.+leftExpandColumn :: Int -> Column -> Column+leftExpandColumn n column@(BoxedColumn bm col)+    | n <= VG.length col = column+    | otherwise =+        let extra = n - VG.length col+            origLen = VG.length col+            newBm = case bm of+                Nothing -> Just (buildBitmapFromNulls n [0 .. extra - 1])+                Just b ->+                    let nullPart = VU.replicate ((extra + 7) `shiftR` 3) 0+                     in Just (bitmapConcat extra nullPart origLen b)+            newCol =+                VB.replicate extra (errorWithoutStackTrace "leftExpandColumn: null slot") <> col+         in BoxedColumn newBm newCol+leftExpandColumn n column@(UnboxedColumn bm col)+    | n <= VG.length col = column+    | otherwise =+        let extra = n - VG.length col+            origLen = VG.length col+            newBm = case bm of+                Nothing -> Just (buildBitmapFromNulls n [0 .. extra - 1])+                Just b ->+                    let nullPart = VU.replicate ((extra + 7) `shiftR` 3) 0+                     in Just (bitmapConcat extra nullPart origLen b)+            newCol = runST $ do+                mv <- VUM.new n+                VU.imapM_ (\i x -> VUM.unsafeWrite mv (extra + i) x) col+                VU.unsafeFreeze mv+         in UnboxedColumn newBm newCol++{- | Concatenates two columns.+Returns Nothing if the columns are of different types.+-}+concatColumns :: Column -> Column -> Either DataFrameException Column+concatColumns left right = case (left, right) of+    (BoxedColumn bmL l, BoxedColumn bmR r) -> case testEquality (typeOf l) (typeOf r) of+        Just Refl ->+            let newBm = case (bmL, bmR) of+                    (Nothing, Nothing) -> Nothing+                    (Just bl, Nothing) ->+                        Just+                            (bitmapConcat (VB.length l) bl (VB.length r) (allValidBitmap (VB.length r)))+                    (Nothing, Just br) ->+                        Just+                            (bitmapConcat (VB.length l) (allValidBitmap (VB.length l)) (VB.length r) br)+                    (Just bl, Just br) -> Just (bitmapConcat (VB.length l) bl (VB.length r) br)+             in pure (BoxedColumn newBm (l <> r))+        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))+    (UnboxedColumn bmL l, UnboxedColumn bmR r) -> case testEquality (typeOf l) (typeOf r) of+        Just Refl ->+            let newBm = case (bmL, bmR) of+                    (Nothing, Nothing) -> Nothing+                    (Just bl, Nothing) ->+                        Just+                            (bitmapConcat (VU.length l) bl (VU.length r) (allValidBitmap (VU.length r)))+                    (Nothing, Just br) ->+                        Just+                            (bitmapConcat (VU.length l) (allValidBitmap (VU.length l)) (VU.length r) br)+                    (Just bl, Just br) -> Just (bitmapConcat (VU.length l) bl (VU.length r) br)+             in pure (UnboxedColumn newBm (l <> r))+        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))+    _ -> Left (mismatchErr (typeOf right) (typeOf left))+  where+    mismatchErr ::+        forall (x :: Type) (y :: Type). TypeRep x -> TypeRep y -> DataFrameException+    mismatchErr ta tb =+        withTypeable ta $+            withTypeable tb $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right ta+                        , expectedType = Right tb+                        , callingFunctionName = Just "concatColumns"+                        , errorColumnName = Nothing+                        }+                    )++{- | Concatenates two columns.++Works similar to 'concatColumns', but unlike that function, it will also combine columns of different types+by wrapping the values in an Either.++E.g. combining Column containing [1,2] with Column containing ["a","b"]+will result in a Column containing [Left 1, Left 2, Right "a", Right "b"].+-}++{- | O(n) Concatenate a list of same-type columns in a single allocation.+All columns must have the same constructor and element type (as they will+within a single Parquet column). Calls 'error' on mismatch.+-}+concatManyColumns :: [Column] -> Column+concatManyColumns [] = fromList ([] :: [Maybe Int])+concatManyColumns [c] = c+concatManyColumns (c0 : cs) = case c0 of+    BoxedColumn bm0 v0 ->+        let getCol (BoxedColumn bm v) = case testEquality (typeOf v0) (typeOf v) of+                Just Refl -> (bm, v)+                Nothing -> error "concatManyColumns: BoxedColumn type mismatch"+            getCol _ = error "concatManyColumns: column constructor mismatch"+            rest = map getCol cs+            allVecs = v0 : map snd rest+            allBms = bm0 : map fst rest+            newBm+                | all isNothing allBms = Nothing+                | otherwise =+                    let pairs = zip allVecs allBms+                        expandedBms = map (\(v, mb) -> fromMaybe (allValidBitmap (VB.length v)) mb) pairs+                        go b1 n1 b2 n2 = bitmapConcat n1 b1 n2 b2+                        concatBms [] = VU.empty+                        concatBms [(b, v)] = b+                        concatBms ((b1, v1) : (b2, v2) : rest') =+                            let merged = go b1 (VB.length v1) b2 (VB.length v2)+                             in concatBms ((merged, v1 <> v2) : rest')+                     in Just $ concatBms (zip expandedBms allVecs)+         in BoxedColumn newBm (VB.concat allVecs)+    UnboxedColumn bm0 v0 ->+        let getCol (UnboxedColumn bm v) = case testEquality (typeOf v0) (typeOf v) of+                Just Refl -> (bm, v)+                Nothing -> error "concatManyColumns: UnboxedColumn type mismatch"+            getCol _ = error "concatManyColumns: column constructor mismatch"+            rest = map getCol cs+            allVecs = v0 : map snd rest+            allBms = bm0 : map fst rest+            newBm+                | all isNothing allBms = Nothing+                | otherwise =+                    let pairs = zip allVecs allBms+                        expandedBms = map (\(v, mb) -> fromMaybe (allValidBitmap (VU.length v)) mb) pairs+                        go b1 n1 b2 n2 = bitmapConcat n1 b1 n2 b2+                        concatBms [] = VU.empty+                        concatBms [(b, _)] = b+                        concatBms ((b1, v1) : (b2, v2) : rest') =+                            let merged = go b1 (VU.length v1) b2 (VU.length v2)+                             in concatBms ((merged, v1 <> v2) : rest')+                     in Just $ concatBms (zip expandedBms allVecs)+         in UnboxedColumn newBm (VU.concat allVecs)++concatColumnsEither :: Column -> Column -> Column+concatColumnsEither (BoxedColumn bmL left) (BoxedColumn bmR right) = case testEquality (typeOf left) (typeOf right) of+    Nothing ->+        BoxedColumn Nothing $ fmap Left left <> fmap Right right+    Just Refl ->+        let newBm = case (bmL, bmR) of+                (Nothing, Nothing) -> Nothing+                (Just bl, Nothing) ->+                    Just+                        ( bitmapConcat+                            (VB.length left)+                            bl+                            (VB.length right)+                            (allValidBitmap (VB.length right))+                        )+                (Nothing, Just br) ->+                    Just+                        ( bitmapConcat+                            (VB.length left)+                            (allValidBitmap (VB.length left))+                            (VB.length right)+                            br+                        )+                (Just bl, Just br) -> Just (bitmapConcat (VB.length left) bl (VB.length right) br)+         in BoxedColumn newBm $ left <> right+concatColumnsEither (UnboxedColumn bmL left) (UnboxedColumn bmR right) = case testEquality (typeOf left) (typeOf right) of+    Nothing ->+        BoxedColumn Nothing $+            fmap Left (VG.convert left) <> fmap Right (VG.convert right)+    Just Refl ->+        let newBm = case (bmL, bmR) of+                (Nothing, Nothing) -> Nothing+                (Just bl, Nothing) ->+                    Just+                        ( bitmapConcat+                            (VU.length left)+                            bl+                            (VU.length right)+                            (allValidBitmap (VU.length right))+                        )+                (Nothing, Just br) ->+                    Just+                        ( bitmapConcat+                            (VU.length left)+                            (allValidBitmap (VU.length left))+                            (VU.length right)+                            br+                        )+                (Just bl, Just br) -> Just (bitmapConcat (VU.length left) bl (VU.length right) br)+         in UnboxedColumn newBm $ left <> right+concatColumnsEither (BoxedColumn _ left) (UnboxedColumn _ right) =+    BoxedColumn Nothing $ fmap Left left <> fmap Right (VG.convert right)+concatColumnsEither (UnboxedColumn _ left) (BoxedColumn _ right) =+    BoxedColumn Nothing $ fmap Left (VG.convert left) <> fmap Right right++-- | Allocate a mutable column of size @n@ matching the constructor/type of the given column.+newMutableColumn :: Int -> Column -> IO MutableColumn+newMutableColumn n (BoxedColumn _ (_ :: VB.Vector a)) =+    MBoxedColumn <$> (VBM.new n :: IO (VBM.IOVector a))+newMutableColumn n (UnboxedColumn _ (_ :: VU.Vector a)) =+    MUnboxedColumn <$> (VUM.new n :: IO (VUM.IOVector a))++-- | Copy a column chunk into a mutable column starting at offset @off@.+copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()+copyIntoMutableColumn (MBoxedColumn (mv :: VBM.IOVector b)) off (BoxedColumn _ (v :: VB.Vector a)) =+    case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v+        Nothing -> error "copyIntoMutableColumn: Boxed type mismatch"+copyIntoMutableColumn (MUnboxedColumn (mv :: VUM.IOVector b)) off (UnboxedColumn _ (v :: VU.Vector a)) =+    case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> VG.imapM_ (\i x -> VUM.unsafeWrite mv (off + i) x) v+        Nothing -> error "copyIntoMutableColumn: Unboxed type mismatch"+copyIntoMutableColumn _ _ _ =+    error "copyIntoMutableColumn: constructor mismatch"++-- | Freeze a mutable column into an immutable column.+freezeMutableColumn :: MutableColumn -> IO Column+freezeMutableColumn (MBoxedColumn mv) = BoxedColumn Nothing <$> VB.unsafeFreeze mv+freezeMutableColumn (MUnboxedColumn mv) = UnboxedColumn Nothing <$> VU.unsafeFreeze mv++{- | O(n) Converts a column to a list. Throws an exception if the wrong type is specified.++__Examples:__++@+> column = fromList [(1 :: Int), 2, 3, 4]+> toList @Int column+[1,2,3,4]+> toList @Double column+exception: ...+@+-}+toList :: forall a. (Columnable a) => Column -> [a]+toList xs = case toVector @a xs of+    Left err -> throw err+    Right val -> VB.toList val++{- | Converts a column to a vector of a specific type.++This is a type-safe conversion that requires the column's element type+to exactly match the requested type. You must specify the desired type+via type applications.++==== __Type Parameters__++[@a@] The element type to convert to+[@v@] The vector type (e.g., 'VU.Vector', 'VB.Vector')++==== __Examples__++>>> toVector @Int @VU.Vector column+Right (unboxed vector of Ints)++>>> toVector @Text @VB.Vector column+Right (boxed vector of Text)++==== __Returns__++* 'Right' - The converted vector if types match+* 'Left' 'TypeMismatchException' - If the column's type doesn't match the requested type++==== __See also__++For numeric conversions with automatic type coercion, see 'toDoubleVector',+'toFloatVector', and 'toIntVector'.+-}+toVector ::+    forall a v.+    (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a)+toVector col = case col of+    BoxedColumn bm (inner :: VB.Vector c) ->+        -- Check if user wants Maybe c (nullable) or c directly+        case testEquality (typeRep @a) (typeRep @c) of+            Just Refl -> Right $ VG.convert inner+            Nothing ->+                -- Try: a = Maybe c+                case testEquality (typeRep @a) (typeRep @(Maybe c)) of+                    Just Refl ->+                        -- Use VB.generate to avoid fusion forcing null slots+                        let !n = VB.length inner+                            maybeVec = case bm of+                                Nothing -> VB.generate n (Just . VB.unsafeIndex inner)+                                Just bitmap -> VB.generate n $ \i ->+                                    if bitmapTestBit bitmap i then Just (VB.unsafeIndex inner i) else Nothing+                         in Right $ VG.convert maybeVec+                    Nothing ->+                        Left $+                            TypeMismatchException+                                ( MkTypeErrorContext+                                    { userType = Right (typeRep @a)+                                    , expectedType = Right (typeRep @c)+                                    , callingFunctionName = Just "toVector"+                                    , errorColumnName = Nothing+                                    }+                                )+    UnboxedColumn bm (inner :: VU.Vector c) ->+        case testEquality (typeRep @a) (typeRep @c) of+            Just Refl -> Right $ VG.convert inner+            Nothing ->+                case testEquality (typeRep @a) (typeRep @(Maybe c)) of+                    Just Refl ->+                        let maybeVec = case bm of+                                Nothing -> VB.generate (VU.length inner) (Just . VU.unsafeIndex inner)+                                Just bitmap -> VB.generate (VU.length inner) $ \i ->+                                    if bitmapTestBit bitmap i then Just (VU.unsafeIndex inner i) else Nothing+                         in Right $ VG.convert maybeVec+                    Nothing ->+                        Left $+                            TypeMismatchException+                                ( MkTypeErrorContext+                                    { userType = Right (typeRep @a)+                                    , expectedType = Right (typeRep @c)+                                    , callingFunctionName = Just "toVector"+                                    , errorColumnName = Nothing+                                    }+                                )++-- Some common types we will use for numerical computing.++{- | Converts a column to an unboxed vector of 'Double' values.++This function performs intelligent type coercion for numeric types:++* If the column is already 'Double', returns it directly+* If the column contains other floating-point types, converts via 'realToFrac'+* If the column contains integral types, converts via 'fromIntegral' (beware of overflow if the type is `Integer`).++==== __Optional column handling__++For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).+This allows optional numeric data to be represented in the resulting vector.++==== __Returns__++* 'Right' - The converted 'Double' vector+* 'Left' 'TypeMismatchException' - If the column is not numeric+-}+toDoubleVector :: Column -> Either DataFrameException (VU.Vector Double)+toDoubleVector column =+    case column of+        UnboxedColumn bm (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Double) of+            Just Refl -> case bm of+                Nothing -> Right f+                Just bitmap -> Right $ VU.imap (\i x -> if bitmapTestBit bitmap i then x else read "NaN") f+            Nothing -> case sFloating @a of+                STrue ->+                    Right+                        ( VU.imap+                            ( \i x -> case bm of+                                Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                _ -> realToFrac x+                            )+                            f+                        )+                SFalse -> case sIntegral @a of+                    STrue ->+                        Right+                            ( VU.imap+                                ( \i x -> case bm of+                                    Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                    _ -> fromIntegral x+                                )+                                f+                            )+                    SFalse ->+                        Left $+                            TypeMismatchException+                                ( MkTypeErrorContext+                                    { userType = Right (typeRep @Double)+                                    , expectedType = Right (typeRep @a)+                                    , callingFunctionName = Just "toDoubleVector"+                                    , errorColumnName = Nothing+                                    }+                                )+        BoxedColumn bm (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of+            Just Refl ->+                Right+                    ( VB.convert $+                        VB.imap+                            ( \i x -> case bm of+                                Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                _ -> fromIntegral x+                            )+                            f+                    )+            Nothing ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @Double)+                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+                            , callingFunctionName = Just "toDoubleVector"+                            , errorColumnName = Nothing+                            }+                        )++{- | Converts a column to an unboxed vector of 'Float' values.++This function performs intelligent type coercion for numeric types:++* If the column is already 'Float', returns it directly+* If the column contains other floating-point types, converts via 'realToFrac'+* If the column contains integral types, converts via 'fromIntegral'+* If the column is boxed 'Integer', converts via 'fromIntegral' (beware of overflow for 64-bit integers and `Integer`)++==== __Optional column handling__++For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).+This allows optional numeric data to be represented in the resulting vector.++==== __Returns__++* 'Right' - The converted 'Float' vector+* 'Left' 'TypeMismatchException' - If the column is not numeric++==== __Precision warning__++Converting from 'Double' to 'Float' may result in loss of precision.+-}+toFloatVector :: Column -> Either DataFrameException (VU.Vector Float)+toFloatVector column =+    case column of+        UnboxedColumn bm (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Float) of+            Just Refl -> case bm of+                Nothing -> Right f+                Just bitmap -> Right $ VU.imap (\i x -> if bitmapTestBit bitmap i then x else read "NaN") f+            Nothing -> case sFloating @a of+                STrue ->+                    Right+                        ( VU.imap+                            ( \i x -> case bm of+                                Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                _ -> realToFrac x+                            )+                            f+                        )+                SFalse -> case sIntegral @a of+                    STrue ->+                        Right+                            ( VU.imap+                                ( \i x -> case bm of+                                    Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                    _ -> fromIntegral x+                                )+                                f+                            )+                    SFalse ->+                        Left $+                            TypeMismatchException+                                ( MkTypeErrorContext+                                    { userType = Right (typeRep @Float)+                                    , expectedType = Right (typeRep @a)+                                    , callingFunctionName = Just "toFloatVector"+                                    , errorColumnName = Nothing+                                    }+                                )+        BoxedColumn bm (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of+            Just Refl ->+                Right+                    ( VB.convert $+                        VB.imap+                            ( \i x -> case bm of+                                Just bitmap | not (bitmapTestBit bitmap i) -> read "NaN"+                                _ -> fromIntegral x+                            )+                            f+                    )+            Nothing ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @Float)+                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+                            , callingFunctionName = Just "toFloatVector"+                            , errorColumnName = Nothing+                            }+                        )++{- | Converts a column to an unboxed vector of 'Int' values.++This function performs intelligent type coercion for numeric types:++* If the column is already 'Int', returns it directly+* If the column contains floating-point types, rounds via 'round' and converts+* If the column contains other integral types, converts via 'fromIntegral'+* If the column is boxed 'Integer', converts via 'fromIntegral'++==== __Returns__++* 'Right' - The converted 'Int' vector+* 'Left' 'TypeMismatchException' - If the column is not numeric++==== __Note__++Unlike 'toDoubleVector' and 'toFloatVector', this function does NOT support+'OptionalColumn'. Optional columns must be handled separately.++==== __Rounding behavior__++Floating-point values are rounded to the nearest integer using 'round'.+For example: 2.5 rounds to 2, 3.5 rounds to 4 (banker's rounding).+-}+toIntVector :: Column -> Either DataFrameException (VU.Vector Int)+toIntVector column =+    case column of+        UnboxedColumn _ (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Int) of+            Just Refl -> Right f+            Nothing -> case sFloating @a of+                STrue -> Right (VU.map (round . realToFrac) f)+                SFalse -> case sIntegral @a of+                    STrue -> Right (VU.map fromIntegral f)+                    SFalse ->+                        Left $+                            TypeMismatchException+                                ( MkTypeErrorContext+                                    { userType = Right (typeRep @Int)+                                    , expectedType = Right (typeRep @a)+                                    , callingFunctionName = Just "toIntVector"+                                    , errorColumnName = Nothing+                                    }+                                )+        BoxedColumn _ (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of+            Just Refl -> Right (VB.convert $ VB.map fromIntegral f)+            Nothing ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @Int)+                            , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+                            , callingFunctionName = Just "toIntVector"+                            , errorColumnName = Nothing+                            }+                        )++toUnboxedVector ::+    forall a.+    (Columnable a, VU.Unbox a) => Column -> Either DataFrameException (VU.Vector a)+toUnboxedVector column =+    case column of+        UnboxedColumn _ (f :: VU.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> Right f+            Nothing ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @Int)+                            , expectedType = Right (typeRep @a)+                            , callingFunctionName = Just "toUnboxedVector"+                            , errorColumnName = Nothing+                            }+                        )+        _ ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+                        , callingFunctionName = Just "toUnboxedVector"+                        , errorColumnName = Nothing+                        }+                    ) {-# INLINE toUnboxedVector #-}
src/DataFrame/Internal/DataFrame.hs view
@@ -1,8 +1,10 @@+{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -17,13 +19,18 @@ import Control.Exception (throw) import Data.Function (on) import Data.List (sortBy, transpose, (\\))-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Data.Maybe (fromMaybe)+import Data.Type.Equality (+    TestEquality (testEquality),+    type (:~:) (Refl),+    type (:~~:) (HRefl),+ ) import DataFrame.Display.Terminal.PrettyPrint import DataFrame.Errors import DataFrame.Internal.Column import DataFrame.Internal.Expression import Text.Printf-import Type.Reflection (typeRep)+import Type.Reflection (Typeable, eqTypeRep, typeRep, pattern App) import Prelude hiding (null)  data DataFrame = DataFrame@@ -110,19 +117,34 @@     let header = map fst (sortBy (compare `on` snd) $ M.toList (columnIndices d))         types = V.toList $ V.filter (/= "") $ V.map getType (columns d)         getType :: Column -> T.Text-        getType (BoxedColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)-        getType (UnboxedColumn (column :: VU.Vector a)) = T.pack $ show (typeRep @a)-        getType (OptionalColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)+        showMaybeType :: forall a. (Typeable a) => String+        showMaybeType =+            let s = show (typeRep @a)+             in "Maybe " <> if ' ' `elem` s then "(" <> s <> ")" else s+        getType (BoxedColumn Nothing (_ :: V.Vector a)) = T.pack $ show (typeRep @a)+        getType (BoxedColumn (Just _) (_ :: V.Vector a)) = T.pack $ showMaybeType @a+        getType (UnboxedColumn Nothing (_ :: VU.Vector a)) = T.pack $ show (typeRep @a)+        getType (UnboxedColumn (Just _) (_ :: VU.Vector a)) = T.pack $ showMaybeType @a         -- Separate out cases dynamically so we don't end up making round trip string         -- copies.         get :: Maybe Column -> V.Vector T.Text-        get (Just (BoxedColumn (column :: V.Vector a))) = case testEquality (typeRep @a) (typeRep @T.Text) of+        get (Just (BoxedColumn (Just bm) (column :: V.Vector a))) =+            V.generate (V.length column) $ \i ->+                if bitmapTestBit bm i+                    then T.pack (show (Just (V.unsafeIndex column i)))+                    else "Nothing"+        get (Just (BoxedColumn Nothing (column :: V.Vector a))) = case testEquality (typeRep @a) (typeRep @T.Text) of             Just Refl -> column             Nothing -> case testEquality (typeRep @a) (typeRep @String) of                 Just Refl -> V.map T.pack column                 Nothing -> V.map (T.pack . show) column-        get (Just (UnboxedColumn column)) = V.map (T.pack . show) (V.convert column)-        get (Just (OptionalColumn column)) = V.map (T.pack . show) column+        get (Just (UnboxedColumn (Just bm) column)) =+            let col = V.convert column+             in V.generate (V.length col) $ \i ->+                    if bitmapTestBit bm i+                        then T.pack (show (Just (V.unsafeIndex col i)))+                        else "Nothing"+        get (Just (UnboxedColumn Nothing column)) = V.map (T.pack . show) (V.convert column)         get Nothing = V.empty         getTextColumnFromFrame df (i, name) = get $ (V.!?) (columns d) ((M.!) (columnIndices d) name)         rows =@@ -161,16 +183,16 @@  {- | Retrieves a column by name from the dataframe, throwing an exception if not found. -This is an unsafe version of 'getColumn' that throws 'ColumnNotFoundException'+This is an unsafe version of 'getColumn' that throws 'ColumnsNotFoundException' if the column does not exist. Use this when you are certain the column exists.  ==== __Throws__ -* 'ColumnNotFoundException' - if the column with the given name does not exist+* 'ColumnsNotFoundException' - if the column with the given name does not exist -} unsafeGetColumn :: T.Text -> DataFrame -> Column unsafeGetColumn name df = case getColumn name df of-    Nothing -> throw $ ColumnNotFoundException name "" (M.keys $ columnIndices df)+    Nothing -> throw $ ColumnsNotFoundException [name] "" (M.keys $ columnIndices df)     Just col -> col  {- | Checks if the dataframe is empty (has no columns).@@ -180,3 +202,40 @@ -} null :: DataFrame -> Bool null df = V.null (columns df)++-- | Convert a DataFrame to a CSV (comma-separated) text.+toCsv :: DataFrame -> T.Text+toCsv = toSeparated ','++-- | Convert a DataFrame to a text representation with a custom separator.+toSeparated :: Char -> DataFrame -> T.Text+toSeparated sep df+    | null df = T.empty+    | otherwise =+        let (rows, _) = dataframeDimensions df+            headers = map fst (sortBy (compare `on` snd) (M.toList (columnIndices df)))+            sepText = T.singleton sep+            headerLine = T.intercalate sepText headers+            dataLines = map (T.intercalate sepText . getRowAsText df) [0 .. rows - 1]+         in T.unlines (headerLine : dataLines)++getRowAsText :: DataFrame -> Int -> [T.Text]+getRowAsText df i = map (`showElement` i) (V.toList (columns df))++showElement :: Column -> Int -> T.Text+showElement (BoxedColumn _ (c :: V.Vector a)) i = case c V.!? i of+    Nothing -> error $ "Column index out of bounds at row " ++ show i+    Just e+        | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) -> e+        | App t1 t2 <- typeRep @a+        , Just HRefl <- eqTypeRep t1 (typeRep @Maybe) ->+            case testEquality t2 (typeRep @T.Text) of+                Just Refl -> fromMaybe "null" e+                Nothing -> stripJust (T.pack (show e))+        | otherwise -> T.pack (show e)+showElement (UnboxedColumn _ c) i = case c VU.!? i of+    Nothing -> error $ "Column index out of bounds at row " ++ show i+    Just e -> T.pack (show e)++stripJust :: T.Text -> T.Text+stripJust = fromMaybe "null" . T.stripPrefix "Just "
src/DataFrame/Internal/Interpreter.hs view
@@ -212,18 +212,21 @@ -} sliceGroups :: Column -> VU.Vector Int -> VU.Vector Int -> V.Vector Column sliceGroups col os indices = case col of-    BoxedColumn vec ->-        let !sorted = V.unsafeBackpermute vec (V.convert indices)+    BoxedColumn bm vec ->+        let !sorted =+                V.generate+                    (VU.length indices)+                    ((vec `V.unsafeIndex`) . (indices `VU.unsafeIndex`))          in V.generate nGroups $ \i ->-                BoxedColumn (V.unsafeSlice (start i) (len i) sorted)-    UnboxedColumn vec ->+                BoxedColumn+                    (fmap (bitmapSlice (start i) (len i)) bm)+                    (V.unsafeSlice (start i) (len i) sorted)+    UnboxedColumn bm vec ->         let !sorted = VU.unsafeBackpermute vec indices          in V.generate nGroups $ \i ->-                UnboxedColumn (VU.unsafeSlice (start i) (len i) sorted)-    OptionalColumn vec ->-        let !sorted = V.unsafeBackpermute vec (V.convert indices)-         in V.generate nGroups $ \i ->-                OptionalColumn (V.unsafeSlice (start i) (len i) sorted)+                UnboxedColumn+                    (fmap (bitmapSlice (start i) (len i)) bm)+                    (VU.unsafeSlice (start i) (len i) sorted)   where     !nGroups = VU.length os - 1     start i = os `VU.unsafeIndex` i@@ -272,7 +275,7 @@     (Columnable b) =>     (Either String Double -> b) -> Column -> Either DataFrameException Column promoteToDoubleWith onResult col = case col of-    UnboxedColumn (v :: VU.Vector c) ->+    UnboxedColumn Nothing (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $@@ -284,35 +287,34 @@                         fromVector @b                             (V.map (onResult . Right . (fromIntegral :: c -> Double)) (VG.convert v))                 SFalse -> castMismatch @c @b-    OptionalColumn (v :: V.Vector (Maybe c)) ->+    UnboxedColumn (Just bm) (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $                     fromVector @b-                        ( V.map-                            (maybe (onResult (Left "null")) (onResult . Right . (realToFrac :: c -> Double)))-                            v+                        ( V.generate (VU.length v) $ \i ->+                            if bitmapTestBit bm i+                                then onResult (Right (realToFrac (VU.unsafeIndex v i) :: Double))+                                else onResult (Left "null")                         )             SFalse -> case sIntegral @c of                 STrue ->                     Right $                         fromVector @b-                            ( V.map-                                ( maybe-                                    (onResult (Left "null"))-                                    (onResult . Right . (fromIntegral :: c -> Double))-                                )-                                v+                            ( V.generate (VU.length v) $ \i ->+                                if bitmapTestBit bm i+                                    then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Double))+                                    else onResult (Left "null")                             )-                SFalse -> tryParseWith @Double onResult col-    BoxedColumn _ -> tryParseWith @Double onResult col+                SFalse -> castMismatch @c @b+    BoxedColumn _ _ -> tryParseWith @Double onResult col  promoteToFloatWith ::     forall b.     (Columnable b) =>     (Either String Float -> b) -> Column -> Either DataFrameException Column promoteToFloatWith onResult col = case col of-    UnboxedColumn (v :: VU.Vector c) ->+    UnboxedColumn Nothing (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $@@ -324,32 +326,34 @@                         fromVector @b                             (V.map (onResult . Right . (fromIntegral :: c -> Float)) (VG.convert v))                 SFalse -> castMismatch @c @b-    OptionalColumn (v :: V.Vector (Maybe c)) ->+    UnboxedColumn (Just bm) (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $                     fromVector @b-                        ( V.map-                            (maybe (onResult (Left "null")) (onResult . Right . (realToFrac :: c -> Float)))-                            v+                        ( V.generate (VU.length v) $ \i ->+                            if bitmapTestBit bm i+                                then onResult (Right (realToFrac (VU.unsafeIndex v i) :: Float))+                                else onResult (Left "null")                         )             SFalse -> case sIntegral @c of                 STrue ->                     Right $                         fromVector @b-                            ( V.map-                                (maybe (onResult (Left "null")) (onResult . Right . (fromIntegral :: c -> Float)))-                                v+                            ( V.generate (VU.length v) $ \i ->+                                if bitmapTestBit bm i+                                    then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Float))+                                    else onResult (Left "null")                             )-                SFalse -> tryParseWith @Float onResult col-    BoxedColumn _ -> tryParseWith @Float onResult col+                SFalse -> castMismatch @c @b+    BoxedColumn _ _ -> tryParseWith @Float onResult col  promoteToIntWith ::     forall b.     (Columnable b) =>     (Either String Int -> b) -> Column -> Either DataFrameException Column promoteToIntWith onResult col = case col of-    UnboxedColumn (v :: VU.Vector c) ->+    UnboxedColumn Nothing (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $@@ -361,28 +365,27 @@                         fromVector @b                             (V.map (onResult . Right . (fromIntegral :: c -> Int)) (VG.convert v))                 SFalse -> castMismatch @c @b-    OptionalColumn (v :: V.Vector (Maybe c)) ->+    UnboxedColumn (Just bm) (v :: VU.Vector c) ->         case sFloating @c of             STrue ->                 Right $                     fromVector @b-                        ( V.map-                            ( maybe-                                (onResult (Left "null"))-                                (onResult . Right . (round . (realToFrac :: c -> Double)))-                            )-                            v+                        ( V.generate (VU.length v) $ \i ->+                            if bitmapTestBit bm i+                                then onResult (Right (round (realToFrac (VU.unsafeIndex v i) :: Double)))+                                else onResult (Left "null")                         )             SFalse -> case sIntegral @c of                 STrue ->                     Right $                         fromVector @b-                            ( V.map-                                (maybe (onResult (Left "null")) (onResult . Right . (fromIntegral :: c -> Int)))-                                v+                            ( V.generate (VU.length v) $ \i ->+                                if bitmapTestBit bm i+                                    then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Int))+                                    else onResult (Left "null")                             )-                SFalse -> tryParseWith @Int onResult col-    BoxedColumn _ -> tryParseWith @Int onResult col+                SFalse -> castMismatch @c @b+    BoxedColumn _ _ -> tryParseWith @Int onResult col  -- | Single parse primitive: apply @onResult@ to the result of 'reads'. parseWith :: (Read a) => (Either String a -> b) -> String -> b@@ -395,27 +398,34 @@     (Columnable a, Columnable b) =>     (Either String a -> b) -> Column -> Either DataFrameException Column tryParseWith onResult col = case col of-    BoxedColumn (v :: V.Vector c) ->-        case testEquality (typeRep @c) (typeRep @String) of-            Just Refl -> Right $ fromVector @b $ V.map (parseWith onResult) v-            Nothing ->-                case testEquality (typeRep @c) (typeRep @T.Text) of-                    Just Refl -> Right $ fromVector @b $ V.map (parseWith onResult . T.unpack) v-                    Nothing -> castMismatch @c @b-    OptionalColumn (v :: V.Vector (Maybe c)) ->+    BoxedColumn bm (v :: V.Vector c) ->         case testEquality (typeRep @c) (typeRep @String) of-            Just Refl ->-                Right $-                    fromVector @b $-                        V.map (maybe (onResult (Left "null")) (parseWith onResult)) v+            Just Refl -> case bm of+                Nothing -> Right $ fromVector @b $ V.map (parseWith onResult) v+                Just bitmap ->+                    Right $+                        fromVector @b $+                            V.imap+                                ( \i x ->+                                    if bitmapTestBit bitmap i then parseWith onResult x else onResult (Left "null")+                                )+                                v             Nothing ->                 case testEquality (typeRep @c) (typeRep @T.Text) of-                    Just Refl ->-                        Right $-                            fromVector @b $-                                V.map (maybe (onResult (Left "null")) (parseWith onResult . T.unpack)) v+                    Just Refl -> case bm of+                        Nothing -> Right $ fromVector @b $ V.map (parseWith onResult . T.unpack) v+                        Just bitmap ->+                            Right $+                                fromVector @b $+                                    V.imap+                                        ( \i x ->+                                            if bitmapTestBit bitmap i+                                                then parseWith onResult (T.unpack x)+                                                else onResult (Left "null")+                                        )+                                        v                     Nothing -> castMismatch @c @b-    UnboxedColumn (_ :: VU.Vector c) -> castMismatch @c @b+    UnboxedColumn _ (_ :: VU.Vector c) -> castMismatch @c @b  {- | When the output type @b@ is @Maybe c@ (or @Maybe (Maybe c)@) and the column stores plain @c@ values, wrap each element in 'Just'.@@ -430,16 +440,15 @@     (Columnable a, Columnable b) =>     (Either String a -> b) -> Column -> Maybe (Either DataFrameException Column) tryMaybeWrap _onResult col = case col of-    UnboxedColumn (v :: VU.Vector c) ->+    UnboxedColumn Nothing (v :: VU.Vector c) ->         let wrapped = V.map Just (VG.convert v) :: V.Vector (Maybe c)          in case testEquality (typeRep @b) (typeRep @(Maybe c)) of                 Just Refl -> Just $ Right $ fromVector @b wrapped                 Nothing ->-                    -- join: b = Maybe (Maybe c) → produce Maybe c column                     case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of                         Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped                         Nothing -> Nothing-    BoxedColumn (v :: V.Vector c) ->+    BoxedColumn Nothing (v :: V.Vector c) ->         let wrapped = V.map Just v :: V.Vector (Maybe c)          in case testEquality (typeRep @b) (typeRep @(Maybe c)) of                 Just Refl -> Just $ Right $ fromVector @b wrapped@@ -447,7 +456,6 @@                     case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of                         Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped                         Nothing -> Nothing-    -- OptionalColumn and NullableColumn are already handled by the hasElemType guards above.     _ -> Nothing  castMismatch ::@@ -482,7 +490,7 @@ eval (FlatCtx df) (Col name) =     case getColumn name df of         Nothing ->-            Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)+            Left $ ColumnsNotFoundException [name] "" (M.keys $ columnIndices df)         Just c             | hasElemType @a c -> Right (Flat c)             | otherwise ->@@ -500,8 +508,8 @@     case getColumn name (fullDataframe gdf) of         Nothing ->             Left $-                ColumnNotFoundException-                    name+                ColumnsNotFoundException+                    [name]                     ""                     (M.keys $ columnIndices $ fullDataframe gdf)         Just c@@ -524,14 +532,14 @@     case getColumn name df of         Nothing ->             Left $-                ColumnNotFoundException name "" (M.keys $ columnIndices df)+                ColumnsNotFoundException [name] "" (M.keys $ columnIndices df)         Just c -> Flat <$> promoteColumnWith onResult c eval (GroupCtx gdf) (CastWith name _tag onResult) =     case getColumn name (fullDataframe gdf) of         Nothing ->             Left $-                ColumnNotFoundException-                    name+                ColumnsNotFoundException+                    [name]                     ""                     (M.keys $ columnIndices $ fullDataframe gdf)         Just c -> do@@ -579,8 +587,8 @@         case getColumn name (fullDataframe gdf) of             Nothing ->                 Left $-                    ColumnNotFoundException-                        name+                    ColumnsNotFoundException+                        [name]                         ""                         (M.keys $ columnIndices $ fullDataframe gdf)             Just col ->@@ -599,8 +607,8 @@                 case getColumn name (fullDataframe gdf) of                     Nothing ->                         Left $-                            ColumnNotFoundException-                                name+                            ColumnsNotFoundException+                                [name]                                 ""                                 (M.keys $ columnIndices $ fullDataframe gdf)                     Just col ->@@ -617,8 +625,8 @@             case getColumn name (fullDataframe gdf) of                 Nothing ->                     Left $-                        ColumnNotFoundException-                            name+                        ColumnsNotFoundException+                            [name]                             ""                             (M.keys $ columnIndices $ fullDataframe gdf)                 Just col ->@@ -766,4 +774,4 @@             -- The Column payload is intentionally unused — the only             -- call-site ('aggregate') immediately throws             -- 'UnaggregatedException' on this constructor.-            Right $ UnAggregated $ BoxedColumn @T.Text V.empty+            Right $ UnAggregated $ BoxedColumn @T.Text Nothing V.empty
src/DataFrame/Internal/Row.hs view
@@ -169,13 +169,12 @@     get name = case getColumn name df of         Nothing ->             throw $-                ColumnNotFoundException-                    name+                ColumnsNotFoundException+                    [name]                     "[INTERNAL] mkRowFromArgs"                     (M.keys $ columnIndices df)-        Just (BoxedColumn column) -> toAny (column V.! i)-        Just (UnboxedColumn column) -> toAny (column VU.! i)-        Just (OptionalColumn column) -> toAny (column V.! i)+        Just (BoxedColumn _ column) -> toAny (column V.! i)+        Just (UnboxedColumn _ column) -> toAny (column VU.! i)  -- This function will return the items in the order that is specified -- by the user. For example, if the dataframe consists of the columns@@ -193,17 +192,14 @@                 ++ "the other columns at index "                 ++ show i     get name = case getColumn name df of-        Just (BoxedColumn c) -> case c V.!? i of-            Just e -> toAny e-            Nothing -> throwError name-        Just (OptionalColumn c) -> case c V.!? i of+        Just (BoxedColumn _ c) -> case c V.!? i of             Just e -> toAny e             Nothing -> throwError name-        Just (UnboxedColumn c) -> case c VU.!? i of+        Just (UnboxedColumn _ c) -> case c VU.!? i of             Just e -> toAny e             Nothing -> throwError name         Nothing ->-            throw $ ColumnNotFoundException name "mkRowRep" (M.keys $ columnIndices df)+            throw $ ColumnsNotFoundException [name] "mkRowRep" (M.keys $ columnIndices df)  sortedIndexes' :: [Bool] -> V.Vector Row -> VU.Vector Int sortedIndexes' flipCompare rows = runST $ do
src/DataFrame/Internal/Types.hs view
@@ -27,7 +27,7 @@ data Rep     = RBoxed     | RUnboxed-    | ROptional+    | RNullableBoxed  -- | Type-level if statement. type family If (cond :: Bool) (yes :: k) (no :: k) :: k where@@ -68,9 +68,9 @@     Numeric Float = 'True     Numeric _ = 'False --- | Compute the column representation tag for any ‘a’.+-- | Compute the column representation tag for any 'a'. type family KindOf a :: Rep where-    KindOf (Maybe a) = 'ROptional+    KindOf (Maybe a) = 'RNullableBoxed     KindOf a = If (Unboxable a) 'RUnboxed 'RBoxed  -- | Type-level boolean for constraint/type comparison.
src/DataFrame/Lazy/IO/Binary.hs view
@@ -51,11 +51,14 @@ import qualified Data.Vector.Unboxed as VU  import Data.Bits (setBit, shiftL, testBit, (.|.))-import Data.Maybe (fromMaybe, isJust) import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl)) import Data.Word (Word16, Word32, Word64, Word8) import qualified DataFrame.Internal.Binary as Binary-import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.Column (+    Column (..),+    bitmapTestBit,+    buildBitmapFromValid,+ ) import DataFrame.Internal.DataFrame (DataFrame (..)) import Foreign (ForeignPtr, castForeignPtr, plusForeignPtr, sizeOf) import System.Directory (getTemporaryDirectory, removeFile)@@ -110,48 +113,51 @@     nameLen = fromIntegral (BS.length nameBytes) :: Word16  columnTypeTag :: Column -> Word8-columnTypeTag (UnboxedColumn (_ :: VU.Vector a)) =+columnTypeTag (UnboxedColumn Nothing (_ :: VU.Vector a)) =     case testEquality (typeRep @a) (typeRep @Int) of         Just Refl -> tagInt         Nothing -> case testEquality (typeRep @a) (typeRep @Double) of             Just Refl -> tagDouble             Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"-columnTypeTag (BoxedColumn _) = tagText-columnTypeTag (OptionalColumn (_ :: V.Vector (Maybe a))) =+columnTypeTag (UnboxedColumn (Just _) (_ :: VU.Vector a)) =     case testEquality (typeRep @a) (typeRep @Int) of         Just Refl -> tagMaybeInt         Nothing -> case testEquality (typeRep @a) (typeRep @Double) of             Just Refl -> tagMaybeDouble-            Nothing -> tagMaybeText+            Nothing -> error "spillToDisk: unsupported nullable UnboxedColumn element type"+columnTypeTag (BoxedColumn Nothing _) = tagText+columnTypeTag (BoxedColumn (Just _) _) = tagMaybeText  buildColumnData :: Int -> Column -> BSB.Builder-buildColumnData _ (UnboxedColumn (v :: VU.Vector a)) =+buildColumnData _ (UnboxedColumn Nothing (v :: VU.Vector a)) =     case testEquality (typeRep @a) (typeRep @Int) of         Just Refl -> buildIntVector v         Nothing ->             case testEquality (typeRep @a) (typeRep @Double) of                 Just Refl -> buildDoubleVector v                 Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"-buildColumnData _ (BoxedColumn (v :: V.Vector a)) =-    case testEquality (typeRep @a) (typeRep @T.Text) of-        Just Refl -> buildTextVector v-        Nothing -> error "spillToDisk: unsupported BoxedColumn element type"-buildColumnData _ (OptionalColumn (v :: V.Vector (Maybe a))) =+buildColumnData _ (UnboxedColumn (Just bm) (v :: VU.Vector a)) =     case testEquality (typeRep @a) (typeRep @Int) of         Just Refl ->-            buildNullBitmap (V.map isJust v)-                <> buildIntVector (VU.convert (V.map (fromMaybe 0) v))+            buildNullBitmap (V.generate (VU.length v) (bitmapTestBit bm))+                <> buildIntVector v         Nothing ->             case testEquality (typeRep @a) (typeRep @Double) of                 Just Refl ->-                    buildNullBitmap (V.map isJust v)-                        <> buildDoubleVector (VU.convert (V.map (fromMaybe 0.0) v))-                Nothing ->-                    let showText x = case testEquality (typeRep @a) (typeRep @T.Text) of-                            Just Refl -> x-                            Nothing -> T.pack (show x)-                        texts = V.map (maybe T.empty showText) v-                     in buildNullBitmap (V.map isJust v) <> buildTextVector texts+                    buildNullBitmap (V.generate (VU.length v) (bitmapTestBit bm))+                        <> buildDoubleVector v+                Nothing -> error "spillToDisk: unsupported nullable UnboxedColumn element type"+buildColumnData _ (BoxedColumn Nothing (v :: V.Vector a)) =+    case testEquality (typeRep @a) (typeRep @T.Text) of+        Just Refl -> buildTextVector v+        Nothing -> error "spillToDisk: unsupported BoxedColumn element type"+buildColumnData _ (BoxedColumn (Just bm) (v :: V.Vector a)) =+    let isValidVec = V.generate (V.length v) (bitmapTestBit bm)+        showText x = case testEquality (typeRep @a) (typeRep @T.Text) of+            Just Refl -> x+            Nothing -> T.pack (show x)+        texts = V.imap (\i x -> if bitmapTestBit bm i then showText x else T.empty) v+     in buildNullBitmap isValidVec <> buildTextVector texts  {- | Bulk-encode an Int vector as 8-byte LE values (native layout on LE platforms). hPutBuilder flushes synchronously so the underlying ForeignPtr outlives the Builder.@@ -249,37 +255,28 @@ readColumnData bs off nrows tag     | tag == tagInt = do         (off', v) <- readIntColumn bs off nrows-        return (off', UnboxedColumn v)+        return (off', UnboxedColumn Nothing v)     | tag == tagDouble = do         (off', v) <- readDoubleColumn bs off nrows-        return (off', UnboxedColumn v)+        return (off', UnboxedColumn Nothing v)     | tag == tagText = do         (off', v) <- readTextColumn bs off nrows-        return (off', BoxedColumn v)+        return (off', BoxedColumn Nothing v)     | tag == tagMaybeInt = do         (off1, bitmap) <- readNullBitmap bs off nrows         (off2, v) <- readIntColumn bs off1 nrows-        let maybes =-                V.fromList-                    (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::-                    V.Vector (Maybe Int)-        return (off2, OptionalColumn maybes)+        let bm = buildBitmapFromValid (VU.fromList (map (\b -> if b then 1 else 0) bitmap))+        return (off2, UnboxedColumn (Just bm) v)     | tag == tagMaybeDouble = do         (off1, bitmap) <- readNullBitmap bs off nrows         (off2, v) <- readDoubleColumn bs off1 nrows-        let maybes =-                V.fromList-                    (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::-                    V.Vector (Maybe Double)-        return (off2, OptionalColumn maybes)+        let bm = buildBitmapFromValid (VU.fromList (map (\b -> if b then 1 else 0) bitmap))+        return (off2, UnboxedColumn (Just bm) v)     | tag == tagMaybeText = do         (off1, bitmap) <- readNullBitmap bs off nrows         (off2, v) <- readTextColumn bs off1 nrows-        let maybes =-                V.fromList-                    (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (V.toList v)) ::-                    V.Vector (Maybe T.Text)-        return (off2, OptionalColumn maybes)+        let bm = buildBitmapFromValid (VU.fromList (map (\b -> if b then 1 else 0) bitmap))+        return (off2, BoxedColumn (Just bm) v)     | otherwise = Left ("unknown type tag " <> show tag)  {- | Zero-copy Int column read: reuses the ByteString buffer's ForeignPtr.
src/DataFrame/Lazy/IO/CSV.hs view
@@ -30,6 +30,7 @@     Column (..),     MutableColumn (..),     columnLength,+    ensureOptional,     freezeColumn',     writeColumn,  )@@ -60,7 +61,7 @@     ReadOptions         { hasHeader = True         , inferTypes = True-        , safeRead = True+        , safeRead = False         , rowRange = Nothing         , seekPos = Nothing         , totalRows = Nothing@@ -218,7 +219,8 @@     IO Column freezeColumn mutableCols nulls opts colIndex = do     col <- VM.unsafeRead mutableCols colIndex-    freezeColumn' (nulls V.! colIndex) col+    frozen <- freezeColumn' (nulls V.! colIndex) col+    return $! if safeRead opts then ensureOptional frozen else frozen {-# INLINE freezeColumn #-}  -- ---------------------------------------------------------------------------@@ -341,12 +343,6 @@             let val = TextEncoding.decodeUtf8Lenient bs              in VM.unsafeWrite col i val >> return (Right True)         Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))-writeColumnBs i bs (MOptionalColumn (col :: VM.IOVector (Maybe a))) =-    case testEquality (typeRep @a) (typeRep @T.Text) of-        Just Refl ->-            let val = TextEncoding.decodeUtf8Lenient bs-             in VM.unsafeWrite col i (Just val) >> return (Right True)-        Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs)) writeColumnBs i bs (MUnboxedColumn (col :: VUM.IOVector a)) =     case testEquality (typeRep @a) (typeRep @Double) of         Just Refl -> case readByteStringDouble bs of@@ -417,7 +413,6 @@ sliceCol :: Int -> MutableColumn -> MutableColumn sliceCol n (MBoxedColumn col) = MBoxedColumn (VM.take n col) sliceCol n (MUnboxedColumn col) = MUnboxedColumn (VUM.take n col)-sliceCol n (MOptionalColumn col) = MOptionalColumn (VM.take n col)  {- | Finds the index of the next unquoted newline (0x0A). Fast path: uses memchr (SIMD) and falls back to a quote-aware linear scan
src/DataFrame/Operations/Aggregation.hs view
@@ -30,6 +30,7 @@     Column (..),     TypedColumn (..),     atIndicesStable,+    bitmapTestBit,  ) import DataFrame.Internal.DataFrame (DataFrame (..), GroupedDataFrame (..)) import DataFrame.Internal.Expression@@ -49,8 +50,8 @@ groupBy names df     | any (`notElem` columnNames df) names =         throw $-            ColumnNotFoundException-                (T.pack $ show $ names L.\\ columnNames df)+            ColumnsNotFoundException+                (names L.\\ columnNames df)                 "groupBy"                 (columnNames df)     | nRows df == 0 =@@ -81,7 +82,7 @@         let selectedCols = map (columns df V.!) indicesToGroup          forM_ selectedCols $ \case-            UnboxedColumn (v :: VU.Vector a) ->+            UnboxedColumn _ (v :: VU.Vector a) ->                 case testEquality (typeRep @a) (typeRep @Int) of                     Just Refl ->                         VU.imapM_@@ -129,31 +130,28 @@                                                         VUM.unsafeWrite mv i (i, hashWithSalt h x)                                                     )                                                     v-            BoxedColumn (v :: V.Vector a) ->+            BoxedColumn bm (v :: V.Vector a) ->                 case testEquality (typeRep @a) (typeRep @T.Text) of                     Just Refl ->                         V.imapM_                             ( \i t -> do                                 (_, !h) <- VUM.unsafeRead mv i-                                VUM.unsafeWrite mv i (i, hashWithSalt h t)+                                let h' = case bm of+                                        Just bm' | not (bitmapTestBit bm' i) -> hashWithSalt h (0 :: Int) -- null sentinel+                                        _ -> hashWithSalt h t+                                VUM.unsafeWrite mv i (i, h')                             )                             v                     Nothing ->                         V.imapM_                             ( \i d -> do-                                let x = hash (show d)                                 (_, !h) <- VUM.unsafeRead mv i-                                VUM.unsafeWrite mv i (i, hashWithSalt h x)+                                let h' = case bm of+                                        Just bm' | not (bitmapTestBit bm' i) -> hashWithSalt h (0 :: Int) -- null sentinel+                                        _ -> hashWithSalt h (hash (show d))+                                VUM.unsafeWrite mv i (i, h')                             )                             v-            OptionalColumn v ->-                V.imapM_-                    ( \i d -> do-                        let x = hash (show d)-                        (_, !h) <- VUM.unsafeRead mv i-                        VUM.unsafeWrite mv i (i, hashWithSalt h x)-                    )-                    v          let numPasses = 4             bucketSize = 65536@@ -197,7 +195,7 @@     let selectedCols = map (columns df V.!) indices      forM_ selectedCols $ \case-        UnboxedColumn (v :: VU.Vector a) ->+        UnboxedColumn _ (v :: VU.Vector a) ->             case testEquality (typeRep @a) (typeRep @Int) of                 Just Refl ->                     VU.imapM_@@ -245,31 +243,28 @@                                                     VUM.unsafeWrite mv i (hashWithSalt h x)                                                 )                                                 v-        BoxedColumn (v :: V.Vector a) ->+        BoxedColumn bm (v :: V.Vector a) ->             case testEquality (typeRep @a) (typeRep @T.Text) of                 Just Refl ->                     V.imapM_                         ( \i (t :: T.Text) -> do                             h <- VUM.unsafeRead mv i-                            VUM.unsafeWrite mv i (hashWithSalt h t)+                            let h' = case bm of+                                    Just bm' | not (bitmapTestBit bm' i) -> hashWithSalt h (0 :: Int)+                                    _ -> hashWithSalt h t+                            VUM.unsafeWrite mv i h'                         )                         v                 Nothing ->                     V.imapM_                         ( \i d -> do-                            let x = hash (show d)+                            let x = case bm of+                                    Just bm' | not (bitmapTestBit bm' i) -> 0 :: Int+                                    _ -> hash (show d)                             h <- VUM.unsafeRead mv i                             VUM.unsafeWrite mv i (hashWithSalt h x)                         )                         v-        OptionalColumn v ->-            V.imapM_-                ( \i d -> do-                    let x = hash (show d)-                    h <- VUM.unsafeRead mv i-                    VUM.unsafeWrite mv i (hashWithSalt h x)-                )-                v      VU.unsafeFreeze mv   where
src/DataFrame/Operations/Core.hs view
@@ -17,6 +17,7 @@ import qualified Data.Vector.Unboxed as VU  import Control.Exception (throw)+import Data.Bits (popCount) import Data.Either import qualified Data.Foldable as Fold import Data.Function (on, (&))@@ -306,7 +307,7 @@     -- | DataFrame to add the column to     DataFrame ->     DataFrame-insertUnboxedVector name xs = insertColumn name (UnboxedColumn xs)+insertUnboxedVector name xs = insertColumn name (UnboxedColumn Nothing xs)  {- | /O(n)/ Add a column to the dataframe. @@ -394,7 +395,7 @@     | null df = throw (EmptyDataSetException "cloneColumn")     | otherwise = fromMaybe         ( throw $-            ColumnNotFoundException original "cloneColumn" (M.keys $ columnIndices df)+            ColumnsNotFoundException [original] "cloneColumn" (M.keys $ columnIndices df)         )         $ do             column <- getColumn original df@@ -485,7 +486,7 @@ renameSafe orig new df     | null df = throw (EmptyDataSetException "rename")     | otherwise = fromMaybe-        (Left $ ColumnNotFoundException orig "rename" (M.keys $ columnIndices df))+        (Left $ ColumnsNotFoundException [orig] "rename" (M.keys $ columnIndices df))         $ do             columnIndex <- M.lookup orig (columnIndices df)             let origRemoved = M.delete orig (columnIndices df)@@ -530,11 +531,11 @@             [ColumnInfo]     indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df))     columnName i = M.lookup i indexMap-    go acc i col@(OptionalColumn (c :: V.Vector a)) =+    go acc i col@(BoxedColumn bm (c :: V.Vector a)) =         let             cname = columnName i             countNulls = nulls col-            columnType = T.pack $ show $ typeRep @a+            columnType = T.pack $ columnTypeString col          in             if isNothing cname                 then acc@@ -545,43 +546,35 @@                         countNulls                         columnType                         : acc-    go acc i col@(BoxedColumn (c :: V.Vector a)) =+    go acc i col@(UnboxedColumn bm c) =         let             cname = columnName i-            columnType = T.pack $ show $ typeRep @a+            countNulls = nulls col+            columnType = T.pack $ columnTypeString col          in             if isNothing cname                 then acc                 else                     ColumnInfo                         (fromMaybe "" cname)-                        (columnLength col)-                        0+                        (columnLength col - countNulls)+                        countNulls                         columnType                         : acc-    go acc i col@(UnboxedColumn c) =-        let-            cname = columnName i-            columnType = T.pack $ columnTypeString col-         in-            -- Unboxed columns cannot have nulls since Maybe-            -- is not an instance of Unbox a-            if isNothing cname-                then acc-                else-                    ColumnInfo (fromMaybe "" cname) (columnLength col) 0 columnType : acc  nulls :: Column -> Int-nulls (OptionalColumn xs) = VG.length $ VG.filter isNothing xs-nulls (BoxedColumn (xs :: V.Vector a)) = case testEquality (typeRep @a) (typeRep @T.Text) of+nulls (BoxedColumn (Just bm) xs) =+    -- count null bits in bitmap+    let n = VG.length xs+     in n - VU.foldl' (\acc b -> acc + popCount b) 0 bm+nulls (BoxedColumn Nothing (xs :: V.Vector a)) = case testEquality (typeRep @a) (typeRep @T.Text) of     Just Refl -> VG.length $ VG.filter isNullish xs     Nothing -> case testEquality (typeRep @a) (typeRep @String) of         Just Refl -> VG.length $ VG.filter (isNullish . T.pack) xs-        Nothing -> case typeRep @a of-            App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of-                Just HRefl -> VG.length $ VG.filter isNothing xs-                Nothing -> 0-            _ -> 0+        Nothing -> 0+nulls (UnboxedColumn (Just bm) xs) =+    let n = VG.length xs+     in n - VU.foldl' (\acc b -> acc + popCount b) 0 bm nulls _ = 0  {- | Creates a dataframe from a list of tuples with name and column.@@ -859,7 +852,8 @@         (Col name) -> case getColumn name df of             Just col -> toVector col             Nothing ->-                Left $ ColumnNotFoundException name "columnAsVector" (M.keys $ columnIndices df)+                Left $+                    ColumnsNotFoundException [name] "columnAsVector" (M.keys $ columnIndices df)         _ -> case interpret df expr of             Left e -> throw e             Right (TColumn col) -> toVector col@@ -876,7 +870,7 @@     Just col -> toIntVector col     Nothing ->         Left $-            ColumnNotFoundException name "columnAsIntVector" (M.keys $ columnIndices df)+            ColumnsNotFoundException [name] "columnAsIntVector" (M.keys $ columnIndices df) columnAsIntVector expr df = case interpret df expr of     Left e -> throw e     Right (TColumn col) -> toIntVector col@@ -893,7 +887,10 @@     Just col -> toDoubleVector col     Nothing ->         Left $-            ColumnNotFoundException name "columnAsDoubleVector" (M.keys $ columnIndices df)+            ColumnsNotFoundException+                [name]+                "columnAsDoubleVector"+                (M.keys $ columnIndices df) columnAsDoubleVector expr df = case interpret df expr of     Left e -> throw e     Right (TColumn col) -> toDoubleVector col@@ -910,7 +907,10 @@     Just col -> toFloatVector col     Nothing ->         Left $-            ColumnNotFoundException name "columnAsFloatVector" (M.keys $ columnIndices df)+            ColumnsNotFoundException+                [name]+                "columnAsFloatVector"+                (M.keys $ columnIndices df) columnAsFloatVector expr df = case interpret df expr of     Left e -> throw e     Right (TColumn col) -> toFloatVector col@@ -923,7 +923,10 @@     Just col -> toUnboxedVector col     Nothing ->         Left $-            ColumnNotFoundException name "columnAsFloatVector" (M.keys $ columnIndices df)+            ColumnsNotFoundException+                [name]+                "columnAsFloatVector"+                (M.keys $ columnIndices df) columnAsUnboxedVector expr df = case interpret df expr of     Left e -> throw e     Right (TColumn col) -> toUnboxedVector col@@ -967,7 +970,8 @@      in map toNamedExpr names   where     identityUExpr name = case getColumn name df of-        Just (BoxedColumn (_ :: V.Vector a)) -> UExpr (Col @a name)-        Just (UnboxedColumn (_ :: VU.Vector a)) -> UExpr (Col @a name)-        Just (OptionalColumn (_ :: V.Vector (Maybe a))) -> UExpr (Col @(Maybe a) name)+        Just (BoxedColumn (Just _) (_ :: V.Vector a)) -> UExpr (Col @(Maybe a) name)+        Just (BoxedColumn Nothing (_ :: V.Vector a)) -> UExpr (Col @a name)+        Just (UnboxedColumn (Just _) (_ :: VU.Vector a)) -> UExpr (Col @(Maybe a) name)+        Just (UnboxedColumn Nothing (_ :: VU.Vector a)) -> UExpr (Col @a name)         Nothing -> error $ "showDerivedExpressions: column not found: " ++ T.unpack name
src/DataFrame/Operations/Join.hs view
@@ -11,6 +11,7 @@ module DataFrame.Operations.Join where  import Control.Applicative ((<|>))+import Control.Exception (throw) import Control.Monad (forM_, when) import Control.Monad.ST (ST, runST) import qualified Data.HashMap.Strict as HM@@ -27,6 +28,9 @@ import qualified Data.Vector.Algorithms.Merge as VA import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM+import DataFrame.Errors (+    DataFrameException (ColumnsNotFoundException),+ ) import DataFrame.Internal.Column as D import DataFrame.Internal.DataFrame as D import DataFrame.Operations.Aggregation as D@@ -148,6 +152,15 @@ keyColIndices :: S.Set T.Text -> DataFrame -> [Int] keyColIndices csSet df = M.elems $ M.restrictKeys (D.columnIndices df) csSet +-- | Validate that all requested join keys exist, then return their indices.+validatedKeyColIndices :: T.Text -> S.Set T.Text -> DataFrame -> [Int]+validatedKeyColIndices callPoint csSet df =+    let columnIdxs = D.columnIndices df+        missingKeys = S.toAscList (csSet `S.difference` M.keysSet columnIdxs)+     in case missingKeys of+            [] -> M.elems $ M.restrictKeys columnIdxs csSet+            _ -> throw (ColumnsNotFoundException missingKeys callPoint (M.keys columnIdxs))+ -- ============================================================ -- Inner Join -- ============================================================@@ -175,36 +188,39 @@ innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame innerJoin cs left right     | D.null right || D.null left = D.empty-    | otherwise =-        let-            csSet = S.fromList cs-            leftRows = fst (D.dimensions left)-            rightRows = fst (D.dimensions right)+    | otherwise = innerJoinNonEmpty cs left right -            leftKeyIdxs = keyColIndices csSet left-            rightKeyIdxs = keyColIndices csSet right-            leftHashes = D.computeRowHashes leftKeyIdxs left-            rightHashes = D.computeRowHashes rightKeyIdxs right+innerJoinNonEmpty :: [T.Text] -> DataFrame -> DataFrame -> DataFrame+innerJoinNonEmpty cs left right =+    let+        csSet = S.fromList cs+        leftRows = fst (D.dimensions left)+        rightRows = fst (D.dimensions right) -            buildRows = min leftRows rightRows-            (leftIxs, rightIxs)-                | buildRows > joinStrategyThreshold =-                    sortMergeInnerKernel leftHashes rightHashes-                | rightRows <= leftRows =-                    -- Build on right (smaller or equal), probe with left-                    hashInnerKernel leftHashes rightHashes-                | otherwise =-                    -- Build on left (smaller), probe with right, swap result-                    let (!rIxs, !lIxs) = hashInnerKernel rightHashes leftHashes-                     in (lIxs, rIxs)-         in-            assembleInner csSet left right leftIxs rightIxs+        leftKeyIdxs = validatedKeyColIndices "innerJoin" csSet left+        rightKeyIdxs = validatedKeyColIndices "innerJoin" csSet right+        leftHashes = D.computeRowHashes leftKeyIdxs left+        rightHashes = D.computeRowHashes rightKeyIdxs right +        buildRows = min leftRows rightRows+        (leftIxs, rightIxs)+            | buildRows > joinStrategyThreshold =+                sortMergeInnerKernel leftHashes rightHashes+            | rightRows <= leftRows =+                -- Build on right (smaller or equal), probe with left+                hashInnerKernel leftHashes rightHashes+            | otherwise =+                -- Build on left (smaller), probe with right, swap result+                let (!rIxs, !lIxs) = hashInnerKernel rightHashes leftHashes+                 in (lIxs, rIxs)+     in+        assembleInner csSet left right leftIxs rightIxs+ -- | Compute hashes for the given key column names in a DataFrame. buildHashColumn :: [T.Text] -> DataFrame -> VU.Vector Int buildHashColumn keys df =     let csSet = S.fromList keys-        keyIdxs = keyColIndices csSet df+        keyIdxs = validatedKeyColIndices "buildHashColumn" csSet df      in D.computeRowHashes keyIdxs df  {- | Probe one batch of rows against a pre-built 'CompactIndex'.@@ -530,29 +546,36 @@ @ -} leftJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-leftJoin cs left right+leftJoin = leftJoinWithCallPoint "leftJoin"++leftJoinWithCallPoint ::+    T.Text -> [T.Text] -> DataFrame -> DataFrame -> DataFrame+leftJoinWithCallPoint callPoint cs left right     | D.null right || D.nRows right == 0 = left     | D.null left || D.nRows left == 0 = D.empty-    | otherwise =-        let-            csSet = S.fromList cs-            rightRows = fst (D.dimensions right)+    | otherwise = leftJoinNonEmpty callPoint cs left right -            leftKeyIdxs = keyColIndices csSet left-            rightKeyIdxs = keyColIndices csSet right-            leftHashes = D.computeRowHashes leftKeyIdxs left-            rightHashes = D.computeRowHashes rightKeyIdxs right+leftJoinNonEmpty :: T.Text -> [T.Text] -> DataFrame -> DataFrame -> DataFrame+leftJoinNonEmpty callPoint cs left right =+    let+        csSet = S.fromList cs+        rightRows = fst (D.dimensions right) -            -- Right is always the build side for left join-            (leftIxs, rightIxs)-                | rightRows > joinStrategyThreshold =-                    sortMergeLeftKernel leftHashes rightHashes-                | otherwise =-                    hashLeftKernel leftHashes rightHashes-         in-            -- rightIxs uses -1 as sentinel for "no match"-            assembleLeft csSet left right leftIxs rightIxs+        leftKeyIdxs = validatedKeyColIndices callPoint csSet left+        rightKeyIdxs = validatedKeyColIndices callPoint csSet right+        leftHashes = D.computeRowHashes leftKeyIdxs left+        rightHashes = D.computeRowHashes rightKeyIdxs right +        -- Right is always the build side for left join+        (leftIxs, rightIxs)+            | rightRows > joinStrategyThreshold =+                sortMergeLeftKernel leftHashes rightHashes+            | otherwise =+                hashLeftKernel leftHashes rightHashes+     in+        -- rightIxs uses -1 as sentinel for "no match"+        assembleLeft csSet left right leftIxs rightIxs+ {- | Hash-based left join kernel. Returns @(leftExpandedIndices, rightExpandedIndices)@ where right indices use @-1@ as sentinel for unmatched rows.@@ -801,34 +824,37 @@ -} rightJoin ::     [T.Text] -> DataFrame -> DataFrame -> DataFrame-rightJoin cs left right = leftJoin cs right left+rightJoin cs left right = leftJoinWithCallPoint "rightJoin" cs right left  fullOuterJoin ::     [T.Text] -> DataFrame -> DataFrame -> DataFrame fullOuterJoin cs left right     | D.null right || D.nRows right == 0 = left     | D.null left || D.nRows left == 0 = right-    | otherwise =-        let-            csSet = S.fromList cs-            leftRows = fst (D.dimensions left)-            rightRows = fst (D.dimensions right)+    | otherwise = fullOuterJoinNonEmpty cs left right -            leftKeyIdxs = keyColIndices csSet left-            rightKeyIdxs = keyColIndices csSet right-            leftHashes = D.computeRowHashes leftKeyIdxs left-            rightHashes = D.computeRowHashes rightKeyIdxs right+fullOuterJoinNonEmpty :: [T.Text] -> DataFrame -> DataFrame -> DataFrame+fullOuterJoinNonEmpty cs left right =+    let+        csSet = S.fromList cs+        leftRows = fst (D.dimensions left)+        rightRows = fst (D.dimensions right) -            -- Both sides can have nulls in full outer-            (leftIxs, rightIxs)-                | max leftRows rightRows > joinStrategyThreshold =-                    sortMergeFullOuterKernel leftHashes rightHashes-                | otherwise =-                    hashFullOuterKernel leftHashes rightHashes-         in-            -- Both index vectors use -1 as sentinel-            assembleFullOuter csSet left right leftIxs rightIxs+        leftKeyIdxs = validatedKeyColIndices "fullOuterJoin" csSet left+        rightKeyIdxs = validatedKeyColIndices "fullOuterJoin" csSet right+        leftHashes = D.computeRowHashes leftKeyIdxs left+        rightHashes = D.computeRowHashes rightKeyIdxs right +        -- Both sides can have nulls in full outer+        (leftIxs, rightIxs)+            | max leftRows rightRows > joinStrategyThreshold =+                sortMergeFullOuterKernel leftHashes rightHashes+            | otherwise =+                hashFullOuterKernel leftHashes rightHashes+     in+        -- Both index vectors use -1 as sentinel+        assembleFullOuter csSet left right leftIxs rightIxs+ {- | Hash-based full outer join kernel. Builds compact indices on both sides. Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinels.@@ -1031,23 +1057,31 @@         insertIfPresent _ Nothing df = df         insertIfPresent name (Just c) df = D.insertColumn name c df -        -- Coalesce two OptionalColumns: take first non-Nothing per row,+        -- Coalesce two nullable columns: take first non-Nothing per row,         -- producing a non-optional column.         coalesceKeyColumn :: Column -> Column -> Column         coalesceKeyColumn-            (OptionalColumn (lCol :: VB.Vector (Maybe a)))-            (OptionalColumn (rCol :: VB.Vector (Maybe b))) =+            (BoxedColumn lBm (lCol :: VB.Vector a))+            (BoxedColumn rBm (rCol :: VB.Vector b)) =                 case testEquality (typeRep @a) (typeRep @b) of                     Just Refl ->-                        D.fromVector $-                            VB.zipWith-                                ( \l r ->-                                    fromMaybe (error "fullOuterJoin: null on both sides of key column") (l <|> r)-                                )-                                lCol-                                rCol+                        let asMaybe bm =+                                VB.imap+                                    ( \i v -> case bm of+                                        Just bm' -> if bitmapTestBit bm' i then Just v else Nothing+                                        Nothing -> Just v+                                    )+                            lMaybe = asMaybe lBm lCol+                            rMaybe = asMaybe rBm rCol+                         in D.fromVector $+                                VB.zipWith+                                    ( \l r ->+                                        fromMaybe (error "fullOuterJoin: null on both sides of key column") (l <|> r)+                                    )+                                    lMaybe+                                    rMaybe                     Nothing -> error "Cannot join columns of different types"-        coalesceKeyColumn _ _ = error "fullOuterJoin: expected OptionalColumn for key columns"+        coalesceKeyColumn _ _ = error "fullOuterJoin: expected nullable column for key columns"      in D.fold             ( \name df ->                 if S.member name csSet
src/DataFrame/Operations/Permutation.hs view
@@ -53,8 +53,8 @@ sortBy sortOrds df     | any (`notElem` columnNames df) names =         throw $-            ColumnNotFoundException-                (T.pack $ show $ names L.\\ columnNames df)+            ColumnsNotFoundException+                (names L.\\ columnNames df)                 "sortBy"                 (columnNames df)     | otherwise =
src/DataFrame/Operations/Statistics.hs view
@@ -213,7 +213,7 @@  _getColumnAsDouble :: T.Text -> DataFrame -> Maybe (VU.Vector Double) _getColumnAsDouble name df = case getColumn name df of-    Just (UnboxedColumn (f :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of+    Just (UnboxedColumn _ (f :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of         Just Refl -> Just f         Nothing -> case sIntegral @a of             STrue -> Just (VU.map fromIntegral f)@@ -222,7 +222,7 @@                 SFalse -> Nothing     Nothing ->         throw $-            ColumnNotFoundException name "_getColumnAsDouble" (M.keys $ columnIndices df)+            ColumnsNotFoundException [name] "_getColumnAsDouble" (M.keys $ columnIndices df)     _ -> Nothing -- Return a type mismatch error here. {-# INLINE _getColumnAsDouble #-} @@ -237,15 +237,12 @@ sum ::     forall a. (Columnable a, Num a) => Expr a -> DataFrame -> a sum (Col name) df = case getColumn name df of-    Nothing -> throw $ ColumnNotFoundException name "sum" (M.keys $ columnIndices df)-    Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of+    Nothing -> throw $ ColumnsNotFoundException [name] "sum" (M.keys $ columnIndices df)+    Just ((UnboxedColumn _ (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of         Just Refl -> VG.sum column         Nothing -> 0-    Just ((BoxedColumn (column :: V.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of+    Just ((BoxedColumn _ (column :: V.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of         Just Refl -> VG.sum column-        Nothing -> 0-    Just ((OptionalColumn (column :: V.Vector (Maybe a')))) -> case testEquality (typeRep @a') (typeRep @a) of-        Just Refl -> VG.sum (VG.map (fromMaybe 0) column)         Nothing -> 0 sum expr df = case interpret df expr of     Left e -> throw e
src/DataFrame/Operations/Subset.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}@@ -44,10 +46,23 @@ import DataFrame.Operations.Core import DataFrame.Operations.Merge () import DataFrame.Operations.Transformations (apply)+import DataFrame.Operators import System.Random import Type.Reflection import Prelude hiding (filter, take) +#if MIN_VERSION_random(1,3,0)+type SplittableGen g = (SplitGen g, RandomGen g)++splitForStratified :: SplittableGen g => g -> (g, g)+splitForStratified = splitGen+#else+type SplittableGen g = RandomGen g++splitForStratified :: SplittableGen g => g -> (g, g)+splitForStratified = split+#endif+ -- | O(k * n) Take the first n rows of a DataFrame. take :: Int -> DataFrame -> DataFrame take n d = d{columns = V.map (takeColumn n') (columns d), dataframeDimensions = (n', c)}@@ -116,10 +131,33 @@ filter (Col filterColumnName) condition df = case getColumn filterColumnName df of     Nothing ->         throw $-            ColumnNotFoundException filterColumnName "filter" (M.keys $ columnIndices df)-    Just (BoxedColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df-    Just (OptionalColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df-    Just (UnboxedColumn (column :: VU.Vector b)) -> filterByVector filterColumnName column condition df+            ColumnsNotFoundException [filterColumnName] "filter" (M.keys $ columnIndices df)+    Just col@(BoxedColumn bm (column :: V.Vector b)) ->+        -- Check direct type match first, then try Maybe b match for nullable columns+        case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> filterByVector filterColumnName column condition df+            Nothing -> case (bm, typeRep @a) of+                (Just bm', App tMaybe tInner) -> case eqTypeRep tMaybe (typeRep @Maybe) of+                    Just HRefl -> case testEquality tInner (typeRep @b) of+                        Just Refl ->+                            let maybeVec = V.imap (\i v -> if bitmapTestBit bm' i then Just v else Nothing) column+                             in filterByVector filterColumnName maybeVec condition df+                        Nothing -> filterByVector filterColumnName column condition df+                    Nothing -> filterByVector filterColumnName column condition df+                _ -> filterByVector filterColumnName column condition df+    Just col@(UnboxedColumn bm (column :: VU.Vector b)) ->+        case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> filterByVector filterColumnName column condition df+            Nothing -> case (bm, typeRep @a) of+                (Just bm', App tMaybe tInner) -> case eqTypeRep tMaybe (typeRep @Maybe) of+                    Just HRefl -> case testEquality tInner (typeRep @b) of+                        Just Refl ->+                            let maybeVec = V.generate (VU.length column) $ \i ->+                                    if bitmapTestBit bm' i then Just (VU.unsafeIndex column i) else Nothing+                             in filterByVector filterColumnName maybeVec condition df+                        Nothing -> filterByVector filterColumnName column condition df+                    Nothing -> filterByVector filterColumnName column condition df+                _ -> filterByVector filterColumnName column condition df filter expr condition df =     let         (TColumn col) = case interpret @a df (normalize expr) of@@ -193,9 +231,12 @@ filterJust :: T.Text -> DataFrame -> DataFrame filterJust name df = case getColumn name df of     Nothing ->-        throw $ ColumnNotFoundException name "filterJust" (M.keys $ columnIndices df)-    Just column@(OptionalColumn (col :: V.Vector (Maybe a))) -> filter (Col @(Maybe a) name) isJust df & apply @(Maybe a) fromJust name-    Just column -> df+        throw $ ColumnsNotFoundException [name] "filterJust" (M.keys $ columnIndices df)+    Just column | hasMissing column -> case column of+        BoxedColumn (Just _) (col :: V.Vector a) -> filter (Col @(Maybe a) name) isJust df & apply @(Maybe a) fromJust name+        UnboxedColumn (Just _) (col :: VU.Vector a) -> filter (Col @(Maybe a) name) isJust df & apply @(Maybe a) fromJust name+        _ -> df+    Just _ -> df  {- | O(k) returns all rows with `Nothing` in a give column. @@ -204,8 +245,12 @@ filterNothing :: T.Text -> DataFrame -> DataFrame filterNothing name df = case getColumn name df of     Nothing ->-        throw $ ColumnNotFoundException name "filterNothing" (M.keys $ columnIndices df)-    Just (OptionalColumn (col :: V.Vector (Maybe a))) -> filter (Col @(Maybe a) name) isNothing df+        throw $+            ColumnsNotFoundException [name] "filterNothing" (M.keys $ columnIndices df)+    Just column | hasMissing column -> case column of+        BoxedColumn (Just _) (col :: V.Vector a) -> filter (Col @(Maybe a) name) isNothing df+        UnboxedColumn (Just _) (col :: VU.Vector a) -> filter (Col @(Maybe a) name) isNothing df+        _ -> df     _ -> df  {- | O(n * k) removes all rows with `Nothing` from the dataframe.@@ -242,8 +287,8 @@     | L.null cs = empty     | any (`notElem` columnNames df) cs =         throw $-            ColumnNotFoundException-                (T.pack $ show $ cs L.\\ columnNames df)+            ColumnsNotFoundException+                (cs L.\\ columnNames df)                 "select"                 (columnNames df)     | otherwise =@@ -345,23 +390,12 @@ sample pureGen p df =     let         rand = generateRandomVector pureGen (fst (dataframeDimensions df))+        cRand = col @Double "__rand__"      in         df-            & insertUnboxedVector "__rand__" rand-            & filterWhere-                ( Binary-                    ( MkBinaryOp-                        { binaryFn = (>=)-                        , binaryName = "geq"-                        , binarySymbol = Just ">="-                        , binaryCommutative = False-                        , binaryPrecedence = 1-                        }-                    )-                    (Col @Double "__rand__")-                    (Lit (1 - p))-                )-            & exclude ["__rand__"]+            & insertUnboxedVector (name cRand) rand+            & filterWhere (cRand .>=. Lit (1 - p))+            & exclude [name cRand]  {- | Split a dataset into two. The first in the tuple gets a sample of p (0 <= p <= 1) and the second gets (1 - p). This is useful for creating test and train splits. @@ -377,38 +411,16 @@ randomSplit pureGen p df =     let         rand = generateRandomVector pureGen (fst (dataframeDimensions df))-        withRand = df & insertUnboxedVector "__rand__" rand+        cRand = col @Double "__rand__"+        withRand = df & insertUnboxedVector (name cRand) rand      in         ( withRand-            & filterWhere-                ( Binary-                    ( MkBinaryOp-                        { binaryFn = (<=)-                        , binaryName = "leq"-                        , binarySymbol = Just "<="-                        , binaryCommutative = False-                        , binaryPrecedence = 1-                        }-                    )-                    (Col @Double "__rand__")-                    (Lit p)-                )-            & exclude ["__rand__"]+            & filterWhere (cRand .<=. Lit p)+            & exclude [name cRand]         , withRand             & filterWhere-                ( Binary-                    ( MkBinaryOp-                        { binaryFn = (>)-                        , binaryName = "gt"-                        , binarySymbol = Just ">"-                        , binaryCommutative = False-                        , binaryPrecedence = 1-                        }-                    )-                    (Col @Double "__rand__")-                    (Lit p)-                )-            & exclude ["__rand__"]+                (cRand .>. Lit p)+            & exclude [name cRand]         )  {- | Creates n folds of a dataframe.@@ -424,46 +436,20 @@ kFolds pureGen folds df =     let         rand = generateRandomVector pureGen (fst (dataframeDimensions df))-        withRand = df & insertUnboxedVector "__rand__" rand+        cRand = col @Double "__rand__"+        withRand = df & insertUnboxedVector (name cRand) rand         partitionSize = 1 / fromIntegral folds         singleFold n d =-            d-                & filterWhere-                    ( Binary-                        ( MkBinaryOp-                            { binaryFn = (>=)-                            , binaryName = "geq"-                            , binarySymbol = Just ">="-                            , binaryCommutative = False-                            , binaryPrecedence = 1-                            }-                        )-                        (Col @Double "__rand__")-                        (Lit (fromIntegral n * partitionSize))-                    )+            d & filterWhere (cRand .>=. Lit (fromIntegral n * partitionSize))         go (-1) _ = []         go n d =             let                 d' = singleFold n d-                d'' =-                    d-                        & filterWhere-                            ( Binary-                                ( MkBinaryOp-                                    { binaryFn = (<)-                                    , binaryName = "lt"-                                    , binarySymbol = Just "<"-                                    , binaryCommutative = False-                                    , binaryPrecedence = 1-                                    }-                                )-                                (Col @Double "__rand__")-                                (Lit (fromIntegral n * partitionSize))-                            )+                d'' = d & filterWhere (cRand .<. Lit (fromIntegral n * partitionSize))              in                 d' : go (n - 1) d''      in-        map (exclude ["__rand__"]) (go (folds - 1) withRand)+        map (exclude [name cRand]) (go (folds - 1) withRand)  generateRandomVector :: (RandomGen g) => g -> Int -> VU.Vector Double generateRandomVector pureGen k = VU.fromList $ go pureGen k@@ -477,12 +463,19 @@  -- | Convert any Column to a vector of Text labels (one per row). columnToTextVec :: Column -> V.Vector T.Text-columnToTextVec (BoxedColumn (col :: V.Vector a)) =-    case testEquality (typeRep @a) (typeRep @T.Text) of-        Just Refl -> col-        Nothing -> V.map (T.pack . show) col-columnToTextVec (UnboxedColumn col) = V.map (T.pack . show) (V.convert col)-columnToTextVec (OptionalColumn col) = V.map (T.pack . show) col+columnToTextVec (BoxedColumn bm (col :: V.Vector a)) =+    case bm of+        Nothing -> case testEquality (typeRep @a) (typeRep @T.Text) of+            Just Refl -> col+            Nothing -> V.map (T.pack . show) col+        Just bitmap ->+            V.imap (\i x -> if bitmapTestBit bitmap i then T.pack (show x) else "null") col+columnToTextVec (UnboxedColumn bm col) =+    case bm of+        Nothing -> V.map (T.pack . show) (V.convert col)+        Just bitmap ->+            V.generate (VU.length col) $ \i ->+                if bitmapTestBit bitmap i then T.pack (show (col VU.! i)) else "null"  -- | Build a map from stringified label to row indices. groupByIndices :: Column -> M.Map T.Text (VU.Vector Int)@@ -513,7 +506,7 @@ -} stratifiedSample ::     forall a g.-    (SplitGen g, RandomGen g, Columnable a) =>+    (SplittableGen g, Columnable a) =>     g -> Double -> Expr a -> DataFrame -> DataFrame stratifiedSample gen p strataCol df =     let col = case strataCol of@@ -523,7 +516,7 @@         go _ [] = mempty         go g (ixs : rest) =             let stratum = rowsAtIndices ixs df-                (g1, g2) = splitGen g+                (g1, g2) = splitForStratified g              in sample g1 p stratum <> go g2 rest      in go gen groups @@ -537,7 +530,7 @@ -} stratifiedSplit ::     forall a g.-    (SplitGen g, RandomGen g, Columnable a) =>+    (SplittableGen g, Columnable a) =>     g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame) stratifiedSplit gen p strataCol df =     let col = case strataCol of@@ -547,7 +540,7 @@         go _ [] = (mempty, mempty)         go g (ixs : rest) =             let stratum = rowsAtIndices ixs df-                (g1, g2) = splitGen g+                (g1, g2) = splitForStratified g                 (tr, va) = randomSplit g1 p stratum                 (trAcc, vaAcc) = go g2 rest              in (tr <> trAcc, va <> vaAcc)
src/DataFrame/Operations/Transformations.hs view
@@ -21,9 +21,9 @@ import Data.Maybe import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..)) import DataFrame.Internal.Column (-    Column (..),     Columnable,     TypedColumn (..),+    hasMissing,     ifoldrColumn,     imapColumn,     mapColumn,@@ -63,7 +63,8 @@     DataFrame ->     Either DataFrameException DataFrame safeApply f columnName d = case getColumn columnName d of-    Nothing -> Left $ ColumnNotFoundException columnName "apply" (M.keys $ columnIndices d)+    Nothing ->+        Left $ ColumnsNotFoundException [columnName] "apply" (M.keys $ columnIndices d)     Just column -> do         column' <- mapColumn f column         pure $ insertColumn columnName column' d@@ -163,8 +164,8 @@ applyWhere condition filterColumnName f columnName df = case getColumn filterColumnName df of     Nothing ->         throw $-            ColumnNotFoundException-                filterColumnName+            ColumnsNotFoundException+                [filterColumnName]                 "applyWhere"                 (M.keys $ columnIndices df)     Just column -> case ifoldrColumn@@ -193,7 +194,7 @@ applyAtIndex i f columnName df = case getColumn columnName df of     Nothing ->         throw $-            ColumnNotFoundException columnName "applyAtIndex" (M.keys $ columnIndices df)+            ColumnsNotFoundException [columnName] "applyAtIndex" (M.keys $ columnIndices df)     Just column -> case imapColumn (\index value -> if index == i then f value else value) column of         Left e -> throw e         Right column' -> insertColumn columnName column' df@@ -208,8 +209,9 @@     DataFrame imputeCore (Col columnName) value df = case getColumn columnName df of     Nothing ->-        throw $ ColumnNotFoundException columnName "impute" (M.keys $ columnIndices df)-    Just (OptionalColumn _) -> case safeApply (fromMaybe value) columnName df of+        throw $+            ColumnsNotFoundException [columnName] "impute" (M.keys $ columnIndices df)+    Just col | hasMissing col -> case safeApply (fromMaybe value) columnName df of         Left (TypeMismatchException context) -> throw $ TypeMismatchException (context{callingFunctionName = Just "impute"})         Left exception -> throw exception         Right res -> res
src/DataFrame/Operations/Typing.hs view
@@ -11,11 +11,15 @@  import Control.Applicative (asum) import Control.Monad (join)-import Data.Maybe (fromMaybe) import qualified Data.Proxy as P import Data.Time import Data.Type.Equality (TestEquality (..))-import DataFrame.Internal.Column (Column (..), fromVector)+import DataFrame.Internal.Column (+    Column (..),+    bitmapTestBit,+    ensureOptional,+    fromVector,+ ) import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn) import DataFrame.Internal.Parsing import DataFrame.Internal.Schema@@ -55,19 +59,22 @@ parseDefaults opts df = df{columns = V.map (parseDefault opts) (columns df)}  parseDefault :: ParseOptions -> Column -> Column-parseDefault opts (BoxedColumn (c :: V.Vector a)) =+parseDefault opts (BoxedColumn Nothing (c :: V.Vector a)) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of             Just Refl -> parseFromExamples opts (V.map T.pack c)-            Nothing -> BoxedColumn c+            Nothing -> BoxedColumn Nothing c         Just Refl -> parseFromExamples opts c-parseDefault opts (OptionalColumn (c :: V.Vector (Maybe a))) =+parseDefault opts (BoxedColumn (Just bm) (c :: V.Vector a)) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of             Just Refl ->-                parseFromExamples opts (V.map (T.pack . fromMaybe "") c)-            Nothing -> BoxedColumn c-        Just Refl -> parseFromExamples opts (V.map (fromMaybe "") c)+                parseFromExamples+                    opts+                    (V.imap (\i x -> if bitmapTestBit bm i then T.pack x else "") c)+            Nothing -> BoxedColumn (Just bm) c+        Just Refl ->+            parseFromExamples opts (V.imap (\i x -> if bitmapTestBit bm i then x else "") c) parseDefault _ column = column  parseFromExamples :: ParseOptions -> V.Vector T.Text -> Column@@ -78,14 +85,16 @@         examples = V.map converter (V.take (sampleSize opts) cols)         asMaybeText = V.map converter cols         dfmt = parseDateFormat opts+        result =+            case makeParsingAssumption dfmt examples of+                BoolAssumption -> handleBoolAssumption asMaybeText+                IntAssumption -> handleIntAssumption asMaybeText+                DoubleAssumption -> handleDoubleAssumption asMaybeText+                TextAssumption -> handleTextAssumption asMaybeText+                DateAssumption -> handleDateAssumption dfmt asMaybeText+                NoAssumption -> handleNoAssumption dfmt asMaybeText      in-        case makeParsingAssumption dfmt examples of-            BoolAssumption -> handleBoolAssumption asMaybeText-            IntAssumption -> handleIntAssumption asMaybeText-            DoubleAssumption -> handleDoubleAssumption asMaybeText-            TextAssumption -> handleTextAssumption asMaybeText-            DateAssumption -> handleDateAssumption dfmt asMaybeText-            NoAssumption -> handleNoAssumption dfmt asMaybeText+        if parseSafe opts then ensureOptional result else result  handleBoolAssumption :: V.Vector (Maybe T.Text) -> Column handleBoolAssumption asMaybeText@@ -232,7 +241,7 @@             ts   where     asType :: SchemaType -> Column -> Column-    asType (SType (_ :: P.Proxy a)) c@(BoxedColumn (col :: V.Vector b)) = case typeRep @a of+    asType (SType (_ :: P.Proxy a)) c@(BoxedColumn _ (col :: V.Vector b)) = case typeRep @a of         App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of             Just HRefl -> case testEquality (typeRep @a) (typeRep @b) of                 Just Refl -> c
src/DataFrame/Synthesis.hs view
@@ -177,7 +177,7 @@             Left e -> throw e             Right v -> v         hasInvalid = case res of-            (TColumn (UnboxedColumn (col :: VU.Vector b))) -> case testEquality (typeRep @Double) (typeRep @b) of+            (TColumn (UnboxedColumn _ (col :: VU.Vector b))) -> case testEquality (typeRep @Double) (typeRep @b) of                 Just Refl -> VU.any (\n -> isNaN n || isInfinite n) col                 Nothing -> False             _ -> False
src/DataFrame/Typed.hs view
@@ -183,13 +183,16 @@     -- * Template Haskell     deriveSchema,     deriveSchemaFromCsvFile,+    deriveSchemaFromCsvFileWith,      -- * Schema type families (for advanced use)     Lookup,+    SafeLookup,     HasName,     SubsetSchema,     ExcludeSchema,     RenameInSchema,+    RenameManyInSchema,     RemoveColumn,     Impute,     Append,@@ -225,7 +228,11 @@ import DataFrame.Typed.Join (fullOuterJoin, innerJoin, leftJoin, rightJoin) import DataFrame.Typed.Operations import DataFrame.Typed.Schema-import DataFrame.Typed.TH (deriveSchema, deriveSchemaFromCsvFile)+import DataFrame.Typed.TH (+    deriveSchema,+    deriveSchemaFromCsvFile,+    deriveSchemaFromCsvFileWith,+ ) import DataFrame.Typed.Types (     Column,     TSortOrder (..),
src/DataFrame/Typed/Access.hs view
@@ -21,7 +21,7 @@ import DataFrame.Internal.Column (Columnable) import DataFrame.Internal.Expression (Expr (Col)) import qualified DataFrame.Operations.Core as D-import DataFrame.Typed.Schema (AssertPresent, Lookup)+import DataFrame.Typed.Schema (AssertPresent, SafeLookup) import DataFrame.Typed.Types (TypedDataFrame (..))  {- | Retrieve a column as a boxed 'Vector', with the type determined by@@ -30,7 +30,7 @@ columnAsVector ::     forall name cols a.     ( KnownSymbol name-    , a ~ Lookup name cols+    , a ~ SafeLookup name cols     , Columnable a     , AssertPresent name cols     ) =>@@ -44,7 +44,7 @@ columnAsList ::     forall name cols a.     ( KnownSymbol name-    , a ~ Lookup name cols+    , a ~ SafeLookup name cols     , Columnable a     , AssertPresent name cols     ) =>
src/DataFrame/Typed/Expr.hs view
@@ -155,7 +155,7 @@  ) import DataFrame.Internal.Types (Promote, PromoteDiv) -import DataFrame.Typed.Schema (AssertPresent, Lookup)+import DataFrame.Typed.Schema (AssertPresent, SafeLookup) import DataFrame.Typed.Types (TExpr (..), TSortOrder (..)) import Prelude hiding (maximum, minimum, sum) @@ -172,7 +172,7 @@ col ::     forall (name :: Symbol) cols a.     ( KnownSymbol name-    , a ~ Lookup name cols+    , a ~ SafeLookup name cols     , Columnable a     , AssertPresent name cols     ) =>
src/DataFrame/Typed/Freeze.hs view
@@ -18,6 +18,7 @@ import qualified Data.Text as T import Type.Reflection (SomeTypeRep) +import Data.List (stripPrefix) import qualified DataFrame.Internal.Column as C import qualified DataFrame.Internal.DataFrame as D import DataFrame.Operations.Core (columnNames)@@ -81,6 +82,17 @@                             <> ", got "                             <> T.pack (C.columnTypeString col) --- | Check if a Column's element type matches the expected SomeTypeRep.+{- | Check if a Column's element type matches the expected SomeTypeRep.+For nullable columns (those with a bitmap), @Maybe a@ in the schema matches+a column whose inner type is @a@, since we store nullable data as+@BoxedColumn (Just bm) a@ or @UnboxedColumn (Just bm) a@ rather than+@Column (Maybe a)@.+-} matchesType :: SomeTypeRep -> C.Column -> Bool-matchesType expected col = T.pack (show expected) == T.pack (C.columnTypeString col)+matchesType expected col =+    let expectedStr = show expected+        colTypeStr = C.columnTypeString col+     in expectedStr == colTypeStr+            || ( C.hasMissing col -- nullable column: schema says "Maybe X", column stores "X" with a bitmap+                    && Just colTypeStr == stripPrefix "Maybe " expectedStr+               )
src/DataFrame/Typed/Operations.hs view
@@ -328,7 +328,7 @@     forall name a cols.     ( KnownSymbol name     , Columnable a-    , a ~ Lookup name cols+    , a ~ SafeLookup name cols     , AssertPresent name cols     ) =>     TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
src/DataFrame/Typed/Schema.hs view
@@ -16,6 +16,7 @@ module DataFrame.Typed.Schema (     -- * Type families for schema manipulation     Lookup,+    SafeLookup,     HasName,     RemoveColumn,     Impute,@@ -75,6 +76,15 @@     Lookup name '[] =         TypeError             ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")++{- | Like 'Lookup', but returns a harmless fallback ('Int') instead of+'TypeError' when the column is not found.  Use together with+'AssertPresent' so the error fires exactly once.+-}+type family SafeLookup (name :: Symbol) (cols :: [Type]) :: Type where+    SafeLookup name (Column name a ': _) = a+    SafeLookup name (Column _ _ ': rest) = SafeLookup name rest+    SafeLookup name '[] = Int  -- | Unwrap a Maybe from a type after we impute values. type family Impute (name :: Symbol) (cols :: [Type]) :: [Type] where
src/DataFrame/Typed/TH.hs view
@@ -8,6 +8,7 @@     -- * Schema inference     deriveSchema,     deriveSchemaFromCsvFile,+    deriveSchemaFromCsvFileWith,      -- * Re-export for TH splices     TypedDataFrame,@@ -49,8 +50,11 @@     pure [TySynD synName [] schemaType]  deriveSchemaFromCsvFile :: String -> String -> DecsQ-deriveSchemaFromCsvFile typeName path = do-    df <- liftIO (D.readCsv path)+deriveSchemaFromCsvFile = deriveSchemaFromCsvFileWith D.defaultReadOptions++deriveSchemaFromCsvFileWith :: D.ReadOptions -> String -> String -> DecsQ+deriveSchemaFromCsvFileWith opts typeName path = do+    df <- liftIO (D.readSeparated opts path)     deriveSchema typeName df  getSchemaInfo :: D.DataFrame -> [(T.Text, String)]
tests/DecisionTree.hs view
@@ -439,7 +439,7 @@         [ ("label", DI.fromList (replicate 6 "pos" ++ replicate 6 "neg" :: [T.Text]))         ,             ( "x"-            , DI.OptionalColumn+            , DI.fromVector                 ( V.fromList $                     map (Just . fromIntegral) ([1 .. 6] :: [Int])                         ++ map (Just . fromIntegral) ([7 .. 12] :: [Int]) ::@@ -455,7 +455,7 @@         [ ("label", DI.fromList (["pos", "pos", "pos", "neg", "neg", "neg"] :: [T.Text]))         ,             ( "x"-            , DI.OptionalColumn+            , DI.fromVector                 ( V.fromList                     [Just 1.0, Nothing, Just 3.0, Just 7.0, Nothing, Just 9.0] ::                     V.Vector (Maybe Double)@@ -463,16 +463,16 @@             )         ] --- numericCols picks up OptionalColumn (Maybe Double) as NMaybeDouble.+-- numericCols picks up DI.fromVector (Maybe Double) as NMaybeDouble. numericColsNullableDoubleTest :: Test numericColsNullableDoubleTest = TestCase $ do     let exprs = numericCols nullableSepDF         hasMD = any (\case NMaybeDouble _ -> True; _ -> False) exprs     assertBool-        "numericCols finds NMaybeDouble for OptionalColumn (Maybe Double)"+        "numericCols finds NMaybeDouble for DI.fromVector (Maybe Double)"         hasMD --- numericCols picks up OptionalColumn (Maybe Int) as NMaybeDouble (via whenPresent).+-- numericCols picks up DI.fromVector (Maybe Int) as NMaybeDouble (via whenPresent). numericColsNullableIntTest :: Test numericColsNullableIntTest = TestCase $ do     let df =@@ -480,18 +480,18 @@                 [ ("label", DI.fromList (["pos", "neg"] :: [T.Text]))                 ,                     ( "n"-                    , DI.OptionalColumn (V.fromList [Just (1 :: Int), Just 2] :: V.Vector (Maybe Int))+                    , DI.fromVector (V.fromList [Just (1 :: Int), Just 2] :: V.Vector (Maybe Int))                     )                 ]         hasMD = any (\case NMaybeDouble _ -> True; _ -> False) (numericCols df)-    assertBool "numericCols finds NMaybeDouble for OptionalColumn (Maybe Int)" hasMD+    assertBool "numericCols finds NMaybeDouble for DI.fromVector (Maybe Int)" hasMD --- generateNumericConds is non-empty for a DF with an OptionalColumn (Maybe Double).+-- generateNumericConds is non-empty for a DF with an DI.fromVector (Maybe Double). numericCondsNullableNonEmptyTest :: Test numericCondsNullableNonEmptyTest =     TestCase $         assertBool-            "generateNumericConds non-empty for OptionalColumn (Maybe Double)"+            "generateNumericConds non-empty for DI.fromVector (Maybe Double)"             (not (null (generateNumericConds defaultTreeConfig nullableSepDF)))  -- Null values evaluate to False for threshold conditions (null rows route right).@@ -502,7 +502,7 @@                 [ ("label", DI.fromList (["A", "B"] :: [T.Text]))                 ,                     ( "x"-                    , DI.OptionalColumn+                    , DI.fromVector                         (V.fromList [Nothing, Just (5.0 :: Double)] :: V.Vector (Maybe Double))                     )                 ]@@ -541,14 +541,14 @@         (loss >= 0.0 && loss <= 1.0)  -- numericExprsWithTerms produces cross-column combinations when one col is--- OptionalColumn (Maybe Double) and another is a plain UnboxedColumn Double.+-- DI.fromVector (Maybe Double) and another is a plain UnboxedColumn Double. numericExprsWithTermsMixedTest :: Test numericExprsWithTermsMixedTest = TestCase $ do     let df =             D.fromNamedColumns                 [                     ( "x"-                    , DI.OptionalColumn+                    , DI.fromVector                         (V.fromList [Just 1.0, Just 2.0, Just 3.0] :: V.Vector (Maybe Double))                     )                 , ("y", DI.fromList ([4.0, 5.0, 6.0] :: [Double]))
tests/GenDataFrame.hs view
@@ -14,9 +14,9 @@ genColumn :: Int -> Gen Column genColumn len =     oneof-        [ BoxedColumn . V.fromList <$> vectorOf len (arbitrary @Int)-        , UnboxedColumn . VU.fromList <$> vectorOf len (arbitrary @Double)-        , OptionalColumn . V.fromList <$> vectorOf len (arbitrary @(Maybe Int))+        [ BoxedColumn Nothing . V.fromList <$> vectorOf len (arbitrary @Int)+        , UnboxedColumn Nothing . VU.fromList <$> vectorOf len (arbitrary @Double)+        , fromVector . V.fromList <$> vectorOf len (arbitrary @(Maybe Int))         ]  genDataFrame :: Gen DataFrame
tests/Main.hs view
@@ -33,6 +33,7 @@ import qualified Operations.Subset import qualified Operations.Take import qualified Operations.Typing+import qualified Operations.WriteCsv import qualified Parquet import qualified Properties @@ -53,6 +54,7 @@             ++ Operations.Nullable.tests             ++ Operations.Provenance.tests             ++ Operations.ReadCsv.tests+            ++ Operations.WriteCsv.tests             ++ Operations.Shuffle.tests             ++ Operations.Sort.tests             ++ Operations.Statistics.tests
tests/Operations/Apply.hs view
@@ -40,7 +40,7 @@     TestCase         ( assertEqual             "Boxed apply unboxed when result is unboxed"-            (Just $ DI.UnboxedColumn (VU.fromList (replicate 26 (1 :: Int))))+            (Just $ DI.UnboxedColumn Nothing (VU.fromList (replicate 26 (1 :: Int))))             (DI.getColumn "test2" $ D.apply @String (const (1 :: Int)) "test2" testData)         ) @@ -49,7 +49,7 @@     TestCase         ( assertEqual             "Boxed apply remains in boxed vector"-            (Just $ DI.BoxedColumn (V.fromList (replicate 26 (1 :: Integer))))+            (Just $ DI.BoxedColumn Nothing (V.fromList (replicate 26 (1 :: Integer))))             (DI.getColumn "test2" $ D.apply @String (const (1 :: Integer)) "test2" testData)         ) @@ -204,6 +204,7 @@             "applyWhere works as intended"             ( Just $                 DI.UnboxedColumn+                    Nothing                     (VU.fromList (zipWith ($) (cycle [id, (+ 1)]) [(1 :: Int) .. 26]))             )             ( D.getColumn "test5" $@@ -223,7 +224,7 @@     TestCase         ( assertEqual             "impute fills Nothing with the given value"-            (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 0, 3]))+            (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 0, 3]))             (DI.getColumn "opt" $ impute (F.col @(Maybe Int) "opt") 0 imputeData)         ) 
tests/Operations/Derive.hs view
@@ -32,6 +32,7 @@             "derive works with column expression"             ( Just $                 DI.BoxedColumn+                    Nothing                     (V.fromList (zipWith (\n c -> show n ++ [c]) [1 .. 26] ['a' .. 'z']))             )             ( DI.getColumn "test4" $
tests/Operations/GroupBy.hs view
@@ -47,7 +47,7 @@     TestCase         ( assertExpectException             "[Error Case]"-            (D.columnNotFound "[\"test0\"]" "groupBy" (D.columnNames testData))+            (D.columnsNotFound ["test0"] "groupBy" (D.columnNames testData))             (print $ D.groupBy ["test0"] testData)         ) 
tests/Operations/InsertColumn.hs view
@@ -31,7 +31,9 @@     TestCase         ( assertEqual             "Two columns should be equal"-            (Just $ DI.BoxedColumn (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]))+            ( Just $+                DI.BoxedColumn Nothing (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"])+            )             ( DI.getColumn "new" $                 D.insertVector "new" (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty             )@@ -57,7 +59,7 @@     TestCase         ( assertEqual             "Value should be boxed"-            (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3]))+            (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 2, 3]))             (DI.getColumn "new" $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) D.empty)         ) @@ -77,7 +79,7 @@         ( assertEqual             "Missing values should be replaced with Nothing"             ( Just $-                DI.OptionalColumn+                DI.fromVector                     (V.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing])             )             ( DI.getColumn "newer" $@@ -92,7 +94,7 @@         ( assertEqual             "Missing values should be replaced with Nothing"             ( Just $-                DI.OptionalColumn+                DI.fromVector                     (V.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing])             )             ( DI.getColumn "newer" $@@ -106,7 +108,7 @@     TestCase         ( assertEqual             "Missing values should be replaced with Nothing"-            (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3, 0, 0]))+            (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 2, 3, 0, 0]))             ( DI.getColumn "newer" $                 D.insertVectorWithDefault 0 "newer" (V.fromList [1 :: Int, 2, 3]) $                     D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty@@ -118,7 +120,7 @@     TestCase         ( assertEqual             "Lists should be the same size"-            (Just $ DI.UnboxedColumn (VU.fromList [(6 :: Int) .. 10]))+            (Just $ DI.UnboxedColumn Nothing (VU.fromList [(6 :: Int) .. 10]))             ( DI.getColumn "newer" $                 D.insertVectorWithDefault 0 "newer" (V.fromList [(6 :: Int) .. 10]) $                     D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty
tests/Operations/Join.hs view
@@ -4,7 +4,9 @@  module Operations.Join where -import Data.Text (Text)+import Assertions (assertExpectException)+import Control.Exception (evaluate)+import Data.Text (Text, unpack) import Data.These import qualified DataFrame as D import qualified DataFrame.Functions as F@@ -26,6 +28,21 @@         , ("B", D.fromList ["B0" :: Text, "B1", "B2"])         ] +assertMissingJoinColumns :: String -> [Text] -> D.DataFrame -> Assertion+assertMissingJoinColumns preface missingKeys result = do+    assertExpectException+        preface+        (if length missingKeys == 1 then "Column not found" else "Columns not found")+        (evaluate (D.nRows result))+    mapM_+        ( \missingKey ->+            assertExpectException preface (unpack missingKey) (evaluate (D.nRows result))+        )+        missingKeys++assertMissingJoinColumn :: String -> Text -> D.DataFrame -> Assertion+assertMissingJoinColumn preface missingKey = assertMissingJoinColumns preface [missingKey]+ testInnerJoin :: Test testInnerJoin =     TestCase@@ -253,6 +270,59 @@             (D.sortBy [D.Asc (F.col @Text "key")] (fullOuterJoin ["key"] dfL dfR))         ) +testInnerJoinMissingKey :: Test+testInnerJoinMissingKey =+    TestCase $+        assertMissingJoinColumn+            "Inner join should fail when the join key is missing"+            "Cats"+            (innerJoin ["Cats"] df1 df2)++testLeftJoinMissingKey :: Test+testLeftJoinMissingKey =+    TestCase $+        assertMissingJoinColumn+            "Left join should fail when the join key is missing"+            "Cats"+            (leftJoin ["Cats"] df1 df2)++testRightJoinMissingKey :: Test+testRightJoinMissingKey =+    TestCase $+        assertMissingJoinColumn+            "Right join should fail when the join key is missing"+            "Animals"+            (rightJoin ["Animals"] df1 df2)++testFullOuterJoinMissingKey :: Test+testFullOuterJoinMissingKey =+    TestCase $+        assertMissingJoinColumn+            "Full outer join should fail when the join key is missing"+            "Cats"+            (fullOuterJoin ["Cats"] df1 df2)++testInnerJoinMissingKeys :: Test+testInnerJoinMissingKeys =+    TestCase $+        assertMissingJoinColumns+            "Inner join should report every missing join key"+            ["Animals", "Cats"]+            (innerJoin ["Animals", "Cats"] df1 df2)++testInnerJoinMissingKeysSuggestion :: Test+testInnerJoinMissingKeysSuggestion =+    TestCase $+        let typoDf =+                D.fromNamedColumns+                    [ ("hello", D.fromList ["H" :: Text])+                    , ("world", D.fromList ["W" :: Text])+                    ]+         in assertExpectException+                "Inner join should report consolidated suggestions for missing join keys"+                "Did you mean [\"hello\", \"world\"]?"+                (evaluate (D.nRows (innerJoin ["helo", "wrld"] typoDf typoDf)))+ -- Empty DataFrame fixtures: same schema as df1/df2 but zero rows. emptyDf1 :: D.DataFrame emptyDf1 =@@ -353,6 +423,12 @@     , TestLabel "leftJoinWithCollisions" testLeftJoinWithCollisions     , TestLabel "rightJoinWithCollisions" testRightJoinWithCollisions     , TestLabel "outerJoinWithCollisions" testOuterJoinWithCollisions+    , TestLabel "innerJoinMissingKey" testInnerJoinMissingKey+    , TestLabel "leftJoinMissingKey" testLeftJoinMissingKey+    , TestLabel "rightJoinMissingKey" testRightJoinMissingKey+    , TestLabel "fullOuterJoinMissingKey" testFullOuterJoinMissingKey+    , TestLabel "innerJoinMissingKeys" testInnerJoinMissingKeys+    , TestLabel "innerJoinMissingKeysSuggestion" testInnerJoinMissingKeysSuggestion     , TestLabel "innerJoinBothEmpty" testInnerJoinBothEmpty     , TestLabel "innerJoinLeftEmpty" testInnerJoinLeftEmpty     , TestLabel "innerJoinRightEmpty" testInnerJoinRightEmpty
tests/Operations/Nullable.hs view
@@ -27,7 +27,7 @@ testData =     D.fromNamedColumns         [ ("x", DI.fromList ([1, 2, 3] :: [Int]))-        , ("y", DI.OptionalColumn (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+        , ("y", DI.fromVector (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))         ]  -- | col @Int .+ col @(Maybe Int)  should give Maybe Int column@@ -36,7 +36,7 @@     TestCase         ( assertEqual             "Int .+ Maybe Int = Maybe Int"-            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))             ( DI.getColumn "result" $                 D.derive                     "result"@@ -51,7 +51,7 @@     TestCase         ( assertEqual             "Maybe Int .+ Int = Maybe Int"-            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))             ( DI.getColumn "result" $                 D.derive                     "result"@@ -81,7 +81,7 @@     TestCase         ( assertEqual             "Maybe Int .+ Maybe Int = Maybe Int"-            (Just $ DI.OptionalColumn (V.fromList [Just 20, Nothing, Just 60 :: Maybe Int]))+            (Just $ DI.fromVector (V.fromList [Just 20, Nothing, Just 60 :: Maybe Int]))             ( DI.getColumn "result" $                 D.derive                     "result"@@ -97,7 +97,7 @@         ( assertEqual             "Int .- Maybe Int = Maybe Int"             ( Just $-                DI.OptionalColumn (V.fromList [Just (-9), Nothing, Just (-27) :: Maybe Int])+                DI.fromVector (V.fromList [Just (-9), Nothing, Just (-27) :: Maybe Int])             )             ( DI.getColumn "result" $                 D.derive@@ -114,7 +114,7 @@         ( assertEqual             "Int .== Maybe Int = Maybe Bool"             ( Just $-                DI.OptionalColumn (V.fromList [Just False, Nothing, Just False :: Maybe Bool])+                DI.fromVector (V.fromList [Just False, Nothing, Just False :: Maybe Bool])             )             ( DI.getColumn "result" $                 D.derive@@ -150,7 +150,7 @@         ( assertEqual             "nullLift negate (Maybe Int) propagates Nothing"             ( Just $-                DI.OptionalColumn (V.fromList [Just (-10), Nothing, Just (-30) :: Maybe Int])+                DI.fromVector (V.fromList [Just (-10), Nothing, Just (-30) :: Maybe Int])             )             ( DI.getColumn "result" $                 D.derive@@ -181,7 +181,7 @@     TestCase         ( assertEqual             "nullLift2 (+) Int (Maybe Int) = Maybe Int"-            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))             ( DI.getColumn "result" $                 D.derive                     "result"@@ -196,7 +196,7 @@     TestCase         ( assertEqual             "nullLift2 (+) (Maybe Int) Int = Maybe Int"-            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))             ( DI.getColumn "result" $                 D.derive                     "result"@@ -229,11 +229,11 @@ crossData =     D.fromNamedColumns         [ ("x", DI.fromList ([1, 2, 3] :: [Int]))-        , ("y", DI.OptionalColumn (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+        , ("y", DI.fromVector (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))         , ("d", DI.fromList ([1.5, 2.5, 3.5] :: [Double]))         ,             ( "md"-            , DI.OptionalColumn (V.fromList [Just 10.5, Nothing, Just 30.5 :: Maybe Double])+            , DI.fromVector (V.fromList [Just 10.5, Nothing, Just 30.5 :: Maybe Double])             )         ] @@ -268,7 +268,7 @@         ( assertEqual             "Maybe Int .+ Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+                DI.fromVector (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @(Maybe Int) "y" .+ F.col @Double "d") crossData@@ -282,7 +282,7 @@         ( assertEqual             "Int .+ Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+                DI.fromVector (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @Int "x" .+ F.col @(Maybe Double) "md") crossData@@ -296,7 +296,7 @@         ( assertEqual             "Maybe Int .+ Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 20.5, Nothing, Just 60.5 :: Maybe Double])+                DI.fromVector (V.fromList [Just 20.5, Nothing, Just 60.5 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive@@ -349,7 +349,7 @@         ( assertEqual             "Maybe Int .- Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 8.5, Nothing, Just 26.5 :: Maybe Double])+                DI.fromVector (V.fromList [Just 8.5, Nothing, Just 26.5 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @(Maybe Int) "y" .- F.col @Double "d") crossData@@ -363,7 +363,7 @@         ( assertEqual             "Int .- Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn+                DI.fromVector                     (V.fromList [Just (-9.5), Nothing, Just (-27.5) :: Maybe Double])             )             ( DI.getColumn "result" $@@ -378,7 +378,7 @@         ( assertEqual             "Maybe Int .- Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn+                DI.fromVector                     (V.fromList [Just (-0.5), Nothing, Just (-0.5) :: Maybe Double])             )             ( DI.getColumn "result" $@@ -408,7 +408,7 @@         ( assertEqual             "Maybe Int .* Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 15.0, Nothing, Just 105.0 :: Maybe Double])+                DI.fromVector (V.fromList [Just 15.0, Nothing, Just 105.0 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @(Maybe Int) "y" .* F.col @Double "d") crossData@@ -422,7 +422,7 @@         ( assertEqual             "Int .* Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 10.5, Nothing, Just 91.5 :: Maybe Double])+                DI.fromVector (V.fromList [Just 10.5, Nothing, Just 91.5 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @Int "x" .* F.col @(Maybe Double) "md") crossData@@ -436,7 +436,7 @@         ( assertEqual             "Maybe Int .* Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 105.0, Nothing, Just 915.0 :: Maybe Double])+                DI.fromVector (V.fromList [Just 105.0, Nothing, Just 915.0 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive@@ -451,11 +451,11 @@ divData =     D.fromNamedColumns         [ ("x", DI.fromList ([2, 4, 6] :: [Int]))-        , ("y", DI.OptionalColumn (V.fromList [Just 4, Nothing, Just 6 :: Maybe Int]))+        , ("y", DI.fromVector (V.fromList [Just 4, Nothing, Just 6 :: Maybe Int]))         , ("d", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))         ,             ( "md"-            , DI.OptionalColumn (V.fromList [Just 1.0, Nothing, Just 3.0 :: Maybe Double])+            , DI.fromVector (V.fromList [Just 1.0, Nothing, Just 3.0 :: Maybe Double])             )         ] @@ -490,7 +490,7 @@         ( assertEqual             "Maybe Int ./ Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+                DI.fromVector (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @Double "d") divData@@ -504,7 +504,7 @@         ( assertEqual             "Int ./ Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 2.0, Nothing, Just 2.0 :: Maybe Double])+                DI.fromVector (V.fromList [Just 2.0, Nothing, Just 2.0 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @Int "x" ./ F.col @(Maybe Double) "md") divData@@ -518,7 +518,7 @@         ( assertEqual             "Maybe Int ./ Maybe Double = Maybe Double"             ( Just $-                DI.OptionalColumn (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+                DI.fromVector (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])             )             ( DI.getColumn "result" $                 D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @(Maybe Double) "md") divData@@ -705,7 +705,7 @@     TestCase         ( assertEqual             "apply negate to Maybe Int column fmaps"-            (Just $ DI.OptionalColumn (V.fromList [Just (-10), Nothing, Just (-30 :: Int)]))+            (Just $ DI.fromVector (V.fromList [Just (-10), Nothing, Just (-30 :: Int)]))             ( DI.getColumn "y" $                 D.apply @Int negate "y" testData             )
tests/Operations/ReadCsv.hs view
@@ -3,156 +3,29 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- Test fixtures inspired by csv-spectrum (https://github.com/max-mapper/csv-spectrum)- module Operations.ReadCsv where -import qualified Data.List as L-import qualified Data.Map as M import qualified Data.Text as T-import qualified Data.Text.IO as TIO-import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D -import Data.Function (on)-import Data.Maybe (fromMaybe)-import Data.Type.Equality (testEquality, (:~:) (Refl)) import DataFrame.Internal.Column (Column (..), columnTypeString)-import qualified DataFrame.Internal.Column as DI import DataFrame.Internal.DataFrame (     DataFrame (..),-    columnIndices,-    columns,     dataframeDimensions,     getColumn,  )-import System.Directory (removeFile)-import System.IO (IOMode (..), withFile) import Test.HUnit import Type.Reflection (typeRep) -fixtureDir :: FilePath-fixtureDir = "./tests/data/unstable_csv/"--tempDir :: FilePath-tempDir = "./tests/data/unstable_csv/"+arbuthnotPath :: FilePath+arbuthnotPath = "./tests/data/arbuthnot.csv"  readCsvNoInfer :: FilePath -> IO DataFrame readCsvNoInfer =     D.readCsvWithOpts         D.defaultReadOptions{D.typeSpec = D.NoInference} ------------------------------------------------------------------------------------ Pretty-printer-----------------------------------------------------------------------------------prettyPrintCsv :: FilePath -> DataFrame -> IO ()-prettyPrintCsv = prettyPrintSeparated ','--prettyPrintTsv :: FilePath -> DataFrame -> IO ()-prettyPrintTsv = prettyPrintSeparated '\t'--prettyPrintSeparated :: Char -> FilePath -> DataFrame -> IO ()-prettyPrintSeparated sep filepath df = withFile filepath WriteMode $ \handle -> do-    let (rows, _) = dataframeDimensions df-    let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df)))-    TIO.hPutStrLn-        handle-        (T.intercalate (T.singleton sep) (map (escapeField sep) headers))-    -- Write data rows-    mapM_-        (TIO.hPutStrLn handle . T.intercalate (T.singleton sep) . getRowEscaped sep df)-        [0 .. rows - 1]---- Note: The unstable parser does not unescape doubled quotes (""  -> "),--- so we must not double-escape them here. We only wrap in quotes when needed.-escapeField :: Char -> T.Text -> T.Text-escapeField sep field-    | needsQuoting = T.concat ["\"", field, "\""]-    | otherwise = field-  where-    needsQuoting =-        T.any (\c -> c == sep || c == '\n' || c == '\r' || c == '"') field---- | Get a row from the DataFrame with all fields escaped-getRowEscaped :: Char -> DataFrame -> Int -> [T.Text]-getRowEscaped sep df i = V.ifoldr go [] (columns df)-  where-    go :: Int -> Column -> [T.Text] -> [T.Text]-    go _ (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of-        Just e -> escapeField sep textRep : acc-          where-            textRep = case testEquality (typeRep @a) (typeRep @T.Text) of-                Just Refl -> e-                Nothing -> T.pack (show e)-        Nothing -> acc-    go _ (UnboxedColumn c) acc = case c VU.!? i of-        Just e -> escapeField sep (T.pack (show e)) : acc-        Nothing -> acc-    go _ (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of-        Just e -> escapeField sep textRep : acc-          where-            textRep = case testEquality (typeRep @a) (typeRep @T.Text) of-                Just Refl -> fromMaybe "" e-                Nothing -> case e of-                    Just val -> T.pack (show val)-                    Nothing -> ""-        Nothing -> acc--testFastCsv :: String -> FilePath -> Test-testFastCsv name csvPath = TestLabel ("fast_roundtrip_" <> name) $ TestCase $ do-    dfOriginal <- D.fastReadCsvUnstable csvPath-    let tempPath = tempDir <> "temp_fast_" <> name <> ".csv"-    prettyPrintCsv tempPath dfOriginal-    dfRoundtrip <- D.fastReadCsvUnstable tempPath-    assertEqual-        ("Fast round-trip should produce equivalent DataFrame for " <> name)-        dfOriginal-        dfRoundtrip-    removeFile tempPath--testTsv :: String -> FilePath -> Test-testTsv name tsvPath = TestLabel ("roundtrip_tsv_" <> name) $ TestCase $ do-    dfOriginal <- D.readTsvUnstable tsvPath-    let tempPath = tempDir <> "temp_" <> name <> ".tsv"-    prettyPrintTsv tempPath dfOriginal-    dfRoundtrip <- D.readTsvUnstable tempPath-    assertEqual-        ("TSV round-trip should produce equivalent DataFrame for " <> name)-        dfOriginal-        dfRoundtrip-    removeFile tempPath---- Individual round-trip test cases for each fixture--testSimpleFast :: Test-testSimpleFast = testFastCsv "simple" (fixtureDir <> "simple.csv")--testCommaInQuotesFast :: Test-testCommaInQuotesFast = testFastCsv "comma_in_quotes" (fixtureDir <> "comma_in_quotes.csv")--testEscapedQuotesFast :: Test-testEscapedQuotesFast = testFastCsv "escaped_quotes" (fixtureDir <> "escaped_quotes.csv")--testNewlinesFast :: Test-testNewlinesFast = testFastCsv "newlines" (fixtureDir <> "newlines.csv")--testUtf8Fast :: Test-testUtf8Fast = testFastCsv "utf8" (fixtureDir <> "utf8.csv")--testQuotesAndNewlinesFast :: Test-testQuotesAndNewlinesFast = testFastCsv "quotes_and_newlines" (fixtureDir <> "quotes_and_newlines.csv")--testEmptyValuesFast :: Test-testEmptyValuesFast = testFastCsv "empty_values" (fixtureDir <> "empty_values.csv")--testJsonDataFast :: Test-testJsonDataFast = testFastCsv "json_data" (fixtureDir <> "json_data.csv")--arbuthnotPath :: FilePath-arbuthnotPath = "./tests/data/arbuthnot.csv"- -- SpecifyTypes with NoInference fallback: named column is typed, rest stay Text specifyTypesNoInferenceFallback :: Test specifyTypesNoInferenceFallback =@@ -168,11 +41,11 @@                 arbuthnotPath         -- "year" must be Int         case getColumn "year" df of-            Just col@(UnboxedColumn _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+            Just col@(UnboxedColumn _ _) -> assertEqual "year should be Int" "Int" (columnTypeString col)             _ -> assertFailure "expected UnboxedColumn for 'year'"         -- "boys" unspecified + NoInference → stays Text         case getColumn "boys" df of-            Just col@(BoxedColumn _) -> assertEqual "boys should be Text" "Text" (columnTypeString col)+            Just col@(BoxedColumn _ _) -> assertEqual "boys should be Text" "Text" (columnTypeString col)             _ -> assertFailure "expected BoxedColumn for 'boys' with NoInference fallback"  -- SpecifyTypes with InferFromSample fallback: named column typed, rest inferred@@ -190,11 +63,11 @@                 arbuthnotPath         -- "year" must be Int (explicitly specified)         case getColumn "year" df of-            Just col@(UnboxedColumn _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+            Just col@(UnboxedColumn _ _) -> assertEqual "year should be Int" "Int" (columnTypeString col)             _ -> assertFailure "expected UnboxedColumn for 'year'"         -- "boys" unspecified + InferFromSample → inferred as Int         case getColumn "boys" df of-            Just col@(UnboxedColumn _) -> assertEqual "boys should be Int" "Int" (columnTypeString col)+            Just col@(UnboxedColumn _ _) -> assertEqual "boys should be Int" "Int" (columnTypeString col)             _ ->                 assertFailure "expected UnboxedColumn for 'boys' with InferFromSample fallback" @@ -213,128 +86,17 @@                     }                 arbuthnotPath         case getColumn "girls" df of-            Just col@(UnboxedColumn _) -> assertEqual "girls should be Int" "Int" (columnTypeString col)+            Just col@(UnboxedColumn _ _) -> assertEqual "girls should be Int" "Int" (columnTypeString col)             _ ->                 assertFailure                     "expected UnboxedColumn for 'girls' via fallback InferFromSample 10" -testCrlfCsv :: Test-testCrlfCsv = TestLabel "malformed_crlf_csv" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "crlf.csv")-    let (rows, cols) = dataframeDimensions df-    assertEqual "crlf.csv: 2 data rows" 2 rows-    assertEqual "crlf.csv: 2 columns" 2 cols-    case getColumn "name" df of-        Nothing -> assertFailure "crlf.csv: column 'name' missing"-        Just col ->-            assertEqual-                "crlf.csv: name has no \\r"-                (DI.fromList @T.Text ["Alice", "Bob"])-                col--testCrlfTsv :: Test-testCrlfTsv = TestLabel "malformed_crlf_tsv" $ TestCase $ do-    df <- D.readTsvUnstable (fixtureDir <> "crlf.tsv")-    let (rows, _) = dataframeDimensions df-    assertEqual "crlf.tsv: 1 data row" 1 rows-    case getColumn "name" df of-        Nothing -> assertFailure "crlf.tsv: column 'name' missing"-        Just col ->-            assertEqual-                "crlf.tsv: name has no \\r"-                (DI.fromList @T.Text ["Alice"])-                col--testHeaderOnly :: Test-testHeaderOnly = TestLabel "malformed_header_only" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "header_only.csv")-    let (rows, cols) = dataframeDimensions df-    assertEqual "header_only.csv: 0 data rows" 0 rows-    assertEqual "header_only.csv: 3 columns" 3 cols-    let names = map fst . L.sortBy (compare `on` snd) . M.toList $ columnIndices df-    assertEqual "header_only.csv: column names" ["first", "second", "third"] names--testTrailingBlankLine :: Test-testTrailingBlankLine = TestLabel "malformed_trailing_blank_line" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "trailing_blank_line.csv")-    -- blank line contributes 1 extra delimiter; (3+3+1) div 3 = 2, numRow=1-    assertEqual-        "trailing_blank_line.csv: 1 data row visible"-        1-        (fst (dataframeDimensions df))--testAllEmptyRow :: Test-testAllEmptyRow = TestLabel "malformed_all_empty_row" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "all_empty_row.csv")-    assertEqual "all_empty_row.csv: 1 data row" 1 (fst (dataframeDimensions df))-    let checkEmpty colName =-            case getColumn colName df of-                Nothing -> assertFailure ("column '" <> T.unpack colName <> "' missing")-                Just col ->-                    assertEqual-                        (T.unpack colName <> " is Nothing (empty field → null)")-                        (DI.fromList @(Maybe T.Text) [Nothing])-                        col-    mapM_ checkEmpty ["a", "b", "c"]--testSingleCol :: Test-testSingleCol = TestLabel "malformed_single_col" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "single_col.csv")-    let (rows, cols) = dataframeDimensions df-    assertEqual "single_col.csv: 3 data rows" 3 rows-    assertEqual "single_col.csv: 1 column" 1 cols-    case getColumn "name" df of-        Nothing -> assertFailure "single_col.csv: column 'name' missing"-        Just col ->-            assertEqual-                "single_col.csv: correct values"-                (DI.fromList @T.Text ["Alice", "Bob", "Carol"])-                col--testWhitespaceFields :: Test-testWhitespaceFields = TestLabel "malformed_whitespace_fields" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "whitespace_fields.csv")-    assertEqual-        "whitespace_fields.csv: 2 data rows"-        2-        (fst (dataframeDimensions df))-    case getColumn "name" df of-        Nothing -> assertFailure "whitespace_fields.csv: 'name' missing"-        Just col -> assertEqual "name stripped" (DI.fromList @T.Text ["Alice", "Bob"]) col-    case getColumn "city" df of-        Nothing -> assertFailure "whitespace_fields.csv: 'city' missing"-        Just col ->-            assertEqual-                "city stripped"-                (DI.fromList @T.Text ["New York", "Los Angeles"])-                col---- File: a,b,c header; row "1,2" (short); row "X,Y,Z" (full)--- Total delimiters: 3 (header) + 2 (short) + 3 (full) = 8--- numCol=3, totalRows=8 div 3=2, numRow=1--- Row-1 stride offsets 3,4,5 → fields "1","2","X"  (X bleeds in from next row)-testMissingFields :: Test-testMissingFields = TestLabel "malformed_missing_fields" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "missing_fields.csv")-    assertEqual-        "missing_fields.csv: integer-division gives 1 visible row"-        1-        (fst (dataframeDimensions df))-    case getColumn "c" df of-        Nothing -> assertFailure "missing_fields.csv: column 'c' missing"-        Just col ->-            -- "X" bleeds from the start of the next row into the missing slot-            assertEqual-                "missing_fields.csv: col 'c' bleeds 'X' from next row"-                (DI.fromList @T.Text ["X"])-                col- -- File: a,b,c header; row "1,2,3,EXTRA" -- Total delimiters: 3 + 4 = 7; 7 div 3 = 2, numRow=1 -- Row-1 strides 3,4,5 → "1","2","3" — EXTRA (stride 6) is never accessed testExtraFields :: Test testExtraFields = TestLabel "malformed_extra_fields" $ TestCase $ do-    df <- readCsvNoInfer (fixtureDir <> "extra_fields.csv")+    df <- readCsvNoInfer ("./tests/data/unstable_csv/" <> "extra_fields.csv")     assertEqual "extra_fields.csv: 1 data row" 1 (fst (dataframeDimensions df))     assertEqual "extra_fields.csv: 3 columns" 3 (snd (dataframeDimensions df))     case getColumn "c" df of@@ -342,49 +104,13 @@         Just col ->             assertEqual                 "extra_fields.csv: 'c' = '3' (EXTRA ignored)"-                (DI.fromList @T.Text ["3"])-                col--testNoTrailingNewline :: Test-testNoTrailingNewline = TestLabel "malformed_no_trailing_newline" $ TestCase $ do-    df <- D.readCsvUnstable (fixtureDir <> "no_trailing_newline.csv")-    assertEqual-        "no_trailing_newline.csv: 1 data row"-        1-        (fst (dataframeDimensions df))-    case getColumn "name" df of-        Nothing -> assertFailure "no_trailing_newline.csv: 'name' missing"-        Just col -> assertEqual "name = Alice" (DI.fromList @T.Text ["Alice"]) col-    case getColumn "city" df of-        Nothing -> assertFailure "no_trailing_newline.csv: 'city' missing"-        Just col ->-            assertEqual-                "city = London (synthetic delimiter worked)"-                (DI.fromList @T.Text ["London"])+                (D.fromList @T.Text ["3"])                 col  tests :: [Test] tests =-    [ testSimpleFast-    , testCommaInQuotesFast-    , testQuotesAndNewlinesFast-    , testEscapedQuotesFast-    , testNewlinesFast-    , testUtf8Fast-    , testQuotesAndNewlinesFast-    , testEmptyValuesFast-    , testJsonDataFast-    , specifyTypesNoInferenceFallback+    [ specifyTypesNoInferenceFallback     , specifyTypesInferFallback     , specifyTypesSampleSize-    , testCrlfCsv-    , testCrlfTsv-    , testHeaderOnly-    , testTrailingBlankLine-    , testAllEmptyRow-    , testSingleCol-    , testWhitespaceFields-    , testMissingFields     , testExtraFields-    , testNoTrailingNewline     ]
tests/Operations/Sort.hs view
@@ -85,7 +85,7 @@     TestCase         ( assertExpectException             "[Error Case]"-            (D.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))+            (D.columnsNotFound ["test0"] "sortBy" (D.columnNames testData))             (print $ D.sortBy [D.Asc (F.col @Int "test0")] testData)         ) 
tests/Operations/Statistics.hs view
@@ -244,7 +244,7 @@                     (abs (r - 1.0) < 1e-10)         ) --- Requesting a missing column should throw ColumnNotFoundException+-- Requesting a missing column should throw ColumnsNotFoundException correlationMissingColumn :: Test correlationMissingColumn =     TestCase
tests/Operations/Typing.hs view
@@ -4,5125 +4,1108 @@ module Operations.Typing where  import qualified Data.Text as T-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU-import qualified DataFrame as D-import qualified DataFrame.Internal.Column as DI-import qualified DataFrame.Operations.Typing as D--import Data.Time (Day, fromGregorian)-import Test.HUnit (Test (TestCase, TestLabel), assertEqual)--testData :: D.DataFrame-testData =-    D.fromNamedColumns-        [ ("test1", DI.fromList ([1 .. 26] :: [Int]))-        , ("test2", DI.fromList ['a' .. 'z'])-        ]---- Dimensions-correctDimensions :: Test-correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))--emptyDataframeDimensions :: Test-emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))--dimensionsTest :: [Test]-dimensionsTest =-    [ TestLabel "dimensions_correctDimensions" correctDimensions-    , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions-    ]---- PARSING TESTS-------- 1. SIMPLE CASES-parseBools :: Test-parseBools =-    let afterParse :: [Bool]-        afterParse = [True, True, True] ++ [False, False, False]-        beforeParse :: [T.Text]-        beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools without missing values as UnboxedColumn of Bools"-                expected-                actual-            )--parseInts :: Test-parseInts =-    let afterParse :: [Int]-        afterParse = [1 .. 50]-        beforeParse :: [T.Text]-        beforeParse = T.pack . show <$> [1 .. 50]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints without missing values as UnboxedColumn of Ints"-                expected-                actual-            )--parseDoubles :: Test-parseDoubles =-    let afterParse :: [Double]-        afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]-        beforeParse :: [T.Text]-        beforeParse =-            T.pack . show-                <$> [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"-                expected-                actual-            )--parseDates :: Test-parseDates =-    let afterParse :: [Day]-        afterParse =-            [ fromGregorian 2020 02 12-            , fromGregorian 2020 02 13-            , fromGregorian 2020 02 14-            , fromGregorian 2020 02 15-            , fromGregorian 2020 02 16-            , fromGregorian 2020 02 17-            , fromGregorian 2020 02 18-            , fromGregorian 2020 02 19-            , fromGregorian 2020 02 20-            , fromGregorian 2020 02 21-            , fromGregorian 2020 02 22-            , fromGregorian 2020 02 23-            , fromGregorian 2020 02 24-            , fromGregorian 2020 02 25-            , fromGregorian 2020 02 26-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates without missing values as BoxedColumn of Days"-                expected-                actual-            )--parseTexts :: Test-parseTexts =-    let afterParse :: [T.Text]-        afterParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , "Surrender now or prepare to fight!"-            , "Meowth, that's right!"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , "Surrender now or prepare to fight!"-            , "Meowth, that's right!"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Text without missing values as BoxedColumn of Text"-                expected-                actual-            )----- 2. COMBINATION CASES-parseBoolsAndIntsAsTexts :: Test-parseBoolsAndIntsAsTexts =-    let afterParse :: [T.Text]-        afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]-        beforeParse :: [T.Text]-        beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses mixture of Bools and Ints as Text"-                expected-                actual-            )--parseIntsAndDoublesAsDoubles :: Test-parseIntsAndDoublesAsDoubles =-    let afterParse :: [Double]-        afterParse =-            [ 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-            , 1.0-            , 2.0-            , 3.0-            , 4.0-            , 5.0-            , 6.0-            , 7.0-            , 8.0-            , 9.0-            , 10.0-            , 11.0-            , 12.0-            , 13.0-            , 14.0-            , 15.0-            , 16.0-            , 17.0-            , 18.0-            , 19.0-            , 20.0-            , 21.0-            , 22.0-            , 23.0-            , 24.0-            , 25.0-            , 26.0-            , 27.0-            , 28.0-            , 29.0-            , 30.0-            , 31.0-            , 32.0-            , 33.0-            , 34.0-            , 35.0-            , 36.0-            , 37.0-            , 38.0-            , 39.0-            , 40.0-            , 41.0-            , 42.0-            , 43.0-            , 44.0-            , 45.0-            , 46.0-            , 47.0-            , 48.0-            , 49.0-            , 50.0-            , 3.14-            , 2.22-            , 8.55-            , 23.3-            , 12.22222235049450945049504950-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"-                expected-                actual-            )--parseIntsAndDatesAsTexts :: Test-parseIntsAndDatesAsTexts =-    let afterParse :: [T.Text]-        afterParse =-            [ "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"-            , "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            , "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"-                expected-                actual-            )--parseTextsAndDoublesAsTexts :: Test-parseTextsAndDoublesAsTexts =-    let afterParse :: [T.Text]-        afterParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"-                expected-                actual-            )--parseDatesAndTextsAsTexts :: Test-parseDatesAndTextsAsTexts =-    let afterParse :: [T.Text]-        afterParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            , "Jessie"-            , "James"-            , "Meowth"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            , "Jessie"-            , "James"-            , "Meowth"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"-                expected-                actual-            )---- 3A. PARSING WITH SAFEREAD OFF--parseBoolsWithoutSafeRead :: Test-parseBoolsWithoutSafeRead =-    let afterParse :: [Bool]-        afterParse = replicate 10 True ++ replicate 10 False-        beforeParse :: [T.Text]-        beforeParse = replicate 10 "true" ++ replicate 10 "false"-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"-                expected-                actual-            )--parseIntsWithoutSafeRead :: Test-parseIntsWithoutSafeRead =-    let afterParse :: [Int]-        afterParse =-            [ 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-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"-                expected-                actual-            )--parseDoublesWithoutSafeRead :: Test-parseDoublesWithoutSafeRead =-    let afterParse :: [Double]-        afterParse =-            [ 1.0-            , 2.0-            , 3.0-            , 4.0-            , 5.0-            , 6.0-            , 7.0-            , 8.0-            , 9.0-            , 10.0-            , 11.0-            , 12.0-            , 13.0-            , 14.0-            , 15.0-            , 16.0-            , 17.0-            , 18.0-            , 19.0-            , 20.0-            , 21.0-            , 22.0-            , 23.0-            , 24.0-            , 25.0-            , 26.0-            , 27.0-            , 28.0-            , 29.0-            , 30.0-            , 31.0-            , 32.0-            , 33.0-            , 34.0-            , 35.0-            , 36.0-            , 37.0-            , 38.0-            , 39.0-            , 40.0-            , 41.0-            , 42.0-            , 43.0-            , 44.0-            , 45.0-            , 46.0-            , 47.0-            , 48.0-            , 49.0-            , 50.0-            , 3.14-            , 2.22-            , 8.55-            , 23.3-            , 12.22222235049450945049504950-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"-                expected-                actual-            )--parseDatesWithoutSafeRead :: Test-parseDatesWithoutSafeRead =-    let afterParse :: [Day]-        afterParse =-            [ fromGregorian 2020 02 12-            , fromGregorian 2020 02 13-            , fromGregorian 2020 02 14-            , fromGregorian 2020 02 15-            , fromGregorian 2020 02 16-            , fromGregorian 2020 02 17-            , fromGregorian 2020 02 18-            , fromGregorian 2020 02 19-            , fromGregorian 2020 02 20-            , fromGregorian 2020 02 21-            , fromGregorian 2020 02 22-            , fromGregorian 2020 02 23-            , fromGregorian 2020 02 24-            , fromGregorian 2020 02 25-            , fromGregorian 2020 02 26-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates without missing values as BoxedColumn of Days"-                expected-                actual-            )--parseTextsWithoutSafeRead :: Test-parseTextsWithoutSafeRead =-    let afterParse :: [T.Text]-        afterParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , "Surrender now or prepare to fight!"-            , "Meowth, that's right!"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , "Surrender now or prepare to fight!"-            , "Meowth, that's right!"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Text without missing values as BoxedColumn of Text"-                expected-                actual-            )--parseBoolsAndEmptyStringsWithoutSafeRead :: Test-parseBoolsAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe Bool]-        afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)-        beforeParse :: [T.Text]-        beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"-                expected-                actual-            )--parseIntsAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe Int]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1-            , Just 2-            , Just 3-            , Just 4-            , Just 5-            , Just 6-            , Just 7-            , Just 8-            , Just 9-            , Just 10-            , Just 11-            , Just 12-            , Just 13-            , Just 14-            , Just 15-            , Just 16-            , Just 17-            , Just 18-            , Just 19-            , Just 20-            , Just 21-            , Just 22-            , Just 23-            , Just 24-            , Just 25-            , Just 26-            , Just 27-            , Just 28-            , Just 29-            , Just 30-            , Just 31-            , Just 32-            , Just 33-            , Just 34-            , Just 35-            , Just 36-            , Just 37-            , Just 38-            , Just 39-            , Just 40-            , Just 41-            , Just 42-            , Just 43-            , Just 44-            , Just 45-            , Just 46-            , Just 47-            , Just 48-            , Just 49-            , Just 50-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , "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"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Nothing-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Nothing-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Nothing-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Nothing-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Nothing-            , Just 3.14-            , Just 2.22-            , Just 8.55-            , Just 23.3-            , Just 12.22222235049451-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , ""-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , ""-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , ""-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , ""-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , ""-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"-                expected-                actual-            )--parseDatesAndEmptyStringsWithoutSafeRead :: Test-parseDatesAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe Day]-        afterParse =-            [ Just $ fromGregorian 2020 02 12-            , Just $ fromGregorian 2020 02 13-            , Just $ fromGregorian 2020 02 14-            , Nothing-            , Just $ fromGregorian 2020 02 15-            , Just $ fromGregorian 2020 02 16-            , Just $ fromGregorian 2020 02 17-            , Nothing-            , Just $ fromGregorian 2020 02 18-            , Just $ fromGregorian 2020 02 19-            , Just $ fromGregorian 2020 02 20-            , Nothing-            , Just $ fromGregorian 2020 02 21-            , Just $ fromGregorian 2020 02 22-            , Just $ fromGregorian 2020 02 23-            , Nothing-            , Just $ fromGregorian 2020 02 24-            , Just $ fromGregorian 2020 02 25-            , Just $ fromGregorian 2020 02 26-            , Nothing-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , ""-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , ""-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , ""-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , ""-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            , ""-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"-                expected-                actual-            )--parseTextsAndEmptyStringsWithoutSafeRead :: Test-parseTextsAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Just "To"-            , Just "protect"-            , Just "the"-            , Just "world"-            , Just "from"-            , Just "devastation"-            , Nothing-            , Just "To"-            , Just "unite"-            , Just "all"-            , Just "people"-            , Just "within"-            , Just "our"-            , Just "nation"-            , Nothing-            , Just "To"-            , Just "denounce"-            , Just "the"-            , Just "evils"-            , Just "of"-            , Just "truth"-            , Just "and"-            , Just "love"-            , Nothing-            , Just "To"-            , Just "extend"-            , Just "our"-            , Just "reach"-            , Just "to"-            , Just "the"-            , Just "stars"-            , Just "above"-            , Nothing-            , Just "JESSIE!"-            , Just "JAMES!"-            , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , Nothing-            , Just "Surrender now or prepare to fight!"-            , Nothing-            , Just "Meowth, that's right!"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , ""-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , ""-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , ""-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , ""-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , ""-            , "Surrender now or prepare to fight!"-            , ""-            , "Meowth, that's right!"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"-                expected-                actual-            )--parseBoolsAndNullishStringsWithoutSafeRead :: Test-parseBoolsAndNullishStringsWithoutSafeRead =-    let afterParse :: [T.Text]-        afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"-        beforeParse :: [T.Text]-        beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"-                expected-                actual-            )--parseIntsAndNullishStringsWithoutSafeRead :: Test-parseIntsAndNullishStringsWithoutSafeRead =-    let afterParse :: [T.Text]-        afterParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "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"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "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"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"-                expected-                actual-            )--parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithoutSafeRead =-    let afterParse :: [T.Text]-        afterParse =-            [ "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , "Nothing"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "N/A"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "NULL"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "null"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "NAN"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , "Nothing"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "N/A"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "NULL"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "null"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "NAN"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"-                expected-                actual-            )--parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithoutSafeRead =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Just "N/A"-            , Just "N/A"-            , Just "N/A"-            , Just "N/A"-            , Just "N/A"-            , Nothing-            , Just "1"-            , Just "2"-            , Just "3"-            , Just "4"-            , Just "5"-            , Just "6"-            , Just "7"-            , Just "8"-            , Just "9"-            , Just "10"-            , Nothing-            , Just "11"-            , Just "12"-            , Just "13"-            , Just "14"-            , Just "15"-            , Just "16"-            , Just "17"-            , Just "18"-            , Just "19"-            , Just "20"-            , Nothing-            , Just "21"-            , Just "22"-            , Just "23"-            , Just "24"-            , Just "25"-            , Just "26"-            , Just "27"-            , Just "28"-            , Just "29"-            , Just "30"-            , Nothing-            , Just "31"-            , Just "32"-            , Just "33"-            , Just "34"-            , Just "35"-            , Just "36"-            , Just "37"-            , Just "38"-            , Just "39"-            , Just "40"-            , Nothing-            , Just "41"-            , Just "42"-            , Just "43"-            , Just "44"-            , Just "45"-            , Just "46"-            , Just "47"-            , Just "48"-            , Just "49"-            , Just "50"-            , Nothing-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , ""-            , "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"-            , ""-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"-                expected-                actual-            )--parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithoutSafeRead =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Just "To"-            , Just "protect"-            , Just "the"-            , Just "world"-            , Just "from"-            , Just "devastation"-            , Nothing-            , Just "To"-            , Just "unite"-            , Just "all"-            , Just "people"-            , Just "within"-            , Just "our"-            , Just "nation"-            , Nothing-            , Just "To"-            , Just "denounce"-            , Just "the"-            , Just "evils"-            , Just "of"-            , Just "truth"-            , Just "and"-            , Just "love"-            , Nothing-            , Just "To"-            , Just "extend"-            , Just "our"-            , Just "reach"-            , Just "to"-            , Just "the"-            , Just "stars"-            , Just "above"-            , Nothing-            , Just "JESSIE!"-            , Just "JAMES!"-            , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , Nothing-            , Just "Surrender now or prepare to fight!"-            , Nothing-            , Just "Meowth, that's right!"-            , Just "NaN"-            , Just "Nothing"-            , Just "N/A"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , ""-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , ""-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , ""-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , ""-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , ""-            , "Surrender now or prepare to fight!"-            , ""-            , "Meowth, that's right!"-            , "NaN"-            , "Nothing"-            , "N/A"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"-                expected-                actual-            )---- 3B. PARSING WITH SAFEREAD ON-parseBoolsAndEmptyStringsWithSafeRead :: Test-parseBoolsAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Bool]-        afterParse = replicate 10 Nothing ++ replicate 10 (Just True)-        beforeParse :: [T.Text]-        beforeParse = replicate 10 "" ++ replicate 10 "true"-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"-                expected-                actual-            )--parseIntsAndEmptyStringsWithSafeRead :: Test-parseIntsAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Int]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1-            , Just 2-            , Just 3-            , Just 4-            , Just 5-            , Just 6-            , Just 7-            , Just 8-            , Just 9-            , Just 10-            , Just 11-            , Just 12-            , Just 13-            , Just 14-            , Just 15-            , Just 16-            , Just 17-            , Just 18-            , Just 19-            , Just 20-            , Just 21-            , Just 22-            , Just 23-            , Just 24-            , Just 25-            , Just 26-            , Just 27-            , Just 28-            , Just 29-            , Just 30-            , Just 31-            , Just 32-            , Just 33-            , Just 34-            , Just 35-            , Just 36-            , Just 37-            , Just 38-            , Just 39-            , Just 40-            , Just 41-            , Just 42-            , Just 43-            , Just 44-            , Just 45-            , Just 46-            , Just 47-            , Just 48-            , Just 49-            , Just 50-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , "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"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Nothing-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Nothing-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Nothing-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Nothing-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Nothing-            , Just 3.14-            , Just 2.22-            , Just 8.55-            , Just 23.3-            , Just 12.22222235049451-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , ""-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , ""-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , ""-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , ""-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , ""-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"-                expected-                actual-            )--parseDatesAndEmptyStringsWithSafeRead :: Test-parseDatesAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Day]-        afterParse =-            [ Just $ fromGregorian 2020 02 12-            , Just $ fromGregorian 2020 02 13-            , Just $ fromGregorian 2020 02 14-            , Nothing-            , Just $ fromGregorian 2020 02 15-            , Just $ fromGregorian 2020 02 16-            , Just $ fromGregorian 2020 02 17-            , Nothing-            , Just $ fromGregorian 2020 02 18-            , Just $ fromGregorian 2020 02 19-            , Just $ fromGregorian 2020 02 20-            , Nothing-            , Just $ fromGregorian 2020 02 21-            , Just $ fromGregorian 2020 02 22-            , Just $ fromGregorian 2020 02 23-            , Nothing-            , Just $ fromGregorian 2020 02 24-            , Just $ fromGregorian 2020 02 25-            , Just $ fromGregorian 2020 02 26-            , Nothing-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , ""-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , ""-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , ""-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , ""-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            , ""-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"-                expected-                actual-            )--parseTextsAndEmptyStringsWithSafeRead :: Test-parseTextsAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Just "To"-            , Just "protect"-            , Just "the"-            , Just "world"-            , Just "from"-            , Just "devastation"-            , Nothing-            , Just "To"-            , Just "unite"-            , Just "all"-            , Just "people"-            , Just "within"-            , Just "our"-            , Just "nation"-            , Nothing-            , Just "To"-            , Just "denounce"-            , Just "the"-            , Just "evils"-            , Just "of"-            , Just "truth"-            , Just "and"-            , Just "love"-            , Nothing-            , Just "To"-            , Just "extend"-            , Just "our"-            , Just "reach"-            , Just "to"-            , Just "the"-            , Just "stars"-            , Just "above"-            , Nothing-            , Just "JESSIE!"-            , Just "JAMES!"-            , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , Nothing-            , Just "Surrender now or prepare to fight!"-            , Nothing-            , Just "Meowth, that's right!"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , ""-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , ""-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , ""-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , ""-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , ""-            , "Surrender now or prepare to fight!"-            , ""-            , "Meowth, that's right!"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"-                expected-                actual-            )--parseIntsAndNullishStringsWithSafeRead :: Test-parseIntsAndNullishStringsWithSafeRead =-    let afterParse :: [Maybe Int]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1-            , Just 2-            , Just 3-            , Just 4-            , Just 5-            , Just 6-            , Just 7-            , Just 8-            , Just 9-            , Just 10-            , Just 11-            , Just 12-            , Just 13-            , Just 14-            , Just 15-            , Just 16-            , Just 17-            , Just 18-            , Just 19-            , Just 20-            , Just 21-            , Just 22-            , Just 23-            , Just 24-            , Just 25-            , Just 26-            , Just 27-            , Just 28-            , Just 29-            , Just 30-            , Just 31-            , Just 32-            , Just 33-            , Just 34-            , Just 35-            , Just 36-            , Just 37-            , Just 38-            , Just 39-            , Just 40-            , Just 41-            , Just 42-            , Just 43-            , Just 44-            , Just 45-            , Just 46-            , Just 47-            , Just 48-            , Just 49-            , Just 50-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "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"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"-                expected-                actual-            )--parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithSafeRead =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Nothing-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Nothing-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Nothing-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Nothing-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Nothing-            , Just 3.14-            , Just 2.22-            , Just 8.55-            , Just 23.3-            , Just 12.03-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , "Nothing"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "N/A"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "NULL"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "null"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "NAN"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.03"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"-                expected-                actual-            )--parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Int]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1-            , Just 2-            , Just 3-            , Just 4-            , Just 5-            , Just 6-            , Just 7-            , Just 8-            , Just 9-            , Just 10-            , Nothing-            , Just 11-            , Just 12-            , Just 13-            , Just 14-            , Just 15-            , Just 16-            , Just 17-            , Just 18-            , Just 19-            , Just 20-            , Nothing-            , Just 21-            , Just 22-            , Just 23-            , Just 24-            , Just 25-            , Just 26-            , Just 27-            , Just 28-            , Just 29-            , Just 30-            , Nothing-            , Just 31-            , Just 32-            , Just 33-            , Just 34-            , Just 35-            , Just 36-            , Just 37-            , Just 38-            , Just 39-            , Just 40-            , Nothing-            , Just 41-            , Just 42-            , Just 43-            , Just 44-            , Just 45-            , Just 46-            , Just 47-            , Just 48-            , Just 49-            , Just 50-            , Nothing-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "Nothing"-            , ""-            , "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"-            , ""-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"-                expected-                actual-            )--parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1-            , Just 2-            , Just 3-            , Just 4-            , Just 5-            , Just 6-            , Just 7-            , Just 8-            , Just 9-            , Just 10-            , Nothing-            , Just 11-            , Just 12-            , Just 13-            , Just 14-            , Just 15-            , Just 16-            , Just 17-            , Just 18-            , Just 19-            , Just 20-            , Nothing-            , Just 21-            , Just 22-            , Just 23-            , Just 24-            , Just 25-            , Just 26-            , Just 27-            , Just 28-            , Just 29-            , Just 30-            , Nothing-            , Just 3.14-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "N/A"-            , "N/A"-            , "N/A"-            , "N/A"-            , "Nothing"-            , ""-            , "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"-            , ""-            , "3.14"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"-                expected-                actual-            )--parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithSafeRead =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Just "To"-            , Just "protect"-            , Just "the"-            , Just "world"-            , Just "from"-            , Just "devastation"-            , Nothing-            , Just "To"-            , Just "unite"-            , Just "all"-            , Just "people"-            , Just "within"-            , Just "our"-            , Just "nation"-            , Nothing-            , Just "To"-            , Just "denounce"-            , Just "the"-            , Just "evils"-            , Just "of"-            , Just "truth"-            , Just "and"-            , Just "love"-            , Nothing-            , Just "To"-            , Just "extend"-            , Just "our"-            , Just "reach"-            , Just "to"-            , Just "the"-            , Just "stars"-            , Just "above"-            , Nothing-            , Just "JESSIE!"-            , Just "JAMES!"-            , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , Nothing-            , Just "Surrender now or prepare to fight!"-            , Nothing-            , Just "Meowth, that's right!"-            , Nothing-            , Nothing-            , Nothing-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , "To"-            , "protect"-            , "the"-            , "world"-            , "from"-            , "devastation"-            , ""-            , "To"-            , "unite"-            , "all"-            , "people"-            , "within"-            , "our"-            , "nation"-            , ""-            , "To"-            , "denounce"-            , "the"-            , "evils"-            , "of"-            , "truth"-            , "and"-            , "love"-            , ""-            , "To"-            , "extend"-            , "our"-            , "reach"-            , "to"-            , "the"-            , "stars"-            , "above"-            , ""-            , "JESSIE!"-            , "JAMES!"-            , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"-            , ""-            , "Surrender now or prepare to fight!"-            , ""-            , "Meowth, that's right!"-            , "NaN"-            , "Nothing"-            , "N/A"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"-                expected-                actual-            )---- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.-parseBoolsWithOneExample :: Test-parseBoolsWithOneExample =-    let afterParse :: [Bool]-        afterParse = False : replicate 50 True-        beforeParse :: [T.Text]-        beforeParse = "false" : replicate 50 "true"-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"-                expected-                actual-            )--parseBoolsWithManyExamples :: Test-parseBoolsWithManyExamples =-    let afterParse :: [Bool]-        afterParse = False : replicate 50 True-        beforeParse :: [T.Text]-        beforeParse = "false" : replicate 50 "true"-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"-                expected-                actual-            )--parseIntsWithOneExample :: Test-parseIntsWithOneExample =-    let afterParse :: [Int]-        afterParse =-            [ 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-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"-                expected-                actual-            )--parseIntsWithTwentyFiveExamples :: Test-parseIntsWithTwentyFiveExamples =-    let afterParse :: [Int]-        afterParse =-            [ 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-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 25}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"-                expected-                actual-            )--parseIntsWithFortyNineExamples :: Test-parseIntsWithFortyNineExamples =-    let afterParse :: [Int]-        afterParse =-            [ 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-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"-                expected-                actual-            )--parseDatesWithOneExample :: Test-parseDatesWithOneExample =-    let afterParse :: [Day]-        afterParse =-            [ fromGregorian 2020 02 12-            , fromGregorian 2020 02 13-            , fromGregorian 2020 02 14-            , fromGregorian 2020 02 15-            , fromGregorian 2020 02 16-            , fromGregorian 2020 02 17-            , fromGregorian 2020 02 18-            , fromGregorian 2020 02 19-            , fromGregorian 2020 02 20-            , fromGregorian 2020 02 21-            , fromGregorian 2020 02 22-            , fromGregorian 2020 02 23-            , fromGregorian 2020 02 24-            , fromGregorian 2020 02 25-            , fromGregorian 2020 02 26-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"-                expected-                actual-            )--parseDatesWithFifteenExamples :: Test-parseDatesWithFifteenExamples =-    let afterParse :: [Day]-        afterParse =-            [ fromGregorian 2020 02 12-            , fromGregorian 2020 02 13-            , fromGregorian 2020 02 14-            , fromGregorian 2020 02 15-            , fromGregorian 2020 02 16-            , fromGregorian 2020 02 17-            , fromGregorian 2020 02 18-            , fromGregorian 2020 02 19-            , fromGregorian 2020 02 20-            , fromGregorian 2020 02 21-            , fromGregorian 2020 02 22-            , fromGregorian 2020 02 23-            , fromGregorian 2020 02 24-            , fromGregorian 2020 02 25-            , fromGregorian 2020 02 26-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "2020-02-12"-            , "2020-02-13"-            , "2020-02-14"-            , "2020-02-15"-            , "2020-02-16"-            , "2020-02-17"-            , "2020-02-18"-            , "2020-02-19"-            , "2020-02-20"-            , "2020-02-21"-            , "2020-02-22"-            , "2020-02-23"-            , "2020-02-24"-            , "2020-02-25"-            , "2020-02-26"-            ]-        expected = DI.BoxedColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 15}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"-                expected-                actual-            )--parseIntsAndDoublesAsDoublesWithOneExample :: Test-parseIntsAndDoublesAsDoublesWithOneExample =-    let afterParse :: [Double]-        afterParse =-            [ 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-            , 1.0-            , 2.0-            , 3.0-            , 4.0-            , 5.0-            , 6.0-            , 7.0-            , 8.0-            , 9.0-            , 10.0-            , 11.0-            , 12.0-            , 13.0-            , 14.0-            , 15.0-            , 16.0-            , 17.0-            , 18.0-            , 19.0-            , 20.0-            , 21.0-            , 22.0-            , 23.0-            , 24.0-            , 25.0-            , 26.0-            , 27.0-            , 28.0-            , 29.0-            , 30.0-            , 31.0-            , 32.0-            , 33.0-            , 34.0-            , 35.0-            , 36.0-            , 37.0-            , 38.0-            , 39.0-            , 40.0-            , 41.0-            , 42.0-            , 43.0-            , 44.0-            , 45.0-            , 46.0-            , 47.0-            , 48.0-            , 49.0-            , 50.0-            , 3.14-            , 2.22-            , 8.55-            , 23.3-            , 12.22222235049450945049504950-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"-                expected-                actual-            )--parseIntsAndDoublesAsDoublesWithManyExamples :: Test-parseIntsAndDoublesAsDoublesWithManyExamples =-    let afterParse :: [Double]-        afterParse =-            [ 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-            , 1.0-            , 2.0-            , 3.0-            , 4.0-            , 5.0-            , 6.0-            , 7.0-            , 8.0-            , 9.0-            , 10.0-            , 11.0-            , 12.0-            , 13.0-            , 14.0-            , 15.0-            , 16.0-            , 17.0-            , 18.0-            , 19.0-            , 20.0-            , 21.0-            , 22.0-            , 23.0-            , 24.0-            , 25.0-            , 26.0-            , 27.0-            , 28.0-            , 29.0-            , 30.0-            , 31.0-            , 32.0-            , 33.0-            , 34.0-            , 35.0-            , 36.0-            , 37.0-            , 38.0-            , 39.0-            , 40.0-            , 41.0-            , 42.0-            , 43.0-            , 44.0-            , 45.0-            , 46.0-            , 47.0-            , 48.0-            , 49.0-            , 50.0-            , 3.14-            , 2.22-            , 8.55-            , 23.3-            , 12.22222235049450945049504950-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ "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"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 50}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Just 3.14-            , Just 2.22-            , Just 8.55-            , Just 23.3-            , Just 12.22222235049451-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , "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"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::-    Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =-    let afterParse :: [Maybe Double]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Just 1.0-            , Just 2.0-            , Just 3.0-            , Just 4.0-            , Just 5.0-            , Just 6.0-            , Just 7.0-            , Just 8.0-            , Just 9.0-            , Just 10.0-            , Just 11.0-            , Just 12.0-            , Just 13.0-            , Just 14.0-            , Just 15.0-            , Just 16.0-            , Just 17.0-            , Just 18.0-            , Just 19.0-            , Just 20.0-            , Just 21.0-            , Just 22.0-            , Just 23.0-            , Just 24.0-            , Just 25.0-            , Just 26.0-            , Just 27.0-            , Just 28.0-            , Just 29.0-            , Just 30.0-            , Just 31.0-            , Just 32.0-            , Just 33.0-            , Just 34.0-            , Just 35.0-            , Just 36.0-            , Just 37.0-            , Just 38.0-            , Just 39.0-            , Just 40.0-            , Just 41.0-            , Just 42.0-            , Just 43.0-            , Just 44.0-            , Just 45.0-            , Just 46.0-            , Just 47.0-            , Just 48.0-            , Just 49.0-            , Just 50.0-            , Just 3.14-            , Just 2.22-            , Just 8.55-            , Just 23.3-            , Just 12.22222235049451-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , "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"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "31.0"-            , "32.0"-            , "33.0"-            , "34.0"-            , "35.0"-            , "36.0"-            , "37.0"-            , "38.0"-            , "39.0"-            , "40.0"-            , "41.0"-            , "42.0"-            , "43.0"-            , "44.0"-            , "45.0"-            , "46.0"-            , "47.0"-            , "48.0"-            , "49.0"-            , "50.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::-    Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "1"-            , Just "2"-            , Just "3"-            , Just "4"-            , Just "5"-            , Just "6"-            , Just "7"-            , Just "8"-            , Just "9"-            , Just "10"-            , Just "11"-            , Just "12"-            , Just "13"-            , Just "14"-            , Just "15"-            , Just "16"-            , Just "17"-            , Just "18"-            , Just "19"-            , Just "20"-            , Just "1.0"-            , Just "2.0"-            , Just "3.0"-            , Just "4.0"-            , Just "5.0"-            , Just "6.0"-            , Just "7.0"-            , Just "8.0"-            , Just "9.0"-            , Just "10.0"-            , Just "11.0"-            , Just "12.0"-            , Just "13.0"-            , Just "14.0"-            , Just "15.0"-            , Just "16.0"-            , Just "17.0"-            , Just "18.0"-            , Just "19.0"-            , Just "20.0"-            , Just "21.0"-            , Just "22.0"-            , Just "23.0"-            , Just "24.0"-            , Just "25.0"-            , Just "26.0"-            , Just "27.0"-            , Just "28.0"-            , Just "29.0"-            , Just "30.0"-            , Just "3.14"-            , Just "2.22"-            , Just "8.55"-            , Just "23.3"-            , Just "12.22222235049451"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , "11"-            , "12"-            , "13"-            , "14"-            , "15"-            , "16"-            , "17"-            , "18"-            , "19"-            , "20"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"-                expected-                actual-            )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::-    Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =-    let afterParse :: [Maybe T.Text]-        afterParse =-            [ Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Nothing-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "NaN"-            , Just "N/A"-            , Just "1"-            , Just "2"-            , Just "3"-            , Just "4"-            , Just "5"-            , Just "6"-            , Just "7"-            , Just "8"-            , Just "9"-            , Just "10"-            , Just "11"-            , Just "12"-            , Just "13"-            , Just "14"-            , Just "15"-            , Just "16"-            , Just "17"-            , Just "18"-            , Just "19"-            , Just "20"-            , Just "1.0"-            , Just "2.0"-            , Just "3.0"-            , Just "4.0"-            , Just "5.0"-            , Just "6.0"-            , Just "7.0"-            , Just "8.0"-            , Just "9.0"-            , Just "10.0"-            , Just "11.0"-            , Just "12.0"-            , Just "13.0"-            , Just "14.0"-            , Just "15.0"-            , Just "16.0"-            , Just "17.0"-            , Just "18.0"-            , Just "19.0"-            , Just "20.0"-            , Just "21.0"-            , Just "22.0"-            , Just "23.0"-            , Just "24.0"-            , Just "25.0"-            , Just "26.0"-            , Just "27.0"-            , Just "28.0"-            , Just "29.0"-            , Just "30.0"-            , Just "3.14"-            , Just "2.22"-            , Just "8.55"-            , Just "23.3"-            , Just "12.22222235049451"-            ]-        beforeParse :: [T.Text]-        beforeParse =-            [ ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , ""-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "NaN"-            , "N/A"-            , "1"-            , "2"-            , "3"-            , "4"-            , "5"-            , "6"-            , "7"-            , "8"-            , "9"-            , "10"-            , "11"-            , "12"-            , "13"-            , "14"-            , "15"-            , "16"-            , "17"-            , "18"-            , "19"-            , "20"-            , "1.0"-            , "2.0"-            , "3.0"-            , "4.0"-            , "5.0"-            , "6.0"-            , "7.0"-            , "8.0"-            , "9.0"-            , "10.0"-            , "11.0"-            , "12.0"-            , "13.0"-            , "14.0"-            , "15.0"-            , "16.0"-            , "17.0"-            , "18.0"-            , "19.0"-            , "20.0"-            , "21.0"-            , "22.0"-            , "23.0"-            , "24.0"-            , "25.0"-            , "26.0"-            , "27.0"-            , "28.0"-            , "29.0"-            , "30.0"-            , "3.14"-            , "2.22"-            , "8.55"-            , "23.3"-            , "12.22222235049451"-            ]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault-                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})-                $ DI.fromVector-                $ V.fromList beforeParse-     in TestCase-            ( assertEqual-                "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"-                expected-                actual-            )---- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY--parseManyNullishAndOneInt :: Test-parseManyNullishAndOneInt =-    let afterParse :: [Maybe Int]-        afterParse = replicate 100 Nothing ++ [Just 100000]-        beforeParse :: [T.Text]-        beforeParse = replicate 100 "NaN" ++ ["100000"]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDouble :: Test-parseManyNullishAndOneDouble =-    let afterParse :: [Maybe Double]-        afterParse = replicate 100 Nothing ++ [Just 3.14]-        beforeParse :: [T.Text]-        beforeParse = replicate 100 "NaN" ++ ["3.14"]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDate :: Test-parseManyNullishAndOneDate =-    let afterParse :: [Maybe Day]-        afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]-        beforeParse :: [T.Text]-        beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndIncorrectDates :: Test-parseManyNullishAndIncorrectDates =-    let afterParse :: [Maybe T.Text]-        afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]-        beforeParse :: [T.Text]-        beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]-        expected = DI.OptionalColumn $ V.fromList afterParse-        actual =-            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $-                DI.fromVector $-                    V.fromList beforeParse-     in TestCase-            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseRepeatedNullish :: Test-parseRepeatedNullish =-    let afterParse :: [Maybe T.Text]-        afterParse = replicate 100 Nothing-        beforeParse :: [T.Text]-        beforeParse = replicate 100 "NaN"-        expected = DI.OptionalColumn $ V.fromList afterParse+import qualified Data.Text.IO as TIO+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Operations.Typing as D++import Data.Time (Day, fromGregorian)+import Test.HUnit (Test (TestCase, TestLabel), assertEqual)++testData :: D.DataFrame+testData =+    D.fromNamedColumns+        [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+        , ("test2", DI.fromList ['a' .. 'z'])+        ]++-- Dimensions+correctDimensions :: Test+correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))++emptyDataframeDimensions :: Test+emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))++dimensionsTest :: [Test]+dimensionsTest =+    [ TestLabel "dimensions_correctDimensions" correctDimensions+    , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions+    ]++-- Shared data directory+typingDataDir :: FilePath+typingDataDir = "./tests/data/typing/"++-- Shared input/expected data+intsInput :: [T.Text]+intsInput = T.pack . show <$> ([1 .. 50] :: [Int])++intsExpected :: [Int]+intsExpected = [1 .. 50]++doublesSpecial :: [Double]+doublesSpecial = [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]++doublesSpecialInput :: [T.Text]+doublesSpecialInput = ["3.14", "2.22", "8.55", "23.3", "12.22222235049451"]++doublesExpected :: [Double]+doublesExpected = [1.0 .. 50.0] ++ doublesSpecial++doublesInput :: [T.Text]+doublesInput = (T.pack . show <$> ([1.0 .. 50.0] :: [Double])) ++ doublesSpecialInput++intsAndDoublesExpected :: [Double]+intsAndDoublesExpected = ([1 .. 50] :: [Double]) ++ doublesExpected++intsAndDoublesInput :: [T.Text]+intsAndDoublesInput = intsInput ++ doublesInput++datesExpected :: [Day]+datesExpected = [fromGregorian 2020 02 12 .. fromGregorian 2020 02 26]++datesInput :: [T.Text]+datesInput = T.pack . show <$> datesExpected++-- PARSING TESTS+------- 1. SIMPLE CASES+parseBools :: Test+parseBools =+    let afterParse :: [Bool]+        afterParse = [True, True, True] ++ [False, False, False]+        beforeParse :: [T.Text]+        beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools"+                expected+                actual+            )++parseInts :: Test+parseInts =+    let afterParse :: [Int]+        afterParse = [1 .. 50]+        beforeParse :: [T.Text]+        beforeParse = T.pack . show <$> [1 .. 50]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints"+                expected+                actual+            )++parseDoubles :: Test+parseDoubles =+    let afterParse :: [Double]+        afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]+        beforeParse :: [T.Text]+        beforeParse =+            T.pack . show+                <$> [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Doubles without missing values as OptionalColumn of Maybe Doubles"+                expected+                actual+            )++parseDates :: Test+parseDates =+    let afterParse = datesExpected+        beforeParse = datesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates without missing values as OptionalColumn of Maybe Days"+                expected+                actual+            )++parseTexts :: Test+parseTexts = TestCase $ do+    texts <- T.lines <$> TIO.readFile (typingDataDir <> "texts.txt")+    let expected = DI.fromVector $ V.fromList texts+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList texts+    assertEqual+        "Correctly parses Text without missing values as OptionalColumn of Maybe Text"+        expected+        actual++--- 2. COMBINATION CASES+parseBoolsAndIntsAsTexts :: Test+parseBoolsAndIntsAsTexts =+    let afterParse :: [T.Text]+        afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+        beforeParse :: [T.Text]+        beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses mixture of Bools and Ints as Maybe Text"+                expected+                actual+            )++parseIntsAndDoublesAsDoubles :: Test+parseIntsAndDoublesAsDoubles =+    let afterParse = intsAndDoublesExpected+        beforeParse = intsAndDoublesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles"+                expected+                actual+            )++parseIntsAndDatesAsTexts :: Test+parseIntsAndDatesAsTexts =+    let data30 = T.pack . show <$> ([1 .. 30] :: [Int])+        data9dates = take 9 datesInput+        afterParse :: [T.Text]+        afterParse = data30 ++ data9dates+        beforeParse :: [T.Text]+        beforeParse = data30 ++ data9dates+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Dates as OptionalColumn of Maybe Texts"+                expected+                actual+            )++parseTextsAndDoublesAsTexts :: Test+parseTextsAndDoublesAsTexts =+    let shortTexts = ["To", "protect", "the", "world", "from", "devastation"]+        afterParse :: [T.Text]+        afterParse = shortTexts ++ doublesInput+        beforeParse :: [T.Text]+        beforeParse = shortTexts ++ doublesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Texts and Doubles as OptionalColumn of Maybe Texts"+                expected+                actual+            )++parseDatesAndTextsAsTexts :: Test+parseDatesAndTextsAsTexts =+    let extra = ["Jessie", "James", "Meowth"]+        afterParse :: [T.Text]+        afterParse = datesInput ++ extra+        beforeParse :: [T.Text]+        beforeParse = datesInput ++ extra+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Dates and Texts as OptionalColumn of Maybe Texts"+                expected+                actual+            )++-- 3A. PARSING WITH SAFEREAD OFF++parseBoolsWithoutSafeRead :: Test+parseBoolsWithoutSafeRead =+    let afterParse :: [Bool]+        afterParse = replicate 10 True ++ replicate 10 False+        beforeParse :: [T.Text]+        beforeParse = replicate 10 "true" ++ replicate 10 "false"+        expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"+                expected+                actual+            )++parseIntsWithoutSafeRead :: Test+parseIntsWithoutSafeRead =+    let afterParse = intsExpected+        beforeParse = intsInput+        expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"+                expected+                actual+            )++parseDoublesWithoutSafeRead :: Test+parseDoublesWithoutSafeRead =+    let afterParse = doublesExpected+        beforeParse = doublesInput+        expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"+                expected+                actual+            )++parseDatesWithoutSafeRead :: Test+parseDatesWithoutSafeRead =+    let afterParse = datesExpected+        beforeParse = datesInput+        expected = DI.BoxedColumn Nothing $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates without missing values as BoxedColumn of Days"+                expected+                actual+            )++parseTextsWithoutSafeRead :: Test+parseTextsWithoutSafeRead = TestCase $ do+    texts <- T.lines <$> TIO.readFile (typingDataDir <> "texts.txt")+    let expected = DI.BoxedColumn Nothing $ V.fromList texts+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList texts+    assertEqual+        "Correctly parses Text without missing values as BoxedColumn of Text"+        expected+        actual++parseBoolsAndEmptyStringsWithoutSafeRead :: Test+parseBoolsAndEmptyStringsWithoutSafeRead =+    let afterParse :: [Maybe Bool]+        afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)+        beforeParse :: [T.Text]+        beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"+                expected+                actual+            )++parseIntsAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndEmptyStringsWithoutSafeRead =+    let beforeParse = replicate 5 "" ++ intsInput+        afterParse :: [Maybe Int]+        afterParse = replicate 5 Nothing ++ map Just intsExpected+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =+    let intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+        dblGrps =+            [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+            , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+            , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+            , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+            ]+        beforeParse = intGrp ++ [""] ++ concatMap (\g -> g ++ [""]) dblGrps ++ doublesSpecialInput+        afterParse :: [Maybe Double]+        afterParse =+            map Just ([1.0 .. 10.0] :: [Double])+                ++ [Nothing]+                ++ concatMap+                    (\g -> map Just g ++ [Nothing])+                    [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+                ++ map Just doublesSpecial+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+                expected+                actual+            )++parseDatesAndEmptyStringsWithoutSafeRead :: Test+parseDatesAndEmptyStringsWithoutSafeRead =+    -- Pattern: 3 dates, empty, 3 dates, empty, 3 dates, empty, 3 dates, empty, 3 dates, empty+    let groups = chunksOf3 datesExpected+        beforeParse = concatMap (\g -> map (T.pack . show) g ++ [""]) groups+        afterParse :: [Maybe Day]+        afterParse = concatMap (\g -> map Just g ++ [Nothing]) groups+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"+                expected+                actual+            )+  where+    chunksOf3 [] = []+    chunksOf3 xs = take 3 xs : chunksOf3 (drop 3 xs)++parseTextsAndEmptyStringsWithoutSafeRead :: Test+parseTextsAndEmptyStringsWithoutSafeRead = TestCase $ do+    raw <- T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties.txt")+    let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList raw+    assertEqual+        "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+        expected+        actual++parseBoolsAndNullishStringsWithoutSafeRead :: Test+parseBoolsAndNullishStringsWithoutSafeRead =+    let afterParse :: [T.Text]+        afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+        beforeParse :: [T.Text]+        beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+        expected = DI.BoxedColumn Nothing $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"+                expected+                actual+            )++parseIntsAndNullishStringsWithoutSafeRead :: Test+parseIntsAndNullishStringsWithoutSafeRead =+    let beforeParse = replicate 5 "N/A" ++ intsInput+        afterParse :: [T.Text]+        afterParse = beforeParse+        expected = DI.BoxedColumn Nothing $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"+                expected+                actual+            )++parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithoutSafeRead =+    -- Nullish separators are literal text strings (not interpreted as null when safeRead=False)+    let nullishSeps = ["Nothing", "N/A", "NULL", "null", "NAN"]+        intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+        dblGrps =+            [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+            , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+            , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+            , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+            ]+        beforeParse =+            intGrp+                ++ concat (zipWith (:) nullishSeps dblGrps)+                ++ doublesSpecialInput+        afterParse :: [T.Text]+        afterParse = beforeParse+        expected = DI.BoxedColumn Nothing $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+                expected+                actual+            )++parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithoutSafeRead =+    -- Pattern: 5x"N/A", then groups of ("" : 10 ints), with trailing ""+    let groups = [[1 .. 10], [11 .. 20], [21 .. 30], [31 .. 40], [41 .. 50]] :: [[Int]]+        beforeParse =+            replicate 5 "N/A"+                ++ concatMap (\g -> "" : map (T.pack . show) g) groups+                ++ [""]+        afterParse :: [Maybe T.Text]+        afterParse =+            replicate 5 (Just "N/A")+                ++ concatMap (\g -> Nothing : map (Just . T.pack . show) g) groups+                ++ [Nothing]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"+                expected+                actual+            )++parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithoutSafeRead = TestCase $ do+    raw <-+        T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties_and_nullish.txt")+    -- safeRead=False: empty strings -> Nothing, nullish text stays as Just+    let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList raw+    assertEqual+        "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+        expected+        actual++-- 3B. PARSING WITH SAFEREAD ON+parseBoolsAndEmptyStringsWithSafeRead :: Test+parseBoolsAndEmptyStringsWithSafeRead =+    let afterParse :: [Maybe Bool]+        afterParse = replicate 10 Nothing ++ replicate 10 (Just True)+        beforeParse :: [T.Text]+        beforeParse = replicate 10 "" ++ replicate 10 "true"+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"+                expected+                actual+            )++parseIntsAndEmptyStringsWithSafeRead :: Test+parseIntsAndEmptyStringsWithSafeRead =+    let beforeParse = replicate 5 "" ++ intsInput+        afterParse :: [Maybe Int]+        afterParse = replicate 5 Nothing ++ map Just intsExpected+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithSafeRead =+    let intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+        dblGrps =+            [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+            , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+            , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+            , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+            ]+        beforeParse = intGrp ++ [""] ++ concatMap (\g -> g ++ [""]) dblGrps ++ doublesSpecialInput+        afterParse :: [Maybe Double]+        afterParse =+            map Just ([1.0 .. 10.0] :: [Double])+                ++ [Nothing]+                ++ concatMap+                    (\g -> map Just g ++ [Nothing])+                    [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+                ++ map Just doublesSpecial+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"+                expected+                actual+            )++parseDatesAndEmptyStringsWithSafeRead :: Test+parseDatesAndEmptyStringsWithSafeRead =+    let groups = chunksOf3 datesExpected+        beforeParse = concatMap (\g -> map (T.pack . show) g ++ [""]) groups+        afterParse :: [Maybe Day]+        afterParse = concatMap (\g -> map Just g ++ [Nothing]) groups+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"+                expected+                actual+            )+  where+    chunksOf3 [] = []+    chunksOf3 xs = take 3 xs : chunksOf3 (drop 3 xs)++parseTextsAndEmptyStringsWithSafeRead :: Test+parseTextsAndEmptyStringsWithSafeRead = TestCase $ do+    raw <- T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties.txt")+    let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList raw+    assertEqual+        "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"+        expected+        actual++parseIntsAndNullishStringsWithSafeRead :: Test+parseIntsAndNullishStringsWithSafeRead =+    let beforeParse = replicate 5 "N/A" ++ intsInput+        afterParse :: [Maybe Int]+        afterParse = replicate 5 Nothing ++ map Just intsExpected+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"+                expected+                actual+            )++parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithSafeRead =+    -- Note: this test uses different special doubles and nullish separators than the safeRead=False version+    let nullishSeps = ["Nothing", "N/A", "NULL", "null", "NAN"]+        intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+        dblGrps =+            [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+            , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+            , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+            , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+            ]+        beforeParse =+            intGrp+                ++ concat (zipWith (:) nullishSeps dblGrps)+                ++ [nullishSeps !! 4]+                ++ ["3.14", "2.22", "8.55", "23.3", "12.03"]+        afterParse :: [Maybe Double]+        afterParse =+            map Just ([1.0 .. 10.0] :: [Double])+                ++ [Nothing]+                ++ concatMap+                    (\g -> map Just g ++ [Nothing])+                    [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+                ++ map Just [3.14, 2.22, 8.55, 23.3, 12.03]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+                expected+                actual+            )++parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithSafeRead =+    -- safeRead=True: N/A, Nothing -> Nothing; "" -> Nothing+    let groups = [[1 .. 10], [11 .. 20], [21 .. 30], [31 .. 40], [41 .. 50]] :: [[Int]]+        beforeParse =+            ["N/A", "N/A", "N/A", "N/A", "Nothing"]+                ++ concatMap (\g -> "" : map (T.pack . show) g) groups+                ++ [""]+        afterParse :: [Maybe Int]+        afterParse =+            replicate 5 Nothing+                ++ concatMap (\g -> Nothing : map Just g) groups+                ++ [Nothing]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"+                expected+                actual+            )++parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =+    -- Data: 4xN/A, "Nothing", "", then 10 ints (1-10), "", 10 ints (11-20), "", 10 ints (21-30), "", 3.14+    let beforeParse =+            ["N/A", "N/A", "N/A", "N/A", "Nothing", ""]+                ++ map (T.pack . show) ([1 .. 10] :: [Int])+                ++ [""]+                ++ map (T.pack . show) ([11 .. 20] :: [Int])+                ++ [""]+                ++ map (T.pack . show) ([21 .. 30] :: [Int])+                ++ ["", "3.14"]+        afterParse :: [Maybe Double]+        afterParse =+            replicate 6 Nothing+                ++ map Just ([1 .. 10] :: [Double])+                ++ [Nothing]+                ++ map Just ([11 .. 20] :: [Double])+                ++ [Nothing]+                ++ map Just ([21 .. 30] :: [Double])+                ++ [Nothing, Just 3.14]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"+                expected+                actual+            )++parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithSafeRead = TestCase $ do+    raw <-+        T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties_and_nullish.txt")+    -- safeRead=True: empty strings and nullish tokens (NaN, Nothing, N/A) -> Nothing+    let isNullish t = t `elem` ["NaN", "Nothing", "N/A", "nan", "null", "NULL", "NA", "na", "NAN"]+        afterParse = map (\t -> if t == "" || isNullish t then Nothing else Just t) raw+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList raw+    assertEqual+        "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+        expected+        actual++-- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.+parseBoolsWithOneExample :: Test+parseBoolsWithOneExample =+    let afterParse :: [Bool]+        afterParse = False : replicate 50 True+        beforeParse :: [T.Text]+        beforeParse = "false" : replicate 50 "true"+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools with only one example"+                expected+                actual+            )++parseBoolsWithManyExamples :: Test+parseBoolsWithManyExamples =+    let afterParse :: [Bool]+        afterParse = False : replicate 50 True+        beforeParse :: [T.Text]+        beforeParse = "false" : replicate 50 "true"+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools with only one example"+                expected+                actual+            )++parseIntsWithOneExample :: Test+parseIntsWithOneExample =+    let afterParse = intsExpected+        beforeParse = intsInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with only one example"+                expected+                actual+            )++parseIntsWithTwentyFiveExamples :: Test+parseIntsWithTwentyFiveExamples =+    let afterParse = intsExpected+        beforeParse = intsInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 25}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with some examples"+                expected+                actual+            )++parseIntsWithFortyNineExamples :: Test+parseIntsWithFortyNineExamples =+    let afterParse = intsExpected+        beforeParse = intsInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with many examples"+                expected+                actual+            )++parseDatesWithOneExample :: Test+parseDatesWithOneExample =+    let afterParse = datesExpected+        beforeParse = datesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates without missing values as OptionalColumn of Maybe Days with only one example"+                expected+                actual+            )++parseDatesWithFifteenExamples :: Test+parseDatesWithFifteenExamples =+    let afterParse = datesExpected+        beforeParse = datesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 15}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses Dates without missing values as OptionalColumn of Maybe Days with many examples"+                expected+                actual+            )++parseIntsAndDoublesAsDoublesWithOneExample :: Test+parseIntsAndDoublesAsDoublesWithOneExample =+    let afterParse = intsAndDoublesExpected+        beforeParse = intsAndDoublesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles with just one example"+                expected+                actual+            )++parseIntsAndDoublesAsDoublesWithManyExamples :: Test+parseIntsAndDoublesAsDoublesWithManyExamples =+    let afterParse = intsAndDoublesExpected+        beforeParse = intsAndDoublesInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 50}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles with many examples"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =+    -- Pattern: 10 empties, 50 ints (as int text), 50 doubles (as double text), 5 specials+    let beforeParse =+            replicate 10 ""+                ++ intsInput+                ++ (T.pack . show <$> ([1.0 .. 50.0] :: [Double]))+                ++ doublesSpecialInput+        afterParse :: [Maybe Double]+        afterParse =+            replicate 10 Nothing+                ++ map Just ([1.0 .. 50.0] :: [Double])+                ++ map Just ([1.0 .. 50.0] :: [Double])+                ++ map Just doublesSpecial+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::+    Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =+    let beforeParse =+            replicate 10 ""+                ++ intsInput+                ++ (T.pack . show <$> ([1.0 .. 50.0] :: [Double]))+                ++ doublesSpecialInput+        afterParse :: [Maybe Double]+        afterParse =+            replicate 10 Nothing+                ++ map Just ([1.0 .. 50.0] :: [Double])+                ++ map Just ([1.0 .. 50.0] :: [Double])+                ++ map Just doublesSpecial+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::+    Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =+    -- Pattern: 10 empties, 5x(NaN,N/A), 20 ints (1-20), 30 doubles (1.0-30.0), 5 specials+    let nullishPairs = concat (replicate 5 ["NaN", "N/A"])+        beforeParse =+            replicate 10 ""+                ++ nullishPairs+                ++ (T.pack . show <$> ([1 .. 20] :: [Int]))+                ++ (T.pack . show <$> ([1.0 .. 30.0] :: [Double]))+                ++ doublesSpecialInput+        afterParse :: [Maybe T.Text]+        afterParse =+            replicate 10 Nothing+                ++ map Just nullishPairs+                ++ map (Just . T.pack . show) ([1 .. 20] :: [Int])+                ++ map (Just . T.pack . show) ([1.0 .. 30.0] :: [Double])+                ++ map Just doublesSpecialInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"+                expected+                actual+            )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::+    Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =+    let nullishPairs = concat (replicate 5 ["NaN", "N/A"])+        beforeParse =+            replicate 10 ""+                ++ nullishPairs+                ++ (T.pack . show <$> ([1 .. 20] :: [Int]))+                ++ (T.pack . show <$> ([1.0 .. 30.0] :: [Double]))+                ++ doublesSpecialInput+        afterParse :: [Maybe T.Text]+        afterParse =+            replicate 10 Nothing+                ++ map Just nullishPairs+                ++ map (Just . T.pack . show) ([1 .. 20] :: [Int])+                ++ map (Just . T.pack . show) ([1.0 .. 30.0] :: [Double])+                ++ map Just doublesSpecialInput+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse+     in TestCase+            ( assertEqual+                "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"+                expected+                actual+            )++-- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY++parseManyNullishAndOneInt :: Test+parseManyNullishAndOneInt =+    let afterParse :: [Maybe Int]+        afterParse = replicate 100 Nothing ++ [Just 100000]+        beforeParse :: [T.Text]+        beforeParse = replicate 100 "NaN" ++ ["100000"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDouble :: Test+parseManyNullishAndOneDouble =+    let afterParse :: [Maybe Double]+        afterParse = replicate 100 Nothing ++ [Just 3.14]+        beforeParse :: [T.Text]+        beforeParse = replicate 100 "NaN" ++ ["3.14"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDate :: Test+parseManyNullishAndOneDate =+    let afterParse :: [Maybe Day]+        afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]+        beforeParse :: [T.Text]+        beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndIncorrectDates :: Test+parseManyNullishAndIncorrectDates =+    let afterParse :: [Maybe T.Text]+        afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]+        beforeParse :: [T.Text]+        beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]+        expected = DI.fromVector $ V.fromList afterParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse+     in TestCase+            (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseRepeatedNullish :: Test+parseRepeatedNullish =+    let afterParse :: [Maybe T.Text]+        afterParse = replicate 100 Nothing+        beforeParse :: [T.Text]+        beforeParse = replicate 100 "NaN"+        expected = DI.fromVector $ V.fromList afterParse         actual =             D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $                 DI.fromVector $
+ tests/Operations/WriteCsv.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.WriteCsv where++import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame (..), toCsv, toSeparated)+import Test.HUnit++-- Basic test: Int and Text columns produce correct CSV+toCsvBasic :: Test+toCsvBasic = TestLabel "toCsv_basic" $ TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("name", DI.fromList @T.Text ["Alice", "Bob", "Charlie"])+                , ("age", DI.fromList @Int [30, 25, 35])+                ]+        expected = "name,age\nAlice,30\nBob,25\nCharlie,35\n"+    assertEqual "basic toCsv" expected (toCsv df)++-- Empty DataFrame produces empty text+toCsvEmpty :: Test+toCsvEmpty =+    TestLabel "toCsv_empty" $+        TestCase $+            assertEqual "empty toCsv" T.empty (toCsv D.empty)++-- toSeparated with tab produces tab-delimited output+toSeparatedTab :: Test+toSeparatedTab = TestLabel "toSeparated_tab" $ TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList @Int [1, 2])+                , ("y", DI.fromList @Int [3, 4])+                ]+        expected = "x\ty\n1\t3\n2\t4\n"+    assertEqual "tab separated" expected (toSeparated '\t' df)++-- Double values render correctly+toCsvDouble :: Test+toCsvDouble = TestLabel "toCsv_double" $ TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("value", DI.fromList @Double [1.5, 2.0, 2.5])+                ]+        expected = "value\n1.5\n2.0\n2.5\n"+    assertEqual "double toCsv" expected (toCsv df)++-- Single column DataFrame+toCsvSingleColumn :: Test+toCsvSingleColumn = TestLabel "toCsv_single_column" $ TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("id", DI.fromList @Int [10, 20, 30])+                ]+        expected = "id\n10\n20\n30\n"+    assertEqual "single column toCsv" expected (toCsv df)++-- Round trip: toCsv then readCsv preserves data+toCsvRoundTrip :: Test+toCsvRoundTrip = TestLabel "toCsv_roundTrip" $ TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("a", DI.fromList @Int [1, 2, 3])+                , ("b", DI.fromList @T.Text ["hello", "world", "test"])+                ]+    let csvText = toCsv df+    let tmpPath = "/tmp/dataframe_test_toCsv_roundtrip.csv"+    TIO.writeFile tmpPath csvText+    df' <- D.readCsv tmpPath+    assertEqual+        "round trip dimensions"+        (dataframeDimensions df)+        (dataframeDimensions df')+    assertEqual "round trip data" df df'++tests :: [Test]+tests =+    [ toCsvBasic+    , toCsvEmpty+    , toSeparatedTab+    , toCsvDouble+    , toCsvSingleColumn+    , toCsvRoundTrip+    ]
tests/Parquet.hs view
@@ -7,13 +7,12 @@ import qualified DataFrame as D import qualified DataFrame.Functions as F import qualified DataFrame.IO.Parquet as DP+import ParquetTestData (allTypes, mtCarsDataset, tinyPagesLast10, transactions)  import qualified Data.ByteString as BS import Data.Int import qualified Data.Set as S-import Data.Text (Text) import qualified Data.Text as T-import Data.Time import Data.Word import DataFrame.IO.Parquet.Thrift (     columnMetaData,@@ -35,46 +34,6 @@ import GHC.IO (unsafePerformIO) import Test.HUnit -allTypes :: D.DataFrame-allTypes =-    D.fromNamedColumns-        [ ("id", D.fromList [4 :: Int32, 5, 6, 7, 2, 3, 0, 1])-        , ("bool_col", D.fromList [True, False, True, False, True, False, True, False])-        , ("tinyint_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])-        , ("smallint_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])-        , ("int_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])-        , ("bigint_col", D.fromList [0 :: Int64, 10, 0, 10, 0, 10, 0, 10])-        , ("float_col", D.fromList [0 :: Float, 1.1, 0, 1.1, 0, 1.1, 0, 1.1])-        , ("double_col", D.fromList [0 :: Double, 10.1, 0, 10.1, 0, 10.1, 0, 10.1])-        ,-            ( "date_string_col"-            , D.fromList-                [ "03/01/09" :: Text-                , "03/01/09"-                , "04/01/09"-                , "04/01/09"-                , "02/01/09"-                , "02/01/09"-                , "01/01/09"-                , "01/01/09"-                ]-            )-        , ("string_col", D.fromList (take 8 (cycle ["0" :: Text, "1"])))-        ,-            ( "timestamp_col"-            , D.fromList-                [ UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 0}-                , UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 60}-                , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 0}-                , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 60}-                , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 0}-                , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 60}-                , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 0}-                , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 60}-                ]-            )-        ]- testBothReadParquetPaths :: ((FilePath -> IO D.DataFrame) -> Test) -> Test testBothReadParquetPaths test =     TestList@@ -102,87 +61,6 @@             )         ) -tinyPagesLast10 :: D.DataFrame-tinyPagesLast10 =-    D.fromNamedColumns-        [ ("id", D.fromList @Int32 (reverse [6174 .. 6183]))-        , ("bool_col", D.fromList @Bool (Prelude.take 10 (cycle [False, True])))-        , ("tinyint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])-        , ("smallint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])-        , ("int_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])-        , ("bigint_col", D.fromList @Int64 [30, 20, 10, 0, 90, 80, 70, 60, 50, 40])-        ,-            ( "float_col"-            , D.fromList @Float [3.3, 2.2, 1.1, 0, 9.9, 8.8, 7.7, 6.6, 5.5, 4.4]-            )-        ,-            ( "date_string_col"-            , D.fromList @Text-                [ "09/11/10"-                , "09/11/10"-                , "09/11/10"-                , "09/11/10"-                , "09/10/10"-                , "09/10/10"-                , "09/10/10"-                , "09/10/10"-                , "09/10/10"-                , "09/10/10"-                ]-            )-        ,-            ( "string_col"-            , D.fromList @Text ["3", "2", "1", "0", "9", "8", "7", "6", "5", "4"]-            )-        ,-            ( "timestamp_col"-            , D.fromList @UTCTime-                [ UTCTime-                    { utctDay = fromGregorian 2010 9 10-                    , utctDayTime = secondsToDiffTime 85384-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 10-                    , utctDayTime = secondsToDiffTime 85324-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 10-                    , utctDayTime = secondsToDiffTime 85264-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 10-                    , utctDayTime = secondsToDiffTime 85204-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 85144-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 85084-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 85024-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 84964-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 84904-                    }-                , UTCTime-                    { utctDay = fromGregorian 2010 9 9-                    , utctDayTime = secondsToDiffTime 84844-                    }-                ]-            )-        , ("year", D.fromList @Int32 (replicate 10 2010))-        , ("month", D.fromList @Int32 (replicate 10 9))-        ]- allTypesTinyPagesLastFew :: Test allTypesTinyPagesLastFew = testBothReadParquetPaths $ \readParquet ->     TestCase@@ -327,167 +205,6 @@             )         ) -transactions :: D.DataFrame-transactions =-    D.fromNamedColumns-        [ ("transaction_id", D.fromList [1 :: Int32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])-        ,-            ( "event_time"-            , D.fromList-                [ UTCTime-                    { utctDay = fromGregorian 2024 1 3-                    , utctDayTime = secondsToDiffTime 29564 + picosecondsToDiffTime 2311000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 3-                    , utctDayTime = secondsToDiffTime 35101 + picosecondsToDiffTime 118900000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 4-                    , utctDayTime = secondsToDiffTime 39802 + picosecondsToDiffTime 774512000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 5-                    , utctDayTime = secondsToDiffTime 53739 + picosecondsToDiffTime 1000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 6-                    , utctDayTime = secondsToDiffTime 8278 + picosecondsToDiffTime 543210000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 6-                    , utctDayTime = secondsToDiffTime 8284 + picosecondsToDiffTime 211000000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 7-                    , utctDayTime = secondsToDiffTime 63000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 8-                    , utctDayTime = secondsToDiffTime 24259 + picosecondsToDiffTime 390000000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 9-                    , utctDayTime = secondsToDiffTime 48067 + picosecondsToDiffTime 812345000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 10-                    , utctDayTime = secondsToDiffTime 82799 + picosecondsToDiffTime 999999000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 11-                    , utctDayTime = secondsToDiffTime 36000 + picosecondsToDiffTime 100000000000-                    }-                , UTCTime-                    { utctDay = fromGregorian 2024 1 12-                    , utctDayTime = secondsToDiffTime 56028 + picosecondsToDiffTime 667891000000-                    }-                ]-            )-        ,-            ( "user_email"-            , D.fromList-                [ "alice@example.com" :: Text-                , "bob@example.com"-                , "carol@example.com"-                , "alice@example.com"-                , "dave@example.com"-                , "dave@example.com"-                , "eve@example.com"-                , "frank@example.com"-                , "grace@example.com"-                , "dave@example.com"-                , "alice@example.com"-                , "heidi@example.com"-                ]-            )-        ,-            ( "transaction_type"-            , D.fromList-                [ "purchase" :: Text-                , "purchase"-                , "refund"-                , "purchase"-                , "purchase"-                , "purchase"-                , "purchase"-                , "withdrawal"-                , "purchase"-                , "purchase"-                , "purchase"-                , "refund"-                ]-            )-        ,-            ( "amount"-            , D.fromList-                [ 142.50 :: Double-                , 29.99-                , 89.00-                , 2399.00-                , 15.00-                , 15.00-                , 450.75-                , 200.00-                , 55.20-                , 3200.00-                , 74.99-                , 120.00-                ]-            )-        ,-            ( "currency"-            , D.fromList-                [ "USD" :: Text-                , "USD"-                , "EUR"-                , "USD"-                , "GBP"-                , "GBP"-                , "USD"-                , "EUR"-                , "CAD"-                , "USD"-                , "USD"-                , "GBP"-                ]-            )-        ,-            ( "status"-            , D.fromList-                [ "approved" :: Text-                , "approved"-                , "approved"-                , "declined"-                , "approved"-                , "declined"-                , "approved"-                , "approved"-                , "approved"-                , "flagged"-                , "approved"-                , "approved"-                ]-            )-        ,-            ( "location"-            , D.fromList-                [ "New York, US" :: Text-                , "London, GB"-                , "Berlin, DE"-                , "New York, US"-                , "Manchester, GB"-                , "Lagos, NG"-                , "San Francisco, US"-                , "Paris, FR"-                , "Toronto, CA"-                , "New York, US"-                , "New York, US"-                , "Edinburgh, GB"-                ]-            )-        ]- transactionsTest :: Test transactionsTest = testBothReadParquetPaths $ \readParquet ->     TestCase@@ -496,455 +213,6 @@             transactions             (unsafePerformIO (readParquet "./tests/data/transactions.parquet"))         )--mtCarsDataset :: D.DataFrame-mtCarsDataset =-    D.fromNamedColumns-        [-            ( "model"-            , D.fromList-                [ "Mazda RX4" :: Text-                , "Mazda RX4 Wag"-                , "Datsun 710"-                , "Hornet 4 Drive"-                , "Hornet Sportabout"-                , "Valiant"-                , "Duster 360"-                , "Merc 240D"-                , "Merc 230"-                , "Merc 280"-                , "Merc 280C"-                , "Merc 450SE"-                , "Merc 450SL"-                , "Merc 450SLC"-                , "Cadillac Fleetwood"-                , "Lincoln Continental"-                , "Chrysler Imperial"-                , "Fiat 128"-                , "Honda Civic"-                , "Toyota Corolla"-                , "Toyota Corona"-                , "Dodge Challenger"-                , "AMC Javelin"-                , "Camaro Z28"-                , "Pontiac Firebird"-                , "Fiat X1-9"-                , "Porsche 914-2"-                , "Lotus Europa"-                , "Ford Pantera L"-                , "Ferrari Dino"-                , "Maserati Bora"-                , "Volvo 142E"-                ]-            )-        ,-            ( "mpg"-            , D.fromList-                [ 21.0 :: Double-                , 21.0-                , 22.8-                , 21.4-                , 18.7-                , 18.1-                , 14.3-                , 24.4-                , 22.8-                , 19.2-                , 17.8-                , 16.4-                , 17.3-                , 15.2-                , 10.4-                , 10.4-                , 14.7-                , 32.4-                , 30.4-                , 33.9-                , 21.5-                , 15.5-                , 15.2-                , 13.3-                , 19.2-                , 27.3-                , 26.0-                , 30.4-                , 15.8-                , 19.7-                , 15.0-                , 21.4-                ]-            )-        ,-            ( "cyl"-            , D.fromList-                [ 6 :: Int32-                , 6-                , 4-                , 6-                , 8-                , 6-                , 8-                , 4-                , 4-                , 6-                , 6-                , 8-                , 8-                , 8-                , 8-                , 8-                , 8-                , 4-                , 4-                , 4-                , 4-                , 8-                , 8-                , 8-                , 8-                , 4-                , 4-                , 4-                , 8-                , 6-                , 8-                , 4-                ]-            )-        ,-            ( "disp"-            , D.fromList-                [ 160.0 :: Double-                , 160.0-                , 108.0-                , 258.0-                , 360.0-                , 225.0-                , 360.0-                , 146.7-                , 140.8-                , 167.6-                , 167.6-                , 275.8-                , 275.8-                , 275.8-                , 472.0-                , 460.0-                , 440.0-                , 78.7-                , 75.7-                , 71.1-                , 120.1-                , 318.0-                , 304.0-                , 350.0-                , 400.0-                , 79.0-                , 120.3-                , 95.1-                , 351.0-                , 145.0-                , 301.0-                , 121.0-                ]-            )-        ,-            ( "hp"-            , D.fromList-                [ 110 :: Int32-                , 110-                , 93-                , 110-                , 175-                , 105-                , 245-                , 62-                , 95-                , 123-                , 123-                , 180-                , 180-                , 180-                , 205-                , 215-                , 230-                , 66-                , 52-                , 65-                , 97-                , 150-                , 150-                , 245-                , 175-                , 66-                , 91-                , 113-                , 264-                , 175-                , 335-                , 109-                ]-            )-        ,-            ( "drat"-            , D.fromList-                [ 3.9 :: Double-                , 3.9-                , 3.85-                , 3.08-                , 3.15-                , 2.76-                , 3.21-                , 3.69-                , 3.92-                , 3.92-                , 3.92-                , 3.07-                , 3.07-                , 3.07-                , 2.93-                , 3.0-                , 3.23-                , 4.08-                , 4.93-                , 4.22-                , 3.7-                , 2.76-                , 3.15-                , 3.73-                , 3.08-                , 4.08-                , 4.43-                , 3.77-                , 4.22-                , 3.62-                , 3.54-                , 4.11-                ]-            )-        ,-            ( "wt"-            , D.fromList-                [ 2.62 :: Double-                , 2.875-                , 2.32-                , 3.215-                , 3.44-                , 3.46-                , 3.57-                , 3.19-                , 3.15-                , 3.44-                , 3.44-                , 4.07-                , 3.73-                , 3.78-                , 5.25-                , 5.424-                , 5.345-                , 2.2-                , 1.615-                , 1.835-                , 2.465-                , 3.52-                , 3.435-                , 3.84-                , 3.845-                , 1.935-                , 2.14-                , 1.513-                , 3.17-                , 2.77-                , 3.57-                , 2.78-                ]-            )-        ,-            ( "qsec"-            , D.fromList-                [ 16.46 :: Double-                , 17.02-                , 18.61-                , 19.44-                , 17.02-                , 20.22-                , 15.84-                , 20.0-                , 22.9-                , 18.3-                , 18.9-                , 17.4-                , 17.6-                , 18.0-                , 17.98-                , 17.82-                , 17.42-                , 19.47-                , 18.52-                , 19.9-                , 20.01-                , 16.87-                , 17.3-                , 15.41-                , 17.05-                , 18.9-                , 16.7-                , 16.9-                , 14.5-                , 15.5-                , 14.6-                , 18.6-                ]-            )-        ,-            ( "vs"-            , D.fromList-                [ 0 :: Int32-                , 0-                , 1-                , 1-                , 0-                , 1-                , 0-                , 1-                , 1-                , 1-                , 1-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 1-                , 1-                , 1-                , 1-                , 0-                , 0-                , 0-                , 0-                , 1-                , 0-                , 1-                , 0-                , 0-                , 0-                , 1-                ]-            )-        ,-            ( "am"-            , D.fromList-                [ 1 :: Int32-                , 1-                , 1-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 0-                , 1-                , 1-                , 1-                , 0-                , 0-                , 0-                , 0-                , 0-                , 1-                , 1-                , 1-                , 1-                , 1-                , 1-                , 1-                ]-            )-        ,-            ( "gear"-            , D.fromList-                [ 4 :: Int32-                , 4-                , 4-                , 3-                , 3-                , 3-                , 3-                , 4-                , 4-                , 4-                , 4-                , 3-                , 3-                , 3-                , 3-                , 3-                , 3-                , 4-                , 4-                , 4-                , 3-                , 3-                , 3-                , 3-                , 3-                , 4-                , 5-                , 5-                , 5-                , 5-                , 5-                , 4-                ]-            )-        ,-            ( "carb"-            , D.fromList-                [ 4 :: Int32-                , 4-                , 1-                , 1-                , 2-                , 1-                , 4-                , 2-                , 2-                , 4-                , 4-                , 3-                , 3-                , 3-                , 4-                , 4-                , 4-                , 1-                , 2-                , 1-                , 1-                , 2-                , 2-                , 4-                , 2-                , 1-                , 2-                , 2-                , 4-                , 6-                , 8-                , 2-                ]-            )-        ]  mtCars :: Test mtCars =
+ tests/ParquetTestData.hs view
@@ -0,0 +1,727 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Large DataFrame fixtures used by the Parquet test suite.+Kept in a separate module to avoid cluttering Parquet.hs.+-}+module ParquetTestData (+    allTypes,+    tinyPagesLast10,+    transactions,+    mtCarsDataset,+) where++import Data.Int+import Data.Text (Text)+import Data.Time+import qualified DataFrame as D++allTypes :: D.DataFrame+allTypes =+    D.fromNamedColumns+        [ ("id", D.fromList [4 :: Int32, 5, 6, 7, 2, 3, 0, 1])+        , ("bool_col", D.fromList [True, False, True, False, True, False, True, False])+        , ("tinyint_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])+        , ("smallint_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])+        , ("int_col", D.fromList [0 :: Int32, 1, 0, 1, 0, 1, 0, 1])+        , ("bigint_col", D.fromList [0 :: Int64, 10, 0, 10, 0, 10, 0, 10])+        , ("float_col", D.fromList [0 :: Float, 1.1, 0, 1.1, 0, 1.1, 0, 1.1])+        , ("double_col", D.fromList [0 :: Double, 10.1, 0, 10.1, 0, 10.1, 0, 10.1])+        ,+            ( "date_string_col"+            , D.fromList+                [ "03/01/09" :: Text+                , "03/01/09"+                , "04/01/09"+                , "04/01/09"+                , "02/01/09"+                , "02/01/09"+                , "01/01/09"+                , "01/01/09"+                ]+            )+        , ("string_col", D.fromList (take 8 (cycle ["0" :: Text, "1"])))+        ,+            ( "timestamp_col"+            , D.fromList+                [ UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 0}+                , UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 60}+                , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 0}+                , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 60}+                , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 0}+                , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 60}+                , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 0}+                , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 60}+                ]+            )+        ]++tinyPagesLast10 :: D.DataFrame+tinyPagesLast10 =+    D.fromNamedColumns+        [ ("id", D.fromList @Int32 (reverse [6174 .. 6183]))+        , ("bool_col", D.fromList @Bool (take 10 (cycle [False, True])))+        , ("tinyint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+        , ("smallint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+        , ("int_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+        , ("bigint_col", D.fromList @Int64 [30, 20, 10, 0, 90, 80, 70, 60, 50, 40])+        ,+            ( "float_col"+            , D.fromList @Float [3.3, 2.2, 1.1, 0, 9.9, 8.8, 7.7, 6.6, 5.5, 4.4]+            )+        ,+            ( "date_string_col"+            , D.fromList @Text+                [ "09/11/10"+                , "09/11/10"+                , "09/11/10"+                , "09/11/10"+                , "09/10/10"+                , "09/10/10"+                , "09/10/10"+                , "09/10/10"+                , "09/10/10"+                , "09/10/10"+                ]+            )+        ,+            ( "string_col"+            , D.fromList @Text ["3", "2", "1", "0", "9", "8", "7", "6", "5", "4"]+            )+        ,+            ( "timestamp_col"+            , D.fromList @UTCTime+                [ UTCTime+                    { utctDay = fromGregorian 2010 9 10+                    , utctDayTime = secondsToDiffTime 85384+                    }+                , UTCTime+                    { utctDay = fromGregorian 2010 9 10+                    , utctDayTime = secondsToDiffTime 85324+                    }+                , UTCTime+                    { utctDay = fromGregorian 2010 9 10+                    , utctDayTime = secondsToDiffTime 85264+                    }+                , UTCTime+                    { utctDay = fromGregorian 2010 9 10+                    , utctDayTime = secondsToDiffTime 85204+                    }+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85144}+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85084}+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85024}+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84964}+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84904}+                , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84844}+                ]+            )+        , ("year", D.fromList @Int32 (replicate 10 2010))+        , ("month", D.fromList @Int32 (replicate 10 9))+        ]++transactions :: D.DataFrame+transactions =+    D.fromNamedColumns+        [ ("transaction_id", D.fromList [1 :: Int32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])+        ,+            ( "event_time"+            , D.fromList+                [ UTCTime+                    { utctDay = fromGregorian 2024 1 3+                    , utctDayTime = secondsToDiffTime 29564 + picosecondsToDiffTime 2311000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 3+                    , utctDayTime = secondsToDiffTime 35101 + picosecondsToDiffTime 118900000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 4+                    , utctDayTime = secondsToDiffTime 39802 + picosecondsToDiffTime 774512000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 5+                    , utctDayTime = secondsToDiffTime 53739 + picosecondsToDiffTime 1000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 6+                    , utctDayTime = secondsToDiffTime 8278 + picosecondsToDiffTime 543210000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 6+                    , utctDayTime = secondsToDiffTime 8284 + picosecondsToDiffTime 211000000000+                    }+                , UTCTime{utctDay = fromGregorian 2024 1 7, utctDayTime = secondsToDiffTime 63000}+                , UTCTime+                    { utctDay = fromGregorian 2024 1 8+                    , utctDayTime = secondsToDiffTime 24259 + picosecondsToDiffTime 390000000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 9+                    , utctDayTime = secondsToDiffTime 48067 + picosecondsToDiffTime 812345000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 10+                    , utctDayTime = secondsToDiffTime 82799 + picosecondsToDiffTime 999999000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 11+                    , utctDayTime = secondsToDiffTime 36000 + picosecondsToDiffTime 100000000000+                    }+                , UTCTime+                    { utctDay = fromGregorian 2024 1 12+                    , utctDayTime = secondsToDiffTime 56028 + picosecondsToDiffTime 667891000000+                    }+                ]+            )+        ,+            ( "user_email"+            , D.fromList+                [ "alice@example.com" :: Text+                , "bob@example.com"+                , "carol@example.com"+                , "alice@example.com"+                , "dave@example.com"+                , "dave@example.com"+                , "eve@example.com"+                , "frank@example.com"+                , "grace@example.com"+                , "dave@example.com"+                , "alice@example.com"+                , "heidi@example.com"+                ]+            )+        ,+            ( "transaction_type"+            , D.fromList+                [ "purchase" :: Text+                , "purchase"+                , "refund"+                , "purchase"+                , "purchase"+                , "purchase"+                , "purchase"+                , "withdrawal"+                , "purchase"+                , "purchase"+                , "purchase"+                , "refund"+                ]+            )+        ,+            ( "amount"+            , D.fromList+                [ 142.50 :: Double+                , 29.99+                , 89.00+                , 2399.00+                , 15.00+                , 15.00+                , 450.75+                , 200.00+                , 55.20+                , 3200.00+                , 74.99+                , 120.00+                ]+            )+        ,+            ( "currency"+            , D.fromList+                [ "USD" :: Text+                , "USD"+                , "EUR"+                , "USD"+                , "GBP"+                , "GBP"+                , "USD"+                , "EUR"+                , "CAD"+                , "USD"+                , "USD"+                , "GBP"+                ]+            )+        ,+            ( "status"+            , D.fromList+                [ "approved" :: Text+                , "approved"+                , "approved"+                , "declined"+                , "approved"+                , "declined"+                , "approved"+                , "approved"+                , "approved"+                , "flagged"+                , "approved"+                , "approved"+                ]+            )+        ,+            ( "location"+            , D.fromList+                [ "New York, US" :: Text+                , "London, GB"+                , "Berlin, DE"+                , "New York, US"+                , "Manchester, GB"+                , "Lagos, NG"+                , "San Francisco, US"+                , "Paris, FR"+                , "Toronto, CA"+                , "New York, US"+                , "New York, US"+                , "Edinburgh, GB"+                ]+            )+        ]++mtCarsDataset :: D.DataFrame+mtCarsDataset =+    D.fromNamedColumns+        [+            ( "model"+            , D.fromList+                [ "Mazda RX4" :: Text+                , "Mazda RX4 Wag"+                , "Datsun 710"+                , "Hornet 4 Drive"+                , "Hornet Sportabout"+                , "Valiant"+                , "Duster 360"+                , "Merc 240D"+                , "Merc 230"+                , "Merc 280"+                , "Merc 280C"+                , "Merc 450SE"+                , "Merc 450SL"+                , "Merc 450SLC"+                , "Cadillac Fleetwood"+                , "Lincoln Continental"+                , "Chrysler Imperial"+                , "Fiat 128"+                , "Honda Civic"+                , "Toyota Corolla"+                , "Toyota Corona"+                , "Dodge Challenger"+                , "AMC Javelin"+                , "Camaro Z28"+                , "Pontiac Firebird"+                , "Fiat X1-9"+                , "Porsche 914-2"+                , "Lotus Europa"+                , "Ford Pantera L"+                , "Ferrari Dino"+                , "Maserati Bora"+                , "Volvo 142E"+                ]+            )+        ,+            ( "mpg"+            , D.fromList+                [ 21.0 :: Double+                , 21.0+                , 22.8+                , 21.4+                , 18.7+                , 18.1+                , 14.3+                , 24.4+                , 22.8+                , 19.2+                , 17.8+                , 16.4+                , 17.3+                , 15.2+                , 10.4+                , 10.4+                , 14.7+                , 32.4+                , 30.4+                , 33.9+                , 21.5+                , 15.5+                , 15.2+                , 13.3+                , 19.2+                , 27.3+                , 26.0+                , 30.4+                , 15.8+                , 19.7+                , 15.0+                , 21.4+                ]+            )+        ,+            ( "cyl"+            , D.fromList+                [ 6 :: Int32+                , 6+                , 4+                , 6+                , 8+                , 6+                , 8+                , 4+                , 4+                , 6+                , 6+                , 8+                , 8+                , 8+                , 8+                , 8+                , 8+                , 4+                , 4+                , 4+                , 4+                , 8+                , 8+                , 8+                , 8+                , 4+                , 4+                , 4+                , 8+                , 6+                , 8+                , 4+                ]+            )+        ,+            ( "disp"+            , D.fromList+                [ 160.0 :: Double+                , 160.0+                , 108.0+                , 258.0+                , 360.0+                , 225.0+                , 360.0+                , 146.7+                , 140.8+                , 167.6+                , 167.6+                , 275.8+                , 275.8+                , 275.8+                , 472.0+                , 460.0+                , 440.0+                , 78.7+                , 75.7+                , 71.1+                , 120.1+                , 318.0+                , 304.0+                , 350.0+                , 400.0+                , 79.0+                , 120.3+                , 95.1+                , 351.0+                , 145.0+                , 301.0+                , 121.0+                ]+            )+        ,+            ( "hp"+            , D.fromList+                [ 110 :: Int32+                , 110+                , 93+                , 110+                , 175+                , 105+                , 245+                , 62+                , 95+                , 123+                , 123+                , 180+                , 180+                , 180+                , 205+                , 215+                , 230+                , 66+                , 52+                , 65+                , 97+                , 150+                , 150+                , 245+                , 175+                , 66+                , 91+                , 113+                , 264+                , 175+                , 335+                , 109+                ]+            )+        ,+            ( "drat"+            , D.fromList+                [ 3.9 :: Double+                , 3.9+                , 3.85+                , 3.08+                , 3.15+                , 2.76+                , 3.21+                , 3.69+                , 3.92+                , 3.92+                , 3.92+                , 3.07+                , 3.07+                , 3.07+                , 2.93+                , 3.0+                , 3.23+                , 4.08+                , 4.93+                , 4.22+                , 3.7+                , 2.76+                , 3.15+                , 3.73+                , 3.08+                , 4.08+                , 4.43+                , 3.77+                , 4.22+                , 3.62+                , 3.54+                , 4.11+                ]+            )+        ,+            ( "wt"+            , D.fromList+                [ 2.62 :: Double+                , 2.875+                , 2.32+                , 3.215+                , 3.44+                , 3.46+                , 3.57+                , 3.19+                , 3.15+                , 3.44+                , 3.44+                , 4.07+                , 3.73+                , 3.78+                , 5.25+                , 5.424+                , 5.345+                , 2.2+                , 1.615+                , 1.835+                , 2.465+                , 3.52+                , 3.435+                , 3.84+                , 3.845+                , 1.935+                , 2.14+                , 1.513+                , 3.17+                , 2.77+                , 3.57+                , 2.78+                ]+            )+        ,+            ( "qsec"+            , D.fromList+                [ 16.46 :: Double+                , 17.02+                , 18.61+                , 19.44+                , 17.02+                , 20.22+                , 15.84+                , 20.0+                , 22.9+                , 18.3+                , 18.9+                , 17.4+                , 17.6+                , 18.0+                , 17.98+                , 17.82+                , 17.42+                , 19.47+                , 18.52+                , 19.9+                , 20.01+                , 16.87+                , 17.3+                , 15.41+                , 17.05+                , 18.9+                , 16.7+                , 16.9+                , 14.5+                , 15.5+                , 14.6+                , 18.6+                ]+            )+        ,+            ( "vs"+            , D.fromList+                [ 0 :: Int32+                , 0+                , 1+                , 1+                , 0+                , 1+                , 0+                , 1+                , 1+                , 1+                , 1+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 1+                , 1+                , 1+                , 1+                , 0+                , 0+                , 0+                , 0+                , 1+                , 0+                , 1+                , 0+                , 0+                , 0+                , 1+                ]+            )+        ,+            ( "am"+            , D.fromList+                [ 1 :: Int32+                , 1+                , 1+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 0+                , 1+                , 1+                , 1+                , 0+                , 0+                , 0+                , 0+                , 0+                , 1+                , 1+                , 1+                , 1+                , 1+                , 1+                , 1+                ]+            )+        ,+            ( "gear"+            , D.fromList+                [ 4 :: Int32+                , 4+                , 4+                , 3+                , 3+                , 3+                , 3+                , 4+                , 4+                , 4+                , 4+                , 3+                , 3+                , 3+                , 3+                , 3+                , 3+                , 4+                , 4+                , 4+                , 3+                , 3+                , 3+                , 3+                , 3+                , 4+                , 5+                , 5+                , 5+                , 5+                , 5+                , 4+                ]+            )+        ,+            ( "carb"+            , D.fromList+                [ 4 :: Int32+                , 4+                , 1+                , 1+                , 2+                , 1+                , 4+                , 2+                , 2+                , 4+                , 4+                , 3+                , 3+                , 3+                , 4+                , 4+                , 4+                , 1+                , 2+                , 1+                , 1+                , 2+                , 2+                , 4+                , 2+                , 1+                , 2+                , 2+                , 4+                , 6+                , 8+                , 2+                ]+            )+        ]
+ tests/data/typing/texts.txt view
@@ -0,0 +1,34 @@+To+protect+the+world+from+devastation+To+unite+all+people+within+our+nation+To+denounce+the+evils+of+truth+and+love+To+extend+our+reach+to+the+stars+above+JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!+Surrender now or prepare to fight!+Meowth, that's right!
+ tests/data/typing/texts_with_empties.txt view
@@ -0,0 +1,41 @@++To+protect+the+world+from+devastation++To+unite+all+people+within+our+nation++To+denounce+the+evils+of+truth+and+love++To+extend+our+reach+to+the+stars+above++JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!++Surrender now or prepare to fight!++Meowth, that's right!
+ tests/data/typing/texts_with_empties_and_nullish.txt view
@@ -0,0 +1,44 @@++To+protect+the+world+from+devastation++To+unite+all+people+within+our+nation++To+denounce+the+evils+of+truth+and+love++To+extend+our+reach+to+the+stars+above++JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!++Surrender now or prepare to fight!++Meowth, that's right!+NaN+Nothing+N/A