diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,14 @@
-# Revision history for javelin
-
-## Release 0.1.1.0
-
-* Added the `Data.Series.Index.indexed` function
-* Replace all INLINE pragmas for INLINABLE, which will improve compilation speed and performance.
-
-## Release 0.1.0.0
-
-* This is the first version of `javelin` and associated packages.
+# Revision history for javelin
+
+## Release 0.1.2.0
+
+* Fixed an issue where `Series` could be corrupted while using `aggregateWith`.
+
+## Release 0.1.1.0
+
+* Added the `Data.Series.Index.indexed` function
+* Replace all INLINE pragmas for INLINABLE, which will improve compilation speed and performance.
+
+## Release 0.1.0.0
+
+* This is the first version of `javelin` and associated packages.
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,20 +1,20 @@
-Copyright (c) Laurent P. René de Cotret
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be included
-in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Copyright (c) Laurent P. René de Cotret
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/benchmarks/Comparison.hs b/benchmarks/Comparison.hs
--- a/benchmarks/Comparison.hs
+++ b/benchmarks/Comparison.hs
@@ -1,201 +1,201 @@
--- This benchmarking script is forked from
--- https://github.com/haskell-perf/dictionaries/blob/master/Time.hs
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-# LANGUAGE BangPatterns              #-}
-{-# LANGUAGE ExistentialQuantification #-}
-
-module Main (main) where
-
-import           Control.DeepSeq  ( NFData, force )
-import qualified Control.Foldl    as Fold
-import           Control.Monad    ( when )
-import           Criterion.Main   ( defaultMainWith, defaultConfig, bench, bgroup, env, nf )
-import           Criterion.Types  ( Config(csvFile) )
-import           Data.List        ( foldl' )
-import qualified Data.Map.Lazy
-import qualified Data.Map.Strict
-import           Data.MonoTraversable ( ofoldlUnwrap )
-import           Data.Set         ( Set )
-import qualified Data.Set         as Set 
-import qualified Data.Series
-import qualified Data.Series.Unboxed
-import qualified Data.Series.Index as Index
-import qualified Data.Vector
-import qualified Data.Vector.Unboxed
-import           System.Directory ( doesFileExist, removeFile )
-import           System.Random    ( mkStdGen, Random(randoms) )
-
-data Lookup =
-  forall f. (NFData (f Int)) =>
-            Lookup String
-                   ([(Int, Int)] -> f Int)
-                   (Int -> f Int ->  Maybe Int)
-
-data Sum =
-  forall f. (NFData (f Int)) =>
-            Sum String ([(Int, Int)] -> f Int) (f Int -> Int)
-
-data Fold =
-  forall f. (NFData (f Double)) =>
-            Fold String ([(Int, Double)] -> f Double) (f Double -> Double)
-
-data Mappend = 
-  forall f. (NFData (f Int), Monoid (f Int)) =>
-            Mappend String 
-                   ([(Int, Int)] -> f Int)
-
-data SliceByKeys =
-  forall f. (NFData (f Int), Monoid (f Int)) =>
-            SliceByKeys String 
-                   ([(Int, Int)] -> f Int)
-                   (Set Int -> f Int -> f Int)
-
-
-
-
-main :: IO ()
-main = do
-  let fp = "out.csv"
-  exists <- doesFileExist fp
-  when exists (removeFile fp)
-  defaultMainWith
-    defaultConfig {csvFile = Just fp}
-    [ bgroup
-        "Lookup Int (Randomized)"
-        (lookupRandomized
-           [ Lookup "Data.Map.Lazy" Data.Map.Lazy.fromList Data.Map.Lazy.lookup
-           , Lookup
-               "Data.Map.Strict"
-               Data.Map.Strict.fromList
-               Data.Map.Strict.lookup
-           , Lookup
-               "Data.Series"
-               Data.Series.fromList
-               (flip Data.Series.at)
-           , Lookup 
-                "Data.Vector"
-                (Data.Vector.fromList . map fst)
-                (\ix -> Data.Vector.find (==ix))
-           , Lookup
-               "Data.Series.Unboxed"
-               Data.Series.Unboxed.fromList
-               (flip Data.Series.Unboxed.at)
-           , Lookup 
-                "Data.Vector.Unboxed"
-                (Data.Vector.Unboxed.fromList . map fst)
-                (\ix -> Data.Vector.Unboxed.find (==ix))
-           ])
-    , bgroup
-        "Sum Int (Randomized)"
-        (sumRandomized
-           [ Sum "Data.Map.Lazy"   Data.Map.Lazy.fromList sum
-           , Sum "Data.Map.Strict" Data.Map.Strict.fromList sum
-           , Sum "Data.Series" Data.Series.fromList sum
-           , Sum "Data.Vector" (Data.Vector.fromList . map snd) sum
-           , Sum "Data.Series.Unboxed"  Data.Series.Unboxed.fromList Data.Series.Unboxed.sum
-           , Sum "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) Data.Vector.Unboxed.sum
-           ])
-    , bgroup
-        "Fold mean (Randomized)"
-        (foldRandomized
-           [ Fold "Data.Map.Lazy"   Data.Map.Lazy.fromList (Fold.fold Fold.mean)
-           , Fold "Data.Map.Strict" Data.Map.Strict.fromList (Fold.fold Fold.mean)
-           , Fold "Data.Series" Data.Series.fromList (Data.Series.fold Fold.mean)
-           , Fold "Data.Vector" (Data.Vector.fromList . map snd) (Fold.fold Fold.mean)
-           , Fold "Data.Series.Unboxed"  Data.Series.Unboxed.fromList (Data.Series.Unboxed.fold Fold.mean)
-           , Fold "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) (Fold.purely ofoldlUnwrap Fold.mean)
-           ])
-    , bgroup
-      "Mappend Int (Randomized)"
-      ( mappendRandomized 
-          [ Mappend "Data.Map.Lazy" Data.Map.Lazy.fromList
-          , Mappend "Data.Map.Strict" Data.Map.Strict.fromList
-          , Mappend "Data.Series" Data.Series.fromList
-          , Mappend "Data.Vector" (Data.Vector.fromList . map snd)
-          , Mappend "Data.Series.Unboxed" Data.Series.Unboxed.fromList
-          , Mappend "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd)
-          ])
-    , bgroup
-      "Slice by keys (Randomized)"
-      ( sliceByKeyRandomized 
-          [ SliceByKeys "Data.Map.Lazy" 
-                        Data.Map.Lazy.fromList
-                        (flip Data.Map.Lazy.restrictKeys)
-          , SliceByKeys "Data.Map.Strict" 
-                        Data.Map.Strict.fromList
-                        (flip Data.Map.Strict.restrictKeys)
-          , SliceByKeys "Data.Series" 
-                        Data.Series.fromList
-                        (\ks xs -> xs `Data.Series.select` Index.fromSet ks)
-          , SliceByKeys "Data.Series.Unboxed" 
-                        Data.Series.Unboxed.fromList
-                        (\ks xs -> xs `Data.Series.Unboxed.select` Index.fromSet ks)
-          ])
-    ]
-
-  where
-    lookupRandomized funcs =
-      [ env
-        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             !elems = force (fromList list)
-          in pure (list, elems))
-        (\(~(list, elems)) ->
-           bench (title ++ ":" ++ show i) $
-           nf
-             (foldl'
-                  (\_ k ->
-                     case func k elems of
-                       Just !v -> v
-                       Nothing -> 0)
-                  0)
-             (map fst list))
-      | i <- [10, 100, 1000, 10000]
-      , Lookup title fromList func <- funcs
-      ]
-    sumRandomized funcs =
-      [ env
-        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             !elems = force (fromList list)
-          in pure (list, elems))
-        (\(~(_, elems)) ->
-           bench (title ++ ":" ++ show i) $
-           nf func elems)
-      | i <- [10, 100, 1000, 10000, 100000, 1000000]
-      , Sum title fromList func <- funcs
-      ]
-    foldRandomized funcs =
-      [ env
-        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             !elems = force (fromList list)
-          in pure (list, elems))
-        (\(~(_, elems)) ->
-           bench (title ++ ":" ++ show i) $
-           nf func elems)
-      | i <- [10, 100, 1000, 10000, 100000, 1000000]
-      , Fold title fromList func <- funcs
-      ]
-    mappendRandomized funcs =
-      [ env
-        (let list1 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             list2 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             !elems1 = force (fromList list1)
-             !elems2 = force (fromList list2)
-          in pure (elems1, elems2))
-        (\(~(elems1, elems2)) ->
-           bench (title ++ ":" ++ show i) $
-           nf mconcat [elems1, elems2])
-      | i <- [10, 100, 1000, 10000, 100000, 1000000]
-      , Mappend title fromList <- funcs
-      ]
-    sliceByKeyRandomized funcs = 
-      [ env
-        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
-             keys  = Set.fromList $ take (round ((fromIntegral i / 10) :: Double)) (randoms (mkStdGen 0) :: [Int])
-             !elems = force (fromList list)
-          in pure (keys, elems))
-        (\(~(keys, elems)) ->
-           bench (title ++ ":" ++ show i) $
-           nf (slice keys) elems)
-      | i <- [10, 100, 1000, 10000, 100000, 1000000]
-      , SliceByKeys title fromList slice <- funcs
+-- This benchmarking script is forked from
+-- https://github.com/haskell-perf/dictionaries/blob/master/Time.hs
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE BangPatterns              #-}
+{-# LANGUAGE ExistentialQuantification #-}
+
+module Main (main) where
+
+import           Control.DeepSeq  ( NFData, force )
+import qualified Control.Foldl    as Fold
+import           Control.Monad    ( when )
+import           Criterion.Main   ( defaultMainWith, defaultConfig, bench, bgroup, env, nf )
+import           Criterion.Types  ( Config(csvFile) )
+import           Data.List        ( foldl' )
+import qualified Data.Map.Lazy
+import qualified Data.Map.Strict
+import           Data.MonoTraversable ( ofoldlUnwrap )
+import           Data.Set         ( Set )
+import qualified Data.Set         as Set 
+import qualified Data.Series
+import qualified Data.Series.Unboxed
+import qualified Data.Series.Index as Index
+import qualified Data.Vector
+import qualified Data.Vector.Unboxed
+import           System.Directory ( doesFileExist, removeFile )
+import           System.Random    ( mkStdGen, Random(randoms) )
+
+data Lookup =
+  forall f. (NFData (f Int)) =>
+            Lookup String
+                   ([(Int, Int)] -> f Int)
+                   (Int -> f Int ->  Maybe Int)
+
+data Sum =
+  forall f. (NFData (f Int)) =>
+            Sum String ([(Int, Int)] -> f Int) (f Int -> Int)
+
+data Fold =
+  forall f. (NFData (f Double)) =>
+            Fold String ([(Int, Double)] -> f Double) (f Double -> Double)
+
+data Mappend = 
+  forall f. (NFData (f Int), Monoid (f Int)) =>
+            Mappend String 
+                   ([(Int, Int)] -> f Int)
+
+data SliceByKeys =
+  forall f. (NFData (f Int), Monoid (f Int)) =>
+            SliceByKeys String 
+                   ([(Int, Int)] -> f Int)
+                   (Set Int -> f Int -> f Int)
+
+
+
+
+main :: IO ()
+main = do
+  let fp = "out.csv"
+  exists <- doesFileExist fp
+  when exists (removeFile fp)
+  defaultMainWith
+    defaultConfig {csvFile = Just fp}
+    [ bgroup
+        "Lookup Int (Randomized)"
+        (lookupRandomized
+           [ Lookup "Data.Map.Lazy" Data.Map.Lazy.fromList Data.Map.Lazy.lookup
+           , Lookup
+               "Data.Map.Strict"
+               Data.Map.Strict.fromList
+               Data.Map.Strict.lookup
+           , Lookup
+               "Data.Series"
+               Data.Series.fromList
+               (flip Data.Series.at)
+           , Lookup 
+                "Data.Vector"
+                (Data.Vector.fromList . map fst)
+                (\ix -> Data.Vector.find (==ix))
+           , Lookup
+               "Data.Series.Unboxed"
+               Data.Series.Unboxed.fromList
+               (flip Data.Series.Unboxed.at)
+           , Lookup 
+                "Data.Vector.Unboxed"
+                (Data.Vector.Unboxed.fromList . map fst)
+                (\ix -> Data.Vector.Unboxed.find (==ix))
+           ])
+    , bgroup
+        "Sum Int (Randomized)"
+        (sumRandomized
+           [ Sum "Data.Map.Lazy"   Data.Map.Lazy.fromList sum
+           , Sum "Data.Map.Strict" Data.Map.Strict.fromList sum
+           , Sum "Data.Series" Data.Series.fromList sum
+           , Sum "Data.Vector" (Data.Vector.fromList . map snd) sum
+           , Sum "Data.Series.Unboxed"  Data.Series.Unboxed.fromList Data.Series.Unboxed.sum
+           , Sum "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) Data.Vector.Unboxed.sum
+           ])
+    , bgroup
+        "Fold mean (Randomized)"
+        (foldRandomized
+           [ Fold "Data.Map.Lazy"   Data.Map.Lazy.fromList (Fold.fold Fold.mean)
+           , Fold "Data.Map.Strict" Data.Map.Strict.fromList (Fold.fold Fold.mean)
+           , Fold "Data.Series" Data.Series.fromList (Data.Series.fold Fold.mean)
+           , Fold "Data.Vector" (Data.Vector.fromList . map snd) (Fold.fold Fold.mean)
+           , Fold "Data.Series.Unboxed"  Data.Series.Unboxed.fromList (Data.Series.Unboxed.fold Fold.mean)
+           , Fold "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) (Fold.purely ofoldlUnwrap Fold.mean)
+           ])
+    , bgroup
+      "Mappend Int (Randomized)"
+      ( mappendRandomized 
+          [ Mappend "Data.Map.Lazy" Data.Map.Lazy.fromList
+          , Mappend "Data.Map.Strict" Data.Map.Strict.fromList
+          , Mappend "Data.Series" Data.Series.fromList
+          , Mappend "Data.Vector" (Data.Vector.fromList . map snd)
+          , Mappend "Data.Series.Unboxed" Data.Series.Unboxed.fromList
+          , Mappend "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd)
+          ])
+    , bgroup
+      "Slice by keys (Randomized)"
+      ( sliceByKeyRandomized 
+          [ SliceByKeys "Data.Map.Lazy" 
+                        Data.Map.Lazy.fromList
+                        (flip Data.Map.Lazy.restrictKeys)
+          , SliceByKeys "Data.Map.Strict" 
+                        Data.Map.Strict.fromList
+                        (flip Data.Map.Strict.restrictKeys)
+          , SliceByKeys "Data.Series" 
+                        Data.Series.fromList
+                        (\ks xs -> xs `Data.Series.select` Index.fromSet ks)
+          , SliceByKeys "Data.Series.Unboxed" 
+                        Data.Series.Unboxed.fromList
+                        (\ks xs -> xs `Data.Series.Unboxed.select` Index.fromSet ks)
+          ])
+    ]
+
+  where
+    lookupRandomized funcs =
+      [ env
+        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             !elems = force (fromList list)
+          in pure (list, elems))
+        (\(~(list, elems)) ->
+           bench (title ++ ":" ++ show i) $
+           nf
+             (foldl'
+                  (\_ k ->
+                     case func k elems of
+                       Just !v -> v
+                       Nothing -> 0)
+                  0)
+             (map fst list))
+      | i <- [10, 100, 1000, 10000]
+      , Lookup title fromList func <- funcs
+      ]
+    sumRandomized funcs =
+      [ env
+        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             !elems = force (fromList list)
+          in pure (list, elems))
+        (\(~(_, elems)) ->
+           bench (title ++ ":" ++ show i) $
+           nf func elems)
+      | i <- [10, 100, 1000, 10000, 100000, 1000000]
+      , Sum title fromList func <- funcs
+      ]
+    foldRandomized funcs =
+      [ env
+        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             !elems = force (fromList list)
+          in pure (list, elems))
+        (\(~(_, elems)) ->
+           bench (title ++ ":" ++ show i) $
+           nf func elems)
+      | i <- [10, 100, 1000, 10000, 100000, 1000000]
+      , Fold title fromList func <- funcs
+      ]
+    mappendRandomized funcs =
+      [ env
+        (let list1 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             list2 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             !elems1 = force (fromList list1)
+             !elems2 = force (fromList list2)
+          in pure (elems1, elems2))
+        (\(~(elems1, elems2)) ->
+           bench (title ++ ":" ++ show i) $
+           nf mconcat [elems1, elems2])
+      | i <- [10, 100, 1000, 10000, 100000, 1000000]
+      , Mappend title fromList <- funcs
+      ]
+    sliceByKeyRandomized funcs = 
+      [ env
+        (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])
+             keys  = Set.fromList $ take (round ((fromIntegral i / 10) :: Double)) (randoms (mkStdGen 0) :: [Int])
+             !elems = force (fromList list)
+          in pure (keys, elems))
+        (\(~(keys, elems)) ->
+           bench (title ++ ":" ++ show i) $
+           nf (slice keys) elems)
+      | i <- [10, 100, 1000, 10000, 100000, 1000000]
+      , SliceByKeys title fromList slice <- funcs
       ]
diff --git a/benchmarks/Operations.hs b/benchmarks/Operations.hs
--- a/benchmarks/Operations.hs
+++ b/benchmarks/Operations.hs
@@ -1,70 +1,70 @@
-
-import           Control.DeepSeq    ( rnf )
-import           Control.Exception  ( evaluate )
-import           Criterion.Main     ( bench, whnf, defaultMain )
-
-import           Data.Foldable      ( Foldable(foldl') )
-import           Data.Set           ( Set )     
-import qualified Data.Set           as Set
-import           Data.Series        ( Series )
-import qualified Data.Series        as Series
-import qualified Data.Series.Index  as Index
-
-
-main :: IO ()
-main = do
-    let srs1        = Series.fromList $ zip [0..] [1::Int .. 2^(12::Int)]
-        srs2        = Series.fromList $ zip [0,2..] [1::Int .. 2^(12::Int)]
-        elems      = Index.toSet $ Series.index srs1
-        small      = Set.fromAscList  [1::Int ..  2^(8::Int)]
-        elems_even = Set.fromDistinctAscList  [2::Int, 4..2^(12::Int)]
-        elems_odd  = Set.fromDistinctAscList  [1::Int, 3..2^(12::Int)]
-    evaluate $ rnf [elems, small, elems_even, elems_odd]
-    evaluate $ rnf [srs1, srs2]
-    defaultMain
-        [ bench "at" $ whnf (at elems_even) srs1
-        , bench "iat" $ whnf (iat elems_even) srs1
-        , bench "select" $ whnf (select elems_odd) srs1
-        , bench "mappend" $ whnf (mappend' srs1) srs2
-        , bench "zipWithMatched" $ whnf (zipWithMatched srs1) srs1
-        , bench "group by ... aggregate with ..." $ whnf (groupbyagg small) srs1
-        , bench "group by ... fold with ..." $ whnf (groupbyfold small) srs1
-        ]
-
-at :: Set Int -> Series Int Int -> Int
-at xs s = foldl' go 0 xs
-    where
-        go n x = case s `Series.at` x of 
-            Just _ -> n + 1
-            Nothing -> n
-
-iat :: Set Int -> Series Int Int -> Int
-iat xs s = foldl' go 0 xs
-    where
-        go n x = case s `Series.iat` x of 
-            Just _ -> n + 1
-            Nothing -> n
-  
-select :: Set Int -> Series Int Int -> Int
-select ks s = foldl' go 0 ks
-    where
-        go n k = n + length (s `Series.select` ((k-100) `Series.to` (k+100)))
-
-
-mappend' :: Series Int Int -> Series Int Int -> Int
-mappend' xs ys = sum $ xs <> ys
-
-
-zipWithMatched :: Series Int Int -> Series Int Int -> Int
-zipWithMatched xs ys = length $ Series.zipWithMatched (+) xs ys
-
-
-groupbyagg :: Set Int -> Series Int Int -> Int
-groupbyagg ks s = foldl' go 0 ks
-    where
-        go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.aggregateWith` sum)
-
-groupbyfold :: Set Int -> Series Int Int -> Int
-groupbyfold ks s = foldl' go 0 ks
-    where
+
+import           Control.DeepSeq    ( rnf )
+import           Control.Exception  ( evaluate )
+import           Criterion.Main     ( bench, whnf, defaultMain )
+
+import           Data.Foldable      ( Foldable(foldl') )
+import           Data.Set           ( Set )     
+import qualified Data.Set           as Set
+import           Data.Series        ( Series )
+import qualified Data.Series        as Series
+import qualified Data.Series.Index  as Index
+
+
+main :: IO ()
+main = do
+    let srs1        = Series.fromList $ zip [0..] [1::Int .. 2^(12::Int)]
+        srs2        = Series.fromList $ zip [0,2..] [1::Int .. 2^(12::Int)]
+        elems      = Index.toSet $ Series.index srs1
+        small      = Set.fromAscList  [1::Int ..  2^(8::Int)]
+        elems_even = Set.fromDistinctAscList  [2::Int, 4..2^(12::Int)]
+        elems_odd  = Set.fromDistinctAscList  [1::Int, 3..2^(12::Int)]
+    evaluate $ rnf [elems, small, elems_even, elems_odd]
+    evaluate $ rnf [srs1, srs2]
+    defaultMain
+        [ bench "at" $ whnf (at elems_even) srs1
+        , bench "iat" $ whnf (iat elems_even) srs1
+        , bench "select" $ whnf (select elems_odd) srs1
+        , bench "mappend" $ whnf (mappend' srs1) srs2
+        , bench "zipWithMatched" $ whnf (zipWithMatched srs1) srs1
+        , bench "group by ... aggregate with ..." $ whnf (groupbyagg small) srs1
+        , bench "group by ... fold with ..." $ whnf (groupbyfold small) srs1
+        ]
+
+at :: Set Int -> Series Int Int -> Int
+at xs s = foldl' go 0 xs
+    where
+        go n x = case s `Series.at` x of 
+            Just _ -> n + 1
+            Nothing -> n
+
+iat :: Set Int -> Series Int Int -> Int
+iat xs s = foldl' go 0 xs
+    where
+        go n x = case s `Series.iat` x of 
+            Just _ -> n + 1
+            Nothing -> n
+  
+select :: Set Int -> Series Int Int -> Int
+select ks s = foldl' go 0 ks
+    where
+        go n k = n + length (s `Series.select` ((k-100) `Series.to` (k+100)))
+
+
+mappend' :: Series Int Int -> Series Int Int -> Int
+mappend' xs ys = sum $ xs <> ys
+
+
+zipWithMatched :: Series Int Int -> Series Int Int -> Int
+zipWithMatched xs ys = length $ Series.zipWithMatched (+) xs ys
+
+
+groupbyagg :: Set Int -> Series Int Int -> Int
+groupbyagg ks s = foldl' go 0 ks
+    where
+        go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.aggregateWith` sum)
+
+groupbyfold :: Set Int -> Series Int Int -> Int
+groupbyfold ks s = foldl' go 0 ks
+    where
         go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.foldWith` (+))
diff --git a/javelin.cabal b/javelin.cabal
--- a/javelin.cabal
+++ b/javelin.cabal
@@ -1,136 +1,136 @@
-cabal-version:      3.0
-name:               javelin
-version:            0.1.1.0
-synopsis:           Labeled one-dimensional arrays
-license:            MIT
-license-file:       LICENSE
-author:             Laurent P. René de Cotret
-maintainer:         laurent.decotret@outlook.com
-category:           Data, Data Structures, Data Science
-build-type:         Simple
-extra-doc-files:    CHANGELOG.md
-                    files/aapl.txt
-tested-with:        GHC ==9.8.1 
-                     || ==9.6.3
-                     || ==9.4.7  
-description:
-        
-        This package implements 'Series', labeled one-dimensional arrays
-        combining properties from maps and arrays.
-        
-        To get started, the important modules are:
-        
-        ["Data.Series"] Boxed series of arbitrary types.
-        
-        ["Data.Series.Unboxed"] Series of unboxed data types for better performance, at the cost of flexibility.
-        
-        ["Data.Series.Generic"] Generic interface to manipulate any type of 'Series'.
-        
-        ["Data.Series.Index"] Index containing series keys.
-        
-        To get started, please take a look at the tutorial ("Data.Series.Tutorial").
-        
-
-common common
-    default-language: GHC2021
-    ghc-options: -Wall
-                 -Wcompat
-                 -Widentities
-                 -Wincomplete-uni-patterns
-                 -Wincomplete-record-updates
-                 -Wredundant-constraints
-                 -fhide-source-paths
-                 -Wpartial-fields
-
-library
-    import:           common
-    hs-source-dirs:   src
-    exposed-modules:  Data.Series
-                      Data.Series.Generic
-                      Data.Series.Generic.Internal
-                      Data.Series.Index
-                      Data.Series.Index.Internal
-                      Data.Series.Tutorial
-                      Data.Series.Unboxed
-    other-modules:    Data.Series.Generic.Aggregation
-                      Data.Series.Generic.Definition
-                      Data.Series.Generic.Scans
-                      Data.Series.Generic.View
-                      Data.Series.Generic.Zip
-                      Data.Series.Index.Definition
-    build-depends:    base                >=4.15.0.0 && <4.20,
-                      containers          >=0.6      && <0.8,
-                      deepseq             >=1.4      && <1.6,
-                      foldl               ^>=1.4,
-                      indexed-traversable ^>=0.1,
-                      vector              >=0.12.3.0 && <0.14,
-                      vector-algorithms   ^>=0.9
-
-test-suite javelin-test
-    import:           common
-    type:             exitcode-stdio-1.0
-    hs-source-dirs:   test
-    main-is:          Main.hs
-    other-modules:    Test.Data.Series
-                      Test.Data.Series.Index
-                      Test.Data.Series.Generic.Aggregation
-                      Test.Data.Series.Generic.Definition
-                      Test.Data.Series.Generic.View
-                      Test.Data.Series.Generic.Zip
-    build-depends:    base,
-                      containers,
-                      foldl,
-                      hedgehog,
-                      HUnit,
-                      javelin,
-                      tasty,
-                      tasty-hedgehog,
-                      tasty-hspec,
-                      tasty-hunit,
-                      vector
-
-
--- Running the 'comparison-containers' benchmark is expected
--- to be done in conjunction with the cabal.project.profiling project file:
--- > cabal bench comparison-containers --project=cabal.project.profiling
-benchmark comparison-containers
-    import:           common
-    type:             exitcode-stdio-1.0
-    ghc-options:      -rtsopts
-    hs-source-dirs:   benchmarks
-    main-is:          Comparison.hs
-    build-depends:    base,
-                      containers,
-                      foldl,
-                      mono-traversable,
-                      javelin,
-                      vector, 
-                      criterion, 
-                      deepseq, 
-                      random, 
-                      directory
-
-
--- Running the 'operations' benchmark is expected
--- to be done in conjunction with the cabal.project.profiling project file:
--- > cabal bench operations --project=cabal.project.profiling
-benchmark operations
-    import:           common
-    type:             exitcode-stdio-1.0
-    ghc-options:      -rtsopts
-    hs-source-dirs:   benchmarks
-    main-is:          Operations.hs
-    build-depends:    base,
-                      containers,
-                      deepseq,
-                      foldl,
-                      javelin,
-                      criterion
-
-
-executable bench-report
-    import:           common
-    main-is:          bench-report.hs
-    hs-source-dirs:   scripts
-    build-depends:    base, 
-                      csv ^>=0.1
+cabal-version:      3.0
+name:               javelin
+version:            0.1.2.0
+synopsis:           Labeled one-dimensional arrays
+license:            MIT
+license-file:       LICENSE
+author:             Laurent P. René de Cotret
+maintainer:         laurent.decotret@outlook.com
+category:           Data, Data Structures, Data Science
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+                    files/aapl.txt
+tested-with:        GHC ==9.8.1 
+                     || ==9.6.3
+                     || ==9.4.7  
+description:
+        
+        This package implements 'Series', labeled one-dimensional arrays
+        combining properties from maps and arrays.
+        
+        To get started, the important modules are:
+        
+        ["Data.Series"] Boxed series of arbitrary types.
+        
+        ["Data.Series.Unboxed"] Series of unboxed data types for better performance, at the cost of flexibility.
+        
+        ["Data.Series.Generic"] Generic interface to manipulate any type of 'Series'.
+        
+        ["Data.Series.Index"] Index containing series keys.
+        
+        To get started, please take a look at the tutorial ("Data.Series.Tutorial").
+        
+
+common common
+    default-language: GHC2021
+    ghc-options: -Wall
+                 -Wcompat
+                 -Widentities
+                 -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates
+                 -Wredundant-constraints
+                 -fhide-source-paths
+                 -Wpartial-fields
+
+library
+    import:           common
+    hs-source-dirs:   src
+    exposed-modules:  Data.Series
+                      Data.Series.Generic
+                      Data.Series.Generic.Internal
+                      Data.Series.Index
+                      Data.Series.Index.Internal
+                      Data.Series.Tutorial
+                      Data.Series.Unboxed
+    other-modules:    Data.Series.Generic.Aggregation
+                      Data.Series.Generic.Definition
+                      Data.Series.Generic.Scans
+                      Data.Series.Generic.View
+                      Data.Series.Generic.Zip
+                      Data.Series.Index.Definition
+    build-depends:    base                >=4.15.0.0 && <4.20,
+                      containers          >=0.6      && <0.8,
+                      deepseq             >=1.4      && <1.6,
+                      foldl               ^>=1.4,
+                      indexed-traversable ^>=0.1,
+                      vector              >=0.12.3.0 && <0.14,
+                      vector-algorithms   ^>=0.9
+
+test-suite javelin-test
+    import:           common
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    other-modules:    Test.Data.Series
+                      Test.Data.Series.Index
+                      Test.Data.Series.Generic.Aggregation
+                      Test.Data.Series.Generic.Definition
+                      Test.Data.Series.Generic.View
+                      Test.Data.Series.Generic.Zip
+    build-depends:    base,
+                      containers,
+                      foldl,
+                      hedgehog,
+                      HUnit,
+                      javelin,
+                      tasty,
+                      tasty-hedgehog,
+                      tasty-hspec,
+                      tasty-hunit,
+                      vector
+
+
+-- Running the 'comparison-containers' benchmark is expected
+-- to be done in conjunction with the cabal.project.profiling project file:
+-- > cabal bench comparison-containers --project=cabal.project.profiling
+benchmark comparison-containers
+    import:           common
+    type:             exitcode-stdio-1.0
+    ghc-options:      -rtsopts
+    hs-source-dirs:   benchmarks
+    main-is:          Comparison.hs
+    build-depends:    base,
+                      containers,
+                      foldl,
+                      mono-traversable,
+                      javelin,
+                      vector, 
+                      criterion, 
+                      deepseq, 
+                      random, 
+                      directory
+
+
+-- Running the 'operations' benchmark is expected
+-- to be done in conjunction with the cabal.project.profiling project file:
+-- > cabal bench operations --project=cabal.project.profiling
+benchmark operations
+    import:           common
+    type:             exitcode-stdio-1.0
+    ghc-options:      -rtsopts
+    hs-source-dirs:   benchmarks
+    main-is:          Operations.hs
+    build-depends:    base,
+                      containers,
+                      deepseq,
+                      foldl,
+                      javelin,
+                      criterion
+
+
+executable bench-report
+    import:           common
+    main-is:          bench-report.hs
+    hs-source-dirs:   scripts
+    build-depends:    base, 
+                      csv ^>=0.1
diff --git a/scripts/bench-report.hs b/scripts/bench-report.hs
--- a/scripts/bench-report.hs
+++ b/scripts/bench-report.hs
@@ -1,100 +1,100 @@
--- This script has been forked from:
--- https://github.com/haskell-perf/sets/blob/master/Report.hs
-{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
-module Main (main) where
-
-import Data.Function        ( on )
-import Data.List            ( groupBy, intercalate, nub )
-import System.Environment   ( getArgs )
-import Text.CSV             ( parseCSVFromFile )
-import Text.Printf          ( printf )
-
-main :: IO ()
-main = do
-  from:to:_ <- getArgs
-  reportFromCsv from to
-
-reportFromCsv :: FilePath -> FilePath -> IO ()
-reportFromCsv from to = do
-  result <- parseCSVFromFile from
-  case result of
-    Right (_:rows) -> do
-      writeFile to
-        (unlines
-           (map
-              format
-              (filter
-                 (not . all (all null))
-                 (groupBy (on (==) (takeWhile (/= '/') . concat . take 1)) rows))))
-    _ -> error "Couldn't parse csv"
-
-format :: [[String]] -> String
-format rows =
-  ("## " ++ takeWhile (/= '/') (concat (concat (take 1 (drop 1 rows))))) ++
-  "\n\n" ++
-  unlines
-    [ "|Name|" ++ intercalate "|" scales ++ "|"
-    , "|" ++ concat (replicate (1 + length scales) "---|")
-    ] ++
-  unlines
-    (map
-       (\name ->
-          "|" ++ name ++ "|" ++ intercalate "|" (valuesByName name) ++ "|")
-       names)
-  where
-    valuesByName name =
-      map
-        (\row@(_:avg:_) ->
-           let scale = rowScale row
-           in float (valuesByScale scale) (read avg))
-        (filter ((== name) . rowName) rows)
-    valuesByScale scale =
-      map (\(_:avg:_) -> read avg) (filter ((== scale) . rowScale) rows)
-    names = nub (map rowName rows)
-    scales = nub (map rowScale rows)
-    rowName row =
-      let s =
-            takeWhile
-              (/= ':')
-              (dropWhile (== '/') (dropWhile (/= '/') (concat (take 1 row))))
-      in s
-    rowScale row =
-      let scale = dropWhile (== ':') (dropWhile (/= ':') (concat (take 1 row)))
-      in scale
-
-float :: [Double] -> Double -> String
-float others x = let (scale, ext) = secs (mean others)
-                 in with (x * scale) ext
-
--- | Convert a number of seconds to a string.  The string will consist
--- of four decimal places, followed by a short description of the time
--- units.
-secs :: Double -> (Double, String)
-secs k
-    | k >= 1     = 1    `pair` "s"
-    | k >= 1e-3  = 1e3  `pair` "ms"
-    | k >= 1e-6  = 1e6  `pair` "μs"
-    | k >= 1e-9  = 1e9  `pair` "ns"
-    | k >= 1e-12 = 1e12 `pair` "ps"
-    | k >= 1e-15 = 1e15 `pair` "fs"
-    | k >= 1e-18 = 1e18 `pair` "as"
-    | otherwise = error "Bad scale"
-  where pair= (,)
-
-with :: Double -> String -> String
-with (t :: Double) (u :: String)
-    | t >= 1e9  = printf "%.4g %s" t u
-    | t >= 1e3  = printf "%.0f %s" t u
-    | t >= 1e2  = printf "%.1f %s" t u
-    | t >= 1e1  = printf "%.2f %s" t u
-    | otherwise = printf "%.3f %s" t u
-
--- | Simple rolling average.
-mean :: [Double] -> Double
-mean =
-    snd .
-    foldr
-        (\x (cnt,avg) ->
-              ( cnt + 1
-              , (x + avg * cnt) / (cnt + 1)))
+-- This script has been forked from:
+-- https://github.com/haskell-perf/sets/blob/master/Report.hs
+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
+module Main (main) where
+
+import Data.Function        ( on )
+import Data.List            ( groupBy, intercalate, nub )
+import System.Environment   ( getArgs )
+import Text.CSV             ( parseCSVFromFile )
+import Text.Printf          ( printf )
+
+main :: IO ()
+main = do
+  from:to:_ <- getArgs
+  reportFromCsv from to
+
+reportFromCsv :: FilePath -> FilePath -> IO ()
+reportFromCsv from to = do
+  result <- parseCSVFromFile from
+  case result of
+    Right (_:rows) -> do
+      writeFile to
+        (unlines
+           (map
+              format
+              (filter
+                 (not . all (all null))
+                 (groupBy (on (==) (takeWhile (/= '/') . concat . take 1)) rows))))
+    _ -> error "Couldn't parse csv"
+
+format :: [[String]] -> String
+format rows =
+  ("## " ++ takeWhile (/= '/') (concat (concat (take 1 (drop 1 rows))))) ++
+  "\n\n" ++
+  unlines
+    [ "|Name|" ++ intercalate "|" scales ++ "|"
+    , "|" ++ concat (replicate (1 + length scales) "---|")
+    ] ++
+  unlines
+    (map
+       (\name ->
+          "|" ++ name ++ "|" ++ intercalate "|" (valuesByName name) ++ "|")
+       names)
+  where
+    valuesByName name =
+      map
+        (\row@(_:avg:_) ->
+           let scale = rowScale row
+           in float (valuesByScale scale) (read avg))
+        (filter ((== name) . rowName) rows)
+    valuesByScale scale =
+      map (\(_:avg:_) -> read avg) (filter ((== scale) . rowScale) rows)
+    names = nub (map rowName rows)
+    scales = nub (map rowScale rows)
+    rowName row =
+      let s =
+            takeWhile
+              (/= ':')
+              (dropWhile (== '/') (dropWhile (/= '/') (concat (take 1 row))))
+      in s
+    rowScale row =
+      let scale = dropWhile (== ':') (dropWhile (/= ':') (concat (take 1 row)))
+      in scale
+
+float :: [Double] -> Double -> String
+float others x = let (scale, ext) = secs (mean others)
+                 in with (x * scale) ext
+
+-- | Convert a number of seconds to a string.  The string will consist
+-- of four decimal places, followed by a short description of the time
+-- units.
+secs :: Double -> (Double, String)
+secs k
+    | k >= 1     = 1    `pair` "s"
+    | k >= 1e-3  = 1e3  `pair` "ms"
+    | k >= 1e-6  = 1e6  `pair` "μs"
+    | k >= 1e-9  = 1e9  `pair` "ns"
+    | k >= 1e-12 = 1e12 `pair` "ps"
+    | k >= 1e-15 = 1e15 `pair` "fs"
+    | k >= 1e-18 = 1e18 `pair` "as"
+    | otherwise = error "Bad scale"
+  where pair= (,)
+
+with :: Double -> String -> String
+with (t :: Double) (u :: String)
+    | t >= 1e9  = printf "%.4g %s" t u
+    | t >= 1e3  = printf "%.0f %s" t u
+    | t >= 1e2  = printf "%.1f %s" t u
+    | t >= 1e1  = printf "%.2f %s" t u
+    | otherwise = printf "%.3f %s" t u
+
+-- | Simple rolling average.
+mean :: [Double] -> Double
+mean =
+    snd .
+    foldr
+        (\x (cnt,avg) ->
+              ( cnt + 1
+              , (x + avg * cnt) / (cnt + 1)))
         (0, 0)
diff --git a/src/Data/Series.hs b/src/Data/Series.hs
--- a/src/Data/Series.hs
+++ b/src/Data/Series.hs
@@ -1,1361 +1,1361 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Series
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT
--- Maintainer  :  laurent.decotret@outlook.com
--- Portability :  portable
---
--- This module contains data structures and functions to work with 'Series' capable of holding any Haskell value. 
--- For better performance, at the cost of less flexibility, see the "Data.Series.Unboxed".
---
--- = Introduction to series
---
--- A 'Series' of type @Series k a@ is a labeled array of values of type @a@,
--- indexed by keys of type @k@.
---
--- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:
---
---      * random access by key ( \(O(\log n)\) );
---      * slice by key ( \(O(\log n)\) ).
---
--- Like `Data.Vector.Vector`, they support efficient:
---
---      * random access by index ( \(O(1)\) );
---      * slice by index ( \(O(1)\) );
---      * numerical operations.
---
--- This module re-exports most of the content of "Data.Series.Generic", with type signatures 
--- specialized to the boxed container type `Data.Vector.Vector`.
---
--- For better performance (at the cost of more constraints), especially when it comes to numerical calculations, prefer to
--- use "Data.Series.Unboxed", which contains an implementation of series specialized to the unboxed container type `Data.Vector.Unboxed.Vector`.
- 
-module Data.Series (
-    Series, index, values,
-
-    -- * Building/converting 'Series'
-    singleton, fromIndex,
-    -- ** Lists
-    fromList, toList,
-    -- ** Vectors
-    fromVector, toVector,
-    -- ** Handling duplicates
-    Occurrence, fromListDuplicates, fromVectorDuplicates,
-    -- ** Strict Maps
-    fromStrictMap, toStrictMap,
-    -- ** Lazy Maps
-    fromLazyMap, toLazyMap,
-    -- ** Ad-hoc conversion with other data structures
-    IsSeries(..),
-    -- ** Conversion between 'Series' types
-    G.convert,
-
-    -- * Mapping and filtering
-    map, mapWithKey, mapIndex, concatMap,
-    take, takeWhile, drop, dropWhile, filter, filterWithKey,
-    -- ** Mapping with effects
-    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,
-
-    -- * Combining series
-    zipWith, zipWithMatched, zipWithKey,
-    zipWith3, zipWithMatched3, zipWithKey3,
-    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
-    zipWithMonoid, esum, eproduct, unzip, unzip3,
-
-    -- * Index manipulation
-    require, catMaybes, dropIndex,
-
-    -- * Accessors
-    -- ** Bulk access
-    select, selectWhere, Range, to, from, upto, Selection, 
-    -- ** Single-element access
-    at, iat,
-
-    -- * Replacing values
-    replace, (|->), (<-|),
-
-    -- * Scans
-    forwardFill,
-
-    -- * Grouping and windowing operations
-    groupBy, Grouping, aggregateWith, foldWith, 
-    windowing, expanding,
-
-    -- * Folds
-    fold, foldM, foldWithKey, foldMWithKey, foldMapWithKey,
-    -- ** Specialized folds
-    G.mean, G.variance, G.std,
-    length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, 
-    argmin, argmax,
-
-    -- * Scans
-    postscanl, prescanl,
-
-    -- * Displaying 'Series'
-    display, displayWith,
-    noLongerThan,
-    DisplayOptions(..), G.defaultDisplayOptions
-) where
-
-import           Control.Foldl       ( Fold, FoldM )
-import qualified Data.Map.Lazy       as ML
-import qualified Data.Map.Strict     as MS
-import           Data.Series.Index   ( Index )
-import           Data.Series.Generic ( IsSeries(..), Range, Selection, ZipStrategy, Occurrence, DisplayOptions(..)
-                                     , to, from, upto, skipStrategy, mapStrategy, constStrategy, noLongerThan
-                                     )
-import qualified Data.Series.Generic as G
-import           Data.Vector         ( Vector )
-
-import           Prelude             hiding ( map, concatMap, zipWith, zipWith3, filter, take, takeWhile, drop, dropWhile, last, unzip, unzip3
-                                            , length, null, all, any, and, or, sum, product, maximum, minimum, 
-                                            )
-
--- $setup
--- >>> import qualified Data.Series as Series
--- >>> import qualified Data.Series.Index as Index
-
-infixl 1 `select` 
-infix 6 |->, <-|
-
--- | A series is a labeled array of values of type @a@,
--- indexed by keys of type @k@.
---
--- Like @Data.Map@ and @Data.HashMap@, they support efficient:
---
---      * random access by key ( \(O(\log n)\) );
---      * slice by key ( \(O(\log n)\) ).
---
--- Like @Data.Vector.Vector@, they support efficient:
---
---      * random access by index ( \(O(1)\) );
---      * slice by index ( \(O(1)\) );
---      * numerical operations.
-type Series = G.Series Vector
-
-
-index :: Series k a -> Index k
-{-# INLINABLE index #-}
-index = G.index
-
-
-values :: Series k a -> Vector a
-{-# INLINABLE values #-}
-values = G.values
-
-
--- | Create a 'Series' with a single element.
-singleton :: k -> a -> Series k a
-{-# INLINABLE singleton #-}
-singleton = G.singleton
-
-
--- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
---
--- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']
--- index | values
--- ----- | ------
---   'a' |      0
---   'b' |      0
---   'c' |      0
---   'd' |      0
-fromIndex :: (k -> a) -> Index k -> Series k a
-{-# INLINABLE fromIndex #-}
-fromIndex = G.fromIndex
-
-
--- | Construct a series from a list of key-value pairs. There is no
--- condition on the order of pairs.
---
--- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]
--- >>> xs
--- index | values
--- ----- | ------
---   'a' |      5
---   'b' |      0
---   'd' |      1
---
--- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
-fromList :: Ord k => [(k, a)] -> Series k a
-{-# INLINABLE fromList #-}
-fromList = G.fromList
-
-
--- | Construct a series from a list of key-value pairs.
--- Contrary to `fromList`, values at duplicate keys are preserved. To keep each
--- key unique, an `Occurrence` number counts up.
---
--- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
--- >>> xs
---   index | values
---   ----- | ------
--- ('a',0) |      5
--- ('b',0) |      0
--- ('d',0) |      1
--- ('d',1) |     -4
--- ('d',2) |      7
-fromListDuplicates :: Ord k => [(k, a)] -> Series (k, Occurrence) a
-{-# INLINABLE fromListDuplicates #-}
-fromListDuplicates = G.fromListDuplicates
-
-
--- | Construct a list from key-value pairs. The elements are in order sorted by key:
---
--- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]
--- >>> xs
--- index | values
--- ----- | ------
---   'a' |      5
---   'b' |      0
---   'd' |      1
--- >>> toList xs
--- [('a',5),('b',0),('d',1)]
-toList :: Series k a -> [(k, a)]
-{-# INLINABLE toList #-}
-toList = G.toList
-
-
--- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
-toVector :: Series k a -> Vector (k, a)
-{-# INLINABLE toVector #-}
-toVector = G.toVector
-
-
--- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
--- condition on the order of pairs. Duplicate keys are silently dropped. If you
--- need to handle duplicate keys, see 'fromVectorDuplicates'.
---
--- Note that due to differences in sorting,
--- @'Series.fromList'@ and @'Series.fromVector' . 'Vector.fromList'@ 
--- may not be equivalent if the input list contains duplicate keys.
-fromVector :: Ord k => Vector (k, a) -> Series k a
-{-# INLINABLE fromVector #-}
-fromVector = G.fromVector
-
-
--- | Construct a series from a 'Vector' of key-value pairs.
--- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each
--- key unique, an 'Occurrence' number counts up.
---
--- >>> import qualified Data.Vector as Vector
--- >>> let xs = fromVectorDuplicates $ Vector.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
--- >>> xs
---   index | values
---   ----- | ------
--- ('a',0) |      5
--- ('b',0) |      0
--- ('d',0) |      1
--- ('d',1) |     -4
--- ('d',2) |      7
-fromVectorDuplicates :: Ord k => Vector (k, a) -> Series (k, Occurrence) a
-{-# INLINABLE fromVectorDuplicates #-}
-fromVectorDuplicates = G.fromVectorDuplicates
-
-
--- | Convert a series into a lazy @Map@.
-toLazyMap :: Series k a -> ML.Map k a
-{-# INLINABLE toLazyMap #-}
-toLazyMap = G.toLazyMap
-
-
--- | Construct a series from a lazy @Map@.
-fromLazyMap :: ML.Map k a -> Series k a
-{-# INLINABLE fromLazyMap #-}
-fromLazyMap = G.fromLazyMap
-
-
--- | Convert a series into a strict @Map@.
-toStrictMap :: Series k a -> MS.Map k a
-{-# INLINABLE toStrictMap #-}
-toStrictMap = G.toStrictMap
-
-
--- | Construct a series from a strict @Map@.
-fromStrictMap :: MS.Map k a -> Series k a
-{-# INLINABLE fromStrictMap #-}
-fromStrictMap = G.fromStrictMap
-
-
--- | \(O(n)\) Map every element of a 'Series'.
-map :: (a -> b) -> Series k a -> Series k b
-{-# INLINABLE map #-}
-map = G.map
-
-
--- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
-mapWithKey :: (k -> a -> b) -> Series k a -> Series k b
-{-# INLINABLE mapWithKey #-}
-mapWithKey = G.mapWithKey
-
-
--- | \(O(n \log n)\).
--- Map each key in the index to another value. Note that the resulting series
--- may have less elements, because each key must be unique.
---
--- In case new keys are conflicting, the first element is kept.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> import qualified Data.List
--- >>> xs `mapIndex` (Data.List.take 1)
--- index | values
--- ----- | ------
---   "L" |      4
---   "P" |      1
-mapIndex :: (Ord k, Ord g) => Series k a -> (k -> g) -> Series g a
-{-# INLINABLE mapIndex #-}
-mapIndex = G.mapIndex
-
-
--- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
-concatMap :: Ord k 
-          => (a -> Series k b) 
-          -> Series k a 
-          -> Series k b
-{-# INLINABLE concatMap #-}
-concatMap = G.concatMap
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, yielding a series of results.
-mapWithKeyM :: (Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)
-{-# INLINABLE mapWithKeyM #-}
-mapWithKeyM = G.mapWithKeyM
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, discarding the results.
-mapWithKeyM_ :: Monad m => (k -> a -> m b) -> Series k a -> m ()
-{-# INLINABLE mapWithKeyM_ #-}
-mapWithKeyM_ = G.mapWithKeyM_
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- yielding a series of results.
-forWithKeyM :: (Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)
-{-# INLINABLE forWithKeyM #-}
-forWithKeyM = G.forWithKeyM
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- discarding the results.
-forWithKeyM_ :: Monad m => Series k a -> (k -> a -> m b) -> m ()
-{-# INLINABLE forWithKeyM_ #-}
-forWithKeyM_ = G.forWithKeyM_
-
-
--- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. 
-traverseWithKey :: (Applicative t, Ord k)
-                => (k -> a -> t b) 
-                -> Series k a 
-                -> t (Series k b)
-{-# INLINABLE traverseWithKey #-}
-traverseWithKey = G.traverseWithKey
-
-
--- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
--- >>> take 2 xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-take :: Int -> Series k a -> Series k a
-{-# INLINABLE take #-}
-take = G.take
-
-
--- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
-
--- >>> takeWhile (>1) xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-takeWhile :: (a -> Bool) -> Series k a -> Series k a
-takeWhile = G.takeWhile
-
-
--- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
--- >>> drop 2 xs
---    index | values
---    ----- | ------
---  "Paris" |      1
--- "Vienna" |      5
-drop :: Int -> Series k a -> Series k a
-{-# INLINABLE drop #-}
-drop = G.drop
-
-
--- | \(O(n)\) Returns the complement of `takeWhile`.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
-
--- >>> dropWhile (>1) xs
---    index | values
---    ----- | ------
---  "Paris" |      1
--- "Vienna" |      5
-dropWhile :: (a -> Bool) -> Series k a -> Series k a
-dropWhile = G.dropWhile
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. For keys present only in the left or right series, 
--- the value 'Nothing' is returned.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWith (+) xs ys
---   index |  values
---   ----- |  ------
--- "alpha" | Just 10
---  "beta" | Just 12
--- "delta" | Nothing
--- "gamma" | Nothing
---
--- To only combine elements where keys are in both series, see 'zipWithMatched'.
-zipWith :: (Ord k) 
-        => (a -> b -> c) -> Series k a -> Series k b -> Series k (Maybe c)
-zipWith = G.zipWith 
-{-# INLINABLE zipWith #-}
-
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. For keys present only in the left or right series, 
--- the value 'Nothing' is returned.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
--- >>> zipWith3 (\x y z -> x + y + z) xs ys zs
---     index |  values
---     ----- |  ------
---   "alpha" | Just 30
---    "beta" | Nothing
---   "delta" | Nothing
--- "epsilon" | Nothing
---   "gamma" | Nothing
---
--- To only combine elements where keys are in all series, see 'zipWithMatched3'
-zipWith3 :: (Ord k) 
-         => (a -> b -> c -> d) 
-         -> Series k a 
-         -> Series k b 
-         -> Series k c 
-         -> Series k (Maybe d)
-{-# INLINABLE zipWith3 #-}
-zipWith3 = G.zipWith3
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWithMatched (+) xs ys
---   index | values
---   ----- | ------
--- "alpha" |     10
---  "beta" |     12
---
--- To combine elements where keys are in either series, see 'zipWith'.
-zipWithMatched :: Ord k => (a -> b -> c) -> Series k a -> Series k b -> Series k c
-{-# INLINABLE zipWithMatched #-}
-zipWithMatched = G.zipWithMatched
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys not present in all three series are dropped.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
---   index | values
---   ----- | ------
--- "alpha" |     30
-zipWithMatched3 :: (Ord k) 
-                => (a -> b -> c -> d) 
-                -> Series k a 
-                -> Series k b 
-                -> Series k c
-                -> Series k d
-{-# INLINABLE zipWithMatched3 #-}
-zipWithMatched3 = G.zipWithMatched3
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
---
--- To combine elements where keys are in either series, see 'zipWith'
-zipWithKey :: (Ord k) 
-           => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c
-{-# INLINABLE zipWithKey #-}
-zipWithKey = G.zipWithKey
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
---
--- To combine elements where keys are in any series, see 'zipWith3'
-zipWithKey3 :: (Ord k) 
-            => (k -> a -> b -> c -> d) 
-            -> Series k a 
-            -> Series k b 
-            -> Series k c
-            -> Series k d
-{-# INLINABLE zipWithKey3 #-}
-zipWithKey3 = G.zipWithKey3
-
-
--- | Zip two 'Series' with a combining function, applying a `ZipStrategy` when one key is present in one of the 'Series' but not both.
---
--- In the example below, we want to set the value to @-100@ (via @`constStrategy` (-100)@) for keys which are only present 
--- in the left 'Series', and drop keys (via `skipStrategy`) which are only present in the `right 'Series'  
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy  xs ys
---   index | values
---   ----- | ------
--- "alpha" |     10
---  "beta" |     12
--- "gamma" |   -100
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @`zipWithMatched` f@ 
--- than using @`zipWithStrategy` f skipStrategy skipStrategy@.
-zipWithStrategy :: (Ord k) 
-               => (a -> b -> c)     -- ^ Function to combine values when present in both series
-               -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
-               -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
-               -> Series k a
-               -> Series k b 
-               -> Series k c
-{-# INLINABLE zipWithStrategy #-}
-zipWithStrategy = G.zipWithStrategy
-
-
--- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
--- present in one of the 'Series' but not all of the others.
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
--- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
-zipWithStrategy3 :: (Ord k) 
-                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
-                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
-                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
-                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
-                -> Series k a
-                -> Series k b 
-                -> Series k c
-                -> Series k d
-{-# INLINABLE zipWithStrategy3 #-}
-zipWithStrategy3 = G.zipWithStrategy3
-
-
--- | Zip two 'Series' with a combining function. The value for keys which are missing from
--- either 'Series' is replaced with the appropriate `mempty` value.
---
--- >>> import Data.Monoid ( Sum(..) )
--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
--- >>> Series.zipWith (<>) xs ys
---        index |                  values
---        ----- |                  ------
--- "2023-01-01" | Just (Sum {getSum = 6})
--- "2023-01-02" |                 Nothing
--- "2023-01-03" |                 Nothing
--- >>> zipWithMonoid (<>) xs ys
---        index |           values
---        ----- |           ------
--- "2023-01-01" | Sum {getSum = 6}
--- "2023-01-02" | Sum {getSum = 2}
--- "2023-01-03" | Sum {getSum = 7}
-zipWithMonoid :: ( Monoid a, Monoid b, Ord k) 
-              => (a -> b -> c)
-              -> Series k a
-              -> Series k b 
-              -> Series k c
-zipWithMonoid = G.zipWithMonoid
-{-# INLINABLE zipWithMonoid #-}
-
-
--- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `esum` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |      6
--- "2023-01-02" |      2
--- "2023-01-03" |      7
-esum :: (Ord k, Num a) 
-     => Series k a 
-     -> Series k a
-     -> Series k a
-esum = G.esum
-{-# INLINABLE esum #-}
-
-
--- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `eproduct` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |     10
--- "2023-01-02" |      3
--- "2023-01-03" |      7
-eproduct :: (Ord k, Num a) 
-         => Series k a 
-         -> Series k a
-         -> Series k a
-eproduct = G.eproduct
-{-# INLINABLE eproduct #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 2-tuples.
-unzip :: Series k (a, b)
-      -> ( Series k a
-         , Series k b
-         )
-unzip = G.unzip
-{-# INLINABLE unzip #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 3-tuples.
-unzip3 :: Series k (a, b, c)
-       -> ( Series k a
-          , Series k b
-          , Series k c
-          )
-unzip3 = G.unzip3
-{-# INLINABLE unzip3 #-}
-
-
--- | Require a series to have a specific `Index`.
--- Contrary to @select@, all keys in the `Index` will be present in the resulting series.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> xs `require` Index.fromList ["Paris", "Lisbon", "Taipei"]
---    index |  values
---    ----- |  ------
--- "Lisbon" |  Just 4
---  "Paris" |  Just 1
--- "Taipei" | Nothing
-require :: Ord k => Series k a -> Index k -> Series k (Maybe a)
-{-# INLINABLE require #-}
-require = G.require 
-
-
--- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will
--- be indexed from 0.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> dropIndex xs
--- index | values
--- ----- | ------
---     0 |      4
---     1 |      2
---     2 |      1
-dropIndex :: Series k a -> Series Int a
-{-# INLINABLE dropIndex #-}
-dropIndex = G.dropIndex
-
-
--- | Filter elements. Only elements for which the predicate is @True@ are kept. 
--- Notice that the filtering is done on the values, not on the keys.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> filter (>2) xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
---
--- See also 'filterWithKey'.
-filter :: Ord k => (a -> Bool) -> Series k a -> Series k a
-{-# INLINABLE filter #-}
-filter = G.filter
-
-
--- | Filter elements, taking into account the corresponding key. Only elements for which 
--- the predicate is @True@ are kept. 
-filterWithKey :: Ord k 
-              => (k -> a -> Bool) 
-              -> Series k a 
-              -> Series k a
-{-# INLINABLE filterWithKey #-}
-filterWithKey = G.filterWithKey
-
-
--- | Drop elements which are not available (NA). 
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> let ys = xs `require` Index.fromList ["Paris", "London", "Lisbon", "Toronto"]
--- >>> ys
---     index |  values
---     ----- |  ------
---  "Lisbon" |  Just 4
---  "London" |  Just 2
---   "Paris" |  Just 1
--- "Toronto" | Nothing
--- >>> catMaybes ys
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
-catMaybes :: Ord k => Series k (Maybe a) -> Series k a
-{-# INLINABLE catMaybes #-}
-catMaybes = G.catMaybes
-
-
--- | Select a subseries. There are a few ways to do this.
---
--- The first way to do this is to select a sub-series based on random keys. For example,
--- selecting a subseries from an `Index`:
---
--- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
--- >>> xs `select` Index.fromList ['a', 'd']
--- index | values
--- ----- | ------
---   'a' |     10
---   'd' |     40
---
--- The second way to select a sub-series is to select all keys in a range:
---
--- >>> xs `select` 'b' `to` 'c'
--- index | values
--- ----- | ------
---   'b' |     20
---   'c' |     30
---
--- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not
--- in the series, it'll be ignored:
---
--- >>> xs `select` Index.fromList ['a', 'd', 'e']
--- index | values
--- ----- | ------
---   'a' |     10
---   'd' |     40
---
--- See `require` if you want to ensure that all keys are present.
-select :: (Selection s, Ord k) => Series k a -> s k -> Series k a
-select = G.select
-
-
--- | Select a sub-series from a series matching a condition.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> xs `selectWhere` (fmap (>1) xs)
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-selectWhere :: Ord k => Series k a -> Series k Bool -> Series k a
-{-# INLINABLE selectWhere #-}
-selectWhere = G.selectWhere
-
-
--- | \(O(\log n)\). Extract a single value from a series, by key.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs `at` "Paris"
--- Just 1
--- >>> xs `at` "Sydney"
--- Nothing
-at :: Ord k => Series k a -> k -> Maybe a
-{-# INLINABLE at #-}
-at = G.at
-
-
--- | \(O(1)\). Extract a single value from a series, by index.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> xs `iat` 0
--- Just 4
--- >>> xs `iat` 3
--- Nothing
-iat :: Series k a -> Int -> Maybe a
-{-# INLINABLE iat #-}
-iat = G.iat
-
-
--- | Replace values in the right series from values in the left series at matching keys.
--- Keys not in the right series are unaffected.
--- 
--- See `(|->)` and `(<-|)`, which might be more readable.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> ys `replace` xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-replace :: Ord k => Series k a -> Series k a -> Series k a
-{-# INLINABLE replace #-}
-replace = G.replace
-
-
--- | Replace values in the right series from values in the left series at matching keys.
--- Keys not in the right series are unaffected.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> ys |-> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-(|->) :: (Ord k) => Series k a -> Series k a -> Series k a
-{-# INLINABLE (|->) #-}
-(|->) = (G.|->)
-
-
--- | Replace values in the left series from values in the right series at matching keys.
--- Keys not in the left series are unaffected.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> xs <-| ys
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-(<-|) :: (Ord k) => Series k a -> Series k a -> Series k a
-{-# INLINABLE (<-|) #-}
-(<-|) = (G.<-|)
-
-
--- | \(O(n)\) Replace all instances of 'Nothing' with the last previous
--- value which was not 'Nothing'.
---
--- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)
--- >>> xs
--- index |  values
--- ----- |  ------
---     0 |  Just 1
---     1 |  Just 2
---     2 | Nothing
---     3 |  Just 3
--- >>> forwardFill 0 xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      2
---     3 |      3
---
--- If the first entry of the series is missing, the first input to 'forwardFill' will be used:
---
--- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)
--- >>> ys
--- index |  values
--- ----- |  ------
---     0 | Nothing
---     1 |  Just 2
---     2 | Nothing
---     3 |  Just 3
--- >>> forwardFill 0 ys
--- index | values
--- ----- | ------
---     0 |      0
---     1 |      2
---     2 |      2
---     3 |      3
-forwardFill :: a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.
-            -> Series v (Maybe a)
-            -> Series v a
-{-# INLINABLE forwardFill #-}
-forwardFill = G.forwardFill
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double
--- >>> xs
--- index | values
--- ----- | ------
---     0 |    1.0
---     1 |    2.0
---     2 |    3.0
---     3 |    4.0
--- >>> import Control.Foldl (variance)
--- >>> fold variance xs
--- 1.25
---
--- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
--- account while folding.
-fold :: Fold a b -> Series k a -> b
-fold = G.fold
-{-# INLINABLE fold #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
---
--- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
--- account while folding.
-foldM :: (Monad m) 
-      => FoldM m a b  
-      -> Series k a 
-      -> m b
-foldM = G.foldM
-{-# INLINABLE foldM #-}
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.
-foldWithKey :: Fold (k, a) b -> Series k a -> b
-foldWithKey = G.foldWithKey
-{-# INLINABLE foldWithKey #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
-foldMWithKey :: (Monad m) 
-             => FoldM m (k, a) b  
-             -> Series k a 
-             -> m b
-foldMWithKey = G.foldMWithKey
-{-# INLINABLE foldMWithKey #-}
-
-
--- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine
--- the results.
-foldMapWithKey :: Monoid m => (k -> a -> m) -> Series k a -> m
-{-# INLINABLE foldMapWithKey #-}
-foldMapWithKey = G.foldMapWithKey
-
-
--- | Group values in a 'Series' by some grouping function (@k -> g@).
--- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
---
--- This function is expected to be used in conjunction with 'aggregateWith':
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
-groupBy :: Series k a      -- ^ Grouping function
-        ->(k -> g)         -- ^ Input series
-        -> Grouping k g a  -- ^ Grouped series
-{-# INLINABLE groupBy #-}
-groupBy = G.groupBy
-
--- | Representation of a 'Series' being grouped.
-type Grouping k g a = G.Grouping k g Vector a
-
-
--- | Aggregate groups resulting from a call to 'groupBy':
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
---
--- If you want to aggregate groups using a binary function, see 'foldWith' which
--- may be much faster.
-aggregateWith :: (Ord g) 
-              => Grouping k g a 
-              -> (Series k a -> b) 
-              -> Series g b
-{-# INLINABLE aggregateWith #-}
-aggregateWith = G.aggregateWith
-
-
--- | Aggregate each group in a 'Grouping' using a binary function.
--- While this is not as expressive as 'aggregateWith', users looking for maximum
--- performance should use 'foldWith' as much as possible.
-foldWith :: Ord g 
-         => Grouping k g a
-         -> (a -> a -> a)
-         -> Series g a
-{-# INLINABLE foldWith #-}
-foldWith = G.foldWith
-
-
--- | Expanding window aggregation.
---
--- >>> import qualified Data.Series as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 3)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in (xs `expanding` sum) :: Series.Series Int Int 
--- :}
--- index | values
--- ----- | ------
---     1 |      0
---     2 |      1
---     3 |      3
---     4 |      6
---     5 |     10
---     6 |     15
-expanding :: Series k a        -- ^ Series vector
-          -> (Series k a -> b) -- ^ Aggregation function
-          -> Series k b        -- ^ Resulting vector
-{-# INLINABLE expanding #-}
-expanding = G.expanding
-
-
--- | General-purpose window aggregation.
---
--- >>> import qualified Data.Series as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 3)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in windowing (\k -> k `to` (k+2)) sum xs
--- :}
--- index | values
--- ----- | ------
---     1 |      3
---     2 |      6
---     3 |      9
---     4 |     12
---     5 |      9
---     6 |      5
-windowing :: Ord k
-          => (k -> Range k)
-          -> (Series k a -> b)
-          -> Series k a
-          -> Series k b
-{-# INLINABLE windowing #-}
-windowing = G.windowing
-
-
--- | \(O(1)\) Test whether a 'Series' is empty.
-null :: Series k a -> Bool
-{-# INLINABLE null #-}
-null = G.null
-
-
--- |\(O(1)\) Extract the length of a 'Series'.
-length :: Series k a -> Int
-{-# INLINABLE length #-}
-length = G.length
-
-
--- | \(O(n)\) Check if all elements satisfy the predicate.
-all :: (a -> Bool) -> Series k a -> Bool
-{-# INLINABLE all #-}
-all = G.all
-
-
--- | \(O(n)\) Check if any element satisfies the predicate.
-any :: (a -> Bool) -> Series k a -> Bool
-{-# INLINABLE any #-}
-any = G.any
-
-
--- | \(O(n)\) Check if all elements are 'True'.
-and :: Series k Bool -> Bool
-{-# INLINABLE and #-}
-and = G.and
-
-
--- | \(O(n)\) Check if any element is 'True'.
-or :: Series k Bool -> Bool
-{-# INLINABLE or #-}
-or = G.or
-
-
--- | \(O(n)\) Compute the sum of the elements.
-sum :: (Num a) => Series k a -> a
-{-# INLINABLE sum #-}
-sum = G.sum
-
-
--- | \(O(n)\) Compute the product of the elements.
-product :: (Num a) => Series k a -> a
-{-# INLINABLE product #-}
-product = G.product
-
-
--- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
---
--- See also 'argmax'.
-maximum :: (Ord a) => Series k a -> Maybe a
-{-# INLINABLE maximum #-}
-maximum = G.maximum
-
-
--- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
-maximumOn :: (Ord b) => (a -> b) -> Series k a -> Maybe a
-{-# INLINABLE maximumOn #-}
-maximumOn = G.maximumOn
-
-
--- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
---
--- See also 'argmin'.
-minimum :: (Ord a) => Series k a -> Maybe a
-{-# INLINABLE minimum #-}
-minimum = G.minimum
-
-
--- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
-minimumOn :: (Ord b) => (a -> b) -> Series k a -> Maybe a
-{-# INLINABLE minimumOn #-}
-minimumOn = G.minimumOn
-
-
--- | \(O(n)\) Find the index of the maximum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the maximum element is returned.
---
--- >>> :{ 
---     let (xs :: Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 7)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmax xs 
--- :}
--- Just 4
-argmax :: Ord a => Series k a -> Maybe k
-argmax = G.argmax
-{-# INLINABLE argmax #-}
-
-
--- | \(O(n)\) Find the index of the minimum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the minimum element is returned.
--- >>> :{ 
---     let (xs :: Series Int Int) 
---          = Series.fromList [ (1, 1)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 0)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmin xs 
--- :}
--- Just 4
-argmin :: Ord a => Series k a -> Maybe k
-argmin = G.argmin
-{-# INLINABLE argmin #-}
-
-
--- | \(O(n)\) Left-to-right postscan.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> postscanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      3
---     2 |      6
---     3 |     10
-postscanl :: (a -> b -> a) -> a -> Series k b -> Series k a
-{-# INLINABLE postscanl #-}
-postscanl = G.postscanl
-
-
--- | \(O(n)\) Left-to-right prescan.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> prescanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      0
---     1 |      1
---     2 |      3
---     3 |      6
-prescanl :: (a -> b -> a) -> a -> Series k b -> Series k a
-{-# INLINABLE prescanl #-}
-prescanl = G.prescanl
-
-
--- | Display a 'Series' using default 'DisplayOptions'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
--- >>> putStrLn $ display xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---   ... |    ...
---     4 |      5
---     5 |      6
---     6 |      7
-display :: (Show k, Show a) 
-        => Series k a 
-        -> String
-display = G.display
-
-
--- | Display a 'Series' using customizable 'DisplayOptions'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
--- >>> import Data.List (replicate)
--- >>> :{
---     let opts = DisplayOptions { maximumNumberOfRows  = 4
---                               , indexHeader = "keys"
---                               , valuesHeader = "vals"
---                               , keyDisplayFunction   = (\i -> replicate i 'x') `noLongerThan` 5
---                               , valueDisplayFunction = (\i -> replicate i 'o') 
---                               }
---      in putStrLn $ displayWith opts xs
--- :}
---   keys |    vals
---  ----- |  ------
---        |       o
---      x |      oo
---    ... |     ...
---  xxxxx |  oooooo
--- xxx... | ooooooo
-displayWith :: DisplayOptions k a
-            -> Series k a 
-            -> String
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Series
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains data structures and functions to work with 'Series' capable of holding any Haskell value. 
+-- For better performance, at the cost of less flexibility, see the "Data.Series.Unboxed".
+--
+-- = Introduction to series
+--
+-- A 'Series' of type @Series k a@ is a labeled array of values of type @a@,
+-- indexed by keys of type @k@.
+--
+-- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:
+--
+--      * random access by key ( \(O(\log n)\) );
+--      * slice by key ( \(O(\log n)\) ).
+--
+-- Like `Data.Vector.Vector`, they support efficient:
+--
+--      * random access by index ( \(O(1)\) );
+--      * slice by index ( \(O(1)\) );
+--      * numerical operations.
+--
+-- This module re-exports most of the content of "Data.Series.Generic", with type signatures 
+-- specialized to the boxed container type `Data.Vector.Vector`.
+--
+-- For better performance (at the cost of more constraints), especially when it comes to numerical calculations, prefer to
+-- use "Data.Series.Unboxed", which contains an implementation of series specialized to the unboxed container type `Data.Vector.Unboxed.Vector`.
+ 
+module Data.Series (
+    Series, index, values,
+
+    -- * Building/converting 'Series'
+    singleton, fromIndex,
+    -- ** Lists
+    fromList, toList,
+    -- ** Vectors
+    fromVector, toVector,
+    -- ** Handling duplicates
+    Occurrence, fromListDuplicates, fromVectorDuplicates,
+    -- ** Strict Maps
+    fromStrictMap, toStrictMap,
+    -- ** Lazy Maps
+    fromLazyMap, toLazyMap,
+    -- ** Ad-hoc conversion with other data structures
+    IsSeries(..),
+    -- ** Conversion between 'Series' types
+    G.convert,
+
+    -- * Mapping and filtering
+    map, mapWithKey, mapIndex, concatMap,
+    take, takeWhile, drop, dropWhile, filter, filterWithKey,
+    -- ** Mapping with effects
+    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,
+
+    -- * Combining series
+    zipWith, zipWithMatched, zipWithKey,
+    zipWith3, zipWithMatched3, zipWithKey3,
+    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
+    zipWithMonoid, esum, eproduct, unzip, unzip3,
+
+    -- * Index manipulation
+    require, catMaybes, dropIndex,
+
+    -- * Accessors
+    -- ** Bulk access
+    select, selectWhere, Range, to, from, upto, Selection, 
+    -- ** Single-element access
+    at, iat,
+
+    -- * Replacing values
+    replace, (|->), (<-|),
+
+    -- * Scans
+    forwardFill,
+
+    -- * Grouping and windowing operations
+    groupBy, Grouping, aggregateWith, foldWith, 
+    windowing, expanding,
+
+    -- * Folds
+    fold, foldM, foldWithKey, foldMWithKey, foldMapWithKey,
+    -- ** Specialized folds
+    G.mean, G.variance, G.std,
+    length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, 
+    argmin, argmax,
+
+    -- * Scans
+    postscanl, prescanl,
+
+    -- * Displaying 'Series'
+    display, displayWith,
+    noLongerThan,
+    DisplayOptions(..), G.defaultDisplayOptions
+) where
+
+import           Control.Foldl       ( Fold, FoldM )
+import qualified Data.Map.Lazy       as ML
+import qualified Data.Map.Strict     as MS
+import           Data.Series.Index   ( Index )
+import           Data.Series.Generic ( IsSeries(..), Range, Selection, ZipStrategy, Occurrence, DisplayOptions(..)
+                                     , to, from, upto, skipStrategy, mapStrategy, constStrategy, noLongerThan
+                                     )
+import qualified Data.Series.Generic as G
+import           Data.Vector         ( Vector )
+
+import           Prelude             hiding ( map, concatMap, zipWith, zipWith3, filter, take, takeWhile, drop, dropWhile, last, unzip, unzip3
+                                            , length, null, all, any, and, or, sum, product, maximum, minimum, 
+                                            )
+
+-- $setup
+-- >>> import qualified Data.Series as Series
+-- >>> import qualified Data.Series.Index as Index
+
+infixl 1 `select` 
+infix 6 |->, <-|
+
+-- | A series is a labeled array of values of type @a@,
+-- indexed by keys of type @k@.
+--
+-- Like @Data.Map@ and @Data.HashMap@, they support efficient:
+--
+--      * random access by key ( \(O(\log n)\) );
+--      * slice by key ( \(O(\log n)\) ).
+--
+-- Like @Data.Vector.Vector@, they support efficient:
+--
+--      * random access by index ( \(O(1)\) );
+--      * slice by index ( \(O(1)\) );
+--      * numerical operations.
+type Series = G.Series Vector
+
+
+index :: Series k a -> Index k
+{-# INLINABLE index #-}
+index = G.index
+
+
+values :: Series k a -> Vector a
+{-# INLINABLE values #-}
+values = G.values
+
+
+-- | Create a 'Series' with a single element.
+singleton :: k -> a -> Series k a
+{-# INLINABLE singleton #-}
+singleton = G.singleton
+
+
+-- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
+--
+-- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']
+-- index | values
+-- ----- | ------
+--   'a' |      0
+--   'b' |      0
+--   'c' |      0
+--   'd' |      0
+fromIndex :: (k -> a) -> Index k -> Series k a
+{-# INLINABLE fromIndex #-}
+fromIndex = G.fromIndex
+
+
+-- | Construct a series from a list of key-value pairs. There is no
+-- condition on the order of pairs.
+--
+-- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]
+-- >>> xs
+-- index | values
+-- ----- | ------
+--   'a' |      5
+--   'b' |      0
+--   'd' |      1
+--
+-- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
+fromList :: Ord k => [(k, a)] -> Series k a
+{-# INLINABLE fromList #-}
+fromList = G.fromList
+
+
+-- | Construct a series from a list of key-value pairs.
+-- Contrary to `fromList`, values at duplicate keys are preserved. To keep each
+-- key unique, an `Occurrence` number counts up.
+--
+-- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+-- >>> xs
+--   index | values
+--   ----- | ------
+-- ('a',0) |      5
+-- ('b',0) |      0
+-- ('d',0) |      1
+-- ('d',1) |     -4
+-- ('d',2) |      7
+fromListDuplicates :: Ord k => [(k, a)] -> Series (k, Occurrence) a
+{-# INLINABLE fromListDuplicates #-}
+fromListDuplicates = G.fromListDuplicates
+
+
+-- | Construct a list from key-value pairs. The elements are in order sorted by key:
+--
+-- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]
+-- >>> xs
+-- index | values
+-- ----- | ------
+--   'a' |      5
+--   'b' |      0
+--   'd' |      1
+-- >>> toList xs
+-- [('a',5),('b',0),('d',1)]
+toList :: Series k a -> [(k, a)]
+{-# INLINABLE toList #-}
+toList = G.toList
+
+
+-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
+toVector :: Series k a -> Vector (k, a)
+{-# INLINABLE toVector #-}
+toVector = G.toVector
+
+
+-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
+-- condition on the order of pairs. Duplicate keys are silently dropped. If you
+-- need to handle duplicate keys, see 'fromVectorDuplicates'.
+--
+-- Note that due to differences in sorting,
+-- @'Series.fromList'@ and @'Series.fromVector' . 'Vector.fromList'@ 
+-- may not be equivalent if the input list contains duplicate keys.
+fromVector :: Ord k => Vector (k, a) -> Series k a
+{-# INLINABLE fromVector #-}
+fromVector = G.fromVector
+
+
+-- | Construct a series from a 'Vector' of key-value pairs.
+-- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each
+-- key unique, an 'Occurrence' number counts up.
+--
+-- >>> import qualified Data.Vector as Vector
+-- >>> let xs = fromVectorDuplicates $ Vector.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+-- >>> xs
+--   index | values
+--   ----- | ------
+-- ('a',0) |      5
+-- ('b',0) |      0
+-- ('d',0) |      1
+-- ('d',1) |     -4
+-- ('d',2) |      7
+fromVectorDuplicates :: Ord k => Vector (k, a) -> Series (k, Occurrence) a
+{-# INLINABLE fromVectorDuplicates #-}
+fromVectorDuplicates = G.fromVectorDuplicates
+
+
+-- | Convert a series into a lazy @Map@.
+toLazyMap :: Series k a -> ML.Map k a
+{-# INLINABLE toLazyMap #-}
+toLazyMap = G.toLazyMap
+
+
+-- | Construct a series from a lazy @Map@.
+fromLazyMap :: ML.Map k a -> Series k a
+{-# INLINABLE fromLazyMap #-}
+fromLazyMap = G.fromLazyMap
+
+
+-- | Convert a series into a strict @Map@.
+toStrictMap :: Series k a -> MS.Map k a
+{-# INLINABLE toStrictMap #-}
+toStrictMap = G.toStrictMap
+
+
+-- | Construct a series from a strict @Map@.
+fromStrictMap :: MS.Map k a -> Series k a
+{-# INLINABLE fromStrictMap #-}
+fromStrictMap = G.fromStrictMap
+
+
+-- | \(O(n)\) Map every element of a 'Series'.
+map :: (a -> b) -> Series k a -> Series k b
+{-# INLINABLE map #-}
+map = G.map
+
+
+-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
+mapWithKey :: (k -> a -> b) -> Series k a -> Series k b
+{-# INLINABLE mapWithKey #-}
+mapWithKey = G.mapWithKey
+
+
+-- | \(O(n \log n)\).
+-- Map each key in the index to another value. Note that the resulting series
+-- may have less elements, because each key must be unique.
+--
+-- In case new keys are conflicting, the first element is kept.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> import qualified Data.List
+-- >>> xs `mapIndex` (Data.List.take 1)
+-- index | values
+-- ----- | ------
+--   "L" |      4
+--   "P" |      1
+mapIndex :: (Ord k, Ord g) => Series k a -> (k -> g) -> Series g a
+{-# INLINABLE mapIndex #-}
+mapIndex = G.mapIndex
+
+
+-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
+concatMap :: Ord k 
+          => (a -> Series k b) 
+          -> Series k a 
+          -> Series k b
+{-# INLINABLE concatMap #-}
+concatMap = G.concatMap
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, yielding a series of results.
+mapWithKeyM :: (Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)
+{-# INLINABLE mapWithKeyM #-}
+mapWithKeyM = G.mapWithKeyM
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, discarding the results.
+mapWithKeyM_ :: Monad m => (k -> a -> m b) -> Series k a -> m ()
+{-# INLINABLE mapWithKeyM_ #-}
+mapWithKeyM_ = G.mapWithKeyM_
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- yielding a series of results.
+forWithKeyM :: (Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)
+{-# INLINABLE forWithKeyM #-}
+forWithKeyM = G.forWithKeyM
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- discarding the results.
+forWithKeyM_ :: Monad m => Series k a -> (k -> a -> m b) -> m ()
+{-# INLINABLE forWithKeyM_ #-}
+forWithKeyM_ = G.forWithKeyM_
+
+
+-- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. 
+traverseWithKey :: (Applicative t, Ord k)
+                => (k -> a -> t b) 
+                -> Series k a 
+                -> t (Series k b)
+{-# INLINABLE traverseWithKey #-}
+traverseWithKey = G.traverseWithKey
+
+
+-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+-- >>> take 2 xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+take :: Int -> Series k a -> Series k a
+{-# INLINABLE take #-}
+take = G.take
+
+
+-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+
+-- >>> takeWhile (>1) xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+takeWhile :: (a -> Bool) -> Series k a -> Series k a
+takeWhile = G.takeWhile
+
+
+-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+-- >>> drop 2 xs
+--    index | values
+--    ----- | ------
+--  "Paris" |      1
+-- "Vienna" |      5
+drop :: Int -> Series k a -> Series k a
+{-# INLINABLE drop #-}
+drop = G.drop
+
+
+-- | \(O(n)\) Returns the complement of `takeWhile`.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+
+-- >>> dropWhile (>1) xs
+--    index | values
+--    ----- | ------
+--  "Paris" |      1
+-- "Vienna" |      5
+dropWhile :: (a -> Bool) -> Series k a -> Series k a
+dropWhile = G.dropWhile
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. For keys present only in the left or right series, 
+-- the value 'Nothing' is returned.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWith (+) xs ys
+--   index |  values
+--   ----- |  ------
+-- "alpha" | Just 10
+--  "beta" | Just 12
+-- "delta" | Nothing
+-- "gamma" | Nothing
+--
+-- To only combine elements where keys are in both series, see 'zipWithMatched'.
+zipWith :: (Ord k) 
+        => (a -> b -> c) -> Series k a -> Series k b -> Series k (Maybe c)
+zipWith = G.zipWith 
+{-# INLINABLE zipWith #-}
+
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. For keys present only in the left or right series, 
+-- the value 'Nothing' is returned.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
+-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
+-- >>> zipWith3 (\x y z -> x + y + z) xs ys zs
+--     index |  values
+--     ----- |  ------
+--   "alpha" | Just 30
+--    "beta" | Nothing
+--   "delta" | Nothing
+-- "epsilon" | Nothing
+--   "gamma" | Nothing
+--
+-- To only combine elements where keys are in all series, see 'zipWithMatched3'
+zipWith3 :: (Ord k) 
+         => (a -> b -> c -> d) 
+         -> Series k a 
+         -> Series k b 
+         -> Series k c 
+         -> Series k (Maybe d)
+{-# INLINABLE zipWith3 #-}
+zipWith3 = G.zipWith3
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWithMatched (+) xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     10
+--  "beta" |     12
+--
+-- To combine elements where keys are in either series, see 'zipWith'.
+zipWithMatched :: Ord k => (a -> b -> c) -> Series k a -> Series k b -> Series k c
+{-# INLINABLE zipWithMatched #-}
+zipWithMatched = G.zipWithMatched
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys not present in all three series are dropped.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
+-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
+-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
+--   index | values
+--   ----- | ------
+-- "alpha" |     30
+zipWithMatched3 :: (Ord k) 
+                => (a -> b -> c -> d) 
+                -> Series k a 
+                -> Series k b 
+                -> Series k c
+                -> Series k d
+{-# INLINABLE zipWithMatched3 #-}
+zipWithMatched3 = G.zipWithMatched3
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+--
+-- To combine elements where keys are in either series, see 'zipWith'
+zipWithKey :: (Ord k) 
+           => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c
+{-# INLINABLE zipWithKey #-}
+zipWithKey = G.zipWithKey
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+--
+-- To combine elements where keys are in any series, see 'zipWith3'
+zipWithKey3 :: (Ord k) 
+            => (k -> a -> b -> c -> d) 
+            -> Series k a 
+            -> Series k b 
+            -> Series k c
+            -> Series k d
+{-# INLINABLE zipWithKey3 #-}
+zipWithKey3 = G.zipWithKey3
+
+
+-- | Zip two 'Series' with a combining function, applying a `ZipStrategy` when one key is present in one of the 'Series' but not both.
+--
+-- In the example below, we want to set the value to @-100@ (via @`constStrategy` (-100)@) for keys which are only present 
+-- in the left 'Series', and drop keys (via `skipStrategy`) which are only present in the `right 'Series'  
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy  xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     10
+--  "beta" |     12
+-- "gamma" |   -100
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @`zipWithMatched` f@ 
+-- than using @`zipWithStrategy` f skipStrategy skipStrategy@.
+zipWithStrategy :: (Ord k) 
+               => (a -> b -> c)     -- ^ Function to combine values when present in both series
+               -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
+               -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
+               -> Series k a
+               -> Series k b 
+               -> Series k c
+{-# INLINABLE zipWithStrategy #-}
+zipWithStrategy = G.zipWithStrategy
+
+
+-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
+-- present in one of the 'Series' but not all of the others.
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
+-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
+zipWithStrategy3 :: (Ord k) 
+                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
+                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
+                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
+                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
+                -> Series k a
+                -> Series k b 
+                -> Series k c
+                -> Series k d
+{-# INLINABLE zipWithStrategy3 #-}
+zipWithStrategy3 = G.zipWithStrategy3
+
+
+-- | Zip two 'Series' with a combining function. The value for keys which are missing from
+-- either 'Series' is replaced with the appropriate `mempty` value.
+--
+-- >>> import Data.Monoid ( Sum(..) )
+-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
+-- >>> Series.zipWith (<>) xs ys
+--        index |                  values
+--        ----- |                  ------
+-- "2023-01-01" | Just (Sum {getSum = 6})
+-- "2023-01-02" |                 Nothing
+-- "2023-01-03" |                 Nothing
+-- >>> zipWithMonoid (<>) xs ys
+--        index |           values
+--        ----- |           ------
+-- "2023-01-01" | Sum {getSum = 6}
+-- "2023-01-02" | Sum {getSum = 2}
+-- "2023-01-03" | Sum {getSum = 7}
+zipWithMonoid :: ( Monoid a, Monoid b, Ord k) 
+              => (a -> b -> c)
+              -> Series k a
+              -> Series k b 
+              -> Series k c
+zipWithMonoid = G.zipWithMonoid
+{-# INLINABLE zipWithMonoid #-}
+
+
+-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `esum` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |      6
+-- "2023-01-02" |      2
+-- "2023-01-03" |      7
+esum :: (Ord k, Num a) 
+     => Series k a 
+     -> Series k a
+     -> Series k a
+esum = G.esum
+{-# INLINABLE esum #-}
+
+
+-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `eproduct` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |     10
+-- "2023-01-02" |      3
+-- "2023-01-03" |      7
+eproduct :: (Ord k, Num a) 
+         => Series k a 
+         -> Series k a
+         -> Series k a
+eproduct = G.eproduct
+{-# INLINABLE eproduct #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 2-tuples.
+unzip :: Series k (a, b)
+      -> ( Series k a
+         , Series k b
+         )
+unzip = G.unzip
+{-# INLINABLE unzip #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 3-tuples.
+unzip3 :: Series k (a, b, c)
+       -> ( Series k a
+          , Series k b
+          , Series k c
+          )
+unzip3 = G.unzip3
+{-# INLINABLE unzip3 #-}
+
+
+-- | Require a series to have a specific `Index`.
+-- Contrary to @select@, all keys in the `Index` will be present in the resulting series.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> xs `require` Index.fromList ["Paris", "Lisbon", "Taipei"]
+--    index |  values
+--    ----- |  ------
+-- "Lisbon" |  Just 4
+--  "Paris" |  Just 1
+-- "Taipei" | Nothing
+require :: Ord k => Series k a -> Index k -> Series k (Maybe a)
+{-# INLINABLE require #-}
+require = G.require 
+
+
+-- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will
+-- be indexed from 0.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> dropIndex xs
+-- index | values
+-- ----- | ------
+--     0 |      4
+--     1 |      2
+--     2 |      1
+dropIndex :: Series k a -> Series Int a
+{-# INLINABLE dropIndex #-}
+dropIndex = G.dropIndex
+
+
+-- | Filter elements. Only elements for which the predicate is @True@ are kept. 
+-- Notice that the filtering is done on the values, not on the keys.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> filter (>2) xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+--
+-- See also 'filterWithKey'.
+filter :: Ord k => (a -> Bool) -> Series k a -> Series k a
+{-# INLINABLE filter #-}
+filter = G.filter
+
+
+-- | Filter elements, taking into account the corresponding key. Only elements for which 
+-- the predicate is @True@ are kept. 
+filterWithKey :: Ord k 
+              => (k -> a -> Bool) 
+              -> Series k a 
+              -> Series k a
+{-# INLINABLE filterWithKey #-}
+filterWithKey = G.filterWithKey
+
+
+-- | Drop elements which are not available (NA). 
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> let ys = xs `require` Index.fromList ["Paris", "London", "Lisbon", "Toronto"]
+-- >>> ys
+--     index |  values
+--     ----- |  ------
+--  "Lisbon" |  Just 4
+--  "London" |  Just 2
+--   "Paris" |  Just 1
+-- "Toronto" | Nothing
+-- >>> catMaybes ys
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+catMaybes :: Ord k => Series k (Maybe a) -> Series k a
+{-# INLINABLE catMaybes #-}
+catMaybes = G.catMaybes
+
+
+-- | Select a subseries. There are a few ways to do this.
+--
+-- The first way to do this is to select a sub-series based on random keys. For example,
+-- selecting a subseries from an `Index`:
+--
+-- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
+-- >>> xs `select` Index.fromList ['a', 'd']
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'd' |     40
+--
+-- The second way to select a sub-series is to select all keys in a range:
+--
+-- >>> xs `select` 'b' `to` 'c'
+-- index | values
+-- ----- | ------
+--   'b' |     20
+--   'c' |     30
+--
+-- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not
+-- in the series, it'll be ignored:
+--
+-- >>> xs `select` Index.fromList ['a', 'd', 'e']
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'd' |     40
+--
+-- See `require` if you want to ensure that all keys are present.
+select :: (Selection s, Ord k) => Series k a -> s k -> Series k a
+select = G.select
+
+
+-- | Select a sub-series from a series matching a condition.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> xs `selectWhere` (fmap (>1) xs)
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+selectWhere :: Ord k => Series k a -> Series k Bool -> Series k a
+{-# INLINABLE selectWhere #-}
+selectWhere = G.selectWhere
+
+
+-- | \(O(\log n)\). Extract a single value from a series, by key.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs `at` "Paris"
+-- Just 1
+-- >>> xs `at` "Sydney"
+-- Nothing
+at :: Ord k => Series k a -> k -> Maybe a
+{-# INLINABLE at #-}
+at = G.at
+
+
+-- | \(O(1)\). Extract a single value from a series, by index.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> xs `iat` 0
+-- Just 4
+-- >>> xs `iat` 3
+-- Nothing
+iat :: Series k a -> Int -> Maybe a
+{-# INLINABLE iat #-}
+iat = G.iat
+
+
+-- | Replace values in the right series from values in the left series at matching keys.
+-- Keys not in the right series are unaffected.
+-- 
+-- See `(|->)` and `(<-|)`, which might be more readable.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> ys `replace` xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+replace :: Ord k => Series k a -> Series k a -> Series k a
+{-# INLINABLE replace #-}
+replace = G.replace
+
+
+-- | Replace values in the right series from values in the left series at matching keys.
+-- Keys not in the right series are unaffected.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> ys |-> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+(|->) :: (Ord k) => Series k a -> Series k a -> Series k a
+{-# INLINABLE (|->) #-}
+(|->) = (G.|->)
+
+
+-- | Replace values in the left series from values in the right series at matching keys.
+-- Keys not in the left series are unaffected.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> xs <-| ys
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+(<-|) :: (Ord k) => Series k a -> Series k a -> Series k a
+{-# INLINABLE (<-|) #-}
+(<-|) = (G.<-|)
+
+
+-- | \(O(n)\) Replace all instances of 'Nothing' with the last previous
+-- value which was not 'Nothing'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)
+-- >>> xs
+-- index |  values
+-- ----- |  ------
+--     0 |  Just 1
+--     1 |  Just 2
+--     2 | Nothing
+--     3 |  Just 3
+-- >>> forwardFill 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      2
+--     3 |      3
+--
+-- If the first entry of the series is missing, the first input to 'forwardFill' will be used:
+--
+-- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)
+-- >>> ys
+-- index |  values
+-- ----- |  ------
+--     0 | Nothing
+--     1 |  Just 2
+--     2 | Nothing
+--     3 |  Just 3
+-- >>> forwardFill 0 ys
+-- index | values
+-- ----- | ------
+--     0 |      0
+--     1 |      2
+--     2 |      2
+--     3 |      3
+forwardFill :: a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.
+            -> Series v (Maybe a)
+            -> Series v a
+{-# INLINABLE forwardFill #-}
+forwardFill = G.forwardFill
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |    1.0
+--     1 |    2.0
+--     2 |    3.0
+--     3 |    4.0
+-- >>> import Control.Foldl (variance)
+-- >>> fold variance xs
+-- 1.25
+--
+-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
+-- account while folding.
+fold :: Fold a b -> Series k a -> b
+fold = G.fold
+{-# INLINABLE fold #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
+--
+-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
+-- account while folding.
+foldM :: (Monad m) 
+      => FoldM m a b  
+      -> Series k a 
+      -> m b
+foldM = G.foldM
+{-# INLINABLE foldM #-}
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.
+foldWithKey :: Fold (k, a) b -> Series k a -> b
+foldWithKey = G.foldWithKey
+{-# INLINABLE foldWithKey #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
+foldMWithKey :: (Monad m) 
+             => FoldM m (k, a) b  
+             -> Series k a 
+             -> m b
+foldMWithKey = G.foldMWithKey
+{-# INLINABLE foldMWithKey #-}
+
+
+-- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine
+-- the results.
+foldMapWithKey :: Monoid m => (k -> a -> m) -> Series k a -> m
+{-# INLINABLE foldMapWithKey #-}
+foldMapWithKey = G.foldMapWithKey
+
+
+-- | Group values in a 'Series' by some grouping function (@k -> g@).
+-- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
+--
+-- This function is expected to be used in conjunction with 'aggregateWith':
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+groupBy :: Series k a      -- ^ Grouping function
+        ->(k -> g)         -- ^ Input series
+        -> Grouping k g a  -- ^ Grouped series
+{-# INLINABLE groupBy #-}
+groupBy = G.groupBy
+
+-- | Representation of a 'Series' being grouped.
+type Grouping k g a = G.Grouping k g Vector a
+
+
+-- | Aggregate groups resulting from a call to 'groupBy':
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+--
+-- If you want to aggregate groups using a binary function, see 'foldWith' which
+-- may be much faster.
+aggregateWith :: (Ord g) 
+              => Grouping k g a 
+              -> (Series k a -> b) 
+              -> Series g b
+{-# INLINABLE aggregateWith #-}
+aggregateWith = G.aggregateWith
+
+
+-- | Aggregate each group in a 'Grouping' using a binary function.
+-- While this is not as expressive as 'aggregateWith', users looking for maximum
+-- performance should use 'foldWith' as much as possible.
+foldWith :: Ord g 
+         => Grouping k g a
+         -> (a -> a -> a)
+         -> Series g a
+{-# INLINABLE foldWith #-}
+foldWith = G.foldWith
+
+
+-- | Expanding window aggregation.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 3)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in (xs `expanding` sum) :: Series.Series Int Int 
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      0
+--     2 |      1
+--     3 |      3
+--     4 |      6
+--     5 |     10
+--     6 |     15
+expanding :: Series k a        -- ^ Series vector
+          -> (Series k a -> b) -- ^ Aggregation function
+          -> Series k b        -- ^ Resulting vector
+{-# INLINABLE expanding #-}
+expanding = G.expanding
+
+
+-- | General-purpose window aggregation.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 3)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in windowing (\k -> k `to` (k+2)) sum xs
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      3
+--     2 |      6
+--     3 |      9
+--     4 |     12
+--     5 |      9
+--     6 |      5
+windowing :: Ord k
+          => (k -> Range k)
+          -> (Series k a -> b)
+          -> Series k a
+          -> Series k b
+{-# INLINABLE windowing #-}
+windowing = G.windowing
+
+
+-- | \(O(1)\) Test whether a 'Series' is empty.
+null :: Series k a -> Bool
+{-# INLINABLE null #-}
+null = G.null
+
+
+-- |\(O(1)\) Extract the length of a 'Series'.
+length :: Series k a -> Int
+{-# INLINABLE length #-}
+length = G.length
+
+
+-- | \(O(n)\) Check if all elements satisfy the predicate.
+all :: (a -> Bool) -> Series k a -> Bool
+{-# INLINABLE all #-}
+all = G.all
+
+
+-- | \(O(n)\) Check if any element satisfies the predicate.
+any :: (a -> Bool) -> Series k a -> Bool
+{-# INLINABLE any #-}
+any = G.any
+
+
+-- | \(O(n)\) Check if all elements are 'True'.
+and :: Series k Bool -> Bool
+{-# INLINABLE and #-}
+and = G.and
+
+
+-- | \(O(n)\) Check if any element is 'True'.
+or :: Series k Bool -> Bool
+{-# INLINABLE or #-}
+or = G.or
+
+
+-- | \(O(n)\) Compute the sum of the elements.
+sum :: (Num a) => Series k a -> a
+{-# INLINABLE sum #-}
+sum = G.sum
+
+
+-- | \(O(n)\) Compute the product of the elements.
+product :: (Num a) => Series k a -> a
+{-# INLINABLE product #-}
+product = G.product
+
+
+-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+--
+-- See also 'argmax'.
+maximum :: (Ord a) => Series k a -> Maybe a
+{-# INLINABLE maximum #-}
+maximum = G.maximum
+
+
+-- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
+maximumOn :: (Ord b) => (a -> b) -> Series k a -> Maybe a
+{-# INLINABLE maximumOn #-}
+maximumOn = G.maximumOn
+
+
+-- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+--
+-- See also 'argmin'.
+minimum :: (Ord a) => Series k a -> Maybe a
+{-# INLINABLE minimum #-}
+minimum = G.minimum
+
+
+-- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
+minimumOn :: (Ord b) => (a -> b) -> Series k a -> Maybe a
+{-# INLINABLE minimumOn #-}
+minimumOn = G.minimumOn
+
+
+-- | \(O(n)\) Find the index of the maximum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the maximum element is returned.
+--
+-- >>> :{ 
+--     let (xs :: Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 7)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmax xs 
+-- :}
+-- Just 4
+argmax :: Ord a => Series k a -> Maybe k
+argmax = G.argmax
+{-# INLINABLE argmax #-}
+
+
+-- | \(O(n)\) Find the index of the minimum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the minimum element is returned.
+-- >>> :{ 
+--     let (xs :: Series Int Int) 
+--          = Series.fromList [ (1, 1)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 0)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmin xs 
+-- :}
+-- Just 4
+argmin :: Ord a => Series k a -> Maybe k
+argmin = G.argmin
+{-# INLINABLE argmin #-}
+
+
+-- | \(O(n)\) Left-to-right postscan.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> postscanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      3
+--     2 |      6
+--     3 |     10
+postscanl :: (a -> b -> a) -> a -> Series k b -> Series k a
+{-# INLINABLE postscanl #-}
+postscanl = G.postscanl
+
+
+-- | \(O(n)\) Left-to-right prescan.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> prescanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      0
+--     1 |      1
+--     2 |      3
+--     3 |      6
+prescanl :: (a -> b -> a) -> a -> Series k b -> Series k a
+{-# INLINABLE prescanl #-}
+prescanl = G.prescanl
+
+
+-- | Display a 'Series' using default 'DisplayOptions'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
+-- >>> putStrLn $ display xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--   ... |    ...
+--     4 |      5
+--     5 |      6
+--     6 |      7
+display :: (Show k, Show a) 
+        => Series k a 
+        -> String
+display = G.display
+
+
+-- | Display a 'Series' using customizable 'DisplayOptions'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
+-- >>> import Data.List (replicate)
+-- >>> :{
+--     let opts = DisplayOptions { maximumNumberOfRows  = 4
+--                               , indexHeader = "keys"
+--                               , valuesHeader = "vals"
+--                               , keyDisplayFunction   = (\i -> replicate i 'x') `noLongerThan` 5
+--                               , valueDisplayFunction = (\i -> replicate i 'o') 
+--                               }
+--      in putStrLn $ displayWith opts xs
+-- :}
+--   keys |    vals
+--  ----- |  ------
+--        |       o
+--      x |      oo
+--    ... |     ...
+--  xxxxx |  oooooo
+-- xxx... | ooooooo
+displayWith :: DisplayOptions k a
+            -> Series k a 
+            -> String
 displayWith = G.displayWith
diff --git a/src/Data/Series/Generic.hs b/src/Data/Series/Generic.hs
--- a/src/Data/Series/Generic.hs
+++ b/src/Data/Series/Generic.hs
@@ -1,98 +1,98 @@
-{-# LANGUAGE NoImplicitPrelude #-}
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Series.Generic
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT
--- Maintainer  :  laurent.decotret@outlook.com
--- Portability :  portable
---
--- This module contains data structures and functions to work with any type of 'Series', 
--- including boxed and unboxed types.
---
--- Use the definitions in this module if you want to support all types of 'Series' at once.
-module Data.Series.Generic (
-    -- * Definition
-    Series(index, values),
-    convert,
-
-    -- * Building/converting 'Series'
-    singleton, fromIndex,
-    -- ** Lists
-    fromList, toList,
-    -- ** Vectors
-    fromVector, toVector,
-    -- ** Handling duplicates
-    Occurrence, fromListDuplicates, fromVectorDuplicates,
-    -- ** Strict Maps
-    fromStrictMap, toStrictMap,
-    -- ** Lazy Maps
-    fromLazyMap, toLazyMap,
-    -- ** Ad-hoc conversion with other data structures
-    IsSeries(..),
-
-    -- * Mapping and filtering
-    map, mapWithKey, mapIndex, concatMap, filter, filterWithKey, 
-    take, takeWhile, drop, dropWhile,
-    -- ** Mapping with effects
-    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,
-
-    -- * Folding
-    fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMapWithKey,
-    -- ** Specialized folds
-    mean, variance, std, 
-    length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
-    argmax, argmin,
-
-    -- * Scans
-    postscanl, prescanl, forwardFill,
-
-    -- * Combining series
-    zipWith, zipWithMatched, zipWithKey,
-    zipWith3, zipWithMatched3, zipWithKey3,
-    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
-    zipWithMonoid, esum, eproduct, unzip, unzip3,
-
-    -- * Index manipulation
-    require, requireWith, catMaybes, dropIndex,
-
-    -- * Accessors
-    -- ** Bulk access
-    select, selectWhere, Range, to, from, upto, Selection, 
-    -- ** Single-element access
-    at, iat,
-
-    -- * Replacement
-    replace, (|->), (<-|),
-
-    -- * Grouping and windowing operations
-    groupBy, Grouping, aggregateWith, foldWith, 
-    windowing, expanding,
-
-    -- * Displaying 'Series'
-    display, displayWith,
-    noLongerThan,
-    DisplayOptions(..), defaultDisplayOptions
-) where
-
-import Control.Foldl                    ( mean, variance, std )
-import Data.Series.Generic.Aggregation  ( groupBy, Grouping, aggregateWith, foldWith
-                                        , windowing, expanding, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn
-                                        , argmax, argmin,
-                                        )
-import Data.Series.Generic.Definition   ( Series(index, values), IsSeries(..), Occurrence, convert, singleton, fromIndex, fromStrictMap
-                                        , toStrictMap, fromLazyMap, toLazyMap, fromList, fromListDuplicates, toList
-                                        , fromVector, fromVectorDuplicates, toVector
-                                        , map, mapWithKey, mapIndex, concatMap, length, null, take, takeWhile, drop, dropWhile
-                                        , mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, fold, foldM
-                                        , foldWithKey, foldMWithKey, foldMap, foldMapWithKey
-                                        , display, displayWith, noLongerThan, DisplayOptions(..), defaultDisplayOptions
-                                        )
-import Data.Series.Generic.Scans        ( postscanl, prescanl, forwardFill )
-import Data.Series.Generic.View         ( Range, Selection, at, iat, select, selectWhere, to, from, upto, filter, filterWithKey, require, requireWith
-                                        , catMaybes, dropIndex,
-                                        )
-import Data.Series.Generic.Zip          ( zipWith, zipWithMatched, zipWithKey, zipWith3, zipWithMatched3, zipWithKey3, replace
-                                        , (|->), (<-|), zipWithStrategy, zipWithStrategy3, ZipStrategy, skipStrategy, mapStrategy, constStrategy
-                                        , zipWithMonoid, esum, eproduct, unzip, unzip3
-                                        )
+{-# LANGUAGE NoImplicitPrelude #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Series.Generic
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains data structures and functions to work with any type of 'Series', 
+-- including boxed and unboxed types.
+--
+-- Use the definitions in this module if you want to support all types of 'Series' at once.
+module Data.Series.Generic (
+    -- * Definition
+    Series(index, values),
+    convert,
+
+    -- * Building/converting 'Series'
+    singleton, fromIndex,
+    -- ** Lists
+    fromList, toList,
+    -- ** Vectors
+    fromVector, toVector,
+    -- ** Handling duplicates
+    Occurrence, fromListDuplicates, fromVectorDuplicates,
+    -- ** Strict Maps
+    fromStrictMap, toStrictMap,
+    -- ** Lazy Maps
+    fromLazyMap, toLazyMap,
+    -- ** Ad-hoc conversion with other data structures
+    IsSeries(..),
+
+    -- * Mapping and filtering
+    map, mapWithKey, mapIndex, concatMap, filter, filterWithKey, 
+    take, takeWhile, drop, dropWhile,
+    -- ** Mapping with effects
+    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,
+
+    -- * Folding
+    fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMapWithKey,
+    -- ** Specialized folds
+    mean, variance, std, 
+    length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
+    argmax, argmin,
+
+    -- * Scans
+    postscanl, prescanl, forwardFill,
+
+    -- * Combining series
+    zipWith, zipWithMatched, zipWithKey,
+    zipWith3, zipWithMatched3, zipWithKey3,
+    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
+    zipWithMonoid, esum, eproduct, unzip, unzip3,
+
+    -- * Index manipulation
+    require, requireWith, catMaybes, dropIndex,
+
+    -- * Accessors
+    -- ** Bulk access
+    select, selectWhere, Range, to, from, upto, Selection, 
+    -- ** Single-element access
+    at, iat,
+
+    -- * Replacement
+    replace, (|->), (<-|),
+
+    -- * Grouping and windowing operations
+    groupBy, Grouping, aggregateWith, foldWith, 
+    windowing, expanding,
+
+    -- * Displaying 'Series'
+    display, displayWith,
+    noLongerThan,
+    DisplayOptions(..), defaultDisplayOptions
+) where
+
+import Control.Foldl                    ( mean, variance, std )
+import Data.Series.Generic.Aggregation  ( groupBy, Grouping, aggregateWith, foldWith
+                                        , windowing, expanding, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn
+                                        , argmax, argmin,
+                                        )
+import Data.Series.Generic.Definition   ( Series(index, values), IsSeries(..), Occurrence, convert, singleton, fromIndex, fromStrictMap
+                                        , toStrictMap, fromLazyMap, toLazyMap, fromList, fromListDuplicates, toList
+                                        , fromVector, fromVectorDuplicates, toVector
+                                        , map, mapWithKey, mapIndex, concatMap, length, null, take, takeWhile, drop, dropWhile
+                                        , mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, fold, foldM
+                                        , foldWithKey, foldMWithKey, foldMap, foldMapWithKey
+                                        , display, displayWith, noLongerThan, DisplayOptions(..), defaultDisplayOptions
+                                        )
+import Data.Series.Generic.Scans        ( postscanl, prescanl, forwardFill )
+import Data.Series.Generic.View         ( Range, Selection, at, iat, select, selectWhere, to, from, upto, filter, filterWithKey, require, requireWith
+                                        , catMaybes, dropIndex,
+                                        )
+import Data.Series.Generic.Zip          ( zipWith, zipWithMatched, zipWithKey, zipWith3, zipWithMatched3, zipWithKey3, replace
+                                        , (|->), (<-|), zipWithStrategy, zipWithStrategy3, ZipStrategy, skipStrategy, mapStrategy, constStrategy
+                                        , zipWithMonoid, esum, eproduct, unzip, unzip3
+                                        )
diff --git a/src/Data/Series/Generic/Aggregation.hs b/src/Data/Series/Generic/Aggregation.hs
--- a/src/Data/Series/Generic/Aggregation.hs
+++ b/src/Data/Series/Generic/Aggregation.hs
@@ -1,326 +1,332 @@
-module Data.Series.Generic.Aggregation ( 
-    -- * Grouping
-    Grouping,
-    groupBy,
-    aggregateWith,
-    foldWith,
-
-    -- * Windowing
-    expanding,
-    windowing,
-
-    -- * Folding
-    all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
-    argmax, argmin,
-) where
-
-import qualified Data.List 
-import qualified Data.Map.Strict                as Map
-import           Data.Ord                       ( Down(..) )
-import           Data.Series.Generic.Definition ( Series(..) )
-import qualified Data.Series.Generic.Definition as GSeries
-import           Data.Series.Generic.View       ( Range, slice, select )
-import qualified Data.Vector                    as Boxed
-import           Data.Vector.Generic            ( Vector )
-import qualified Data.Vector.Generic            as Vector
-import           Prelude                        hiding ( last, null, length, all, any, and, or, sum, product, maximum, minimum )
-
--- $setup
--- >>> import qualified Data.Series as Series
--- >>> import qualified Data.Set as Set
-
--- | Group values in a 'Series' by some grouping function (@k -> g@).
--- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
---
--- This function is expected to be used in conjunction with @aggregate@:
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
-groupBy :: Series v k a       -- ^ Input series
-        -> (k -> g)           -- ^ Grouping function
-        -> Grouping k g v a   -- ^ Grouped series
-{-# INLINABLE groupBy #-}
-groupBy = MkGrouping
-
-
--- | Representation of a 'Series' being grouped.
-data Grouping k g v a 
-    = MkGrouping (Series v k a)  (k -> g)
-
-
--- | Aggregate groups resulting from a call to 'groupBy':
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
---
--- If you want to aggregate groups using a binary function, see 'foldWith' which
--- may be much faster.
-aggregateWith :: (Ord g, Vector v a, Vector v b) 
-              => Grouping k g v a 
-              -> (Series v k a -> b) 
-              -> Series v g b
-{-# INLINABLE aggregateWith #-}
-aggregateWith (MkGrouping xs by) f
-    = GSeries.fromStrictMap 
-    $ fmap (f . GSeries.fromDistinctAscList)
-    -- We're using a list fold to limit the number of 
-    -- type constraints. This is about as fast as it is 
-    -- with a Vector fold
-    $ Data.List.foldl' acc mempty 
-    $ GSeries.toList xs
-    where
-        acc !m (key, val) = Map.insertWith (<>) (by key) (Data.List.singleton (key, val)) m
-
-
--- | Fold over each group in a 'Grouping' using a binary function.
--- While this is not as expressive as 'aggregateWith', users looking for maximum
--- performance should use 'foldWith' as much as possible.
---
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `foldWith` min
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
-foldWith :: (Ord g, Vector v a) 
-         => Grouping k g v a
-         -> (a -> a -> a)
-         -> Series v g a
-{-# INLINABLE foldWith #-}
-foldWith (MkGrouping xs by) f 
-    = GSeries.fromStrictMap 
-    -- We're using a list fold to limit the number of 
-    -- type constraints. This is about as fast as it is 
-    -- with a Vector fold
-    $ Data.List.foldl' acc mempty 
-    $ GSeries.toList xs
-    where
-        acc !m (key, val) = Map.insertWith f (by key) val m
-
-
--- | Expanding window aggregation.
---
--- >>> import qualified Data.Series as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 3)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in (xs `expanding` sum) :: Series.Series Int Int 
--- :}
--- index | values
--- ----- | ------
---     1 |      0
---     2 |      1
---     3 |      3
---     4 |      6
---     5 |     10
---     6 |     15
-expanding :: (Vector v a, Vector v b) 
-          => Series v k a        -- ^ Series vector
-          -> (Series v k a -> b) -- ^ Aggregation function
-          -> Series v k b        -- ^ Resulting vector
-{-# INLINABLE expanding #-}
-expanding vs f = MkSeries (index vs) $ Vector.unfoldrExactN (GSeries.length vs) go 0
-    where
-        -- Recall that `slice` does NOT include the right index
-        go ix = (f $ slice 0 (ix + 1) vs, ix + 1)
-
-
--- | General-purpose window aggregation.
---
--- >>> import qualified Data.Series as Series 
--- >>> import           Data.Series ( to )
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 3)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in windowing (\k -> k `to` (k + 2)) sum xs
--- :}
--- index | values
--- ----- | ------
---     1 |      3
---     2 |      6
---     3 |      9
---     4 |     12
---     5 |      9
---     6 |      5
-windowing :: (Ord k, Vector v a, Vector v b)
-          => (k -> Range k)
-          -> (Series v k a -> b)
-          -> Series v k a
-          -> Series v k b
-{-# INLINABLE windowing #-}
-windowing range agg series 
-    = GSeries.mapWithKey (\k _ -> agg $ series `select` range k) series
-
-
--- | \(O(n)\) Check if all elements satisfy the predicate.
-all :: Vector v a => (a -> Bool) -> Series v k a -> Bool
-{-# INLINABLE all #-}
-all f = Vector.all f . values
-
-
--- | \(O(n)\) Check if any element satisfies the predicate.
-any :: Vector v a => (a -> Bool) -> Series v k a -> Bool
-{-# INLINABLE any #-}
-any f = Vector.any f . values
-
-
--- | \(O(n)\) Check if all elements are 'True'.
-and :: Vector v Bool => Series v k Bool -> Bool
-{-# INLINABLE and #-}
-and = Vector.and . values
-
-
--- | \(O(n)\) Check if any element is 'True'.
-or :: Vector v Bool => Series v k Bool -> Bool
-{-# INLINABLE or #-}
-or = Vector.or . values
-
-
--- | \(O(n)\) Compute the sum of the elements.
-sum :: (Num a, Vector v a) => Series v k a -> a
-{-# INLINABLE sum #-}
-sum = Vector.sum . values
-
-
--- | \(O(n)\) Compute the product of the elements.
-product :: (Num a, Vector v a) => Series v k a -> a
-{-# INLINABLE product #-}
-product = Vector.product . values
-
-
-nothingIfEmpty :: Vector v a 
-               => (Series v k a -> b) -> (Series v k a -> Maybe b)
-nothingIfEmpty f xs = if GSeries.null xs then Nothing else Just (f xs) 
-
-
--- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
-maximum :: (Ord a, Vector v a) => Series v k a -> Maybe a
-{-# INLINABLE maximum #-}
-maximum = nothingIfEmpty $ Vector.maximum . values
-
-
--- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
-maximumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
-{-# INLINABLE maximumOn #-}
-maximumOn f = nothingIfEmpty $ Vector.maximumOn f . values
-
-
--- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
-minimum :: (Ord a, Vector v a) => Series v k a -> Maybe a
-{-# INLINABLE minimum #-}
-minimum = nothingIfEmpty $ Vector.minimum . values
-
-
--- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
-minimumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
-{-# INLINABLE minimumOn #-}
-minimumOn f = nothingIfEmpty $ Vector.minimumOn f . values
-
-
--- | \(O(n)\) Find the index of the maximum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the maximum element is returned.
---
--- >>> import qualified Data.Series as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 7)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmax xs 
--- :}
--- Just 4
-argmax :: (Ord a, Vector v a)
-       => Series v k a
-       -> Maybe k
-{-# INLINABLE argmax #-}
-argmax xs | GSeries.null xs = Nothing
-          | otherwise = Just 
-                      . fst 
-                      -- We're forcing the use of boxed vectors in order to
-                      -- reduce the constraints on the vector instance
-                      . Boxed.maximumOn snd 
-                      . GSeries.toVector
-                      . GSeries.convert
-                      $ xs
-
-
--- | \(O(n)\) Find the index of the minimum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the minimum element is returned.
---
--- >>> import qualified Data.Series as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 1)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 0)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmin xs 
--- :}
--- Just 4
-argmin :: (Ord a, Vector v a, Vector v (Down a))
-       => Series v k a
-       -> Maybe k
-{-# INLINABLE argmin #-}
+module Data.Series.Generic.Aggregation ( 
+    -- * Grouping
+    Grouping,
+    groupBy,
+    aggregateWith,
+    foldWith,
+
+    -- * Windowing
+    expanding,
+    windowing,
+
+    -- * Folding
+    all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
+    argmax, argmin,
+) where
+
+import qualified Data.List 
+import qualified Data.Map.Strict                as Map
+import           Data.Ord                       ( Down(..) )
+import           Data.Series.Generic.Definition ( Series(..) )
+import qualified Data.Series.Generic.Definition as GSeries
+import           Data.Series.Generic.View       ( Range, slice, select )
+import qualified Data.Vector                    as Boxed
+import           Data.Vector.Generic            ( Vector )
+import qualified Data.Vector.Generic            as Vector
+import           Prelude                        hiding ( last, null, length, all, any, and, or, sum, product, maximum, minimum )
+
+-- $setup
+-- >>> import qualified Data.Series as Series
+-- >>> import qualified Data.Set as Set
+
+-- | Group values in a 'Series' by some grouping function (@k -> g@).
+-- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
+--
+-- This function is expected to be used in conjunction with @aggregate@:
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+groupBy :: Series v k a       -- ^ Input series
+        -> (k -> g)           -- ^ Grouping function
+        -> Grouping k g v a   -- ^ Grouped series
+{-# INLINABLE groupBy #-}
+groupBy = MkGrouping
+
+
+-- | Representation of a 'Series' being grouped.
+data Grouping k g v a 
+    = MkGrouping (Series v k a)  (k -> g)
+
+
+-- | Aggregate groups resulting from a call to 'groupBy':
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+--
+-- If you want to aggregate groups using a binary function, see 'foldWith' which
+-- may be much faster.
+aggregateWith :: (Ord g, Vector v a, Vector v b) 
+              => Grouping k g v a 
+              -> (Series v k a -> b) 
+              -> Series v g b
+{-# INLINABLE aggregateWith #-}
+aggregateWith (MkGrouping xs by) f
+    = GSeries.fromStrictMap 
+    -- Using `fromDistinctAscList` is predicated on a particular structure
+    -- created by the `acc` function below.
+    -- This is rather unsafe, and has been the source of bugs in the past
+    $ fmap (f . GSeries.fromDistinctAscList)
+    -- We're using a list fold to limit the number of 
+    -- type constraints. This is about as fast as it is 
+    -- with a Vector fold
+    $ Data.List.foldl' acc mempty 
+    $ GSeries.toList xs
+    where
+        acc !m (key, val) = Map.insertWith (flip (<>)) -- Flipping arguments to ensure that keys are ordered as expected
+                                           (by key) 
+                                           (Data.List.singleton (key, val)) 
+                                           m
+
+
+-- | Fold over each group in a 'Grouping' using a binary function.
+-- While this is not as expressive as 'aggregateWith', users looking for maximum
+-- performance should use 'foldWith' as much as possible.
+--
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `foldWith` min
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+foldWith :: (Ord g, Vector v a) 
+         => Grouping k g v a
+         -> (a -> a -> a)
+         -> Series v g a
+{-# INLINABLE foldWith #-}
+foldWith (MkGrouping xs by) f 
+    = GSeries.fromStrictMap 
+    -- We're using a list fold to limit the number of 
+    -- type constraints. This is about as fast as it is 
+    -- with a Vector fold
+    $ Data.List.foldl' acc mempty 
+    $ GSeries.toList xs
+    where
+        acc !m (key, val) = Map.insertWith f (by key) val m
+
+
+-- | Expanding window aggregation.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 3)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in (xs `expanding` sum) :: Series.Series Int Int 
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      0
+--     2 |      1
+--     3 |      3
+--     4 |      6
+--     5 |     10
+--     6 |     15
+expanding :: (Vector v a, Vector v b) 
+          => Series v k a        -- ^ Series vector
+          -> (Series v k a -> b) -- ^ Aggregation function
+          -> Series v k b        -- ^ Resulting vector
+{-# INLINABLE expanding #-}
+expanding vs f = MkSeries (index vs) $ Vector.unfoldrExactN (GSeries.length vs) go 0
+    where
+        -- Recall that `slice` does NOT include the right index
+        go ix = (f $ slice 0 (ix + 1) vs, ix + 1)
+
+
+-- | General-purpose window aggregation.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> import           Data.Series ( to )
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 3)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in windowing (\k -> k `to` (k + 2)) sum xs
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      3
+--     2 |      6
+--     3 |      9
+--     4 |     12
+--     5 |      9
+--     6 |      5
+windowing :: (Ord k, Vector v a, Vector v b)
+          => (k -> Range k)
+          -> (Series v k a -> b)
+          -> Series v k a
+          -> Series v k b
+{-# INLINABLE windowing #-}
+windowing range agg series 
+    = GSeries.mapWithKey (\k _ -> agg $ series `select` range k) series
+
+
+-- | \(O(n)\) Check if all elements satisfy the predicate.
+all :: Vector v a => (a -> Bool) -> Series v k a -> Bool
+{-# INLINABLE all #-}
+all f = Vector.all f . values
+
+
+-- | \(O(n)\) Check if any element satisfies the predicate.
+any :: Vector v a => (a -> Bool) -> Series v k a -> Bool
+{-# INLINABLE any #-}
+any f = Vector.any f . values
+
+
+-- | \(O(n)\) Check if all elements are 'True'.
+and :: Vector v Bool => Series v k Bool -> Bool
+{-# INLINABLE and #-}
+and = Vector.and . values
+
+
+-- | \(O(n)\) Check if any element is 'True'.
+or :: Vector v Bool => Series v k Bool -> Bool
+{-# INLINABLE or #-}
+or = Vector.or . values
+
+
+-- | \(O(n)\) Compute the sum of the elements.
+sum :: (Num a, Vector v a) => Series v k a -> a
+{-# INLINABLE sum #-}
+sum = Vector.sum . values
+
+
+-- | \(O(n)\) Compute the product of the elements.
+product :: (Num a, Vector v a) => Series v k a -> a
+{-# INLINABLE product #-}
+product = Vector.product . values
+
+
+nothingIfEmpty :: Vector v a 
+               => (Series v k a -> b) -> (Series v k a -> Maybe b)
+nothingIfEmpty f xs = if GSeries.null xs then Nothing else Just (f xs) 
+
+
+-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
+maximum :: (Ord a, Vector v a) => Series v k a -> Maybe a
+{-# INLINABLE maximum #-}
+maximum = nothingIfEmpty $ Vector.maximum . values
+
+
+-- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+maximumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
+{-# INLINABLE maximumOn #-}
+maximumOn f = nothingIfEmpty $ Vector.maximumOn f . values
+
+
+-- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+minimum :: (Ord a, Vector v a) => Series v k a -> Maybe a
+{-# INLINABLE minimum #-}
+minimum = nothingIfEmpty $ Vector.minimum . values
+
+
+-- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+minimumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
+{-# INLINABLE minimumOn #-}
+minimumOn f = nothingIfEmpty $ Vector.minimumOn f . values
+
+
+-- | \(O(n)\) Find the index of the maximum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the maximum element is returned.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 7)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmax xs 
+-- :}
+-- Just 4
+argmax :: (Ord a, Vector v a)
+       => Series v k a
+       -> Maybe k
+{-# INLINABLE argmax #-}
+argmax xs | GSeries.null xs = Nothing
+          | otherwise = Just 
+                      . fst 
+                      -- We're forcing the use of boxed vectors in order to
+                      -- reduce the constraints on the vector instance
+                      . Boxed.maximumOn snd 
+                      . GSeries.toVector
+                      . GSeries.convert
+                      $ xs
+
+
+-- | \(O(n)\) Find the index of the minimum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the minimum element is returned.
+--
+-- >>> import qualified Data.Series as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 1)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 0)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmin xs 
+-- :}
+-- Just 4
+argmin :: (Ord a, Vector v a, Vector v (Down a))
+       => Series v k a
+       -> Maybe k
+{-# INLINABLE argmin #-}
 argmin = argmax . GSeries.map Down
diff --git a/src/Data/Series/Generic/Definition.hs b/src/Data/Series/Generic/Definition.hs
--- a/src/Data/Series/Generic/Definition.hs
+++ b/src/Data/Series/Generic/Definition.hs
@@ -1,832 +1,832 @@
-{-# LANGUAGE DerivingStrategies    #-}
-{-# LANGUAGE QuantifiedConstraints #-}
-{-# LANGUAGE RecordWildCards       #-}
-{-# LANGUAGE TypeFamilies          #-}
-{-# LANGUAGE UndecidableInstances  #-}
-
-module Data.Series.Generic.Definition ( 
-    Series(..),
-
-    convert,
-
-    -- * Basic interface
-    singleton,
-    headM, lastM, map, mapWithKey, mapIndex, concatMap, fold, foldM, 
-    foldWithKey, foldMWithKey, foldMap, bifoldMap, foldMapWithKey, 
-    length, null, take, takeWhile, drop, dropWhile,
-    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,
-    traverseWithKey,
-
-    fromIndex,
-    -- * Conversion to/from Series
-    IsSeries(..),
-    -- ** Conversion to/from Maps
-    fromStrictMap,
-    toStrictMap,
-    fromLazyMap,
-    toLazyMap,
-    -- ** Conversion to/from list
-    fromList,
-    toList,
-    -- *** Unsafe construction
-    fromDistinctAscList,
-    -- ** Conversion to/from vectors
-    fromVector,
-    toVector,
-    -- *** Unsafe construction
-    fromDistinctAscVector,
-    -- ** Handling duplicates
-    Occurrence, fromListDuplicates, fromVectorDuplicates,
-
-    -- * Displaying 'Series'
-    display, displayWith,
-    noLongerThan,
-    DisplayOptions(..), defaultDisplayOptions
-) where
-
-import           Control.DeepSeq        ( NFData(rnf) )
-import           Control.Foldl          ( Fold(..), FoldM(..) )
-import           Control.Monad.ST       ( runST )
-import           Data.Bifoldable        ( Bifoldable )
-import qualified Data.Bifoldable        as Bifoldable
-import qualified Data.Foldable          as Foldable
-import           Data.Foldable.WithIndex ( FoldableWithIndex(..))
-import           Data.Function          ( on )
-import           Data.Functor.WithIndex ( FunctorWithIndex(imap) )
-
-import           Data.IntMap.Strict     ( IntMap )
-import qualified Data.IntMap.Strict     as IntMap
-import qualified Data.List              as List
-import qualified Data.Map.Lazy          as ML
-import           Data.Map.Strict        ( Map )
-import qualified Data.Map.Strict        as MS
-import           Data.Sequence          ( Seq )
-import qualified Data.Sequence          as Seq
-import           Data.Semigroup         ( Semigroup(..) )
-import           Data.Series.Index      ( Index )
-import qualified Data.Series.Index      as Index
-import qualified Data.Series.Index.Internal as Index.Internal
-import           Data.Set               ( Set )
-import qualified Data.Set               as Set
-import           Data.Traversable.WithIndex ( TraversableWithIndex(..) )
-import qualified Data.Vector            as Boxed
-import           Data.Vector.Algorithms.Intro ( sortUniqBy, sortBy )
-import           Data.Vector.Generic    ( Vector )
-import qualified Data.Vector.Generic    as Vector
-import qualified Data.Vector.Generic.Mutable as GM
-import qualified Data.Vector.Unboxed         as U
-import qualified Data.Vector.Unboxed.Mutable as UM
- 
-import           Prelude                hiding ( take, takeWhile, drop, dropWhile, map, concatMap, foldMap, sum, length, null )
-import qualified Prelude                as P
-
-
-
--- | A @Series v k a@ is a labeled array of type @v@ filled with values of type @a@,
--- indexed by keys of type @k@.
---
--- Like 'Data.Map.Strict.Map', they support efficient:
---
---      * random access by key ( \(O(\log n)\) );
---      * slice by key ( \(O(\log n)\) ).
---
--- Like 'Data.Vector.Vector', they support efficient:
---
---      * random access by index ( \(O(1)\) );
---      * slice by index ( \(O(1)\) );
---      * numerical operations.
---
-data Series v k a 
-    -- The reason the index is a set of keys is that we *want* keys to be ordered.
-    -- This allows for efficient slicing of the underlying values, because
-    -- if @k1 < k2@, then the values are also at indices @ix1 < ix2@.
-    = MkSeries { index  :: Index k -- ^ The 'Index' of a series, which contains its (unique) keys in ascending order.
-               , values :: v a     -- ^ The values of a series, in the order of its (unique) keys.
-               }
-
-
--- | \(O(n)\) Convert between two types of 'Series'.
-convert :: (Vector v1 a, Vector v2 a) => Series v1 k a -> Series v2 k a
-{-# INLINABLE convert #-}
-convert (MkSeries ix vs) = MkSeries ix $ Vector.convert vs 
-
-
--- | \(O(1)\) Create a 'Series' with a single element.
-singleton :: Vector v a => k -> a -> Series v k a
-{-# INLINABLE singleton #-}
-singleton k v = MkSeries (Index.singleton k) $ Vector.singleton v
-
-
--- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
-fromIndex :: (Vector v a) 
-          => (k -> a) -> Index k -> Series v k a
-{-# INLINABLE fromIndex #-}
-fromIndex f ix = MkSeries ix $ Vector.convert 
-                             $ Boxed.map f -- Using boxed vector to prevent a (Vector v k) constraint
-                             $ Index.toAscVector ix
-
-
--- | The 'IsSeries' typeclass allow for ad-hoc definition
--- of conversion functions, converting to / from 'Series'.
-class IsSeries t v k a where
-    -- | Construct a 'Series' from some container of key-values pairs. There is no
-    -- condition on the order of pairs. Duplicate keys are silently dropped. If you
-    -- need to handle duplicate keys, see 'fromListDuplicates' or 'fromVectorDuplicates'.
-    toSeries    :: t -> Series v k a
-
-    -- | Construct a container from key-value pairs of a 'Series'. 
-    -- The elements are returned in ascending order of keys. 
-    fromSeries  :: Series v k a -> t
-
-
-instance (Ord k, Vector v a) => IsSeries [(k, a)] v k a where
-    -- | Construct a series from a list of key-value pairs. There is no
-    -- condition on the order of pairs.
-    --
-    -- >>> let xs = toSeries [('b', 0::Int), ('a', 5), ('d', 1) ]
-    -- >>> xs
-    -- index | values
-    -- ----- | ------
-    --   'a' |      5
-    --   'b' |      0
-    --   'd' |      1
-    --
-    -- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
-    toSeries :: [(k, a)] -> Series v k a
-    toSeries = toSeries . MS.fromList
-    {-# INLINABLE toSeries #-}
-
-    -- | Construct a list from key-value pairs. The elements are in order sorted by key:
-    --
-    -- >>> let xs = Series.toSeries [ ('b', 0::Int), ('a', 5), ('d', 1) ]
-    -- >>> xs
-    -- index | values
-    -- ----- | ------
-    --   'a' |      5
-    --   'b' |      0
-    --   'd' |      1
-    -- >>> fromSeries xs
-    -- [('a',5),('b',0),('d',1)]
-    fromSeries :: Series v k a -> [(k, a)]
-    fromSeries (MkSeries ks vs)= zip (Index.toAscList ks) (Vector.toList vs)
-    {-# INLINABLE fromSeries #-}
-
-
--- | Construct a 'Series' from a list of key-value pairs. There is no
--- condition on the order of pairs. Duplicate keys are silently dropped. If you
--- need to handle duplicate keys, see 'fromListDuplicates'.
-fromList :: (Vector v a, Ord k) => [(k, a)] -> Series v k a
-{-# INLINABLE fromList #-}
-fromList = toSeries
-
-
--- | \(O(n)\) Build a 'Series' from a list of pairs, where the first elements of the pairs (the keys)
--- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.
-fromDistinctAscList :: (Vector v a) => [(k, a)] -> Series v k a
-fromDistinctAscList xs 
-    = let (!ks, !vs) = unzip xs 
-       in MkSeries (Index.Internal.fromDistinctAscList ks) (Vector.fromListN (List.length vs) vs)
-
-
--- | Integer-like, non-negative number that specifies how many occurrences
--- of a key is present in a 'Series'.
---
--- The easiest way to convert from an 'Occurrence' to another integer-like type
--- is the 'fromIntegral' function.
-newtype Occurrence = MkOcc Int
-    deriving (Eq, Enum, Num, Ord, Integral, Real)
-    deriving newtype (Show, U.Unbox) 
-
--- Occurrence needs to be an 'U.Unbox' type
--- so that 'fromVectorDuplicates' works with unboxed vectors
--- and series.
-newtype instance UM.MVector s Occurrence = MV_Occ (UM.MVector s Int)
-newtype instance U.Vector Occurrence = V_Occ (U.Vector Int)
-deriving instance GM.MVector UM.MVector Occurrence
-deriving instance Vector U.Vector Occurrence 
-
-
--- | Construct a series from a list of key-value pairs.
--- Contrary to 'fromList', values at duplicate keys are preserved. To keep each
--- key unique, an 'Occurrence' number counts up.
-fromListDuplicates :: (Vector v a, Ord k) => [(k, a)] -> Series v (k, Occurrence) a
-{-# INLINABLE fromListDuplicates #-}
-fromListDuplicates = convert . fromVectorDuplicates . Boxed.fromList
-
-
--- | Construct a list from key-value pairs. The elements are in order sorted by key. 
-toList :: Vector v a => Series v k a -> [(k, a)]
-{-# INLINABLE toList #-}
-toList (MkSeries ks vs) = zip (Index.toAscList ks) (Vector.toList vs)
-
-
-instance (Ord k) => IsSeries (Boxed.Vector (k, a)) Boxed.Vector k a where
-    toSeries = fromVector
-    {-# INLINABLE toSeries #-}
-
-    fromSeries = toVector
-    {-# INLINABLE fromSeries #-}
-
-
-instance (Ord k, U.Unbox a, U.Unbox k) => IsSeries (U.Vector (k, a)) U.Vector k a where
-    toSeries :: U.Vector (k, a) -> Series U.Vector k a
-    toSeries = fromVector
-    {-# INLINABLE toSeries #-}
-
-    fromSeries :: Series U.Vector k a -> U.Vector (k, a)
-    fromSeries = toVector
-    {-# INLINABLE fromSeries #-}
-
-
--- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
--- condition on the order of pairs. Duplicate keys are silently dropped. If you
--- need to handle duplicate keys, see 'fromVectorDuplicates'.
---
--- Note that due to differences in sorting,
--- 'Series.fromList' and @'Series.fromVector' . 'Vector.fromList'@
--- may not be equivalent if the input list contains duplicate keys.
-fromVector :: (Ord k, Vector v k, Vector v a, Vector v (k, a))
-           => v (k, a) -> Series v k a
-{-# INLINABLE fromVector #-}
-fromVector vec = let (indexVector, valuesVector) = Vector.unzip $ runST $ do
-                        mv <- Vector.thaw vec
-                        -- Note that we're using this particular flavor of `sortUniqBy`
-                        -- because it both sorts AND removes duplicate keys
-                        destMV <- sortUniqBy (compare `on` fst) mv
-                        v <- Vector.freeze destMV
-                        pure (Vector.force v)
-              in MkSeries (Index.Internal.fromDistinctAscVector indexVector) valuesVector
-
-
--- | \(O(n)\) Build a 'Series' from a vector of pairs, where the first elements of the pairs (the keys)
--- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.
-fromDistinctAscVector :: (Vector v k, Vector v a, Vector v (k, a))
-                      => v (k, a) -> Series v k a
-fromDistinctAscVector xs 
-    = let (ks, vs) = Vector.unzip xs 
-       in MkSeries (Index.Internal.fromDistinctAscVector ks) vs
-
-
--- | Construct a 'Series' from a 'Vector' of key-value pairs, where there may be duplicate keys. 
--- There is no condition on the order of pairs.
-fromVectorDuplicates :: (Ord k, Vector v k, Vector v a, Vector v (k, a), Vector v (k, Occurrence))
-                     => v (k, a) -> Series v (k, Occurrence) a
-{-# INLINABLE fromVectorDuplicates #-}
-fromVectorDuplicates vec 
-    = let (indexVector, valuesVector) 
-            = Vector.unzip $ runST $ do
-                mv <- Vector.thaw vec
-                sortBy (compare `on` fst) mv
-                v <- Vector.freeze mv
-                pure (Vector.force v)
-        in MkSeries (Index.Internal.fromDistinctAscVector (occurences indexVector)) valuesVector
-    where
-        occurences vs 
-            | Vector.null vs        = Vector.empty
-            | Vector.length vs == 1 = Vector.map (,0) vs
-            | otherwise             = Vector.scanl f (Vector.head vs, 0) (Vector.tail vs)
-            where
-                f (lastKey, lastOcc) newKey 
-                    | lastKey == newKey = (newKey, lastOcc + 1)
-                    | otherwise         = (newKey, 0)
-
-
--- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
-toVector :: (Vector v a, Vector v k, Vector v (k, a)) 
-         => Series v k a -> v (k, a)
-{-# INLINABLE toVector #-}
-toVector (MkSeries ks vs) = Vector.zip (Index.toAscVector ks) vs
-
-
-instance (Vector v a) => IsSeries (Map k a) v k a where
-    toSeries :: Map k a -> Series v k a
-    toSeries mp = MkSeries 
-                { index  = Index.fromSet $ MS.keysSet mp
-                , values = Vector.fromListN (MS.size mp) $ MS.elems mp
-                }
-    {-# INLINABLE toSeries #-}
-
-    fromSeries :: Series v k a -> Map k a
-    fromSeries (MkSeries ks vs)
-        = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
-    {-# INLINABLE fromSeries #-}
-
-
-toLazyMap :: (Vector v a) => Series v k a -> Map k a
-{-# INLINABLE toLazyMap #-}
-toLazyMap = fromSeries
-
-
--- | Construct a series from a lazy 'Data.Map.Lazy.Map'.
-fromLazyMap :: (Vector v a) => ML.Map k a -> Series v k a
-{-# INLINABLE fromLazyMap #-}
-fromLazyMap = toSeries
-
-
--- | Convert a series into a strict 'Data.Map.Strict.Map'.
-toStrictMap :: (Vector v a) => Series v k a -> Map k a
-{-# INLINABLE toStrictMap #-}
-toStrictMap (MkSeries ks vs) = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
-
-
--- | Construct a series from a strict 'Data.Map.Strict.Map'.
-fromStrictMap :: (Vector v a) => MS.Map k a -> Series v k a
-{-# INLINABLE fromStrictMap #-}
-fromStrictMap mp = MkSeries { index  = Index.toIndex $ MS.keysSet mp
-                            , values = Vector.fromListN (MS.size mp) $ MS.elems mp
-                            }
-
-
-instance (Vector v a) => IsSeries (IntMap a) v Int a where
-    toSeries :: IntMap a -> Series v Int a
-    toSeries im = MkSeries 
-                { index  = Index.toIndex $ IntMap.keysSet im
-                , values = Vector.fromListN (IntMap.size im)  $ IntMap.elems im 
-                }
-    {-# INLINABLE toSeries #-}
-
-    fromSeries :: Series v Int a -> IntMap a
-    fromSeries (MkSeries ks vs) 
-        = IntMap.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
-    {-# INLINABLE fromSeries #-}
-
-
-instance (Ord k, Vector v a) => IsSeries (Seq (k, a)) v k a where
-    toSeries :: Seq (k, a) -> Series v k a
-    toSeries = toSeries . Foldable.toList
-    {-# INLINABLE toSeries #-}
-
-    fromSeries :: Series v k a -> Seq (k, a)
-    fromSeries = Seq.fromList . fromSeries
-    {-# INLINABLE fromSeries #-}
-
-
-instance (Vector v a) => IsSeries (Set (k, a)) v k a where
-    toSeries :: Set (k, a) -> Series v k a
-    toSeries = fromDistinctAscList . Set.toAscList
-    {-# INLINABLE toSeries #-}
-
-    fromSeries :: Series v k a -> Set (k, a)
-    fromSeries = Set.fromDistinctAscList . toList
-    {-# INLINABLE fromSeries #-}
-
-
--- | Get the first value of a 'Series'. If the 'Series' is empty,
--- this function returns 'Nothing'.
-headM :: Vector v a => Series v k a -> Maybe a
-{-# INLINABLE headM #-}
-headM (MkSeries _ vs) = Vector.headM vs
-
-
--- | Get the last value of a 'Series'. If the 'Series' is empty,
--- this function returns 'Nothing'.
-lastM :: Vector v a => Series v k a -> Maybe a
-{-# INLINABLE lastM #-}
-lastM (MkSeries _ vs) = Vector.lastM vs
-
-
--- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
-take :: Vector v a => Int -> Series v k a -> Series v k a
-{-# INLINABLE take #-}
-take n (MkSeries ks vs) 
-    -- Index.take is O(log n) while Vector.take is O(1)
-    = MkSeries (Index.take n ks) (Vector.take n vs)
-
-
--- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
-drop :: Vector v a => Int -> Series v k a -> Series v k a
-{-# INLINABLE drop #-}
-drop n (MkSeries ks vs) 
-    -- Index.drop is O(log n) while Vector.drop is O(1)
-    = MkSeries (Index.drop n ks) (Vector.drop n vs)
-
-
--- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
-takeWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a
-{-# INLINABLE takeWhile #-}
-takeWhile f (MkSeries ix vs) = let taken = Vector.takeWhile f vs
-                 in MkSeries { index  = Index.take (Vector.length taken) ix
-                             , values = taken 
-                             }
-
-
--- | \(O(n)\) Returns the complement of 'takeWhile'.
-dropWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a
-{-# INLINABLE dropWhile #-}
-dropWhile f (MkSeries ix vs) = let dropped = Vector.dropWhile f vs
-                 in MkSeries { index  = Index.drop (Index.size ix - Vector.length dropped) ix
-                             , values = dropped
-                             }
-
-
--- | \(O(n)\) Map every element of a 'Series'.
-map :: (Vector v a, Vector v b) 
-    => (a -> b) -> Series v k a -> Series v k b
-{-# INLINABLE map #-}
-map f (MkSeries ix xs) = MkSeries ix $ Vector.map f xs
-
-
--- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
-mapWithKey :: (Vector v a, Vector v b) 
-           => (k -> a -> b) -> Series v k a -> Series v k b
-{-# INLINABLE mapWithKey #-}
-mapWithKey f (MkSeries ix xs) 
-    -- We're using boxed vectors to map because we don't want any restrictions
-    -- on the index type, i.e. we don't want the constraint Vector v k
-    = let vs = Boxed.zipWith f (Index.toAscVector ix) (Vector.convert xs)
-       in MkSeries ix (Vector.convert vs)
-
-
--- | \(O(n \log n)\).
--- Map each key in the index to another value. Note that the resulting series
--- may have less elements, because each key must be unique.
---
--- In case new keys are conflicting, the first element is kept.
-mapIndex :: (Vector v a, Ord k, Ord g) => Series v k a -> (k -> g) -> Series v g a
-{-# INLINABLE mapIndex #-}
-mapIndex (MkSeries index values) f
-    -- Note that the order in which items are kept appears to be backwards;
-    -- See the examples for Data.Map.Strict.fromListWith
-    = let mapping   = MS.fromListWith (\_ x -> x) $ [(f k, k) | k <- Index.toAscList index]
-          newvalues = fmap (\k -> values Vector.! Index.Internal.findIndex k index) mapping
-       in toSeries newvalues
-
-
--- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
-concatMap :: (Vector v a, Vector v k, Vector v b, Vector v (k, a), Vector v (k, b), Ord k) 
-          => (a -> Series v k b) 
-          -> Series v k a 
-          -> Series v k b
-{-# INLINABLE concatMap #-}
-concatMap f = fromVector 
-            . Vector.concatMap (toVector . f . snd) 
-            . toVector
-
-
-instance (Vector v a, Ord k) => Semigroup (Series v k a) where
-    {-# INLINABLE (<>) #-}
-    (<>) :: Series v k a -> Series v k a -> Series v k a
-    -- Despite all my effort, merging via conversion to Map remains fastest.
-    xs <> ys = toSeries $ toStrictMap xs <> toStrictMap ys
-
-    {-# INLINABLE sconcat #-}
-    sconcat = toSeries . sconcat . fmap toStrictMap
-
-
-instance (Vector v a, Ord k) => Monoid (Series v k a) where
-    {-# INLINABLE mempty #-}
-    mempty :: Series v k a
-    mempty = MkSeries mempty Vector.empty
-
-    {-# INLINABLE mappend #-}
-    mappend :: Series v k a -> Series v k a -> Series v k a
-    mappend = (<>)
-
-    {-# INLINABLE mconcat #-}
-    mconcat :: [Series v k a] -> Series v k a
-    mconcat = toSeries . mconcat . fmap toStrictMap
-
-
-instance (Vector v a, Eq k, Eq a) => Eq (Series v k a) where
-    {-# INLINABLE (==) #-}
-    (==) :: Series v k a -> Series v k a -> Bool
-    (MkSeries ks1 vs1) == (MkSeries ks2 vs2) = (ks1 == ks2) && (vs1 `Vector.eq` vs2)
-
-
-instance (Vector v a, Ord (v a), Ord k, Ord a) => Ord (Series v k a) where
-    {-# INLINABLE compare #-}
-    compare :: Series v k a -> Series v k a -> Ordering
-    compare (MkSeries ks1 vs1) (MkSeries ks2 vs2) = compare (ks1, vs1) (ks2, vs2)
-
-
-instance (Functor v) => Functor (Series v k) where
-    {-# INLINABLE fmap #-}
-    fmap :: (a -> b) -> Series v k a -> Series v k b
-    fmap f (MkSeries ks vs) = MkSeries ks (fmap f vs)
-
-
-instance (forall a. Vector v a, Functor v) => FunctorWithIndex k (Series v k) where
-    {-# INLINABLE imap #-}
-    imap :: (k -> a -> b) -> Series v k a -> Series v k b
-    imap = mapWithKey
-
-
--- Inlining all methods in 'Foldable'
--- is important in order for folds over a boxed
--- Series to have performance characteristics
--- be as close as possible to boxed vectors 
-instance (Foldable v) => Foldable (Series v k) where
-    {-# INLINABLE fold #-}
-    fold :: Monoid m => Series v k m -> m
-    fold = Foldable.fold . values
-
-    {-# INLINABLE foldMap #-}
-    foldMap :: (Monoid m) => (a -> m) -> Series v k a -> m
-    foldMap f = Foldable.foldMap f . values
-
-    {-# INLINABLE foldMap' #-}
-    foldMap' :: (Monoid m) => (a -> m) -> Series v k a -> m
-    foldMap' f = Foldable.foldMap f . values
-
-    {-# INLINABLE foldr #-}
-    foldr :: (a -> b -> b) -> b -> Series v k a -> b
-    foldr f i = Foldable.foldr f i . values
-
-    {-# INLINABLE foldr' #-}
-    foldr' :: (a -> b -> b) -> b -> Series v k a -> b
-    foldr' f i = Foldable.foldr' f i . values
-
-    {-# INLINABLE foldl #-}
-    foldl :: (b -> a -> b) -> b -> Series v k a -> b
-    foldl f i = Foldable.foldl f i . values
-
-    {-# INLINABLE foldl' #-}
-    foldl' :: (b -> a -> b) -> b -> Series v k a -> b
-    foldl' f i = Foldable.foldl' f i . values
-
-    {-# INLINABLE foldr1 #-}
-    foldr1 :: (a -> a -> a) -> Series v k a -> a
-    foldr1 f = Foldable.foldr1 f . values
-
-    {-# INLINABLE foldl1 #-}
-    foldl1 :: (a -> a -> a) -> Series v k a -> a
-    foldl1 f = Foldable.foldl1 f . values
-
-    {-# INLINABLE toList #-}
-    toList :: Series v k a -> [a]
-    toList = Foldable.toList . values
-
-    {-# INLINABLE null #-}
-    null :: Series v k a -> Bool
-    null = Foldable.null . values
-
-    {-# INLINABLE length #-}
-    length :: Series v k a -> Int
-    length = Foldable.length . values
-
-    {-# INLINABLE elem #-}
-    elem :: Eq a => a -> Series v k a -> Bool
-    elem e = Foldable.elem e . values
-
-    {-# INLINABLE maximum #-}
-    maximum :: Ord a => Series v k a -> a
-    maximum = Foldable.maximum . values
-
-    {-# INLINABLE minimum #-}
-    minimum :: Ord a => Series v k a -> a
-    minimum = Foldable.minimum . values
-
-    {-# INLINABLE sum #-}
-    sum :: Num a => Series v k a -> a
-    sum = Foldable.sum . values
-
-    {-# INLINABLE product #-}
-    product :: Num a => Series v k a -> a
-    product = Foldable.product . values
-
-
-instance (forall a. Vector v a, Vector v k, Foldable v, Functor v) => FoldableWithIndex k (Series v k) where
-    {-# INLINABLE ifoldMap #-}
-    ifoldMap :: Monoid m => (k -> a -> m) -> Series v k a -> m
-    ifoldMap = foldMapWithKey
-
-
-instance (Foldable v) => Bifoldable (Series v) where
-    {-# INLINABLE bifoldMap #-}
-    bifoldMap :: Monoid m => (k -> m) -> (a -> m) -> Series v k a -> m
-    bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Foldable.foldMap fv vs
-
-
-instance (Traversable v) => Traversable (Series v k) where
-    {-# INLINABLE traverse #-}
-    traverse :: Applicative f
-             => (a -> f b) -> Series v k a -> f (Series v k b)
-    traverse f (MkSeries ix vs) = MkSeries ix <$> traverse f vs
-
-
-instance (forall a. Vector v a, Functor v, Foldable v, Ord k, Traversable v) => TraversableWithIndex k (Series v k) where
-    {-# INLINABLE itraverse #-}
-    itraverse :: Applicative f => (k -> a -> f b) -> Series v k a -> f (Series v k b)
-    itraverse = traverseWithKey
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series'.
---
--- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
--- account while folding.
-fold :: Vector v a 
-     => Fold a b  
-     -> Series v k a 
-     -> b
-fold (Fold step init' extract) 
-    = extract . Vector.foldl' step init' . values
-{-# INLINABLE fold #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
---
--- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
--- account while folding.
-foldM :: (Monad m, Vector v a)
-      => FoldM m a b  
-      -> Series v k a 
-      -> m b
-foldM (FoldM step init' extract) xs
-    = init' >>= \i -> Vector.foldM' step i (values xs) >>= extract
-{-# INLINABLE foldM #-}
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series', where the 'Fold' takes keys into account.
-foldWithKey :: (Vector v a, Vector v k, Vector v (k, a)) 
-            => Fold (k, a) b  
-            -> Series v k a 
-            -> b
-foldWithKey (Fold step init' extract) 
-    = extract . Vector.foldl' step init' . toVector
-{-# INLINABLE foldWithKey #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
-foldMWithKey :: (Monad m, Vector v a, Vector v k, Vector v (k, a)) 
-             => FoldM m (k, a) b
-             -> Series v k a 
-             -> m b
-foldMWithKey (FoldM step init' extract) xs
-    = init' >>= \i -> Vector.foldM' step i (toVector xs) >>= extract
-{-# INLINABLE foldMWithKey #-}
-
-
--- | \(O(n)\) Fold over elements in a 'Series'.
-foldMap :: (Monoid m, Vector v a) => (a -> m) -> Series v k a -> m
-{-# INLINABLE foldMap #-}
-foldMap f = Vector.foldMap f . values
-
-
--- | \(O(n)\) Fold over pairs of keys and elements in a 'Series'.
--- See also 'bifoldMap'.
-foldMapWithKey :: (Monoid m, Vector v a, Vector v k, Vector v (k, a)) => (k -> a -> m) -> Series v k a -> m
-{-# INLINABLE foldMapWithKey #-}
-foldMapWithKey f = Vector.foldMap (uncurry f) . toVector
-
-
--- | \(O(n)\) Fold over keys and elements separately in a 'Series'.
--- See also 'foldMapWithKey'.
-bifoldMap :: (Vector v a, Monoid m) => (k -> m) -> (a -> m) -> Series v k a -> m
-{-# INLINABLE bifoldMap #-}
-bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Vector.foldMap fv vs
-
-
--- | \(O(1)\) Extract the length of a 'Series'.
-length :: Vector v a => Series v k a -> Int
-{-# INLINABLE length #-}
-length = Vector.length . values
-
-
--- | \(O(1)\) Test whether a 'Series' is empty.
-null :: Vector v a => Series v k a -> Bool
-{-# INLINABLE null #-}
-null = Vector.null . values
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, yielding a series of results.
-mapWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) 
-            => (k -> a -> m b) -> Series v k a -> m (Series v k b)
-{-# INLINABLE mapWithKeyM #-}
-mapWithKeyM f xs = let f' (key, val) = (key,) <$> f key val
-           in fmap fromList $ traverse f' $ toList xs
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, discarding the results.
-mapWithKeyM_ :: (Vector v a, Monad m) 
-             => (k -> a -> m b) -> Series v k a -> m ()
-{-# INLINABLE mapWithKeyM_ #-}
-mapWithKeyM_ f xs = let f' (key, val) = (key,) <$> f key val
-           in mapM_ f' $ toList xs
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- yielding a series of results.
-forWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) => Series v k a -> (k -> a -> m b) -> m (Series v k b)
-{-# INLINABLE forWithKeyM #-}
-forWithKeyM = flip mapWithKeyM
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- discarding the results.
-forWithKeyM_ :: (Vector v a, Monad m) => Series v k a -> (k -> a -> m b) -> m ()
-{-# INLINABLE forWithKeyM_ #-}
-forWithKeyM_ = flip mapWithKeyM_
-
-
--- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. 
-traverseWithKey :: (Applicative t, Ord k, Traversable v, Vector v a, Vector v b, Vector v k, Vector v (k, a),  Vector v (k, b))
-                => (k -> a -> t b) 
-                -> Series v k a 
-                -> t (Series v k b)
-{-# INLINABLE traverseWithKey #-}
-traverseWithKey f = fmap fromVector 
-                  . traverse (\(k, x) -> (k,) <$> f k x) 
-                  . toVector
-
-
-instance (NFData (v a), NFData k) => NFData (Series v k a) where
-    rnf :: Series v k a -> ()
-    rnf (MkSeries ks vs) = rnf ks `seq` rnf vs
-
-
-instance (Vector v a, Ord k, Show k, Show a) => Show (Series v k a) where
-    show :: Series v k a -> String
-    show = display
-
-
--- | Options controlling how to display 'Series' in the 'displayWith' function.
--- Default options are provided by 'defaultDisplayOptions'.
---
--- To help with creating 'DisplayOptions', see 'noLongerThan'.
-data DisplayOptions k a
-    = DisplayOptions
-    { maximumNumberOfRows  :: Int
-    -- ^ Maximum number of rows shown. These rows will be distributed evenly
-    -- between the start of the 'Series' and the end. 
-    , indexHeader          :: String
-    -- ^ Header of the index column.
-    , valuesHeader         :: String
-    -- ^ Header of the values column.
-    , keyDisplayFunction   :: k -> String
-    -- ^ Function used to display keys from the 'Series'. Use 'noLongerThan'
-    -- to control the width of the index column.
-    , valueDisplayFunction :: a -> String
-    -- ^ Function used to display values from the 'Series'. Use 'noLongerThan'
-    -- to control the width of the values column.
-    }
-
-
--- | Default 'Series' display options.
-defaultDisplayOptions :: (Show k, Show a) => DisplayOptions k a
-defaultDisplayOptions 
-    = DisplayOptions { maximumNumberOfRows  = 6
-                     , indexHeader          = "index"
-                     , valuesHeader         = "values"
-                     , keyDisplayFunction   = show
-                     , valueDisplayFunction = show
-                     }
-
-
--- | This function modifies existing functions to limit the width of its result.
---
--- >>> let limit7 = (show :: Int -> String) `noLongerThan` 7
--- >>> limit7 123456789
--- "123456..."
-noLongerThan :: (a -> String) -> Int -> (a -> String)
-noLongerThan f len x 
-    = let raw = f x
-       in if List.length raw <= max 0 len
-        then raw
-        else List.take (List.length raw - 3) raw <> "..."
-
-
--- | Display a 'Series' using default 'DisplayOptions'.
-display :: (Vector v a, Show k, Show a) 
-        => Series v k a 
-        -> String
-display = displayWith defaultDisplayOptions
-
-
--- | Display a 'Series' using customizable 'DisplayOptions'.
-displayWith :: (Vector v a) 
-            => DisplayOptions k a
-            -> Series v k a 
-            -> String
-displayWith DisplayOptions{..} xs
-    = formatGrid $ if length xs > max 0 maximumNumberOfRows
-        then let headlength = max 0 maximumNumberOfRows `div` 2
-                 taillength = max 0 maximumNumberOfRows - headlength
-              in mconcat [ [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ take headlength xs]
-                         , [ ("...", "...") ]
-                         , [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ drop (length xs - taillength) xs]
-                         ] 
-        else [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList xs ]
-
-    where
-        -- | Format a grid represented by a list of rows, where every row is a list of items
-        -- All columns will have a fixed width
-        formatGrid :: [ (String, String) ] -- List of rows
-                   -> String
-        formatGrid rows = mconcat $ List.intersperse "\n" 
-                                  $ [ pad indexWidth k <> " | " <> pad valuesWidth v 
-                                    | (k, v) <- rows'
-                                    ] 
-            where
-                rows' = [ (indexHeader, valuesHeader) ] <> [ ("-----", "------")] <> rows
-                (indexCol, valuesCol) = unzip rows'
-                width col = maximum (P.length <$> col)
-                indexWidth = width indexCol
-                valuesWidth = width valuesCol
-
-                -- | Pad a string to a minimum of @n@ characters wide.
-                pad :: Int -> String -> String 
-                pad n s
-                    | n <= P.length s = s
-                    | otherwise     = replicate (n - P.length s) ' ' <> s
+{-# LANGUAGE DerivingStrategies    #-}
+{-# LANGUAGE QuantifiedConstraints #-}
+{-# LANGUAGE RecordWildCards       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE UndecidableInstances  #-}
+
+module Data.Series.Generic.Definition ( 
+    Series(..),
+
+    convert,
+
+    -- * Basic interface
+    singleton,
+    headM, lastM, map, mapWithKey, mapIndex, concatMap, fold, foldM, 
+    foldWithKey, foldMWithKey, foldMap, bifoldMap, foldMapWithKey, 
+    length, null, take, takeWhile, drop, dropWhile,
+    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,
+    traverseWithKey,
+
+    fromIndex,
+    -- * Conversion to/from Series
+    IsSeries(..),
+    -- ** Conversion to/from Maps
+    fromStrictMap,
+    toStrictMap,
+    fromLazyMap,
+    toLazyMap,
+    -- ** Conversion to/from list
+    fromList,
+    toList,
+    -- *** Unsafe construction
+    fromDistinctAscList,
+    -- ** Conversion to/from vectors
+    fromVector,
+    toVector,
+    -- *** Unsafe construction
+    fromDistinctAscVector,
+    -- ** Handling duplicates
+    Occurrence, fromListDuplicates, fromVectorDuplicates,
+
+    -- * Displaying 'Series'
+    display, displayWith,
+    noLongerThan,
+    DisplayOptions(..), defaultDisplayOptions
+) where
+
+import           Control.DeepSeq        ( NFData(rnf) )
+import           Control.Foldl          ( Fold(..), FoldM(..) )
+import           Control.Monad.ST       ( runST )
+import           Data.Bifoldable        ( Bifoldable )
+import qualified Data.Bifoldable        as Bifoldable
+import qualified Data.Foldable          as Foldable
+import           Data.Foldable.WithIndex ( FoldableWithIndex(..))
+import           Data.Function          ( on )
+import           Data.Functor.WithIndex ( FunctorWithIndex(imap) )
+
+import           Data.IntMap.Strict     ( IntMap )
+import qualified Data.IntMap.Strict     as IntMap
+import qualified Data.List              as List
+import qualified Data.Map.Lazy          as ML
+import           Data.Map.Strict        ( Map )
+import qualified Data.Map.Strict        as MS
+import           Data.Sequence          ( Seq )
+import qualified Data.Sequence          as Seq
+import           Data.Semigroup         ( Semigroup(..) )
+import           Data.Series.Index      ( Index )
+import qualified Data.Series.Index      as Index
+import qualified Data.Series.Index.Internal as Index.Internal
+import           Data.Set               ( Set )
+import qualified Data.Set               as Set
+import           Data.Traversable.WithIndex ( TraversableWithIndex(..) )
+import qualified Data.Vector            as Boxed
+import           Data.Vector.Algorithms.Intro ( sortUniqBy, sortBy )
+import           Data.Vector.Generic    ( Vector )
+import qualified Data.Vector.Generic    as Vector
+import qualified Data.Vector.Generic.Mutable as GM
+import qualified Data.Vector.Unboxed         as U
+import qualified Data.Vector.Unboxed.Mutable as UM
+ 
+import           Prelude                hiding ( take, takeWhile, drop, dropWhile, map, concatMap, foldMap, sum, length, null )
+import qualified Prelude                as P
+
+
+
+-- | A @Series v k a@ is a labeled array of type @v@ filled with values of type @a@,
+-- indexed by keys of type @k@.
+--
+-- Like 'Data.Map.Strict.Map', they support efficient:
+--
+--      * random access by key ( \(O(\log n)\) );
+--      * slice by key ( \(O(\log n)\) ).
+--
+-- Like 'Data.Vector.Vector', they support efficient:
+--
+--      * random access by index ( \(O(1)\) );
+--      * slice by index ( \(O(1)\) );
+--      * numerical operations.
+--
+data Series v k a 
+    -- The reason the index is a set of keys is that we *want* keys to be ordered.
+    -- This allows for efficient slicing of the underlying values, because
+    -- if @k1 < k2@, then the values are also at indices @ix1 < ix2@.
+    = MkSeries { index  :: Index k -- ^ The 'Index' of a series, which contains its (unique) keys in ascending order.
+               , values :: v a     -- ^ The values of a series, in the order of its (unique) keys.
+               }
+
+
+-- | \(O(n)\) Convert between two types of 'Series'.
+convert :: (Vector v1 a, Vector v2 a) => Series v1 k a -> Series v2 k a
+{-# INLINABLE convert #-}
+convert (MkSeries ix vs) = MkSeries ix $ Vector.convert vs 
+
+
+-- | \(O(1)\) Create a 'Series' with a single element.
+singleton :: Vector v a => k -> a -> Series v k a
+{-# INLINABLE singleton #-}
+singleton k v = MkSeries (Index.singleton k) $ Vector.singleton v
+
+
+-- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
+fromIndex :: (Vector v a) 
+          => (k -> a) -> Index k -> Series v k a
+{-# INLINABLE fromIndex #-}
+fromIndex f ix = MkSeries ix $ Vector.convert 
+                             $ Boxed.map f -- Using boxed vector to prevent a (Vector v k) constraint
+                             $ Index.toAscVector ix
+
+
+-- | The 'IsSeries' typeclass allow for ad-hoc definition
+-- of conversion functions, converting to / from 'Series'.
+class IsSeries t v k a where
+    -- | Construct a 'Series' from some container of key-values pairs. There is no
+    -- condition on the order of pairs. Duplicate keys are silently dropped. If you
+    -- need to handle duplicate keys, see 'fromListDuplicates' or 'fromVectorDuplicates'.
+    toSeries    :: t -> Series v k a
+
+    -- | Construct a container from key-value pairs of a 'Series'. 
+    -- The elements are returned in ascending order of keys. 
+    fromSeries  :: Series v k a -> t
+
+
+instance (Ord k, Vector v a) => IsSeries [(k, a)] v k a where
+    -- | Construct a series from a list of key-value pairs. There is no
+    -- condition on the order of pairs.
+    --
+    -- >>> let xs = toSeries [('b', 0::Int), ('a', 5), ('d', 1) ]
+    -- >>> xs
+    -- index | values
+    -- ----- | ------
+    --   'a' |      5
+    --   'b' |      0
+    --   'd' |      1
+    --
+    -- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
+    toSeries :: [(k, a)] -> Series v k a
+    toSeries = toSeries . MS.fromList
+    {-# INLINABLE toSeries #-}
+
+    -- | Construct a list from key-value pairs. The elements are in order sorted by key:
+    --
+    -- >>> let xs = Series.toSeries [ ('b', 0::Int), ('a', 5), ('d', 1) ]
+    -- >>> xs
+    -- index | values
+    -- ----- | ------
+    --   'a' |      5
+    --   'b' |      0
+    --   'd' |      1
+    -- >>> fromSeries xs
+    -- [('a',5),('b',0),('d',1)]
+    fromSeries :: Series v k a -> [(k, a)]
+    fromSeries (MkSeries ks vs)= zip (Index.toAscList ks) (Vector.toList vs)
+    {-# INLINABLE fromSeries #-}
+
+
+-- | Construct a 'Series' from a list of key-value pairs. There is no
+-- condition on the order of pairs. Duplicate keys are silently dropped. If you
+-- need to handle duplicate keys, see 'fromListDuplicates'.
+fromList :: (Vector v a, Ord k) => [(k, a)] -> Series v k a
+{-# INLINABLE fromList #-}
+fromList = toSeries
+
+
+-- | \(O(n)\) Build a 'Series' from a list of pairs, where the first elements of the pairs (the keys)
+-- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.
+fromDistinctAscList :: (Vector v a) => [(k, a)] -> Series v k a
+fromDistinctAscList xs 
+    = let (!ks, !vs) = unzip xs 
+       in MkSeries (Index.Internal.fromDistinctAscList ks) (Vector.fromListN (List.length vs) vs)
+
+
+-- | Integer-like, non-negative number that specifies how many occurrences
+-- of a key is present in a 'Series'.
+--
+-- The easiest way to convert from an 'Occurrence' to another integer-like type
+-- is the 'fromIntegral' function.
+newtype Occurrence = MkOcc Int
+    deriving (Eq, Enum, Num, Ord, Integral, Real)
+    deriving newtype (Show, U.Unbox) 
+
+-- Occurrence needs to be an 'U.Unbox' type
+-- so that 'fromVectorDuplicates' works with unboxed vectors
+-- and series.
+newtype instance UM.MVector s Occurrence = MV_Occ (UM.MVector s Int)
+newtype instance U.Vector Occurrence = V_Occ (U.Vector Int)
+deriving instance GM.MVector UM.MVector Occurrence
+deriving instance Vector U.Vector Occurrence 
+
+
+-- | Construct a series from a list of key-value pairs.
+-- Contrary to 'fromList', values at duplicate keys are preserved. To keep each
+-- key unique, an 'Occurrence' number counts up.
+fromListDuplicates :: (Vector v a, Ord k) => [(k, a)] -> Series v (k, Occurrence) a
+{-# INLINABLE fromListDuplicates #-}
+fromListDuplicates = convert . fromVectorDuplicates . Boxed.fromList
+
+
+-- | Construct a list from key-value pairs. The elements are in order sorted by key. 
+toList :: Vector v a => Series v k a -> [(k, a)]
+{-# INLINABLE toList #-}
+toList (MkSeries ks vs) = zip (Index.toAscList ks) (Vector.toList vs)
+
+
+instance (Ord k) => IsSeries (Boxed.Vector (k, a)) Boxed.Vector k a where
+    toSeries = fromVector
+    {-# INLINABLE toSeries #-}
+
+    fromSeries = toVector
+    {-# INLINABLE fromSeries #-}
+
+
+instance (Ord k, U.Unbox a, U.Unbox k) => IsSeries (U.Vector (k, a)) U.Vector k a where
+    toSeries :: U.Vector (k, a) -> Series U.Vector k a
+    toSeries = fromVector
+    {-# INLINABLE toSeries #-}
+
+    fromSeries :: Series U.Vector k a -> U.Vector (k, a)
+    fromSeries = toVector
+    {-# INLINABLE fromSeries #-}
+
+
+-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
+-- condition on the order of pairs. Duplicate keys are silently dropped. If you
+-- need to handle duplicate keys, see 'fromVectorDuplicates'.
+--
+-- Note that due to differences in sorting,
+-- 'Series.fromList' and @'Series.fromVector' . 'Vector.fromList'@
+-- may not be equivalent if the input list contains duplicate keys.
+fromVector :: (Ord k, Vector v k, Vector v a, Vector v (k, a))
+           => v (k, a) -> Series v k a
+{-# INLINABLE fromVector #-}
+fromVector vec = let (indexVector, valuesVector) = Vector.unzip $ runST $ do
+                        mv <- Vector.thaw vec
+                        -- Note that we're using this particular flavor of `sortUniqBy`
+                        -- because it both sorts AND removes duplicate keys
+                        destMV <- sortUniqBy (compare `on` fst) mv
+                        v <- Vector.freeze destMV
+                        pure (Vector.force v)
+              in MkSeries (Index.Internal.fromDistinctAscVector indexVector) valuesVector
+
+
+-- | \(O(n)\) Build a 'Series' from a vector of pairs, where the first elements of the pairs (the keys)
+-- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.
+fromDistinctAscVector :: (Vector v k, Vector v a, Vector v (k, a))
+                      => v (k, a) -> Series v k a
+fromDistinctAscVector xs 
+    = let (ks, vs) = Vector.unzip xs 
+       in MkSeries (Index.Internal.fromDistinctAscVector ks) vs
+
+
+-- | Construct a 'Series' from a 'Vector' of key-value pairs, where there may be duplicate keys. 
+-- There is no condition on the order of pairs.
+fromVectorDuplicates :: (Ord k, Vector v k, Vector v a, Vector v (k, a), Vector v (k, Occurrence))
+                     => v (k, a) -> Series v (k, Occurrence) a
+{-# INLINABLE fromVectorDuplicates #-}
+fromVectorDuplicates vec 
+    = let (indexVector, valuesVector) 
+            = Vector.unzip $ runST $ do
+                mv <- Vector.thaw vec
+                sortBy (compare `on` fst) mv
+                v <- Vector.freeze mv
+                pure (Vector.force v)
+        in MkSeries (Index.Internal.fromDistinctAscVector (occurences indexVector)) valuesVector
+    where
+        occurences vs 
+            | Vector.null vs        = Vector.empty
+            | Vector.length vs == 1 = Vector.map (,0) vs
+            | otherwise             = Vector.scanl f (Vector.head vs, 0) (Vector.tail vs)
+            where
+                f (lastKey, lastOcc) newKey 
+                    | lastKey == newKey = (newKey, lastOcc + 1)
+                    | otherwise         = (newKey, 0)
+
+
+-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
+toVector :: (Vector v a, Vector v k, Vector v (k, a)) 
+         => Series v k a -> v (k, a)
+{-# INLINABLE toVector #-}
+toVector (MkSeries ks vs) = Vector.zip (Index.toAscVector ks) vs
+
+
+instance (Vector v a) => IsSeries (Map k a) v k a where
+    toSeries :: Map k a -> Series v k a
+    toSeries mp = MkSeries 
+                { index  = Index.fromSet $ MS.keysSet mp
+                , values = Vector.fromListN (MS.size mp) $ MS.elems mp
+                }
+    {-# INLINABLE toSeries #-}
+
+    fromSeries :: Series v k a -> Map k a
+    fromSeries (MkSeries ks vs)
+        = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
+    {-# INLINABLE fromSeries #-}
+
+
+toLazyMap :: (Vector v a) => Series v k a -> Map k a
+{-# INLINABLE toLazyMap #-}
+toLazyMap = fromSeries
+
+
+-- | Construct a series from a lazy 'Data.Map.Lazy.Map'.
+fromLazyMap :: (Vector v a) => ML.Map k a -> Series v k a
+{-# INLINABLE fromLazyMap #-}
+fromLazyMap = toSeries
+
+
+-- | Convert a series into a strict 'Data.Map.Strict.Map'.
+toStrictMap :: (Vector v a) => Series v k a -> Map k a
+{-# INLINABLE toStrictMap #-}
+toStrictMap (MkSeries ks vs) = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
+
+
+-- | Construct a series from a strict 'Data.Map.Strict.Map'.
+fromStrictMap :: (Vector v a) => MS.Map k a -> Series v k a
+{-# INLINABLE fromStrictMap #-}
+fromStrictMap mp = MkSeries { index  = Index.toIndex $ MS.keysSet mp
+                            , values = Vector.fromListN (MS.size mp) $ MS.elems mp
+                            }
+
+
+instance (Vector v a) => IsSeries (IntMap a) v Int a where
+    toSeries :: IntMap a -> Series v Int a
+    toSeries im = MkSeries 
+                { index  = Index.toIndex $ IntMap.keysSet im
+                , values = Vector.fromListN (IntMap.size im)  $ IntMap.elems im 
+                }
+    {-# INLINABLE toSeries #-}
+
+    fromSeries :: Series v Int a -> IntMap a
+    fromSeries (MkSeries ks vs) 
+        = IntMap.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)
+    {-# INLINABLE fromSeries #-}
+
+
+instance (Ord k, Vector v a) => IsSeries (Seq (k, a)) v k a where
+    toSeries :: Seq (k, a) -> Series v k a
+    toSeries = toSeries . Foldable.toList
+    {-# INLINABLE toSeries #-}
+
+    fromSeries :: Series v k a -> Seq (k, a)
+    fromSeries = Seq.fromList . fromSeries
+    {-# INLINABLE fromSeries #-}
+
+
+instance (Vector v a) => IsSeries (Set (k, a)) v k a where
+    toSeries :: Set (k, a) -> Series v k a
+    toSeries = fromDistinctAscList . Set.toAscList
+    {-# INLINABLE toSeries #-}
+
+    fromSeries :: Series v k a -> Set (k, a)
+    fromSeries = Set.fromDistinctAscList . toList
+    {-# INLINABLE fromSeries #-}
+
+
+-- | Get the first value of a 'Series'. If the 'Series' is empty,
+-- this function returns 'Nothing'.
+headM :: Vector v a => Series v k a -> Maybe a
+{-# INLINABLE headM #-}
+headM (MkSeries _ vs) = Vector.headM vs
+
+
+-- | Get the last value of a 'Series'. If the 'Series' is empty,
+-- this function returns 'Nothing'.
+lastM :: Vector v a => Series v k a -> Maybe a
+{-# INLINABLE lastM #-}
+lastM (MkSeries _ vs) = Vector.lastM vs
+
+
+-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
+take :: Vector v a => Int -> Series v k a -> Series v k a
+{-# INLINABLE take #-}
+take n (MkSeries ks vs) 
+    -- Index.take is O(log n) while Vector.take is O(1)
+    = MkSeries (Index.take n ks) (Vector.take n vs)
+
+
+-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
+drop :: Vector v a => Int -> Series v k a -> Series v k a
+{-# INLINABLE drop #-}
+drop n (MkSeries ks vs) 
+    -- Index.drop is O(log n) while Vector.drop is O(1)
+    = MkSeries (Index.drop n ks) (Vector.drop n vs)
+
+
+-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
+takeWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a
+{-# INLINABLE takeWhile #-}
+takeWhile f (MkSeries ix vs) = let taken = Vector.takeWhile f vs
+                 in MkSeries { index  = Index.take (Vector.length taken) ix
+                             , values = taken 
+                             }
+
+
+-- | \(O(n)\) Returns the complement of 'takeWhile'.
+dropWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a
+{-# INLINABLE dropWhile #-}
+dropWhile f (MkSeries ix vs) = let dropped = Vector.dropWhile f vs
+                 in MkSeries { index  = Index.drop (Index.size ix - Vector.length dropped) ix
+                             , values = dropped
+                             }
+
+
+-- | \(O(n)\) Map every element of a 'Series'.
+map :: (Vector v a, Vector v b) 
+    => (a -> b) -> Series v k a -> Series v k b
+{-# INLINABLE map #-}
+map f (MkSeries ix xs) = MkSeries ix $ Vector.map f xs
+
+
+-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
+mapWithKey :: (Vector v a, Vector v b) 
+           => (k -> a -> b) -> Series v k a -> Series v k b
+{-# INLINABLE mapWithKey #-}
+mapWithKey f (MkSeries ix xs) 
+    -- We're using boxed vectors to map because we don't want any restrictions
+    -- on the index type, i.e. we don't want the constraint Vector v k
+    = let vs = Boxed.zipWith f (Index.toAscVector ix) (Vector.convert xs)
+       in MkSeries ix (Vector.convert vs)
+
+
+-- | \(O(n \log n)\).
+-- Map each key in the index to another value. Note that the resulting series
+-- may have less elements, because each key must be unique.
+--
+-- In case new keys are conflicting, the first element is kept.
+mapIndex :: (Vector v a, Ord k, Ord g) => Series v k a -> (k -> g) -> Series v g a
+{-# INLINABLE mapIndex #-}
+mapIndex (MkSeries index values) f
+    -- Note that the order in which items are kept appears to be backwards;
+    -- See the examples for Data.Map.Strict.fromListWith
+    = let mapping   = MS.fromListWith (\_ x -> x) $ [(f k, k) | k <- Index.toAscList index]
+          newvalues = fmap (\k -> values Vector.! Index.Internal.findIndex k index) mapping
+       in toSeries newvalues
+
+
+-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
+concatMap :: (Vector v a, Vector v k, Vector v b, Vector v (k, a), Vector v (k, b), Ord k) 
+          => (a -> Series v k b) 
+          -> Series v k a 
+          -> Series v k b
+{-# INLINABLE concatMap #-}
+concatMap f = fromVector 
+            . Vector.concatMap (toVector . f . snd) 
+            . toVector
+
+
+instance (Vector v a, Ord k) => Semigroup (Series v k a) where
+    {-# INLINABLE (<>) #-}
+    (<>) :: Series v k a -> Series v k a -> Series v k a
+    -- Despite all my effort, merging via conversion to Map remains fastest.
+    xs <> ys = toSeries $ toStrictMap xs <> toStrictMap ys
+
+    {-# INLINABLE sconcat #-}
+    sconcat = toSeries . sconcat . fmap toStrictMap
+
+
+instance (Vector v a, Ord k) => Monoid (Series v k a) where
+    {-# INLINABLE mempty #-}
+    mempty :: Series v k a
+    mempty = MkSeries mempty Vector.empty
+
+    {-# INLINABLE mappend #-}
+    mappend :: Series v k a -> Series v k a -> Series v k a
+    mappend = (<>)
+
+    {-# INLINABLE mconcat #-}
+    mconcat :: [Series v k a] -> Series v k a
+    mconcat = toSeries . mconcat . fmap toStrictMap
+
+
+instance (Vector v a, Eq k, Eq a) => Eq (Series v k a) where
+    {-# INLINABLE (==) #-}
+    (==) :: Series v k a -> Series v k a -> Bool
+    (MkSeries ks1 vs1) == (MkSeries ks2 vs2) = (ks1 == ks2) && (vs1 `Vector.eq` vs2)
+
+
+instance (Vector v a, Ord (v a), Ord k, Ord a) => Ord (Series v k a) where
+    {-# INLINABLE compare #-}
+    compare :: Series v k a -> Series v k a -> Ordering
+    compare (MkSeries ks1 vs1) (MkSeries ks2 vs2) = compare (ks1, vs1) (ks2, vs2)
+
+
+instance (Functor v) => Functor (Series v k) where
+    {-# INLINABLE fmap #-}
+    fmap :: (a -> b) -> Series v k a -> Series v k b
+    fmap f (MkSeries ks vs) = MkSeries ks (fmap f vs)
+
+
+instance (forall a. Vector v a, Functor v) => FunctorWithIndex k (Series v k) where
+    {-# INLINABLE imap #-}
+    imap :: (k -> a -> b) -> Series v k a -> Series v k b
+    imap = mapWithKey
+
+
+-- Inlining all methods in 'Foldable'
+-- is important in order for folds over a boxed
+-- Series to have performance characteristics
+-- be as close as possible to boxed vectors 
+instance (Foldable v) => Foldable (Series v k) where
+    {-# INLINABLE fold #-}
+    fold :: Monoid m => Series v k m -> m
+    fold = Foldable.fold . values
+
+    {-# INLINABLE foldMap #-}
+    foldMap :: (Monoid m) => (a -> m) -> Series v k a -> m
+    foldMap f = Foldable.foldMap f . values
+
+    {-# INLINABLE foldMap' #-}
+    foldMap' :: (Monoid m) => (a -> m) -> Series v k a -> m
+    foldMap' f = Foldable.foldMap f . values
+
+    {-# INLINABLE foldr #-}
+    foldr :: (a -> b -> b) -> b -> Series v k a -> b
+    foldr f i = Foldable.foldr f i . values
+
+    {-# INLINABLE foldr' #-}
+    foldr' :: (a -> b -> b) -> b -> Series v k a -> b
+    foldr' f i = Foldable.foldr' f i . values
+
+    {-# INLINABLE foldl #-}
+    foldl :: (b -> a -> b) -> b -> Series v k a -> b
+    foldl f i = Foldable.foldl f i . values
+
+    {-# INLINABLE foldl' #-}
+    foldl' :: (b -> a -> b) -> b -> Series v k a -> b
+    foldl' f i = Foldable.foldl' f i . values
+
+    {-# INLINABLE foldr1 #-}
+    foldr1 :: (a -> a -> a) -> Series v k a -> a
+    foldr1 f = Foldable.foldr1 f . values
+
+    {-# INLINABLE foldl1 #-}
+    foldl1 :: (a -> a -> a) -> Series v k a -> a
+    foldl1 f = Foldable.foldl1 f . values
+
+    {-# INLINABLE toList #-}
+    toList :: Series v k a -> [a]
+    toList = Foldable.toList . values
+
+    {-# INLINABLE null #-}
+    null :: Series v k a -> Bool
+    null = Foldable.null . values
+
+    {-# INLINABLE length #-}
+    length :: Series v k a -> Int
+    length = Foldable.length . values
+
+    {-# INLINABLE elem #-}
+    elem :: Eq a => a -> Series v k a -> Bool
+    elem e = Foldable.elem e . values
+
+    {-# INLINABLE maximum #-}
+    maximum :: Ord a => Series v k a -> a
+    maximum = Foldable.maximum . values
+
+    {-# INLINABLE minimum #-}
+    minimum :: Ord a => Series v k a -> a
+    minimum = Foldable.minimum . values
+
+    {-# INLINABLE sum #-}
+    sum :: Num a => Series v k a -> a
+    sum = Foldable.sum . values
+
+    {-# INLINABLE product #-}
+    product :: Num a => Series v k a -> a
+    product = Foldable.product . values
+
+
+instance (forall a. Vector v a, Vector v k, Foldable v, Functor v) => FoldableWithIndex k (Series v k) where
+    {-# INLINABLE ifoldMap #-}
+    ifoldMap :: Monoid m => (k -> a -> m) -> Series v k a -> m
+    ifoldMap = foldMapWithKey
+
+
+instance (Foldable v) => Bifoldable (Series v) where
+    {-# INLINABLE bifoldMap #-}
+    bifoldMap :: Monoid m => (k -> m) -> (a -> m) -> Series v k a -> m
+    bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Foldable.foldMap fv vs
+
+
+instance (Traversable v) => Traversable (Series v k) where
+    {-# INLINABLE traverse #-}
+    traverse :: Applicative f
+             => (a -> f b) -> Series v k a -> f (Series v k b)
+    traverse f (MkSeries ix vs) = MkSeries ix <$> traverse f vs
+
+
+instance (forall a. Vector v a, Functor v, Foldable v, Ord k, Traversable v) => TraversableWithIndex k (Series v k) where
+    {-# INLINABLE itraverse #-}
+    itraverse :: Applicative f => (k -> a -> f b) -> Series v k a -> f (Series v k b)
+    itraverse = traverseWithKey
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series'.
+--
+-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
+-- account while folding.
+fold :: Vector v a 
+     => Fold a b  
+     -> Series v k a 
+     -> b
+fold (Fold step init' extract) 
+    = extract . Vector.foldl' step init' . values
+{-# INLINABLE fold #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
+--
+-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
+-- account while folding.
+foldM :: (Monad m, Vector v a)
+      => FoldM m a b  
+      -> Series v k a 
+      -> m b
+foldM (FoldM step init' extract) xs
+    = init' >>= \i -> Vector.foldM' step i (values xs) >>= extract
+{-# INLINABLE foldM #-}
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series', where the 'Fold' takes keys into account.
+foldWithKey :: (Vector v a, Vector v k, Vector v (k, a)) 
+            => Fold (k, a) b  
+            -> Series v k a 
+            -> b
+foldWithKey (Fold step init' extract) 
+    = extract . Vector.foldl' step init' . toVector
+{-# INLINABLE foldWithKey #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
+foldMWithKey :: (Monad m, Vector v a, Vector v k, Vector v (k, a)) 
+             => FoldM m (k, a) b
+             -> Series v k a 
+             -> m b
+foldMWithKey (FoldM step init' extract) xs
+    = init' >>= \i -> Vector.foldM' step i (toVector xs) >>= extract
+{-# INLINABLE foldMWithKey #-}
+
+
+-- | \(O(n)\) Fold over elements in a 'Series'.
+foldMap :: (Monoid m, Vector v a) => (a -> m) -> Series v k a -> m
+{-# INLINABLE foldMap #-}
+foldMap f = Vector.foldMap f . values
+
+
+-- | \(O(n)\) Fold over pairs of keys and elements in a 'Series'.
+-- See also 'bifoldMap'.
+foldMapWithKey :: (Monoid m, Vector v a, Vector v k, Vector v (k, a)) => (k -> a -> m) -> Series v k a -> m
+{-# INLINABLE foldMapWithKey #-}
+foldMapWithKey f = Vector.foldMap (uncurry f) . toVector
+
+
+-- | \(O(n)\) Fold over keys and elements separately in a 'Series'.
+-- See also 'foldMapWithKey'.
+bifoldMap :: (Vector v a, Monoid m) => (k -> m) -> (a -> m) -> Series v k a -> m
+{-# INLINABLE bifoldMap #-}
+bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Vector.foldMap fv vs
+
+
+-- | \(O(1)\) Extract the length of a 'Series'.
+length :: Vector v a => Series v k a -> Int
+{-# INLINABLE length #-}
+length = Vector.length . values
+
+
+-- | \(O(1)\) Test whether a 'Series' is empty.
+null :: Vector v a => Series v k a -> Bool
+{-# INLINABLE null #-}
+null = Vector.null . values
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, yielding a series of results.
+mapWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) 
+            => (k -> a -> m b) -> Series v k a -> m (Series v k b)
+{-# INLINABLE mapWithKeyM #-}
+mapWithKeyM f xs = let f' (key, val) = (key,) <$> f key val
+           in fmap fromList $ traverse f' $ toList xs
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, discarding the results.
+mapWithKeyM_ :: (Vector v a, Monad m) 
+             => (k -> a -> m b) -> Series v k a -> m ()
+{-# INLINABLE mapWithKeyM_ #-}
+mapWithKeyM_ f xs = let f' (key, val) = (key,) <$> f key val
+           in mapM_ f' $ toList xs
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- yielding a series of results.
+forWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) => Series v k a -> (k -> a -> m b) -> m (Series v k b)
+{-# INLINABLE forWithKeyM #-}
+forWithKeyM = flip mapWithKeyM
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- discarding the results.
+forWithKeyM_ :: (Vector v a, Monad m) => Series v k a -> (k -> a -> m b) -> m ()
+{-# INLINABLE forWithKeyM_ #-}
+forWithKeyM_ = flip mapWithKeyM_
+
+
+-- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. 
+traverseWithKey :: (Applicative t, Ord k, Traversable v, Vector v a, Vector v b, Vector v k, Vector v (k, a),  Vector v (k, b))
+                => (k -> a -> t b) 
+                -> Series v k a 
+                -> t (Series v k b)
+{-# INLINABLE traverseWithKey #-}
+traverseWithKey f = fmap fromVector 
+                  . traverse (\(k, x) -> (k,) <$> f k x) 
+                  . toVector
+
+
+instance (NFData (v a), NFData k) => NFData (Series v k a) where
+    rnf :: Series v k a -> ()
+    rnf (MkSeries ks vs) = rnf ks `seq` rnf vs
+
+
+instance (Vector v a, Ord k, Show k, Show a) => Show (Series v k a) where
+    show :: Series v k a -> String
+    show = display
+
+
+-- | Options controlling how to display 'Series' in the 'displayWith' function.
+-- Default options are provided by 'defaultDisplayOptions'.
+--
+-- To help with creating 'DisplayOptions', see 'noLongerThan'.
+data DisplayOptions k a
+    = DisplayOptions
+    { maximumNumberOfRows  :: Int
+    -- ^ Maximum number of rows shown. These rows will be distributed evenly
+    -- between the start of the 'Series' and the end. 
+    , indexHeader          :: String
+    -- ^ Header of the index column.
+    , valuesHeader         :: String
+    -- ^ Header of the values column.
+    , keyDisplayFunction   :: k -> String
+    -- ^ Function used to display keys from the 'Series'. Use 'noLongerThan'
+    -- to control the width of the index column.
+    , valueDisplayFunction :: a -> String
+    -- ^ Function used to display values from the 'Series'. Use 'noLongerThan'
+    -- to control the width of the values column.
+    }
+
+
+-- | Default 'Series' display options.
+defaultDisplayOptions :: (Show k, Show a) => DisplayOptions k a
+defaultDisplayOptions 
+    = DisplayOptions { maximumNumberOfRows  = 6
+                     , indexHeader          = "index"
+                     , valuesHeader         = "values"
+                     , keyDisplayFunction   = show
+                     , valueDisplayFunction = show
+                     }
+
+
+-- | This function modifies existing functions to limit the width of its result.
+--
+-- >>> let limit7 = (show :: Int -> String) `noLongerThan` 7
+-- >>> limit7 123456789
+-- "123456..."
+noLongerThan :: (a -> String) -> Int -> (a -> String)
+noLongerThan f len x 
+    = let raw = f x
+       in if List.length raw <= max 0 len
+        then raw
+        else List.take (List.length raw - 3) raw <> "..."
+
+
+-- | Display a 'Series' using default 'DisplayOptions'.
+display :: (Vector v a, Show k, Show a) 
+        => Series v k a 
+        -> String
+display = displayWith defaultDisplayOptions
+
+
+-- | Display a 'Series' using customizable 'DisplayOptions'.
+displayWith :: (Vector v a) 
+            => DisplayOptions k a
+            -> Series v k a 
+            -> String
+displayWith DisplayOptions{..} xs
+    = formatGrid $ if length xs > max 0 maximumNumberOfRows
+        then let headlength = max 0 maximumNumberOfRows `div` 2
+                 taillength = max 0 maximumNumberOfRows - headlength
+              in mconcat [ [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ take headlength xs]
+                         , [ ("...", "...") ]
+                         , [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ drop (length xs - taillength) xs]
+                         ] 
+        else [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList xs ]
+
+    where
+        -- | Format a grid represented by a list of rows, where every row is a list of items
+        -- All columns will have a fixed width
+        formatGrid :: [ (String, String) ] -- List of rows
+                   -> String
+        formatGrid rows = mconcat $ List.intersperse "\n" 
+                                  $ [ pad indexWidth k <> " | " <> pad valuesWidth v 
+                                    | (k, v) <- rows'
+                                    ] 
+            where
+                rows' = [ (indexHeader, valuesHeader) ] <> [ ("-----", "------")] <> rows
+                (indexCol, valuesCol) = unzip rows'
+                width col = maximum (P.length <$> col)
+                indexWidth = width indexCol
+                valuesWidth = width valuesCol
+
+                -- | Pad a string to a minimum of @n@ characters wide.
+                pad :: Int -> String -> String 
+                pad n s
+                    | n <= P.length s = s
+                    | otherwise     = replicate (n - P.length s) ' ' <> s
diff --git a/src/Data/Series/Generic/Internal.hs b/src/Data/Series/Generic/Internal.hs
--- a/src/Data/Series/Generic/Internal.hs
+++ b/src/Data/Series/Generic/Internal.hs
@@ -1,27 +1,27 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Series.Generic.Internal
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT
--- Maintainer  :  laurent.decotret@outlook.com
--- Portability :  portable
---
--- = WARNING
---
--- This module is considered __internal__. Using the 'Series' constructor
--- directly may result in loss or corruption of data if not handled carefully.
---
--- The Package Versioning Policy still applies.
-
-module Data.Series.Generic.Internal ( 
-    -- * Constructor
-    Series(..),
-    -- * Unsafe construction
-    fromDistinctAscList,
-    fromDistinctAscVector,
-    -- * Unsafe selection
-    selectSubset
-) where
-
-import Data.Series.Generic.Definition   ( Series(..), fromDistinctAscList, fromDistinctAscVector )
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Series.Generic.Internal
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- = WARNING
+--
+-- This module is considered __internal__. Using the 'Series' constructor
+-- directly may result in loss or corruption of data if not handled carefully.
+--
+-- The Package Versioning Policy still applies.
+
+module Data.Series.Generic.Internal ( 
+    -- * Constructor
+    Series(..),
+    -- * Unsafe construction
+    fromDistinctAscList,
+    fromDistinctAscVector,
+    -- * Unsafe selection
+    selectSubset
+) where
+
+import Data.Series.Generic.Definition   ( Series(..), fromDistinctAscList, fromDistinctAscVector )
 import Data.Series.Generic.View         ( selectSubset )
diff --git a/src/Data/Series/Generic/Scans.hs b/src/Data/Series/Generic/Scans.hs
--- a/src/Data/Series/Generic/Scans.hs
+++ b/src/Data/Series/Generic/Scans.hs
@@ -1,112 +1,112 @@
-
-module Data.Series.Generic.Scans (
-    postscanl,
-    prescanl,
-
-    -- * Filling missing data
-    forwardFill,
-) where
-
-import           Data.Series.Generic.Definition ( Series(..) )
-
-import           Data.Vector.Generic            ( Vector )
-import qualified Data.Vector.Generic            as Vector    
-
--- $setup
--- >>> import qualified Data.Series.Generic ( Series )
--- >>> import qualified Data.Series.Generic as Series
--- >>> import qualified Data.Series.Index as Index
-
--- | \(O(n)\) Left-to-right postscan.
---
--- >>> import qualified Data.Vector as V 
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> postscanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      3
---     2 |      6
---     3 |     10
-postscanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a
-{-# INLINABLE postscanl #-}
-postscanl f s (MkSeries ix vs) = MkSeries ix $ Vector.postscanl f s vs
-
-
--- | \(O(n)\) Left-to-right prescan.
---
--- >>> import qualified Data.Vector as V 
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> prescanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      0
---     1 |      1
---     2 |      3
---     3 |      6
-prescanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a
-{-# INLINABLE prescanl #-}
-prescanl f s (MkSeries ix vs) = MkSeries ix $ Vector.prescanl f s vs
-
-
--- | \(O(n)\) Replace all instances of 'Nothing' with the last previous
--- value which was not 'Nothing'.
---
--- >>> import qualified Data.Vector as V 
--- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)
--- >>> xs
--- index |  values
--- ----- |  ------
---     0 |  Just 1
---     1 |  Just 2
---     2 | Nothing
---     3 |  Just 3
--- >>> forwardFill 0 xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      2
---     3 |      3
---
--- If the first entry of the series is missing, the first input to 'forwardFill' will be used:
---
--- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)
--- >>> ys
--- index |  values
--- ----- |  ------
---     0 | Nothing
---     1 |  Just 2
---     2 | Nothing
---     3 |  Just 3
--- >>> forwardFill 0 ys
--- index | values
--- ----- | ------
---     0 |      0
---     1 |      2
---     2 |      2
---     3 |      3
-forwardFill :: (Vector v a, Vector v (Maybe a))
-            => a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.
-            -> Series v k (Maybe a)
-            -> Series v k a
-{-# INLINABLE forwardFill #-}
-forwardFill = postscanl go
-    where
-        go :: a -> Maybe a -> a
-        go lastValid Nothing = lastValid
-        go _        (Just v) = v
+
+module Data.Series.Generic.Scans (
+    postscanl,
+    prescanl,
+
+    -- * Filling missing data
+    forwardFill,
+) where
+
+import           Data.Series.Generic.Definition ( Series(..) )
+
+import           Data.Vector.Generic            ( Vector )
+import qualified Data.Vector.Generic            as Vector    
+
+-- $setup
+-- >>> import qualified Data.Series.Generic ( Series )
+-- >>> import qualified Data.Series.Generic as Series
+-- >>> import qualified Data.Series.Index as Index
+
+-- | \(O(n)\) Left-to-right postscan.
+--
+-- >>> import qualified Data.Vector as V 
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> postscanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      3
+--     2 |      6
+--     3 |     10
+postscanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a
+{-# INLINABLE postscanl #-}
+postscanl f s (MkSeries ix vs) = MkSeries ix $ Vector.postscanl f s vs
+
+
+-- | \(O(n)\) Left-to-right prescan.
+--
+-- >>> import qualified Data.Vector as V 
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> prescanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      0
+--     1 |      1
+--     2 |      3
+--     3 |      6
+prescanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a
+{-# INLINABLE prescanl #-}
+prescanl f s (MkSeries ix vs) = MkSeries ix $ Vector.prescanl f s vs
+
+
+-- | \(O(n)\) Replace all instances of 'Nothing' with the last previous
+-- value which was not 'Nothing'.
+--
+-- >>> import qualified Data.Vector as V 
+-- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)
+-- >>> xs
+-- index |  values
+-- ----- |  ------
+--     0 |  Just 1
+--     1 |  Just 2
+--     2 | Nothing
+--     3 |  Just 3
+-- >>> forwardFill 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      2
+--     3 |      3
+--
+-- If the first entry of the series is missing, the first input to 'forwardFill' will be used:
+--
+-- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)
+-- >>> ys
+-- index |  values
+-- ----- |  ------
+--     0 | Nothing
+--     1 |  Just 2
+--     2 | Nothing
+--     3 |  Just 3
+-- >>> forwardFill 0 ys
+-- index | values
+-- ----- | ------
+--     0 |      0
+--     1 |      2
+--     2 |      2
+--     3 |      3
+forwardFill :: (Vector v a, Vector v (Maybe a))
+            => a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.
+            -> Series v k (Maybe a)
+            -> Series v k a
+{-# INLINABLE forwardFill #-}
+forwardFill = postscanl go
+    where
+        go :: a -> Maybe a -> a
+        go lastValid Nothing = lastValid
+        go _        (Just v) = v
diff --git a/src/Data/Series/Generic/View.hs b/src/Data/Series/Generic/View.hs
--- a/src/Data/Series/Generic/View.hs
+++ b/src/Data/Series/Generic/View.hs
@@ -1,336 +1,336 @@
-module Data.Series.Generic.View (
-    -- * Accessing a single element
-    (!),
-    at,
-    iat,
-
-    -- * Bulk access
-    select,
-    slice,
-    selectWhere,
-    selectSubset,
-    Selection,
-
-    -- * Resizing
-    require,
-    requireWith,
-    filter,
-    filterWithKey,
-    catMaybes,
-    dropIndex,
-
-    -- * Creating and accessing ranges
-    Range(..),
-    to,
-    from,
-    upto,
-) where
-
-
-import           Data.Functor           ( (<&>) )
-import           Data.Series.Index      ( Index )
-import qualified Data.Series.Index      as Index
-import qualified Data.Series.Index.Internal as Index.Internal
-import           Data.Maybe             ( fromJust, isJust )
-import           Data.Series.Generic.Definition ( Series(..) )
-import qualified Data.Series.Generic.Definition as G
-import           Data.Set               ( Set )
-import qualified Data.Set               as Set
-import qualified Data.Vector            as Boxed
-import           Data.Vector.Generic    ( Vector )
-import qualified Data.Vector.Generic    as Vector
-
-import           Prelude                hiding ( filter )
-
--- $setup
--- >>> import qualified Data.Series as Series
--- >>> import qualified Data.Series.Index as Index 
-
-infixr 9 `to` -- Ensure that @to@ binds strongest
-infixl 1 `select` 
-
-
--- | \(O(1)\). Extract a single value from a series, by index. 
--- An exception is thrown if the index is out-of-bounds.
---
--- A safer alternative is @iat@, which returns 'Nothing' if the index is
--- out-of-bounds.
-(!) :: Vector v a => Series v k a -> Int -> a
-(MkSeries _ vs) ! ix = (Vector.!) vs ix
-
-
--- | \(O(\log n)\). Extract a single value from a series, by key.
-at :: (Vector v a, Ord k) => Series v k a -> k -> Maybe a
-at (MkSeries ks vs) k = Index.lookupIndex k ks <&> Vector.unsafeIndex vs 
-{-# INLINABLE at #-}
-
-
--- | \(O(1)\). Extract a single value from a series, by index.
-iat :: Vector v a => Series v k a -> Int -> Maybe a
-iat (MkSeries _ vs) =  (Vector.!?) vs
-{-# INLINABLE iat #-}
-
-
--- | Require a series with a new index.
--- Contrary to 'select', all keys in @'Index' k@ will be present in the re-indexed series.
-require :: (Vector v a, Vector v (Maybe a), Ord k) 
-        => Series v k a -> Index k -> Series v k (Maybe a)
-{-# INLINABLE require #-}
-require = requireWith (const Nothing) Just
-
-
--- | Generalization of 'require', which maps missing keys to values.
--- This is particularly useful for 'Vector' instances which don't support 'Maybe', like "Data.Vector.Unboxed".
-requireWith :: (Vector v a, Vector v b, Ord k)
-            => (k -> b)  -- ^ Function to apply to keys which are missing from the input series, but required in the input index
-            -> (a -> b)  -- ^ Function to apply to values which are in the input series and input index.
-            -> Series v k a 
-            -> Index k 
-            -> Series v k b
-{-# INLINABLE requireWith #-}
-requireWith replacement f xs ss 
-    = let existingKeys = index xs `Index.intersection` ss
-          newKeys      = ss `Index.difference` existingKeys
-       in G.map f (xs `selectSubset` existingKeys) <> MkSeries newKeys (Vector.fromListN (Index.size newKeys) (replacement <$> Index.toAscList newKeys))
-
-
--- | \(O(n)\) Drop the index of a series by replacing it with an @Int@-based index. Values will
--- be indexed from 0.
-dropIndex :: Series v k a -> Series v Int a
-{-# INLINABLE dropIndex #-}
-dropIndex (MkSeries ks vs) = MkSeries (Index.Internal.fromDistinctAscList [0..Index.size ks - 1]) vs
-
-
--- | Filter elements. Only elements for which the predicate is @True@ are kept. 
--- Notice that the filtering is done on the values, not on the keys; see 'filterWithKey'
--- to filter while taking keys into account.
-filter :: (Vector v a, Vector v Int, Ord k) 
-       => (a -> Bool) -> Series v k a -> Series v k a
-{-# INLINABLE filter #-}
-filter predicate xs@(MkSeries ks vs) 
-    = let indicesToKeep = Vector.findIndices predicate vs
-          keysToKeep = Index.Internal.fromDistinctAscList [Index.Internal.elemAt ix ks | ix <- Vector.toList indicesToKeep]
-       in xs `select` keysToKeep
-
-
--- | Filter elements, taking into account the corresponding key. Only elements for which 
--- the predicate is @True@ are kept. 
-filterWithKey :: (Vector v a, Vector v Int, Vector v Bool, Ord k) 
-              => (k -> a -> Bool) 
-              -> Series v k a 
-              -> Series v k a
-{-# INLINABLE filterWithKey #-}
-filterWithKey predicate xs = xs `selectWhere` G.mapWithKey predicate xs
-
-
--- | \(O(n)\) Only keep elements which are @'Just' v@. 
-catMaybes :: (Vector v a, Vector v (Maybe a), Vector v Int, Ord k) 
-       => Series v k (Maybe a) -> Series v k a
-{-# INLINABLE catMaybes #-}
-catMaybes = G.map fromJust . filter isJust
-
-
--- | Datatype representing an /inclusive/ range of keys, which can either be bounded
--- or unbounded. The canonical ways to construct a 'Range' are to use 'to', 'from', and 'upto':
---
--- >>> 'a' `to` 'z'
--- Range (from 'a' to 'z')
--- >>> from 'd'
--- Range (from 'd')
--- >>> upto 'q'
--- Range (up to 'q')
---
--- A 'Range' can be used to efficiently select a sub-series with 'select'.
-data Range k 
-    = BoundedRange k k
-    | From k
-    | UpTo k
-    deriving (Eq)
-
-
-instance Show k => Show (Range k) where
-    show :: Range k -> String
-    show (BoundedRange start stop) = mconcat ["Range (from ", show start, " to ", show stop, ")"]
-    show (From start) = mconcat ["Range (from ", show start, ")"]
-    show (UpTo stop) = mconcat ["Range (up to ", show stop, ")"]
-
-
--- | Find the keys which are in range. In case of an empty 'Series',
--- the returned value is 'Nothing'.
-keysInRange :: Ord k => Series v k a -> Range k -> Maybe (k, k)
-{-# INLINABLE keysInRange #-}
-keysInRange (MkSeries ks _) rng
-    = let inrange = inRange rng
-       in if Set.null inrange 
-            then Nothing
-            else Just (Set.findMin inrange, Set.findMax inrange)
-    where
-        inRange (BoundedRange start stop)  = Set.takeWhileAntitone (<= stop) 
-                                           $ Set.dropWhileAntitone (< start) $ Index.toSet ks
-        inRange (From start)               = Set.dropWhileAntitone (< start) $ Index.toSet ks
-        inRange (UpTo stop)                = Set.takeWhileAntitone (<= stop) $ Index.toSet ks
-
-
--- | Create a bounded 'Range' which can be used for slicing. This function
--- is expected to be used in conjunction with 'select'.
---
--- For unbound ranges, see 'from' and 'upto'.
-to :: Ord k => k -> k -> Range k
-to k1 k2 = BoundedRange (min k1 k2) (max k1 k2)
-
-
--- | Create an unbounded 'Range' which can be used for slicing. 
--- This function is expected to be used in conjunction with 'select'. 
---
--- For bound ranges, see 'to'.
-from :: k -> Range k
-from = From
-
-
--- | Create an unbounded 'Range' which can be used for slicing. This function
--- is expected to be used in conjunction with 'select'. 
---
--- For bound ranges, see 'to'.
-upto :: k -> Range k
-upto = UpTo
-
-
--- | Class for datatypes which can be used to select sub-series using 'select'.
---
--- There are two use-cases for 'select':
---
---  * Bulk random-access (selecting from an 'Index' of keys);
---  * Bulk ordered access (selecting from a 'Range' of keys).
---
--- See the documentation for 'select'.
-class Selection s where
-    -- | Select a subseries. There are two main ways to do this.
-    --
-    -- The first way to do this is to select a sub-series based on keys:
-    --
-    -- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
-    -- >>> xs `select` Index.fromList ['a', 'd']
-    -- index | values
-    -- ----- | ------
-    --   'a' |     10
-    --   'd' |     40
-    --
-    -- The second way to select a sub-series is to select all keys in a range:
-    --
-    -- >>> xs `select` 'b' `to` 'c'
-    -- index | values
-    -- ----- | ------
-    --   'b' |     20
-    --   'c' |     30
-    --
-    -- Such ranges can also be unbounded. (i.e. all keys smaller or larger than some key), like so:
-    --
-    -- >>> xs `select` upto 'c'
-    -- index | values
-    -- ----- | ------
-    --   'a' |     10
-    --   'b' |     20
-    --   'c' |     30
-    -- >>> xs `select` from 'c'
-    -- index | values
-    -- ----- | ------
-    --   'c' |     30
-    --   'd' |     40
-    --
-    -- Note that with 'select', you'll always get a sub-series; if you ask for a key which is not
-    -- in the series, it'll be ignored:
-    --
-    -- >>> xs `select` Index.fromList ['a', 'd', 'e']
-    -- index | values
-    -- ----- | ------
-    --   'a' |     10
-    --   'd' |     40
-    --
-    -- See 'require' if you want to ensure that all keys are present.
-    select :: (Vector v a, Ord k) => Series v k a -> s k -> Series v k a
-
-
-instance Selection Index where
-    -- | Select all keys in 'Index' from a series. Keys which are not
-    -- in the series are ignored.
-    select :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a
-    {-# INLINABLE select #-}
-    select xs ss
-        = let selectedKeys = index xs `Index.intersection` ss
-            -- Surprisingly, using `Vector.backpermute` does not
-            -- perform as well as `Vector.map (Vector.unsafeIndex vs)`
-            -- for large Series
-           in xs `selectSubset` selectedKeys
-
-
--- | Selecting a sub-series from a 'Set' is a convenience
--- function. Internally, the 'Set' is converted to an index first.
-instance Selection Set where
-    select :: (Vector v a, Ord k) => Series v k a -> Set k -> Series v k a
-    {-# INLINABLE select #-}
-    select xs = select xs . Index.fromSet
-
-
--- | Selecting a sub-series from a list is a convenience
--- function. Internally, the list is converted to an index first.
-instance Selection [] where
-    select :: (Vector v a, Ord k) => Series v k a -> [k] -> Series v k a
-    {-# INLINABLE select #-}
-    select xs = select xs . Index.fromList
-
-
--- | Selecting a sub-series based on a @Range@ is most performant.
--- Constructing a @Range@ is most convenient using the 'to' function.
-instance Selection Range where
-    select :: (Vector v a, Ord k) => Series v k a -> Range k -> Series v k a
-    {-# INLINABLE select #-}
-    select series rng = case keysInRange series rng of 
-        Nothing              -> mempty
-        Just (kstart, kstop) -> let indexOf xs k = Index.Internal.findIndex k (index xs)
-                                 in slice (series `indexOf` kstart) (1 + series `indexOf` kstop) series
-
-
--- | Select a sub-series from a series matching a condition.
-selectWhere :: (Vector v a, Vector v Int, Vector v Bool, Ord k) => Series v k a -> Series v k Bool -> Series v k a
-{-# INLINABLE selectWhere #-}
-selectWhere xs ys = xs `select` Index.fromSet keysWhereTrue
-    where
-        (MkSeries _ cond) = ys `select` index xs
-        whereValuesAreTrue = Set.fromDistinctAscList $ Vector.toList (Vector.findIndices id cond)
-        keysWhereTrue = Set.mapMonotonic (`Index.Internal.elemAt` index xs) whereValuesAreTrue
-
-
--- | Implementation of `select` where the selection keys are known
--- to be a subset of the series. This precondition is NOT checked.
---
--- This is a performance optimization and therefore is not normally exposed.
-selectSubset :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a
-{-# INLINABLE selectSubset #-}
-selectSubset (MkSeries ks vs) ss
-    -- TODO: 
-    --   Is it possible to scan over the series once
-    --   while filtering away on keys? Initial attempts did not lead
-    --   to performance improvements, but I can't imagine that calling
-    --   `Index.Internal.findIndex` repeatedly is efficient
-    --
-    --   Maybe use Data.Series.Index.indexed to traverse the index once?
-    = MkSeries ss $ Boxed.convert
-                  $ Boxed.map (Vector.unsafeIndex vs . (`Index.Internal.findIndex` ks))
-                  $ Index.toAscVector ss
-
-
--- | \(O(\log n)\) Yield a subseries based on integer indices. The end index is not included.
-slice :: Vector v a
-      => Int -- ^ Start index
-      -> Int -- ^ End index, which is not included
-      -> Series v k a 
-      -> Series v k a
-{-# INLINABLE slice #-}
-slice start stop (MkSeries ks vs) 
-    = let stop' = min (Vector.length vs) stop
-    -- Index.take is O(log n) while Vector.slice is O(1)
-    in MkSeries { index  = Index.take (stop' - start) $ Index.drop start ks
-                , values = Vector.slice start (stop' - start) vs
-                }
-
-
+module Data.Series.Generic.View (
+    -- * Accessing a single element
+    (!),
+    at,
+    iat,
+
+    -- * Bulk access
+    select,
+    slice,
+    selectWhere,
+    selectSubset,
+    Selection,
+
+    -- * Resizing
+    require,
+    requireWith,
+    filter,
+    filterWithKey,
+    catMaybes,
+    dropIndex,
+
+    -- * Creating and accessing ranges
+    Range(..),
+    to,
+    from,
+    upto,
+) where
+
+
+import           Data.Functor           ( (<&>) )
+import           Data.Series.Index      ( Index )
+import qualified Data.Series.Index      as Index
+import qualified Data.Series.Index.Internal as Index.Internal
+import           Data.Maybe             ( fromJust, isJust )
+import           Data.Series.Generic.Definition ( Series(..) )
+import qualified Data.Series.Generic.Definition as G
+import           Data.Set               ( Set )
+import qualified Data.Set               as Set
+import qualified Data.Vector            as Boxed
+import           Data.Vector.Generic    ( Vector )
+import qualified Data.Vector.Generic    as Vector
+
+import           Prelude                hiding ( filter )
+
+-- $setup
+-- >>> import qualified Data.Series as Series
+-- >>> import qualified Data.Series.Index as Index 
+
+infixr 9 `to` -- Ensure that @to@ binds strongest
+infixl 1 `select` 
+
+
+-- | \(O(1)\). Extract a single value from a series, by index. 
+-- An exception is thrown if the index is out-of-bounds.
+--
+-- A safer alternative is @iat@, which returns 'Nothing' if the index is
+-- out-of-bounds.
+(!) :: Vector v a => Series v k a -> Int -> a
+(MkSeries _ vs) ! ix = (Vector.!) vs ix
+
+
+-- | \(O(\log n)\). Extract a single value from a series, by key.
+at :: (Vector v a, Ord k) => Series v k a -> k -> Maybe a
+at (MkSeries ks vs) k = Index.lookupIndex k ks <&> Vector.unsafeIndex vs 
+{-# INLINABLE at #-}
+
+
+-- | \(O(1)\). Extract a single value from a series, by index.
+iat :: Vector v a => Series v k a -> Int -> Maybe a
+iat (MkSeries _ vs) =  (Vector.!?) vs
+{-# INLINABLE iat #-}
+
+
+-- | Require a series with a new index.
+-- Contrary to 'select', all keys in @'Index' k@ will be present in the re-indexed series.
+require :: (Vector v a, Vector v (Maybe a), Ord k) 
+        => Series v k a -> Index k -> Series v k (Maybe a)
+{-# INLINABLE require #-}
+require = requireWith (const Nothing) Just
+
+
+-- | Generalization of 'require', which maps missing keys to values.
+-- This is particularly useful for 'Vector' instances which don't support 'Maybe', like "Data.Vector.Unboxed".
+requireWith :: (Vector v a, Vector v b, Ord k)
+            => (k -> b)  -- ^ Function to apply to keys which are missing from the input series, but required in the input index
+            -> (a -> b)  -- ^ Function to apply to values which are in the input series and input index.
+            -> Series v k a 
+            -> Index k 
+            -> Series v k b
+{-# INLINABLE requireWith #-}
+requireWith replacement f xs ss 
+    = let existingKeys = index xs `Index.intersection` ss
+          newKeys      = ss `Index.difference` existingKeys
+       in G.map f (xs `selectSubset` existingKeys) <> MkSeries newKeys (Vector.fromListN (Index.size newKeys) (replacement <$> Index.toAscList newKeys))
+
+
+-- | \(O(n)\) Drop the index of a series by replacing it with an @Int@-based index. Values will
+-- be indexed from 0.
+dropIndex :: Series v k a -> Series v Int a
+{-# INLINABLE dropIndex #-}
+dropIndex (MkSeries ks vs) = MkSeries (Index.Internal.fromDistinctAscList [0..Index.size ks - 1]) vs
+
+
+-- | Filter elements. Only elements for which the predicate is @True@ are kept. 
+-- Notice that the filtering is done on the values, not on the keys; see 'filterWithKey'
+-- to filter while taking keys into account.
+filter :: (Vector v a, Vector v Int, Ord k) 
+       => (a -> Bool) -> Series v k a -> Series v k a
+{-# INLINABLE filter #-}
+filter predicate xs@(MkSeries ks vs) 
+    = let indicesToKeep = Vector.findIndices predicate vs
+          keysToKeep = Index.Internal.fromDistinctAscList [Index.Internal.elemAt ix ks | ix <- Vector.toList indicesToKeep]
+       in xs `select` keysToKeep
+
+
+-- | Filter elements, taking into account the corresponding key. Only elements for which 
+-- the predicate is @True@ are kept. 
+filterWithKey :: (Vector v a, Vector v Int, Vector v Bool, Ord k) 
+              => (k -> a -> Bool) 
+              -> Series v k a 
+              -> Series v k a
+{-# INLINABLE filterWithKey #-}
+filterWithKey predicate xs = xs `selectWhere` G.mapWithKey predicate xs
+
+
+-- | \(O(n)\) Only keep elements which are @'Just' v@. 
+catMaybes :: (Vector v a, Vector v (Maybe a), Vector v Int, Ord k) 
+       => Series v k (Maybe a) -> Series v k a
+{-# INLINABLE catMaybes #-}
+catMaybes = G.map fromJust . filter isJust
+
+
+-- | Datatype representing an /inclusive/ range of keys, which can either be bounded
+-- or unbounded. The canonical ways to construct a 'Range' are to use 'to', 'from', and 'upto':
+--
+-- >>> 'a' `to` 'z'
+-- Range (from 'a' to 'z')
+-- >>> from 'd'
+-- Range (from 'd')
+-- >>> upto 'q'
+-- Range (up to 'q')
+--
+-- A 'Range' can be used to efficiently select a sub-series with 'select'.
+data Range k 
+    = BoundedRange k k
+    | From k
+    | UpTo k
+    deriving (Eq)
+
+
+instance Show k => Show (Range k) where
+    show :: Range k -> String
+    show (BoundedRange start stop) = mconcat ["Range (from ", show start, " to ", show stop, ")"]
+    show (From start) = mconcat ["Range (from ", show start, ")"]
+    show (UpTo stop) = mconcat ["Range (up to ", show stop, ")"]
+
+
+-- | Find the keys which are in range. In case of an empty 'Series',
+-- the returned value is 'Nothing'.
+keysInRange :: Ord k => Series v k a -> Range k -> Maybe (k, k)
+{-# INLINABLE keysInRange #-}
+keysInRange (MkSeries ks _) rng
+    = let inrange = inRange rng
+       in if Set.null inrange 
+            then Nothing
+            else Just (Set.findMin inrange, Set.findMax inrange)
+    where
+        inRange (BoundedRange start stop)  = Set.takeWhileAntitone (<= stop) 
+                                           $ Set.dropWhileAntitone (< start) $ Index.toSet ks
+        inRange (From start)               = Set.dropWhileAntitone (< start) $ Index.toSet ks
+        inRange (UpTo stop)                = Set.takeWhileAntitone (<= stop) $ Index.toSet ks
+
+
+-- | Create a bounded 'Range' which can be used for slicing. This function
+-- is expected to be used in conjunction with 'select'.
+--
+-- For unbound ranges, see 'from' and 'upto'.
+to :: Ord k => k -> k -> Range k
+to k1 k2 = BoundedRange (min k1 k2) (max k1 k2)
+
+
+-- | Create an unbounded 'Range' which can be used for slicing. 
+-- This function is expected to be used in conjunction with 'select'. 
+--
+-- For bound ranges, see 'to'.
+from :: k -> Range k
+from = From
+
+
+-- | Create an unbounded 'Range' which can be used for slicing. This function
+-- is expected to be used in conjunction with 'select'. 
+--
+-- For bound ranges, see 'to'.
+upto :: k -> Range k
+upto = UpTo
+
+
+-- | Class for datatypes which can be used to select sub-series using 'select'.
+--
+-- There are two use-cases for 'select':
+--
+--  * Bulk random-access (selecting from an 'Index' of keys);
+--  * Bulk ordered access (selecting from a 'Range' of keys).
+--
+-- See the documentation for 'select'.
+class Selection s where
+    -- | Select a subseries. There are two main ways to do this.
+    --
+    -- The first way to do this is to select a sub-series based on keys:
+    --
+    -- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
+    -- >>> xs `select` Index.fromList ['a', 'd']
+    -- index | values
+    -- ----- | ------
+    --   'a' |     10
+    --   'd' |     40
+    --
+    -- The second way to select a sub-series is to select all keys in a range:
+    --
+    -- >>> xs `select` 'b' `to` 'c'
+    -- index | values
+    -- ----- | ------
+    --   'b' |     20
+    --   'c' |     30
+    --
+    -- Such ranges can also be unbounded. (i.e. all keys smaller or larger than some key), like so:
+    --
+    -- >>> xs `select` upto 'c'
+    -- index | values
+    -- ----- | ------
+    --   'a' |     10
+    --   'b' |     20
+    --   'c' |     30
+    -- >>> xs `select` from 'c'
+    -- index | values
+    -- ----- | ------
+    --   'c' |     30
+    --   'd' |     40
+    --
+    -- Note that with 'select', you'll always get a sub-series; if you ask for a key which is not
+    -- in the series, it'll be ignored:
+    --
+    -- >>> xs `select` Index.fromList ['a', 'd', 'e']
+    -- index | values
+    -- ----- | ------
+    --   'a' |     10
+    --   'd' |     40
+    --
+    -- See 'require' if you want to ensure that all keys are present.
+    select :: (Vector v a, Ord k) => Series v k a -> s k -> Series v k a
+
+
+instance Selection Index where
+    -- | Select all keys in 'Index' from a series. Keys which are not
+    -- in the series are ignored.
+    select :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a
+    {-# INLINABLE select #-}
+    select xs ss
+        = let selectedKeys = index xs `Index.intersection` ss
+            -- Surprisingly, using `Vector.backpermute` does not
+            -- perform as well as `Vector.map (Vector.unsafeIndex vs)`
+            -- for large Series
+           in xs `selectSubset` selectedKeys
+
+
+-- | Selecting a sub-series from a 'Set' is a convenience
+-- function. Internally, the 'Set' is converted to an index first.
+instance Selection Set where
+    select :: (Vector v a, Ord k) => Series v k a -> Set k -> Series v k a
+    {-# INLINABLE select #-}
+    select xs = select xs . Index.fromSet
+
+
+-- | Selecting a sub-series from a list is a convenience
+-- function. Internally, the list is converted to an index first.
+instance Selection [] where
+    select :: (Vector v a, Ord k) => Series v k a -> [k] -> Series v k a
+    {-# INLINABLE select #-}
+    select xs = select xs . Index.fromList
+
+
+-- | Selecting a sub-series based on a @Range@ is most performant.
+-- Constructing a @Range@ is most convenient using the 'to' function.
+instance Selection Range where
+    select :: (Vector v a, Ord k) => Series v k a -> Range k -> Series v k a
+    {-# INLINABLE select #-}
+    select series rng = case keysInRange series rng of 
+        Nothing              -> mempty
+        Just (kstart, kstop) -> let indexOf xs k = Index.Internal.findIndex k (index xs)
+                                 in slice (series `indexOf` kstart) (1 + series `indexOf` kstop) series
+
+
+-- | Select a sub-series from a series matching a condition.
+selectWhere :: (Vector v a, Vector v Int, Vector v Bool, Ord k) => Series v k a -> Series v k Bool -> Series v k a
+{-# INLINABLE selectWhere #-}
+selectWhere xs ys = xs `select` Index.fromSet keysWhereTrue
+    where
+        (MkSeries _ cond) = ys `select` index xs
+        whereValuesAreTrue = Set.fromDistinctAscList $ Vector.toList (Vector.findIndices id cond)
+        keysWhereTrue = Set.mapMonotonic (`Index.Internal.elemAt` index xs) whereValuesAreTrue
+
+
+-- | Implementation of `select` where the selection keys are known
+-- to be a subset of the series. This precondition is NOT checked.
+--
+-- This is a performance optimization and therefore is not normally exposed.
+selectSubset :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a
+{-# INLINABLE selectSubset #-}
+selectSubset (MkSeries ks vs) ss
+    -- TODO: 
+    --   Is it possible to scan over the series once
+    --   while filtering away on keys? Initial attempts did not lead
+    --   to performance improvements, but I can't imagine that calling
+    --   `Index.Internal.findIndex` repeatedly is efficient
+    --
+    --   Maybe use Data.Series.Index.indexed to traverse the index once?
+    = MkSeries ss $ Boxed.convert
+                  $ Boxed.map (Vector.unsafeIndex vs . (`Index.Internal.findIndex` ks))
+                  $ Index.toAscVector ss
+
+
+-- | \(O(\log n)\) Yield a subseries based on integer indices. The end index is not included.
+slice :: Vector v a
+      => Int -- ^ Start index
+      -> Int -- ^ End index, which is not included
+      -> Series v k a 
+      -> Series v k a
+{-# INLINABLE slice #-}
+slice start stop (MkSeries ks vs) 
+    = let stop' = min (Vector.length vs) stop
+    -- Index.take is O(log n) while Vector.slice is O(1)
+    in MkSeries { index  = Index.take (stop' - start) $ Index.drop start ks
+                , values = Vector.slice start (stop' - start) vs
+                }
+
+
diff --git a/src/Data/Series/Generic/Zip.hs b/src/Data/Series/Generic/Zip.hs
--- a/src/Data/Series/Generic/Zip.hs
+++ b/src/Data/Series/Generic/Zip.hs
@@ -1,463 +1,463 @@
-module Data.Series.Generic.Zip (
-    zipWith, zipWithMatched, zipWithKey,
-    zipWith3, zipWithMatched3, zipWithKey3,
-    replace, (|->), (<-|),
-    
-    -- * Generalized zipping with strategies
-    zipWithStrategy,
-    zipWithStrategy3,
-    ZipStrategy,
-    skipStrategy,
-    mapStrategy,
-    constStrategy,
-
-    -- * Special case of zipping monoids
-    zipWithMonoid,
-    esum, eproduct,
-
-    -- * Unzipping
-    unzip, unzip3,
-) where
-
-import qualified Data.Map.Strict                as Map
-import           Data.Monoid                    ( Sum(..), Product(..) )
-import           Data.Series.Generic.Definition ( Series(MkSeries, index, values) )
-import qualified Data.Series.Generic.Definition as G
-import           Data.Series.Generic.View       ( selectSubset, requireWith )
-import           Data.Vector.Generic            ( Vector )
-import qualified Data.Vector.Generic            as Vector
-import qualified Data.Series.Index              as Index
-import qualified Data.Series.Index.Internal     as Index.Internal
-import           Prelude                        hiding ( zipWith, zipWith3, unzip, unzip3 ) 
-
--- $setup
--- >>> import qualified Data.Series as Series
-
-infix 6 |->, <-|
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. For keys present only in the left or right series, 
--- the value 'Nothing' is returned.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWith (+) xs ys
---   index |  values
---   ----- |  ------
--- "alpha" | Just 10
---  "beta" | Just 12
--- "delta" | Nothing
--- "gamma" | Nothing
---
--- To only combine elements where keys are in both series, see 'zipWithMatched'
-zipWith :: (Vector v a, Vector v b, Vector v c, Vector v (Maybe c), Ord k) 
-        => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k (Maybe c)
-zipWith f left right
-    = let matched = zipWithMatched f left right
-          matchedKeys   = index matched
-          allKeys       = index left `Index.union` index right
-          unmatchedKeys = allKeys `Index.difference` matchedKeys
-          unmatched     = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)
-       in G.map Just matched <> unmatched
-{-# INLINABLE zipWith #-}
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. For keys present only in the left or right series, 
--- the value 'Nothing' is returned.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
--- >>> zipWith3 (\x y z -> x + y + z) xs ys zs
---     index |  values
---     ----- |  ------
---   "alpha" | Just 30
---    "beta" | Nothing
---   "delta" | Nothing
--- "epsilon" | Nothing
---   "gamma" | Nothing
---
--- To only combine elements where keys are in all series, see 'zipWithMatched3'
-zipWith3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v (Maybe d), Ord k) 
-         => (a -> b -> c -> d) 
-         -> Series v k a 
-         -> Series v k b 
-         -> Series v k c 
-         -> Series v k (Maybe d)
-zipWith3 f left center right
-    = let matched       = zipWithMatched3 f left center right
-          matchedKeys   = index matched
-          allKeys       = index left `Index.union` index center `Index.union` index right
-          unmatchedKeys = allKeys `Index.difference` matchedKeys
-          unmatched     = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)
-       in G.map Just matched <> unmatched
-{-# INLINABLE zipWith3 #-}
-
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWithMatched (+) xs ys
---   index | values
---   ----- | ------
--- "alpha" |     10
---  "beta" |     12
---
--- To combine elements where keys are in either series, see 'zipWith'. To combine
--- three series, see 'zipWithMatched3'.
-zipWithMatched :: (Vector v a, Vector v b, Vector v c, Ord k) 
-               => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k c
-zipWithMatched f left right
-    = let matchedKeys   = index left `Index.intersection` index right
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of both series
-          (MkSeries _ !xs) = left  `selectSubset` matchedKeys
-          (MkSeries _ !ys) = right `selectSubset` matchedKeys
-          -- The following construction relies on the fact that keys are always sorted
-       in MkSeries matchedKeys $ Vector.zipWith f xs ys
-{-# INLINABLE zipWithMatched #-}
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys not present in all three series are dropped.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
---   index | values
---   ----- | ------
--- "alpha" |     30
-zipWithMatched3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) 
-                => (a -> b -> c -> d) 
-                -> Series v k a 
-                -> Series v k b 
-                -> Series v k c
-                -> Series v k d
-zipWithMatched3 f left center right
-    = let matchedKeys   = index left `Index.intersection` index center `Index.intersection` index right
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of all series
-          (MkSeries _ !xs) = left   `selectSubset` matchedKeys
-          (MkSeries _ !ys) = center `selectSubset` matchedKeys
-          (MkSeries _ !zs) = right  `selectSubset` matchedKeys
-          -- The following construction relies on the fact that keys are always sorted
-       in MkSeries matchedKeys $ Vector.zipWith3 f xs ys zs
-{-# INLINABLE zipWithMatched3 #-}
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
--- 
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWithKey (\k x y -> length k + x + y) xs ys
---   index | values
---   ----- | ------
--- "alpha" |     15
---  "beta" |     16
---
--- To combine elements where keys are in either series, see 'zipWith'
-zipWithKey :: (Vector v a, Vector v b, Vector v c, Vector v k, Ord k) 
-           => (k -> a -> b -> c) -> Series v k a -> Series v k b -> Series v k c
-zipWithKey f left right
-    = let matchedKeys   = index left `Index.intersection` index right
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of both series
-          (MkSeries _ xs) = left  `selectSubset` matchedKeys
-          (MkSeries _ ys) = right `selectSubset` matchedKeys
-          ks              = Index.toAscVector matchedKeys
-          -- The following construction relies on the fact that keys are always sorted
-       in  MkSeries matchedKeys $ Vector.zipWith3 f ks xs ys
-{-# INLINABLE zipWithKey #-}
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys not present in all series are dropped.
--- 
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("beta", 7), ("delta", 5) ]
--- >>> zipWithKey3 (\k x y z -> length k + x + y + z) xs ys zs
---   index | values
---   ----- | ------
--- "alpha" |     35
---  "beta" |     23
-
-zipWithKey3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v k, Ord k) 
-            => (k -> a -> b -> c -> d) 
-            -> Series v k a 
-            -> Series v k b 
-            -> Series v k c
-            -> Series v k d
-zipWithKey3 f left center right
-    = let matchedKeys   = index left `Index.intersection` index right
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of all series
-          (MkSeries _ xs) = left   `selectSubset` matchedKeys
-          (MkSeries _ ys) = center `selectSubset` matchedKeys
-          (MkSeries _ zs) = right  `selectSubset` matchedKeys
-          ks              = Index.toAscVector matchedKeys
-          -- The following construction relies on the fact that keys are always sorted
-       in  MkSeries matchedKeys $ Vector.zipWith4 f ks xs ys zs
-{-# INLINABLE zipWithKey3 #-}
-
-
--- | Replace values from the right series with values from the left series at matching keys.
--- Keys in the right series but not in the right series are unaffected.
-replace :: (Vector v a, Vector v Int, Ord k) 
-        => Series v k a -> Series v k a -> Series v k a
-{-# INLINABLE replace #-}
-xs `replace` ys 
-    = let keysToReplace = index xs `Index.intersection` index ys
-          iixs          = Index.toAscVector $ Index.Internal.mapMonotonic (\k -> Index.Internal.findIndex k (index ys)) keysToReplace
-       in MkSeries (index ys) $ Vector.update_ (values ys) iixs (values (xs `selectSubset` keysToReplace))
-
-
--- | Infix version of 'replace'
-(|->) :: (Vector v a, Vector v Int, Ord k)
-      => Series v k a -> Series v k a -> Series v k a
-{-# INLINABLE (|->) #-}
-(|->) = replace
-
-
--- | Flipped version of '|->',
-(<-|) :: (Vector v a, Vector v Int, Ord k) 
-      => Series v k a -> Series v k a -> Series v k a
-{-# INLINABLE (<-|)  #-}
-(<-|) = flip replace
-
-
--- | A 'ZipStrategy' is a function which is used to decide what to do when a key is missing from one
--- of two 'Series' being zipped together with 'zipWithStrategy'.
---
--- If a 'ZipStrategy' returns 'Nothing', the key is dropped.
--- If a 'ZipStrategy' returns @'Just' v@ for key @k@, then the value @v@ is inserted at key @k@.
---
--- For example, the most basic 'ZipStrategy' is to skip over any key which is missing from the other series.
--- Such a strategy can be written as @skip key value = 'Nothing'@ (see 'skipStrategy').
-type ZipStrategy k a b = (k -> a -> Maybe b)
-
-
--- | This 'ZipStrategy' drops keys which are not present in both 'Series'.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWithStrategy (+) skipStrategy skipStrategy xs ys
---   index | values
---   ----- | ------
--- "alpha" |     10
---  "beta" |     12
-skipStrategy :: ZipStrategy k a b
-skipStrategy _ _ = Nothing
-{-# INLINABLE skipStrategy #-}
-
-
--- | This 'ZipStrategy' sets the value at keys which are not present in both 'Series' 
--- to the some mapping from the value present in one of the series. See the example below.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 5::Int), ("beta", 6), ("delta", 7) ]
--- >>> zipWithStrategy (+) (mapStrategy id) (mapStrategy (*10)) xs ys
---   index | values
---   ----- | ------
--- "alpha" |      5
---  "beta" |      7
--- "delta" |     70
--- "gamma" |      2
-mapStrategy :: (a -> b) -> ZipStrategy k a b
-mapStrategy f _ x = Just (f x)
-{-# INLINABLE mapStrategy #-}
-
-
--- | This 'ZipStrategy' sets a constant value at keys which are not present in both 'Series'.
---
--- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
--- >>> zipWith (+) xs ys
---   index |  values
---   ----- |  ------
--- "alpha" | Just 10
---  "beta" | Just 12
--- "delta" | Nothing
--- "gamma" | Nothing
--- >>> zipWithStrategy (+) (constStrategy (-100)) (constStrategy 200)  xs ys
---   index | values
---   ----- | ------
--- "alpha" |     10
---  "beta" |     12
--- "delta" |    200
--- "gamma" |   -100
-constStrategy :: b -> ZipStrategy k a b
-constStrategy v = mapStrategy (const v)
-{-# INLINABLE constStrategy #-}
-
-
--- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ 
--- than using @'zipWithStrategy' f skipStrategy skipStrategy@.
-zipWithStrategy :: (Vector v a, Vector v b, Vector v c, Ord k) 
-                => (a -> b -> c)     -- ^ Function to combine values when present in both series
-                -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
-                -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
-                -> Series v k a
-                -> Series v k b 
-                -> Series v k c
-zipWithStrategy f whenLeft whenRight left right 
-    = let onlyLeftKeys  = index left  `Index.difference` index right
-          onlyRightKeys = index right `Index.difference` index left
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of both series
-          leftZip =  applyStrategy whenLeft  $ left  `selectSubset` onlyLeftKeys
-          rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys
-          
-        in zipWithMatched f left right <> leftZip <> rightZip
-    where
-        -- Application of the 'ZipStrategy' is done on a `Map` rather than
-        -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to
-        -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`.  
-        applyStrategy strat = G.toSeries 
-                            . Map.mapMaybeWithKey strat
-                            . G.fromSeries
-{-# INLINABLE zipWithStrategy #-}
-
-
--- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
--- present in one of the 'Series' but not all of the others.
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
--- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
-zipWithStrategy3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) 
-                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
-                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
-                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
-                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
-                -> Series v k a
-                -> Series v k b 
-                -> Series v k c
-                -> Series v k d
-zipWithStrategy3 f whenLeft whenCenter whenRight left center right 
-    = let onlyLeftKeys  = index left    `Index.difference` (index center `Index.union` index right)
-          onlyCenterKeys = index center `Index.difference` (index left   `Index.union` index right)
-          onlyRightKeys = index right   `Index.difference` (index center `Index.union` index left)
-          -- Recall that `selectSubset` is a performance optimization
-          -- and is generally unsafe to use; however, in this case, we know
-          -- that `matchedKeys` are subsets of the index of all series
-          leftZip =  applyStrategy whenLeft  $ left     `selectSubset` onlyLeftKeys
-          centerZip = applyStrategy whenCenter $ center `selectSubset` onlyCenterKeys
-          rightZip = applyStrategy whenRight $ right    `selectSubset` onlyRightKeys
-          
-        in zipWithMatched3 f left center right <> leftZip <> centerZip <> rightZip
-    where
-        -- Application of the 'ZipStrategy' is done on a `Map` rather than
-        -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to
-        -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`.  
-        applyStrategy strat = G.toSeries 
-                            . Map.mapMaybeWithKey strat
-                            . G.fromSeries
-{-# INLINABLE zipWithStrategy3 #-}
-
-
--- | Zip two 'Series' with a combining function. The value for keys which are missing from
--- either 'Series' is replaced with the appropriate 'mempty' value.
---
--- >>> import Data.Monoid ( Sum(..) )
--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
--- >>> zipWith (<>) xs ys
---        index |                  values
---        ----- |                  ------
--- "2023-01-01" | Just (Sum {getSum = 6})
--- "2023-01-02" |                 Nothing
--- "2023-01-03" |                 Nothing
--- >>> zipWithMonoid (<>) xs ys
---        index |           values
---        ----- |           ------
--- "2023-01-01" | Sum {getSum = 6}
--- "2023-01-02" | Sum {getSum = 2}
--- "2023-01-03" | Sum {getSum = 7}
-zipWithMonoid :: ( Monoid a, Monoid b
-                 , Vector v a, Vector v b, Vector v c
-                 , Ord k
-                 ) 
-              => (a -> b -> c)
-              -> Series v k a
-              -> Series v k b 
-              -> Series v k c
-zipWithMonoid f left right 
-    = let fullindex = index left `Index.union` index right
-          (MkSeries ix ls) = requireWith (const mempty) id left  fullindex
-          (MkSeries _ rs)  = requireWith (const mempty) id right fullindex          
-        in MkSeries ix $ Vector.zipWith f ls rs
-{-# INLINABLE zipWithMonoid #-}
-
-
--- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `esum` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |      6
--- "2023-01-02" |      2
--- "2023-01-03" |      7
-esum :: (Ord k, Num a, Vector v a, Vector v (Sum a)) 
-     => Series v k a 
-     -> Series v k a
-     -> Series v k a
-esum ls rs = G.map getSum $ zipWithMonoid (<>) (G.map Sum ls) (G.map Sum rs)
-{-# INLINABLE esum #-}
-
-
--- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `eproduct` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |     10
--- "2023-01-02" |      3
--- "2023-01-03" |      7
-eproduct :: (Ord k, Num a, Vector v a, Vector v (Product a)) 
-         => Series v k a 
-         -> Series v k a
-         -> Series v k a
-eproduct ls rs = G.map getProduct $ zipWithMonoid (<>) (G.map Product ls) (G.map Product rs)
-{-# INLINABLE eproduct #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 2-tuples.
-unzip :: (Vector v a, Vector v b, Vector v (a, b)) 
-      => Series v k (a, b)
-      -> ( Series v k a
-         , Series v k b
-         )
-unzip (MkSeries ix vs) 
-    = let (left, right) = Vector.unzip vs
-       in (MkSeries ix left, MkSeries ix right)
-{-# INLINABLE unzip #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 3-tuples.
-unzip3 :: (Vector v a, Vector v b, Vector v c, Vector v (a, b, c)) 
-       => Series v k (a, b, c)
-       -> ( Series v k a
-          , Series v k b
-          , Series v k c
-          )
-unzip3 (MkSeries ix vs) 
-    = let (left, center, right) = Vector.unzip3 vs
-       in (MkSeries ix left, MkSeries ix center, MkSeries ix right)
-{-# INLINABLE unzip3 #-}
+module Data.Series.Generic.Zip (
+    zipWith, zipWithMatched, zipWithKey,
+    zipWith3, zipWithMatched3, zipWithKey3,
+    replace, (|->), (<-|),
+    
+    -- * Generalized zipping with strategies
+    zipWithStrategy,
+    zipWithStrategy3,
+    ZipStrategy,
+    skipStrategy,
+    mapStrategy,
+    constStrategy,
+
+    -- * Special case of zipping monoids
+    zipWithMonoid,
+    esum, eproduct,
+
+    -- * Unzipping
+    unzip, unzip3,
+) where
+
+import qualified Data.Map.Strict                as Map
+import           Data.Monoid                    ( Sum(..), Product(..) )
+import           Data.Series.Generic.Definition ( Series(MkSeries, index, values) )
+import qualified Data.Series.Generic.Definition as G
+import           Data.Series.Generic.View       ( selectSubset, requireWith )
+import           Data.Vector.Generic            ( Vector )
+import qualified Data.Vector.Generic            as Vector
+import qualified Data.Series.Index              as Index
+import qualified Data.Series.Index.Internal     as Index.Internal
+import           Prelude                        hiding ( zipWith, zipWith3, unzip, unzip3 ) 
+
+-- $setup
+-- >>> import qualified Data.Series as Series
+
+infix 6 |->, <-|
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. For keys present only in the left or right series, 
+-- the value 'Nothing' is returned.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWith (+) xs ys
+--   index |  values
+--   ----- |  ------
+-- "alpha" | Just 10
+--  "beta" | Just 12
+-- "delta" | Nothing
+-- "gamma" | Nothing
+--
+-- To only combine elements where keys are in both series, see 'zipWithMatched'
+zipWith :: (Vector v a, Vector v b, Vector v c, Vector v (Maybe c), Ord k) 
+        => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k (Maybe c)
+zipWith f left right
+    = let matched = zipWithMatched f left right
+          matchedKeys   = index matched
+          allKeys       = index left `Index.union` index right
+          unmatchedKeys = allKeys `Index.difference` matchedKeys
+          unmatched     = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)
+       in G.map Just matched <> unmatched
+{-# INLINABLE zipWith #-}
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. For keys present only in the left or right series, 
+-- the value 'Nothing' is returned.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
+-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
+-- >>> zipWith3 (\x y z -> x + y + z) xs ys zs
+--     index |  values
+--     ----- |  ------
+--   "alpha" | Just 30
+--    "beta" | Nothing
+--   "delta" | Nothing
+-- "epsilon" | Nothing
+--   "gamma" | Nothing
+--
+-- To only combine elements where keys are in all series, see 'zipWithMatched3'
+zipWith3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v (Maybe d), Ord k) 
+         => (a -> b -> c -> d) 
+         -> Series v k a 
+         -> Series v k b 
+         -> Series v k c 
+         -> Series v k (Maybe d)
+zipWith3 f left center right
+    = let matched       = zipWithMatched3 f left center right
+          matchedKeys   = index matched
+          allKeys       = index left `Index.union` index center `Index.union` index right
+          unmatchedKeys = allKeys `Index.difference` matchedKeys
+          unmatched     = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)
+       in G.map Just matched <> unmatched
+{-# INLINABLE zipWith3 #-}
+
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWithMatched (+) xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     10
+--  "beta" |     12
+--
+-- To combine elements where keys are in either series, see 'zipWith'. To combine
+-- three series, see 'zipWithMatched3'.
+zipWithMatched :: (Vector v a, Vector v b, Vector v c, Ord k) 
+               => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k c
+zipWithMatched f left right
+    = let matchedKeys   = index left `Index.intersection` index right
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of both series
+          (MkSeries _ !xs) = left  `selectSubset` matchedKeys
+          (MkSeries _ !ys) = right `selectSubset` matchedKeys
+          -- The following construction relies on the fact that keys are always sorted
+       in MkSeries matchedKeys $ Vector.zipWith f xs ys
+{-# INLINABLE zipWithMatched #-}
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys not present in all three series are dropped.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int),  ("beta", 1),   ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11),  ("delta", 13) ]
+-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]
+-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
+--   index | values
+--   ----- | ------
+-- "alpha" |     30
+zipWithMatched3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) 
+                => (a -> b -> c -> d) 
+                -> Series v k a 
+                -> Series v k b 
+                -> Series v k c
+                -> Series v k d
+zipWithMatched3 f left center right
+    = let matchedKeys   = index left `Index.intersection` index center `Index.intersection` index right
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of all series
+          (MkSeries _ !xs) = left   `selectSubset` matchedKeys
+          (MkSeries _ !ys) = center `selectSubset` matchedKeys
+          (MkSeries _ !zs) = right  `selectSubset` matchedKeys
+          -- The following construction relies on the fact that keys are always sorted
+       in MkSeries matchedKeys $ Vector.zipWith3 f xs ys zs
+{-# INLINABLE zipWithMatched3 #-}
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+-- 
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWithKey (\k x y -> length k + x + y) xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     15
+--  "beta" |     16
+--
+-- To combine elements where keys are in either series, see 'zipWith'
+zipWithKey :: (Vector v a, Vector v b, Vector v c, Vector v k, Ord k) 
+           => (k -> a -> b -> c) -> Series v k a -> Series v k b -> Series v k c
+zipWithKey f left right
+    = let matchedKeys   = index left `Index.intersection` index right
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of both series
+          (MkSeries _ xs) = left  `selectSubset` matchedKeys
+          (MkSeries _ ys) = right `selectSubset` matchedKeys
+          ks              = Index.toAscVector matchedKeys
+          -- The following construction relies on the fact that keys are always sorted
+       in  MkSeries matchedKeys $ Vector.zipWith3 f ks xs ys
+{-# INLINABLE zipWithKey #-}
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys not present in all series are dropped.
+-- 
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("beta", 7), ("delta", 5) ]
+-- >>> zipWithKey3 (\k x y z -> length k + x + y + z) xs ys zs
+--   index | values
+--   ----- | ------
+-- "alpha" |     35
+--  "beta" |     23
+
+zipWithKey3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v k, Ord k) 
+            => (k -> a -> b -> c -> d) 
+            -> Series v k a 
+            -> Series v k b 
+            -> Series v k c
+            -> Series v k d
+zipWithKey3 f left center right
+    = let matchedKeys   = index left `Index.intersection` index right
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of all series
+          (MkSeries _ xs) = left   `selectSubset` matchedKeys
+          (MkSeries _ ys) = center `selectSubset` matchedKeys
+          (MkSeries _ zs) = right  `selectSubset` matchedKeys
+          ks              = Index.toAscVector matchedKeys
+          -- The following construction relies on the fact that keys are always sorted
+       in  MkSeries matchedKeys $ Vector.zipWith4 f ks xs ys zs
+{-# INLINABLE zipWithKey3 #-}
+
+
+-- | Replace values from the right series with values from the left series at matching keys.
+-- Keys in the right series but not in the right series are unaffected.
+replace :: (Vector v a, Vector v Int, Ord k) 
+        => Series v k a -> Series v k a -> Series v k a
+{-# INLINABLE replace #-}
+xs `replace` ys 
+    = let keysToReplace = index xs `Index.intersection` index ys
+          iixs          = Index.toAscVector $ Index.Internal.mapMonotonic (\k -> Index.Internal.findIndex k (index ys)) keysToReplace
+       in MkSeries (index ys) $ Vector.update_ (values ys) iixs (values (xs `selectSubset` keysToReplace))
+
+
+-- | Infix version of 'replace'
+(|->) :: (Vector v a, Vector v Int, Ord k)
+      => Series v k a -> Series v k a -> Series v k a
+{-# INLINABLE (|->) #-}
+(|->) = replace
+
+
+-- | Flipped version of '|->',
+(<-|) :: (Vector v a, Vector v Int, Ord k) 
+      => Series v k a -> Series v k a -> Series v k a
+{-# INLINABLE (<-|)  #-}
+(<-|) = flip replace
+
+
+-- | A 'ZipStrategy' is a function which is used to decide what to do when a key is missing from one
+-- of two 'Series' being zipped together with 'zipWithStrategy'.
+--
+-- If a 'ZipStrategy' returns 'Nothing', the key is dropped.
+-- If a 'ZipStrategy' returns @'Just' v@ for key @k@, then the value @v@ is inserted at key @k@.
+--
+-- For example, the most basic 'ZipStrategy' is to skip over any key which is missing from the other series.
+-- Such a strategy can be written as @skip key value = 'Nothing'@ (see 'skipStrategy').
+type ZipStrategy k a b = (k -> a -> Maybe b)
+
+
+-- | This 'ZipStrategy' drops keys which are not present in both 'Series'.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWithStrategy (+) skipStrategy skipStrategy xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     10
+--  "beta" |     12
+skipStrategy :: ZipStrategy k a b
+skipStrategy _ _ = Nothing
+{-# INLINABLE skipStrategy #-}
+
+
+-- | This 'ZipStrategy' sets the value at keys which are not present in both 'Series' 
+-- to the some mapping from the value present in one of the series. See the example below.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 5::Int), ("beta", 6), ("delta", 7) ]
+-- >>> zipWithStrategy (+) (mapStrategy id) (mapStrategy (*10)) xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |      5
+--  "beta" |      7
+-- "delta" |     70
+-- "gamma" |      2
+mapStrategy :: (a -> b) -> ZipStrategy k a b
+mapStrategy f _ x = Just (f x)
+{-# INLINABLE mapStrategy #-}
+
+
+-- | This 'ZipStrategy' sets a constant value at keys which are not present in both 'Series'.
+--
+-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]
+-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]
+-- >>> zipWith (+) xs ys
+--   index |  values
+--   ----- |  ------
+-- "alpha" | Just 10
+--  "beta" | Just 12
+-- "delta" | Nothing
+-- "gamma" | Nothing
+-- >>> zipWithStrategy (+) (constStrategy (-100)) (constStrategy 200)  xs ys
+--   index | values
+--   ----- | ------
+-- "alpha" |     10
+--  "beta" |     12
+-- "delta" |    200
+-- "gamma" |   -100
+constStrategy :: b -> ZipStrategy k a b
+constStrategy v = mapStrategy (const v)
+{-# INLINABLE constStrategy #-}
+
+
+-- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ 
+-- than using @'zipWithStrategy' f skipStrategy skipStrategy@.
+zipWithStrategy :: (Vector v a, Vector v b, Vector v c, Ord k) 
+                => (a -> b -> c)     -- ^ Function to combine values when present in both series
+                -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
+                -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
+                -> Series v k a
+                -> Series v k b 
+                -> Series v k c
+zipWithStrategy f whenLeft whenRight left right 
+    = let onlyLeftKeys  = index left  `Index.difference` index right
+          onlyRightKeys = index right `Index.difference` index left
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of both series
+          leftZip =  applyStrategy whenLeft  $ left  `selectSubset` onlyLeftKeys
+          rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys
+          
+        in zipWithMatched f left right <> leftZip <> rightZip
+    where
+        -- Application of the 'ZipStrategy' is done on a `Map` rather than
+        -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to
+        -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`.  
+        applyStrategy strat = G.toSeries 
+                            . Map.mapMaybeWithKey strat
+                            . G.fromSeries
+{-# INLINABLE zipWithStrategy #-}
+
+
+-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
+-- present in one of the 'Series' but not all of the others.
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
+-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
+zipWithStrategy3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) 
+                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
+                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
+                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
+                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
+                -> Series v k a
+                -> Series v k b 
+                -> Series v k c
+                -> Series v k d
+zipWithStrategy3 f whenLeft whenCenter whenRight left center right 
+    = let onlyLeftKeys  = index left    `Index.difference` (index center `Index.union` index right)
+          onlyCenterKeys = index center `Index.difference` (index left   `Index.union` index right)
+          onlyRightKeys = index right   `Index.difference` (index center `Index.union` index left)
+          -- Recall that `selectSubset` is a performance optimization
+          -- and is generally unsafe to use; however, in this case, we know
+          -- that `matchedKeys` are subsets of the index of all series
+          leftZip =  applyStrategy whenLeft  $ left     `selectSubset` onlyLeftKeys
+          centerZip = applyStrategy whenCenter $ center `selectSubset` onlyCenterKeys
+          rightZip = applyStrategy whenRight $ right    `selectSubset` onlyRightKeys
+          
+        in zipWithMatched3 f left center right <> leftZip <> centerZip <> rightZip
+    where
+        -- Application of the 'ZipStrategy' is done on a `Map` rather than
+        -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to
+        -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`.  
+        applyStrategy strat = G.toSeries 
+                            . Map.mapMaybeWithKey strat
+                            . G.fromSeries
+{-# INLINABLE zipWithStrategy3 #-}
+
+
+-- | Zip two 'Series' with a combining function. The value for keys which are missing from
+-- either 'Series' is replaced with the appropriate 'mempty' value.
+--
+-- >>> import Data.Monoid ( Sum(..) )
+-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
+-- >>> zipWith (<>) xs ys
+--        index |                  values
+--        ----- |                  ------
+-- "2023-01-01" | Just (Sum {getSum = 6})
+-- "2023-01-02" |                 Nothing
+-- "2023-01-03" |                 Nothing
+-- >>> zipWithMonoid (<>) xs ys
+--        index |           values
+--        ----- |           ------
+-- "2023-01-01" | Sum {getSum = 6}
+-- "2023-01-02" | Sum {getSum = 2}
+-- "2023-01-03" | Sum {getSum = 7}
+zipWithMonoid :: ( Monoid a, Monoid b
+                 , Vector v a, Vector v b, Vector v c
+                 , Ord k
+                 ) 
+              => (a -> b -> c)
+              -> Series v k a
+              -> Series v k b 
+              -> Series v k c
+zipWithMonoid f left right 
+    = let fullindex = index left `Index.union` index right
+          (MkSeries ix ls) = requireWith (const mempty) id left  fullindex
+          (MkSeries _ rs)  = requireWith (const mempty) id right fullindex          
+        in MkSeries ix $ Vector.zipWith f ls rs
+{-# INLINABLE zipWithMonoid #-}
+
+
+-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `esum` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |      6
+-- "2023-01-02" |      2
+-- "2023-01-03" |      7
+esum :: (Ord k, Num a, Vector v a, Vector v (Sum a)) 
+     => Series v k a 
+     -> Series v k a
+     -> Series v k a
+esum ls rs = G.map getSum $ zipWithMonoid (<>) (G.map Sum ls) (G.map Sum rs)
+{-# INLINABLE esum #-}
+
+
+-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `eproduct` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |     10
+-- "2023-01-02" |      3
+-- "2023-01-03" |      7
+eproduct :: (Ord k, Num a, Vector v a, Vector v (Product a)) 
+         => Series v k a 
+         -> Series v k a
+         -> Series v k a
+eproduct ls rs = G.map getProduct $ zipWithMonoid (<>) (G.map Product ls) (G.map Product rs)
+{-# INLINABLE eproduct #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 2-tuples.
+unzip :: (Vector v a, Vector v b, Vector v (a, b)) 
+      => Series v k (a, b)
+      -> ( Series v k a
+         , Series v k b
+         )
+unzip (MkSeries ix vs) 
+    = let (left, right) = Vector.unzip vs
+       in (MkSeries ix left, MkSeries ix right)
+{-# INLINABLE unzip #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 3-tuples.
+unzip3 :: (Vector v a, Vector v b, Vector v c, Vector v (a, b, c)) 
+       => Series v k (a, b, c)
+       -> ( Series v k a
+          , Series v k b
+          , Series v k c
+          )
+unzip3 (MkSeries ix vs) 
+    = let (left, center, right) = Vector.unzip3 vs
+       in (MkSeries ix left, MkSeries ix center, MkSeries ix right)
+{-# INLINABLE unzip3 #-}
diff --git a/src/Data/Series/Index.hs b/src/Data/Series/Index.hs
--- a/src/Data/Series/Index.hs
+++ b/src/Data/Series/Index.hs
@@ -1,108 +1,108 @@
------------------------------------------------------------------------------
--- |
--- Module      :  $header
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT-style
--- Maintainer  :  Laurent P. René de Cotret
--- Portability :  portable
---
--- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/
--- keys which can be used to efficient index a 'Data.Series.Series'.
---
--- = Construction
---
--- Constructing an 'Index' can be done from the usual list using `fromList`. Note that 
--- the 'Index' length could be smaller than the input list, due to the requirement that
--- an 'Index' be a sequence of unique keys.  A better way to construct an 'Index' is 
--- to use a 'Data.Set' (`fromSet`)
---
--- For quick INLINABLE definitions of an 'Index', you can also make use of the @OverloadedLists@ extension:
--- 
--- >>> :set -XOverloadedLists
--- >>> let (ix :: Index Int) = [1,2,3,4,5,5,5]
--- >>> ix
--- Index [1,2,3,4,5] 
---
--- Another useful function to construct an 'Index' is `range`. This allows to build an 'Index'
--- from a starting value up to an ending value, with a custom step function. For example,
--- here's an 'Index' with values from 1 to 10, in steps of 3:
---
--- >>> range (+3) (1 :: Int) 10
--- Index [1,4,7,10]
---
--- Note that `range` is a special case of the `unfoldr` function, which is also provided in this module.
---
--- = Set operations
--- 
--- Just like a 'Data.Set', 'Index' supports efficient `member`, `notMember`, `union`, `intersection`, and `difference` operations.
--- Like 'Data.Set', the `Semigroup` and `Monoid` instance of 'Index' are defined using the `union` operation:
---
--- >>> fromList ['a', 'b', 'c'] <> fromList ['b', 'c', 'd']
--- Index "abcd"
---
--- = Mapping
---
--- Because of the restriction that all keys be unique, an 'Index' is not a true `Functor`; you can't use
--- `fmap` to map elements of an index. Instead, you can use the general-purpose function 'map'. If you want
--- to map elements of an 'Index' with a monotonic function (i.e. a function which will not re-order elements and won't
--- create duplicate elements), you can use the 'Data.Series.mapMonotonic' function which operates faster.
---
--- = Indexing
---
--- One of the key operations for 'Data.Series.Series' is to find the integer index of an element in an 'Index'. For this purpose, you
--- can use `lookupIndex`:
---
--- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']
--- Just 1
--- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']
--- Nothing
-
-module Data.Series.Index (
-    Index,
-
-    -- * Creation and Conversion
-    singleton,
-    unfoldr,
-    range,
-    IsIndex(..),
-    fromSet,
-    fromList,
-    fromVector,
-    toSet,
-    toAscList,
-    toAscVector,
-
-    -- * Set-like operations
-    null,
-    member,
-    notMember,
-    union,
-    intersection,
-    difference,
-    symmetricDifference,
-    contains,
-    size,
-    take,
-    drop,
-
-    -- * Mapping and filtering
-    map,
-    indexed,
-    filter,
-    traverse,
-    
-    -- * Indexing
-    lookupIndex,
-
-    -- * Insertion and deletion
-    insert,
-    delete,
-) where
-
-import Data.Series.Index.Definition ( Index, IsIndex(..), singleton, unfoldr, range, fromSet, fromList, fromVector, toSet
-                                    , toAscList, toAscVector, null, member, notMember, union, intersection
-                                    , difference, symmetricDifference, contains, size, take, drop, map, indexed
-                                    , filter, traverse, lookupIndex, insert, delete 
-                                    )
-import Prelude hiding ( null, take, drop, map, filter, traverse )
-
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  $header
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT-style
+-- Maintainer  :  Laurent P. René de Cotret
+-- Portability :  portable
+--
+-- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/
+-- keys which can be used to efficient index a 'Data.Series.Series'.
+--
+-- = Construction
+--
+-- Constructing an 'Index' can be done from the usual list using `fromList`. Note that 
+-- the 'Index' length could be smaller than the input list, due to the requirement that
+-- an 'Index' be a sequence of unique keys.  A better way to construct an 'Index' is 
+-- to use a 'Data.Set' (`fromSet`)
+--
+-- For quick INLINABLE definitions of an 'Index', you can also make use of the @OverloadedLists@ extension:
+-- 
+-- >>> :set -XOverloadedLists
+-- >>> let (ix :: Index Int) = [1,2,3,4,5,5,5]
+-- >>> ix
+-- Index [1,2,3,4,5] 
+--
+-- Another useful function to construct an 'Index' is `range`. This allows to build an 'Index'
+-- from a starting value up to an ending value, with a custom step function. For example,
+-- here's an 'Index' with values from 1 to 10, in steps of 3:
+--
+-- >>> range (+3) (1 :: Int) 10
+-- Index [1,4,7,10]
+--
+-- Note that `range` is a special case of the `unfoldr` function, which is also provided in this module.
+--
+-- = Set operations
+-- 
+-- Just like a 'Data.Set', 'Index' supports efficient `member`, `notMember`, `union`, `intersection`, and `difference` operations.
+-- Like 'Data.Set', the `Semigroup` and `Monoid` instance of 'Index' are defined using the `union` operation:
+--
+-- >>> fromList ['a', 'b', 'c'] <> fromList ['b', 'c', 'd']
+-- Index "abcd"
+--
+-- = Mapping
+--
+-- Because of the restriction that all keys be unique, an 'Index' is not a true `Functor`; you can't use
+-- `fmap` to map elements of an index. Instead, you can use the general-purpose function 'map'. If you want
+-- to map elements of an 'Index' with a monotonic function (i.e. a function which will not re-order elements and won't
+-- create duplicate elements), you can use the 'Data.Series.mapMonotonic' function which operates faster.
+--
+-- = Indexing
+--
+-- One of the key operations for 'Data.Series.Series' is to find the integer index of an element in an 'Index'. For this purpose, you
+-- can use `lookupIndex`:
+--
+-- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']
+-- Just 1
+-- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']
+-- Nothing
+
+module Data.Series.Index (
+    Index,
+
+    -- * Creation and Conversion
+    singleton,
+    unfoldr,
+    range,
+    IsIndex(..),
+    fromSet,
+    fromList,
+    fromVector,
+    toSet,
+    toAscList,
+    toAscVector,
+
+    -- * Set-like operations
+    null,
+    member,
+    notMember,
+    union,
+    intersection,
+    difference,
+    symmetricDifference,
+    contains,
+    size,
+    take,
+    drop,
+
+    -- * Mapping and filtering
+    map,
+    indexed,
+    filter,
+    traverse,
+    
+    -- * Indexing
+    lookupIndex,
+
+    -- * Insertion and deletion
+    insert,
+    delete,
+) where
+
+import Data.Series.Index.Definition ( Index, IsIndex(..), singleton, unfoldr, range, fromSet, fromList, fromVector, toSet
+                                    , toAscList, toAscVector, null, member, notMember, union, intersection
+                                    , difference, symmetricDifference, contains, size, take, drop, map, indexed
+                                    , filter, traverse, lookupIndex, insert, delete 
+                                    )
+import Prelude hiding ( null, take, drop, map, filter, traverse )
+
diff --git a/src/Data/Series/Index/Definition.hs b/src/Data/Series/Index/Definition.hs
--- a/src/Data/Series/Index/Definition.hs
+++ b/src/Data/Series/Index/Definition.hs
@@ -1,517 +1,519 @@
-{-# LANGUAGE TypeFamilies #-}
-{-# OPTIONS_GHC -Wno-redundant-constraints #-}
-
------------------------------------------------------------------------------
--- |
--- Module      :  $header
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT-style
--- Maintainer  :  Laurent P. René de Cotret
--- Portability :  portable
---
--- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/
--- keys which can be used to efficient index a 'Series'.
-
-
-module Data.Series.Index.Definition (
-    Index(..),
-
-    -- * Creation and Conversion
-    singleton,
-    unfoldr,
-    range,
-    fromSet, toSet,
-    fromList, toAscList,
-    fromAscList, fromDistinctAscList,
-    fromVector, toAscVector,
-    fromAscVector, fromDistinctAscVector,
-    -- ** Ad-hoc conversion with other data structures
-    IsIndex(..),
-    
-    -- * Set-like operations
-    null,
-    member,
-    notMember,
-    union,
-    intersection,
-    difference,
-    symmetricDifference,
-    cartesianProduct,
-    contains,
-    size,
-    take,
-    drop,
-
-    -- * Mapping and filtering
-    map,
-    mapMonotonic,
-    indexed,
-    filter,
-    traverse,
-    
-    -- * Indexing
-    findIndex,
-    lookupIndex,
-    elemAt,
-
-    -- * Insertion and deletion
-    insert,
-    delete,
-) where
-
-import           Control.DeepSeq        ( NFData )
-import           Control.Monad          ( guard )
-import           Control.Monad.ST       ( runST )
-import           Data.Coerce            ( coerce )
-import qualified Data.Foldable          as Foldable
-import           Data.Functor           ( ($>) )
-import           Data.IntSet            ( IntSet )
-import qualified Data.IntSet            as IntSet
-import qualified Data.List              as List
-import           Data.Sequence          ( Seq )
-import qualified Data.Sequence          as Seq
-import           Data.Set               ( Set )
-import qualified Data.Set               as Set
-import qualified Data.Traversable       as Traversable
-import qualified Data.Vector            as Boxed
-import           Data.Vector.Algorithms.Intro ( sortUniq )
-import           Data.Vector.Generic    ( Vector )
-import qualified Data.Vector.Generic    as Vector
-import qualified Data.Vector.Generic.Mutable as M
-import qualified Data.Vector.Unboxed    as Unboxed
-import           GHC.Exts               ( IsList )
-import qualified GHC.Exts               as Exts
-import           GHC.Stack              ( HasCallStack )
-import           Prelude                as P hiding ( null, take, drop, map, filter, traverse, product )
-
--- $setup
--- >>> import Data.Series.Index
--- >>> import qualified Data.Vector as Vector
-
-
--- | Representation of the index of a series.
--- An index is a sequence of sorted elements. All elements are unique, much like a 'Set'.
---
--- You can construct an 'Index' from a set ('fromSet'), from a list ('fromList'), or from a vector ('fromVector'). You can 
--- also make use of the @OverloadedLists@ extension:
---
--- >>> :set -XOverloadedLists
--- >>> let (ix :: Index Int) = [1, 2, 3]
--- >>> ix
--- Index [1,2,3]
---
--- Since keys in an 'Index' are always sorted and unique, 'Index' is not a 'Functor'. To map a function
--- over an 'Index', use 'map'.
-newtype Index k = MkIndex (Set k)
-    deriving (Eq, Ord, Semigroup, Monoid, Foldable, NFData)
-
-
-instance Ord k => IsList (Index k) where
-    type Item (Index k) = k
-    fromList :: [k] -> Index k
-    fromList = fromList
-    toList :: Index k -> [Exts.Item (Index k)]
-    toList = toAscList
-
-
-instance Show k => Show (Index k) where
-    show :: Index k -> String
-    show (MkIndex s) = "Index " ++ show (Set.toList s)
-
-
--- | \(O(1)\)  Create a singleton 'Index'.
-singleton :: k -> Index k
-singleton = MkIndex . Set.singleton
-{-# INLINABLE singleton #-}
-
-
--- | \(O(n \log n)\) Create an 'Index' from a seed value. 
--- Note that the order in which elements are generated does not matter; elements are stored
--- in order. See the example below.
---
--- >>> unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)
--- Index [1,2,3,4,5,6,7]
-unfoldr :: Ord a => (b -> Maybe (a, b)) -> b -> Index a
-unfoldr f = fromList . List.unfoldr f
-{-# INLINABLE unfoldr #-}
-
-
--- | \(O(n \log n)\) Create an 'Index' as a range of values. @range f start end@ will generate 
--- an 'Index' with values @[start, f start, f (f start), ... ]@ such that the largest element
--- less or equal to @end@ is included. See examples below.
---
--- >>> range (+3) (1 :: Int) 10
--- Index [1,4,7,10]
--- >>> range (+3) (1 :: Int) 11
--- Index [1,4,7,10]
-range :: Ord a 
-      => (a -> a) -- ^ Function to generate the next element in the index
-      -> a        -- ^ Starting value of the 'Index'
-      -> a        -- ^ Ending value of the 'Index', which may or may not be contained
-      -> Index a
-range next start end 
-    = unfoldr (\x -> guard (x <= end) $> (x, next x)) start
-{-# INLINABLE range #-}
-
-
--- | The 'IsIndex' typeclass allow for ad-hoc definition
--- of conversion functions, converting to / from 'Index'.
-class IsIndex t k where
-    -- | Construct an 'Index' from some container of keys. There is no
-    -- condition on the order of keys. Duplicate keys are silently dropped.
-    toIndex    :: t -> Index k
-
-    -- | Construct a container from keys of an 'Index'. 
-    -- The elements are returned in ascending order of keys.
-    fromIndex  :: Index k -> t
-
-
-instance IsIndex (Set k) k where
-    -- | \(O(1)\) Build an 'Index' from a 'Set'.
-    toIndex :: Set k -> Index k
-    toIndex = coerce
-    {-# INLINABLE toIndex #-}
-
-    -- | \(O(1)\) Build an 'Index' from a 'Set'.
-    fromIndex :: Index k -> Set k
-    fromIndex = coerce
-    {-# INLINABLE fromIndex #-}
-
-
-instance Ord k => IsIndex [k] k where
-    -- | \(O(n \log n)\) Build an 'Index' from a list.
-    toIndex :: [k] -> Index k
-    toIndex = fromList
-    {-# INLINABLE toIndex #-}
-
-    -- | \(O(n)\) Convert an 'Index' to a list.
-    fromIndex :: Index k -> [k]
-    fromIndex = toAscList
-    {-# INLINABLE fromIndex #-}
-
-
-instance Ord k => IsIndex (Seq k) k where
-    -- | \(O(n \log n)\) Build an 'Index' from a 'Seq'.
-    toIndex :: Seq k -> Index k
-    toIndex = fromList . Foldable.toList
-    {-# INLINABLE toIndex #-}
-
-    -- | \(O(n)\) Convert an 'Index' to a 'Seq'.
-    fromIndex :: Index k -> Seq k
-    fromIndex = Seq.fromList . toAscList
-    {-# INLINABLE fromIndex #-}
-
-
-instance IsIndex IntSet Int where
-    -- | \(O(n \min(n,W))\), where \W\ is the number of bits in an 'Int' on your platform (32 or 64).
-    toIndex :: IntSet -> Index Int
-    toIndex = fromDistinctAscList . IntSet.toList
-    {-# INLINABLE toIndex #-}
-    
-    -- | \(O(n)\) Convert an 'Index' to an 'IntSet.
-    fromIndex :: Index Int -> IntSet
-    fromIndex = IntSet.fromDistinctAscList . toAscList
-    {-# INLINABLE fromIndex #-}
-
-
-instance (Ord k) => IsIndex (Boxed.Vector k) k where
-    toIndex :: Boxed.Vector k -> Index k
-    toIndex = fromVector
-    {-# INLINABLE toIndex #-} 
-
-    fromIndex :: Index k -> Boxed.Vector k
-    fromIndex = toAscVector
-    {-# INLINABLE fromIndex #-}
-
-
-instance (Ord k, Unboxed.Unbox k) => IsIndex (Unboxed.Vector k) k where
-    toIndex :: Unboxed.Vector k -> Index k
-    toIndex = fromVector
-    {-# INLINABLE toIndex #-} 
-
-    fromIndex :: Index k -> Unboxed.Vector k
-    fromIndex ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze
-    {-# INLINABLE fromIndex #-}
-
-
--- | \(O(1)\) Build an 'Index' from a 'Set'.
-fromSet :: Set k -> Index k
-fromSet = toIndex
-{-# INLINABLE fromSet #-}
-
-
--- | \(O(n \log n)\) Build an 'Index' from a list. Note that since an 'Index' is
--- composed of unique elements, the length of the index may not be
--- the same as the length of the input list:
---
--- >>> fromList ['c', 'a', 'b', 'b']
--- Index "abc"
---
--- If the list is already sorted, `fromAscList` is generally faster.
-fromList :: Ord k => [k] -> Index k
-fromList = fromSet . Set.fromList
-{-# INLINABLE fromList #-}
-
-
--- | \(O(n)\) Build an 'Index' from a list of elements in ascending order. The precondition
--- that elements already be sorted is not checked.
--- 
--- Note that since an 'Index' is composed of unique elements, the length of 
--- the index may not be the same as the length of the input list.
-fromAscList :: Eq k => [k] -> Index k
-fromAscList = toIndex . Set.fromAscList
-{-# INLINABLE fromAscList #-}
-
-
--- | \(O(n)\) Build an 'Index' from a list of distinct elements in ascending order. The precondition
--- that elements be unique and sorted is not checked.
-fromDistinctAscList :: [k] -> Index k
-fromDistinctAscList = MkIndex . Set.fromDistinctAscList
-{-# INLINABLE fromDistinctAscList #-}
-
-
--- | \(O(n \log n)\) Build an 'Index' from a 'Vector'. Note that since an 'Index' is
--- composed of unique elements, the length of the index may not be
--- the same as the length of the input vector:
---
--- >>> import Data.Vector as V
--- >>> fromVector $ V.fromList ['c', 'a', 'b', 'b']
--- Index "abc"
---
--- If the 'Vector' is already sorted, 'fromAscVector' is generally faster.
-fromVector :: (Vector v k, Ord k) => v k -> Index k
-fromVector vs = fromDistinctAscVector $ runST $ Vector.thaw vs >>= sortUniq >>= Vector.freeze
-{-# INLINABLE fromVector #-}
-
-
--- | \(O(n \log n)\) Build an 'Index' from a 'Vector' of elements in ascending order. The precondition
--- that elements already be sorted is not checked. 
---
--- Note that since an 'Index' is composed of unique elements, 
--- the length of the index may not be the same as the length of the input vector:
---
--- >>> import Data.Vector as V
--- >>> fromAscVector $ V.fromList ['a', 'b', 'b', 'c']
--- Index "abc"
-fromAscVector :: (Vector v k, Ord k) => v k -> Index k
-fromAscVector = fromAscList . Vector.toList
-{-# INLINABLE fromAscVector #-}
-
-
--- | \(O(n)\) Build an 'Index' from a 'Vector' of unique elements in ascending order. The precondition
--- that elements already be unique and sorted is not checked.
-fromDistinctAscVector :: Vector v k => v k -> Index k
-fromDistinctAscVector = fromDistinctAscList . Vector.toList
-{-# INLINABLE fromDistinctAscVector #-}
-
-
--- | \(O(1)\) Convert an 'Index' to a 'Set'.
-toSet :: Index k -> Set k
-toSet = fromIndex
-{-# INLINABLE toSet #-}
-
-
--- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.
-toAscList :: Index k -> [k]
-toAscList (MkIndex s) = Set.toAscList s
-{-# INLINABLE toAscList #-}
-
-
--- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.
-toAscVector :: Vector v k => Index k -> v k
-toAscVector ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze
-{-# INLINABLE toAscVector #-}
-
-
--- | \(O(1)\) Returns 'True' for an empty 'Index', and @False@ otherwise.
-null :: Index k -> Bool
-null (MkIndex ix) = Set.null ix
-{-# INLINABLE null #-}
-
-
--- | \(O(n \log n)\) Check whether the element is in the index.
-member :: Ord k => k -> Index k -> Bool
-member k (MkIndex ix) = k `Set.member` ix
-{-# INLINABLE member #-}
-
-
--- | \(O(n \log n)\) Check whether the element is NOT in the index.
-notMember :: Ord k => k -> Index k -> Bool
-notMember k = not . member k
-{-# INLINABLE notMember #-}
-
-
--- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Union of two 'Index', containing
--- elements either in the left index, right right index, or both.
-union :: Ord k => Index k -> Index k -> Index k
-union = (<>)
-{-# INLINABLE union #-}
-
-
--- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Intersection of two 'Index', containing
--- elements which are in both the left index and the right index.
-intersection :: Ord k => Index k -> Index k -> Index k
-intersection (MkIndex ix) (MkIndex jx) = MkIndex $ ix `Set.intersection` jx
-{-# INLINABLE intersection #-}
-
-
--- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Returns the elements of the first index 
--- which are not found in the second index.
---
--- >>> difference (fromList ['a', 'b', 'c']) (fromList ['b', 'c', 'd'])
--- Index "a"
-difference :: Ord k => Index k -> Index k -> Index k
-difference (MkIndex ix) (MkIndex jx) = MkIndex $ Set.difference ix jx
-{-# INLINABLE difference #-}
-
-
--- | \(O(n+m)\). The symmetric difference of two 'Index'.
--- The first element of the tuple is an 'Index' containing all elements which
--- are only found in the left 'Index', while the second element of the tuple is an 'Index' containing
--- all elements which are only found in the right 'Index':
---
--- >>> left = fromList ['a', 'b', 'c']
--- >>> right = fromList ['c', 'd', 'e']
--- >>> left `symmetricDifference` right
--- (Index "ab",Index "de")
-symmetricDifference :: Ord k => Index k -> Index k -> (Index k, Index k)
-symmetricDifference left right = (left `difference` right, right `difference` left)
-{-# INLINABLE symmetricDifference #-}
-
-
--- | \(O(n m)\) Take the cartesian product of two 'Index':
---
--- >>> (range (+1) (1 :: Int) 2) `cartesianProduct` (range (+1) (3 :: Int) 4)
--- Index [(1,3),(1,4),(2,3),(2,4)]
-cartesianProduct :: Index k -> Index g -> Index (k, g)
-cartesianProduct (MkIndex xs) (MkIndex ys) 
-    = MkIndex $ Set.cartesianProduct xs ys
-{-# INLINABLE cartesianProduct #-}
-
-
--- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\).
--- @(ix1 \'contains\' ix2)@ indicates whether all keys in @ix2@ are also in @ix1@.
-contains :: Ord k => Index k -> Index k -> Bool
-contains (MkIndex ix1) (MkIndex ix2)= ix2 `Set.isSubsetOf` ix1
-{-# INLINABLE contains #-}
-
-
--- | \(O(1)\) Returns the number of keys in the index.
-size :: Index k -> Int
-size (MkIndex ix) = Set.size ix
-{-# INLINABLE size #-}
-
-
--- | \(O(\log n)\). Take @n@ elements from the index, in ascending order.
--- Taking more than the number of elements in the index is a no-op:
---
--- >>> take 10 $ fromList [1::Int,2,3]
--- Index [1,2,3]
-take :: Int -> Index k -> Index k
-take n (MkIndex ix) = MkIndex (Set.take n ix)
-{-# INLINABLE take #-}
-
-
--- | \(O(\log n)\). Drop @n@ elements from the index, in ascending order.
-drop :: Int -> Index k -> Index k
-drop n (MkIndex ix) = MkIndex (Set.drop n ix)
-{-# INLINABLE drop #-}
-
-
--- | \(O(n \log n)\) Map a function over keys in the index.
--- Note that since keys in an 'Index' are unique, the length of the resulting
--- index may not be the same as the input:
---
--- >>> map (\x -> if even x then 0::Int else 1) $ fromList [0::Int,1,2,3,4]
--- Index [0,1]
---
--- If the mapping is monotonic, see 'mapMonotonic', which has better performance
--- characteristics.
-map :: Ord g => (k -> g) -> Index k -> Index g
-map f (MkIndex ix) = MkIndex $ Set.map f ix
-{-# INLINABLE map #-}
-
-
--- | \(O(n)\) Map a monotonic function over keys in the index. /Monotonic/ means that if @a < b@, then @f a < f b@.
--- Using 'mapMonononic' can be much faster than using 'map' for a large 'Index'.
--- Note that the precondiction that the function be monotonic is not checked.
---
--- >>> mapMonotonic (+1) $ fromList [0::Int,1,2,3,4,5]
--- Index [1,2,3,4,5,6]
-mapMonotonic :: (k -> g) -> Index k -> Index g
-mapMonotonic f (MkIndex ix) = MkIndex $ Set.mapMonotonic f ix
-{-# INLINABLE mapMonotonic #-}
-
-
--- | \(O(n)\) Pair each key in the index with its position in the index, starting with 0:
---
--- @since 0.1.1.0
---
--- >>> indexed (fromList ['a', 'b', 'c', 'd'])
--- Index [(0,'a'),(1,'b'),(2,'c'),(3,'d')]
-indexed :: Index k -> Index (Int, k)
-indexed = fromDistinctAscList 
-        . zip [0..] 
-        . toAscList
-{-# INLINABLE indexed #-}
-
-
--- | \(O(n)\) Filter elements satisfying a predicate.
---
--- >>> filter even $ fromList [1::Int,2,3,4,5]
--- Index [2,4]
-filter :: (k -> Bool) -> Index k -> Index k
-filter p (MkIndex ix) = MkIndex $ Set.filter p ix
-{-# INLINABLE filter #-}
-
-
--- | \(O(\log n)\). Returns the integer /index/ of a key. This function raises an exception
--- if the key is not in the 'Index'; see 'lookupIndex' for a safe version.
---
--- >>> findIndex 'b' $ fromList ['a', 'b', 'c']
--- 1
-findIndex :: HasCallStack => Ord k => k -> Index k -> Int
-findIndex e (MkIndex ix) = Set.findIndex e ix 
-{-# INLINABLE findIndex #-}
-
-
--- | \(O(\log n)\). Returns the integer /index/ of a key, if the key is in the index.
---
--- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']
--- Just 1
--- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']
--- Nothing
-lookupIndex :: Ord k => k -> Index k -> Maybe Int
-lookupIndex e (MkIndex ix) = Set.lookupIndex e ix
-{-# INLINABLE lookupIndex #-}
-
-
--- | \(O(\log n)\) Returns the element at some integer index. This function raises
--- an exception if the integer index is out-of-bounds.
-elemAt :: HasCallStack => Int -> Index k -> k
-elemAt n (MkIndex ix) = Set.elemAt n ix
-{-# INLINABLE elemAt #-}
-
-
--- | \(O(\log n)\). Insert a key in an 'Index'. If the key is already 
--- present, the 'Index' will not change.
-insert :: Ord k => k -> Index k -> Index k
-insert k (MkIndex ix) = MkIndex $ k `Set.insert` ix
-{-# INLINABLE insert #-}
-
-
--- | \(O(\log n)\). Delete a key from an 'Index', if this key is present
--- in the index.
-delete :: Ord k => k -> Index k -> Index k
-delete k (MkIndex ix) = MkIndex $ k `Set.delete` ix
-{-# INLINABLE delete #-}
-
-
--- | \(O(n \log n)\). Map each element of an 'Index' to an applicative action, 
--- evaluate these actions from left to right, and collect the results.
---
--- Note that the data type 'Index' is not a member of 'Traversable'
--- because it is not a 'Functor'.
-traverse :: (Applicative f, Ord b) => (k -> f b) -> Index k -> f (Index b)
-traverse f = fmap fromList . Traversable.traverse f . toAscList
-{-# INLINABLE traverse #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  $header
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT-style
+-- Maintainer  :  Laurent P. René de Cotret
+-- Portability :  portable
+--
+-- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/
+-- keys which can be used to efficient index a 'Series'.
+
+
+module Data.Series.Index.Definition (
+    Index(..),
+
+    -- * Creation and Conversion
+    singleton,
+    unfoldr,
+    range,
+    fromSet, toSet,
+    fromList, toAscList,
+    fromAscList, fromDistinctAscList,
+    fromVector, toAscVector,
+    fromAscVector, fromDistinctAscVector,
+    -- ** Ad-hoc conversion with other data structures
+    IsIndex(..),
+    
+    -- * Set-like operations
+    null,
+    member,
+    notMember,
+    union,
+    intersection,
+    difference,
+    symmetricDifference,
+    cartesianProduct,
+    contains,
+    size,
+    take,
+    drop,
+
+    -- * Mapping and filtering
+    map,
+    mapMonotonic,
+    indexed,
+    filter,
+    traverse,
+    
+    -- * Indexing
+    findIndex,
+    lookupIndex,
+    elemAt,
+
+    -- * Insertion and deletion
+    insert,
+    delete,
+) where
+
+import           Control.DeepSeq        ( NFData )
+import           Control.Monad          ( guard )
+import           Control.Monad.ST       ( runST )
+import           Data.Coerce            ( coerce )
+import qualified Data.Foldable          as Foldable
+import           Data.Functor           ( ($>) )
+import           Data.IntSet            ( IntSet )
+import qualified Data.IntSet            as IntSet
+import qualified Data.List              as List
+import           Data.Sequence          ( Seq )
+import qualified Data.Sequence          as Seq
+import           Data.Set               ( Set )
+import qualified Data.Set               as Set
+import qualified Data.Traversable       as Traversable
+import qualified Data.Vector            as Boxed
+import           Data.Vector.Algorithms.Intro ( sortUniq )
+import           Data.Vector.Generic    ( Vector )
+import qualified Data.Vector.Generic    as Vector
+import qualified Data.Vector.Generic.Mutable as M
+import qualified Data.Vector.Unboxed    as Unboxed
+import           GHC.Exts               ( IsList )
+import qualified GHC.Exts               as Exts
+import           GHC.Stack              ( HasCallStack )
+import           Prelude                as P hiding ( null, take, drop, map, filter, traverse, product )
+
+-- $setup
+-- >>> import Data.Series.Index
+-- >>> import qualified Data.Vector as Vector
+
+
+-- | Representation of the index of a series.
+-- An index is a sequence of sorted elements. All elements are unique, much like a 'Set'.
+--
+-- You can construct an 'Index' from a set ('fromSet'), from a list ('fromList'), or from a vector ('fromVector'). You can 
+-- also make use of the @OverloadedLists@ extension:
+--
+-- >>> :set -XOverloadedLists
+-- >>> let (ix :: Index Int) = [1, 2, 3]
+-- >>> ix
+-- Index [1,2,3]
+--
+-- Since keys in an 'Index' are always sorted and unique, 'Index' is not a 'Functor'. To map a function
+-- over an 'Index', use 'map'.
+newtype Index k = MkIndex (Set k)
+    deriving (Eq, Ord, Semigroup, Monoid, Foldable, NFData)
+
+
+instance Ord k => IsList (Index k) where
+    type Item (Index k) = k
+    fromList :: [k] -> Index k
+    fromList = fromList
+    toList :: Index k -> [Exts.Item (Index k)]
+    toList = toAscList
+
+
+instance Show k => Show (Index k) where
+    show :: Index k -> String
+    show (MkIndex s) = "Index " ++ show (Set.toList s)
+
+
+-- | \(O(1)\)  Create a singleton 'Index'.
+singleton :: k -> Index k
+singleton = MkIndex . Set.singleton
+{-# INLINABLE singleton #-}
+
+
+-- | \(O(n \log n)\) Create an 'Index' from a seed value. 
+-- Note that the order in which elements are generated does not matter; elements are stored
+-- in order. See the example below.
+--
+-- >>> unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)
+-- Index [1,2,3,4,5,6,7]
+unfoldr :: Ord a => (b -> Maybe (a, b)) -> b -> Index a
+unfoldr f = fromList . List.unfoldr f
+{-# INLINABLE unfoldr #-}
+
+
+-- | \(O(n \log n)\) Create an 'Index' as a range of values. @range f start end@ will generate 
+-- an 'Index' with values @[start, f start, f (f start), ... ]@ such that the largest element
+-- less or equal to @end@ is included. See examples below.
+--
+-- >>> range (+3) (1 :: Int) 10
+-- Index [1,4,7,10]
+-- >>> range (+3) (1 :: Int) 11
+-- Index [1,4,7,10]
+range :: Ord a 
+      => (a -> a) -- ^ Function to generate the next element in the index
+      -> a        -- ^ Starting value of the 'Index'
+      -> a        -- ^ Ending value of the 'Index', which may or may not be contained
+      -> Index a
+range next start end 
+    = unfoldr (\x -> guard (x <= end) $> (x, next x)) start
+{-# INLINABLE range #-}
+
+
+-- | The 'IsIndex' typeclass allow for ad-hoc definition
+-- of conversion functions, converting to / from 'Index'.
+class IsIndex t k where
+    -- | Construct an 'Index' from some container of keys. There is no
+    -- condition on the order of keys. Duplicate keys are silently dropped.
+    toIndex    :: t -> Index k
+
+    -- | Construct a container from keys of an 'Index'. 
+    -- The elements are returned in ascending order of keys.
+    fromIndex  :: Index k -> t
+
+
+instance IsIndex (Set k) k where
+    -- | \(O(1)\) Build an 'Index' from a 'Set'.
+    toIndex :: Set k -> Index k
+    toIndex = coerce
+    {-# INLINABLE toIndex #-}
+
+    -- | \(O(1)\) Build an 'Index' from a 'Set'.
+    fromIndex :: Index k -> Set k
+    fromIndex = coerce
+    {-# INLINABLE fromIndex #-}
+
+
+instance Ord k => IsIndex [k] k where
+    -- | \(O(n \log n)\) Build an 'Index' from a list.
+    toIndex :: [k] -> Index k
+    toIndex = fromList
+    {-# INLINABLE toIndex #-}
+
+    -- | \(O(n)\) Convert an 'Index' to a list.
+    fromIndex :: Index k -> [k]
+    fromIndex = toAscList
+    {-# INLINABLE fromIndex #-}
+
+
+instance Ord k => IsIndex (Seq k) k where
+    -- | \(O(n \log n)\) Build an 'Index' from a 'Seq'.
+    toIndex :: Seq k -> Index k
+    toIndex = fromList . Foldable.toList
+    {-# INLINABLE toIndex #-}
+
+    -- | \(O(n)\) Convert an 'Index' to a 'Seq'.
+    fromIndex :: Index k -> Seq k
+    fromIndex = Seq.fromList . toAscList
+    {-# INLINABLE fromIndex #-}
+
+
+instance IsIndex IntSet Int where
+    -- | \(O(n \min(n,W))\), where \W\ is the number of bits in an 'Int' on your platform (32 or 64).
+    toIndex :: IntSet -> Index Int
+    toIndex = fromDistinctAscList . IntSet.toList
+    {-# INLINABLE toIndex #-}
+    
+    -- | \(O(n)\) Convert an 'Index' to an 'IntSet.
+    fromIndex :: Index Int -> IntSet
+    fromIndex = IntSet.fromDistinctAscList . toAscList
+    {-# INLINABLE fromIndex #-}
+
+
+instance (Ord k) => IsIndex (Boxed.Vector k) k where
+    toIndex :: Boxed.Vector k -> Index k
+    toIndex = fromVector
+    {-# INLINABLE toIndex #-} 
+
+    fromIndex :: Index k -> Boxed.Vector k
+    fromIndex = toAscVector
+    {-# INLINABLE fromIndex #-}
+
+
+instance (Ord k, Unboxed.Unbox k) => IsIndex (Unboxed.Vector k) k where
+    toIndex :: Unboxed.Vector k -> Index k
+    toIndex = fromVector
+    {-# INLINABLE toIndex #-} 
+
+    fromIndex :: Index k -> Unboxed.Vector k
+    fromIndex ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze
+    {-# INLINABLE fromIndex #-}
+
+
+-- | \(O(1)\) Build an 'Index' from a 'Set'.
+fromSet :: Set k -> Index k
+fromSet = toIndex
+{-# INLINABLE fromSet #-}
+
+
+-- | \(O(n \log n)\) Build an 'Index' from a list. Note that since an 'Index' is
+-- composed of unique elements, the length of the index may not be
+-- the same as the length of the input list:
+--
+-- >>> fromList ['c', 'a', 'b', 'b']
+-- Index "abc"
+--
+-- If the list is already sorted, `fromAscList` is generally faster.
+fromList :: Ord k => [k] -> Index k
+fromList = fromSet . Set.fromList
+{-# INLINABLE fromList #-}
+
+
+-- | \(O(n)\) Build an 'Index' from a list of elements in ascending order. The precondition
+-- that elements already be sorted is not checked.
+-- 
+-- Note that since an 'Index' is composed of unique elements, the length of 
+-- the index may not be the same as the length of the input list.
+fromAscList :: Eq k => [k] -> Index k
+fromAscList = toIndex . Set.fromAscList
+{-# INLINABLE fromAscList #-}
+
+
+-- | \(O(n)\) Build an 'Index' from a list of distinct elements in ascending order. The precondition
+-- that elements be unique and sorted is not checked.
+fromDistinctAscList :: [k] -> Index k
+fromDistinctAscList = MkIndex . Set.fromDistinctAscList
+{-# INLINABLE fromDistinctAscList #-}
+
+
+-- | \(O(n \log n)\) Build an 'Index' from a 'Vector'. Note that since an 'Index' is
+-- composed of unique elements, the length of the index may not be
+-- the same as the length of the input vector:
+--
+-- >>> import Data.Vector as V
+-- >>> fromVector $ V.fromList ['c', 'a', 'b', 'b']
+-- Index "abc"
+--
+-- If the 'Vector' is already sorted, 'fromAscVector' is generally faster.
+fromVector :: (Vector v k, Ord k) => v k -> Index k
+fromVector vs = fromDistinctAscVector $ runST $ Vector.thaw vs >>= sortUniq >>= Vector.freeze
+{-# INLINABLE fromVector #-}
+
+
+-- | \(O(n \log n)\) Build an 'Index' from a 'Vector' of elements in ascending order. The precondition
+-- that elements already be sorted is not checked. 
+--
+-- Note that since an 'Index' is composed of unique elements, 
+-- the length of the index may not be the same as the length of the input vector:
+--
+-- >>> import Data.Vector as V
+-- >>> fromAscVector $ V.fromList ['a', 'b', 'b', 'c']
+-- Index "abc"
+fromAscVector :: (Vector v k, Ord k) => v k -> Index k
+fromAscVector = fromAscList . Vector.toList
+{-# INLINABLE fromAscVector #-}
+
+
+-- | \(O(n)\) Build an 'Index' from a 'Vector' of unique elements in ascending order. The precondition
+-- that elements already be unique and sorted is not checked.
+fromDistinctAscVector :: Vector v k => v k -> Index k
+fromDistinctAscVector = fromDistinctAscList . Vector.toList
+{-# INLINABLE fromDistinctAscVector #-}
+
+
+-- | \(O(1)\) Convert an 'Index' to a 'Set'.
+toSet :: Index k -> Set k
+toSet = fromIndex
+{-# INLINABLE toSet #-}
+
+
+-- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.
+toAscList :: Index k -> [k]
+toAscList (MkIndex s) = Set.toAscList s
+{-# INLINABLE toAscList #-}
+
+
+-- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.
+toAscVector :: Vector v k => Index k -> v k
+toAscVector ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze
+{-# INLINABLE toAscVector #-}
+
+
+-- | \(O(1)\) Returns 'True' for an empty 'Index', and @False@ otherwise.
+null :: Index k -> Bool
+null (MkIndex ix) = Set.null ix
+{-# INLINABLE null #-}
+
+
+-- | \(O(n \log n)\) Check whether the element is in the index.
+member :: Ord k => k -> Index k -> Bool
+member k (MkIndex ix) = k `Set.member` ix
+{-# INLINABLE member #-}
+
+
+-- | \(O(n \log n)\) Check whether the element is NOT in the index.
+notMember :: Ord k => k -> Index k -> Bool
+notMember k = not . member k
+{-# INLINABLE notMember #-}
+
+
+-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Union of two 'Index', containing
+-- elements either in the left index, right right index, or both.
+union :: Ord k => Index k -> Index k -> Index k
+union = (<>)
+{-# INLINABLE union #-}
+
+
+-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Intersection of two 'Index', containing
+-- elements which are in both the left index and the right index.
+intersection :: Ord k => Index k -> Index k -> Index k
+intersection (MkIndex ix) (MkIndex jx) = MkIndex $ ix `Set.intersection` jx
+{-# INLINABLE intersection #-}
+
+
+-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Returns the elements of the first index 
+-- which are not found in the second index.
+--
+-- >>> difference (fromList ['a', 'b', 'c']) (fromList ['b', 'c', 'd'])
+-- Index "a"
+difference :: Ord k => Index k -> Index k -> Index k
+difference (MkIndex ix) (MkIndex jx) = MkIndex $ Set.difference ix jx
+{-# INLINABLE difference #-}
+
+
+-- | \(O(n+m)\). The symmetric difference of two 'Index'.
+-- The first element of the tuple is an 'Index' containing all elements which
+-- are only found in the left 'Index', while the second element of the tuple is an 'Index' containing
+-- all elements which are only found in the right 'Index':
+--
+-- >>> left = fromList ['a', 'b', 'c']
+-- >>> right = fromList ['c', 'd', 'e']
+-- >>> left `symmetricDifference` right
+-- (Index "ab",Index "de")
+symmetricDifference :: Ord k => Index k -> Index k -> (Index k, Index k)
+symmetricDifference left right = (left `difference` right, right `difference` left)
+{-# INLINABLE symmetricDifference #-}
+
+
+-- | \(O(n m)\) Take the cartesian product of two 'Index':
+--
+-- >>> (range (+1) (1 :: Int) 2) `cartesianProduct` (range (+1) (3 :: Int) 4)
+-- Index [(1,3),(1,4),(2,3),(2,4)]
+cartesianProduct :: Index k -> Index g -> Index (k, g)
+cartesianProduct (MkIndex xs) (MkIndex ys) 
+    = MkIndex $ Set.cartesianProduct xs ys
+{-# INLINABLE cartesianProduct #-}
+
+
+-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\).
+-- @(ix1 \'contains\' ix2)@ indicates whether all keys in @ix2@ are also in @ix1@.
+contains :: Ord k => Index k -> Index k -> Bool
+contains (MkIndex ix1) (MkIndex ix2)= ix2 `Set.isSubsetOf` ix1
+{-# INLINABLE contains #-}
+
+
+-- | \(O(1)\) Returns the number of keys in the index.
+size :: Index k -> Int
+size (MkIndex ix) = Set.size ix
+{-# INLINABLE size #-}
+
+
+-- | \(O(\log n)\). Take @n@ elements from the index, in ascending order.
+-- Taking more than the number of elements in the index is a no-op:
+--
+-- >>> take 10 $ fromList [1::Int,2,3]
+-- Index [1,2,3]
+take :: Int -> Index k -> Index k
+take n (MkIndex ix) = MkIndex (Set.take n ix)
+{-# INLINABLE take #-}
+
+
+-- | \(O(\log n)\). Drop @n@ elements from the index, in ascending order.
+drop :: Int -> Index k -> Index k
+drop n (MkIndex ix) = MkIndex (Set.drop n ix)
+{-# INLINABLE drop #-}
+
+
+-- | \(O(n \log n)\) Map a function over keys in the index.
+-- Note that since keys in an 'Index' are unique, the length of the resulting
+-- index may not be the same as the input:
+--
+-- >>> map (\x -> if even x then 0::Int else 1) $ fromList [0::Int,1,2,3,4]
+-- Index [0,1]
+--
+-- If the mapping is monotonic, see 'mapMonotonic', which has better performance
+-- characteristics.
+map :: Ord g => (k -> g) -> Index k -> Index g
+map f (MkIndex ix) = MkIndex $ Set.map f ix
+{-# INLINABLE map #-}
+
+
+-- | \(O(n)\) Map a monotonic function over keys in the index. /Monotonic/ means that if @a < b@, then @f a < f b@.
+-- Using 'mapMonononic' can be much faster than using 'map' for a large 'Index'.
+-- Note that the precondiction that the function be monotonic is not checked.
+--
+-- >>> mapMonotonic (+1) $ fromList [0::Int,1,2,3,4,5]
+-- Index [1,2,3,4,5,6]
+mapMonotonic :: (k -> g) -> Index k -> Index g
+mapMonotonic f (MkIndex ix) = MkIndex $ Set.mapMonotonic f ix
+{-# INLINABLE mapMonotonic #-}
+
+
+-- | \(O(n)\) Pair each key in the index with its position in the index, starting with 0:
+--
+-- @since 0.1.1.0
+--
+-- >>> indexed (fromList ['a', 'b', 'c', 'd'])
+-- Index [(0,'a'),(1,'b'),(2,'c'),(3,'d')]
+indexed :: Index k -> Index (Int, k)
+indexed = fromDistinctAscList 
+        . zip [0..] 
+        . toAscList
+{-# INLINABLE indexed #-}
+
+
+-- | \(O(n)\) Filter elements satisfying a predicate.
+--
+-- >>> filter even $ fromList [1::Int,2,3,4,5]
+-- Index [2,4]
+filter :: (k -> Bool) -> Index k -> Index k
+filter p (MkIndex ix) = MkIndex $ Set.filter p ix
+{-# INLINABLE filter #-}
+
+
+-- | \(O(\log n)\). Returns the integer /index/ of a key. This function raises an exception
+-- if the key is not in the 'Index'; see 'lookupIndex' for a safe version.
+--
+-- >>> findIndex 'b' $ fromList ['a', 'b', 'c']
+-- 1
+findIndex :: HasCallStack => Ord k => k -> Index k -> Int
+findIndex e (MkIndex ix) = Set.findIndex e ix 
+{-# INLINABLE findIndex #-}
+
+
+-- | \(O(\log n)\). Returns the integer /index/ of a key, if the key is in the index.
+--
+-- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']
+-- Just 1
+-- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']
+-- Nothing
+lookupIndex :: Ord k => k -> Index k -> Maybe Int
+lookupIndex e (MkIndex ix) = Set.lookupIndex e ix
+{-# INLINABLE lookupIndex #-}
+
+
+-- | \(O(\log n)\) Returns the element at some integer index. 
+-- 
+-- This function raises an exception if the integer index is out-of-bounds. 
+-- Consider using 'lookupIndex' instead.
+elemAt :: HasCallStack => Int -> Index k -> k
+elemAt n (MkIndex ix) = Set.elemAt n ix
+{-# INLINABLE elemAt #-}
+
+
+-- | \(O(\log n)\). Insert a key in an 'Index'. If the key is already 
+-- present, the 'Index' will not change.
+insert :: Ord k => k -> Index k -> Index k
+insert k (MkIndex ix) = MkIndex $ k `Set.insert` ix
+{-# INLINABLE insert #-}
+
+
+-- | \(O(\log n)\). Delete a key from an 'Index', if this key is present
+-- in the index.
+delete :: Ord k => k -> Index k -> Index k
+delete k (MkIndex ix) = MkIndex $ k `Set.delete` ix
+{-# INLINABLE delete #-}
+
+
+-- | \(O(n \log n)\). Map each element of an 'Index' to an applicative action, 
+-- evaluate these actions from left to right, and collect the results.
+--
+-- Note that the data type 'Index' is not a member of 'Traversable'
+-- because it is not a 'Functor'.
+traverse :: (Applicative f, Ord b) => (k -> f b) -> Index k -> f (Index b)
+traverse f = fmap fromList . Traversable.traverse f . toAscList
+{-# INLINABLE traverse #-}
diff --git a/src/Data/Series/Index/Internal.hs b/src/Data/Series/Index/Internal.hs
--- a/src/Data/Series/Index/Internal.hs
+++ b/src/Data/Series/Index/Internal.hs
@@ -1,39 +1,39 @@
-{-# LANGUAGE TypeFamilies #-}
-
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Series.Generic.Internal
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT
--- Maintainer  :  laurent.decotret@outlook.com
--- Portability :  portable
---
--- = WARNING
---
--- This module is considered __internal__. It contains functions
--- which may be unsafe to use in general, for example requiring 
--- the data to be pre-sorted like 'fromDistinctAscList'.
---
--- The Package Versioning Policy still applies.
-
-module Data.Series.Index.Internal(
-    Index(..),
-
-    -- * Unsafe construction
-    fromAscList,
-    fromDistinctAscList,
-    fromAscVector,
-    fromDistinctAscVector,
-
-    -- * Functions with unchecked pre-conditions
-    mapMonotonic,
-
-    -- * Unsafe indexing
-    elemAt,
-    findIndex,
-
-) where
-
-import Data.Series.Index.Definition ( Index(..), fromAscList, fromDistinctAscList, fromAscVector
-                                    , fromDistinctAscVector, mapMonotonic, elemAt, findIndex
-                                    )
+{-# LANGUAGE TypeFamilies #-}
+
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Series.Generic.Internal
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- = WARNING
+--
+-- This module is considered __internal__. It contains functions
+-- which may be unsafe to use in general, for example requiring 
+-- the data to be pre-sorted like 'fromDistinctAscList'.
+--
+-- The Package Versioning Policy still applies.
+
+module Data.Series.Index.Internal(
+    Index(..),
+
+    -- * Unsafe construction
+    fromAscList,
+    fromDistinctAscList,
+    fromAscVector,
+    fromDistinctAscVector,
+
+    -- * Functions with unchecked pre-conditions
+    mapMonotonic,
+
+    -- * Unsafe indexing
+    elemAt,
+    findIndex,
+
+) where
+
+import Data.Series.Index.Definition ( Index(..), fromAscList, fromDistinctAscList, fromAscVector
+                                    , fromDistinctAscVector, mapMonotonic, elemAt, findIndex
+                                    )
diff --git a/src/Data/Series/Tutorial.hs b/src/Data/Series/Tutorial.hs
--- a/src/Data/Series/Tutorial.hs
+++ b/src/Data/Series/Tutorial.hs
@@ -1,770 +1,770 @@
-{-# OPTIONS_GHC -fno-warn-unused-imports #-}
-
-module Data.Series.Tutorial (
-    -- * Introduction
-    -- $introduction
-
-    -- * Construction
-    -- $construction
-
-    -- * Index
-    -- $index
-
-    -- * Selections
-    -- ** Single-key selection
-    -- $singlekey
-
-    -- ** Bulk selections
-    -- $multikey
-
-    -- * Filtering and mapping
-    -- $filteringandmapping
-
-    -- * Folding
-    -- $folding
-
-    -- * Grouping
-    -- $grouping
-
-    -- * Window aggregation
-    -- $windowing
-
-    -- * Combining 'Series' together
-    -- $zipping
-
-    -- * Conclusion
-    -- $conclusion and further reading
-    
-    -- * Advanced topics
-    -- ** Handling duplicate keys
-    -- $duplicates
-
-    -- ** Unboxed and generic series
-    -- $unboxed
-
-    -- ** Replacing values
-    -- $replacement
-
-    -- ** Comparison with other data structures
-    -- $comparison
-
-) where
-
-import           Control.Foldl   ( Fold )
-import           Data.Series     ( IsSeries(..), Series, Occurrence, at, iat, select, to, from, upto, require
-                                 , groupBy, aggregateWith, (<-|), (|->), Range, windowing
-                                 )
-import qualified Data.Series     as Series
-import qualified Data.Series.Generic
-import           Data.Series.Index ( Index )
-import qualified Data.Series.Index as Index
-import qualified Data.Series.Unboxed
-import           Data.Set        ( Set )
-import qualified Data.Set
-import           Data.Map.Strict ( Map )
-import qualified Data.Map.Strict
-import qualified Data.Map.Merge.Strict
-import           Numeric.Natural ( Natural)
-import qualified Data.List
-import qualified Data.Vector
-import qualified Data.Vector.Unboxed
-
-{- $introduction
-
-This is a short user guide on how to get started using @javelin@ and its various modules.
-
-The central data structure at the heart of this package is the 'Series'. A @'Series' k a@ 
-is a labeled array of type @v@ filled with values of type @a@, indexed by keys of type @k@.
-
-Like 'Data.Map.Strict.Map', 'Series' support efficient:
-
-* random access by key ( \(O(\log n)\) );
-* slice by key ( \(O(\log n)\) ).
-
-Like 'Data.Vector.Vector', 'Series' support efficient:
-
-* numerical operations.
-* random access by index ( \(O(1)\) );
-* slice by index ( \(O(1)\) );     
-
-To follow along this tutorial, the following imports are expected:
-
->>> import Data.Series as Series
--}
-
-{- $construction 
-
-The easiest way to create a 'Series' is to do it from a list using 'Data.Series.fromList':
-
->>> Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]
-index | values
------ | ------
-  'a' |      1
-  'b' |      2
-  'c' |      3
-  'd' |      4
-
-Note what happens when we have the same key (@\'a\'@) attached to multiple values:
-
->>> Series.fromList [ ('a', 1::Int), ('a', 0), ('b', 2), ('c', 3), ('d', 4) ]
-index | values
------ | ------
-  'a' |      0
-  'b' |      2
-  'c' |      3
-  'd' |      4
-
-'Series', like 'Map's, have unique keys; therefore, the output series may 
-not be the same length as the input series. See further below for an 
-explanation of how to handle duplicate keys. 
-
-Since 'Series' are like 'Map', it's easy to convert between the two:
-
->>> let mp = Data.Map.Strict.fromList [ ('a', 0::Int), ('a', 1), ('b', 2), ('c', 3), ('d', 4) ]
->>> mp
-fromList [('a',1),('b',2),('c',3),('d',4)]
->>> Series.fromStrictMap mp
-index | values
------ | ------
-  'a' |      1
-  'b' |      2
-  'c' |      3
-  'd' |      4
-
-Of course, 'Series.fromLazyMap' is also available. In fact, conversion to/from 'Series' is supported for
-many types; see the 'IsSeries' typeclass and its methods, 'toSeries' and 'fromSeries'.
-
--}
-
-{- $index
-
-'Series' have two components: values and an index.
-
-The index (of type @'Index' k@) is an ordered set of unique elements which allows to determine 
-where are each values in the series. Since all keys in an 'Index' are unique and sorted, it
-is fast to find the value associated to any random key.
-
-As we'll see soon, 'Index' is an important data structure which can be used to slice through a 'Series', 
-so let's get comfortable with them.
-
->>> import qualified Data.Series.Index as Index
-
-An 'Index' can be constructed from a list:
-
->>> Index.fromList [5::Int,5,4,3,2,1,5,5,5]
-Index [1,2,3,4,5]
-
-As you see above, repeated elements (in this case, @5@) won't be repeated in the 'Index'. Therefore, it often makes 
-more sense to construct an 'Index' using 'Index.fromSet' from a 'Set' from "Data.Set".
-
-One common way to construct an 'Index' is to programmatically __unfold__ a seed value using 
-'Index.unfoldr'. Below, we want to generate numbers from 7 down to 1:
-
->>> Index.unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)
-Index [1,2,3,4,5,6,7]
-
-This task is so common that there is a convenience function to create ranges, 'Index.range'. 
-For example, if you want to create an 'Index' of values starting at 1 and ending at 10, in 
-steps of 3:
-
->>> Index.range (+3) (1 :: Int) 10
-Index [1,4,7,10]
-
-An 'Index' is very much like a 'Set', so you can 
-
-* check for membership using 'Index.member';
-* combine two 'Index' using 'Index.union', 'Index.intersection', and 'Index.difference';
-* find the integer index of a key using 'Index.lookupIndex';
-
-and more.
-
--}
-
-{- $singlekey 
-
-Single-element selections are performed using 'at', which selects a single element by key. 'at' is safe;
-if the key is missing, 'Nothing' is returned:
-
->>> let xs = Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]
->>> xs
-index | values
------ | ------
-  'a' |      1
-  'b' |      2
-  'c' |      3
-  'd' |      4
->>> xs `at` 'a'
-Just 1
->>> xs `at` 'z'
-Nothing
-
--}
-
-{- $multikey 
-
-Bulk selection, also known as *slicing*, is the method by which we extract a sub-series from a series.
-In the examples below, we'll assume that we have the series @aapl_close@ is available in-scope, which represents
-the closing price of Apple stock:
-
->>> :{
-let aapl_close = Series.fromList [ ("2010-01-04", 6.5522 :: Double)
-                                 , ("2010-01-05", 6.5636)
-                                 , ("2010-01-06", 6.4592)
-                                 , ("2010-01-07", 6.4472)
-                                 , ("2010-01-08", 6.4901)
-                                 -- No prices during the weekend
-                                 , ("2010-01-11", 6.5152)
-                                 , ("2010-01-12", 6.4047)
-                                 , ("2010-01-13", 6.3642)
-                                 , ("2010-01-14", 6.4328)
-                                 , ("2010-01-15", 6.4579)
-                                 ]
-    :}
-
-Bulk selection is done via the 'select' function. 'select' works with many types of inputs. 
-For example, we can query for a contiguous range of keys by using 'to':
-
->>> aapl_close `select` "2010-01-04" `to` "2010-01-08"
-       index | values
-       ----- | ------
-"2010-01-04" | 6.5522
-"2010-01-05" | 6.5636
-"2010-01-06" | 6.4592
-"2010-01-07" | 6.4472
-"2010-01-08" | 6.4901
-
-You can also request unbounded ranges. For example all dates up to @"2010-01-08"@ using 'upto':
-
->>> aapl_close `select` upto "2010-01-08"
-       index | values
-       ----- | ------
-"2010-01-04" | 6.5522
-"2010-01-05" | 6.5636
-"2010-01-06" | 6.4592
-"2010-01-07" | 6.4472
-"2010-01-08" | 6.4901
-
-There's also the other unbound range, 'from':
-
->>> aapl_close `select` from "2010-01-11"
-       index | values
-       ----- | ------
-"2010-01-11" | 6.5152
-"2010-01-12" | 6.4047
-"2010-01-13" | 6.3642
-"2010-01-14" | 6.4328
-"2010-01-15" | 6.4579
-
-Note that the bounds may contain less data than you think! For example, 
-let's look at a 5-day range:
-
->>> aapl_close `select` "2010-01-08" `to` "2010-01-12"
-       index | values
-       ----- | ------
-"2010-01-08" | 6.4901
-"2010-01-11" | 6.5152
-"2010-01-12" | 6.4047
-
-We've requested a range of 5 days (@"2010-01-08"@, @"2010-01-09"@, @"2010-01-10"@, @"2010-01-11"@, @"2010-01-12"@), 
-but there's no data in our series with the keys @"2010-01-09"@ and @"2010-01-10"@, because it was the week-end 
-(stock markets are usually closed on week-ends). 
-
-Sometimes you want to be more specific than a contiguous range of data; 'select' 
-also supports bulk *random* access like so:
-
->>> aapl_close `select` ["2010-01-08", "2010-01-10", "2010-01-12"]
-       index | values
-       ----- | ------
-"2010-01-08" | 6.4901
-"2010-01-12" | 6.4047
-
-Note above that we've requested data for the date @"2010-01-10"@, but it's missing. Therefore, 
-the data isn't returned. If you want to get a sub-series which has the exact index that 
-you've asked for, you can use 'require' in combination with an 'Index':
-
->>> import qualified Data.Series.Index as Index
->>> aapl_close `require` Index.fromList ["2010-01-08", "2010-01-10", "2010-01-12"]
-       index |      values
-       ----- |      ------
-"2010-01-08" | Just 6.4901
-"2010-01-10" |     Nothing
-"2010-01-12" | Just 6.4047
-
-Using 'require' or 'select' in conjunction with 'Index.range' is very powerful.
-
--}
-
-{- $filteringandmapping 
-
-'Series' support operations on both their index and their values. To illustrate 
-this, let's load some latitude and longitude data for some cities.
-
-We'll assume that the following types are in scope:
-
->>> import Data.Fixed (Centi)
->>> data Position = Pos { latitude :: Centi, longitude :: Centi } deriving (Show)
->>> :{
-    let cities = Series.fromList [ ("Paris"::String , Pos  48.86    2.35)
-                                 , ("New York City" , Pos  40.71   (-74.01))
-                                 , ("Taipei"        , Pos  25.04    121.56)
-                                 , ("Buenos Aires"  , Pos (-34.60) (-58.38)) 
-                                 ]
-    :}
-
-We can easily filter for data just like you would filter a list. 
-In this example, let's find cities in the western hemisphere (i.e. cities 
-which have negative longitudes), using 'Series.filter':
-
->>> Series.filter (\pos -> longitude pos < 0) cities
-          index |                                      values
-          ----- |                                      ------
- "Buenos Aires" | Pos {latitude = -34.60, longitude = -58.38}
-"New York City" |  Pos {latitude = 40.71, longitude = -74.01}
-
-We can transform the values of a 'Series' using 'Series.map'. In this example, 
-let's isolate the latitude of cities in the western hemisphere:
-
->>> let western_cities = Series.filter (\pos -> longitude pos < 0) cities
->>> Series.map latitude western_cities
-          index | values
-          ----- | ------
- "Buenos Aires" | -34.60
-"New York City" |  40.71
-
-Finally, we can summarize the 'Series' by reducing all its values. 
-Let's average the latitude of cities in the western hemisphere:
-
->>> import Data.Series ( mean )
->>> let latitudes = Series.map latitude western_cities
->>> Series.fold mean latitudes
-3.05
-
-The next section introduces 'Series.fold' more generally.
--}
-
-{- $folding
-
-Folding refers to the action of aggregating values in a 'Series' to a single value.
-Folding 'Series' is done through the 'Series.fold' function. Its type signature is:
-
->>> :t Series.fold
-Series.fold :: Fold a b -> Series k a -> b
-
-Here, @'Fold' a b@ represents a calculation which takes in values of type @a@, and will ultimately produce a
-final value of type b. Such calculations are provided by the @foldl@ package (see 'Control.Foldl'), although
-some of its functions are re-exported by "Data.Series" (and "Data.Series.Unboxed"), such as 'Data.Series.mean'.
-
-Let's look at an example. First, we'll need some data. We'll use end-of-day stock prices for Apple Inc:
-
->>> import Data.Fixed ( Centi )
->>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"
->>> aapl_closing 
-       index |  values
-       ----- |  ------
-"1980-12-12" |  0.1007
-"1980-12-15" | 9.54e-2
-"1980-12-16" | 8.84e-2
-         ... |     ...
-"2022-01-05" |  174.92
-"2022-01-06" |   172.0
-"2022-01-07" |  172.17
-
-Normally we would use an appropriate datetime type for the index of @aapl_closing@, 
-for example from the @time@ package, but we're keeping it simple for this tutorial. 
-
-Prices have changed a lot over the years, so we'll restrict ourselves to 2021:
-
->>> let aapl_closing_2021 = aapl_closing `select` "2021-01-01" `to` "2021-12-31"
->>> aapl_closing_2021
-       index |   values
-       ----- |   ------
-"2021-01-04" | 128.6174
-"2021-01-05" | 130.2076
-"2021-01-06" | 125.8246
-         ... |      ...
-"2021-12-29" |   179.38
-"2021-12-30" |    178.2
-"2021-12-31" |   177.57
-
-To calculate the average closing price over the year 2021, we use 'Data.Series.fold' in conjunction with
-'Data.Series.mean':
-
->>> Series.fold Series.mean aapl_closing_2021
-140.61256349206354
-
-One of the magic things about 'Fold' is that it's possible to combine them in such a way that you can 
-traverse a 'Series' only once, which is important for good performance. As an example, we'll calculate
-both the mean closing price AND the standard deviation of closing prices.
-
->>> let meanAndStdDev = (,) <$> Data.Series.mean <*> Data.Series.std
->>> Series.fold meanAndStdDev aapl_closing_2021
-(140.61256349206354,14.811663837435361)
-
-See 'Control.Foldl' from the @foldl@ package for more information on 'Fold'.
--}
-
-{- $grouping
-
-One important feature of 'Series' is the ability to efficiently group values 
-together based on their keys.
-
-Let's load some stock price data again for this part:
-
->>> import Data.Fixed ( Centi )
->>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"
->>> aapl_closing 
-       index |  values
-       ----- |  ------
-"1980-12-12" |  0.1007
-"1980-12-15" | 9.54e-2
-"1980-12-16" | 8.84e-2
-         ... |     ...
-"2022-01-05" |  174.92
-"2022-01-06" |   172.0
-"2022-01-07" |  172.17
-
-Grouping involves two steps:
-
-  (1) Grouping keys in some way using 'groupBy';
-  (2) Aggregating the values in each group using 'aggregateWith' or other variants.
-
-Let's find the highest closing price of each month. First, we need to define
-our grouping function:
-
->>> :{ 
-       -- | Extract the year and month from a date like XXXX-YY-ZZ. For example:
-       -- 
-       -- >>> month "2023-01-01"
-       -- "2023-01"
-       month :: String -> String
-       month = take 7
-    :}
-
-Then, we can group keys by month and take the 'maximum' of each group:
-
->>> aapl_closing `groupBy` month `aggregateWith` maximum
-    index | values
-    ----- | ------
-"1980-12" | 0.1261
-"1981-01" | 0.1208
-"1981-02" | 0.1007
-      ... |    ...
-"2021-11" |  165.3
-"2021-12" | 180.33
-"2022-01" | 182.01
-
-This means, for example, that the maximum closing price for Apple stock in the 
-month of November 2021 was $165.30 per share. This library also contains 
-numerical aggregation functions such as 'Data.Series.mean' and 'Data.Series.std'. Therefore, in order 
-to find the monthly average Apple closing price, rounded to the nearest cent:
-
->>> import Data.Series (mean)
->>> let (roundToCent :: Double -> Double) = \x -> fromIntegral ((round $ x * 100) :: Int) / 100
->>> aapl_closing `groupBy` month `aggregateWith` (roundToCent . Series.fold mean)
-    index | values
-    ----- | ------
-"1980-12" |   0.11
-"1981-01" |   0.11
-"1981-02" | 9.0e-2
-      ... |    ...
-"2021-11" | 154.21
-"2021-12" | 173.55
-"2022-01" | 176.16
-
--}
-
-{- $windowing
-
-Windowing aggregation refers to the practice of aggregating values in a window around every key.
-
-General-purpose windowing is done using the 'windowing' function. Let's look at its
-type signature:
-
->>> :t windowing
-windowing
-  :: Ord k =>
-     (k -> Range k) -> (Series k a -> b) -> Series k a -> Series k b
-
-Here, @`windowing` window aggfunc xs@ is a new series @'Series' k b@ where
-for every key @k@, the values in the range @window k@ are aggregated by @aggfunc@
-and placed in the resulting series at key @k@. Here's an example where
-for every key @k@, we add the values at @k@ and @k+1@:
-
->>> :{ 
-let (xs :: Series Int Int) 
-      = Series.fromList [ (1, 0)
-                        , (2, 1)
-                        , (3, 2)
-                        , (4, 3)
-                        , (5, 4)
-                        , (6, 5)
-                        ]
-in windowing (\k -> k `to` (k + 1)) sum xs
-:}
-index | values
------ | ------
-    1 |      1
-    2 |      3
-    3 |      5
-    4 |      7
-    5 |      9
-    6 |      5
-
-'windowing' can be used to compute so-called rolling aggregations. An example of
-this is to compute the rolling mean of the last 3 keys:
-
->>> import Data.Series ( mean )
->>> :{ 
-let rollingMean = windowing (\k -> (k-3) `to` k) (Series.fold mean)
-    (xs :: Series Int Double) 
-      = Series.fromList [ (1, 0)
-                        , (2, 1)
-                        , (3, 2)
-                        , (4, 3)
-                        , (5, 4)
-                        , (6, 5)
-                        ]
- in (rollingMean xs) :: Series Int Double
-:}
-index | values
------ | ------
-    1 |    0.0
-    2 |    0.5
-    3 |    1.0
-    4 |    1.5
-    5 |    2.5
-    6 |    3.5
-
--}
-
-{- $zipping 
-
-An important class of operations are combining two 'Series' together, also known as *zipping*. 
-For lists, Haskell has 'Data.List.zipWith'. 'Series' also have 'Series.zipWith' and variants:
-
-* 'Series.zipWith', which combines two series with some elementwise function;
-* 'Series.zipWithMatched', which combines two series with some elementwise function 
-  on keys which are in *both* maps;
-* 'Series.zipWithStrategy', which combines two series with some elementwise 
-  function and supports custom operations to deal with missing keys;
-
-To illustrate the differences between the various zipping functions, 
-consider the following two series. There's population:
-
->>> :set -XNumericUnderscores
->>> import Data.Fixed (Centi)
->>> :{ 
-    -- Most recent population estimate rounded to the nearest million
-    let population = Series.fromList [ ("Canada"::String, 40_000_000::Centi)
-                                     , ("Kenya"         , 56_000_000)
-                                     , ("Poland"        , 38_000_000)
-                                     , ("Singapore"     ,  6_000_000)
-                                     ]
-    :}
-
-and there's total land mass:
-
->>> :{ 
-    -- Land mass in square kilometer
-    let landmass = Series.fromList [ ("Brazil"::String, 8_520_000::Centi)
-                                   , ("Canada",         9_990_000)
-                                   , ("Kenya",            580_000)
-                                   , ("Poland",           313_000)
-                                   ] 
-    :}
-
-@'Series.zipWith' f left right@ combines the series @left@ and @right@ using the 
-function @f@ which admits two arguments, for all keys one-by-one. If a key 
-is missing from either @left@ or @right@, 'Series.zipWith' returns 'Nothing'. For example, 
-the population density per country would be:
-
->>> Series.zipWith (/) population landmass
-      index |      values
-      ----- |      ------
-   "Brazil" |     Nothing
-   "Canada" |   Just 4.00
-    "Kenya" |  Just 96.55
-   "Poland" | Just 121.40
-"Singapore" |     Nothing
-
-Since we don't have population estimates for Brazil and no land mass 
-information for Singapore, we can't calculate their population densities.
-
-Sometimes, we only care about the results of @'Series.zipWith' f@ where keys are 
-in both series. In this case, we can use 'Series.zipWithMatched':
-
->>> Series.zipWithMatched (/) population landmass
-   index | values
-   ----- | ------
-"Canada" |   4.00
- "Kenya" |  96.55
-"Poland" | 121.40
-
-Finally, in case we want full control over what to do when a key is missing, 
-we can use @Series.zipWithStrategy'. For example, consider the case where:
-
-* If population numbers are missing, I want to set the density to 0;
-* If land mass information is missing, I wait to skip calculating the density of this country. 
-
->>> import Data.Series (skipStrategy, constStrategy)
->>> let noPopulationStrategy = Series.constStrategy 0
->>> let noLandmassStrategy   = Series.skipStrategy
->>> Series.zipWithStrategy (/) noPopulationStrategy noLandmassStrategy population landmass
-      index | values
-      ----- | ------
-   "Canada" |   4.00
-    "Kenya" |  96.55
-   "Poland" | 121.40
-"Singapore" |   0.00
-
-As you can imagine, 'Series.zipWithStrategy' is the most general and gives the most control, but is less easy 
-to use than 'Series.zipWith' and 'Series.zipWithMatched'.
-
--}
-
-{- $conclusion
-
-This section concludes the introductory tutorial to the @javelin@ package and its "Data.Series" module.
-
-For a more in-depth look at this package, you can read the full documentation for each module:
-
-* "Data.Series"
-* "Data.Series.Index"
-* "Data.Series.Unboxed"
-* "Data.Series.Generic"
-
--}
-
-{- $duplicates
-
-If you must build a 'Series' with duplicate keys, you can use the 'Data.Series.fromListDuplicates' or 
-'Data.Series.fromVectorDuplicates' functions. 
-In the example below, the key @\'d\'@ is repeated three times:
-
->>> Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
-  index | values
-  ----- | ------
-('a',0) |      5
-('b',0) |      0
-('d',0) |      1
-('d',1) |     -4
-('d',2) |      7
-
-Note that the 'Series' produced by 'Data.Series.fromListDuplicates' still has unique keys, but each key is a 
-composite of a character and an occurrence. This is reflected in the type:
-
->>> :t Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
-Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
-  :: Series (Char, Occurrence) Int
-
-Here, 'Data.Series.Occurrence' is a non-negative number, and can be converted to 
-other integer-like numbers using 'fromIntegral'. In practice, you should aim to aggregate your 'Series' to remove duplicate keys, for example
-using 'Data.Series.groupBy' and grouping on the first element of the key ('fst'):
-
->>> let xs = Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
->>> xs `groupBy` fst `aggregateWith` sum
-index | values
------ | ------
-  'a' |      5
-  'b' |      0
-  'd' |      4
-
--}
-
-{- $unboxed 
-
-The 'Data.Series.Series' defined in "Data.Series" are based on 'Data.Vector.Vector' from "Data.Vector". 
-This implementation is nice because such 'Series' can hold _any_ Haskell type. However, because
-Haskell types can be arbitrarily complex, numerical operations on 'Series' may not be as fast
-as could be.
-
-For simpler types such as 'Double' and 'Int', a different kind of series can be used to
-speed up numerical calculations: 'Data.Series.Unboxed.Series' from the "Data.Series.Unboxed" module.
-Such 'Data.Series.Unboxed.Series' are much more limited: they can only contain datatypes which are
-instances of 'Data.Vector.Unboxed.Unbox'. 
-
-This then brings the question: how can you write software which supports both ordinary 'Data.Series.Series'
-__and__ unboxed 'Data.Series.Unboxed.Series'? The answer is to use functions from the "Data.Series.Generic".
-
-For example, we could implement the dot product of two series as:
-
->>> import qualified Data.Series.Generic as G
->>> import Data.Vector.Generic ( Vector )
->>> :{
-      dot :: (Ord k, Num a, Vector v a) => G.Series v k a -> G.Series v k a -> a
-      dot v1 v2 = G.sum $ G.zipWithMatched (*) v1 v2
-    :}
-
-You can convert between the two types of series using the 'Data.Series.Generic.convert' function.
-
--}
-
-{- $replacement 
-
-'Series.map' allows to map every value of a series. How about replacing *some* 
-values in a series? The function 'Data.Series.replace' (and its infix variant, '|->') replaces values in the right operand 
-which have an analogue in the left operand:
-
->>> import Data.Series ( (|->) )
->>> let nan = (0/0) :: Double
->>> let right = Series.fromList [('a', 1), ('b', nan), ('c', 3), ('d', nan)]
->>> right
-index | values
------ | ------
-  'a' |    1.0
-  'b' |    NaN
-  'c' |    3.0
-  'd' |    NaN
->>> let left = Series.fromList [('b', 0::Double), ('d', 0), ('e', 0)]
->>> left
-index | values
------ | ------
-  'b' |    0.0
-  'd' |    0.0
-  'e' |    0.0
->>> left |-> right
-index | values
------ | ------
-  'a' |    1.0
-  'b' |    0.0
-  'c' |    3.0
-  'd' |    0.0
-
-In the example above, the key @\'e\'@ is ignored since it was not in the @right@ 
-series to begin with.
-
-The flipped version, '<-|', is also available.
-
--}
-
-{- $comparison 
-
-Below is a table showing which operations on "Data.Series" have analogues for 
-other data structures.
-
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Action                          | "Data.Series"                  | "Data.Map.Strict"               | "Data.List"       | "Data.Vector"        |
-+=================================+================================+=================================+===================+======================+
-| Mapping values                  | 'Data.Series.map'              | 'Data.Map.Strict.map'           | 'map'             | 'Data.Vector.map'    |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Mapping index                   | 'Data.Series.mapIndex'         | 'Data.Map.Strict.mapKeys'       |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Mapping values with key         | 'Data.Series.mapWithKey'       | 'Data.Map.Strict.mapWithKey'    |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Filtering values                | 'Data.Series.filter'           | 'Data.Map.Strict.filter'        | 'filter'          | 'Data.Vector.filter' |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Filtering index                 | 'Data.Series.select',          | 'Data.Map.Strict.filterWithKey' |                   |                      |
-|                                 | 'Data.Series.filterWithKey'    |                                 |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Indexing by key                 | 'Data.Series.at'               | 'Data.Map.Strict.lookup'        |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Indexing by position            | 'Data.Series.iat'              |                                 | 'Data.List.!'     | 'Data.Vector.!'      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Combine two structures key-wise | 'Data.Series.zipWith'          | 'Data.Map.Merge.Strict.merge'   |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Union                           | 'Data.Series.<>'               | 'Data.Map.Strict.union'         | 'Data.List.union' |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-| Group keys                      | 'Data.Series.groupBy'          |                                 |                   |                      |
-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
-
--}
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+
+module Data.Series.Tutorial (
+    -- * Introduction
+    -- $introduction
+
+    -- * Construction
+    -- $construction
+
+    -- * Index
+    -- $index
+
+    -- * Selections
+    -- ** Single-key selection
+    -- $singlekey
+
+    -- ** Bulk selections
+    -- $multikey
+
+    -- * Filtering and mapping
+    -- $filteringandmapping
+
+    -- * Folding
+    -- $folding
+
+    -- * Grouping
+    -- $grouping
+
+    -- * Window aggregation
+    -- $windowing
+
+    -- * Combining 'Series' together
+    -- $zipping
+
+    -- * Conclusion
+    -- $conclusion and further reading
+    
+    -- * Advanced topics
+    -- ** Handling duplicate keys
+    -- $duplicates
+
+    -- ** Unboxed and generic series
+    -- $unboxed
+
+    -- ** Replacing values
+    -- $replacement
+
+    -- ** Comparison with other data structures
+    -- $comparison
+
+) where
+
+import           Control.Foldl   ( Fold )
+import           Data.Series     ( IsSeries(..), Series, Occurrence, at, iat, select, to, from, upto, require
+                                 , groupBy, aggregateWith, (<-|), (|->), Range, windowing
+                                 )
+import qualified Data.Series     as Series
+import qualified Data.Series.Generic
+import           Data.Series.Index ( Index )
+import qualified Data.Series.Index as Index
+import qualified Data.Series.Unboxed
+import           Data.Set        ( Set )
+import qualified Data.Set
+import           Data.Map.Strict ( Map )
+import qualified Data.Map.Strict
+import qualified Data.Map.Merge.Strict
+import           Numeric.Natural ( Natural)
+import qualified Data.List
+import qualified Data.Vector
+import qualified Data.Vector.Unboxed
+
+{- $introduction
+
+This is a short user guide on how to get started using @javelin@ and its various modules.
+
+The central data structure at the heart of this package is the 'Series'. A @'Series' k a@ 
+is a labeled array of type @v@ filled with values of type @a@, indexed by keys of type @k@.
+
+Like 'Data.Map.Strict.Map', 'Series' support efficient:
+
+* random access by key ( \(O(\log n)\) );
+* slice by key ( \(O(\log n)\) ).
+
+Like 'Data.Vector.Vector', 'Series' support efficient:
+
+* numerical operations.
+* random access by index ( \(O(1)\) );
+* slice by index ( \(O(1)\) );     
+
+To follow along this tutorial, the following imports are expected:
+
+>>> import Data.Series as Series
+-}
+
+{- $construction 
+
+The easiest way to create a 'Series' is to do it from a list using 'Data.Series.fromList':
+
+>>> Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]
+index | values
+----- | ------
+  'a' |      1
+  'b' |      2
+  'c' |      3
+  'd' |      4
+
+Note what happens when we have the same key (@\'a\'@) attached to multiple values:
+
+>>> Series.fromList [ ('a', 1::Int), ('a', 0), ('b', 2), ('c', 3), ('d', 4) ]
+index | values
+----- | ------
+  'a' |      0
+  'b' |      2
+  'c' |      3
+  'd' |      4
+
+'Series', like 'Map's, have unique keys; therefore, the output series may 
+not be the same length as the input series. See further below for an 
+explanation of how to handle duplicate keys. 
+
+Since 'Series' are like 'Map', it's easy to convert between the two:
+
+>>> let mp = Data.Map.Strict.fromList [ ('a', 0::Int), ('a', 1), ('b', 2), ('c', 3), ('d', 4) ]
+>>> mp
+fromList [('a',1),('b',2),('c',3),('d',4)]
+>>> Series.fromStrictMap mp
+index | values
+----- | ------
+  'a' |      1
+  'b' |      2
+  'c' |      3
+  'd' |      4
+
+Of course, 'Series.fromLazyMap' is also available. In fact, conversion to/from 'Series' is supported for
+many types; see the 'IsSeries' typeclass and its methods, 'toSeries' and 'fromSeries'.
+
+-}
+
+{- $index
+
+'Series' have two components: values and an index.
+
+The index (of type @'Index' k@) is an ordered set of unique elements which allows to determine 
+where are each values in the series. Since all keys in an 'Index' are unique and sorted, it
+is fast to find the value associated to any random key.
+
+As we'll see soon, 'Index' is an important data structure which can be used to slice through a 'Series', 
+so let's get comfortable with them.
+
+>>> import qualified Data.Series.Index as Index
+
+An 'Index' can be constructed from a list:
+
+>>> Index.fromList [5::Int,5,4,3,2,1,5,5,5]
+Index [1,2,3,4,5]
+
+As you see above, repeated elements (in this case, @5@) won't be repeated in the 'Index'. Therefore, it often makes 
+more sense to construct an 'Index' using 'Index.fromSet' from a 'Set' from "Data.Set".
+
+One common way to construct an 'Index' is to programmatically __unfold__ a seed value using 
+'Index.unfoldr'. Below, we want to generate numbers from 7 down to 1:
+
+>>> Index.unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)
+Index [1,2,3,4,5,6,7]
+
+This task is so common that there is a convenience function to create ranges, 'Index.range'. 
+For example, if you want to create an 'Index' of values starting at 1 and ending at 10, in 
+steps of 3:
+
+>>> Index.range (+3) (1 :: Int) 10
+Index [1,4,7,10]
+
+An 'Index' is very much like a 'Set', so you can 
+
+* check for membership using 'Index.member';
+* combine two 'Index' using 'Index.union', 'Index.intersection', and 'Index.difference';
+* find the integer index of a key using 'Index.lookupIndex';
+
+and more.
+
+-}
+
+{- $singlekey 
+
+Single-element selections are performed using 'at', which selects a single element by key. 'at' is safe;
+if the key is missing, 'Nothing' is returned:
+
+>>> let xs = Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]
+>>> xs
+index | values
+----- | ------
+  'a' |      1
+  'b' |      2
+  'c' |      3
+  'd' |      4
+>>> xs `at` 'a'
+Just 1
+>>> xs `at` 'z'
+Nothing
+
+-}
+
+{- $multikey 
+
+Bulk selection, also known as *slicing*, is the method by which we extract a sub-series from a series.
+In the examples below, we'll assume that we have the series @aapl_close@ is available in-scope, which represents
+the closing price of Apple stock:
+
+>>> :{
+let aapl_close = Series.fromList [ ("2010-01-04", 6.5522 :: Double)
+                                 , ("2010-01-05", 6.5636)
+                                 , ("2010-01-06", 6.4592)
+                                 , ("2010-01-07", 6.4472)
+                                 , ("2010-01-08", 6.4901)
+                                 -- No prices during the weekend
+                                 , ("2010-01-11", 6.5152)
+                                 , ("2010-01-12", 6.4047)
+                                 , ("2010-01-13", 6.3642)
+                                 , ("2010-01-14", 6.4328)
+                                 , ("2010-01-15", 6.4579)
+                                 ]
+    :}
+
+Bulk selection is done via the 'select' function. 'select' works with many types of inputs. 
+For example, we can query for a contiguous range of keys by using 'to':
+
+>>> aapl_close `select` "2010-01-04" `to` "2010-01-08"
+       index | values
+       ----- | ------
+"2010-01-04" | 6.5522
+"2010-01-05" | 6.5636
+"2010-01-06" | 6.4592
+"2010-01-07" | 6.4472
+"2010-01-08" | 6.4901
+
+You can also request unbounded ranges. For example all dates up to @"2010-01-08"@ using 'upto':
+
+>>> aapl_close `select` upto "2010-01-08"
+       index | values
+       ----- | ------
+"2010-01-04" | 6.5522
+"2010-01-05" | 6.5636
+"2010-01-06" | 6.4592
+"2010-01-07" | 6.4472
+"2010-01-08" | 6.4901
+
+There's also the other unbound range, 'from':
+
+>>> aapl_close `select` from "2010-01-11"
+       index | values
+       ----- | ------
+"2010-01-11" | 6.5152
+"2010-01-12" | 6.4047
+"2010-01-13" | 6.3642
+"2010-01-14" | 6.4328
+"2010-01-15" | 6.4579
+
+Note that the bounds may contain less data than you think! For example, 
+let's look at a 5-day range:
+
+>>> aapl_close `select` "2010-01-08" `to` "2010-01-12"
+       index | values
+       ----- | ------
+"2010-01-08" | 6.4901
+"2010-01-11" | 6.5152
+"2010-01-12" | 6.4047
+
+We've requested a range of 5 days (@"2010-01-08"@, @"2010-01-09"@, @"2010-01-10"@, @"2010-01-11"@, @"2010-01-12"@), 
+but there's no data in our series with the keys @"2010-01-09"@ and @"2010-01-10"@, because it was the week-end 
+(stock markets are usually closed on week-ends). 
+
+Sometimes you want to be more specific than a contiguous range of data; 'select' 
+also supports bulk *random* access like so:
+
+>>> aapl_close `select` ["2010-01-08", "2010-01-10", "2010-01-12"]
+       index | values
+       ----- | ------
+"2010-01-08" | 6.4901
+"2010-01-12" | 6.4047
+
+Note above that we've requested data for the date @"2010-01-10"@, but it's missing. Therefore, 
+the data isn't returned. If you want to get a sub-series which has the exact index that 
+you've asked for, you can use 'require' in combination with an 'Index':
+
+>>> import qualified Data.Series.Index as Index
+>>> aapl_close `require` Index.fromList ["2010-01-08", "2010-01-10", "2010-01-12"]
+       index |      values
+       ----- |      ------
+"2010-01-08" | Just 6.4901
+"2010-01-10" |     Nothing
+"2010-01-12" | Just 6.4047
+
+Using 'require' or 'select' in conjunction with 'Index.range' is very powerful.
+
+-}
+
+{- $filteringandmapping 
+
+'Series' support operations on both their index and their values. To illustrate 
+this, let's load some latitude and longitude data for some cities.
+
+We'll assume that the following types are in scope:
+
+>>> import Data.Fixed (Centi)
+>>> data Position = Pos { latitude :: Centi, longitude :: Centi } deriving (Show)
+>>> :{
+    let cities = Series.fromList [ ("Paris"::String , Pos  48.86    2.35)
+                                 , ("New York City" , Pos  40.71   (-74.01))
+                                 , ("Taipei"        , Pos  25.04    121.56)
+                                 , ("Buenos Aires"  , Pos (-34.60) (-58.38)) 
+                                 ]
+    :}
+
+We can easily filter for data just like you would filter a list. 
+In this example, let's find cities in the western hemisphere (i.e. cities 
+which have negative longitudes), using 'Series.filter':
+
+>>> Series.filter (\pos -> longitude pos < 0) cities
+          index |                                      values
+          ----- |                                      ------
+ "Buenos Aires" | Pos {latitude = -34.60, longitude = -58.38}
+"New York City" |  Pos {latitude = 40.71, longitude = -74.01}
+
+We can transform the values of a 'Series' using 'Series.map'. In this example, 
+let's isolate the latitude of cities in the western hemisphere:
+
+>>> let western_cities = Series.filter (\pos -> longitude pos < 0) cities
+>>> Series.map latitude western_cities
+          index | values
+          ----- | ------
+ "Buenos Aires" | -34.60
+"New York City" |  40.71
+
+Finally, we can summarize the 'Series' by reducing all its values. 
+Let's average the latitude of cities in the western hemisphere:
+
+>>> import Data.Series ( mean )
+>>> let latitudes = Series.map latitude western_cities
+>>> Series.fold mean latitudes
+3.05
+
+The next section introduces 'Series.fold' more generally.
+-}
+
+{- $folding
+
+Folding refers to the action of aggregating values in a 'Series' to a single value.
+Folding 'Series' is done through the 'Series.fold' function. Its type signature is:
+
+>>> :t Series.fold
+Series.fold :: Fold a b -> Series k a -> b
+
+Here, @'Fold' a b@ represents a calculation which takes in values of type @a@, and will ultimately produce a
+final value of type b. Such calculations are provided by the @foldl@ package (see 'Control.Foldl'), although
+some of its functions are re-exported by "Data.Series" (and "Data.Series.Unboxed"), such as 'Data.Series.mean'.
+
+Let's look at an example. First, we'll need some data. We'll use end-of-day stock prices for Apple Inc:
+
+>>> import Data.Fixed ( Centi )
+>>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"
+>>> aapl_closing 
+       index |  values
+       ----- |  ------
+"1980-12-12" |  0.1007
+"1980-12-15" | 9.54e-2
+"1980-12-16" | 8.84e-2
+         ... |     ...
+"2022-01-05" |  174.92
+"2022-01-06" |   172.0
+"2022-01-07" |  172.17
+
+Normally we would use an appropriate datetime type for the index of @aapl_closing@, 
+for example from the @time@ package, but we're keeping it simple for this tutorial. 
+
+Prices have changed a lot over the years, so we'll restrict ourselves to 2021:
+
+>>> let aapl_closing_2021 = aapl_closing `select` "2021-01-01" `to` "2021-12-31"
+>>> aapl_closing_2021
+       index |   values
+       ----- |   ------
+"2021-01-04" | 128.6174
+"2021-01-05" | 130.2076
+"2021-01-06" | 125.8246
+         ... |      ...
+"2021-12-29" |   179.38
+"2021-12-30" |    178.2
+"2021-12-31" |   177.57
+
+To calculate the average closing price over the year 2021, we use 'Data.Series.fold' in conjunction with
+'Data.Series.mean':
+
+>>> Series.fold Series.mean aapl_closing_2021
+140.61256349206354
+
+One of the magic things about 'Fold' is that it's possible to combine them in such a way that you can 
+traverse a 'Series' only once, which is important for good performance. As an example, we'll calculate
+both the mean closing price AND the standard deviation of closing prices.
+
+>>> let meanAndStdDev = (,) <$> Data.Series.mean <*> Data.Series.std
+>>> Series.fold meanAndStdDev aapl_closing_2021
+(140.61256349206354,14.811663837435361)
+
+See 'Control.Foldl' from the @foldl@ package for more information on 'Fold'.
+-}
+
+{- $grouping
+
+One important feature of 'Series' is the ability to efficiently group values 
+together based on their keys.
+
+Let's load some stock price data again for this part:
+
+>>> import Data.Fixed ( Centi )
+>>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"
+>>> aapl_closing 
+       index |  values
+       ----- |  ------
+"1980-12-12" |  0.1007
+"1980-12-15" | 9.54e-2
+"1980-12-16" | 8.84e-2
+         ... |     ...
+"2022-01-05" |  174.92
+"2022-01-06" |   172.0
+"2022-01-07" |  172.17
+
+Grouping involves two steps:
+
+  (1) Grouping keys in some way using 'groupBy';
+  (2) Aggregating the values in each group using 'aggregateWith' or other variants.
+
+Let's find the highest closing price of each month. First, we need to define
+our grouping function:
+
+>>> :{ 
+       -- | Extract the year and month from a date like XXXX-YY-ZZ. For example:
+       -- 
+       -- >>> month "2023-01-01"
+       -- "2023-01"
+       month :: String -> String
+       month = take 7
+    :}
+
+Then, we can group keys by month and take the 'maximum' of each group:
+
+>>> aapl_closing `groupBy` month `aggregateWith` maximum
+    index | values
+    ----- | ------
+"1980-12" | 0.1261
+"1981-01" | 0.1208
+"1981-02" | 0.1007
+      ... |    ...
+"2021-11" |  165.3
+"2021-12" | 180.33
+"2022-01" | 182.01
+
+This means, for example, that the maximum closing price for Apple stock in the 
+month of November 2021 was $165.30 per share. This library also contains 
+numerical aggregation functions such as 'Data.Series.mean' and 'Data.Series.std'. Therefore, in order 
+to find the monthly average Apple closing price, rounded to the nearest cent:
+
+>>> import Data.Series (mean)
+>>> let (roundToCent :: Double -> Double) = \x -> fromIntegral ((round $ x * 100) :: Int) / 100
+>>> aapl_closing `groupBy` month `aggregateWith` (roundToCent . Series.fold mean)
+    index | values
+    ----- | ------
+"1980-12" |   0.11
+"1981-01" |   0.11
+"1981-02" | 9.0e-2
+      ... |    ...
+"2021-11" | 154.21
+"2021-12" | 173.55
+"2022-01" | 176.16
+
+-}
+
+{- $windowing
+
+Windowing aggregation refers to the practice of aggregating values in a window around every key.
+
+General-purpose windowing is done using the 'windowing' function. Let's look at its
+type signature:
+
+>>> :t windowing
+windowing
+  :: Ord k =>
+     (k -> Range k) -> (Series k a -> b) -> Series k a -> Series k b
+
+Here, @`windowing` window aggfunc xs@ is a new series @'Series' k b@ where
+for every key @k@, the values in the range @window k@ are aggregated by @aggfunc@
+and placed in the resulting series at key @k@. Here's an example where
+for every key @k@, we add the values at @k@ and @k+1@:
+
+>>> :{ 
+let (xs :: Series Int Int) 
+      = Series.fromList [ (1, 0)
+                        , (2, 1)
+                        , (3, 2)
+                        , (4, 3)
+                        , (5, 4)
+                        , (6, 5)
+                        ]
+in windowing (\k -> k `to` (k + 1)) sum xs
+:}
+index | values
+----- | ------
+    1 |      1
+    2 |      3
+    3 |      5
+    4 |      7
+    5 |      9
+    6 |      5
+
+'windowing' can be used to compute so-called rolling aggregations. An example of
+this is to compute the rolling mean of the last 3 keys:
+
+>>> import Data.Series ( mean )
+>>> :{ 
+let rollingMean = windowing (\k -> (k-3) `to` k) (Series.fold mean)
+    (xs :: Series Int Double) 
+      = Series.fromList [ (1, 0)
+                        , (2, 1)
+                        , (3, 2)
+                        , (4, 3)
+                        , (5, 4)
+                        , (6, 5)
+                        ]
+ in (rollingMean xs) :: Series Int Double
+:}
+index | values
+----- | ------
+    1 |    0.0
+    2 |    0.5
+    3 |    1.0
+    4 |    1.5
+    5 |    2.5
+    6 |    3.5
+
+-}
+
+{- $zipping 
+
+An important class of operations are combining two 'Series' together, also known as *zipping*. 
+For lists, Haskell has 'Data.List.zipWith'. 'Series' also have 'Series.zipWith' and variants:
+
+* 'Series.zipWith', which combines two series with some elementwise function;
+* 'Series.zipWithMatched', which combines two series with some elementwise function 
+  on keys which are in *both* maps;
+* 'Series.zipWithStrategy', which combines two series with some elementwise 
+  function and supports custom operations to deal with missing keys;
+
+To illustrate the differences between the various zipping functions, 
+consider the following two series. There's population:
+
+>>> :set -XNumericUnderscores
+>>> import Data.Fixed (Centi)
+>>> :{ 
+    -- Most recent population estimate rounded to the nearest million
+    let population = Series.fromList [ ("Canada"::String, 40_000_000::Centi)
+                                     , ("Kenya"         , 56_000_000)
+                                     , ("Poland"        , 38_000_000)
+                                     , ("Singapore"     ,  6_000_000)
+                                     ]
+    :}
+
+and there's total land mass:
+
+>>> :{ 
+    -- Land mass in square kilometer
+    let landmass = Series.fromList [ ("Brazil"::String, 8_520_000::Centi)
+                                   , ("Canada",         9_990_000)
+                                   , ("Kenya",            580_000)
+                                   , ("Poland",           313_000)
+                                   ] 
+    :}
+
+@'Series.zipWith' f left right@ combines the series @left@ and @right@ using the 
+function @f@ which admits two arguments, for all keys one-by-one. If a key 
+is missing from either @left@ or @right@, 'Series.zipWith' returns 'Nothing'. For example, 
+the population density per country would be:
+
+>>> Series.zipWith (/) population landmass
+      index |      values
+      ----- |      ------
+   "Brazil" |     Nothing
+   "Canada" |   Just 4.00
+    "Kenya" |  Just 96.55
+   "Poland" | Just 121.40
+"Singapore" |     Nothing
+
+Since we don't have population estimates for Brazil and no land mass 
+information for Singapore, we can't calculate their population densities.
+
+Sometimes, we only care about the results of @'Series.zipWith' f@ where keys are 
+in both series. In this case, we can use 'Series.zipWithMatched':
+
+>>> Series.zipWithMatched (/) population landmass
+   index | values
+   ----- | ------
+"Canada" |   4.00
+ "Kenya" |  96.55
+"Poland" | 121.40
+
+Finally, in case we want full control over what to do when a key is missing, 
+we can use @Series.zipWithStrategy'. For example, consider the case where:
+
+* If population numbers are missing, I want to set the density to 0;
+* If land mass information is missing, I wait to skip calculating the density of this country. 
+
+>>> import Data.Series (skipStrategy, constStrategy)
+>>> let noPopulationStrategy = Series.constStrategy 0
+>>> let noLandmassStrategy   = Series.skipStrategy
+>>> Series.zipWithStrategy (/) noPopulationStrategy noLandmassStrategy population landmass
+      index | values
+      ----- | ------
+   "Canada" |   4.00
+    "Kenya" |  96.55
+   "Poland" | 121.40
+"Singapore" |   0.00
+
+As you can imagine, 'Series.zipWithStrategy' is the most general and gives the most control, but is less easy 
+to use than 'Series.zipWith' and 'Series.zipWithMatched'.
+
+-}
+
+{- $conclusion
+
+This section concludes the introductory tutorial to the @javelin@ package and its "Data.Series" module.
+
+For a more in-depth look at this package, you can read the full documentation for each module:
+
+* "Data.Series"
+* "Data.Series.Index"
+* "Data.Series.Unboxed"
+* "Data.Series.Generic"
+
+-}
+
+{- $duplicates
+
+If you must build a 'Series' with duplicate keys, you can use the 'Data.Series.fromListDuplicates' or 
+'Data.Series.fromVectorDuplicates' functions. 
+In the example below, the key @\'d\'@ is repeated three times:
+
+>>> Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+  index | values
+  ----- | ------
+('a',0) |      5
+('b',0) |      0
+('d',0) |      1
+('d',1) |     -4
+('d',2) |      7
+
+Note that the 'Series' produced by 'Data.Series.fromListDuplicates' still has unique keys, but each key is a 
+composite of a character and an occurrence. This is reflected in the type:
+
+>>> :t Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+  :: Series (Char, Occurrence) Int
+
+Here, 'Data.Series.Occurrence' is a non-negative number, and can be converted to 
+other integer-like numbers using 'fromIntegral'. In practice, you should aim to aggregate your 'Series' to remove duplicate keys, for example
+using 'Data.Series.groupBy' and grouping on the first element of the key ('fst'):
+
+>>> let xs = Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+>>> xs `groupBy` fst `aggregateWith` sum
+index | values
+----- | ------
+  'a' |      5
+  'b' |      0
+  'd' |      4
+
+-}
+
+{- $unboxed 
+
+The 'Data.Series.Series' defined in "Data.Series" are based on 'Data.Vector.Vector' from "Data.Vector". 
+This implementation is nice because such 'Series' can hold _any_ Haskell type. However, because
+Haskell types can be arbitrarily complex, numerical operations on 'Series' may not be as fast
+as could be.
+
+For simpler types such as 'Double' and 'Int', a different kind of series can be used to
+speed up numerical calculations: 'Data.Series.Unboxed.Series' from the "Data.Series.Unboxed" module.
+Such 'Data.Series.Unboxed.Series' are much more limited: they can only contain datatypes which are
+instances of 'Data.Vector.Unboxed.Unbox'. 
+
+This then brings the question: how can you write software which supports both ordinary 'Data.Series.Series'
+__and__ unboxed 'Data.Series.Unboxed.Series'? The answer is to use functions from the "Data.Series.Generic".
+
+For example, we could implement the dot product of two series as:
+
+>>> import qualified Data.Series.Generic as G
+>>> import Data.Vector.Generic ( Vector )
+>>> :{
+      dot :: (Ord k, Num a, Vector v a) => G.Series v k a -> G.Series v k a -> a
+      dot v1 v2 = G.sum $ G.zipWithMatched (*) v1 v2
+    :}
+
+You can convert between the two types of series using the 'Data.Series.Generic.convert' function.
+
+-}
+
+{- $replacement 
+
+'Series.map' allows to map every value of a series. How about replacing *some* 
+values in a series? The function 'Data.Series.replace' (and its infix variant, '|->') replaces values in the right operand 
+which have an analogue in the left operand:
+
+>>> import Data.Series ( (|->) )
+>>> let nan = (0/0) :: Double
+>>> let right = Series.fromList [('a', 1), ('b', nan), ('c', 3), ('d', nan)]
+>>> right
+index | values
+----- | ------
+  'a' |    1.0
+  'b' |    NaN
+  'c' |    3.0
+  'd' |    NaN
+>>> let left = Series.fromList [('b', 0::Double), ('d', 0), ('e', 0)]
+>>> left
+index | values
+----- | ------
+  'b' |    0.0
+  'd' |    0.0
+  'e' |    0.0
+>>> left |-> right
+index | values
+----- | ------
+  'a' |    1.0
+  'b' |    0.0
+  'c' |    3.0
+  'd' |    0.0
+
+In the example above, the key @\'e\'@ is ignored since it was not in the @right@ 
+series to begin with.
+
+The flipped version, '<-|', is also available.
+
+-}
+
+{- $comparison 
+
+Below is a table showing which operations on "Data.Series" have analogues for 
+other data structures.
+
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Action                          | "Data.Series"                  | "Data.Map.Strict"               | "Data.List"       | "Data.Vector"        |
++=================================+================================+=================================+===================+======================+
+| Mapping values                  | 'Data.Series.map'              | 'Data.Map.Strict.map'           | 'map'             | 'Data.Vector.map'    |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Mapping index                   | 'Data.Series.mapIndex'         | 'Data.Map.Strict.mapKeys'       |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Mapping values with key         | 'Data.Series.mapWithKey'       | 'Data.Map.Strict.mapWithKey'    |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Filtering values                | 'Data.Series.filter'           | 'Data.Map.Strict.filter'        | 'filter'          | 'Data.Vector.filter' |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Filtering index                 | 'Data.Series.select',          | 'Data.Map.Strict.filterWithKey' |                   |                      |
+|                                 | 'Data.Series.filterWithKey'    |                                 |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Indexing by key                 | 'Data.Series.at'               | 'Data.Map.Strict.lookup'        |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Indexing by position            | 'Data.Series.iat'              |                                 | 'Data.List.!'     | 'Data.Vector.!'      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Combine two structures key-wise | 'Data.Series.zipWith'          | 'Data.Map.Merge.Strict.merge'   |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Union                           | 'Data.Series.<>'               | 'Data.Map.Strict.union'         | 'Data.List.union' |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+| Group keys                      | 'Data.Series.groupBy'          |                                 |                   |                      |
++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+
+
+-}
diff --git a/src/Data/Series/Unboxed.hs b/src/Data/Series/Unboxed.hs
--- a/src/Data/Series/Unboxed.hs
+++ b/src/Data/Series/Unboxed.hs
@@ -1,1291 +1,1291 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Series.Unboxed
--- Copyright   :  (c) Laurent P. René de Cotret
--- License     :  MIT
--- Maintainer  :  laurent.decotret@outlook.com
--- Portability :  portable
---
--- This module contains data structures and functions to work with 'Series' capable of holding unboxed values,
--- i.e. values of types which are instances of `Unbox`.
---
--- = Why use unboxed series?
---
--- Unboxed series can have much better performance, at the cost of less flexibility. For example,
--- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of 
--- `Functor` or `Foldable`.
---
--- If you are hesitating, you should prefer the series implementation in the "Data.Series" module.
---
--- = Introduction to series
---
--- A 'Series' of type @Series k a@ is a labeled array of values of type @a@,
--- indexed by keys of type @k@.
---
--- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:
---
---      * random access by key ( \(O(\log n)\) );
---      * slice by key ( \(O(\log n)\) ).
---
--- Like `Data.Vector.Vector`, they support efficient:
---
---      * random access by index ( \(O(1)\) );
---      * slice by index ( \(O(1)\) );
---      * numerical operations.
---
--- This module re-exports most of the content of "Data.Series.Generic", with type signatures 
--- specialized to the unboxed vector type `Data.Vector.Unboxed.Vector`.
- 
-module Data.Series.Unboxed (
-    Series, index, values,
-
-    -- * Building/converting 'Series'
-    singleton, fromIndex,
-    -- ** Lists
-    fromList, toList,
-    -- ** Vectors
-    fromVector, toVector,
-    -- ** Handling duplicates
-    Occurrence, fromListDuplicates, fromVectorDuplicates,
-    -- ** Strict Maps
-    fromStrictMap, toStrictMap,
-    -- ** Lazy Maps
-    fromLazyMap, toLazyMap,
-    -- ** Ad-hoc conversion with other data structures
-    IsSeries(..),
-    -- ** Conversion between 'Series' types
-    G.convert,
-
-    -- * Mapping and filtering
-    map, mapWithKey, mapIndex, concatMap,
-    take, takeWhile, drop, dropWhile, filter, filterWithKey,
-    -- ** Mapping with effects
-    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,
-
-    -- * Combining series
-    zipWithMatched, zipWithKey,
-    zipWithMatched3, zipWithKey3,
-    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
-    zipWithMonoid, esum, eproduct, unzip, unzip3,
-
-    -- * Index manipulation
-    require, dropIndex,
-
-    -- * Accessors
-    -- ** Bulk access
-    select, selectWhere, Range, to, from, upto, Selection, 
-    -- ** Single-element access
-    at, iat,
-
-    -- * Replacement
-    replace, (|->), (<-|),
-
-    -- * Grouping and windowing operations
-    groupBy, Grouping, aggregateWith, foldWith, 
-    windowing, expanding,
-
-    -- * Folds
-    -- ** General folds
-    fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMap', foldMapWithKey,
-    -- ** Specialized folds
-    G.mean, G.variance, G.std,
-    null, length, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
-    argmin, argmax,
-
-    -- * Scans
-    postscanl, prescanl,
-
-    -- * Displaying 'Series'
-    display, displayWith,
-    noLongerThan,
-    DisplayOptions(..), G.defaultDisplayOptions
-) where
-
-import           Control.Foldl       ( Fold, FoldM )
-import qualified Data.Map.Lazy       as ML
-import qualified Data.Map.Strict     as MS
-import           Data.Series.Index   ( Index )
-import           Data.Series.Generic.View 
-                                     ( Range, Selection, to, from, upto )
-import           Data.Series.Generic ( IsSeries(..), ZipStrategy, Occurrence, DisplayOptions(..), skipStrategy, mapStrategy, constStrategy
-                                     , noLongerThan 
-                                     )
-import qualified Data.Series.Generic as G
-import           Data.Vector.Unboxed ( Vector, Unbox )
-import qualified Data.Vector.Unboxed as Vector
-
-import           Prelude             hiding ( map, concatMap, zipWith, filter, foldMap, null, length, all, any, and, or
-                                            , sum, product, maximum, minimum, take, takeWhile, drop, dropWhile
-                                            , last, unzip, unzip3
-                                            )
-
--- $setup
--- >>> import qualified Data.Series.Unboxed as Series
--- >>> import qualified Data.Series.Index as Index
-
-infixl 1 `select` 
-infix 6 |->, <-|
-
--- | A series is a labeled array of values of type @a@,
--- indexed by keys of type @k@.
---
--- Like @Data.Map@ and @Data.HashMap@, they support efficient:
---
---      * random access by key ( \(O(\log n)\) );
---      * slice by key ( \(O(\log n)\) ).
---
--- Like @Data.Vector.Vector@, they support efficient:
---
---      * random access by index ( \(O(1)\) );
---      * slice by index ( \(O(1)\) );
---      * numerical operations.
-type Series = G.Series Vector
-
-
-index :: Series k a -> Index k
-{-# INLINABLE index #-}
-index = G.index
-
-
-values :: Series k a -> Vector a
-{-# INLINABLE values #-}
-values = G.values
-
-
--- | Create a 'Series' with a single element.
-singleton :: Unbox a => k -> a -> Series k a
-{-# INLINABLE singleton #-}
-singleton = G.singleton
-
-
--- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
---
--- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']
--- index | values
--- ----- | ------
---   'a' |      0
---   'b' |      0
---   'c' |      0
---   'd' |      0
-fromIndex :: Unbox a
-          => (k -> a) -> Index k -> Series k a
-{-# INLINABLE fromIndex #-}
-fromIndex = G.fromIndex
-
-
--- | Construct a series from a list of key-value pairs. There is no
--- condition on the order of pairs.
---
--- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]
--- >>> xs
--- index | values
--- ----- | ------
---   'a' |      5
---   'b' |      0
---   'd' |      1
---
--- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
-fromList :: (Ord k, Unbox a) => [(k, a)] -> Series k a
-{-# INLINABLE fromList #-}
-fromList = G.fromList
-
-
--- | Construct a series from a list of key-value pairs.
--- Contrary to `fromList`, values at duplicate keys are preserved. To keep each
--- key unique, an `Occurrence` number counts up.
---
--- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
--- >>> xs
---   index | values
---   ----- | ------
--- ('a',0) |      5
--- ('b',0) |      0
--- ('d',0) |      1
--- ('d',1) |     -4
--- ('d',2) |      7
-fromListDuplicates :: (Ord k, Unbox a) => [(k, a)] -> Series (k, Occurrence) a
-{-# INLINABLE fromListDuplicates #-}
-fromListDuplicates = G.fromListDuplicates
-
-
--- | Construct a list from key-value pairs. The elements are in order sorted by key:
---
--- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]
--- >>> xs
--- index | values
--- ----- | ------
---   'a' |      5
---   'b' |      0
---   'd' |      1
--- >>> toList xs
--- [('a',5),('b',0),('d',1)]
-toList :: Unbox a => Series k a -> [(k, a)]
-{-# INLINABLE toList #-}
-toList = G.toList
-
-
--- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
-toVector :: (Unbox a, Unbox k) => Series k a -> Vector (k, a)
-{-# INLINABLE toVector #-}
-toVector = G.toVector
-
-
--- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
--- condition on the order of pairs. Duplicate keys are silently dropped. If you
--- need to handle duplicate keys, see 'fromVectorDuplicates'.
---
--- Note that due to differences in sorting,
--- @Series.fromList@ and @Series.fromVector . Vector.fromList@ 
--- may not be equivalent if the input list contains duplicate keys.
-fromVector :: (Ord k, Unbox k, Unbox a)
-           => Vector (k, a) -> Series k a
-{-# INLINABLE fromVector #-}
-fromVector = G.fromVector
-
-
--- | Construct a series from a 'Vector' of key-value pairs.
--- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each
--- key unique, an 'Occurrence' number counts up.
---
--- >>> import qualified Data.Vector.Unboxed as Unboxed
--- >>> let xs = fromVectorDuplicates $ Unboxed.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
--- >>> xs
---   index | values
---   ----- | ------
--- ('a',0) |      5
--- ('b',0) |      0
--- ('d',0) |      1
--- ('d',1) |     -4
--- ('d',2) |      7
-fromVectorDuplicates :: (Unbox k, Unbox a, Ord k) => Vector (k, a) -> Series (k, Occurrence) a
-{-# INLINABLE fromVectorDuplicates #-}
-fromVectorDuplicates = G.fromVectorDuplicates
-
-
--- | Convert a series into a lazy @Map@.
-toLazyMap :: (Unbox a) => Series k a -> ML.Map k a
-{-# INLINABLE toLazyMap #-}
-toLazyMap = G.toLazyMap
-
-
--- | Construct a series from a lazy @Map@.
-fromLazyMap :: (Unbox a) => ML.Map k a -> Series k a
-{-# INLINABLE fromLazyMap #-}
-fromLazyMap = G.fromLazyMap
-
-
--- | Convert a series into a strict @Map@.
-toStrictMap :: (Unbox a) => Series k a -> MS.Map k a
-{-# INLINABLE toStrictMap #-}
-toStrictMap = G.toStrictMap
-
--- | Construct a series from a strict @Map@.
-fromStrictMap :: (Unbox a) => MS.Map k a -> Series k a
-{-# INLINABLE fromStrictMap #-}
-fromStrictMap = G.fromStrictMap
-
-
--- | \(O(n)\) Map every element of a 'Series'.
-map :: (Unbox a, Unbox b) => (a -> b) -> Series k a -> Series k b
-{-# INLINABLE map #-}
-map = G.map
-
-
--- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
-mapWithKey :: (Unbox a, Unbox b) => (k -> a -> b) -> Series k a -> Series k b
-{-# INLINABLE mapWithKey #-}
-mapWithKey = G.mapWithKey
-
-
--- | \(O(n \log n)\).
--- Map each key in the index to another value. Note that the resulting series
--- may have less elements, because each key must be unique.
---
--- In case new keys are conflicting, the first element is kept.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> import qualified Data.List
--- >>> xs `mapIndex` (Data.List.take 1)
--- index | values
--- ----- | ------
---   "L" |      4
---   "P" |      1
-mapIndex :: (Unbox a, Ord k, Ord g) => Series k a -> (k -> g) -> Series g a
-{-# INLINABLE mapIndex #-}
-mapIndex = G.mapIndex
-
-
--- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
-concatMap :: (Unbox a, Unbox k, Unbox b, Ord k) 
-          => (a -> Series k b) 
-          -> Series k a 
-          -> Series k b
-{-# INLINABLE concatMap #-}
-concatMap = G.concatMap
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, yielding a series of results.
-mapWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)
-{-# INLINABLE mapWithKeyM #-}
-mapWithKeyM = G.mapWithKeyM
-
-
--- | \(O(n)\) Apply the monadic action to every element of a series and its
--- index, discarding the results.
-mapWithKeyM_ :: (Unbox a, Monad m) => (k -> a -> m b) -> Series k a -> m ()
-{-# INLINABLE mapWithKeyM_ #-}
-mapWithKeyM_ = G.mapWithKeyM_
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- yielding a series of results.
-forWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)
-{-# INLINABLE forWithKeyM #-}
-forWithKeyM = G.forWithKeyM
-
-
--- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
--- discarding the results.
-forWithKeyM_ :: (Unbox a, Monad m) => Series k a -> (k -> a -> m b) -> m ()
-{-# INLINABLE forWithKeyM_ #-}
-forWithKeyM_ = G.forWithKeyM_
-
-
--- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
--- >>> take 2 xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-take :: Unbox a => Int -> Series k a -> Series k a
-{-# INLINABLE take #-}
-take = G.take
-
-
--- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
-
--- >>> takeWhile (>1) xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-takeWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a
-takeWhile = G.takeWhile
-
-
--- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
--- >>> drop 2 xs
---    index | values
---    ----- | ------
---  "Paris" |      1
--- "Vienna" |      5
-drop :: Unbox a => Int -> Series k a -> Series k a
-{-# INLINABLE drop #-}
-drop = G.drop
-
-
--- | \(O(n)\) Returns the complement of `takeWhile`.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- "Vienna" |      5
-
--- >>> dropWhile (>1) xs
---    index | values
---    ----- | ------
---  "Paris" |      1
--- "Vienna" |      5
-dropWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a
-dropWhile = G.dropWhile
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
---
--- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),  ('g', 2) ]
--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
--- >>> zipWithMatched (+) xs ys
--- index | values
--- ----- | ------
---   'a' |     10
---   'b' |     12
-zipWithMatched :: (Unbox a, Unbox b, Unbox c, Ord k) 
-               => (a -> b -> c) -> Series k a -> Series k b -> Series k c
-{-# INLINABLE zipWithMatched #-}
-zipWithMatched = G.zipWithMatched
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys not present in all three series are dropped.
---
--- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),   ('g', 2) ]
--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11),  ('d', 13) ]
--- >>> let zs = Series.fromList [ ('a', 20::Int), ('d', 13), ('e', 6) ]
--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
--- index | values
--- ----- | ------
---   'a' |     30
-zipWithMatched3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Ord k) 
-                => (a -> b -> c -> d) 
-                -> Series k a 
-                -> Series k b 
-                -> Series k c
-                -> Series k d
-{-# INLINABLE zipWithMatched3 #-}
-zipWithMatched3 = G.zipWithMatched3
-
-
--- | Apply a function elementwise to two series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
--- 
---
--- >>> import Data.Char ( ord )
--- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('c', 2) ]
--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
--- >>> zipWithKey (\k x y -> ord k + x + y) xs ys
--- index | values
--- ----- | ------
---   'a' |    107
---   'b' |    110
-zipWithKey :: (Unbox a, Unbox b, Unbox c, Unbox k, Ord k)  
-           => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c
-{-# INLINABLE zipWithKey #-}
-zipWithKey = G.zipWithKey
-
-
--- | Apply a function elementwise to three series, matching elements
--- based on their keys. Keys present only in the left or right series are dropped.
--- 
--- >>> import Data.Char ( ord )
--- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]
--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
--- >>> let zs = Series.fromList [ ('a', 20::Int), ('b', 7), ('d', 5) ]
--- >>> zipWithKey3 (\k x y z -> ord k + x + y + z) xs ys zs
--- index | values
--- ----- | ------
---   'a' |    127
---   'b' |    117
-zipWithKey3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Unbox k, Ord k) 
-            => (k -> a -> b -> c -> d) 
-            -> Series k a 
-            -> Series k b 
-            -> Series k c
-            -> Series k d
-{-# INLINABLE zipWithKey3 #-}
-zipWithKey3 = G.zipWithKey3
-
-
--- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.
---
--- In the example below, we want to set the value to @-100@ (via @'constStrategy' (-100)@) for keys which are only present 
--- in the left 'Series', and drop keys (via 'skipStrategy') which are only present in the `right 'Series'  
---
--- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),  ('g', 2) ]
--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
--- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy  xs ys
--- index | values
--- ----- | ------
---   'a' |     10
---   'b' |     12
---   'g' |   -100
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ 
--- than using @'zipWithStrategy' f 'skipStrategy' 'skipStrategy'@.
-zipWithStrategy :: (Ord k, Unbox a, Unbox b, Unbox c) 
-                => (a -> b -> c)     -- ^ Function to combine values when present in both series
-                -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
-                -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
-                -> Series k a
-                -> Series k b 
-                -> Series k c
-{-# INLINABLE zipWithStrategy #-}
-zipWithStrategy = G.zipWithStrategy
-
-
--- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
--- present in one of the 'Series' but not all of the others.
---
--- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
--- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
-zipWithStrategy3 :: (Ord k, Unbox a, Unbox b, Unbox c, Unbox d) 
-                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
-                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
-                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
-                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
-                -> Series k a
-                -> Series k b 
-                -> Series k c
-                -> Series k d
-zipWithStrategy3 = G.zipWithStrategy3
-{-# INLINABLE zipWithStrategy3 #-}
-
-
--- | Zip two 'Series' with a combining function. The value for keys which are missing from
--- either 'Series' is replaced with the appropriate `mempty` value.
---
--- >>> import Data.Monoid ( Sum(..) )
--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
--- >>> zipWithMonoid (<>) xs ys
---        index |           values
---        ----- |           ------
--- "2023-01-01" | Sum {getSum = 6}
--- "2023-01-02" | Sum {getSum = 2}
--- "2023-01-03" | Sum {getSum = 7}
-zipWithMonoid :: ( Monoid a, Monoid b
-                 , Unbox a, Unbox b, Unbox c
-                 , Ord k
-                 ) 
-              => (a -> b -> c)
-              -> Series k a
-              -> Series k b 
-              -> Series k c
-zipWithMonoid = G.zipWithMonoid
-{-# INLINABLE zipWithMonoid #-}
-
-
--- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `esum` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |      6
--- "2023-01-02" |      2
--- "2023-01-03" |      7
-esum :: (Ord k, Num a, Unbox a) 
-     => Series k a 
-     -> Series k a
-     -> Series k a
-esum = G.esum
-{-# INLINABLE esum #-}
-
-
--- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
---
--- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
--- >>> xs `eproduct` ys
---        index | values
---        ----- | ------
--- "2023-01-01" |     10
--- "2023-01-02" |      3
--- "2023-01-03" |      7
-eproduct :: (Ord k, Num a, Unbox a) 
-         => Series k a 
-         -> Series k a
-         -> Series k a
-eproduct = G.eproduct
-{-# INLINABLE eproduct #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 2-tuples.
-unzip :: (Unbox a, Unbox b) 
-      => Series k (a, b)
-      -> ( Series k a
-         , Series k b
-         )
-unzip = G.unzip
-{-# INLINABLE unzip #-}
-
-
--- | \(O(n)\) Unzip a 'Series' of 3-tuples.
-unzip3 :: (Unbox a, Unbox b, Unbox c) 
-       => Series k (a, b, c)
-       -> ( Series k a
-          , Series k b
-          , Series k c
-          )
-unzip3 = G.unzip3
-{-# INLINABLE unzip3 #-}
-
-
--- | Require a series to have a specific `Index`. 
--- Contrary to @select@, all keys in the `Index` will be present in the resulting series.
---
--- Note that unlike the implementation for boxed series (`Data.Series.require`), missing keys need to be mapped to some values because unboxed
--- series cannot contain values of type @`Maybe` a@. 
---
--- In the example below, the missing value for key @\"Taipei\"@ is mapped to 0:
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> require (const 0) xs (Index.fromList ["Paris", "Lisbon", "Taipei"])
---    index | values
---    ----- | ------
--- "Lisbon" |      4
---  "Paris" |      1
--- "Taipei" |      0
-require :: (Unbox a, Ord k) 
-        => (k -> a) -> Series k a -> Index k -> Series k a
-{-# INLINABLE require #-}
-require f = G.requireWith f id
-
-
--- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will
--- be indexed from 0.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> dropIndex xs
--- index | values
--- ----- | ------
---     0 |      4
---     1 |      2
---     2 |      1
-dropIndex :: Series k a -> Series Int a
-{-# INLINABLE dropIndex #-}
-dropIndex = G.dropIndex
-
-
--- | Filter elements. Only elements for which the predicate is @True@ are kept. 
--- Notice that the filtering is done on the values, not on the keys.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> filter (>2) xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
---
--- See also 'filterWithKey'.
-filter :: (Unbox a, Ord k) => (a -> Bool) -> Series k a -> Series k a
-{-# INLINABLE filter #-}
-filter = G.filter
-
-
--- | Filter elements, taking into account the corresponding key. Only elements for which 
--- the predicate is @True@ are kept. 
-filterWithKey :: (Unbox a, Ord k) 
-              => (k -> a -> Bool) 
-              -> Series k a 
-              -> Series k a
-{-# INLINABLE filterWithKey #-}
-filterWithKey = G.filterWithKey
-
-
--- | Select a subseries. There are a few ways to do this.
---
--- The first way to do this is to select a sub-series based on random keys. For example,
--- selecting a subseries from an `Index`:
---
--- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
--- >>> xs `select` Index.fromList ['a', 'd']
--- index | values
--- ----- | ------
---   'a' |     10
---   'd' |     40
---
--- The second way to select a sub-series is to select all keys in a range:
---
--- >>> xs `select` 'b' `to` 'c'
--- index | values
--- ----- | ------
---   'b' |     20
---   'c' |     30
---
--- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not
--- in the series, it'll be ignored:
---
--- >>> xs `select` Index.fromList ['a', 'd', 'e']
--- index | values
--- ----- | ------
---   'a' |     10
---   'd' |     40
---
--- See `require` if you want to ensure that all keys are present.
-select :: (Unbox a, Selection s, Ord k) => Series k a -> s k -> Series k a
-select = G.select
-
-
--- | Select a sub-series from a series matching a condition.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> xs `selectWhere` (Series.map (>1) xs)
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
-selectWhere :: (Unbox a, Ord k) => Series k a -> Series k Bool -> Series k a
-{-# INLINABLE selectWhere #-}
-selectWhere = G.selectWhere
-
-
--- | \(O(\log n)\). Extract a single value from a series, by key.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs `at` "Paris"
--- Just 1
--- >>> xs `at` "Sydney"
--- Nothing
-at :: (Unbox a, Ord k) => Series k a -> k -> Maybe a
-{-# INLINABLE at #-}
-at = G.at
-
-
--- | \(O(1)\). Extract a single value from a series, by index.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> xs `iat` 0
--- Just 4
--- >>> xs `iat` 3
--- Nothing
-iat :: Unbox a => Series k a -> Int -> Maybe a
-{-# INLINABLE iat #-}
-iat = G.iat
-
-
--- | Replace values in the right series from values in the left series at matching keys.
--- Keys not in the right series are unaffected.
--- 
--- See `(|->)` and `(<-|)`, which might be more readable.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> ys `replace` xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-replace :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
-{-# INLINABLE replace #-}
-replace = G.replace
-
-
--- | Replace values in the right series from values in the left series at matching keys.
--- Keys not in the right series are unaffected.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> ys |-> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-(|->) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
-{-# INLINABLE (|->) #-}
-(|->) = (G.|->)
-
-
--- | Replace values in the left series from values in the right series at matching keys.
--- Keys not in the left series are unaffected.
---
--- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
--- >>> xs
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |      1
--- >>> let ys = Series.singleton "Paris" (99::Int)
--- >>> xs <-| ys
---    index | values
---    ----- | ------
--- "Lisbon" |      4
--- "London" |      2
---  "Paris" |     99
-(<-|) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
-{-# INLINABLE (<-|) #-}
-(<-|) = (G.<-|)
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double
--- >>> xs
--- index | values
--- ----- | ------
---     0 |    1.0
---     1 |    2.0
---     2 |    3.0
---     3 |    4.0
--- >>> import Control.Foldl (variance)
--- >>> fold variance xs
--- 1.25
---
--- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
--- account while folding.
-fold :: Unbox a 
-     => Fold a b -> Series k a -> b
-fold = G.fold
-{-# INLINABLE fold #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
---
--- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
--- account while folding.
-foldM :: (Monad m, Unbox a) 
-      => FoldM m a b  
-      -> Series k a 
-      -> m b
-foldM = G.foldM
-{-# INLINABLE foldM #-}
-
-
--- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.
-foldWithKey :: (Unbox k, Unbox a) 
-            => Fold (k, a) b -> Series k a -> b
-foldWithKey = G.foldWithKey
-{-# INLINABLE foldWithKey #-}
-
-
--- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
-foldMWithKey :: (Monad m, Unbox a, Unbox k) 
-             => FoldM m (k, a) b  
-             -> Series k a 
-             -> m b
-foldMWithKey = G.foldMWithKey
-{-# INLINABLE foldMWithKey #-}
-
-
--- | \(O(n)\) Map each element of the structure to a monoid and combine
--- the results.
-foldMap :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m
-{-# INLINABLE foldMap #-}
-foldMap = G.foldMap
-
-
--- | \(O(n)\) Like 'foldMap', but strict in the accumulator. It uses the same
--- implementation as the corresponding method of the 'Foldable' type class.
-foldMap' :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m
-{-# INLINABLE foldMap' #-}
-foldMap' f = Vector.foldMap' f . values
-
-
--- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine
--- the results.
-foldMapWithKey :: (Monoid m, Unbox a, Unbox k) => (k -> a -> m) -> Series k a -> m
-{-# INLINABLE foldMapWithKey #-}
-foldMapWithKey = G.foldMapWithKey
-
-
--- | Group values in a 'Series' by some grouping function (@k -> g@).
--- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
---
--- This function is expected to be used in conjunction with @aggregate@:
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
-groupBy :: Series k a      -- ^ Grouping function
-        -> (k -> g)        -- ^ Input series
-        -> Grouping k g a  -- ^ Grouped series
-{-# INLINABLE groupBy #-}
-groupBy = G.groupBy
-
-
--- | Representation of a 'Series' being grouped.
-type Grouping k g a = G.Grouping k g Vector a
-
-
--- | Aggregate groups resulting from a call to 'groupBy':
--- 
--- >>> import Data.Maybe ( fromMaybe )
--- >>> type Date = (Int, String)
--- >>> month :: (Date -> String) = snd
--- >>> :{ 
---     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
---                              , ((2021, "January"), -5)
---                              , ((2020, "June")   , 20)
---                              , ((2021, "June")   , 25) 
---                              ]
---      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
--- :}
---     index | values
---     ----- | ------
--- "January" |     -5
---    "June" |     20
---
--- If you want to aggregate groups using a binary function, see 'foldWith' which
--- may be much faster.
-aggregateWith :: (Ord g, Unbox a, Unbox b) 
-              => Grouping k g a 
-              -> (Series k a -> b) 
-              -> Series g b
-{-# INLINABLE aggregateWith #-}
-aggregateWith = G.aggregateWith
-
-
--- | Aggregate each group in a 'Grouping' using a binary function.
--- While this is not as expressive as 'aggregateWith', users looking for maximum
--- performance should use 'foldWith' as much as possible.
-foldWith :: (Ord g, Unbox a) 
-         => Grouping k g a
-         -> (a -> a -> a)
-         -> Series g a
-{-# INLINABLE foldWith #-}
-foldWith = G.foldWith
-
-
--- | Expanding window aggregation.
---
--- >>> :{ 
---     let (xs :: Series Int Int) 
---          = fromList [ (1, 0)
---                     , (2, 1)
---                     , (3, 2)
---                     , (4, 3)
---                     , (5, 4)
---                     , (6, 5)
---                     ]
---     in (xs `expanding` sum) :: Series Int Int 
--- :}
--- index | values
--- ----- | ------
---     1 |      0
---     2 |      1
---     3 |      3
---     4 |      6
---     5 |     10
---     6 |     15
-expanding :: (Unbox a, Unbox b) 
-          => Series k a        -- ^ Series vector
-          -> (Series k a -> b) -- ^ Aggregation function
-          -> Series k b        -- ^ Resulting vector
-{-# INLINABLE expanding #-}
-expanding = G.expanding
-
-
--- | General-purpose window aggregation.
---
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 3)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in windowing (\k -> k `to` (k+2)) sum xs
--- :}
--- index | values
--- ----- | ------
---     1 |      3
---     2 |      6
---     3 |      9
---     4 |     12
---     5 |      9
---     6 |      5
-windowing :: (Ord k, Unbox a, Unbox b)
-          => (k -> Range k)
-          -> (Series k a -> b)
-          -> Series k a
-          -> Series k b
-{-# INLINABLE windowing #-}
-windowing = G.windowing 
-
-
--- | \(O(1)\) Test whether a 'Series' is empty.
-null :: Unbox a => Series k a -> Bool
-{-# INLINABLE null #-}
-null = G.null
-
-
--- |\(O(1)\) Extract the length of a 'Series'.
-length :: Unbox a => Series k a -> Int
-{-# INLINABLE length #-}
-length = G.length
-
-
--- | \(O(n)\) Check if all elements satisfy the predicate.
-all :: Unbox a => (a -> Bool) -> Series k a -> Bool
-{-# INLINABLE all #-}
-all = G.all
-
-
--- | \(O(n)\) Check if any element satisfies the predicate.
-any :: Unbox a => (a -> Bool) -> Series k a -> Bool
-{-# INLINABLE any #-}
-any = G.any
-
-
--- | \(O(n)\) Check if all elements are 'True'.
-and :: Series k Bool -> Bool
-{-# INLINABLE and #-}
-and = G.and
-
-
--- | \(O(n)\) Check if any element is 'True'.
-or :: Series k Bool -> Bool
-{-# INLINABLE or #-}
-or = G.or
-
-
--- | \(O(n)\) Compute the sum of the elements.
-sum :: (Unbox a, Num a) => Series k a -> a
-{-# INLINABLE sum #-}
-sum = G.sum
-
-
--- | \(O(n)\) Compute the product of the elements.
-product :: (Unbox a, Num a) => Series k a -> a
-{-# INLINABLE product #-}
-product = G.product
-
-
--- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
---
--- See also 'argmax'.
-maximum :: (Ord a, Unbox a) => Series k a -> Maybe a
-{-# INLINABLE maximum #-}
-maximum = G.maximum
-
-
--- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
-maximumOn :: (Ord b, Unbox a) => (a -> b) -> Series k a -> Maybe a
-{-# INLINABLE maximumOn #-}
-maximumOn = G.maximumOn
-
-
--- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
--- If the 'Series' is empty, @Nothing@ is returned.
---
--- See also 'argmin'.
-minimum :: (Ord a, Unbox a) => Series k a -> Maybe a
-{-# INLINABLE minimum #-}
-minimum = G.minimum
-
-
--- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
--- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
-minimumOn :: (Ord b, Unbox a) => (a -> b) -> Series k a -> Maybe a
-{-# INLINABLE minimumOn #-}
-minimumOn = G.minimumOn
-
-
--- | \(O(n)\) Find the index of the maximum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the maximum element is returned.
---
--- >>> import qualified Data.Series.Unboxed as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 0)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 7)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmax xs 
--- :}
--- Just 4
-argmax :: (Ord a, Unbox a)
-       => Series k a
-       -> Maybe k
-argmax = G.argmax
-{-# INLINABLE argmax #-}
-
-
--- | \(O(n)\) Find the index of the minimum element in the input series.
--- If the input series is empty, 'Nothing' is returned.
---
--- The index of the first occurrence of the minimum element is returned.
--- >>> import qualified Data.Series.Unboxed as Series 
--- >>> :{ 
---     let (xs :: Series.Series Int Int) 
---          = Series.fromList [ (1, 1)
---                            , (2, 1)
---                            , (3, 2)
---                            , (4, 0)
---                            , (5, 4)
---                            , (6, 5)
---                            ]
---     in argmin xs 
--- :}
--- Just 4
-argmin :: (Ord a, Unbox a)
-       => Series k a
-       -> Maybe k
-argmin = G.argmin
-{-# INLINABLE argmin #-}
-
-
--- | \(O(n)\) Left-to-right postscan.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> postscanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      3
---     2 |      6
---     3 |     10
-postscanl :: (Unbox a, Unbox b) 
-          => (a -> b -> a) -> a -> Series k b -> Series k a
-{-# INLINABLE postscanl #-}
-postscanl = G.postscanl
-
-
--- | \(O(n)\) Left-to-right prescan.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
--- >>> xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---     3 |      4
--- >>> prescanl (+) 0 xs
--- index | values
--- ----- | ------
---     0 |      0
---     1 |      1
---     2 |      3
---     3 |      6
-prescanl :: (Unbox a, Unbox b) 
-         => (a -> b -> a) -> a -> Series k b -> Series k a
-{-# INLINABLE prescanl #-}
-prescanl = G.prescanl
-
-
--- | Display a 'Series' using default 'DisplayOptions'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
--- >>> putStrLn $ display xs
--- index | values
--- ----- | ------
---     0 |      1
---     1 |      2
---     2 |      3
---   ... |    ...
---     4 |      5
---     5 |      6
---     6 |      7
-display :: (Unbox a, Show k, Show a) 
-        => Series k a 
-        -> String
-display = G.display
-
-
--- | Display a 'Series' using customizable 'DisplayOptions'.
---
--- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
--- >>> import Data.List (replicate)
--- >>> :{
---     let opts = DisplayOptions { maximumNumberOfRows  = 4
---                               , indexHeader = "keys"
---                               , valuesHeader = "vals"
---                               , keyDisplayFunction   = (\i -> replicate i 'x') `noLongerThan` 5
---                               , valueDisplayFunction = (\i -> replicate i 'o') 
---                               }
---      in putStrLn $ displayWith opts xs
--- :}
---   keys |    vals
---  ----- |  ------
---        |       o
---      x |      oo
---    ... |     ...
---  xxxxx |  oooooo
--- xxx... | ooooooo
-displayWith :: (Unbox a) 
-            => DisplayOptions k a
-            -> Series k a 
-            -> String
-displayWith = G.displayWith
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Series.Unboxed
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains data structures and functions to work with 'Series' capable of holding unboxed values,
+-- i.e. values of types which are instances of `Unbox`.
+--
+-- = Why use unboxed series?
+--
+-- Unboxed series can have much better performance, at the cost of less flexibility. For example,
+-- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of 
+-- `Functor` or `Foldable`.
+--
+-- If you are hesitating, you should prefer the series implementation in the "Data.Series" module.
+--
+-- = Introduction to series
+--
+-- A 'Series' of type @Series k a@ is a labeled array of values of type @a@,
+-- indexed by keys of type @k@.
+--
+-- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:
+--
+--      * random access by key ( \(O(\log n)\) );
+--      * slice by key ( \(O(\log n)\) ).
+--
+-- Like `Data.Vector.Vector`, they support efficient:
+--
+--      * random access by index ( \(O(1)\) );
+--      * slice by index ( \(O(1)\) );
+--      * numerical operations.
+--
+-- This module re-exports most of the content of "Data.Series.Generic", with type signatures 
+-- specialized to the unboxed vector type `Data.Vector.Unboxed.Vector`.
+ 
+module Data.Series.Unboxed (
+    Series, index, values,
+
+    -- * Building/converting 'Series'
+    singleton, fromIndex,
+    -- ** Lists
+    fromList, toList,
+    -- ** Vectors
+    fromVector, toVector,
+    -- ** Handling duplicates
+    Occurrence, fromListDuplicates, fromVectorDuplicates,
+    -- ** Strict Maps
+    fromStrictMap, toStrictMap,
+    -- ** Lazy Maps
+    fromLazyMap, toLazyMap,
+    -- ** Ad-hoc conversion with other data structures
+    IsSeries(..),
+    -- ** Conversion between 'Series' types
+    G.convert,
+
+    -- * Mapping and filtering
+    map, mapWithKey, mapIndex, concatMap,
+    take, takeWhile, drop, dropWhile, filter, filterWithKey,
+    -- ** Mapping with effects
+    mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,
+
+    -- * Combining series
+    zipWithMatched, zipWithKey,
+    zipWithMatched3, zipWithKey3,
+    ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,
+    zipWithMonoid, esum, eproduct, unzip, unzip3,
+
+    -- * Index manipulation
+    require, dropIndex,
+
+    -- * Accessors
+    -- ** Bulk access
+    select, selectWhere, Range, to, from, upto, Selection, 
+    -- ** Single-element access
+    at, iat,
+
+    -- * Replacement
+    replace, (|->), (<-|),
+
+    -- * Grouping and windowing operations
+    groupBy, Grouping, aggregateWith, foldWith, 
+    windowing, expanding,
+
+    -- * Folds
+    -- ** General folds
+    fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMap', foldMapWithKey,
+    -- ** Specialized folds
+    G.mean, G.variance, G.std,
+    null, length, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
+    argmin, argmax,
+
+    -- * Scans
+    postscanl, prescanl,
+
+    -- * Displaying 'Series'
+    display, displayWith,
+    noLongerThan,
+    DisplayOptions(..), G.defaultDisplayOptions
+) where
+
+import           Control.Foldl       ( Fold, FoldM )
+import qualified Data.Map.Lazy       as ML
+import qualified Data.Map.Strict     as MS
+import           Data.Series.Index   ( Index )
+import           Data.Series.Generic.View 
+                                     ( Range, Selection, to, from, upto )
+import           Data.Series.Generic ( IsSeries(..), ZipStrategy, Occurrence, DisplayOptions(..), skipStrategy, mapStrategy, constStrategy
+                                     , noLongerThan 
+                                     )
+import qualified Data.Series.Generic as G
+import           Data.Vector.Unboxed ( Vector, Unbox )
+import qualified Data.Vector.Unboxed as Vector
+
+import           Prelude             hiding ( map, concatMap, zipWith, filter, foldMap, null, length, all, any, and, or
+                                            , sum, product, maximum, minimum, take, takeWhile, drop, dropWhile
+                                            , last, unzip, unzip3
+                                            )
+
+-- $setup
+-- >>> import qualified Data.Series.Unboxed as Series
+-- >>> import qualified Data.Series.Index as Index
+
+infixl 1 `select` 
+infix 6 |->, <-|
+
+-- | A series is a labeled array of values of type @a@,
+-- indexed by keys of type @k@.
+--
+-- Like @Data.Map@ and @Data.HashMap@, they support efficient:
+--
+--      * random access by key ( \(O(\log n)\) );
+--      * slice by key ( \(O(\log n)\) ).
+--
+-- Like @Data.Vector.Vector@, they support efficient:
+--
+--      * random access by index ( \(O(1)\) );
+--      * slice by index ( \(O(1)\) );
+--      * numerical operations.
+type Series = G.Series Vector
+
+
+index :: Series k a -> Index k
+{-# INLINABLE index #-}
+index = G.index
+
+
+values :: Series k a -> Vector a
+{-# INLINABLE values #-}
+values = G.values
+
+
+-- | Create a 'Series' with a single element.
+singleton :: Unbox a => k -> a -> Series k a
+{-# INLINABLE singleton #-}
+singleton = G.singleton
+
+
+-- | \(O(n)\) Generate a 'Series' by mapping every element of its index.
+--
+-- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']
+-- index | values
+-- ----- | ------
+--   'a' |      0
+--   'b' |      0
+--   'c' |      0
+--   'd' |      0
+fromIndex :: Unbox a
+          => (k -> a) -> Index k -> Series k a
+{-# INLINABLE fromIndex #-}
+fromIndex = G.fromIndex
+
+
+-- | Construct a series from a list of key-value pairs. There is no
+-- condition on the order of pairs.
+--
+-- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]
+-- >>> xs
+-- index | values
+-- ----- | ------
+--   'a' |      5
+--   'b' |      0
+--   'd' |      1
+--
+-- If you need to handle duplicate keys, take a look at `fromListDuplicates`.
+fromList :: (Ord k, Unbox a) => [(k, a)] -> Series k a
+{-# INLINABLE fromList #-}
+fromList = G.fromList
+
+
+-- | Construct a series from a list of key-value pairs.
+-- Contrary to `fromList`, values at duplicate keys are preserved. To keep each
+-- key unique, an `Occurrence` number counts up.
+--
+-- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+-- >>> xs
+--   index | values
+--   ----- | ------
+-- ('a',0) |      5
+-- ('b',0) |      0
+-- ('d',0) |      1
+-- ('d',1) |     -4
+-- ('d',2) |      7
+fromListDuplicates :: (Ord k, Unbox a) => [(k, a)] -> Series (k, Occurrence) a
+{-# INLINABLE fromListDuplicates #-}
+fromListDuplicates = G.fromListDuplicates
+
+
+-- | Construct a list from key-value pairs. The elements are in order sorted by key:
+--
+-- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]
+-- >>> xs
+-- index | values
+-- ----- | ------
+--   'a' |      5
+--   'b' |      0
+--   'd' |      1
+-- >>> toList xs
+-- [('a',5),('b',0),('d',1)]
+toList :: Unbox a => Series k a -> [(k, a)]
+{-# INLINABLE toList #-}
+toList = G.toList
+
+
+-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. 
+toVector :: (Unbox a, Unbox k) => Series k a -> Vector (k, a)
+{-# INLINABLE toVector #-}
+toVector = G.toVector
+
+
+-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no
+-- condition on the order of pairs. Duplicate keys are silently dropped. If you
+-- need to handle duplicate keys, see 'fromVectorDuplicates'.
+--
+-- Note that due to differences in sorting,
+-- @Series.fromList@ and @Series.fromVector . Vector.fromList@ 
+-- may not be equivalent if the input list contains duplicate keys.
+fromVector :: (Ord k, Unbox k, Unbox a)
+           => Vector (k, a) -> Series k a
+{-# INLINABLE fromVector #-}
+fromVector = G.fromVector
+
+
+-- | Construct a series from a 'Vector' of key-value pairs.
+-- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each
+-- key unique, an 'Occurrence' number counts up.
+--
+-- >>> import qualified Data.Vector.Unboxed as Unboxed
+-- >>> let xs = fromVectorDuplicates $ Unboxed.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]
+-- >>> xs
+--   index | values
+--   ----- | ------
+-- ('a',0) |      5
+-- ('b',0) |      0
+-- ('d',0) |      1
+-- ('d',1) |     -4
+-- ('d',2) |      7
+fromVectorDuplicates :: (Unbox k, Unbox a, Ord k) => Vector (k, a) -> Series (k, Occurrence) a
+{-# INLINABLE fromVectorDuplicates #-}
+fromVectorDuplicates = G.fromVectorDuplicates
+
+
+-- | Convert a series into a lazy @Map@.
+toLazyMap :: (Unbox a) => Series k a -> ML.Map k a
+{-# INLINABLE toLazyMap #-}
+toLazyMap = G.toLazyMap
+
+
+-- | Construct a series from a lazy @Map@.
+fromLazyMap :: (Unbox a) => ML.Map k a -> Series k a
+{-# INLINABLE fromLazyMap #-}
+fromLazyMap = G.fromLazyMap
+
+
+-- | Convert a series into a strict @Map@.
+toStrictMap :: (Unbox a) => Series k a -> MS.Map k a
+{-# INLINABLE toStrictMap #-}
+toStrictMap = G.toStrictMap
+
+-- | Construct a series from a strict @Map@.
+fromStrictMap :: (Unbox a) => MS.Map k a -> Series k a
+{-# INLINABLE fromStrictMap #-}
+fromStrictMap = G.fromStrictMap
+
+
+-- | \(O(n)\) Map every element of a 'Series'.
+map :: (Unbox a, Unbox b) => (a -> b) -> Series k a -> Series k b
+{-# INLINABLE map #-}
+map = G.map
+
+
+-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.
+mapWithKey :: (Unbox a, Unbox b) => (k -> a -> b) -> Series k a -> Series k b
+{-# INLINABLE mapWithKey #-}
+mapWithKey = G.mapWithKey
+
+
+-- | \(O(n \log n)\).
+-- Map each key in the index to another value. Note that the resulting series
+-- may have less elements, because each key must be unique.
+--
+-- In case new keys are conflicting, the first element is kept.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> import qualified Data.List
+-- >>> xs `mapIndex` (Data.List.take 1)
+-- index | values
+-- ----- | ------
+--   "L" |      4
+--   "P" |      1
+mapIndex :: (Unbox a, Ord k, Ord g) => Series k a -> (k -> g) -> Series g a
+{-# INLINABLE mapIndex #-}
+mapIndex = G.mapIndex
+
+
+-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.
+concatMap :: (Unbox a, Unbox k, Unbox b, Ord k) 
+          => (a -> Series k b) 
+          -> Series k a 
+          -> Series k b
+{-# INLINABLE concatMap #-}
+concatMap = G.concatMap
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, yielding a series of results.
+mapWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)
+{-# INLINABLE mapWithKeyM #-}
+mapWithKeyM = G.mapWithKeyM
+
+
+-- | \(O(n)\) Apply the monadic action to every element of a series and its
+-- index, discarding the results.
+mapWithKeyM_ :: (Unbox a, Monad m) => (k -> a -> m b) -> Series k a -> m ()
+{-# INLINABLE mapWithKeyM_ #-}
+mapWithKeyM_ = G.mapWithKeyM_
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- yielding a series of results.
+forWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)
+{-# INLINABLE forWithKeyM #-}
+forWithKeyM = G.forWithKeyM
+
+
+-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, 
+-- discarding the results.
+forWithKeyM_ :: (Unbox a, Monad m) => Series k a -> (k -> a -> m b) -> m ()
+{-# INLINABLE forWithKeyM_ #-}
+forWithKeyM_ = G.forWithKeyM_
+
+
+-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+-- >>> take 2 xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+take :: Unbox a => Int -> Series k a -> Series k a
+{-# INLINABLE take #-}
+take = G.take
+
+
+-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+
+-- >>> takeWhile (>1) xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+takeWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a
+takeWhile = G.takeWhile
+
+
+-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+-- >>> drop 2 xs
+--    index | values
+--    ----- | ------
+--  "Paris" |      1
+-- "Vienna" |      5
+drop :: Unbox a => Int -> Series k a -> Series k a
+{-# INLINABLE drop #-}
+drop = G.drop
+
+
+-- | \(O(n)\) Returns the complement of `takeWhile`.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- "Vienna" |      5
+
+-- >>> dropWhile (>1) xs
+--    index | values
+--    ----- | ------
+--  "Paris" |      1
+-- "Vienna" |      5
+dropWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a
+dropWhile = G.dropWhile
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+--
+-- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),  ('g', 2) ]
+-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
+-- >>> zipWithMatched (+) xs ys
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'b' |     12
+zipWithMatched :: (Unbox a, Unbox b, Unbox c, Ord k) 
+               => (a -> b -> c) -> Series k a -> Series k b -> Series k c
+{-# INLINABLE zipWithMatched #-}
+zipWithMatched = G.zipWithMatched
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys not present in all three series are dropped.
+--
+-- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),   ('g', 2) ]
+-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11),  ('d', 13) ]
+-- >>> let zs = Series.fromList [ ('a', 20::Int), ('d', 13), ('e', 6) ]
+-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs
+-- index | values
+-- ----- | ------
+--   'a' |     30
+zipWithMatched3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Ord k) 
+                => (a -> b -> c -> d) 
+                -> Series k a 
+                -> Series k b 
+                -> Series k c
+                -> Series k d
+{-# INLINABLE zipWithMatched3 #-}
+zipWithMatched3 = G.zipWithMatched3
+
+
+-- | Apply a function elementwise to two series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+-- 
+--
+-- >>> import Data.Char ( ord )
+-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('c', 2) ]
+-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
+-- >>> zipWithKey (\k x y -> ord k + x + y) xs ys
+-- index | values
+-- ----- | ------
+--   'a' |    107
+--   'b' |    110
+zipWithKey :: (Unbox a, Unbox b, Unbox c, Unbox k, Ord k)  
+           => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c
+{-# INLINABLE zipWithKey #-}
+zipWithKey = G.zipWithKey
+
+
+-- | Apply a function elementwise to three series, matching elements
+-- based on their keys. Keys present only in the left or right series are dropped.
+-- 
+-- >>> import Data.Char ( ord )
+-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]
+-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
+-- >>> let zs = Series.fromList [ ('a', 20::Int), ('b', 7), ('d', 5) ]
+-- >>> zipWithKey3 (\k x y z -> ord k + x + y + z) xs ys zs
+-- index | values
+-- ----- | ------
+--   'a' |    127
+--   'b' |    117
+zipWithKey3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Unbox k, Ord k) 
+            => (k -> a -> b -> c -> d) 
+            -> Series k a 
+            -> Series k b 
+            -> Series k c
+            -> Series k d
+{-# INLINABLE zipWithKey3 #-}
+zipWithKey3 = G.zipWithKey3
+
+
+-- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.
+--
+-- In the example below, we want to set the value to @-100@ (via @'constStrategy' (-100)@) for keys which are only present 
+-- in the left 'Series', and drop keys (via 'skipStrategy') which are only present in the `right 'Series'  
+--
+-- >>> let xs = Series.fromList [ ('a', 0::Int),  ('b', 1),  ('g', 2) ]
+-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]
+-- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy  xs ys
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'b' |     12
+--   'g' |   -100
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ 
+-- than using @'zipWithStrategy' f 'skipStrategy' 'skipStrategy'@.
+zipWithStrategy :: (Ord k, Unbox a, Unbox b, Unbox c) 
+                => (a -> b -> c)     -- ^ Function to combine values when present in both series
+                -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right
+                -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left
+                -> Series k a
+                -> Series k b 
+                -> Series k c
+{-# INLINABLE zipWithStrategy #-}
+zipWithStrategy = G.zipWithStrategy
+
+
+-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is 
+-- present in one of the 'Series' but not all of the others.
+--
+-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ 
+-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.
+zipWithStrategy3 :: (Ord k, Unbox a, Unbox b, Unbox c, Unbox d) 
+                => (a -> b -> c -> d) -- ^ Function to combine values when present in all series
+                -> ZipStrategy k a d  -- ^ Strategy for when the key is in the left series but not in all the others
+                -> ZipStrategy k b d  -- ^ Strategy for when the key is in the center series but not in all the others
+                -> ZipStrategy k c d  -- ^ Strategy for when the key is in the right series but not in all the others
+                -> Series k a
+                -> Series k b 
+                -> Series k c
+                -> Series k d
+zipWithStrategy3 = G.zipWithStrategy3
+{-# INLINABLE zipWithStrategy3 #-}
+
+
+-- | Zip two 'Series' with a combining function. The value for keys which are missing from
+-- either 'Series' is replaced with the appropriate `mempty` value.
+--
+-- >>> import Data.Monoid ( Sum(..) )
+-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]
+-- >>> zipWithMonoid (<>) xs ys
+--        index |           values
+--        ----- |           ------
+-- "2023-01-01" | Sum {getSum = 6}
+-- "2023-01-02" | Sum {getSum = 2}
+-- "2023-01-03" | Sum {getSum = 7}
+zipWithMonoid :: ( Monoid a, Monoid b
+                 , Unbox a, Unbox b, Unbox c
+                 , Ord k
+                 ) 
+              => (a -> b -> c)
+              -> Series k a
+              -> Series k b 
+              -> Series k c
+zipWithMonoid = G.zipWithMonoid
+{-# INLINABLE zipWithMonoid #-}
+
+
+-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `esum` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |      6
+-- "2023-01-02" |      2
+-- "2023-01-03" |      7
+esum :: (Ord k, Num a, Unbox a) 
+     => Series k a 
+     -> Series k a
+     -> Series k a
+esum = G.esum
+{-# INLINABLE esum #-}
+
+
+-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. 
+--
+-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]
+-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]
+-- >>> xs `eproduct` ys
+--        index | values
+--        ----- | ------
+-- "2023-01-01" |     10
+-- "2023-01-02" |      3
+-- "2023-01-03" |      7
+eproduct :: (Ord k, Num a, Unbox a) 
+         => Series k a 
+         -> Series k a
+         -> Series k a
+eproduct = G.eproduct
+{-# INLINABLE eproduct #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 2-tuples.
+unzip :: (Unbox a, Unbox b) 
+      => Series k (a, b)
+      -> ( Series k a
+         , Series k b
+         )
+unzip = G.unzip
+{-# INLINABLE unzip #-}
+
+
+-- | \(O(n)\) Unzip a 'Series' of 3-tuples.
+unzip3 :: (Unbox a, Unbox b, Unbox c) 
+       => Series k (a, b, c)
+       -> ( Series k a
+          , Series k b
+          , Series k c
+          )
+unzip3 = G.unzip3
+{-# INLINABLE unzip3 #-}
+
+
+-- | Require a series to have a specific `Index`. 
+-- Contrary to @select@, all keys in the `Index` will be present in the resulting series.
+--
+-- Note that unlike the implementation for boxed series (`Data.Series.require`), missing keys need to be mapped to some values because unboxed
+-- series cannot contain values of type @`Maybe` a@. 
+--
+-- In the example below, the missing value for key @\"Taipei\"@ is mapped to 0:
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> require (const 0) xs (Index.fromList ["Paris", "Lisbon", "Taipei"])
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+--  "Paris" |      1
+-- "Taipei" |      0
+require :: (Unbox a, Ord k) 
+        => (k -> a) -> Series k a -> Index k -> Series k a
+{-# INLINABLE require #-}
+require f = G.requireWith f id
+
+
+-- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will
+-- be indexed from 0.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> dropIndex xs
+-- index | values
+-- ----- | ------
+--     0 |      4
+--     1 |      2
+--     2 |      1
+dropIndex :: Series k a -> Series Int a
+{-# INLINABLE dropIndex #-}
+dropIndex = G.dropIndex
+
+
+-- | Filter elements. Only elements for which the predicate is @True@ are kept. 
+-- Notice that the filtering is done on the values, not on the keys.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> filter (>2) xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+--
+-- See also 'filterWithKey'.
+filter :: (Unbox a, Ord k) => (a -> Bool) -> Series k a -> Series k a
+{-# INLINABLE filter #-}
+filter = G.filter
+
+
+-- | Filter elements, taking into account the corresponding key. Only elements for which 
+-- the predicate is @True@ are kept. 
+filterWithKey :: (Unbox a, Ord k) 
+              => (k -> a -> Bool) 
+              -> Series k a 
+              -> Series k a
+{-# INLINABLE filterWithKey #-}
+filterWithKey = G.filterWithKey
+
+
+-- | Select a subseries. There are a few ways to do this.
+--
+-- The first way to do this is to select a sub-series based on random keys. For example,
+-- selecting a subseries from an `Index`:
+--
+-- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]
+-- >>> xs `select` Index.fromList ['a', 'd']
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'd' |     40
+--
+-- The second way to select a sub-series is to select all keys in a range:
+--
+-- >>> xs `select` 'b' `to` 'c'
+-- index | values
+-- ----- | ------
+--   'b' |     20
+--   'c' |     30
+--
+-- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not
+-- in the series, it'll be ignored:
+--
+-- >>> xs `select` Index.fromList ['a', 'd', 'e']
+-- index | values
+-- ----- | ------
+--   'a' |     10
+--   'd' |     40
+--
+-- See `require` if you want to ensure that all keys are present.
+select :: (Unbox a, Selection s, Ord k) => Series k a -> s k -> Series k a
+select = G.select
+
+
+-- | Select a sub-series from a series matching a condition.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> xs `selectWhere` (Series.map (>1) xs)
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+selectWhere :: (Unbox a, Ord k) => Series k a -> Series k Bool -> Series k a
+{-# INLINABLE selectWhere #-}
+selectWhere = G.selectWhere
+
+
+-- | \(O(\log n)\). Extract a single value from a series, by key.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs `at` "Paris"
+-- Just 1
+-- >>> xs `at` "Sydney"
+-- Nothing
+at :: (Unbox a, Ord k) => Series k a -> k -> Maybe a
+{-# INLINABLE at #-}
+at = G.at
+
+
+-- | \(O(1)\). Extract a single value from a series, by index.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> xs `iat` 0
+-- Just 4
+-- >>> xs `iat` 3
+-- Nothing
+iat :: Unbox a => Series k a -> Int -> Maybe a
+{-# INLINABLE iat #-}
+iat = G.iat
+
+
+-- | Replace values in the right series from values in the left series at matching keys.
+-- Keys not in the right series are unaffected.
+-- 
+-- See `(|->)` and `(<-|)`, which might be more readable.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> ys `replace` xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+replace :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
+{-# INLINABLE replace #-}
+replace = G.replace
+
+
+-- | Replace values in the right series from values in the left series at matching keys.
+-- Keys not in the right series are unaffected.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> ys |-> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+(|->) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
+{-# INLINABLE (|->) #-}
+(|->) = (G.|->)
+
+
+-- | Replace values in the left series from values in the right series at matching keys.
+-- Keys not in the left series are unaffected.
+--
+-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]
+-- >>> xs
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |      1
+-- >>> let ys = Series.singleton "Paris" (99::Int)
+-- >>> xs <-| ys
+--    index | values
+--    ----- | ------
+-- "Lisbon" |      4
+-- "London" |      2
+--  "Paris" |     99
+(<-|) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a
+{-# INLINABLE (<-|) #-}
+(<-|) = (G.<-|)
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |    1.0
+--     1 |    2.0
+--     2 |    3.0
+--     3 |    4.0
+-- >>> import Control.Foldl (variance)
+-- >>> fold variance xs
+-- 1.25
+--
+-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into
+-- account while folding.
+fold :: Unbox a 
+     => Fold a b -> Series k a -> b
+fold = G.fold
+{-# INLINABLE fold #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.
+--
+-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into
+-- account while folding.
+foldM :: (Monad m, Unbox a) 
+      => FoldM m a b  
+      -> Series k a 
+      -> m b
+foldM = G.foldM
+{-# INLINABLE foldM #-}
+
+
+-- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.
+foldWithKey :: (Unbox k, Unbox a) 
+            => Fold (k, a) b -> Series k a -> b
+foldWithKey = G.foldWithKey
+{-# INLINABLE foldWithKey #-}
+
+
+-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.
+foldMWithKey :: (Monad m, Unbox a, Unbox k) 
+             => FoldM m (k, a) b  
+             -> Series k a 
+             -> m b
+foldMWithKey = G.foldMWithKey
+{-# INLINABLE foldMWithKey #-}
+
+
+-- | \(O(n)\) Map each element of the structure to a monoid and combine
+-- the results.
+foldMap :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m
+{-# INLINABLE foldMap #-}
+foldMap = G.foldMap
+
+
+-- | \(O(n)\) Like 'foldMap', but strict in the accumulator. It uses the same
+-- implementation as the corresponding method of the 'Foldable' type class.
+foldMap' :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m
+{-# INLINABLE foldMap' #-}
+foldMap' f = Vector.foldMap' f . values
+
+
+-- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine
+-- the results.
+foldMapWithKey :: (Monoid m, Unbox a, Unbox k) => (k -> a -> m) -> Series k a -> m
+{-# INLINABLE foldMapWithKey #-}
+foldMapWithKey = G.foldMapWithKey
+
+
+-- | Group values in a 'Series' by some grouping function (@k -> g@).
+-- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
+--
+-- This function is expected to be used in conjunction with @aggregate@:
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+groupBy :: Series k a      -- ^ Grouping function
+        -> (k -> g)        -- ^ Input series
+        -> Grouping k g a  -- ^ Grouped series
+{-# INLINABLE groupBy #-}
+groupBy = G.groupBy
+
+
+-- | Representation of a 'Series' being grouped.
+type Grouping k g a = G.Grouping k g Vector a
+
+
+-- | Aggregate groups resulting from a call to 'groupBy':
+-- 
+-- >>> import Data.Maybe ( fromMaybe )
+-- >>> type Date = (Int, String)
+-- >>> month :: (Date -> String) = snd
+-- >>> :{ 
+--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
+--                              , ((2021, "January"), -5)
+--                              , ((2020, "June")   , 20)
+--                              , ((2021, "June")   , 25) 
+--                              ]
+--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
+-- :}
+--     index | values
+--     ----- | ------
+-- "January" |     -5
+--    "June" |     20
+--
+-- If you want to aggregate groups using a binary function, see 'foldWith' which
+-- may be much faster.
+aggregateWith :: (Ord g, Unbox a, Unbox b) 
+              => Grouping k g a 
+              -> (Series k a -> b) 
+              -> Series g b
+{-# INLINABLE aggregateWith #-}
+aggregateWith = G.aggregateWith
+
+
+-- | Aggregate each group in a 'Grouping' using a binary function.
+-- While this is not as expressive as 'aggregateWith', users looking for maximum
+-- performance should use 'foldWith' as much as possible.
+foldWith :: (Ord g, Unbox a) 
+         => Grouping k g a
+         -> (a -> a -> a)
+         -> Series g a
+{-# INLINABLE foldWith #-}
+foldWith = G.foldWith
+
+
+-- | Expanding window aggregation.
+--
+-- >>> :{ 
+--     let (xs :: Series Int Int) 
+--          = fromList [ (1, 0)
+--                     , (2, 1)
+--                     , (3, 2)
+--                     , (4, 3)
+--                     , (5, 4)
+--                     , (6, 5)
+--                     ]
+--     in (xs `expanding` sum) :: Series Int Int 
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      0
+--     2 |      1
+--     3 |      3
+--     4 |      6
+--     5 |     10
+--     6 |     15
+expanding :: (Unbox a, Unbox b) 
+          => Series k a        -- ^ Series vector
+          -> (Series k a -> b) -- ^ Aggregation function
+          -> Series k b        -- ^ Resulting vector
+{-# INLINABLE expanding #-}
+expanding = G.expanding
+
+
+-- | General-purpose window aggregation.
+--
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 3)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in windowing (\k -> k `to` (k+2)) sum xs
+-- :}
+-- index | values
+-- ----- | ------
+--     1 |      3
+--     2 |      6
+--     3 |      9
+--     4 |     12
+--     5 |      9
+--     6 |      5
+windowing :: (Ord k, Unbox a, Unbox b)
+          => (k -> Range k)
+          -> (Series k a -> b)
+          -> Series k a
+          -> Series k b
+{-# INLINABLE windowing #-}
+windowing = G.windowing 
+
+
+-- | \(O(1)\) Test whether a 'Series' is empty.
+null :: Unbox a => Series k a -> Bool
+{-# INLINABLE null #-}
+null = G.null
+
+
+-- |\(O(1)\) Extract the length of a 'Series'.
+length :: Unbox a => Series k a -> Int
+{-# INLINABLE length #-}
+length = G.length
+
+
+-- | \(O(n)\) Check if all elements satisfy the predicate.
+all :: Unbox a => (a -> Bool) -> Series k a -> Bool
+{-# INLINABLE all #-}
+all = G.all
+
+
+-- | \(O(n)\) Check if any element satisfies the predicate.
+any :: Unbox a => (a -> Bool) -> Series k a -> Bool
+{-# INLINABLE any #-}
+any = G.any
+
+
+-- | \(O(n)\) Check if all elements are 'True'.
+and :: Series k Bool -> Bool
+{-# INLINABLE and #-}
+and = G.and
+
+
+-- | \(O(n)\) Check if any element is 'True'.
+or :: Series k Bool -> Bool
+{-# INLINABLE or #-}
+or = G.or
+
+
+-- | \(O(n)\) Compute the sum of the elements.
+sum :: (Unbox a, Num a) => Series k a -> a
+{-# INLINABLE sum #-}
+sum = G.sum
+
+
+-- | \(O(n)\) Compute the product of the elements.
+product :: (Unbox a, Num a) => Series k a -> a
+{-# INLINABLE product #-}
+product = G.product
+
+
+-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+--
+-- See also 'argmax'.
+maximum :: (Ord a, Unbox a) => Series k a -> Maybe a
+{-# INLINABLE maximum #-}
+maximum = G.maximum
+
+
+-- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
+maximumOn :: (Ord b, Unbox a) => (a -> b) -> Series k a -> Maybe a
+{-# INLINABLE maximumOn #-}
+maximumOn = G.maximumOn
+
+
+-- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
+-- If the 'Series' is empty, @Nothing@ is returned.
+--
+-- See also 'argmin'.
+minimum :: (Ord a, Unbox a) => Series k a -> Maybe a
+{-# INLINABLE minimum #-}
+minimum = G.minimum
+
+
+-- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
+-- In case of a tie, the first occurrence wins. If the 'Series' is empty, @Nothing@ is returned.
+minimumOn :: (Ord b, Unbox a) => (a -> b) -> Series k a -> Maybe a
+{-# INLINABLE minimumOn #-}
+minimumOn = G.minimumOn
+
+
+-- | \(O(n)\) Find the index of the maximum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the maximum element is returned.
+--
+-- >>> import qualified Data.Series.Unboxed as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 0)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 7)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmax xs 
+-- :}
+-- Just 4
+argmax :: (Ord a, Unbox a)
+       => Series k a
+       -> Maybe k
+argmax = G.argmax
+{-# INLINABLE argmax #-}
+
+
+-- | \(O(n)\) Find the index of the minimum element in the input series.
+-- If the input series is empty, 'Nothing' is returned.
+--
+-- The index of the first occurrence of the minimum element is returned.
+-- >>> import qualified Data.Series.Unboxed as Series 
+-- >>> :{ 
+--     let (xs :: Series.Series Int Int) 
+--          = Series.fromList [ (1, 1)
+--                            , (2, 1)
+--                            , (3, 2)
+--                            , (4, 0)
+--                            , (5, 4)
+--                            , (6, 5)
+--                            ]
+--     in argmin xs 
+-- :}
+-- Just 4
+argmin :: (Ord a, Unbox a)
+       => Series k a
+       -> Maybe k
+argmin = G.argmin
+{-# INLINABLE argmin #-}
+
+
+-- | \(O(n)\) Left-to-right postscan.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> postscanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      3
+--     2 |      6
+--     3 |     10
+postscanl :: (Unbox a, Unbox b) 
+          => (a -> b -> a) -> a -> Series k b -> Series k a
+{-# INLINABLE postscanl #-}
+postscanl = G.postscanl
+
+
+-- | \(O(n)\) Left-to-right prescan.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int
+-- >>> xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--     3 |      4
+-- >>> prescanl (+) 0 xs
+-- index | values
+-- ----- | ------
+--     0 |      0
+--     1 |      1
+--     2 |      3
+--     3 |      6
+prescanl :: (Unbox a, Unbox b) 
+         => (a -> b -> a) -> a -> Series k b -> Series k a
+{-# INLINABLE prescanl #-}
+prescanl = G.prescanl
+
+
+-- | Display a 'Series' using default 'DisplayOptions'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
+-- >>> putStrLn $ display xs
+-- index | values
+-- ----- | ------
+--     0 |      1
+--     1 |      2
+--     2 |      3
+--   ... |    ...
+--     4 |      5
+--     5 |      6
+--     6 |      7
+display :: (Unbox a, Show k, Show a) 
+        => Series k a 
+        -> String
+display = G.display
+
+
+-- | Display a 'Series' using customizable 'DisplayOptions'.
+--
+-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int
+-- >>> import Data.List (replicate)
+-- >>> :{
+--     let opts = DisplayOptions { maximumNumberOfRows  = 4
+--                               , indexHeader = "keys"
+--                               , valuesHeader = "vals"
+--                               , keyDisplayFunction   = (\i -> replicate i 'x') `noLongerThan` 5
+--                               , valueDisplayFunction = (\i -> replicate i 'o') 
+--                               }
+--      in putStrLn $ displayWith opts xs
+-- :}
+--   keys |    vals
+--  ----- |  ------
+--        |       o
+--      x |      oo
+--    ... |     ...
+--  xxxxx |  oooooo
+-- xxx... | ooooooo
+displayWith :: (Unbox a) 
+            => DisplayOptions k a
+            -> Series k a 
+            -> String
+displayWith = G.displayWith
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,19 +1,19 @@
-module Main (main) where
-
-import qualified Test.Data.Series
-import qualified Test.Data.Series.Generic.Aggregation
-import qualified Test.Data.Series.Generic.Definition
-import qualified Test.Data.Series.Index
-import qualified Test.Data.Series.Generic.View
-import qualified Test.Data.Series.Generic.Zip
-
-import           Test.Tasty ( defaultMain, testGroup )
-
-main :: IO ()
-main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.tests
-                                            , Test.Data.Series.Index.tests
-                                            , Test.Data.Series.Generic.Aggregation.tests
-                                            , Test.Data.Series.Generic.Definition.tests
-                                            , Test.Data.Series.Generic.View.tests
-                                            , Test.Data.Series.Generic.Zip.tests
-                                            ]
+module Main (main) where
+
+import qualified Test.Data.Series
+import qualified Test.Data.Series.Generic.Aggregation
+import qualified Test.Data.Series.Generic.Definition
+import qualified Test.Data.Series.Index
+import qualified Test.Data.Series.Generic.View
+import qualified Test.Data.Series.Generic.Zip
+
+import           Test.Tasty ( defaultMain, testGroup )
+
+main :: IO ()
+main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.tests
+                                            , Test.Data.Series.Index.tests
+                                            , Test.Data.Series.Generic.Aggregation.tests
+                                            , Test.Data.Series.Generic.Definition.tests
+                                            , Test.Data.Series.Generic.View.tests
+                                            , Test.Data.Series.Generic.Zip.tests
+                                            ]
diff --git a/test/Test/Data/Series.hs b/test/Test/Data/Series.hs
--- a/test/Test/Data/Series.hs
+++ b/test/Test/Data/Series.hs
@@ -1,7 +1,7 @@
-
-module Test.Data.Series (tests) where
-
-import           Test.Tasty           ( testGroup, TestTree ) 
-
-tests :: TestTree
+
+module Test.Data.Series (tests) where
+
+import           Test.Tasty           ( testGroup, TestTree ) 
+
+tests :: TestTree
 tests = testGroup "Data.Series" []
diff --git a/test/Test/Data/Series/Generic/Aggregation.hs b/test/Test/Data/Series/Generic/Aggregation.hs
--- a/test/Test/Data/Series/Generic/Aggregation.hs
+++ b/test/Test/Data/Series/Generic/Aggregation.hs
@@ -1,134 +1,153 @@
-
-module Test.Data.Series.Generic.Aggregation (tests) where
-
-import qualified Data.Map.Strict      as MS
-import qualified Data.Series.Generic  as Series
-import           Data.Series.Generic  ( Series, fromStrictMap, groupBy, aggregateWith, foldWith, windowing, to, expanding)
-import           Data.Vector          ( Vector )
-
-import           Hedgehog             ( property, forAll, (===) )
-import qualified Hedgehog.Gen         as Gen
-import qualified Hedgehog.Range       as Range
-
-import           Prelude              hiding ( zipWith )
-
-import           Test.Tasty           ( testGroup, TestTree )
-import           Test.Tasty.Hedgehog  ( testProperty )
-import           Test.Tasty.HUnit     ( testCase, assertEqual )
-
-tests :: TestTree
-tests = testGroup "Data.Series.Generic.Aggregation" [ testGroupBy
-                                                    , testWindowing
-                                                    , testWindowingRollingForwards
-                                                    , testWindowingRollingBackwards
-                                                    , testPropAggregateVsfoldWith
-                                                    , testExpanding
-                                                    ]
-
-
-testGroupBy :: TestTree
-testGroupBy = testGroup "Data.Series.Generic.groupBy" [ testGroupBy1, testGroupBy2 ]
-    where
-        testGroupBy1 = testCase "groupBy" $ do
-            let (series :: Series Vector String Int) = fromStrictMap $ MS.fromList [("aa", 1), ("ab", 2), ("c", 3), ("dc", 4), ("ae", 5)]
-                expectation = fromStrictMap $ MS.fromList [(1, 3), (2, 1+2+4+5)]
-            
-            assertEqual mempty expectation $ series `groupBy` length `aggregateWith` (Series.sum :: Series Vector String Int -> Int)
-
-        testGroupBy2 = testCase "groupBy" $ do
-            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList $ zip [0,1,2,3] [0,1,2,3]
-                expectation = fromStrictMap $ MS.fromList [(True, 0+2), (False, 1+3)]
-            
-            assertEqual mempty expectation $ series `groupBy` even `aggregateWith` (Series.sum :: Series Vector Int Int -> Int)
-
-
-
-testWindowing :: TestTree
-testWindowing = testCase "Data.Series.Generic.windowing" $ do
-
-    let (xs :: Series Vector Int Int) 
-         = Series.fromList [ (1, 0)
-                           , (2, 1)
-                           , (3, 2)
-                           , (4, 3)
-                           , (5, 4)
-                           , (6, 5)
-                           ]
-        expectation = Series.fromList [ (1, 3)
-                                      , (2, 6)
-                                      , (3, 9)
-                                      , (4, 12)
-                                      , (5, 9)
-                                      , (6, 5)
-                                      ]
-    assertEqual mempty expectation $ windowing (\k -> k `to` (k+2)) sum xs
-
-
-testWindowingRollingForwards :: TestTree
-testWindowingRollingForwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]
-    where
-        test1 = testCase "rollingForwards" $ do
-            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
-                expectation = fromStrictMap $ MS.fromList [ (1, 1+2)
-                                                          , (2, 2+3)
-                                                          , (3, 3+4)
-                                                          , (4, 4+5)
-                                                          , (5, 5)
-                                                          ]
-            
-            assertEqual mempty expectation $ windowing (\k -> k `to` (k + 1)) (Series.sum :: Series Vector Int Int -> Int) series
-
-        test2 = testCase "rollingForwards" $ do
-            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
-                expectation = fromStrictMap $ MS.fromList [ (1, 1+2+3)
-                                                          , (2, 2+3+4)
-                                                          , (3, 3+4+5)
-                                                          , (4, 4+5)
-                                                          , (5, 5)
-                                                          ]
-            
-            assertEqual mempty expectation $ windowing (\k -> k `to` (k + 2)) (Series.sum :: Series Vector Int Int -> Int) series
-
-
-testWindowingRollingBackwards :: TestTree
-testWindowingRollingBackwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]
-    where
-        test1 = testCase "rollingForwards" $ do
-            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
-                expectation = fromStrictMap $ MS.fromList [ (1, 1)
-                                                          , (2, 1+2)
-                                                          , (3, 2+3)
-                                                          , (4, 3+4)
-                                                          , (5, 4+5)
-                                                          ]
-            
-            assertEqual mempty expectation $ windowing (\k -> (k-1) `to` k) (Series.sum :: Series Vector Int Int -> Int) series
-
-        test2 = testCase "rollingForwards" $ do
-            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
-                expectation = fromStrictMap $ MS.fromList [ (1, 1)
-                                                          , (2, 1+2)
-                                                          , (3, 1+2+3)
-                                                          , (4, 2+3+4)
-                                                          , (5, 3+4+5)
-                                                          ]
-            
-            assertEqual mempty expectation $ windowing (\k -> (k-2) `to` k)  (Series.sum :: Series Vector Int Int -> Int) series
-
-
-testPropAggregateVsfoldWith :: TestTree
-testPropAggregateVsfoldWith 
-    = testProperty "check that groupBy and testWindowingRollingForwards are equivalent" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.int $ Range.linear (-500) 500) 
-        let (xs :: Series Vector Int Int) = Series.fromList (zip [0::Int ..] ms)
-
-        xs `groupBy` (`mod` 5) `aggregateWith` (Series.sum :: Series Vector Int Int -> Int) === xs `groupBy` (`mod` 5) `foldWith` (+)
-
-
-testExpanding :: TestTree
-testExpanding = testCase "expanding" $ do
-    let (xs :: Series Vector Char Int) = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1::Int,2,3,4]
-        rs = xs `expanding` Series.sum
-        expectation = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1,1+2,1+2+3,1+2+3+4]
-    
+
+module Test.Data.Series.Generic.Aggregation (tests) where
+
+import qualified Data.IntMap.Strict   as IS
+import qualified Data.Map.Strict      as MS
+import qualified Data.Series.Generic  as Series
+import           Data.Series.Generic  ( Series, fromStrictMap, groupBy, aggregateWith, foldWith, windowing, to, expanding)
+import           Data.Vector          ( Vector )
+import qualified Data.Vector          as Vector
+
+import           Hedgehog             ( property, forAll, (===) )
+import qualified Hedgehog.Gen         as Gen
+import qualified Hedgehog.Range       as Range
+
+import           Prelude              hiding ( zipWith )
+
+import           Test.Tasty           ( testGroup, TestTree )
+import           Test.Tasty.Hedgehog  ( testProperty )
+import           Test.Tasty.HUnit     ( testCase, assertEqual )
+
+tests :: TestTree
+tests = testGroup "Data.Series.Generic.Aggregation" [ testGroupBy
+                                                    , testWindowing
+                                                    , testWindowingRollingForwards
+                                                    , testWindowingRollingBackwards
+                                                    , testPropAggregateVsfoldWith
+                                                    , testExpanding
+                                                    ]
+
+
+testGroupBy :: TestTree
+testGroupBy = testGroup "Data.Series.Generic.groupBy" [ testGroupBy1, testGroupBy2, testGroupBy3, testGroupBy4 ]
+    where
+        testGroupBy1 = testCase "groupBy" $ do
+            let (series :: Series Vector String Int) = fromStrictMap $ MS.fromList [("aa", 1), ("ab", 2), ("c", 3), ("dc", 4), ("ae", 5)]
+                expectation = fromStrictMap $ MS.fromList [(1, 3), (2, 1+2+4+5)]
+            
+            assertEqual mempty expectation $ series `groupBy` length `aggregateWith` (Series.sum :: Series Vector String Int -> Int)
+
+        testGroupBy2 = testCase "groupBy" $ do
+            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList $ zip [0,1,2,3] [0,1,2,3]
+                expectation = fromStrictMap $ MS.fromList [(True, 0+2), (False, 1+3)]
+            
+            assertEqual mempty expectation $ series `groupBy` even `aggregateWith` (Series.sum :: Series Vector Int Int -> Int)
+
+        -- The following example resulted in an exception
+        -- when the implementation of `aggregateWith` didn't aggregate keys in the
+        -- right order
+        testGroupBy3 = testCase "groupBy" $ do
+            let (series :: Series Vector (Int, Int) Int) = fromStrictMap $ MS.fromList [ ((0, 0), 1), ((0, 1), 2) ]
+                expectation = fromStrictMap $ MS.fromList [(0, IS.fromList [(0, 1), (1, 2)])]
+            
+            assertEqual mempty expectation $ series `groupBy` fst `aggregateWith` (IS.fromList . Series.toList . flip Series.mapIndex snd)
+
+        testGroupBy4 = testCase "groupBy" $ do
+            let (series :: Series Vector (Int, Int) Int) = fromStrictMap $ MS.fromList $ zip (zip [0,1,2,3,4,5] [1,1,2,2,3,3]) [2,1,2,1,2,1]
+                expectation = fromStrictMap $ MS.fromList [ (1, Vector.fromList [2,1])
+                                                          , (2, Vector.fromList [2,1])
+                                                          , (3, Vector.fromList [2,1])
+                                                          ]
+            
+            assertEqual mempty expectation $ series `groupBy` snd `aggregateWith` (Series.values)
+
+
+testWindowing :: TestTree
+testWindowing = testCase "Data.Series.Generic.windowing" $ do
+
+    let (xs :: Series Vector Int Int) 
+         = Series.fromList [ (1, 0)
+                           , (2, 1)
+                           , (3, 2)
+                           , (4, 3)
+                           , (5, 4)
+                           , (6, 5)
+                           ]
+        expectation = Series.fromList [ (1, 3)
+                                      , (2, 6)
+                                      , (3, 9)
+                                      , (4, 12)
+                                      , (5, 9)
+                                      , (6, 5)
+                                      ]
+    assertEqual mempty expectation $ windowing (\k -> k `to` (k+2)) sum xs
+
+
+testWindowingRollingForwards :: TestTree
+testWindowingRollingForwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]
+    where
+        test1 = testCase "rollingForwards" $ do
+            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
+                expectation = fromStrictMap $ MS.fromList [ (1, 1+2)
+                                                          , (2, 2+3)
+                                                          , (3, 3+4)
+                                                          , (4, 4+5)
+                                                          , (5, 5)
+                                                          ]
+            
+            assertEqual mempty expectation $ windowing (\k -> k `to` (k + 1)) (Series.sum :: Series Vector Int Int -> Int) series
+
+        test2 = testCase "rollingForwards" $ do
+            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
+                expectation = fromStrictMap $ MS.fromList [ (1, 1+2+3)
+                                                          , (2, 2+3+4)
+                                                          , (3, 3+4+5)
+                                                          , (4, 4+5)
+                                                          , (5, 5)
+                                                          ]
+            
+            assertEqual mempty expectation $ windowing (\k -> k `to` (k + 2)) (Series.sum :: Series Vector Int Int -> Int) series
+
+
+testWindowingRollingBackwards :: TestTree
+testWindowingRollingBackwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]
+    where
+        test1 = testCase "rollingForwards" $ do
+            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
+                expectation = fromStrictMap $ MS.fromList [ (1, 1)
+                                                          , (2, 1+2)
+                                                          , (3, 2+3)
+                                                          , (4, 3+4)
+                                                          , (5, 4+5)
+                                                          ]
+            
+            assertEqual mempty expectation $ windowing (\k -> (k-1) `to` k) (Series.sum :: Series Vector Int Int -> Int) series
+
+        test2 = testCase "rollingForwards" $ do
+            let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
+                expectation = fromStrictMap $ MS.fromList [ (1, 1)
+                                                          , (2, 1+2)
+                                                          , (3, 1+2+3)
+                                                          , (4, 2+3+4)
+                                                          , (5, 3+4+5)
+                                                          ]
+            
+            assertEqual mempty expectation $ windowing (\k -> (k-2) `to` k)  (Series.sum :: Series Vector Int Int -> Int) series
+
+
+testPropAggregateVsfoldWith :: TestTree
+testPropAggregateVsfoldWith 
+    = testProperty "check that groupBy and testWindowingRollingForwards are equivalent" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.int $ Range.linear (-500) 500) 
+        let (xs :: Series Vector Int Int) = Series.fromList (zip [0::Int ..] ms)
+
+        xs `groupBy` (`mod` 5) `aggregateWith` (Series.sum :: Series Vector Int Int -> Int) === xs `groupBy` (`mod` 5) `foldWith` (+)
+
+
+testExpanding :: TestTree
+testExpanding = testCase "expanding" $ do
+    let (xs :: Series Vector Char Int) = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1::Int,2,3,4]
+        rs = xs `expanding` Series.sum
+        expectation = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1,1+2,1+2+3,1+2+3+4]
+    
     assertEqual mempty expectation rs
diff --git a/test/Test/Data/Series/Generic/Definition.hs b/test/Test/Data/Series/Generic/Definition.hs
--- a/test/Test/Data/Series/Generic/Definition.hs
+++ b/test/Test/Data/Series/Generic/Definition.hs
@@ -1,206 +1,206 @@
-
-module Test.Data.Series.Generic.Definition (tests) where
-
-import qualified Control.Foldl        as Fold
-import           Data.Function        ( on )
-import           Data.Functor.Identity ( Identity(..))
-import           Data.List            ( nubBy, sortOn )
-import qualified Data.Map.Strict      as MS
-import qualified Data.Map.Lazy        as ML
-import           Data.Series.Generic  ( Series, Occurrence, fromStrictMap, toStrictMap, fromLazyMap, toLazyMap, fromList, toList, fromVector, toVector )
-import qualified Data.Series.Generic  as Series
-import           Data.Vector          ( Vector )
-import qualified Data.Vector          as Vector
-
-import           Hedgehog             ( property, forAll, (===), tripping )
-import qualified Hedgehog.Gen         as Gen
-import qualified Hedgehog.Range       as Range
-
-import           Test.Tasty           ( testGroup, TestTree ) 
-import           Test.Tasty.Hedgehog  ( testProperty )
-import           Test.Tasty.HUnit     ( testCase, assertEqual )
-
-tests :: TestTree
-tests = testGroup "Data.Series.Generic.Definition" 
-    [ testMappend
-    , testPropMappendLikeMap
-    , testPropShow
-    , testFromStrictMap
-    , testToStrictMap
-    , testPropRoundtripConversionWithStrictMap
-    , testPropRoundtripConversionWithLazyMap
-    , testPropRoundtripConversionWithList
-    , testPropFromListDuplicatesNeverDrops
-    , testPropFromVectorDuplicatesNeverDrops
-    , testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder
-    , testPropRoundtripConversionWithVector
-    , testPropVectorVsList
-    , testFromLazyMap
-    , testToLazyMap
-    , testTakeWhile
-    , testDropWhile
-    , testFold
-    ]
-
-
-testMappend :: TestTree
-testMappend = testCase "(<>)" $ do
-    let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]
-        (s2 :: Series Vector Char Int) = fromList [('b', 10), ('x', 25)]
-        expectation = fromList [('a', 1), ('b', 5),  ('x', 25)]
-    
-    assertEqual mempty expectation (s1 <> s2)
-
-
-testPropMappendLikeMap :: TestTree
-testPropMappendLikeMap 
-    = testProperty "Mappend property similar to Data.Map.Strict" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000)   <*> Gen.alpha)
-        m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 500 1500) <*> Gen.alpha)
-
-        (fromStrictMap :: MS.Map Int Char -> Series Vector Int Char) (m1 <> m2) === fromStrictMap m1 <> fromStrictMap m2
-
-
-testPropShow :: TestTree
-testPropShow
-    = testProperty "Show is never too long" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000)   <*> Gen.alpha)
-
-        let (xs :: Series Vector Int Char) = fromStrictMap m1
-            ls = lines $ show xs
-        if Series.length xs > 6
-            then length ls === 2 + 6 + 1
-            else length ls === 2 + Series.length xs
-
-
-testFromStrictMap :: TestTree
-testFromStrictMap = testCase "fromStrictMap" $ do
-    -- Note the duplicate input at key 'a', which should disappear
-    let input = MS.fromList [('b', 2), ('a', 1), ('a', 1)]
-        (series :: Series Vector Char Int) = fromStrictMap input
-        expectation = fromList [('a', 1), ('b', 2)]
-    
-    assertEqual mempty series expectation
-
-
-testToStrictMap :: TestTree
-testToStrictMap = testCase "toStrictMap" $ do
-    let input = MS.fromList [('b', 2), ('a', 1)]
-        (series :: Series Vector Char Int) = fromStrictMap input
-    
-    assertEqual mempty (toStrictMap series) input
-
-
-testPropRoundtripConversionWithStrictMap :: TestTree
-testPropRoundtripConversionWithStrictMap 
-    = testProperty "Roundtrip property with Data.Map.Strict" $ property $ do
-        ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        tripping ms (fromStrictMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toStrictMap)
-
-
-testPropRoundtripConversionWithLazyMap :: TestTree
-testPropRoundtripConversionWithLazyMap 
-    = testProperty "Roundtrip property with Data.Map.Lazy" $ property $ do
-        ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        tripping (ML.fromDistinctAscList $ MS.toAscList ms) (fromLazyMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toLazyMap)
-
-
-testPropRoundtripConversionWithList :: TestTree
-testPropRoundtripConversionWithList 
-    = testProperty "Roundtrip property with List" $ property $ do
-        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
-
-        -- The property below needs some explanation.
-        -- In case of conflicting keys, a Series will be biased like a Map. Therefore,
-        -- the expected List won't have duplicated (hence the use of nubBy), but the elements which
-        -- are kept are in the order of `reverse xs`.
-        (toList :: Series Vector Int Char -> [(Int, Char)] ) (fromList xs) === sortOn fst (nubBy (\left right -> fst left == fst right) (reverse xs))
-
-
-testPropFromListDuplicatesNeverDrops :: TestTree
-testPropFromListDuplicatesNeverDrops
-    = testProperty "fromListDuplicates never drops elements" $ property $ do
-        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
-        Series.length (Series.fromListDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs
-
-
-testPropFromVectorDuplicatesNeverDrops :: TestTree
-testPropFromVectorDuplicatesNeverDrops
-    = testProperty "fromVectorDuplicates never drops elements" $ property $ do
-        xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
-        Series.length (Series.fromVectorDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs
-
-
-testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder :: TestTree
-testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder
-    = testProperty "fromVectorDuplicates and fromListDuplicates are equivalent" $ property $ do
-        xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
-        Series.fromVectorDuplicates xs === Series.fromListDuplicates (Vector.toList xs)
-
-
-testPropRoundtripConversionWithVector :: TestTree
-testPropRoundtripConversionWithVector 
-    = testProperty "Roundtrip property with Vector" $ property $ do
-        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
-
-        let (srs :: Series Vector Int Char) = fromList xs
-        tripping srs toVector (Just . fromVector)
-
-
-testPropVectorVsList :: TestTree
-testPropVectorVsList 
-    = testProperty "building from a list or vector yields the same results" $ property $ do
-        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
-        -- Note that due to differences in sorting,
-        -- Series.fromList   and Series.fromVector . Vector.fromList 
-        -- are not equivalent if the input list contains duplicate keys.
-        let unique = nubBy ((==) `on` fst) xs 
-        (fromList unique :: Series Vector Int Char) === fromVector (Vector.fromList unique)
-
-
-testFromLazyMap :: TestTree
-testFromLazyMap = testCase "fromLazyMap" $ do
-    let input = ML.fromList [('b', 2), ('a', 1)]
-        (series :: Series Vector Char Int) = fromLazyMap input
-        expectation = fromList [('a', 1), ('b', 2)]
-    
-    assertEqual mempty series expectation
-
-
-testToLazyMap :: TestTree
-testToLazyMap = testCase "toLazyMap" $ do
-    let input = ML.fromList [('b', 2), ('a', 1)]
-        (series :: Series Vector Char Int) = fromLazyMap input
-    
-    assertEqual mempty (toLazyMap series) input
-
-
-testTakeWhile :: TestTree
-testTakeWhile = testProperty "takeWhile behaves like lists" $ property $ do
-    xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))
-    let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
-
-    n  <- forAll $ Gen.int  (Range.linear 1 10)
-    Series.takeWhile (\v -> v `mod` n == 0) ys === Series.fromList (takeWhile (\(_, v) -> v `mod` n == 0) $ Series.toList ys)
-
-
-testDropWhile :: TestTree
-testDropWhile = testProperty "dropWhile behaves like lists" $ property $ do
-    xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))
-    let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
-
-    n  <- forAll $ Gen.int  (Range.linear 1 10)
-    Series.dropWhile (\v -> v `mod` n /= 0) ys === Series.fromList (dropWhile (\(_, v) -> v `mod` n /= 0) $ Series.toList ys)
-
-
-testFold :: TestTree
-testFold = testGroup "fold"
-         [ testProperty "Series.sum and Control.Foldl.sum should be equivalent" $ property $ do
-            xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))
-            let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
-            Series.fold Fold.sum ys === Series.sum ys
-         , testProperty "FoldM Identity should be equivalent to a pure fold" $ property $ do
-            xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))
-            let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
-            runIdentity (Series.foldM (Fold.generalize Fold.sum) ys) === Series.sum ys
+
+module Test.Data.Series.Generic.Definition (tests) where
+
+import qualified Control.Foldl        as Fold
+import           Data.Function        ( on )
+import           Data.Functor.Identity ( Identity(..))
+import           Data.List            ( nubBy, sortOn )
+import qualified Data.Map.Strict      as MS
+import qualified Data.Map.Lazy        as ML
+import           Data.Series.Generic  ( Series, Occurrence, fromStrictMap, toStrictMap, fromLazyMap, toLazyMap, fromList, toList, fromVector, toVector )
+import qualified Data.Series.Generic  as Series
+import           Data.Vector          ( Vector )
+import qualified Data.Vector          as Vector
+
+import           Hedgehog             ( property, forAll, (===), tripping )
+import qualified Hedgehog.Gen         as Gen
+import qualified Hedgehog.Range       as Range
+
+import           Test.Tasty           ( testGroup, TestTree ) 
+import           Test.Tasty.Hedgehog  ( testProperty )
+import           Test.Tasty.HUnit     ( testCase, assertEqual )
+
+tests :: TestTree
+tests = testGroup "Data.Series.Generic.Definition" 
+    [ testMappend
+    , testPropMappendLikeMap
+    , testPropShow
+    , testFromStrictMap
+    , testToStrictMap
+    , testPropRoundtripConversionWithStrictMap
+    , testPropRoundtripConversionWithLazyMap
+    , testPropRoundtripConversionWithList
+    , testPropFromListDuplicatesNeverDrops
+    , testPropFromVectorDuplicatesNeverDrops
+    , testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder
+    , testPropRoundtripConversionWithVector
+    , testPropVectorVsList
+    , testFromLazyMap
+    , testToLazyMap
+    , testTakeWhile
+    , testDropWhile
+    , testFold
+    ]
+
+
+testMappend :: TestTree
+testMappend = testCase "(<>)" $ do
+    let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]
+        (s2 :: Series Vector Char Int) = fromList [('b', 10), ('x', 25)]
+        expectation = fromList [('a', 1), ('b', 5),  ('x', 25)]
+    
+    assertEqual mempty expectation (s1 <> s2)
+
+
+testPropMappendLikeMap :: TestTree
+testPropMappendLikeMap 
+    = testProperty "Mappend property similar to Data.Map.Strict" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000)   <*> Gen.alpha)
+        m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 500 1500) <*> Gen.alpha)
+
+        (fromStrictMap :: MS.Map Int Char -> Series Vector Int Char) (m1 <> m2) === fromStrictMap m1 <> fromStrictMap m2
+
+
+testPropShow :: TestTree
+testPropShow
+    = testProperty "Show is never too long" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000)   <*> Gen.alpha)
+
+        let (xs :: Series Vector Int Char) = fromStrictMap m1
+            ls = lines $ show xs
+        if Series.length xs > 6
+            then length ls === 2 + 6 + 1
+            else length ls === 2 + Series.length xs
+
+
+testFromStrictMap :: TestTree
+testFromStrictMap = testCase "fromStrictMap" $ do
+    -- Note the duplicate input at key 'a', which should disappear
+    let input = MS.fromList [('b', 2), ('a', 1), ('a', 1)]
+        (series :: Series Vector Char Int) = fromStrictMap input
+        expectation = fromList [('a', 1), ('b', 2)]
+    
+    assertEqual mempty series expectation
+
+
+testToStrictMap :: TestTree
+testToStrictMap = testCase "toStrictMap" $ do
+    let input = MS.fromList [('b', 2), ('a', 1)]
+        (series :: Series Vector Char Int) = fromStrictMap input
+    
+    assertEqual mempty (toStrictMap series) input
+
+
+testPropRoundtripConversionWithStrictMap :: TestTree
+testPropRoundtripConversionWithStrictMap 
+    = testProperty "Roundtrip property with Data.Map.Strict" $ property $ do
+        ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        tripping ms (fromStrictMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toStrictMap)
+
+
+testPropRoundtripConversionWithLazyMap :: TestTree
+testPropRoundtripConversionWithLazyMap 
+    = testProperty "Roundtrip property with Data.Map.Lazy" $ property $ do
+        ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        tripping (ML.fromDistinctAscList $ MS.toAscList ms) (fromLazyMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toLazyMap)
+
+
+testPropRoundtripConversionWithList :: TestTree
+testPropRoundtripConversionWithList 
+    = testProperty "Roundtrip property with List" $ property $ do
+        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
+
+        -- The property below needs some explanation.
+        -- In case of conflicting keys, a Series will be biased like a Map. Therefore,
+        -- the expected List won't have duplicated (hence the use of nubBy), but the elements which
+        -- are kept are in the order of `reverse xs`.
+        (toList :: Series Vector Int Char -> [(Int, Char)] ) (fromList xs) === sortOn fst (nubBy (\left right -> fst left == fst right) (reverse xs))
+
+
+testPropFromListDuplicatesNeverDrops :: TestTree
+testPropFromListDuplicatesNeverDrops
+    = testProperty "fromListDuplicates never drops elements" $ property $ do
+        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
+        Series.length (Series.fromListDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs
+
+
+testPropFromVectorDuplicatesNeverDrops :: TestTree
+testPropFromVectorDuplicatesNeverDrops
+    = testProperty "fromVectorDuplicates never drops elements" $ property $ do
+        xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
+        Series.length (Series.fromVectorDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs
+
+
+testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder :: TestTree
+testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder
+    = testProperty "fromVectorDuplicates and fromListDuplicates are equivalent" $ property $ do
+        xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)
+        Series.fromVectorDuplicates xs === Series.fromListDuplicates (Vector.toList xs)
+
+
+testPropRoundtripConversionWithVector :: TestTree
+testPropRoundtripConversionWithVector 
+    = testProperty "Roundtrip property with Vector" $ property $ do
+        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
+
+        let (srs :: Series Vector Int Char) = fromList xs
+        tripping srs toVector (Just . fromVector)
+
+
+testPropVectorVsList :: TestTree
+testPropVectorVsList 
+    = testProperty "building from a list or vector yields the same results" $ property $ do
+        xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)
+        -- Note that due to differences in sorting,
+        -- Series.fromList   and Series.fromVector . Vector.fromList 
+        -- are not equivalent if the input list contains duplicate keys.
+        let unique = nubBy ((==) `on` fst) xs 
+        (fromList unique :: Series Vector Int Char) === fromVector (Vector.fromList unique)
+
+
+testFromLazyMap :: TestTree
+testFromLazyMap = testCase "fromLazyMap" $ do
+    let input = ML.fromList [('b', 2), ('a', 1)]
+        (series :: Series Vector Char Int) = fromLazyMap input
+        expectation = fromList [('a', 1), ('b', 2)]
+    
+    assertEqual mempty series expectation
+
+
+testToLazyMap :: TestTree
+testToLazyMap = testCase "toLazyMap" $ do
+    let input = ML.fromList [('b', 2), ('a', 1)]
+        (series :: Series Vector Char Int) = fromLazyMap input
+    
+    assertEqual mempty (toLazyMap series) input
+
+
+testTakeWhile :: TestTree
+testTakeWhile = testProperty "takeWhile behaves like lists" $ property $ do
+    xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))
+    let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
+
+    n  <- forAll $ Gen.int  (Range.linear 1 10)
+    Series.takeWhile (\v -> v `mod` n == 0) ys === Series.fromList (takeWhile (\(_, v) -> v `mod` n == 0) $ Series.toList ys)
+
+
+testDropWhile :: TestTree
+testDropWhile = testProperty "dropWhile behaves like lists" $ property $ do
+    xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))
+    let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
+
+    n  <- forAll $ Gen.int  (Range.linear 1 10)
+    Series.dropWhile (\v -> v `mod` n /= 0) ys === Series.fromList (dropWhile (\(_, v) -> v `mod` n /= 0) $ Series.toList ys)
+
+
+testFold :: TestTree
+testFold = testGroup "fold"
+         [ testProperty "Series.sum and Control.Foldl.sum should be equivalent" $ property $ do
+            xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))
+            let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
+            Series.fold Fold.sum ys === Series.sum ys
+         , testProperty "FoldM Identity should be equivalent to a pure fold" $ property $ do
+            xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))
+            let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs
+            runIdentity (Series.foldM (Fold.generalize Fold.sum) ys) === Series.sum ys
          ]
diff --git a/test/Test/Data/Series/Generic/View.hs b/test/Test/Data/Series/Generic/View.hs
--- a/test/Test/Data/Series/Generic/View.hs
+++ b/test/Test/Data/Series/Generic/View.hs
@@ -1,143 +1,143 @@
-module Test.Data.Series.Generic.View (tests) where
-
-import qualified Data.Map.Strict      as MS
-import           Data.Series.Generic  ( Series, index, fromStrictMap, fromList, to, from, upto, select
-                                      , selectWhere, require, mapIndex, argmax, argmin, )
-import qualified Data.Series.Index    as Index
-import           Data.Vector          ( Vector )
-
-import           Hedgehog             ( property, forAll, (===), assert )
-import qualified Hedgehog.Gen         as Gen
-import qualified Hedgehog.Range       as Range
-
-import           Test.Tasty           ( testGroup, TestTree )
-import           Test.Tasty.Hedgehog  ( testProperty )
-import           Test.Tasty.HUnit     ( testCase, assertEqual )
-
-tests :: TestTree
-tests = testGroup "Data.Series.Generic.View" [ testSelectRange
-                                             , testSelectUnboundedRange
-                                             , testSelectUnboundedRangeEquivalence
-                                             , testSelectRangeEmptyRange
-                                             , testPropSelectRangeSubseries
-                                             , testSelectSet 
-                                             , testPropSelectSetSubseries
-                                             , testSelectWhere
-                                             , testPropRequire
-                                             , testMapIndex
-                                             , testArgmax
-                                             , testArgmin
-                                             ]
-
-
-testSelectRange :: TestTree
-testSelectRange = testCase "from ... to ..." $ do
-    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-        subSeries = series `select` ('b' `to` 'd')
-        expectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4)]
-    assertEqual mempty expectation subSeries
-
-
-testSelectUnboundedRange :: TestTree
-testSelectUnboundedRange = testCase "from and upto" $ do
-    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-        openLeftsubSeries = series `select` from 'b'
-        openLeftExpectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-    assertEqual mempty openLeftExpectation openLeftsubSeries
-
-    let openRightsubSeries = series `select` upto 'b'
-        openRightExpectation = fromStrictMap $ MS.fromList [('a', 1), ('b', 2)]
-    assertEqual mempty openRightExpectation openRightsubSeries
-
-
-testSelectUnboundedRangeEquivalence :: TestTree
-testSelectUnboundedRangeEquivalence 
-    = testProperty "Combining unbounded ranges is equivalent to a bounded range" 
-    $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        (b1, b2) <- (,) <$> forAll Gen.alpha <*> forAll Gen.alpha
-        let start = min b1 b2
-            end = max b1 b2
-            (xs :: Series Vector Char Int) = fromStrictMap m1
-
-        (xs `select` start `to` end) === ( (xs `select` from start) `select` upto end)
-
-
-testPropSelectRangeSubseries :: TestTree
-testPropSelectRangeSubseries = testProperty "xs `select` <x> `to` <y> always returns a proper subseries" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        start <- forAll Gen.alpha
-        end   <- forAll Gen.alpha
-        let (xs :: Series Vector Char Int) = fromStrictMap m1
-            ys = xs `select` start `to` end
-        
-        assert $ index xs `Index.contains` index ys
-
-
-testSelectRangeEmptyRange :: TestTree
-testSelectRangeEmptyRange = testCase "from ... to ... on an empty `Range``" $ do
-    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-        subSeries = series `select` ('f' `to` 'z')
-    assertEqual mempty mempty subSeries
-
-
-testSelectSet :: TestTree
-testSelectSet = testCase "select" $ do
-    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-        subSeries = series `select` Index.fromList ['a', 'd', 'x']
-        expectation = fromStrictMap $ MS.fromList [('a', 1), ('d', 4)]
-    
-    assertEqual mempty expectation subSeries
-
-
-testPropSelectSetSubseries :: TestTree
-testPropSelectSetSubseries = testProperty "xs `select` <some set> always returns a proper subseries" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        selection <- forAll $ Gen.set (Range.linear 0 10) Gen.alpha
-        let (xs :: Series Vector Char Int) = fromStrictMap m1
-            ys = xs `select` selection
-        
-        assert $ index xs `Index.contains` index ys
-
-
-testSelectWhere :: TestTree
-testSelectWhere = testCase "selectWhere" $ do
-    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
-        subSeries = series `selectWhere` fmap (>3) series 
-        expectation = fromStrictMap $ MS.fromList [('d', 4), ('e', 5)]
-    
-    assertEqual mempty expectation subSeries
-
-
-testPropRequire :: TestTree
-testPropRequire = testProperty "require always returns a series with the expected index" $ property $ do
-    m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.int (Range.linear 0 1000))
-    ss <- forAll $ Gen.set (Range.linear 0 100) (Gen.int (Range.linear (-100) 100))
-    
-    let (xs :: Series Vector Int Int) = fromStrictMap m1
-        ix = Index.fromSet ss
-    index (xs `require` ix) === ix 
-
-
-testMapIndex :: TestTree
-testMapIndex = testCase "mapIndex" $ do
-    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 3), ("bc", 4), ("c", 5)]
-        subSeries = series `mapIndex` take 1
-        expectation = fromList [("a", 1), ("b", 3), ("c", 5)]
-    
-    assertEqual mempty expectation subSeries
-
-
-testArgmax :: TestTree
-testArgmax = testCase "argmax" $ do
-    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 10), ("bc", 4), ("c", 5)]
-        expectation = Just "bb"
-    
-    assertEqual mempty expectation (argmax series)
-
-testArgmin :: TestTree
-testArgmin = testCase "argmin" $ do
-    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", -10), ("bc", 4), ("c", 5)]
-        expectation = Just "bb"
-    
+module Test.Data.Series.Generic.View (tests) where
+
+import qualified Data.Map.Strict      as MS
+import           Data.Series.Generic  ( Series, index, fromStrictMap, fromList, to, from, upto, select
+                                      , selectWhere, require, mapIndex, argmax, argmin, )
+import qualified Data.Series.Index    as Index
+import           Data.Vector          ( Vector )
+
+import           Hedgehog             ( property, forAll, (===), assert )
+import qualified Hedgehog.Gen         as Gen
+import qualified Hedgehog.Range       as Range
+
+import           Test.Tasty           ( testGroup, TestTree )
+import           Test.Tasty.Hedgehog  ( testProperty )
+import           Test.Tasty.HUnit     ( testCase, assertEqual )
+
+tests :: TestTree
+tests = testGroup "Data.Series.Generic.View" [ testSelectRange
+                                             , testSelectUnboundedRange
+                                             , testSelectUnboundedRangeEquivalence
+                                             , testSelectRangeEmptyRange
+                                             , testPropSelectRangeSubseries
+                                             , testSelectSet 
+                                             , testPropSelectSetSubseries
+                                             , testSelectWhere
+                                             , testPropRequire
+                                             , testMapIndex
+                                             , testArgmax
+                                             , testArgmin
+                                             ]
+
+
+testSelectRange :: TestTree
+testSelectRange = testCase "from ... to ..." $ do
+    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        subSeries = series `select` ('b' `to` 'd')
+        expectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4)]
+    assertEqual mempty expectation subSeries
+
+
+testSelectUnboundedRange :: TestTree
+testSelectUnboundedRange = testCase "from and upto" $ do
+    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        openLeftsubSeries = series `select` from 'b'
+        openLeftExpectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+    assertEqual mempty openLeftExpectation openLeftsubSeries
+
+    let openRightsubSeries = series `select` upto 'b'
+        openRightExpectation = fromStrictMap $ MS.fromList [('a', 1), ('b', 2)]
+    assertEqual mempty openRightExpectation openRightsubSeries
+
+
+testSelectUnboundedRangeEquivalence :: TestTree
+testSelectUnboundedRangeEquivalence 
+    = testProperty "Combining unbounded ranges is equivalent to a bounded range" 
+    $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        (b1, b2) <- (,) <$> forAll Gen.alpha <*> forAll Gen.alpha
+        let start = min b1 b2
+            end = max b1 b2
+            (xs :: Series Vector Char Int) = fromStrictMap m1
+
+        (xs `select` start `to` end) === ( (xs `select` from start) `select` upto end)
+
+
+testPropSelectRangeSubseries :: TestTree
+testPropSelectRangeSubseries = testProperty "xs `select` <x> `to` <y> always returns a proper subseries" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        start <- forAll Gen.alpha
+        end   <- forAll Gen.alpha
+        let (xs :: Series Vector Char Int) = fromStrictMap m1
+            ys = xs `select` start `to` end
+        
+        assert $ index xs `Index.contains` index ys
+
+
+testSelectRangeEmptyRange :: TestTree
+testSelectRangeEmptyRange = testCase "from ... to ... on an empty `Range``" $ do
+    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        subSeries = series `select` ('f' `to` 'z')
+    assertEqual mempty mempty subSeries
+
+
+testSelectSet :: TestTree
+testSelectSet = testCase "select" $ do
+    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        subSeries = series `select` Index.fromList ['a', 'd', 'x']
+        expectation = fromStrictMap $ MS.fromList [('a', 1), ('d', 4)]
+    
+    assertEqual mempty expectation subSeries
+
+
+testPropSelectSetSubseries :: TestTree
+testPropSelectSetSubseries = testProperty "xs `select` <some set> always returns a proper subseries" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        selection <- forAll $ Gen.set (Range.linear 0 10) Gen.alpha
+        let (xs :: Series Vector Char Int) = fromStrictMap m1
+            ys = xs `select` selection
+        
+        assert $ index xs `Index.contains` index ys
+
+
+testSelectWhere :: TestTree
+testSelectWhere = testCase "selectWhere" $ do
+    let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        subSeries = series `selectWhere` fmap (>3) series 
+        expectation = fromStrictMap $ MS.fromList [('d', 4), ('e', 5)]
+    
+    assertEqual mempty expectation subSeries
+
+
+testPropRequire :: TestTree
+testPropRequire = testProperty "require always returns a series with the expected index" $ property $ do
+    m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.int (Range.linear 0 1000))
+    ss <- forAll $ Gen.set (Range.linear 0 100) (Gen.int (Range.linear (-100) 100))
+    
+    let (xs :: Series Vector Int Int) = fromStrictMap m1
+        ix = Index.fromSet ss
+    index (xs `require` ix) === ix 
+
+
+testMapIndex :: TestTree
+testMapIndex = testCase "mapIndex" $ do
+    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 3), ("bc", 4), ("c", 5)]
+        subSeries = series `mapIndex` take 1
+        expectation = fromList [("a", 1), ("b", 3), ("c", 5)]
+    
+    assertEqual mempty expectation subSeries
+
+
+testArgmax :: TestTree
+testArgmax = testCase "argmax" $ do
+    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 10), ("bc", 4), ("c", 5)]
+        expectation = Just "bb"
+    
+    assertEqual mempty expectation (argmax series)
+
+testArgmin :: TestTree
+testArgmin = testCase "argmin" $ do
+    let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", -10), ("bc", 4), ("c", 5)]
+        expectation = Just "bb"
+    
     assertEqual mempty expectation (argmin series)
diff --git a/test/Test/Data/Series/Generic/Zip.hs b/test/Test/Data/Series/Generic/Zip.hs
--- a/test/Test/Data/Series/Generic/Zip.hs
+++ b/test/Test/Data/Series/Generic/Zip.hs
@@ -1,147 +1,147 @@
-
-module Test.Data.Series.Generic.Zip ( tests ) where
-
-
-import           Control.Monad        ( forM_ )
-
-import           Data.Maybe           ( fromJust, isNothing )
-import           Data.Monoid          ( Sum(..) )
-import           Data.Series.Generic  ( Series(index), mapStrategy
-                                      , fromStrictMap, fromList, zipWith, select, at, replace, (|->), (<-|)
-                                      )
-import qualified Data.Series.Generic  as Series
-import qualified Data.Series.Index    as Index 
-import           Data.Vector          ( Vector )
-
-import           Hedgehog             ( property, forAll, (===), assert )
-import qualified Hedgehog.Gen         as Gen
-import qualified Hedgehog.Range       as Range
-
-import           Prelude              hiding ( zipWith )
-
-import           Test.Tasty           ( testGroup, TestTree ) 
-import           Test.Tasty.Hedgehog  ( testProperty )
-import           Test.Tasty.HUnit     ( testCase, assertEqual )
-
-tests :: TestTree
-tests = testGroup "Data.Series.Generic.Zip" [ testZipWith
-                                                  , testPropZipWithMatched
-                                                  , testPropZipWithMatchedAndZipWithMonoid
-                                                  , testPropZipWith
-                                                  , testPropReplace
-                                                  , testPropReplaceInfix
-                                                  , testPropZipWithStrategySkipStrategy
-                                                  , testMapStrategy
-                                                  ]
-
-
-testZipWith :: TestTree
-testZipWith = testCase "zipWith" $ do
-    let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]
-        (s2 :: Series Vector Char Int) = fromList [('x', 25), ('b', 10)]
-        expectation = fromList [('a', Nothing), ('b', Just 15),  ('x', Nothing)]
-    
-    assertEqual mempty expectation (zipWith (+) s1 s2)
-
-
-testPropZipWithMatched :: TestTree
-testPropZipWithMatched 
-    = testProperty "zipWith when keys all match" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        let (xs :: Series Vector Char Int) = fromStrictMap m1
-        zipWith (+) xs xs === fmap (Just . (*2)) xs
-
-
-testPropZipWithMatchedAndZipWithMonoid :: TestTree
-testPropZipWithMatchedAndZipWithMonoid 
-    = testProperty "zipWithMonoid and zipWithStrategy give compatible results" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        let (xs :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m1
-            (ys :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m2
-
-            expectation = Series.zipWithStrategy (<>) (mapStrategy id) (mapStrategy id) xs ys
-        
-        expectation === Series.zipWithMonoid (<>) xs ys
-
-
-
-testPropZipWith :: TestTree
-testPropZipWith 
-    = testProperty "zipWith when keys all match" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        let (x1 :: Series Vector String Int) = fromStrictMap m1
-            x2 = fromStrictMap m2
-            common  = index x1 `Index.intersection` index x2
-            symdiff = (index x1 `Index.union` index x2) `Index.difference` common
-            comb = zipWith (+) x1 x2
-
-        forM_ common $ \k -> do
-            let left  = fromJust $ x1 `at` k
-                right = fromJust $ x2 `at` k
-            fromJust (comb `at` k) === Just (left + right)
-        
-        assert $ all isNothing $ Series.values (comb `select` symdiff)
-
-
-testPropReplace :: TestTree
-testPropReplace 
-    = testProperty "replace" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) 
-        ns <- forAll $ Gen.list (Range.linear 0 10)   (Gen.int $ Range.linear (-500) 500) 
-        ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)
-        let (xs :: Series Vector Int Int) = fromList (zip ixs ms)
-            ys = fromList (zip [0..] ns)
-            rs = ys `replace` xs
-
-        index rs === index xs
-
-        let commonKeys = index xs `Index.intersection` index ys
-
-        (rs `select` commonKeys) === (ys `select` commonKeys)
-
-
-testPropReplaceInfix :: TestTree
-testPropReplaceInfix 
-    = testProperty "(|->) and (<-|)" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) 
-        ns <- forAll $ Gen.list (Range.linear 0 10)   (Gen.int $ Range.linear (-500) 500) 
-        ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)
-        let (xs :: Series Vector Int Int) = fromList (zip ixs ms)
-            ys = fromList (zip [0..] ns)
-            rs = ys `replace` xs
-        
-        ys |-> xs === rs 
-        ys |-> xs === xs <-| ys 
-
-
-testPropZipWithStrategySkipStrategy :: TestTree
-testPropZipWithStrategySkipStrategy 
-    = testProperty "zipWithStrategy f skipStrategy skipStrategy is equivalent to zipWithMatched" $ property $ do
-        m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
-        m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
-
-        let (xs :: Series Vector String Int) = fromStrictMap m1
-            ys = fromStrictMap m2
-
-            expectation = Series.zipWithMatched (+) xs ys
-        
-        expectation === Series.zipWithStrategy (+) Series.skipStrategy Series.skipStrategy xs ys
-
-
-testMapStrategy :: TestTree
-testMapStrategy 
-    = testCase "mapStrategy works as expected" $ do
-        let (xs :: Series Vector Int Int) = Series.fromList $ zip [0..] [1,2,3,4,5]
-            ys =                            Series.fromList $ zip [3..]       [3,4,5]
-        
-            expected = Series.fromList [ (0, 1+1)
-                                       , (1, 2+1)
-                                       , (2, 3+1)
-                                       , (3, 4+3)
-                                       , (4, 5+4)
-                                       , (5, 5*2)
-                                       ]
-
-        assertEqual mempty expected $ Series.zipWithStrategy (+) (mapStrategy (+1)) (mapStrategy (*2)) xs ys
+
+module Test.Data.Series.Generic.Zip ( tests ) where
+
+
+import           Control.Monad        ( forM_ )
+
+import           Data.Maybe           ( fromJust, isNothing )
+import           Data.Monoid          ( Sum(..) )
+import           Data.Series.Generic  ( Series(index), mapStrategy
+                                      , fromStrictMap, fromList, zipWith, select, at, replace, (|->), (<-|)
+                                      )
+import qualified Data.Series.Generic  as Series
+import qualified Data.Series.Index    as Index 
+import           Data.Vector          ( Vector )
+
+import           Hedgehog             ( property, forAll, (===), assert )
+import qualified Hedgehog.Gen         as Gen
+import qualified Hedgehog.Range       as Range
+
+import           Prelude              hiding ( zipWith )
+
+import           Test.Tasty           ( testGroup, TestTree ) 
+import           Test.Tasty.Hedgehog  ( testProperty )
+import           Test.Tasty.HUnit     ( testCase, assertEqual )
+
+tests :: TestTree
+tests = testGroup "Data.Series.Generic.Zip" [ testZipWith
+                                                  , testPropZipWithMatched
+                                                  , testPropZipWithMatchedAndZipWithMonoid
+                                                  , testPropZipWith
+                                                  , testPropReplace
+                                                  , testPropReplaceInfix
+                                                  , testPropZipWithStrategySkipStrategy
+                                                  , testMapStrategy
+                                                  ]
+
+
+testZipWith :: TestTree
+testZipWith = testCase "zipWith" $ do
+    let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]
+        (s2 :: Series Vector Char Int) = fromList [('x', 25), ('b', 10)]
+        expectation = fromList [('a', Nothing), ('b', Just 15),  ('x', Nothing)]
+    
+    assertEqual mempty expectation (zipWith (+) s1 s2)
+
+
+testPropZipWithMatched :: TestTree
+testPropZipWithMatched 
+    = testProperty "zipWith when keys all match" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        let (xs :: Series Vector Char Int) = fromStrictMap m1
+        zipWith (+) xs xs === fmap (Just . (*2)) xs
+
+
+testPropZipWithMatchedAndZipWithMonoid :: TestTree
+testPropZipWithMatchedAndZipWithMonoid 
+    = testProperty "zipWithMonoid and zipWithStrategy give compatible results" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        let (xs :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m1
+            (ys :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m2
+
+            expectation = Series.zipWithStrategy (<>) (mapStrategy id) (mapStrategy id) xs ys
+        
+        expectation === Series.zipWithMonoid (<>) xs ys
+
+
+
+testPropZipWith :: TestTree
+testPropZipWith 
+    = testProperty "zipWith when keys all match" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        let (x1 :: Series Vector String Int) = fromStrictMap m1
+            x2 = fromStrictMap m2
+            common  = index x1 `Index.intersection` index x2
+            symdiff = (index x1 `Index.union` index x2) `Index.difference` common
+            comb = zipWith (+) x1 x2
+
+        forM_ common $ \k -> do
+            let left  = fromJust $ x1 `at` k
+                right = fromJust $ x2 `at` k
+            fromJust (comb `at` k) === Just (left + right)
+        
+        assert $ all isNothing $ Series.values (comb `select` symdiff)
+
+
+testPropReplace :: TestTree
+testPropReplace 
+    = testProperty "replace" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) 
+        ns <- forAll $ Gen.list (Range.linear 0 10)   (Gen.int $ Range.linear (-500) 500) 
+        ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)
+        let (xs :: Series Vector Int Int) = fromList (zip ixs ms)
+            ys = fromList (zip [0..] ns)
+            rs = ys `replace` xs
+
+        index rs === index xs
+
+        let commonKeys = index xs `Index.intersection` index ys
+
+        (rs `select` commonKeys) === (ys `select` commonKeys)
+
+
+testPropReplaceInfix :: TestTree
+testPropReplaceInfix 
+    = testProperty "(|->) and (<-|)" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) 
+        ns <- forAll $ Gen.list (Range.linear 0 10)   (Gen.int $ Range.linear (-500) 500) 
+        ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)
+        let (xs :: Series Vector Int Int) = fromList (zip ixs ms)
+            ys = fromList (zip [0..] ns)
+            rs = ys `replace` xs
+        
+        ys |-> xs === rs 
+        ys |-> xs === xs <-| ys 
+
+
+testPropZipWithStrategySkipStrategy :: TestTree
+testPropZipWithStrategySkipStrategy 
+    = testProperty "zipWithStrategy f skipStrategy skipStrategy is equivalent to zipWithMatched" $ property $ do
+        m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
+        m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))
+
+        let (xs :: Series Vector String Int) = fromStrictMap m1
+            ys = fromStrictMap m2
+
+            expectation = Series.zipWithMatched (+) xs ys
+        
+        expectation === Series.zipWithStrategy (+) Series.skipStrategy Series.skipStrategy xs ys
+
+
+testMapStrategy :: TestTree
+testMapStrategy 
+    = testCase "mapStrategy works as expected" $ do
+        let (xs :: Series Vector Int Int) = Series.fromList $ zip [0..] [1,2,3,4,5]
+            ys =                            Series.fromList $ zip [3..]       [3,4,5]
+        
+            expected = Series.fromList [ (0, 1+1)
+                                       , (1, 2+1)
+                                       , (2, 3+1)
+                                       , (3, 4+3)
+                                       , (4, 5+4)
+                                       , (5, 5*2)
+                                       ]
+
+        assertEqual mempty expected $ Series.zipWithStrategy (+) (mapStrategy (+1)) (mapStrategy (*2)) xs ys
diff --git a/test/Test/Data/Series/Index.hs b/test/Test/Data/Series/Index.hs
--- a/test/Test/Data/Series/Index.hs
+++ b/test/Test/Data/Series/Index.hs
@@ -1,123 +1,123 @@
-
-module Test.Data.Series.Index (tests) where
-
-import qualified Data.Series.Index    as Index
-import qualified Data.Series.Index.Internal as Index.Internal
-import qualified Data.Set             as Set
-import qualified Data.Vector          as Vector
-
-import           Hedgehog             ( property, forAll, tripping, assert, (===) )
-import qualified Hedgehog.Gen         as Gen
-import qualified Hedgehog.Range       as Range
-
-
-import           Test.Tasty           ( testGroup, TestTree ) 
-import           Test.Tasty.Hedgehog  ( testProperty )
-
-
-tests :: TestTree
-tests = testGroup "Data.Series.Index" [ testPropRange
-                                      , testPropFromToSet
-                                      , testPropFromToList
-                                      , testPropFromToAscList
-                                      , testPropFromToVector
-                                      , testPropFromToAscVector
-                                      , testPropMemberNotMember
-                                      , testPropIndexed
-                                      , testPropFilter
-                                      ]
-
-
-testPropRange :: TestTree
-testPropRange = testProperty "range always includes the start, and all elements less than/equal to end" $ property $ do
-    start <- forAll $ Gen.int (Range.linear 0 50)
-    end   <- forAll $ Gen.int (Range.linear 51 100)
-    step  <- forAll $ Gen.int (Range.linear 1 5)
-
-    let ix = Index.range (+step) start end 
-
-    assert $ start `Index.member` ix
-    assert $ maximum ix <= end
-
-    if (end - start) `mod` step == 0
-        then assert (end `Index.member` ix)
-        else assert (end `Index.notMember` ix)
-
-
-testPropFromToSet :: TestTree
-testPropFromToSet = testGroup "conversion to/from Set" 
-    [ testProperty "fromSet / toSet" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        tripping (Set.fromList ms) Index.fromSet (Just . Index.toSet)
-    , testProperty "toIndex / fromIndex" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        tripping (Set.fromList ms) (Index.toIndex :: Set.Set (Char, Char) -> Index.Index (Char, Char)) (Just . Index.fromIndex)
-    ]
-
-
-testPropFromToList :: TestTree
-testPropFromToList = testGroup "conversion to/from list" 
-    [ testProperty "fromList / toAscList" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        let index = Index.fromList ms
-        tripping index (reverse . Index.toAscList) (Just . Index.fromList)
-    , testProperty "toIndex / fromIndex" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        let index = Index.toIndex ms :: Index.Index (Char, Char)
-        tripping index (reverse . Index.fromIndex) (Just . (Index.toIndex :: [(Char, Char)] -> Index.Index (Char, Char)))
-    ]
-
-
-testPropFromToAscList :: TestTree
-testPropFromToAscList = testProperty "fromAscList / toAscList" $ property $ do
-    ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-    let index = Index.fromList ms
-    tripping index Index.toAscList (Just . Index.Internal.fromAscList)
-
-
-testPropFromToVector :: TestTree
-testPropFromToVector = testGroup "conversion to/from Vector"
-    [ testProperty "fromVector / toAscVector" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        let index = Index.fromList ms
-        tripping index (Vector.reverse . Index.toAscVector) (Just . Index.fromVector)
-    , testProperty "toIndex / fromIndex" $ property $ do
-        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-        let index = Index.toIndex ms :: Index.Index (Char, Char)
-        tripping index (Vector.reverse . Index.fromIndex) (Just . (Index.toIndex :: Vector.Vector (Char, Char) -> Index.Index (Char, Char)))
-    ]
-
-
-testPropFromToAscVector :: TestTree
-testPropFromToAscVector = testProperty "fromAscVector / toAscVector" $ property $ do
-    ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
-    let index = Index.fromList ms
-    tripping index (Index.toAscVector :: Index.Index (Char, Char) -> Vector.Vector (Char, Char)) (Just . Index.Internal.fromAscVector)
-
-
-testPropMemberNotMember :: TestTree
-testPropMemberNotMember = testProperty "elements are either a member or not a member of the index" $ property $ do
-    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
-    k  <- forAll $ Gen.int (Range.linear (-100) 100)
-
-    let ix = Index.fromList ms
-    assert $ (k `Index.member` ix) /= (k `Index.notMember` ix)
-
-
-testPropIndexed :: TestTree
-testPropIndexed = testProperty "indexed works just like for Vectors" $ property $ do
-    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
-
-    let ix = Index.fromList ms
-    
-    Index.toAscVector (Index.indexed ix) === Vector.indexed (Index.toAscVector ix)
-
-
-testPropFilter :: TestTree
-testPropFilter = testProperty "filter works just like for Sets" $ property $ do
-    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
-
-    let ss = Set.fromList ms
-        ix = Index.fromSet ss
-    
-    Index.fromSet (Set.filter even ss) === Index.filter even ix
+
+module Test.Data.Series.Index (tests) where
+
+import qualified Data.Series.Index    as Index
+import qualified Data.Series.Index.Internal as Index.Internal
+import qualified Data.Set             as Set
+import qualified Data.Vector          as Vector
+
+import           Hedgehog             ( property, forAll, tripping, assert, (===) )
+import qualified Hedgehog.Gen         as Gen
+import qualified Hedgehog.Range       as Range
+
+
+import           Test.Tasty           ( testGroup, TestTree ) 
+import           Test.Tasty.Hedgehog  ( testProperty )
+
+
+tests :: TestTree
+tests = testGroup "Data.Series.Index" [ testPropRange
+                                      , testPropFromToSet
+                                      , testPropFromToList
+                                      , testPropFromToAscList
+                                      , testPropFromToVector
+                                      , testPropFromToAscVector
+                                      , testPropMemberNotMember
+                                      , testPropIndexed
+                                      , testPropFilter
+                                      ]
+
+
+testPropRange :: TestTree
+testPropRange = testProperty "range always includes the start, and all elements less than/equal to end" $ property $ do
+    start <- forAll $ Gen.int (Range.linear 0 50)
+    end   <- forAll $ Gen.int (Range.linear 51 100)
+    step  <- forAll $ Gen.int (Range.linear 1 5)
+
+    let ix = Index.range (+step) start end 
+
+    assert $ start `Index.member` ix
+    assert $ maximum ix <= end
+
+    if (end - start) `mod` step == 0
+        then assert (end `Index.member` ix)
+        else assert (end `Index.notMember` ix)
+
+
+testPropFromToSet :: TestTree
+testPropFromToSet = testGroup "conversion to/from Set" 
+    [ testProperty "fromSet / toSet" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        tripping (Set.fromList ms) Index.fromSet (Just . Index.toSet)
+    , testProperty "toIndex / fromIndex" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        tripping (Set.fromList ms) (Index.toIndex :: Set.Set (Char, Char) -> Index.Index (Char, Char)) (Just . Index.fromIndex)
+    ]
+
+
+testPropFromToList :: TestTree
+testPropFromToList = testGroup "conversion to/from list" 
+    [ testProperty "fromList / toAscList" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        let index = Index.fromList ms
+        tripping index (reverse . Index.toAscList) (Just . Index.fromList)
+    , testProperty "toIndex / fromIndex" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        let index = Index.toIndex ms :: Index.Index (Char, Char)
+        tripping index (reverse . Index.fromIndex) (Just . (Index.toIndex :: [(Char, Char)] -> Index.Index (Char, Char)))
+    ]
+
+
+testPropFromToAscList :: TestTree
+testPropFromToAscList = testProperty "fromAscList / toAscList" $ property $ do
+    ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+    let index = Index.fromList ms
+    tripping index Index.toAscList (Just . Index.Internal.fromAscList)
+
+
+testPropFromToVector :: TestTree
+testPropFromToVector = testGroup "conversion to/from Vector"
+    [ testProperty "fromVector / toAscVector" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        let index = Index.fromList ms
+        tripping index (Vector.reverse . Index.toAscVector) (Just . Index.fromVector)
+    , testProperty "toIndex / fromIndex" $ property $ do
+        ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+        let index = Index.toIndex ms :: Index.Index (Char, Char)
+        tripping index (Vector.reverse . Index.fromIndex) (Just . (Index.toIndex :: Vector.Vector (Char, Char) -> Index.Index (Char, Char)))
+    ]
+
+
+testPropFromToAscVector :: TestTree
+testPropFromToAscVector = testProperty "fromAscVector / toAscVector" $ property $ do
+    ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)
+    let index = Index.fromList ms
+    tripping index (Index.toAscVector :: Index.Index (Char, Char) -> Vector.Vector (Char, Char)) (Just . Index.Internal.fromAscVector)
+
+
+testPropMemberNotMember :: TestTree
+testPropMemberNotMember = testProperty "elements are either a member or not a member of the index" $ property $ do
+    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
+    k  <- forAll $ Gen.int (Range.linear (-100) 100)
+
+    let ix = Index.fromList ms
+    assert $ (k `Index.member` ix) /= (k `Index.notMember` ix)
+
+
+testPropIndexed :: TestTree
+testPropIndexed = testProperty "indexed works just like for Vectors" $ property $ do
+    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
+
+    let ix = Index.fromList ms
+    
+    Index.toAscVector (Index.indexed ix) === Vector.indexed (Index.toAscVector ix)
+
+
+testPropFilter :: TestTree
+testPropFilter = testProperty "filter works just like for Sets" $ property $ do
+    ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))
+
+    let ss = Set.fromList ms
+        ix = Index.fromSet ss
+    
+    Index.fromSet (Set.filter even ss) === Index.filter even ix
