javelin 0.1.1.0 → 0.1.2.0
raw patch · 26 files changed
+7617/−7586 lines, 26 filesdep ~basedep ~deepseqPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base, deepseq
API changes (from Hackage documentation)
Files
- CHANGELOG.md +14/−10
- LICENSE +20/−20
- benchmarks/Comparison.hs +200/−200
- benchmarks/Operations.hs +69/−69
- javelin.cabal +136/−136
- scripts/bench-report.hs +99/−99
- src/Data/Series.hs +1360/−1360
- src/Data/Series/Generic.hs +98/−98
- src/Data/Series/Generic/Aggregation.hs +331/−325
- src/Data/Series/Generic/Definition.hs +832/−832
- src/Data/Series/Generic/Internal.hs +26/−26
- src/Data/Series/Generic/Scans.hs +112/−112
- src/Data/Series/Generic/View.hs +336/−336
- src/Data/Series/Generic/Zip.hs +463/−463
- src/Data/Series/Index.hs +108/−108
- src/Data/Series/Index/Definition.hs +519/−517
- src/Data/Series/Index/Internal.hs +39/−39
- src/Data/Series/Tutorial.hs +770/−770
- src/Data/Series/Unboxed.hs +1291/−1291
- test/Main.hs +19/−19
- test/Test/Data/Series.hs +6/−6
- test/Test/Data/Series/Generic/Aggregation.hs +152/−133
- test/Test/Data/Series/Generic/Definition.hs +205/−205
- test/Test/Data/Series/Generic/View.hs +142/−142
- test/Test/Data/Series/Generic/Zip.hs +147/−147
- test/Test/Data/Series/Index.hs +123/−123
CHANGELOG.md view
@@ -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.
LICENSE view
@@ -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.
benchmarks/Comparison.hs view
@@ -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 ]
benchmarks/Operations.hs view
@@ -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` (+))
javelin.cabal view
@@ -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
scripts/bench-report.hs view
@@ -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)
src/Data/Series.hs view
@@ -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
src/Data/Series/Generic.hs view
@@ -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+ )
src/Data/Series/Generic/Aggregation.hs view
@@ -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
src/Data/Series/Generic/Definition.hs view
@@ -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
src/Data/Series/Generic/Internal.hs view
@@ -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 )
src/Data/Series/Generic/Scans.hs view
@@ -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
src/Data/Series/Generic/View.hs view
@@ -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+ }++
src/Data/Series/Generic/Zip.hs view
@@ -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 #-}
src/Data/Series/Index.hs view
@@ -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 )+
src/Data/Series/Index/Definition.hs view
@@ -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 #-}
src/Data/Series/Index/Internal.hs view
@@ -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+ )
src/Data/Series/Tutorial.hs view
@@ -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' | | | |++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+++-}
src/Data/Series/Unboxed.hs view
@@ -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
test/Main.hs view
@@ -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+ ]
test/Test/Data/Series.hs view
@@ -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" []
test/Test/Data/Series/Generic/Aggregation.hs view
@@ -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
test/Test/Data/Series/Generic/Definition.hs view
@@ -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 ]
test/Test/Data/Series/Generic/View.hs view
@@ -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)
test/Test/Data/Series/Generic/Zip.hs view
@@ -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
test/Test/Data/Series/Index.hs view
@@ -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