javelin 0.1.3.1 → 0.1.4.0
raw patch · 12 files changed
+214/−41 lines, 12 filesdep ~basedep ~deepseqPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base, deepseq
API changes (from Hackage documentation)
+ Data.Series: toSeriesDuplicates :: IsSeries t v k a => t -> Series v (k, Occurrence) a
+ Data.Series.Generic: toSeriesDuplicates :: IsSeries t v k a => t -> Series v (k, Occurrence) a
+ Data.Series.Unboxed: toSeriesDuplicates :: IsSeries t v k a => t -> Series v (k, Occurrence) a
Files
- CHANGELOG.md +6/−0
- javelin.cabal +13/−9
- src/Data/Series/Generic.hs +1/−1
- src/Data/Series/Generic/Definition.hs +85/−3
- src/Data/Series/Generic/Internal.hs +1/−1
- src/Data/Series/Generic/View.hs +1/−1
- src/Data/Series/Generic/Zip.hs +46/−1
- src/Data/Series/Index/Definition.hs +13/−3
- src/Data/Series/Index/Internal.hs +2/−2
- src/Data/Series/Tutorial.hs +29/−19
- src/Data/Series/Unboxed.hs +1/−1
- test/Test/Data/Series/Generic/Definition.hs +16/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for javelin +## Release 0.1.4.0++* Added the `toSeriesDuplicates` method to the `IsSeries` typeclass, to more easily convert to a series+ from a container that may contain duplicates.+* Added `IsSeries` instance for `Series`.+ ## Release 0.1.3.1 * Improved performance for the `Foldable` methods that use `Data.Foldable.foldMap'` under the hood.
javelin.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: javelin-version: 0.1.3.1+version: 0.1.4.0 synopsis: Labeled one-dimensional arrays license: MIT license-file: LICENSE@@ -10,7 +10,8 @@ build-type: Simple extra-doc-files: CHANGELOG.md files/aapl.txt-tested-with: GHC ==9.8.2 +tested-with: GHC ==9.10.1+ || ==9.8.4 || ==9.6.4 || ==9.4.8 description:@@ -29,8 +30,11 @@ ["Data.Series.Index"] Index containing series keys. To get started, please take a look at the tutorial ("Data.Series.Tutorial").- +source-repository head+ type: git+ location: https://github.com/LaurentRDC/javelin+ common common default-language: GHC2021 ghc-options: -Wall@@ -58,9 +62,9 @@ Data.Series.Generic.View Data.Series.Generic.Zip Data.Series.Index.Definition- build-depends: base >=4.15.0.0 && <4.20,+ build-depends: base >=4.15.0.0 && <4.22, containers >=0.6 && <0.8,- deepseq >=1.4 && <1.6,+ deepseq >=1.4 && <1.7, foldl ^>=1.4, indexed-traversable ^>=0.1, vector >=0.12.3.0 && <0.14,@@ -77,7 +81,7 @@ Test.Data.Series.Generic.Definition Test.Data.Series.Generic.View Test.Data.Series.Generic.Zip- build-depends: base>=4.15.0.0 && <4.20,+ build-depends: base >=4.15.0.0 && <4.22, containers, foldl, hedgehog,@@ -99,7 +103,7 @@ ghc-options: -rtsopts hs-source-dirs: benchmarks main-is: Comparison.hs- build-depends: base>=4.15.0.0 && <4.20,+ build-depends: base >=4.15.0.0 && <4.22, containers, foldl, mono-traversable,@@ -120,7 +124,7 @@ ghc-options: -rtsopts hs-source-dirs: benchmarks main-is: Operations.hs- build-depends: base>=4.15.0.0 && <4.20,+ build-depends: base >=4.15.0.0 && <4.22, containers, deepseq, foldl,@@ -132,5 +136,5 @@ import: common main-is: bench-report.hs hs-source-dirs: scripts- build-depends: base>=4.15.0.0 && <4.20, + build-depends: base >=4.15.0.0 && <4.22, csv ^>=0.1
src/Data/Series/Generic.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE NoImplicitPrelude #-} ----------------------------------------------------------------------------- -- |--- Module : Data.Series.Generic+-- Module : $header -- Copyright : (c) Laurent P. René de Cotret -- License : MIT -- Maintainer : laurent.decotret@outlook.com
src/Data/Series/Generic/Definition.hs view
@@ -131,9 +131,17 @@ 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'.+ -- need to handle duplicate keys, see 'toSeriesDuplicates'. toSeries :: t -> Series v k a + -- | Construct a 'Series' from some container of key-values pairs, + -- potentially with duplicates.+ -- Each duplicate key @k@ is affixed with an occurence number, starting+ -- with 0. + --+ -- @since 0.1.4.0+ toSeriesDuplicates :: t -> Series v (k, Occurrence) 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@@ -151,11 +159,32 @@ -- 'b' | 0 -- 'd' | 1 --- -- If you need to handle duplicate keys, take a look at `fromListDuplicates`.+ -- If you need to handle duplicate keys, take a look at `toSeriesDuplicates`. toSeries :: [(k, a)] -> Series v k a toSeries = toSeries . MS.fromList {-# INLINABLE toSeries #-} + + -- | Construct a 'Series' from a list of key-values pairs, + -- potentially with duplicates.+ -- Each duplicate key @k@ is affixed with an occurence number, starting+ -- with 0. + --+ -- >>> let xs = toSeriesDuplicates [('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+ --+ -- @since 0.1.4.0+ toSeriesDuplicates :: [(k, a)] -> Series v (k, Occurrence) a+ toSeriesDuplicates = fromListDuplicates+ {-# INLINABLE toSeriesDuplicates #-}+ -- | 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) ]@@ -209,6 +238,9 @@ -- | 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.+--+-- See 'toSeriesDuplicates' for an overloaded version of this function which supports many more+-- sequence types, without having to convert to a list. fromListDuplicates :: (Vector v a, Ord k) => [(k, a)] -> Series v (k, Occurrence) a {-# INLINABLE fromListDuplicates #-} fromListDuplicates = convert . fromVectorDuplicates . Boxed.fromList@@ -224,15 +256,40 @@ toSeries = fromVector {-# INLINABLE toSeries #-} + -- | @since 0.1.4.0+ toSeriesDuplicates = fromVectorDuplicates+ {-# INLINABLE toSeriesDuplicates #-}+ fromSeries = toVector {-# INLINABLE fromSeries #-} +-- | @since 0.1.4.0+instance (Vector v a, Ord k) => IsSeries (Series v k a) v k a where+ toSeries :: Series v k a -> Series v k a+ toSeries = id+ {-# INLINABLE toSeries #-}++ -- | @since 0.1.4.0+ toSeriesDuplicates :: Series v k a -> Series v (k, Occurrence) a+ toSeriesDuplicates = flip mapIndex (,0::Occurrence)+ {-# INLINABLE toSeriesDuplicates #-}++ fromSeries :: Series v k a -> Series v k a+ fromSeries = id+ {-# 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 #-} + -- | @since 0.1.4.0+ toSeriesDuplicates :: U.Vector (k, a) -> Series U.Vector (k, Occurrence) a+ toSeriesDuplicates = fromVectorDuplicates+ {-# INLINABLE toSeriesDuplicates #-}+ fromSeries :: Series U.Vector k a -> U.Vector (k, a) fromSeries = toVector {-# INLINABLE fromSeries #-}@@ -269,6 +326,9 @@ -- | 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.+--+-- See 'toSeriesDuplicates' for an overloaded version of this function which supports many more+-- sequence types, without having to convert to a vector. 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 #-}@@ -306,6 +366,11 @@ } {-# INLINABLE toSeries #-} + -- | @since 0.1.4.0+ toSeriesDuplicates :: Map k a -> Series v (k, Occurrence) a+ toSeriesDuplicates = toSeries . MS.mapKeysMonotonic (,0::Occurrence)+ {-# INLINABLE toSeriesDuplicates #-}+ fromSeries :: Series v k a -> Map k a fromSeries (MkSeries ks vs) = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)@@ -345,6 +410,13 @@ } {-# INLINABLE toSeries #-} + -- | @since 0.1.4.0+ toSeriesDuplicates :: IntMap a -> Series v (Int, Occurrence) a+ toSeriesDuplicates = fromDistinctAscList + . fmap (\(k::Int, v) -> ( (k, 0::Occurrence), v )) + . IntMap.toAscList+ {-# INLINABLE toSeriesDuplicates #-}+ fromSeries :: Series v Int a -> IntMap a fromSeries (MkSeries ks vs) = IntMap.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)@@ -356,15 +428,25 @@ toSeries = toSeries . Foldable.toList {-# INLINABLE toSeries #-} + -- | @since 0.1.4.0+ toSeriesDuplicates :: Seq (k, a) -> Series v (k, Occurrence) a+ toSeriesDuplicates = fromListDuplicates . Foldable.toList+ {-# INLINABLE toSeriesDuplicates #-}+ 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+instance (Ord k, 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 #-}++ -- | @since 0.1.4.0+ toSeriesDuplicates :: Set (k, a) -> Series v (k, Occurrence) a+ toSeriesDuplicates = fromListDuplicates . Set.toList+ {-# INLINABLE toSeriesDuplicates #-} fromSeries :: Series v k a -> Set (k, a) fromSeries = Set.fromDistinctAscList . toList
src/Data/Series/Generic/Internal.hs view
@@ -1,6 +1,6 @@ ----------------------------------------------------------------------------- -- |--- Module : Data.Series.Generic.Internal+-- Module : $header -- Copyright : (c) Laurent P. René de Cotret -- License : MIT -- Maintainer : laurent.decotret@outlook.com
src/Data/Series/Generic/View.hs view
@@ -28,10 +28,10 @@ import Data.Functor ( (<&>) )+import Data.Maybe ( fromJust, isJust ) 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 )
src/Data/Series/Generic/Zip.hs view
@@ -213,6 +213,21 @@ -- | 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.+--+-- >>> 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 :: (Vector v a, Vector v Int, Ord k) => Series v k a -> Series v k a -> Series v k a {-# INLINABLE replace #-}@@ -223,13 +238,43 @@ -- | Infix version of 'replace'+--+-- >>> 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 (|->) :: (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 '|->',+-- | Flipped version of '|->'+--+-- >>> 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 (<-|) :: (Vector v a, Vector v Int, Ord k) => Series v k a -> Series v k a -> Series v k a {-# INLINABLE (<-|) #-}
src/Data/Series/Index/Definition.hs view
@@ -342,7 +342,7 @@ -- | \(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.+-- elements either in the left index, right index, or both. union :: Ord k => Index k -> Index k -> Index k union = (<>) {-# INLINABLE union #-}@@ -350,6 +350,12 @@ -- | \(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.+--+-- Elements of the result come from the first 'Index':+--+-- >>> import Data.Semigroup ( Arg(..) )+-- >>> fromList [ Arg (0::Int) 'a' ] `intersection` fromList [ Arg 0 'b', Arg 1 'c' ]+-- Index [Arg 0 'a'] intersection :: Ord k => Index k -> Index k -> Index k intersection (MkIndex ix) (MkIndex jx) = MkIndex $ ix `Set.intersection` jx {-# INLINABLE intersection #-}@@ -434,10 +440,13 @@ -- | \(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]+-- +-- The precondiction that the function be monotonic __is not checked__. Using 'mapMonotonic'+-- with a non-monotonic function will __not__ result in an exception; subtle inconsistencies will make you+-- question your own sanity. mapMonotonic :: (k -> g) -> Index k -> Index g mapMonotonic f (MkIndex ix) = MkIndex $ Set.mapMonotonic f ix {-# INLINABLE mapMonotonic #-}@@ -513,7 +522,8 @@ -- 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'.+-- because it is not a 'Functor' due to the restriction that+-- members of an 'Index' be a instance of 'Ord'. 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
@@ -2,7 +2,7 @@ ----------------------------------------------------------------------------- -- |--- Module : Data.Series.Generic.Internal+-- Module : $header -- Copyright : (c) Laurent P. René de Cotret -- License : MIT -- Maintainer : laurent.decotret@outlook.com@@ -25,7 +25,7 @@ fromAscVector, fromDistinctAscVector, - -- * Functions with unchecked pre-conditions+ -- * Unsafe functions with pre-conditions mapMonotonic, -- * Unsafe indexing
src/Data/Series/Tutorial.hs view
@@ -1,5 +1,11 @@ {-# OPTIONS_GHC -fno-warn-unused-imports #-}-+-- |+-- Module : $header+-- Copyright : (c) Laurent P. René de Cotret+-- License : MIT+-- Maintainer : laurent.decotret@outlook.com+-- Portability : portable+-- module Data.Series.Tutorial ( -- * Introduction -- $introduction@@ -64,7 +70,6 @@ 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@@ -115,7 +120,7 @@ '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 +not be the same length as the input. 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:@@ -131,9 +136,11 @@ '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'.+Of course, 'Series.fromLazyMap' is also available. +In fact, conversion to/from 'Series' is supported for many container types; +see the 'IsSeries' typeclass and its methods, 'toSeries', 'toSeriesDuplicates', and 'fromSeries'.+ -} {- $index@@ -170,13 +177,13 @@ >>> Index.range (+3) (1 :: Int) 10 Index [1,4,7,10] -An 'Index' is very much like a 'Set', so you can +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.+and more. See the "Data.Series.Index" module for more information. -} @@ -202,8 +209,8 @@ {- $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+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: >>> :{@@ -266,8 +273,8 @@ "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). +but there's no data in our series with the keys @"2010-01-09"@ and @"2010-01-10"@, because it was the week-end, and +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:@@ -400,7 +407,7 @@ >>> Series.fold meanAndStdDev aapl_closing_2021 (140.61256349206354,14.811663837435361) -See 'Control.Foldl' from the @foldl@ package for more information on 'Fold'.+See "Control.Foldl" from the @foldl@ package for more information on 'Fold'. -} {- $grouping@@ -426,7 +433,7 @@ Grouping involves two steps: (1) Grouping keys in some way using 'groupBy';- (2) Aggregating the values in each group using 'aggregateWith' or other variants.+ (2) Aggregating the values in each group using 'aggregateWith' or other variants, such as 'foldWith'. Let's find the highest closing price of each month. First, we need to define our grouping function:@@ -637,8 +644,9 @@ {- $duplicates -If you must build a 'Series' with duplicate keys, you can use the 'Data.Series.fromListDuplicates' or -'Data.Series.fromVectorDuplicates' functions. +If you must build a 'Series' from a container with duplicate keys, you can use the 'Data.Series.toSeriesDuplicates' function,+or its specializations 'Data.Series.fromListDuplicates' and 'Data.Series.fromVectorDuplicates'.+ In the example below, the key @\'d\'@ is repeated three times: >>> Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]@@ -684,7 +692,7 @@ 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".+__and__ unboxed 'Data.Series.Unboxed.Series'? The answer is to use functions from the "Data.Series.Generic" module. For example, we could implement the dot product of two series as: @@ -695,15 +703,17 @@ dot v1 v2 = G.sum $ G.zipWithMatched (*) v1 v2 :} +The above @dot@ function can be used with any type of 'Series'.+ 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:+'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
src/Data/Series/Unboxed.hs view
@@ -1,6 +1,6 @@ ----------------------------------------------------------------------------- -- |--- Module : Data.Series.Unboxed+-- Module : $header -- Copyright : (c) Laurent P. René de Cotret -- License : MIT -- Maintainer : laurent.decotret@outlook.com
test/Test/Data/Series/Generic/Definition.hs view
@@ -7,6 +7,7 @@ import Data.List ( nubBy, sortOn ) import qualified Data.Map.Strict as MS import qualified Data.Map.Lazy as ML+import qualified Data.Sequence as Seq 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 )@@ -32,6 +33,7 @@ , testPropRoundtripConversionWithList , testPropFromListDuplicatesNeverDrops , testPropFromVectorDuplicatesNeverDrops+ , testPropToSeriesDuplicatesNeverDrops , testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder , testPropRoundtripConversionWithVector , testPropVectorVsList@@ -129,6 +131,20 @@ = 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+++testPropToSeriesDuplicatesNeverDrops :: TestTree+testPropToSeriesDuplicatesNeverDrops+ = testProperty "toSeriesDuplicates never drops elements" $ property $ do+ xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)+ -- List+ Series.length (Series.toSeriesDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs+ -- Vector+ let vs = Vector.fromList xs+ Series.length (Series.toSeriesDuplicates vs :: Series Vector (Int, Occurrence) Char) === length vs+ -- Sequence+ let ss = Seq.fromList xs+ Series.length (Series.toSeriesDuplicates ss :: Series Vector (Int, Occurrence) Char) === length ss testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder :: TestTree